PageRenderTime 42ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/pypress/models/blog.py

https://github.com/laoqiu/pypress-tornado
Python | 447 lines | 372 code | 63 blank | 12 comment | 19 complexity | 857778965fb833ee59bb61c416c5b726 MD5 | raw file
  1. #!/usr/bin/env python
  2. #coding=utf-8
  3. """
  4. models: blog.py
  5. ~~~~~~~~~~~~~
  6. :author: laoqiu.com@gmail.com
  7. """
  8. import re, random
  9. from datetime import datetime
  10. import tornado.web
  11. from pypress.extensions.sqlalchemy import BaseQuery
  12. from pypress.extensions.cache import cached_property, cache
  13. from pypress.extensions.routing import route
  14. from pypress.extensions.permission import Permission, UserNeed
  15. from pypress.helpers import storage, slugify, markdown, endtags
  16. from pypress.permissions import admin, moderator
  17. from pypress.database import db
  18. from pypress.models.users import User
  19. __all__ = ['Post', 'Tag', 'Comment']
  20. class PostQuery(BaseQuery):
  21. def jsonify(self):
  22. for post in self.all():
  23. yield post.json
  24. def as_list(self):
  25. """
  26. Return restricted list of columns for list queries
  27. """
  28. deferred_cols = ("content",
  29. "tags",
  30. "author.email",
  31. "author.activation_key",
  32. "author.date_joined",
  33. "author.last_login")
  34. options = [db.defer(col) for col in deferred_cols]
  35. return self.options(*options)
  36. def get_by_slug(self, slug):
  37. post = self.filter(Post.slug==slug).first()
  38. if post is None:
  39. raise tornado.web.HTTPError(404)
  40. return post
  41. def search(self, keywords):
  42. criteria = []
  43. for keyword in keywords.split():
  44. keyword = '%' + keyword + '%'
  45. criteria.append(db.or_(Post.title.ilike(keyword),
  46. Post.content.ilike(keyword),
  47. Post.tags.ilike(keyword)
  48. ))
  49. q = reduce(db.and_, criteria)
  50. return self.filter(q)
  51. def archive(self, year, month, day):
  52. if not year:
  53. return self
  54. criteria = []
  55. criteria.append(db.extract('year',Post.created_date)==int(year))
  56. if month: criteria.append(db.extract('month',Post.created_date)==int(month))
  57. if day: criteria.append(db.extract('day',Post.created_date)==int(day))
  58. q = reduce(db.and_, criteria)
  59. return self.filter(q)
  60. class Post(db.Model):
  61. __tablename__ = 'posts'
  62. PER_PAGE = 40
  63. query_class = PostQuery
  64. id = db.Column(db.Integer, primary_key=True)
  65. author_id = db.Column(db.Integer,
  66. db.ForeignKey(User.id, ondelete='CASCADE'),
  67. nullable=False)
  68. _title = db.Column("title", db.Unicode(100), index=True)
  69. _slug = db.Column("slug", db.Unicode(50), unique=True, index=True)
  70. content = db.Column(db.UnicodeText)
  71. created_date = db.Column(db.DateTime, default=datetime.utcnow)
  72. update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
  73. _tags = db.Column("tags", db.Unicode(100), index=True)
  74. author = db.relation(User, innerjoin=True, lazy="joined")
  75. __mapper_args__ = {'order_by': id.desc()}
  76. class Permissions(object):
  77. def __init__(self, obj):
  78. self.obj = obj
  79. @cached_property
  80. def edit(self):
  81. return Permission(UserNeed(self.obj.author_id))
  82. @cached_property
  83. def delete(self):
  84. return Permission(UserNeed(self.obj.author_id))
  85. def __init__(self, *args, **kwargs):
  86. super(Post, self).__init__(*args, **kwargs)
  87. def __str__(self):
  88. return self.title
  89. def __repr__(self):
  90. return "<%s>" % self
  91. @cached_property
  92. def permissions(self):
  93. return self.Permissions(self)
  94. def _get_title(self):
  95. return self._title
  96. def _set_title(self, title):
  97. self._title = title.lower().strip()
  98. if self.slug is None:
  99. self.slug = slugify(title)[:50]
  100. title = db.synonym("_title", descriptor=property(_get_title, _set_title))
  101. def _get_slug(self):
  102. return self._slug
  103. def _set_slug(self, slug):
  104. if slug:
  105. self._slug = slugify(slug)
  106. slug = db.synonym("_slug", descriptor=property(_get_slug, _set_slug))
  107. def _get_tags(self):
  108. return self._tags
  109. def _set_tags(self, tags):
  110. self._tags = tags
  111. if self.id:
  112. # ensure existing tag references are removed
  113. d = db.delete(post_tags, post_tags.c.post_id==self.id)
  114. db.engine.execute(d)
  115. for tag in set(self.taglist):
  116. slug = slugify(tag)
  117. tag_obj = Tag.query.filter(Tag.slug==slug).first()
  118. if tag_obj is None:
  119. tag_obj = Tag(name=tag, slug=slug)
  120. db.session.add(tag_obj)
  121. tag_obj.posts.append(self)
  122. tags = db.synonym("_tags", descriptor=property(_get_tags, _set_tags))
  123. @cached_property
  124. def url(self):
  125. return route.url_for('post_view',
  126. self.created_date.year,
  127. self.created_date.month,
  128. self.created_date.day,
  129. self.slug.encode('utf8'))
  130. @cached_property
  131. def taglist(self):
  132. if self.tags is None:
  133. return []
  134. tags = [t.strip() for t in self.tags.split(",")]
  135. return [t for t in tags if t]
  136. @cached_property
  137. def linked_taglist(self):
  138. return [(tag, route.url_for('tag', slugify(tag))) \
  139. for tag in self.taglist]
  140. @cached_property
  141. def prev_post(self):
  142. prev_post = Post.query.filter(Post.created_date < self.created_date) \
  143. .first()
  144. return prev_post
  145. @cached_property
  146. def next_post(self):
  147. next_post = Post.query.filter(Post.created_date > self.created_date) \
  148. .order_by('created_date').first()
  149. return next_post
  150. @cached_property
  151. def summary(self):
  152. s = re.findall(r'(<hr>)', self.content)
  153. if not s:
  154. return self.content
  155. p = s[0]
  156. return endtags(self.content.split(p)[0])
  157. @property
  158. def comments(self):
  159. """
  160. Returns comments in tree. Each parent comment has a "comments"
  161. attribute appended and a "depth" attribute.
  162. """
  163. comments = Comment.query.filter(Comment.post_id==self.id).all()
  164. def _get_comments(parent, depth):
  165. parent.comments = []
  166. parent.depth = depth
  167. for comment in comments:
  168. if comment.parent_id == parent.id:
  169. parent.comments.append(comment)
  170. _get_comments(comment, depth + 1)
  171. parents = [c for c in comments if c.parent_id is None]
  172. for parent in parents:
  173. _get_comments(parent, 0)
  174. return parents
  175. @cached_property
  176. def json(self):
  177. return dict(id=self.id,
  178. title=self.title,
  179. content=self.content,
  180. author=self.author.username)
  181. post_tags = db.Table("post_tags", db.Model.metadata,
  182. db.Column("post_id", db.Integer,
  183. db.ForeignKey('posts.id', ondelete='CASCADE'),
  184. primary_key=True),
  185. db.Column("tag_id", db.Integer,
  186. db.ForeignKey('tags.id', ondelete='CASCADE'),
  187. primary_key=True))
  188. class TagQuery(BaseQuery):
  189. @cache.cached(3600)
  190. def cloud(self):
  191. tags = self.filter(Tag.num_posts > 0).all()
  192. if not tags:
  193. return []
  194. max_posts = max(t.num_posts for t in tags)
  195. min_posts = min(t.num_posts for t in tags)
  196. diff = (max_posts - min_posts) / 10.0
  197. if diff < 0.1:
  198. diff = 0.1
  199. for tag in tags:
  200. tag.size = int(tag.num_posts / diff)
  201. if tag.size < 1:
  202. tag.size = 1
  203. random.shuffle(tags)
  204. return tags
  205. class Tag(db.Model):
  206. __tablename__ = "tags"
  207. query_class = TagQuery
  208. id = db.Column(db.Integer, primary_key=True)
  209. slug = db.Column(db.Unicode(80), unique=True)
  210. posts = db.dynamic_loader(Post, secondary=post_tags, query_class=PostQuery)
  211. _name = db.Column("name", db.Unicode(80), unique=True)
  212. def __init__(self, *args, **kwargs):
  213. super(Tag, self).__init__(*args, **kwargs)
  214. def __str__(self):
  215. return self.name
  216. def __repr__(self):
  217. return "<%s>" % self
  218. def _get_name(self):
  219. return self._name
  220. def _set_name(self, name):
  221. self._name = name.lower().strip()
  222. self.slug = slugify(name)
  223. name = db.synonym("_name", descriptor=property(_get_name, _set_name))
  224. @cached_property
  225. def url(self):
  226. return route.url_for('tag', self.slug)
  227. num_posts = db.column_property(
  228. db.select([db.func.count(post_tags.c.post_id)]).\
  229. where(db.and_(post_tags.c.tag_id==id,
  230. Post.id==post_tags.c.post_id)).as_scalar())
  231. class CommentQuery(BaseQuery):
  232. def as_data(self):
  233. comments = [storage({'author':comment.author,
  234. 'parent':comment.parent,
  235. 'comment':comment.comment,
  236. }) for comment in self.all()]
  237. return comments
  238. class Comment(db.Model):
  239. __tablename__ = "comments"
  240. query_class = CommentQuery
  241. PER_PAGE = 40
  242. id = db.Column(db.Integer, primary_key=True)
  243. post_id = db.Column(db.Integer,
  244. db.ForeignKey(Post.id, ondelete='CASCADE'),
  245. nullable=False)
  246. author_id = db.Column(db.Integer,
  247. db.ForeignKey(User.id, ondelete='CASCADE'))
  248. parent_id = db.Column(db.Integer,
  249. db.ForeignKey("comments.id", ondelete='CASCADE'))
  250. email = db.Column(db.String(50))
  251. nickname = db.Column(db.Unicode(50))
  252. website = db.Column(db.String(100))
  253. comment = db.Column(db.UnicodeText)
  254. created_date = db.Column(db.DateTime, default=datetime.utcnow)
  255. ip = db.Column(db.String(20))
  256. _author = db.relation(User, backref="posts", lazy="joined")
  257. post = db.relation(Post, innerjoin=True, lazy="joined")
  258. parent = db.relation('Comment', lazy="joined", remote_side=[id])
  259. __mapper_args__ = {'order_by' : id.asc()}
  260. class Permissions(object):
  261. def __init__(self, obj):
  262. self.obj = obj
  263. @cached_property
  264. def edit(self):
  265. return Permission(UserNeed(self.obj.author_id))
  266. @cached_property
  267. def reply(self):
  268. return Permission(UserNeed(self.obj.post.author_id))
  269. @cached_property
  270. def delete(self):
  271. return admin & moderator
  272. def __init__(self, *args, **kwargs):
  273. super(Comment, self).__init__(*args, **kwargs)
  274. def __str__(self):
  275. return self.comment
  276. def __repr__(self):
  277. return "<%s>" % self
  278. @cached_property
  279. def permissions(self):
  280. return self.Permissions(self)
  281. def _get_author(self):
  282. if self._author:
  283. self._author.website = None
  284. return self._author
  285. return storage(email = self.email,
  286. nickname = self.nickname,
  287. website = self.website)
  288. def _set_author(self, author):
  289. self._author = author
  290. author = db.synonym("_author", descriptor=property(_get_author, _set_author))
  291. @cached_property
  292. def url(self):
  293. return "%s#comment-%s" % (self.post.url, self.id)
  294. @cached_property
  295. def markdown(self):
  296. return markdown(self.comment or '')
  297. @cached_property
  298. def json(self):
  299. return dict(id=self.id,
  300. author=self.author,
  301. url=self.url,
  302. comment=self.comment,
  303. created_date=self.created_date)
  304. @cached_property
  305. def item(self):
  306. return storage(self.json)
  307. Post.num_comments = db.column_property(
  308. db.select([db.func.count(Comment.post_id)]) \
  309. .where(Comment.post_id==Post.id).as_scalar(), deferred=True)