├── .gitignore
├── README.md
├── app.yaml
├── blog.py
├── handlers
├── __init__.py
├── blog_handler.py
├── comment_handler.py
├── like_handler.py
├── main_handler.py
├── post_handler.py
└── user_handler.py
├── index.yaml
├── models
├── __init__.py
├── comment.py
├── like.py
├── post.py
└── user.py
├── static
└── main.css
├── templates
├── base.html
├── comment.html
├── editcomment.html
├── editpost.html
├── front.html
├── login.html
├── newcomment.html
├── newpost.html
├── permalink.html
├── post.html
├── signup.html
└── welcome.html
└── utility.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### JetBrains template
3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
5 |
6 | # User-specific stuff:
7 | .idea/**/workspace.xml
8 | .idea/**/tasks.xml
9 |
10 | # Sensitive or high-churn files:
11 | .idea/**/dataSources/
12 | .idea/**/dataSources.ids
13 | .idea/**/dataSources.xml
14 | .idea/**/dataSources.local.xml
15 | .idea/**/sqlDataSources.xml
16 | .idea/**/dynamic.xml
17 | .idea/**/uiDesigner.xml
18 |
19 | # Gradle:
20 | .idea/**/gradle.xml
21 | .idea/**/libraries
22 |
23 | # Mongo Explorer plugin:
24 | .idea/**/mongoSettings.xml
25 |
26 | ## File-based project format:
27 | *.iws
28 |
29 | ## Plugin-specific files:
30 |
31 | # IntelliJ
32 | /out/
33 |
34 | # mpeltonen/sbt-idea plugin
35 | .idea_modules/
36 |
37 | # JIRA plugin
38 | atlassian-ide-plugin.xml
39 |
40 | # Crashlytics plugin (for Android Studio and IntelliJ)
41 | com_crashlytics_export_strings.xml
42 | crashlytics.properties
43 | crashlytics-build.properties
44 | fabric.properties
45 | ### macOS template
46 | *.DS_Store
47 | .AppleDouble
48 | .LSOverride
49 |
50 | # Icon must end with two \r
51 | Icon
52 |
53 |
54 | # Thumbnails
55 | ._*
56 |
57 | # Files that might appear in the root of a volume
58 | .DocumentRevisions-V100
59 | .fseventsd
60 | .Spotlight-V100
61 | .TemporaryItems
62 | .Trashes
63 | .VolumeIcon.icns
64 | .com.apple.timemachine.donotpresent
65 |
66 | # Directories potentially created on remote AFP share
67 | .AppleDB
68 | .AppleDesktop
69 | Network Trash Folder
70 | Temporary Items
71 | .apdisk
72 | ### Example user template template
73 | ### Example user template
74 |
75 | # IntelliJ project files
76 | .idea
77 | *.iml
78 | out
79 | gen### Python template
80 | # Byte-compiled / optimized / DLL files
81 | __pycache__/
82 | *.py[cod]
83 | *$py.class
84 |
85 | # C extensions
86 | *.so
87 |
88 | # Distribution / packaging
89 | .Python
90 | env/
91 | build/
92 | develop-eggs/
93 | dist/
94 | downloads/
95 | eggs/
96 | .eggs/
97 | lib/
98 | lib64/
99 | parts/
100 | sdist/
101 | var/
102 | wheels/
103 | *.egg-info/
104 | .installed.cfg
105 | *.egg
106 |
107 | # PyInstaller
108 | # Usually these files are written by a python script from a template
109 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
110 | *.manifest
111 | *.spec
112 |
113 | # Installer logs
114 | pip-log.txt
115 | pip-delete-this-directory.txt
116 |
117 | # Unit test / coverage reports
118 | htmlcov/
119 | .tox/
120 | .coverage
121 | .coverage.*
122 | .cache
123 | nosetests.xml
124 | coverage.xml
125 | *,cover
126 | .hypothesis/
127 |
128 | # Translations
129 | *.mo
130 | *.pot
131 |
132 | # Django stuff:
133 | *.log
134 | local_settings.py
135 |
136 | # Flask stuff:
137 | instance/
138 | .webassets-cache
139 |
140 | # Scrapy stuff:
141 | .scrapy
142 |
143 | # Sphinx documentation
144 | docs/_build/
145 |
146 | # PyBuilder
147 | target/
148 |
149 | # Jupyter Notebook
150 | .ipynb_checkpoints
151 |
152 | # pyenv
153 | .python-version
154 |
155 | # celery beat schedule file
156 | celerybeat-schedule
157 |
158 | # dotenv
159 | .env
160 |
161 | # virtualenv
162 | .venv
163 | venv/
164 | ENV/
165 |
166 | # Spyder project settings
167 | .spyderproject
168 |
169 | # Rope project settings
170 | .ropeproject
171 |
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # multi-user-blog
2 |
3 | fsnd: a multi user blog(along the lines of Medium) where users can sign in and post blog posts as well as 'Like' and 'Comment' on other posts made on the blog.
4 |
5 | ## Setup
6 |
7 | - [install google app engine](https://drive.google.com/open?id=0Byu3UemwRffDc21qd3duLW9LMm8)
8 | - create app in [Developer Console](https://console.developers.google.com/)
9 | - run locally `dev_appserver.py .`
10 | - browse locally via [http://localhost:8080](http://localhost:8080)
11 | - set project `gcloud config set project PROJECT_ID`
12 | - deploy app `gcloud app deploy`
13 | - browse app `gcloud app browse`
14 |
15 | ## Features
16 |
17 | - user - signup, login, logout
18 | - post - new, edit, delete, display
19 | - comment - new, delete, edit
20 | - like - add, delete
21 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: python27
2 | api_version: 1
3 | threadsafe: true
4 |
5 | handlers:
6 | - url: /static
7 | static_dir: static
8 |
9 | - url: /.*
10 | script: blog.app
11 |
12 | libraries:
13 | - name: webapp2
14 | version: latest
15 | - name: jinja2
16 | version: latest
17 |
--------------------------------------------------------------------------------
/blog.py:
--------------------------------------------------------------------------------
1 | import webapp2
2 |
3 | from handlers import *
4 |
5 | app = webapp2.WSGIApplication([('/', MainPage),
6 | ('/blog/?', BlogFront),
7 | ('/blog/([0-9]+)', PostPage),
8 | ('/blog/([0-9]+)/like', LikeBtn),
9 | ('/blog/([0-9]+)/comment/newcomment', NewComment),
10 | ('/blog/([0-9]+)/comment/([0-9]+)/delete', DeleteComment),
11 | ('/blog/([0-9]+)/comment/([0-9]+)/edit', EditComment),
12 | ('/blog/newpost', NewPost),
13 | ('/blog/([0-9]+)/edit', EditPost),
14 | ('/blog/([0-9]+)/delete', DeletePost),
15 | ('/signup', Signup),
16 | ('/welcome', Welcome),
17 | ('/login', Login),
18 | ('/logout', Logout),
19 | ],
20 | debug=True)
21 |
--------------------------------------------------------------------------------
/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | from main_handler import MainPage
2 | from blog_handler import BlogHandler
3 | from user_handler import Signup, Login, Logout, Welcome
4 | from post_handler import BlogFront, PostPage, NewPost, EditPost, DeletePost
5 | from comment_handler import NewComment, EditComment, DeleteComment
6 | from like_handler import LikeBtn
7 |
--------------------------------------------------------------------------------
/handlers/blog_handler.py:
--------------------------------------------------------------------------------
1 | import webapp2
2 |
3 | from models import User
4 | from utility import render_str, make_secure_val, check_secure_val
5 |
6 |
7 | class BlogHandler(webapp2.RequestHandler):
8 | def write(self, *a, **kw):
9 | self.response.out.write(*a, **kw)
10 |
11 | def render_str(self, template, **params):
12 | return render_str(template, **params)
13 |
14 | def render(self, template, **kw):
15 | kw['user'] = self.user
16 | self.write(self.render_str(template, **kw))
17 |
18 | def set_secure_cookie(self, name, val):
19 | cookie_val = make_secure_val(val)
20 | self.response.headers.add_header('Set-Cookie', '%s=%s; Path=/' % (name, cookie_val))
21 |
22 | def read_secure_cookie(self, name):
23 | cookie_val = self.request.cookies.get(name)
24 | return cookie_val and check_secure_val(cookie_val)
25 |
26 | def initialize(self, *a, **kw):
27 | webapp2.RequestHandler.initialize(self, *a, **kw)
28 | uid = self.read_secure_cookie('user_id')
29 | self.user = uid and User.by_id(int(uid))
30 |
31 | def login(self, user):
32 | self.set_secure_cookie('user_id', str(user.key().id()))
33 |
34 | def logout(self):
35 | self.response.headers.add_header('Set-Cookie', 'user_id=; Path=/')
36 |
--------------------------------------------------------------------------------
/handlers/comment_handler.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from blog_handler import BlogHandler
4 | from models import Comment
5 | from utility import user_logged_in, comment_exists, user_owns_comment, post_exists
6 |
7 |
8 | class NewComment(BlogHandler):
9 | @user_logged_in
10 | @post_exists
11 | def get(self, post):
12 | self.render('newcomment.html')
13 |
14 | @user_logged_in
15 | @post_exists
16 | def post(self, post):
17 | content = self.request.get('comment')
18 | if content:
19 | comment = Comment.create(content, self.user, post)
20 | comment.put()
21 | time.sleep(0.1)
22 | self.redirect('/blog/' + str(post.key().id()))
23 | else:
24 | error = "Complete content of comment, please!"
25 | self.render('newcomment.html', comment=content, error=error)
26 |
27 |
28 | class EditComment(BlogHandler):
29 | @user_logged_in
30 | @comment_exists
31 | @user_owns_comment
32 | def get(self, post, comment):
33 | self.render('editcomment.html', comment=comment.content, post_id=post.key().id())
34 |
35 | @user_logged_in
36 | @comment_exists
37 | @user_owns_comment
38 | def post(self, post, comment):
39 | content = self.request.get('comment')
40 | if content:
41 | comment.content = content
42 | comment.put()
43 | time.sleep(0.1)
44 | self.redirect('/blog/' + str(post.key().id()))
45 | else:
46 | error = "Complete content of comment, please!"
47 | self.render('editcomment.html', comment=content, post_id=post.key().id(), error=error)
48 |
49 |
50 | class DeleteComment(BlogHandler):
51 | @user_logged_in
52 | @comment_exists
53 | @user_owns_comment
54 | def post(self, post, comment):
55 | comment.delete()
56 | time.sleep(0.1)
57 | self.redirect('/blog/' + str(post.key().id()))
58 |
--------------------------------------------------------------------------------
/handlers/like_handler.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from blog_handler import BlogHandler
4 | from models import Like
5 | from utility import user_logged_in, post_exists
6 |
7 |
8 | class LikeBtn(BlogHandler):
9 | @user_logged_in
10 | @post_exists
11 | def post(self, post):
12 | like_btn = self.request.get('like-btn')
13 | like = self.user.user_likes.filter("post =", post).get()
14 | if like_btn == 'like' and not like:
15 | like = Like.create(self.user, post)
16 | like.put()
17 | elif like_btn == 'unlike' and like:
18 | like.delete()
19 | time.sleep(0.1)
20 | self.redirect('/blog/' + str(post.key().id()))
21 |
--------------------------------------------------------------------------------
/handlers/main_handler.py:
--------------------------------------------------------------------------------
1 | import webapp2
2 |
3 |
4 | class MainPage(webapp2.RequestHandler):
5 | def get(self):
6 | self.redirect('/blog')
7 |
--------------------------------------------------------------------------------
/handlers/post_handler.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from blog_handler import BlogHandler
4 | from models import Post
5 | from utility import user_logged_in, post_exists, user_owns_post
6 |
7 |
8 | class BlogFront(BlogHandler):
9 | def get(self):
10 | posts = Post.by_limit(10)
11 | self.render("front.html", posts=posts)
12 |
13 |
14 | class PostPage(BlogHandler):
15 | @post_exists
16 | def get(self, post):
17 | liked = self.user and self.user.user_likes.filter("post =", post).count() > 0
18 | comments = post.post_comments.order('-created')
19 | self.render("permalink.html", post=post, user=self.user, liked=liked, comments=comments)
20 |
21 |
22 | class NewPost(BlogHandler):
23 | @user_logged_in
24 | def get(self):
25 | self.render("newpost.html")
26 |
27 | @user_logged_in
28 | def post(self):
29 | subject = self.request.get('subject')
30 | content = self.request.get('content')
31 | if subject and content:
32 | post = Post.create(subject, content, self.user)
33 | post.put()
34 | self.redirect('/blog/' + str(post.key().id()))
35 | else:
36 | error = "Complete subject or content, please!"
37 | self.render("newpost.html", subject=subject, content=content, error=error, user=self.user)
38 |
39 |
40 | class EditPost(BlogHandler):
41 | @user_logged_in
42 | @post_exists
43 | @user_owns_post
44 | def get(self, post):
45 | self.render("editpost.html", subject=post.subject, content=post.content, post_id=post.key().id())
46 |
47 | @user_logged_in
48 | @post_exists
49 | @user_owns_post
50 | def post(self, post):
51 | subject = self.request.get('subject')
52 | content = self.request.get('content')
53 | if subject and content:
54 | post.subject = subject
55 | post.content = content
56 | post.put()
57 | self.redirect('/blog/' + str(post.key().id()))
58 | else:
59 | error = "Complete subject or content, please!"
60 | self.render("editpost.html", subject=subject, content=content, post_id=post.key().id(), error=error)
61 |
62 |
63 | class DeletePost(BlogHandler):
64 | @user_logged_in
65 | @post_exists
66 | @user_owns_post
67 | def post(self, post):
68 | post.delete()
69 | time.sleep(0.1)
70 | self.redirect('/blog')
71 |
--------------------------------------------------------------------------------
/handlers/user_handler.py:
--------------------------------------------------------------------------------
1 | from blog_handler import BlogHandler
2 | from models import User
3 | from utility import valid_email, valid_password, valid_username
4 |
5 |
6 | class Signup(BlogHandler):
7 | def get(self):
8 | self.render("signup.html")
9 |
10 | def post(self):
11 | self.username = self.request.get("username")
12 | self.password = self.request.get("password")
13 | self.verify = self.request.get("verify")
14 | self.email = self.request.get("email")
15 |
16 | params = dict(username=self.username, email=self.email)
17 | passed = True
18 | if not valid_username(self.username):
19 | params["error_username"] = "Invalid username!"
20 | passed = False
21 | else:
22 | user = User.by_name(self.username)
23 | if user:
24 | params["error_username"] = "User already exists!"
25 | passed = False
26 | if not valid_password(self.password):
27 | params["error_password"] = "Invalid password!"
28 | passed = False
29 | elif self.password != self.verify:
30 | params["error_verify"] = "Two passwords not same!"
31 | passed = False
32 | if self.email and not valid_email(self.email):
33 | params["error_email"] = "Invalid email!"
34 | passed = False
35 |
36 | if passed:
37 | user = User.register(self.username, self.password, self.email)
38 | user.put()
39 | self.login(user)
40 | self.redirect('/welcome')
41 | else:
42 | self.render("signup.html", **params)
43 |
44 |
45 | class Welcome(BlogHandler):
46 | def get(self):
47 | if self.user:
48 | username = self.user.name
49 | self.render("welcome.html", username=username)
50 | else:
51 | self.redirect('/signup')
52 |
53 |
54 | class Login(BlogHandler):
55 | def get(self):
56 | self.render("login.html")
57 |
58 | def post(self):
59 | username = self.request.get('username')
60 | password = self.request.get('password')
61 | user = User.login(username, password)
62 | if user:
63 | self.login(user)
64 | self.redirect('/welcome')
65 | else:
66 | self.render("login.html", error="Username and password don't match!")
67 |
68 |
69 | class Logout(BlogHandler):
70 | def get(self):
71 | self.logout()
72 | self.redirect('/blog')
73 |
--------------------------------------------------------------------------------
/index.yaml:
--------------------------------------------------------------------------------
1 | indexes:
2 |
3 | # AUTOGENERATED
4 |
5 | # This index.yaml is automatically updated whenever the dev_appserver
6 | # detects that a new type of query is run. If you want to manage the
7 | # index.yaml file manually, remove the above marker line (the line
8 | # saying "# AUTOGENERATED"). If you want to manage some indexes
9 | # manually, move them above the marker line. The index.yaml file is
10 | # automatically uploaded to the admin console when you next deploy
11 | # your application using appcfg.py.
12 |
13 | - kind: Comment
14 | properties:
15 | - name: post
16 | - name: created
17 | direction: desc
18 |
--------------------------------------------------------------------------------
/models/__init__.py:
--------------------------------------------------------------------------------
1 | from user import User
2 | from comment import Comment
3 | from post import Post
4 | from like import Like
--------------------------------------------------------------------------------
/models/comment.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import db
2 | from user import User
3 | from post import Post
4 | from utility import comment_key, render_str
5 |
6 |
7 | class Comment(db.Model):
8 | user = db.ReferenceProperty(User, required=True, collection_name='user_comments')
9 | post = db.ReferenceProperty(Post, required=True, collection_name='post_comments')
10 | content = db.TextProperty(required=True)
11 | created = db.DateTimeProperty(auto_now_add=True)
12 | last_modified = db.DateTimeProperty(auto_now=True)
13 |
14 | def render(self, post, user):
15 | self._render_text = self.content.replace('\n', '
')
16 | print self._render_text
17 | return render_str('comment.html', comment=self, post=post, user=user)
18 |
19 | @classmethod
20 | def create(cls, content, user, post):
21 | return Comment(content=content, user=user, post=post, parent=comment_key())
22 |
23 | @classmethod
24 | def by_id(cls, comment_id):
25 | return Comment.get_by_id(comment_id, parent=comment_key())
26 |
--------------------------------------------------------------------------------
/models/like.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import db
2 | from user import User
3 | from post import Post
4 | from utility import like_key
5 |
6 | class Like(db.Model):
7 | user = db.ReferenceProperty(User, required=True, collection_name='user_likes')
8 | post = db.ReferenceProperty(Post, required=True, collection_name='post_likes')
9 |
10 | @classmethod
11 | def create(cls, user, post):
12 | return Like(user=user, post=post, parent=like_key())
13 |
--------------------------------------------------------------------------------
/models/post.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import db
2 | from user import User
3 | from utility import render_str, blog_key
4 |
5 |
6 | class Post(db.Model):
7 | subject = db.StringProperty(required=True)
8 | content = db.TextProperty(required=True)
9 | created = db.DateTimeProperty(auto_now_add=True)
10 | last_modified = db.DateTimeProperty(auto_now=True)
11 | user = db.ReferenceProperty(User, required=True, collection_name='user_posts')
12 |
13 | def render(self):
14 | self._render_text = self.content.replace('\n', '
')
15 | return render_str("post.html", p=self)
16 |
17 | @classmethod
18 | def by_limit(cls, limit):
19 | return db.GqlQuery("select * from Post order by created desc limit {}".format(limit))
20 |
21 | @classmethod
22 | def by_id(cls, post_id):
23 | return Post.get_by_id(post_id, parent=blog_key())
24 |
25 | @classmethod
26 | def create(cls, subject, content, user):
27 | return Post(subject=subject, content=content, user=user, parent=blog_key())
28 |
--------------------------------------------------------------------------------
/models/user.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import db
2 | from utility import make_pw_hash, valid_pw, users_key
3 |
4 | class User(db.Model):
5 | name = db.StringProperty(required=True)
6 | pw_hash = db.StringProperty(required=True)
7 | email = db.StringProperty()
8 |
9 | @classmethod
10 | def by_id(cls, uid):
11 | return User.get_by_id(uid, parent=users_key())
12 |
13 | @classmethod
14 | def by_name(cls, name):
15 | return User.all().filter('name =', name).get()
16 |
17 | @classmethod
18 | def register(cls, name, pw, email=None):
19 | pw_hash = make_pw_hash(name, pw)
20 | return User(name=name, pw_hash=pw_hash, email=email, parent=users_key())
21 |
22 | @classmethod
23 | def login(cls, name, pw):
24 | u = cls.by_name(name)
25 | if u and valid_pw(name, pw, u.pw_hash):
26 | return u
27 | return None
--------------------------------------------------------------------------------
/static/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Helvetica, Arial, sans-serif;
3 | font-size: 14px;
4 | width: 800px;
5 | margin: 0 auto;
6 | padding: 10px;
7 | color: #333;
8 | }
9 |
10 | h2 {
11 | font-size: 24px;
12 | font-weight: bold;
13 | margin-bottom: 20px;
14 | }
15 |
16 | .error {
17 | color: red;
18 | }
19 |
20 | label {
21 | display: block;
22 | font-size: 20px;
23 | }
24 |
25 | label + label {
26 | margin-top: 20px;
27 | }
28 |
29 | input[type=text] {
30 | width: 500px;
31 | font-size: 20px;
32 | padding: 2px;
33 | font-family: monospace;
34 | }
35 |
36 | textarea {
37 | width: 500px;
38 | height: 400px;
39 | font-size: 17px;
40 | font-family: monospace;
41 | }
42 |
43 | input[type=submit] {
44 | font-size: 24px;
45 | }
46 |
47 | .main-title {
48 | display: block;
49 | color: #222;
50 | font-size: 40px;
51 | font-weight: bold;
52 | text-align: center;
53 | margin-bottom: 30px;
54 | text-decoration: none;
55 | }
56 |
57 | .post + .post {
58 | margin-top: 15px;
59 | }
60 |
61 | .post-heading {
62 | position: relative;
63 | border-bottom: 3px solid #666;
64 | }
65 |
66 | .post-title {
67 | font-size: 24px;
68 | font-weight: bold;
69 | }
70 |
71 | .post-author {
72 | position: absolute;
73 | right: 200px;
74 | bottom: 0;
75 | color: #718f99;
76 | }
77 |
78 | .post-date {
79 | position: absolute;
80 | right: 0;
81 | bottom: 0;
82 | color: #999;
83 | }
84 |
85 | .post-content {
86 | margin-top: 5px;
87 | }
88 |
89 | .label {
90 | text-align: right
91 | }
92 |
93 | .login-area {
94 | font-size: 13px;
95 | color: gray;
96 | position: absolute;
97 | top: 10px;
98 | right: 10px;
99 | }
100 |
101 | .login-link {
102 | color: gray;
103 | text-decoration: none;
104 | }
105 |
106 | .login-link:visited {
107 | color: gray;
108 | }
109 |
110 | .login-link:hover {
111 | text-decoration: underline;
112 | }
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |