Today we'll make an improvement to blog 4 umbraco that shouldn't have any impact on the frontend of the blog but will make it much easier to handle comment spam and moderate comments. We'll move away from storing the comments as documents and store them in a database table instead. We'll fetch them with some xslt extensions (haven't heard of xslt extensions, be sure to check out the umbraco.tv chapter /documentation/videos/for-developers/xslt-extensions/introduction-to-xslt-extensions)
The first step is to create this new table, we'll need to store the post id, the actual comment (name, email, website, comment), the timestamp and we'll also add a spam field.
CREATE TABLE [Comment]( [id] [int] IDENTITY(1,1) NOT NULL, [blogid] [int] NOT NULL, [postid] [int] NOT NULL, [name] [nvarchar](250) NULL, [email] [nvarchar](250) NULL, [website] [nvarchar](250) NULL, [comment] [text] NULL, [spam] [bit] NULL, [created] [datetime] NULL, CONSTRAINT [PK_Comment] PRIMARY KEY CLUSTERED ( [id] ASC )
(this is the sql server create script, we'll also include a vistadb and mysql create script in the installer)
Instead of creating a comment document with the umbraco api we'll update the method to insert a record in the new table using the umbraco datalayer (to make sure this can be executed on sql server, vistadb and mysql)
ISqlHelper SqlHelper = DataLayerHelper.CreateSqlHelper(umbraco.GlobalSettings.DbDSN); SqlHelper.ExecuteNonQuery( @"insert into Comment(blogid,postid,name,email,website,comment,spam,created) values(@blogid,@postid,@name,@email,@website,@comment,@spam,@created)", SqlHelper.CreateParameter("@blogid", blogid), SqlHelper.CreateParameter("@postid", blogpost.Id), SqlHelper.CreateParameter("@name", name), SqlHelper.CreateParameter("@email", email), SqlHelper.CreateParameter("@website", website), SqlHelper.CreateParameter("@comment", comment), SqlHelper.CreateParameter("@spam", false), SqlHelper.CreateParameter("@created", DateTime.Now));
For showing the comments we'll add some new methods to our blog xslt extensions. Basicly I want to select all comments or select the comment for a specific post. So I'll need a GetComments() method and a GetCommentsForBlog(int id) method. Both will return some xml (XPathNodeIterator) that will look like this:
<comments> <comment id="2" postid="1206" created="7/12/2009 16:22:28"> <name>Tim Geyssens</name> <email>myemail@domain.com</email> <website>http://www.umbraco.org</website> <message>This should be stored in the database</message> </comment> </comments>
<comments>
<comment id="2" postid="1206" created="7/12/2009 16:22:28">
<name>Tim Geyssens</name>
<email>myemail@domain.com</email>
<website>http://www.umbraco.org</website>
<message>This should be stored in the database</message>
</comment>
</comments>
We'll also use the umbraco datalayer to execute these queries and we'll add a method that creates the comment xml from a RecordsReader.
public static XPathNodeIterator GetCommentsForPost(int id) { ISqlHelper SqlHelper = DataLayerHelper.CreateSqlHelper(umbraco.GlobalSettings.DbDSN); IRecordsReader rr = SqlHelper.ExecuteReader( "select * from comment where postid = @postid and spam != 1", SqlHelper.CreateParameter("@postid", id)); return CommentsToXml(rr); } private static XPathNodeIterator CommentsToXml(IRecordsReader rr) { XmlDocument xd = new XmlDocument(); XmlNode x = umbraco.xmlHelper.addTextNode(xd, "comments", ""); while (rr.Read()) { XmlNode c = xd.CreateElement("comment"); c.Attributes.Append(umbraco.xmlHelper.addAttribute(xd, "id", rr.GetInt("id").ToString())); c.Attributes.Append(umbraco.xmlHelper.addAttribute(xd, "postid", rr.GetInt("postid").ToString())); c.Attributes.Append(umbraco.xmlHelper.addAttribute(xd, "created", rr.GetDateTime("created").ToString())); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "name", rr.GetString("name"))); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "email", rr.GetString("email"))); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "website", rr.GetString("website"))); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "message", rr.GetString("comment"))); x.AppendChild(c); } xd.AppendChild(x); return xd.CreateNavigator().Select("."); }
public static XPathNodeIterator GetCommentsForPost(int id) {
ISqlHelper SqlHelper = DataLayerHelper.CreateSqlHelper(umbraco.GlobalSettings.DbDSN); IRecordsReader rr = SqlHelper.ExecuteReader( "select * from comment where postid = @postid and spam != 1", SqlHelper.CreateParameter("@postid", id));
return CommentsToXml(rr); }
private static XPathNodeIterator CommentsToXml(IRecordsReader rr) { XmlDocument xd = new XmlDocument();
XmlNode x = umbraco.xmlHelper.addTextNode(xd, "comments", "");
while (rr.Read()) { XmlNode c = xd.CreateElement("comment");
c.Attributes.Append(umbraco.xmlHelper.addAttribute(xd, "id", rr.GetInt("id").ToString())); c.Attributes.Append(umbraco.xmlHelper.addAttribute(xd, "postid", rr.GetInt("postid").ToString())); c.Attributes.Append(umbraco.xmlHelper.addAttribute(xd, "created", rr.GetDateTime("created").ToString()));
c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "name", rr.GetString("name"))); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "email", rr.GetString("email"))); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "website", rr.GetString("website"))); c.AppendChild(umbraco.xmlHelper.addCDataNode(xd, "message", rr.GetString("comment")));
x.AppendChild(c); }
xd.AppendChild(x);
return xd.CreateNavigator().Select("."); }
With the new xslt extensions in place we'll just need to update our xslt to fetch the comments from these.
So this is how the list comments xslt will look after the update:
<h3 id="comments"> <xsl:value-of select="count(BlogLibrary:GetCommentsForPost($currentPage/@id)//comment)"/> comment(s) for “<xsl:value-of select="$currentPage/@nodeName"/>”</h3> <ol class="commentlist"> <xsl:for-each select="BlogLibrary:GetCommentsForPost($currentPage/@id)//comment"> <li class="alt" id="comment-{@id}"> <div class="commentcontent"> <div class="commentgravatar" style="float:right;margin-right: 5px;"> <img width="40px" height="40px" src="{BlogLibrary:getGravatar(./email, 40, '')}" alt="Gravatar of {./name}"/> </div> <cite> <xsl:value-of select="./name"/> </cite> Says: <br /> <small class="commentmetadata"><xsl:value-of select="umbraco.library:LongDate(@created)"/> </small> <xsl:value-of select="umbraco.library:ReplaceLineBreaks(./message)" disable-output-escaping="yes"/> </div> </li> </xsl:for-each> </ol>
<h3 id="comments"> <xsl:value-of select="count(BlogLibrary:GetCommentsForPost($currentPage/@id)//comment)"/> comment(s) for “<xsl:value-of select="$currentPage/@nodeName"/>”</h3>
<ol class="commentlist"> <xsl:for-each select="BlogLibrary:GetCommentsForPost($currentPage/@id)//comment">
<li class="alt" id="comment-{@id}"> <div class="commentcontent"> <div class="commentgravatar" style="float:right;margin-right: 5px;"> <img width="40px" height="40px" src="{BlogLibrary:getGravatar(./email, 40, '')}" alt="Gravatar of {./name}"/> </div> <cite> <xsl:value-of select="./name"/> </cite> Says: <br /> <small class="commentmetadata"><xsl:value-of select="umbraco.library:LongDate(@created)"/> </small> <xsl:value-of select="umbraco.library:ReplaceLineBreaks(./message)" disable-output-escaping="yes"/> </div>
</li> </xsl:for-each> </ol>
That's it, the blog comments are now stored/fetched from a table instead of using documents.
These posts are excellent chaps - Really good stuff for people like me :) I do have a question for this one though Tim, how are you going to enable administration of the comments? Custom usercontrol in the dashboard? Or is this going to be the next blog?
Great example of how to use extensions to use db. Usually I think it's easier to have the comments in Umbraco with out of the box caching, ui for administration, no need to dev xtensions .. but of cause if you have sh*loads of comments then it might be a good thing. I guess that a later post will show why :-)
@Lee @Jesper, yup we'll look at that in a future chapter
Hi Tim, Nice post, one thought on this is what about sites that are on multiple load balanced servers? I thought the idea of storing all the content in the Umbraco XML file meant that it made sites easily scalable by adding new servers that just required the Umbraco.config XML file updated on each server, but by using XSLT extensions that access the DB directly you are then reliant on the DB for serving content? Just a thought and for most I am sure it won't be a consideration. Chris
Hi Tim, Like the others said: Thanks for an interesting post. One question popped up though: Did you consider storing the IP address of the poster along with the comment? Cheers, Jonas
Very good series of posts!! One minor thing: Shouldn't you use **ntext** instead of text for the comments fields to support unicode? Nico
I love these posts!! One minor optimization... Save the result of BlogLibrary:GetCommentsForPost($currentPage/@id)//comment) to a variable. That way you only hit the database once rather than twice. cheers, doug.
@Nico, @Douglas both true, will be like that in the final package.
I've often wondered what would be the right approach to managing such content. Sometimes creating documents when you just want to manage records just feels wrong to me. I'm hoping that reading such practical and intelligent posts will soak through into my own practical know-how, eventually :) Thanks chaps - keep it up! Barney
Maybe I'm missing something, but by storing the comments in a seperate table, how would you delete unwanted comments or mark them as spam manually? There's no umbraco interface to deal with these comments any more, so you'd need to reach into your database?
@Sebastiaan no worries, we'll add comment moderation in a future chapter
so about comment moderation. how can i set this up?
RE: andrew smith http://our.umbraco.org/projects/blog-4-umbraco/using-blog-4-umbraco/7079-Comment-Moderation,-How
sfdfsd
Could someone please explain how I am able to setup the comment moderation with the script? Thanks!
Hi, could somebody be so nice and explain,where to save those scripts? THX
Wow, its very nice script and also useful, and please let me know where to save the scripts? if so it will be very nice
One minor thing: Shouldn't you use **ntext** instead of text for the comments fields to support unicode?
Nice post, one thought on this is what about sites that are on multiple load balanced servers? I thought the idea of storing all the content in the Umbraco XML file meant that it made sites easily scalable by adding new servers that just required the Umbraco.config XML file updated on each server, but by using XSLT extensions that access the DB directly you are then reliant on the DB for serving content? Just a thought and for most I am sure it won't be a consideration
Great post! Thanks for this!
Hello, Ok can you tel if 4.7.1 already have all this or we should do it? Because as far as I can see database already have table. Need to know about rest of the staff. So far comment does not get stored or appear on the blog at all... Thanks
You comment is being submitted... please wait...
Your comment has been posted on umbraco in reply to: Blog 4 umbraco 2.0.5 – Comments
Something horrible happened. and your comment could not be posted. Please ensure that all fields have correct values, and that yourEmail address is valid.