├── README ├── app.yaml ├── config ├── css └── style.css ├── index.yaml ├── main.py └── templates ├── base.html ├── error.html ├── form.html ├── index.html ├── rss.xml └── upload.html /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realbasic/python-blog-system/4ae0ae25cbe41a711fad50bccbfd469550693081/README -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: python-blog-system 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /css/(.*\.css) 8 | static_files: css/\1 9 | upload: css/(.*\.css) 10 | 11 | - url: .* 12 | script: main.py 13 | 14 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | [default] 2 | step=10 3 | 4 | [log] 5 | google_analytics=UA-XXXXXXXX-X 6 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | div.entry{margin-bottom: 10px;} 2 | div.entryHeader{width:400px;} 3 | div.entry{width:400px;} 4 | div.entryDate{ 5 | font-size:small; 6 | text-align:right; 7 | margin-bottom:2px; 8 | } 9 | div.entryFooter{ 10 | margin-top:5px; 11 | font-size:small; 12 | } 13 | div.entryDate{ 14 | font-size: x-small; 15 | } 16 | div.body{ 17 | font-size:small; 18 | margin-left:5px; 19 | } 20 | h1{ 21 | padding:5px 0 5px 5px; 22 | border-left:#003399 8px solid; 23 | border-bottom:#003399 1px solid; 24 | font-weight: bold; 25 | color:#333; 26 | width:400px; 27 | margin-bottom:0; 28 | } 29 | h2{ 30 | padding:5px 0 5px 5px; 31 | border-left:#009900 8px solid; 32 | border-bottom:#009900 1px solid; 33 | font-size: small; 34 | font-weight: bold; 35 | color:#333; 36 | margin-bottom:0; 37 | width:390px; 38 | } 39 | h3{ 40 | padding:5px 0 5px 5px; 41 | border-left:#99cc00 3px solid; 42 | border-bottom:#009900 1px solid; 43 | font-size: smaller; 44 | color:#333; 45 | margin-bottom:0; 46 | } 47 | .title a{text-decoration: none;} 48 | .delcommentbutton{ 49 | font-size: xx-small; 50 | } 51 | .delcommentpassword{ 52 | font-size: xx-small; 53 | } -------------------------------------------------------------------------------- /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: entry 16 | - name: datetime 17 | 18 | - kind: Entry 19 | properties: 20 | - name: public 21 | - name: datetime 22 | 23 | - kind: Entry 24 | properties: 25 | - name: public 26 | - name: datetime 27 | direction: desc 28 | 29 | - kind: Entry 30 | properties: 31 | - name: public 32 | - name: tags 33 | - name: datetime 34 | direction: desc 35 | 36 | - kind: Entry 37 | properties: 38 | - name: tags 39 | - name: datetime 40 | direction: desc 41 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from google.appengine.ext import webapp, db 4 | from google.appengine.dist import use_library 5 | use_library('django', '1.2') 6 | from google.appengine.ext.webapp import util, template 7 | from google.appengine.api import users, memcache 8 | import urllib, datetime, re, os 9 | from ConfigParser import ConfigParser 10 | config = os.path.join(os.path.dirname(__file__), "config") 11 | default = {"step" : "10", 12 | "analytics" : "UA-XXXXXXXX-X"} 13 | parser = ConfigParser(default) 14 | f_in = open(config, "r") 15 | parser.readfp(f_in) 16 | f_in.close() 17 | step = parser.getint("default","step") 18 | google_analytics = parser.get("log", "google_analytics") 19 | 20 | class AuthHandler(webapp.RequestHandler): 21 | def get(self, key = ""): 22 | if users.get_current_user() == None: 23 | self.response.out.write("Sign in or register." % users.create_login_url("/admin")) 24 | elif users.is_current_user_admin() != True: 25 | self.response.out.write('Your account %s is not admin. Log out and log in with an admin account.' % (users.get_current_user(), users.create_logout_url("/admin"))) 26 | else: 27 | if key: 28 | self.get2(key) 29 | else: 30 | self.get2() 31 | def post(self, key = ""): 32 | if users.is_current_user_admin(): 33 | if key: 34 | self.post2(key) 35 | else: 36 | self.post2() 37 | 38 | class MainHandler(AuthHandler): 39 | def get(self, pageStr): 40 | page = parseInt(pageStr) 41 | entries = filter_entries(Entry.all().order("-datetime")).fetch(step + 1, page * step) 42 | params = {'entries': entries[:step]} 43 | if len(entries) > step: 44 | params['next'] = page + 1 45 | if page > 0: 46 | params['prev'] = page - 1 47 | print_with_template(self, 'index.html', params) 48 | 49 | class RSSHandler(AuthHandler): 50 | def get(self, pageStr): 51 | print_with_template(self, 'rss.xml', {'entries':filter_entries(Entry.all().order("-datetime")).fetch(30)}) 52 | 53 | class AdminHandler(AuthHandler): 54 | def get2(self): 55 | self.redirect("/") 56 | 57 | class PostHandler(AuthHandler): 58 | def get2(self,key = ""): 59 | entry = Entry.get(key) if key != '' else Entry() 60 | print_with_template(self, 'form.html',{'entry':entry, 'key':key}) 61 | def post2(self, key = ""): 62 | if self.request.get("title") != '' and self.request.get("body") != '': 63 | entry = Entry.get(key) if key != '' else Entry() 64 | entry.title = self.request.get("title") 65 | entry.body = self.request.get("body") 66 | entry.datetime = datetime.datetime.strptime(self.request.get("datetime"), "%Y-%m-%d %H:%M:%S") - datetime.timedelta(hours=9) 67 | entry.public = self.request.get("public") == "1" 68 | entry.tags = [] 69 | for tagStr in self.request.get('tags').replace(u' ',' ').replace(' ',' ').replace(',',' ').split(' '): 70 | tag = Tag.all().filter("tag =", tagStr).get() 71 | if tag == None: 72 | tag = Tag(tag = tagStr) 73 | tag.put() 74 | entry.tags.append(tag.key()) 75 | entry.put() 76 | self.redirect('/') 77 | 78 | class PostCommentHandler(AuthHandler): 79 | def post(self, key): 80 | if self.request.get("comment") != '': 81 | Comment( 82 | entry = Entry.get(key), 83 | comment = self.request.get("comment"), 84 | delpass = self.request.get("delpass"), 85 | nickname = self.request.get("nickname") 86 | ).put() 87 | self.redirect("/entry/%s" % key) 88 | 89 | class DeleteHandler(AuthHandler): 90 | def get2(self, key): 91 | db.delete(Entry.get(key)) 92 | self.redirect('/') 93 | 94 | class DeleteCommentHandler(AuthHandler): 95 | def post(self, key): 96 | comment = Comment.get(key) 97 | entry_key = comment.entry.key() 98 | if self.request.get("delpass") == comment.delpass: 99 | db.delete(comment) 100 | self.redirect('/entry/%s' % entry_key) 101 | 102 | class TagHandler(AuthHandler): 103 | def get(self, key, pageStr): 104 | page = parseInt(pageStr) 105 | tagStr = urllib.unquote(key).decode('utf-8') 106 | tag = Tag.all().filter("tag =", tagStr).get() 107 | if tag: 108 | entries = filter_entries(tag.entries.order("-datetime")).fetch(step + 1, page * step) 109 | params = {'entries': entries[:step]} 110 | if len(entries) > step: 111 | params['next'] = page + 1 112 | if page > 0: 113 | params['prev'] = page - 1 114 | params['tag'] = tagStr 115 | print_with_template(self, 'index.html', params) 116 | else: 117 | print_with_template(self, 'error.html', {'message':"Tag %s does not exist" % h(tagStr)}) 118 | 119 | class EntryHandler(AuthHandler): 120 | def get(self, key): 121 | entry = Entry.get(key); 122 | if users.is_current_user_admin() or (entry.datetime > datetime.datetime.now() and entry.public): 123 | print_with_template(self, 'index.html', {'entries':[Entry.get(key)], 'detail':True}) 124 | else: 125 | self.redirect("/") 126 | 127 | class UploaderHandler(AuthHandler): 128 | def get2(self, key): 129 | print_with_template(self, 'upload.html', {'images': Entry.get(key).images, 'key': key}) 130 | def post2(self, key): 131 | if self.request.get('file'): 132 | image = Image() 133 | image.image = self.request.POST.get('file').file.read() 134 | image.contentType = self.request.body_file.vars['file'].headers['content-type'] 135 | image.entry = Entry.get(key) 136 | image.put() 137 | self.redirect('/uploader/' + key) 138 | 139 | class DeleteImageHandler(AuthHandler): 140 | def get2(self, key): 141 | Image.get(key).delete() 142 | self.redirect('/uploader') 143 | 144 | class ImageHandler(AuthHandler): 145 | def get(self, key): 146 | image = quickGet(key) 147 | self.response.headers['Content-Type'] = image.contentType.encode('utf-8') 148 | self.response.out.write(image.image) 149 | 150 | ## Functions 151 | def quickGet(key): 152 | data = memcache.get(key) 153 | if data == None: 154 | data = db.get(key) 155 | memcache.set(key = key, value = data, time=3600) 156 | return data 157 | 158 | def apply_filters(str): 159 | str = h(str) 160 | str = nl2br(str) 161 | str = linkURLs(str) 162 | str = replaceStrongs(str) 163 | str = replaceImages(str) 164 | str = replaceLists(str) 165 | return str 166 | 167 | def urlReplacer(match, limit = 45): 168 | return '%s' % (match.group(), match.group()[:limit] + ('...' if len(match.group()) > limit else '')) 169 | 170 | def linkURLs(str): 171 | return re.sub(r'([^"]|^)(https?|ftp)(://[\w:;/.?%#&=+-]+)', urlReplacer, str) 172 | 173 | def replaceImages(str): 174 | return re.sub(r'\[img:(.*)\]', r'', str) 175 | 176 | def replaceStrongs(str): 177 | str = re.sub(r'\[s(trong)?\]', r'', str) 178 | str = re.sub(r'\[/s(trong)?\]', r'', str) 179 | return str 180 | 181 | def replaceLists(str): 182 | return re.sub(re.compile('^-(.+)$', re.MULTILINE), r'
  • \1
  • ', str) 183 | 184 | def nl2br(str): 185 | return str.replace('\r\n','\n').replace('\n','
    \n') 186 | 187 | def print_with_template(self, view, params = {}): 188 | params.update({ 189 | 'login': users.is_current_user_admin(), 190 | 'logout_url': users.create_logout_url("/"), 191 | 'google_analytics': google_analytics, 192 | }) 193 | fpath = os.path.join(os.path.dirname(__file__), 'templates', view) 194 | html = template.render(fpath, params) 195 | self.response.out.write(html) 196 | 197 | def h(html): 198 | return html.replace('&','&').replace('<','<').replace('>','>').replace('"','"') 199 | 200 | def filter_entries(entries): 201 | if not users.is_current_user_admin(): 202 | entries = entries.filter("datetime <", datetime.datetime.now()).filter("public =", True) 203 | return entries 204 | 205 | def parseInt(str): 206 | try: 207 | return int(str) 208 | except ValueError: 209 | return 0 210 | 211 | ## Models 212 | class Entry(db.Model): 213 | title = db.StringProperty(default = "") 214 | body = db.TextProperty(default = "") 215 | tags = db.ListProperty(db.Key) 216 | datetime = db.DateTimeProperty(auto_now_add = True) 217 | public = db.BooleanProperty(default = True) 218 | @property 219 | def formattedDatetimeInJST(self): 220 | return (self.datetime + datetime.timedelta(hours=9)).strftime("%Y-%m-%d %H:%M:%S") 221 | def tagStr(self): 222 | return " ".join([Tag.get(x).tag for x in self.tags]) 223 | def formatted_body(self): 224 | return apply_filters(self.body) 225 | def tagList(self): 226 | return [Tag.get(t).tag for t in self.tags] 227 | def comment_count(self): 228 | return self.comments.count() 229 | 230 | class Tag(db.Model): 231 | tag = db.StringProperty() 232 | @property 233 | def entries(self): 234 | return Entry.all().filter('tags', self.key()).order('-datetime') 235 | 236 | class Comment(db.Model): 237 | comment = db.TextProperty(default = "") 238 | entry = db.ReferenceProperty(Entry, collection_name = 'comments') 239 | user = db.UserProperty() 240 | datetime = db.DateTimeProperty(auto_now_add = True) 241 | delpass = db.TextProperty() 242 | nickname = db.TextProperty() 243 | 244 | class Image(db.Model): 245 | entry = db.ReferenceProperty(Entry, collection_name = 'images') 246 | image = db.BlobProperty() 247 | contentType = db.StringProperty() 248 | 249 | def main(): 250 | application = webapp.WSGIApplication([ 251 | ('/tag/(.*)/(.*)', TagHandler), 252 | ('/entry/(.*)', EntryHandler), 253 | ('/admin/?(.*)', AdminHandler), 254 | ('/postComment/?(.*)', PostCommentHandler), 255 | ('/post/?(.*)', PostHandler), 256 | ('/rss/?(.*)', RSSHandler), 257 | ('/deleteComment/?(.*)', DeleteCommentHandler), 258 | ('/deleteImage/(.*)', DeleteImageHandler), 259 | ('/delete/?(.*)', DeleteHandler), 260 | ('/uploader/?(.*)', UploaderHandler), 261 | ('/image/(.*)', ImageHandler), 262 | ('/(.*)', MainHandler) 263 | ], debug=True) 264 | util.run_wsgi_app(application) 265 | 266 | if __name__ == '__main__': 267 | main() -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Python blog system 9 | 10 | 11 | {%if google_analytics%} 12 | 22 | {%endif%} 23 | 24 | 25 | 30 | 31 |
    32 |
    33 | {%block contents%} 34 | {%if login%} 35 |

    [New] [Log out]

    36 | {%endif%} 37 | {%for entry in entries%} 38 |
    39 |
    40 |

    41 | {{entry.title}} 42 | {%if login%} 43 | [edit] 44 | {%endif%} 45 |

    46 | 47 |
    48 |
    49 | {{entry.formatted_body|safe}} 50 |
    51 | 57 |

    コメント

    78 | {%else%} 79 | コメント({{entry.comment_count}}) 80 |
    81 | {%endif%} 82 | 83 |
    84 | {%endfor%} 85 |
    86 | {%if prev or prev == 0 %} 87 | [ Prev ] 88 | {%endif%} 89 | {%if next%} 90 | [ Next ] 91 | {%endif%} 92 |
    93 | {%endblock contents%} 94 | 95 |
    96 |
    97 | {%block rightbar%} 98 | 99 | {%endblock rightbar%} 100 |
    101 | 102 | 106 |
    developed by python練習帳
    -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%block contents%} 4 |

    エラー

    5 | {{message}} 6 | {%endblock contents%} -------------------------------------------------------------------------------- /templates/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%block contents%} 4 |
    5 |

    タイトル

    6 |

    本文

    7 |

    タグ

    8 |

    公開日時

    9 |

    公開ステータス

    10 | 11 | 12 |

    画像 [uploader]

    13 |
    14 | {%if key%} 15 | 16 | {%endif%} 17 | 18 |
    19 |
    20 | {%endblock contents%} 21 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | -------------------------------------------------------------------------------- /templates/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Python Blog System 8 | http://python-blog-system.appspot.com 9 | %(title)s 10 | ja 11 | N/A 12 | %(now)s 13 | 14 | {%for entry in entries%} 15 | 16 | {{entry.title}} 17 | http://python-blog-system.appspot.com/entry/%(key)s 18 | {{entry.body}} 19 | {{entry.formattedDatetimeInJST}} 20 | 21 | {%endfor%} 22 | 23 | -------------------------------------------------------------------------------- /templates/upload.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {%block contents%} 5 | {%for image in images%} 6 |


    7 | {%endfor%} 8 |

    9 | {%endblock contents%} 10 | --------------------------------------------------------------------------------