diff options
Diffstat (limited to 'week-7/Final4/blog')
| -rw-r--r-- | week-7/Final4/blog/README.md | 14 | ||||
| -rw-r--r-- | week-7/Final4/blog/app.js | 27 | ||||
| -rw-r--r-- | week-7/Final4/blog/package.json | 22 | ||||
| -rw-r--r-- | week-7/Final4/blog/posts.js | 133 | ||||
| -rw-r--r-- | week-7/Final4/blog/routes/content.js | 214 | ||||
| -rw-r--r-- | week-7/Final4/blog/routes/error.js | 9 | ||||
| -rw-r--r-- | week-7/Final4/blog/routes/index.js | 47 | ||||
| -rw-r--r-- | week-7/Final4/blog/routes/session.js | 170 | ||||
| -rw-r--r-- | week-7/Final4/blog/sessions.js | 65 | ||||
| -rw-r--r-- | week-7/Final4/blog/users.js | 75 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/blog_template.html | 40 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/entry_template.html | 58 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/error_template.html | 12 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/login.html | 47 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/newpost_template.html | 29 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/signup.html | 75 | ||||
| -rw-r--r-- | week-7/Final4/blog/views/welcome.html | 28 |
17 files changed, 1065 insertions, 0 deletions
diff --git a/week-7/Final4/blog/README.md b/week-7/Final4/blog/README.md new file mode 100644 index 0000000..863910e --- /dev/null +++ b/week-7/Final4/blog/README.md @@ -0,0 +1,14 @@ +Blog project for M101JS + +./app.js - entry point +./package.json - npm package description +./routes/ - Application routes +./posts.js - Posts Data Access Helper +./sessions.js - Sessions Data Access Helper +./users.js - Users Data Access Helper +./views/ - html templates + +Getting started + +npm install +node app.js diff --git a/week-7/Final4/blog/app.js b/week-7/Final4/blog/app.js new file mode 100644 index 0000000..f773876 --- /dev/null +++ b/week-7/Final4/blog/app.js @@ -0,0 +1,27 @@ +var express = require('express') + , app = express() // Web framework to handle routing requests + , cons = require('consolidate') // Templating library adapter for Express + , MongoClient = require('mongodb').MongoClient // Driver for connecting to MongoDB + , routes = require('./routes'); // Routes for our application + +MongoClient.connect('mongodb://localhost:27017/blog', function(err, db) { + "use strict"; + if(err) throw err; + + // Register our templating engine + app.engine('html', cons.swig); + app.set('view engine', 'html'); + app.set('views', __dirname + '/views'); + + // Express middleware to populate 'req.cookies' so we can access cookies + app.use(express.cookieParser()); + + // Express middleware to populate 'req.body' so we can access POST variables + app.use(express.bodyParser()); + + // Application routes + routes(app, db); + + app.listen(3000); + console.log('Express server listening on port 3000'); +}); diff --git a/week-7/Final4/blog/package.json b/week-7/Final4/blog/package.json new file mode 100644 index 0000000..093690d --- /dev/null +++ b/week-7/Final4/blog/package.json @@ -0,0 +1,22 @@ +{ + "name": "blog", + "version": "0.0.0", + "description": "Blog Project for M101JS", + "main": "app.js", + "dependencies": { + "bcrypt-nodejs": "~0.0.3", + "consolidate": "~0.9.1", + "express": ">=3.x", + "mongodb": "~1.3.9", + "swig": "~0.14.0", + "validator": "~1.1.3" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": "", + "author": "Shaun Verch <shaun.verch@10gen.com>", + "license": "BSD", + "private": true +} diff --git a/week-7/Final4/blog/posts.js b/week-7/Final4/blog/posts.js new file mode 100644 index 0000000..0769fcf --- /dev/null +++ b/week-7/Final4/blog/posts.js @@ -0,0 +1,133 @@ +/* The PostsDAO must be constructed with a connected database object */ +function PostsDAO(db) { + "use strict"; + + /* If this constructor is called without the "new" operator, "this" points + * to the global object. Log a warning and call it correctly. */ + if (false === (this instanceof PostsDAO)) { + console.log('Warning: PostsDAO constructor called without "new" operator'); + return new PostsDAO(db); + } + + var posts = db.collection("posts"); + + this.insertEntry = function (title, body, tags, author, callback) { + "use strict"; + console.log("inserting blog entry" + title + body); + + // fix up the permalink to not include whitespace + var permalink = title.replace( /\s/g, '_' ); + permalink = permalink.replace( /\W/g, '' ); + + // Build a new post + var post = {"title": title, + "author": author, + "body": body, + "permalink":permalink, + "tags": tags, + "comments": [], + "date": new Date()} + + // now insert the post + posts.insert(post, function (err, result) { + "use strict"; + + if (err) return callback(err, null); + + console.log("Inserted new post"); + callback(err, permalink); + }); + } + + this.getPosts = function(num, callback) { + "use strict"; + + posts.find().sort('date', -1).limit(num).toArray(function(err, items) { + "use strict"; + + if (err) return callback(err, null); + + console.log("Found " + items.length + " posts"); + + callback(err, items); + }); + } + + this.getPostsByTag = function(tag, num, callback) { + "use strict"; + + posts.find({ tags : tag }).sort('date', -1).limit(num).toArray(function(err, items) { + "use strict"; + + if (err) return callback(err, null); + + console.log("Found " + items.length + " posts"); + + callback(err, items); + }); + } + + this.getPostByPermalink = function(permalink, callback) { + "use strict"; + posts.findOne({'permalink': permalink}, function(err, post) { + "use strict"; + + if (err) return callback(err, null); + + // XXX: Look here for final exam to see where we store "num_likes" + + // fix up likes values. set to zero if data is not present + if (typeof post.comments === 'undefined') { + post.comments = []; + } + + // Each comment document in a post should have a "num_likes" entry, so we have to + // iterate all the comments in the post to make sure that is the case + for (var i = 0; i < post.comments.length; i++) { + if (typeof post.comments[i].num_likes === 'undefined') { + post.comments[i].num_likes = 0; + } + post.comments[i].comment_ordinal = i; + } + callback(err, post); + }); + } + + this.addComment = function(permalink, name, email, body, callback) { + "use strict"; + + var comment = {'author': name, 'body': body} + + if (email != "") { + comment['email'] = email + } + + posts.update({'permalink': permalink}, {'$push': {'comments': comment}}, function(err, numModified) { + "use strict"; + + if (err) return callback(err, null); + + callback(err, numModified); + }); + } + + this.incrementLikes = function(permalink, comment_ordinal, callback) { + "use strict"; + + // The "comment_ordinal" argument specifies which comment in the post we are looking at + // Here is an example of how to build a selector with the 'comment_ordinal' variable + // We have to do it this way because a literal object with variables in field names such as: + // { 'comments.' + comment_ordinal + '.author' : 'Frank' } is illegal Javascript. + var selector_example = {}; + var comment_ordinal_example = 0; + selector_example['comments.' + comment_ordinal_example + '.author'] = 'Frank'; + // Now selector_example = { 'comments.0.author' : 'Frank' } which is a selector for the + // string 'Frank' in the 'author' field of the first element of the 'comments' array (which + // is zero indexed). + + // TODO: Final exam question - Increment the number of likes + callback(Error("incrementLikes NYI"), null); + } +} + +module.exports.PostsDAO = PostsDAO; diff --git a/week-7/Final4/blog/routes/content.js b/week-7/Final4/blog/routes/content.js new file mode 100644 index 0000000..6e7491c --- /dev/null +++ b/week-7/Final4/blog/routes/content.js @@ -0,0 +1,214 @@ +var PostsDAO = require('../posts').PostsDAO + , sanitize = require('validator').sanitize; // Helper to sanitize form input + +/* The ContentHandler must be constructed with a connected db */ +function ContentHandler (db) { + "use strict"; + + var posts = new PostsDAO(db); + + this.displayMainPage = function(req, res, next) { + "use strict"; + + posts.getPosts(10, function(err, results) { + "use strict"; + + if (err) return next(err); + + return res.render('blog_template', { + title: 'blog homepage', + username: req.username, + myposts: results + }); + }); + } + + this.displayMainPageByTag = function(req, res, next) { + "use strict"; + + var tag = req.params.tag; + + posts.getPostsByTag(tag, 10, function(err, results) { + "use strict"; + + if (err) return next(err); + + return res.render('blog_template', { + title: 'blog homepage', + username: req.username, + myposts: results + }); + }); + } + + this.displayPostByPermalink = function(req, res, next) { + "use strict"; + + var permalink = req.params.permalink; + + posts.getPostByPermalink(permalink, function(err, post) { + "use strict"; + + if (err) return next(err); + + if (!post) return res.redirect("/post_not_found"); + + // init comment form fields for additional comment + var comment = {'name': req.username, 'body': "", 'email': ""} + + return res.render('entry_template', { + title: 'blog post', + username: req.username, + post: post, + comment: comment, + errors: "" + }); + }); + } + + this.handleNewComment = function(req, res, next) { + "use strict"; + var name = req.body.commentName; + var email = req.body.commentEmail; + var body = req.body.commentBody; + var permalink = req.body.permalink; + + // Override the comment with our actual user name if found + if (req.username) { + name = req.username; + } + + if (!name || !body) { + // user did not fill in enough information + + posts.getPostByPermalink(permalink, function(err, post) { + "use strict"; + + if (err) return next(err); + + if (!post) return res.redirect("/post_not_found"); + + // init comment form fields for additional comment + var comment = {'name': name, 'body': "", 'email': ""} + + var errors = "Post must contain your name and an actual comment." + return res.render('entry_template', { + title: 'blog post', + username: req.username, + post: post, + comment: comment, + errors: errors + }); + }); + + return; + } + + // even if there is no logged in user, we can still post a comment + posts.addComment(permalink, name, email, body, function(err, updated) { + "use strict"; + + if (err) return next(err); + + if (updated == 0) return res.redirect("/post_not_found"); + + return res.redirect("/post/" + permalink); + }); + } + + this.displayPostNotFound = function(req, res, next) { + "use strict"; + return res.send('Sorry, post not found', 404); + } + + this.displayNewPostPage = function(req, res, next) { + "use strict"; + + if (!req.username) return res.redirect("/login"); + + return res.render('newpost_template', { + subject: "", + body: "", + errors: "", + tags: "", + username: req.username + }); + } + + function extract_tags(tags) { + "use strict"; + + var cleaned = []; + + var tags_array = tags.split(','); + + for (var i = 0; i < tags_array.length; i++) { + if ((cleaned.indexOf(tags_array[i]) == -1) && tags_array[i] != "") { + cleaned.push(tags_array[i].replace(/\s/g,'')); + } + } + + return cleaned + } + + this.handleNewPost = function(req, res, next) { + "use strict"; + + var title = req.body.subject + var post = req.body.body + var tags = req.body.tags + + if (!req.username) return res.redirect("/signup"); + + if (!title || !post) { + var errors = "Post must contain a title and blog entry"; + return res.render("newpost_template", {subject:title, username:req.username, body:post, tags:tags, errors:errors}); + } + + var tags_array = extract_tags(tags) + + // looks like a good entry, insert it escaped + var escaped_post = sanitize(post).escape(); + + // substitute some <br> for the paragraph breaks + var formatted_post = escaped_post.replace(/\r?\n/g,'<br>'); + + posts.insertEntry(title, formatted_post, tags_array, req.username, function(err, permalink) { + "use strict"; + + if (err) return next(err); + + // now redirect to the blog permalink + return res.redirect("/post/" + permalink) + }); + } + + this.handleLike = function(req, res, next) { + "use strict"; + + var permalink = req.body.permalink; + permalink = sanitize(permalink).escape(); + + var comment_ordinal = req.body.comment_ordinal; + + posts.getPostByPermalink(permalink, function(err, post) { + "use strict"; + + if (err) return next(err); + + if (!post) return res.redirect("/post_not_found"); + + // it all looks good. increment the ordinal + posts.incrementLikes(permalink, comment_ordinal, function(err, post) { + "use strict"; + + if (err) return next(err); + + // now redirect to the blog permalink + return res.redirect("/post/" + permalink) + }); + }); + } +} + +module.exports = ContentHandler; diff --git a/week-7/Final4/blog/routes/error.js b/week-7/Final4/blog/routes/error.js new file mode 100644 index 0000000..f50f2b6 --- /dev/null +++ b/week-7/Final4/blog/routes/error.js @@ -0,0 +1,9 @@ +// Error handling middleware + +exports.errorHandler = function(err, req, res, next) { + "use strict"; + console.error(err.message); + console.error(err.stack); + res.status(500); + res.render('error_template', { error: err }); +} diff --git a/week-7/Final4/blog/routes/index.js b/week-7/Final4/blog/routes/index.js new file mode 100644 index 0000000..4586e7b --- /dev/null +++ b/week-7/Final4/blog/routes/index.js @@ -0,0 +1,47 @@ +var SessionHandler = require('./session') + , ContentHandler = require('./content') + , ErrorHandler = require('./error').errorHandler; + +module.exports = exports = function(app, db) { + + var sessionHandler = new SessionHandler(db); + var contentHandler = new ContentHandler(db); + + // Middleware to see if a user is logged in + app.use(sessionHandler.isLoggedInMiddleware); + + // The main page of the blog + app.get('/', contentHandler.displayMainPage); + + // The main page of the blog, filtered by tag + app.get('/tag/:tag', contentHandler.displayMainPageByTag); + + // A single post, which can be commented on + app.get("/post/:permalink", contentHandler.displayPostByPermalink); + app.post('/newcomment', contentHandler.handleNewComment); + app.get("/post_not_found", contentHandler.displayPostNotFound); + + // Displays the form allowing a user to add a new post. Only works for logged in users + app.get('/newpost', contentHandler.displayNewPostPage); + app.post('/newpost', contentHandler.handleNewPost); + + // Used to process a like on a blog post + app.post('/like', contentHandler.handleLike); + + // Login form + app.get('/login', sessionHandler.displayLoginPage); + app.post('/login', sessionHandler.handleLoginRequest); + + // Logout page + app.get('/logout', sessionHandler.displayLogoutPage); + + // Welcome page + app.get("/welcome", sessionHandler.displayWelcomePage); + + // Signup form + app.get('/signup', sessionHandler.displaySignupPage); + app.post('/signup', sessionHandler.handleSignup); + + // Error handling middleware + app.use(ErrorHandler); +} diff --git a/week-7/Final4/blog/routes/session.js b/week-7/Final4/blog/routes/session.js new file mode 100644 index 0000000..73fe6d9 --- /dev/null +++ b/week-7/Final4/blog/routes/session.js @@ -0,0 +1,170 @@ +var UsersDAO = require('../users').UsersDAO + , SessionsDAO = require('../sessions').SessionsDAO; + +/* The SessionHandler must be constructed with a connected db */ +function SessionHandler (db) { + "use strict"; + + var users = new UsersDAO(db); + var sessions = new SessionsDAO(db); + + this.isLoggedInMiddleware = function(req, res, next) { + var session_id = req.cookies.session; + sessions.getUsername(session_id, function(err, username) { + "use strict"; + + if (!err && username) { + req.username = username; + } + return next(); + }); + } + + this.displayLoginPage = function(req, res, next) { + "use strict"; + return res.render("login", {username:"", password:"", login_error:""}) + } + + this.handleLoginRequest = function(req, res, next) { + "use strict"; + + var username = req.body.username; + var password = req.body.password; + + console.log("user submitted username: " + username + " pass: " + password); + + users.validateLogin(username, password, function(err, user) { + "use strict"; + + if (err) { + if (err.no_such_user) { + return res.render("login", {username:username, password:"", login_error:"No such user"}); + } + else if (err.invalid_password) { + return res.render("login", {username:username, password:"", login_error:"Invalid password"}); + } + else { + // Some other kind of error + return next(err); + } + } + + sessions.startSession(user['_id'], function(err, session_id) { + "use strict"; + + if (err) return next(err); + + res.cookie('session', session_id); + return res.redirect('/welcome'); + }); + }); + } + + this.displayLogoutPage = function(req, res, next) { + "use strict"; + + var session_id = req.cookies.session; + sessions.endSession(session_id, function (err) { + "use strict"; + + // Even if the user wasn't logged in, redirect to home + res.cookie('session', ''); + return res.redirect('/'); + }); + } + + this.displaySignupPage = function(req, res, next) { + "use strict"; + res.render("signup", {username:"", password:"", + password_error:"", + email:"", username_error:"", email_error:"", + verify_error :""}); + } + + function validateSignup(username, password, verify, email, errors) { + "use strict"; + var USER_RE = /^[a-zA-Z0-9_-]{3,20}$/; + var PASS_RE = /^.{3,20}$/; + var EMAIL_RE = /^[\S]+@[\S]+\.[\S]+$/; + + errors['username_error'] = ""; + errors['password_error'] = ""; + errors['verify_error'] = ""; + errors['email_error'] = ""; + + if (!USER_RE.test(username)) { + errors['username_error'] = "invalid username. try just letters and numbers"; + return false; + } + if (!PASS_RE.test(password)) { + errors['password_error'] = "invalid password."; + return false; + } + if (password != verify) { + errors['verify_error'] = "password must match"; + return false; + } + if (email != "") { + if (!EMAIL_RE.test(email)) { + errors['email_error'] = "invalid email address"; + return false; + } + } + return true; + } + + this.handleSignup = function(req, res, next) { + "use strict"; + + var email = req.body.email + var username = req.body.username + var password = req.body.password + var verify = req.body.verify + + // set these up in case we have an error case + var errors = {'username': username, 'email': email} + if (validateSignup(username, password, verify, email, errors)) { + users.addUser(username, password, email, function(err, user) { + "use strict"; + + if (err) { + // this was a duplicate + if (err.code == '11000') { + errors['username_error'] = "Username already in use. Please choose another"; + return res.render("signup", errors); + } + // this was a different error + else { + return next(err); + } + } + + sessions.startSession(user['_id'], function(err, session_id) { + "use strict"; + + if (err) return next(err); + + res.cookie('session', session_id); + return res.redirect('/welcome'); + }); + }); + } + else { + console.log("user did not validate"); + return res.render("signup", errors); + } + } + + this.displayWelcomePage = function(req, res, next) { + "use strict"; + + if (!req.username) { + console.log("welcome: can't identify user...redirecting to signup"); + return res.redirect("/signup"); + } + + return res.render("welcome", {'username':req.username}) + } +} + +module.exports = SessionHandler; diff --git a/week-7/Final4/blog/sessions.js b/week-7/Final4/blog/sessions.js new file mode 100644 index 0000000..3af169f --- /dev/null +++ b/week-7/Final4/blog/sessions.js @@ -0,0 +1,65 @@ +var crypto = require('crypto'); + +/* The SessionsDAO must be constructed with a connected database object */ +function SessionsDAO(db) { + "use strict"; + + /* If this constructor is called without the "new" operator, "this" points + * to the global object. Log a warning and call it correctly. */ + if (false === (this instanceof SessionsDAO)) { + console.log('Warning: SessionsDAO constructor called without "new" operator'); + return new SessionsDAO(db); + } + + var sessions = db.collection("sessions"); + + this.startSession = function(username, callback) { + "use strict"; + + // Generate session id + var current_date = (new Date()).valueOf().toString(); + var random = Math.random().toString(); + var session_id = crypto.createHash('sha1').update(current_date + random).digest('hex'); + + // Create session document + var session = {'username': username, '_id': session_id} + + // Insert session document + sessions.insert(session, function (err, result) { + "use strict"; + callback(err, session_id); + }); + } + + this.endSession = function(session_id, callback) { + "use strict"; + // Remove session document + sessions.remove({ '_id' : session_id }, function (err, numRemoved) { + "use strict"; + callback(err); + }); + } + this.getUsername = function(session_id, callback) { + "use strict"; + + if (!session_id) { + callback(Error("Session not set"), null); + return; + } + + sessions.findOne({ '_id' : session_id }, function(err, session) { + "use strict"; + + if (err) return callback(err, null); + + if (!session) { + callback(new Error("Session: " + session + " does not exist"), null); + return; + } + + callback(null, session.username); + }); + } +} + +module.exports.SessionsDAO = SessionsDAO; diff --git a/week-7/Final4/blog/users.js b/week-7/Final4/blog/users.js new file mode 100644 index 0000000..5960dbd --- /dev/null +++ b/week-7/Final4/blog/users.js @@ -0,0 +1,75 @@ +var bcrypt = require('bcrypt-nodejs'); + +/* The UsersDAO must be constructed with a connected database object */ +function UsersDAO(db) { + "use strict"; + + /* If this constructor is called without the "new" operator, "this" points + * to the global object. Log a warning and call it correctly. */ + if (false === (this instanceof UsersDAO)) { + console.log('Warning: UsersDAO constructor called without "new" operator'); + return new UsersDAO(db); + } + + var users = db.collection("users"); + + this.addUser = function(username, password, email, callback) { + "use strict"; + + // Generate password hash + var salt = bcrypt.genSaltSync(); + var password_hash = bcrypt.hashSync(password, salt); + + // Create user document + var user = {'_id': username, 'password': password_hash}; + + // Add email if set + if (email != "") { + user['email'] = email; + } + + users.insert(user, function (err, result) { + "use strict"; + + if (!err) { + console.log("Inserted new user"); + return callback(null, result[0]); + } + + return callback(err, null); + }); + } + + this.validateLogin = function(username, password, callback) { + "use strict"; + + // Callback to pass to MongoDB that validates a user document + function validateUserDoc(err, user) { + "use strict"; + + if (err) return callback(err, null); + + if (user) { + if (bcrypt.compareSync(password, user.password)) { + callback(null, user); + } + else { + var invalid_password_error = new Error("Invalid password"); + // Set an extra field so we can distinguish this from a db error + invalid_password_error.invalid_password = true; + callback(invalid_password_error, null); + } + } + else { + var no_such_user_error = new Error("User: " + user + " does not exist"); + // Set an extra field so we can distinguish this from a db error + no_such_user_error.no_such_user = true; + callback(no_such_user_error, null); + } + } + + users.findOne({ '_id' : username }, validateUserDoc); + } +} + +module.exports.UsersDAO = UsersDAO; diff --git a/week-7/Final4/blog/views/blog_template.html b/week-7/Final4/blog/views/blog_template.html new file mode 100644 index 0000000..6af803d --- /dev/null +++ b/week-7/Final4/blog/views/blog_template.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> +<head> +<title>My Blog</title> +</head> +<body> + +{% if username %} +Welcome {{username}} <a href="/logout">Logout</a> | <a href="/newpost">New Post</a><p> +{% else %} +You are not logged in! <a href="/login">Login</a> | <a href="/signup">Sign Up</a><p> +{% endif %} + +<h1>My Blog</h1> + +{% for post in myposts %} +<h2><a href="/post/{{post['permalink']}}">{{post['title']}}</a></h2> +Posted {{post['date']}} <i>By {{post['author']}}</i><br> +Comments: +<a href="/post/{{post['permalink']}}">{{post['comments']|length}}</a> +<hr> +{% autoescape false %} +{{post['body']}} +{% endautoescape %} +<p> +<p> +<em>Filed Under</em>: +{% for tag in post.tags %} + {% if loop.first %} + <a href="/tag/{{tag}}">{{tag}}</a> + {% else %} + , <a href="/tag/{{tag}}">{{tag}}</a> + {% endif %} +{% endfor %} +{% endfor %} +<p> +</body> +</html> + + diff --git a/week-7/Final4/blog/views/entry_template.html b/week-7/Final4/blog/views/entry_template.html new file mode 100644 index 0000000..27a0789 --- /dev/null +++ b/week-7/Final4/blog/views/entry_template.html @@ -0,0 +1,58 @@ +<!doctype HTML> +<html +<head> +<title> +Blog Post +</title> +</head> +<body> +{% if username %} +Welcome {{username}} <a href="/logout">Logout</a> | <a href="/newpost">New Post</a><p> +{% else %} +You are not logged in! <a href="/login">Login</a> | <a href="/signup">Sign Up</a><p> +{% endif %} +<a href="/">Blog Home</a><br><br> + +<h2>{{post['title']}}</h2> +Posted {{post['date']}}<i> By {{post['author']}}</i><br> +<hr> +{% autoescape false %} +{{post['body']}} +{% endautoescape %} +<p> +<em>Filed Under</em>: +{% for tag in post.tags %} + {% if loop.first %} + <a href="/tag/{{tag}}">{{tag}}</a> + {% else %} + , <a href="/tag/{{tag}}">{{tag}}</a> + {% endif %} +{% endfor %} +<p> +Comments: +<ul> +{% for comment in post.comments %} +<form action="/like" method="POST"> +<input type="hidden" name="permalink", value="{{post['permalink']}}"> +<input type="hidden" name="comment_ordinal", value="{{loop.index0}}"> +Author: {{comment['author']}}<br> +Likes: {{comment['num_likes']}} <input type="submit" value="Like"></form><br> +{{comment['body']}}<br> +<hr> +{% endfor %} +<h3>Add a comment</h3> +<form action="/newcomment" method="POST"> +<input type="hidden" name="permalink", value="{{post['permalink']}}"> +<h4>{{errors}}</h4> +<b>Name</b> (required)<br> +<input type="text" name="commentName" size="60" value="{{comment['name']}}"><br> +<b>Email</b> (optional)<br> +<input type="text" name="commentEmail" size="60" value="{{comment['email']}}"><br> +<b>Comment</b><br> +<textarea name="commentBody" cols="60" rows="10">{{comment['body']}}</textarea><br> +<input type="submit" value="Submit"> +</ul> +</body> +</html> + + diff --git a/week-7/Final4/blog/views/error_template.html b/week-7/Final4/blog/views/error_template.html new file mode 100644 index 0000000..c37339f --- /dev/null +++ b/week-7/Final4/blog/views/error_template.html @@ -0,0 +1,12 @@ +<!doctype HTML> +<html> +<head> +<title>Internal Error</title> +</head> +<body> + +Oops..<br> +{{error}} +</body> +</html> + diff --git a/week-7/Final4/blog/views/login.html b/week-7/Final4/blog/views/login.html new file mode 100644 index 0000000..4041d5c --- /dev/null +++ b/week-7/Final4/blog/views/login.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> + +<html> + <head> + <title>Login</title> + <style type="text/css"> + .label {text-align: right} + .error {color: red} + </style> + + </head> + + <body> + <h2>Login</h2> + <form method="post"> + <table> + <tr> + <td class="label"> + Username + </td> + <td> + <input type="text" name="username" value="{{username}}"> + </td> + <td class="error"> + </td> + </tr> + + <tr> + <td class="label"> + Password + </td> + <td> + <input type="password" name="password" value=""> + </td> + <td class="error"> + {{login_error}} + + </td> + </tr> + + </table> + + <input type="submit"> + </form> + </body> + +</html> diff --git a/week-7/Final4/blog/views/newpost_template.html b/week-7/Final4/blog/views/newpost_template.html new file mode 100644 index 0000000..bd67f9c --- /dev/null +++ b/week-7/Final4/blog/views/newpost_template.html @@ -0,0 +1,29 @@ +<!doctype HTML> +<html> +<head> +<title>Create a new post</title> +</head> +<body> + +{% if username %} +Welcome {{username}} <a href="/logout">Logout</a><p> +{% else %} +You are not logged in! <a href="/login">Login</a> | <a href="/signup">Sign Up</a><p> +{% endif %} +<a href="/">Blog Home</a><br><br> + +<form action="/newpost" method="POST"> +{{errors}} +<h2>Title</h2> +<input type="text" name="subject" size="120" value="{{subject}}"><br> +<h2>Blog Entry<h2> +<textarea name="body" cols="120" rows="20">{{body}}</textarea><br> +<h2>Tags</h2> +Comma separated, please<br> +<input type="text" name="tags" size="120" value="{{tags}}"><br> +<p> +<input type="submit" value="Submit"> + +</body> +</html> + diff --git a/week-7/Final4/blog/views/signup.html b/week-7/Final4/blog/views/signup.html new file mode 100644 index 0000000..76bee7f --- /dev/null +++ b/week-7/Final4/blog/views/signup.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> + +<html> + <head> + <title>Sign Up</title> + <style type="text/css"> + .label {text-align: right} + .error {color: red} + </style> + + </head> + + <body> + Already a user? <a href="/login">Login</a><p> + <h2>Signup</h2> + <form method="post"> + <table> + <tr> + <td class="label"> + Username + </td> + <td> + <input type="text" name="username" value="{{username}}"> + </td> + <td class="error"> + {{username_error}} + + </td> + </tr> + + <tr> + <td class="label"> + Password + </td> + <td> + <input type="password" name="password" value=""> + </td> + <td class="error"> + {{password_error}} + + </td> + </tr> + + <tr> + <td class="label"> + Verify Password + </td> + <td> + <input type="password" name="verify" value=""> + </td> + <td class="error"> + {{verify_error}} + + </td> + </tr> + + <tr> + <td class="label"> + Email (optional) + </td> + <td> + <input type="text" name="email" value="{{email}}"> + </td> + <td class="error"> + {{email_error}} + + </td> + </tr> + </table> + + <input type="submit"> + </form> + </body> + +</html> diff --git a/week-7/Final4/blog/views/welcome.html b/week-7/Final4/blog/views/welcome.html new file mode 100644 index 0000000..64f5929 --- /dev/null +++ b/week-7/Final4/blog/views/welcome.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> + +<html> + <head> + <title>Welcome</title> + <style type="text/css"> + .label {text-align: right} + .error {color: red} + </style> + + </head> + + <body> + Welcome {{username}} +<p> +<ul> +<li><a href="/">Goto Blog Home</a></li> +<li> +<a href="/logout">Logout</a> +</li> +<li> +<a href="/newpost">Create a New Post</a> +</li> + + + </body> + +</html> |
