Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added validation for duplicate titles. Restored display of validation…

…s on blogedit page
  • Loading branch information...
commit 03e0c350d5198945d4a8916c9d06924f2de4b978 1 parent 200233b
@hectorcorrea authored
View
2  data/blog.31.html
@@ -1 +1 @@
-Over the last few months I've been playing with Node.js, Express.js, and CoffeeScript...
+this is the content
View
18 data/blogs.json
@@ -1,5 +1,14 @@
{ "nextId": 32, "blogs":[
{
+ "id": 31,
+ "title": "beyond hello world",
+ "createdOn": "2012-09-08T14:01:12.964Z",
+ "updatedOn": "2012-09-08T19:18:42.281Z",
+ "postedOn": "2012-09-08T13:59:19.000Z",
+ "url": "beyond-hello-world",
+ "summary": "this is the summary"
+ },
+ {
"id": 1,
"title": "A Decaf Introduction to CoffeeScript",
"createdOn": "2012-09-02T00:48:28.234Z",
@@ -259,14 +268,5 @@
"postedOn": "2006-05-08T06:38:00.000Z",
"url": "book-review-lean-software-development",
"summary": "A review of the book Lean Software Development by Mary and Tom Poppendieck."
- },
- {
- "id": 31,
- "title": "Beyond Hello World with Node.js, Express.js, and CoffeeScript",
- "summary": "A tour of how I wrote an end-to-end web application with Node.js, Express.js, and CoffeeScript. ",
- "postedOn": "2012-09-08T13:59:19.000Z",
- "createdOn": "2012-09-08T14:01:12.964Z",
- "updatedOn": "2012-09-08T14:01:12.964Z",
- "url": "beyond-hello-world-with-node-js-express-js-and-coffeescript"
}
]}
View
6 data_test/blogs.json
@@ -2,9 +2,9 @@
{
"id": 1,
"title": "updated title 2",
- "createdOn": "2012-09-08T14:07:23.703Z",
- "updatedOn": "2012-09-08T14:07:23.721Z",
- "postedOn": "2012-09-08T14:07:23.721Z",
+ "createdOn": "2012-09-08T19:08:59.488Z",
+ "updatedOn": "2012-09-08T19:08:59.509Z",
+ "postedOn": "2012-09-08T19:08:59.509Z",
"url": "updated-title-2",
"summary": "s1"
}
View
25 logs/2012_09_08.txt
@@ -39,3 +39,28 @@ Error: ENOENT, open './logs/2012-09-08.txt'
2012-09-08 11:45:45.069 INFO: blogRoutes:viewOne dotwiki-2-0
2012-09-08 11:45:45.263 INFO: blogRoutes:viewRecent
2012-09-08 11:45:45.286 INFO: blogRoutes:viewRecent
+2012-09-08 15:09:09.431 INFO: Express server listening on http://localhost:3000 in development mode
+2012-09-08 15:09:09.050 INFO: blogRoutes:viewOne 8-traits-of-great-metro-style-apps
+2012-09-08 15:09:09.096 INFO: blogRoutes:viewRecent
+2012-09-08 15:09:09.645 INFO: blogRoutes:viewOne beyond-hello-world-with-node-js-express-js-and-coffeescript
+2012-09-08 15:09:09.112 INFO: blogRoutes:edit beyond-hello-world-with-node-js-express-js-and-coffeescript
+2012-09-08 15:10:10.926 INFO: Express server listening on http://localhost:3000 in development mode
+2012-09-08 15:10:10.415 INFO: blogRoutes:edit beyond-hello-world-with-node-js-express-js-and-coffeescript
+2012-09-08 15:10:10.503 INFO: blogRoutes:save 31
+2012-09-08 15:10:10.515 INFO: Saved, redirecting to /blog/beyond-hello-world-with-node-js-express-js-and-coffeescript
+2012-09-08 15:10:10.532 INFO: blogRoutes:viewOne beyond-hello-world-with-node-js-express-js-and-coffeescript
+2012-09-08 15:10:10.091 INFO: blogRoutes:edit beyond-hello-world-with-node-js-express-js-and-coffeescript
+2012-09-08 15:11:11.404 INFO: blogRoutes:save 31
+2012-09-08 15:12:12.030 INFO: blogRoutes:save 31
+2012-09-08 15:14:14.335 INFO: blogRoutes:save 31
+2012-09-08 15:14:14.425 INFO: Saved, redirecting to /blog/beyond-hello-world
+2012-09-08 15:14:14.462 INFO: blogRoutes:viewOne beyond-hello-world
+2012-09-08 15:14:14.595 INFO: blogRoutes:edit beyond-hello-world
+2012-09-08 15:14:14.306 INFO: blogRoutes:save 31
+2012-09-08 15:16:16.586 INFO: blogRoutes:save 31
+2012-09-08 15:17:17.256 INFO: blogRoutes:save 31
+2012-09-08 15:18:18.186 INFO: blogRoutes:save 31
+2012-09-08 15:18:18.274 INFO: blogRoutes:save 31
+2012-09-08 15:18:18.299 INFO: Saved, redirecting to /blog/beyond-hello-world
+2012-09-08 15:18:18.356 INFO: blogRoutes:viewOne beyond-hello-world
+2012-09-08 15:18:18.342 INFO: blogRoutes:viewRecent
View
104 models/topicModel.coffee
@@ -36,13 +36,16 @@ class TopicModel
return date instanceof Date && isFinite(date)
- _validate: (topic) ->
+ _validateTopic: (topic, callback) =>
+
valid = true
errors = {
emptyTitle: false
+ duplicateTitle: false
emptySummary: false
emptyContent: false
}
+
if topic?.meta?.title? is false or topic.meta.title is ""
valid = false
errors.emptyTitle = true
@@ -52,7 +55,44 @@ class TopicModel
if topic?.content? is false or topic.content is ""
valid = false
errors.emptyContent = true
- return if valid then null else errors
+
+ if errors.emptyTitle
+ # No need to validate for a duplicate title
+ callback null, errors
+ else
+ @_isDuplicateTitle topic, (err, isDuplicate) =>
+ if err
+ callback err
+ else
+ if isDuplicate
+ valid = false
+ errors.duplicateTitle = true
+ callback null, if valid then null else errors
+
+
+ _isDuplicateTitle: (topic, callback) =>
+
+ valid = true
+ url = @_getUrlFromTitle(topic.meta.title)
+ isNewTopic = isNaN(topic.meta.id)
+
+ if isNewTopic
+ # When validating a new topic, finding any
+ # topic with the same title is enough to
+ # consider it a duplicate
+ @data.findMetaByUrl url, (err, topicFound) =>
+ isDuplicate = topicFound isnt null
+ callback null, isDuplicate
+ else
+ # When updating an existing topic we consider
+ # a duplicate only if the topic found is NOT
+ # the same as the one we are trying to save.
+ @data.findMetaByUrl url, (err, topicFound) =>
+ if topicFound is null
+ callback null, false
+ else
+ isDuplicate = topicFound.id isnt topic.meta.id
+ callback null, isDuplicate
getAll: (callback) =>
@@ -84,7 +124,7 @@ class TopicModel
getRssList: (callback) =>
- @data.getAll (err, topics) ->
+ @data.getAll (err, topics) =>
if err
callback err
else
@@ -96,33 +136,37 @@ class TopicModel
# notice that we need an id
save: (topic, callback) =>
- # Load the topic from the DB
+ # Load the topic from the DB...
@data.findMeta topic.meta.id, (err, meta) =>
if err
callback err
else
- # Merge the topic that we received with the one
+ # ...merge the topic that we received with the one
# on the DB
topic.meta.createdOn = meta.createdOn
topic.meta.updatedOn = new Date()
topic.meta.postedOn = if @_isValidDate(topic.meta.postedOn) then topic.meta.postedOn else new Date()
topic.meta.url = @_getUrlFromTitle(topic.meta.title)
- # Is the topic valid?
- topic.errors = @_validate(topic)
- isTopicValid = topic.errors is null
- if isTopicValid
- # Update the meta data...
- @data.updateMeta topic.meta.id, topic.meta, (err, updatedMeta) =>
- if err
- callback err
- else
- # ... and the content
- @data.updateContent updatedMeta, topic.content, callback
- else
- # topic has {meta: X, content: Y, errors: Z}
- callback null, topic
+ # ...make sure the topic is valid
+ @_validateTopic topic, (err, validationErrors) =>
+ if err
+ callback err
+ else
+ isTopicValid = validationErrors is null
+ if isTopicValid
+ # ...update the meta data
+ @data.updateMeta topic.meta.id, topic.meta, (err, updatedMeta) =>
+ if err
+ callback err
+ else
+ # ... and the content
+ @data.updateContent updatedMeta, topic.content, callback
+ else
+ # topic has {meta: X, content: Y, errors: Z}
+ topic.errors = validationErrors
+ callback null, topic
# topic must be in the form
@@ -135,15 +179,19 @@ class TopicModel
topic.meta.postedOn = if @_isValidDate(topic.meta.postedOn) then topic.meta.postedOn else new Date()
topic.meta.url = @_getUrlFromTitle(topic.meta.title)
- # Is the topic valid?
- topic.errors = @_validate(topic)
- isTopicValid = topic.errors is null
- if isTopicValid
- # Add topic to the database (meta+content)
- @data.addNew topic.meta, topic.content, callback
- else
- # topic has {meta: X, content: Y, errors: Z}
- callback null, topic
+ # ...make sure the topic is valid
+ @_validateTopic topic, (err, validationErrors) =>
+ if err
+ callback err
+ else
+ isTopicValid = validationErrors is null
+ if isTopicValid
+ # Add topic to the database (meta+content)
+ @data.addNew topic.meta, topic.content, callback
+ else
+ # topic has {meta: X, content: Y, errors: Z}
+ topic.errors = validationErrors
+ callback null, topic
exports.TopicModel = TopicModel
View
57 models/topicModelTest.coffee
@@ -31,12 +31,11 @@ testGetUrlFromTitle = ->
test.passIf model._getUrlFromTitle("hello-World.aspx") is "hello-world-aspx", "dots test"
test.passIf model._getUrlFromTitle("hello-c#-World.aspx") is "hello-csharp-world-aspx", "c# test"
test.passIf model._getUrlFromTitle("this is #4") is "this-is-4", "pound (#) test"
- testValidate()
+ testValidateGoodTopic()
-testValidate = ->
- test = new TestUtil("topicModelTest.testValidate", verbose)
-
+testValidateGoodTopic = ->
+ test = new TestUtil("topicModelTest.testValidateGoodTopic", verbose)
goodTopic = {
meta: {
id: 1
@@ -46,21 +45,29 @@ testValidate = ->
content: "hello world content"
}
- test.passIf model._validate(goodTopic) is null, "good topic"
+ model._validateTopic goodTopic, (err, validationErrors) ->
+ test.passIf err is null and validationErrors is null, "good topic"
+ testValidateEmptyTopic()
+
+testValidateEmptyTopic = ->
+ test = new TestUtil("topicModelTest.testValidateEmptyTopic", verbose)
emptyTopic = {}
- test.passIf model._validate(emptyTopic) isnt null, "empty topic"
+ model._validateTopic emptyTopic, (err, validationErrors) ->
+ test.passIf validationErrors isnt null, "empty topic"
+ testValidateEmptyTitle()
- emptyTitleTopic = { meta: { id: 1, summary: "s"}, content: "c" }
- errors = model._validate(emptyTitleTopic)
- test.passIf errors.emptyTitle is true, "empty title"
- testSaveNewGoodTopic()
+testValidateEmptyTitle = ->
+ test = new TestUtil("topicModelTest.testValidate", verbose)
+ emptyTitleTopic = { meta: { id: 1, summary: "s"}, content: "c" }
+ model._validateTopic emptyTitleTopic, (err, validationErrors) ->
+ test.passIf validationErrors.emptyTitle is true, "empty title"
+ testSaveNewGoodTopic()
testSaveNewGoodTopic = ->
test = new TestUtil("topicModelTest.testSaveNewGoodTopic", verbose)
-
newTopic = {
meta: {
title: "new test topic",
@@ -78,6 +85,34 @@ testSaveNewGoodTopic = ->
test.fail "error retrieving new record id: #{data.meta.id} #{err}"
else
test.pass ""
+ testIsDuplicateTitleNew()
+
+
+testIsDuplicateTitleNew = ->
+ test = new TestUtil("topicModelTest.testIsDuplicateTitleNew", verbose)
+
+ newTopic = {
+ meta: {
+ title: "new test topic",
+ summary: "new summary for test topic 2"
+ }
+ content: "new content for test topic 2"
+ }
+
+ model._isDuplicateTitle newTopic, (err, isDuplicate) ->
+ test.passIf isDuplicate, ""
+ testIsDuplicateTitleExisting()
+
+
+testIsDuplicateTitleExisting = ->
+ test = new TestUtil("topicModelTest.testIsDuplicateTitleExisting", verbose)
+
+ model.getOne 1, (err, topic) ->
+ if err
+ test.fail err
+ else
+ model._isDuplicateTitle topic, (err, isDuplicate) ->
+ test.failIf isDuplicate, ""
testSaveNewBadTopic()
View
9 routes/blogRoutes.coffee
@@ -148,8 +148,7 @@ edit = (req, res) ->
Logger.error err
renderNotFound res, err
else
- res.render 'blogEdit', null
- #res.render 'blogEdit', viewModelForTopic(topic, req.app)
+ res.render 'blogEdit', viewModelForTopic(topic, req.app)
save = (req, res) ->
@@ -170,10 +169,11 @@ save = (req, res) ->
model.save topic, (err, savedTopic) ->
if err
# Unexpected error, send user to blogs main page
- Logger.warn "Error while saving: #{err}"
+ Logger.error "Error while saving: #{err}"
res.redirect '/blog'
else if typeof savedTopic.errors isnt 'undefined'
# Validation error, send user to edit this topic
+ Logger.info "Validation errors detected"
res.render 'blogEdit', viewModelForTopic(savedTopic, req.app)
else
Logger.info "Saved, redirecting to /blog/#{savedTopic.meta.url}"
@@ -198,11 +198,12 @@ saveNew = (req, res) ->
model.saveNew topic, (err, savedTopic) ->
if err
# Unexpected error, send user to blogs main page
- Logger.warn "Error while saving #{err}"
+ Logger.error "Error while saving #{err}"
res.redirect '/blog'
else if typeof savedTopic.errors isnt 'undefined'
# Validation error, send user to edit this topic
# savedTopic is in the form {meta: X, content: Y, errors: Z}
+ Logger.info "Validation errors detected"
res.render 'blogEdit', viewModelForTopic(savedTopic, req.app)
else
Logger.info "New topic added, redirecting to /blog/#{savedTopic.meta.url}"
View
14 views/blogedit.ejs
@@ -15,20 +15,28 @@
<p>
<label>Title</label>
<input type="text" name="title" value="{{= topic.meta.title }}" placeholder="Blog title" />
- {{ if(false) { }}
- <small class="error">Enter a title for the blog post</small>
+ {{ if(topic.errors && topic.errors.emptyTitle) { }}
+ <small class="error">Enter a title for the blog post.</small>
+ {{ } }}
+ {{ if(topic.errors && topic.errors.duplicateTitle) { }}
+ <small class="error">There is a already another topic with the exact same title.
+ Please enter a different title for this one.</small>
{{ } }}
</p>
<p>
<label>Summary (raw HTML)<label>
<textarea name="summary", rows="4", cols="60" placeholder="Enter blog post summary">{{= topic.meta.summary }}</textarea>
+ {{ if(topic.errors && topic.errors.emptySummary) { }}
+ <small class="error">Enter a short summary for the blog post.</small>
+ {{ } }}
+
</p>
<p>
<label>Content (raw HTML)<label>
<textarea name="content", rows="20", cols="60" placeholder="Enter blog post content">{{- topic.content }}</textarea>
- {{ if(false) { }}
+ {{ if(topic.errors && topic.errors.emptyContent) { }}
<small class="error">Enter the content of the blog post</small>
{{ } }}
</p>
Please sign in to comment.
Something went wrong with that request. Please try again.