Changeset 1070:045c95f337bb
- Timestamp:
- 06/01/09 14:13:11 (3 years ago)
- Branch:
- default
- Files:
-
- 4 edited
-
sql/001-add-comment-count-column.sql (modified) (1 diff)
-
zine/database.py (modified) (4 diffs)
-
zine/models.py (modified) (8 diffs)
-
zine/widgets.py (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sql/001-add-comment-count-column.sql
r1063 r1070 2 2 3 3 alter table posts add column comment_count integer after comments_enabled not null; 4 5 -- We're missing a migration that splits texts and posts into the old 6 -- table contents and the new "texts" one. Migrations should follow 7 -- soon. Last revision with old tables is 198cd0bdbc05. -
zine/database.py
r1069 r1070 53 53 54 54 55 def create_engine(uri, relative_to=None, echo= False):55 def create_engine(uri, relative_to=None, echo=True): 56 56 """Create a new engine. This works a bit like SQLAlchemy's 57 57 `create_engine` with the difference that it automaticaly set's MySQL … … 252 252 ) 253 253 254 texts = db.Table('texts', metadata, 255 db.Column('text_id', db.Integer, primary_key=True), 256 db.Column('text', db.Text), 257 db.Column('parser_data', db.ZEMLParserData), 258 db.Column('extra', db.PickleType) 259 ) 260 254 261 posts = db.Table('posts', metadata, 255 262 db.Column('post_id', db.Integer, primary_key=True), … … 259 266 db.Column('uid', db.String(250)), 260 267 db.Column('title', db.String(150)), 261 db.Column('text ', db.Text),268 db.Column('text_id', db.Integer, db.ForeignKey('texts.text_id')), 262 269 db.Column('author_id', db.Integer, db.ForeignKey('users.user_id')), 263 db.Column('parser_data', db.ZEMLParserData),264 270 db.Column('comments_enabled', db.Boolean), 265 271 db.Column('comment_count', db.Integer, nullable=False, default=0), 266 272 db.Column('pings_enabled', db.Boolean), 267 273 db.Column('content_type', db.String(40), index=True), 268 db.Column('extra', db.PickleType), 269 db.Column('status', db.Integer) 274 db.Column('status', db.Integer), 270 275 ) 271 276 … … 304 309 db.Column('email', db.String(250)), 305 310 db.Column('www', db.String(200)), 306 db.Column('text ', db.Text),311 db.Column('text_id', db.Integer, db.ForeignKey('texts.text_id')), 307 312 db.Column('is_pingback', db.Boolean, nullable=False), 308 db.Column('parser_data', db.ZEMLParserData),309 313 db.Column('parent_id', db.Integer, db.ForeignKey('comments.comment_id')), 310 314 db.Column('pub_date', db.DateTime), -
zine/models.py
r1068 r1070 17 17 from zine.database import users, categories, posts, post_links, \ 18 18 post_categories, post_tags, tags, comments, groups, group_users, \ 19 privileges, user_privileges, group_privileges, db19 privileges, user_privileges, group_privileges, texts, db 20 20 from zine.utils import zeml 21 21 from zine.utils.text import gen_slug, gen_timestamped_slug, build_tag_uri, \ … … 303 303 304 304 305 class PostQuery(db.Query):305 class _PostQueryBase(db.Query): 306 306 """Add some extra methods to the post model.""" 307 307 … … 312 312 args = map(db.lazyload, lazy or ()) + map(db.defer, deferred or ()) 313 313 return self.options(*args) 314 315 def summary_lightweight(self):316 """A query with lightweight settings for summaries. (Like widgets317 etc.) Does not load text or comments or anything related.318 """319 return self.lightweight(lazy=('comments', 'categories', 'tags'),320 deferred=('parser_data', 'text', 'extra'))321 322 def theme_lightweight(self, key):323 """A query for lightweight settings based on the theme. For example324 to use the lightweight settings for the author overview page you can325 use this query::326 327 Post.query.theme_lightweight('author_overview')328 """329 theme_settings = get_application().theme.settings330 deferred = theme_settings.get('sql.%s.deferred' % key)331 lazy = theme_settings.get('sql.%s.lazy' % key)332 return self.lightweight(deferred, lazy)333 314 334 315 def type(self, content_type): … … 513 494 514 495 515 class Post(_ZEMLDualContainer):496 class _PostBase(object): 516 497 """Represents one blog post.""" 498 499 @property 500 def _privileges(self): 501 return get_application().content_type_privileges[self.content_type] 502 503 @property 504 def EDIT_OWN_PRIVILEGE(self): 505 """The edit-own privilege for this content type.""" 506 return self._privileges[1] 507 508 @property 509 def EDIT_OTHER_PRIVILEGE(self): 510 """The edit-other privilege for this content type.""" 511 return self._privileges[2] 512 513 @property 514 def root_comments(self): 515 """Return only the comments for this post that don't have a parent.""" 516 return [x for x in self.comments if x.parent is None] 517 518 @property 519 def visible_comments(self): 520 """Return only the comments for this post that are visible to 521 the user. 522 """ 523 return [x for x in self.comments if x.visible] 524 525 @property 526 def visible_root_comments(self): 527 """Return only the comments for this post that are visible to 528 the user and that don't have a parent. 529 """ 530 return [x for x in self.comments if x.visible and x.parent is None] 531 532 @property 533 def comment_count(self): 534 """The number of visible comments.""" 535 req = get_request() 536 537 # if the model was loaded with .lightweight() there are no comments 538 # but a _comment_count we can use. 539 if not db.attribute_loaded(self, 'comments'): 540 return self._comment_count 541 542 # otherwise the comments are already available and we can savely 543 # filter it. 544 if req and req.user.is_manager: 545 return len(self.comments) 546 return len([x for x in self.comments if not x.blocked]) 547 548 @property 549 def comment_feed_url(self): 550 """The link to the comment feed.""" 551 return make_external_url(self.slug.rstrip('/') + '/feed.atom') 552 553 @property 554 def is_draft(self): 555 """True if this post is unpublished.""" 556 return self.status == STATUS_DRAFT 557 558 def sync_comment_count(self): 559 """Sync the reflected comment count.""" 560 self._comment_count = self.query \ 561 .published(ignore_privileges=True).count() 562 563 def set_auto_slug(self): 564 """Generate a slug for this post.""" 565 cfg = get_application().cfg 566 slug = gen_slug(self.title) 567 if not slug: 568 slug = to_blog_timezone(self.pub_date).strftime('%H%M') 569 570 full_slug = gen_timestamped_slug(slug, self.content_type, self.pub_date) 571 572 if full_slug != self.slug: 573 while Post.query.autoflush(False).filter_by(slug=full_slug) \ 574 .limit(1).count(): 575 full_slug = increment_string(full_slug) 576 self.slug = full_slug 577 578 def touch_times(self, pub_date=None): 579 """Touches the times for this post. If the pub_date is given the 580 `pub_date` is changed to the given date. If it's not given the 581 current time is assumed if the post status is set to published, 582 otherwise it's set to `None`. 583 584 Additionally the `last_update` is always set to now. 585 """ 586 now = datetime.utcnow() 587 if pub_date is None and self.status == STATUS_PUBLISHED: 588 pub_date = now 589 self.pub_date = pub_date 590 self.last_update = now 591 592 def bind_slug(self, slug=None): 593 """Binds a new slug to the post. If the slug is `None`/empty a new 594 automatically generated slug is created. Otherwise that slug is 595 used and assigned. 596 """ 597 if not slug: 598 self.set_auto_slug() 599 else: 600 self.slug = slug 601 602 def bind_tags(self, tags): 603 """Rebinds the tags to a list of tags (strings, not tag objects).""" 604 current_map = dict((x.name, x) for x in self.tags) 605 currently_attached = set(x.name for x in self.tags) 606 new_tags = set(tags) 607 608 # delete outdated tags 609 for name in currently_attached.difference(new_tags): 610 self.tags.remove(current_map[name]) 611 612 # add new tags 613 for name in new_tags.difference(currently_attached): 614 self.tags.append(Tag.get_or_create(name)) 615 616 def bind_categories(self, categories): 617 """Rebinds the categories to the list passed. The list of objects 618 must be a list of category objects. 619 """ 620 currently_attached = set(self.categories) 621 new_categories = set(categories) 622 623 # delete outdated categories 624 for category in currently_attached.difference(new_categories): 625 self.categories.remove(category) 626 627 # attach new categories 628 for category in new_categories.difference(currently_attached): 629 self.categories.append(category) 630 631 def can_edit(self, user=None): 632 """Checks if the given user (or current user) can edit this post.""" 633 if user is None: 634 user = get_request().user 635 636 return ( 637 user.has_privilege(self.EDIT_OTHER_PRIVILEGE) or 638 (self.author == user and 639 user.has_privilege(self.EDIT_OWN_PRIVILEGE)) 640 ) 641 642 def can_read(self, user=None): 643 """Check if the current user or the user provided can read-access 644 this post. If there is no user there must be a request object 645 for this thread defined. 646 """ 647 # published posts are always accessible 648 if self.status == STATUS_PUBLISHED and self.pub_date is not None and \ 649 self.pub_date <= datetime.utcnow(): 650 return True 651 652 if user is None: 653 user = get_request().user 654 655 # users that are allowed to look at drafts may pass 656 if user.has_privilege(VIEW_DRAFTS): 657 return True 658 659 # if this is protected and user can view protected, allow them 660 if self.status == STATUS_PROTECTED and self.pub_date is not None and \ 661 self.pub_date <= datetime.utcnow() and \ 662 user.has_privilege(VIEW_PROTECTED): 663 return True 664 665 # if we have the privilege to edit other entries or if we are 666 # a blog administrator we can always look at posts. 667 if user.has_privilege(self.EDIT_OTHER_PRIVILEGE): 668 return True 669 670 # otherwise if the user has the EDIT_OWN_PRIVILEGE and the 671 # author of the post, he may look at it as well 672 if user.id == self.author_id and \ 673 user.has_privilege(self.EDIT_OWN_PRIVILEGE): 674 return True 675 676 return False 677 678 @property 679 def is_published(self): 680 """`True` if the post is visible for everyone.""" 681 return self.can_read(AnonymousUser()) 682 683 @property 684 def is_private(self): 685 """`True` if the post is marked private.""" 686 return self.status == STATUS_PRIVATE 687 688 @property 689 def is_protected(self): 690 """`True` if the post is marked protected.""" 691 return self.status == STATUS_PROTECTED 692 693 @property 694 def is_scheduled(self): 695 """True if the item is scheduled for appearing.""" 696 return self.status == STATUS_PUBLISHED and \ 697 self.pub_date > datetime.utcnow() 698 699 def get_url_values(self): 700 return self.slug 701 702 def __repr__(self): 703 return '<%s %r>' % ( 704 self.__class__.__name__, 705 self.title 706 ) 707 708 709 class PostQuery(_PostQueryBase): 710 """Add some extra methods to the post model.""" 711 712 def theme_lightweight(self, key): 713 """A query for lightweight settings based on the theme. For example 714 to use the lightweight settings for the author overview page you can 715 use this query:: 716 717 Post.query.theme_lightweight('author_overview') 718 """ 719 theme_settings = get_application().theme.settings 720 deferred = theme_settings.get('sql.%s.deferred' % key) 721 lazy = theme_settings.get('sql.%s.lazy' % key) 722 return self.lightweight(deferred, lazy) 723 724 725 class SummarizedPostQuery(_PostQueryBase): 726 """Add some extra methods to the summarized post model.""" 727 728 729 class Post(_PostBase, _ZEMLDualContainer): 730 """A full blown post.""" 517 731 518 732 query = db.query_property(PostQuery) … … 554 768 self.uid = uid 555 769 556 @property 557 def _privileges(self): 558 return get_application().content_type_privileges[self.content_type] 559 560 @property 561 def EDIT_OWN_PRIVILEGE(self): 562 """The edit-own privilege for this content type.""" 563 return self._privileges[1] 564 565 @property 566 def EDIT_OTHER_PRIVILEGE(self): 567 """The edit-other privilege for this content type.""" 568 return self._privileges[2] 569 570 @property 571 def root_comments(self): 572 """Return only the comments for this post that don't have a parent.""" 573 return [x for x in self.comments if x.parent is None] 574 575 @property 576 def visible_comments(self): 577 """Return only the comments for this post that are visible to 578 the user. 579 """ 580 return [x for x in self.comments if x.visible] 581 582 @property 583 def visible_root_comments(self): 584 """Return only the comments for this post that are visible to 585 the user and that don't have a parent. 586 """ 587 return [x for x in self.comments if x.visible and x.parent is None] 588 589 @property 590 def comment_count(self): 591 """The number of visible comments.""" 592 req = get_request() 593 594 # if the model was loaded with .lightweight() there are no comments 595 # but a _comment_count we can use. 596 if not db.attribute_loaded(self, 'comments'): 597 return self._comment_count 598 599 # otherwise the comments are already available and we can savely 600 # filter it. 601 if req and req.user.is_manager: 602 return len(self.comments) 603 return len([x for x in self.comments if not x.blocked]) 604 605 @property 606 def comment_feed_url(self): 607 """The link to the comment feed.""" 608 return make_external_url(self.slug.rstrip('/') + '/feed.atom') 609 610 @property 611 def is_draft(self): 612 """True if this post is unpublished.""" 613 return self.status == STATUS_DRAFT 614 615 def sync_comment_count(self): 616 """Sync the reflected comment count.""" 617 self._comment_count = self.query \ 618 .published(ignore_privileges=True).count() 619 620 def set_auto_slug(self): 621 """Generate a slug for this post.""" 622 cfg = get_application().cfg 623 slug = gen_slug(self.title) 624 if not slug: 625 slug = to_blog_timezone(self.pub_date).strftime('%H%M') 626 627 full_slug = gen_timestamped_slug(slug, self.content_type, self.pub_date) 628 629 if full_slug != self.slug: 630 while Post.query.autoflush(False).filter_by(slug=full_slug) \ 631 .limit(1).count(): 632 full_slug = increment_string(full_slug) 633 self.slug = full_slug 634 635 def touch_times(self, pub_date=None): 636 """Touches the times for this post. If the pub_date is given the 637 `pub_date` is changed to the given date. If it's not given the 638 current time is assumed if the post status is set to published, 639 otherwise it's set to `None`. 640 641 Additionally the `last_update` is always set to now. 642 """ 643 now = datetime.utcnow() 644 if pub_date is None and self.status == STATUS_PUBLISHED: 645 pub_date = now 646 self.pub_date = pub_date 647 self.last_update = now 648 649 def bind_slug(self, slug=None): 650 """Binds a new slug to the post. If the slug is `None`/empty a new 651 automatically generated slug is created. Otherwise that slug is 652 used and assigned. 653 """ 654 if not slug: 655 self.set_auto_slug() 656 else: 657 self.slug = slug 658 659 def bind_tags(self, tags): 660 """Rebinds the tags to a list of tags (strings, not tag objects).""" 661 current_map = dict((x.name, x) for x in self.tags) 662 currently_attached = set(x.name for x in self.tags) 663 new_tags = set(tags) 664 665 # delete outdated tags 666 for name in currently_attached.difference(new_tags): 667 self.tags.remove(current_map[name]) 668 669 # add new tags 670 for name in new_tags.difference(currently_attached): 671 self.tags.append(Tag.get_or_create(name)) 672 673 def bind_categories(self, categories): 674 """Rebinds the categories to the list passed. The list of objects 675 must be a list of category objects. 676 """ 677 currently_attached = set(self.categories) 678 new_categories = set(categories) 679 680 # delete outdated categories 681 for category in currently_attached.difference(new_categories): 682 self.categories.remove(category) 683 684 # attach new categories 685 for category in new_categories.difference(currently_attached): 686 self.categories.append(category) 687 688 def can_edit(self, user=None): 689 """Checks if the given user (or current user) can edit this post.""" 690 if user is None: 691 user = get_request().user 692 693 return ( 694 user.has_privilege(self.EDIT_OTHER_PRIVILEGE) or 695 (self.author == user and 696 user.has_privilege(self.EDIT_OWN_PRIVILEGE)) 697 ) 698 699 def can_read(self, user=None): 700 """Check if the current user or the user provided can read-access 701 this post. If there is no user there must be a request object 702 for this thread defined. 703 """ 704 # published posts are always accessible 705 if self.status == STATUS_PUBLISHED and self.pub_date is not None and \ 706 self.pub_date <= datetime.utcnow(): 707 return True 708 709 if user is None: 710 user = get_request().user 711 712 # users that are allowed to look at drafts may pass 713 if user.has_privilege(VIEW_DRAFTS): 714 return True 715 716 # if this is protected and user can view protected, allow them 717 if self.status == STATUS_PROTECTED and self.pub_date is not None and \ 718 self.pub_date <= datetime.utcnow() and \ 719 user.has_privilege(VIEW_PROTECTED): 720 return True 721 722 # if we have the privilege to edit other entries or if we are 723 # a blog administrator we can always look at posts. 724 if user.has_privilege(self.EDIT_OTHER_PRIVILEGE): 725 return True 726 727 # otherwise if the user has the EDIT_OWN_PRIVILEGE and the 728 # author of the post, he may look at it as well 729 if user.id == self.author_id and \ 730 user.has_privilege(self.EDIT_OWN_PRIVILEGE): 731 return True 732 733 return False 734 735 @property 736 def is_published(self): 737 """`True` if the post is visible for everyone.""" 738 return self.can_read(AnonymousUser()) 739 740 @property 741 def is_private(self): 742 """`True` if the post is marked private.""" 743 return self.status == STATUS_PRIVATE 744 745 @property 746 def is_protected(self): 747 """`True` if the post is marked protected.""" 748 return self.status == STATUS_PROTECTED 749 750 @property 751 def is_scheduled(self): 752 """True if the item is scheduled for appearing.""" 753 return self.status == STATUS_PUBLISHED and \ 754 self.pub_date > datetime.utcnow() 755 756 def get_url_values(self): 757 return self.slug 758 759 def __repr__(self): 760 return '<%s %r>' % ( 761 self.__class__.__name__, 762 self.title 763 ) 770 771 class SummarizedPost(_PostBase): 772 """Like a regular post but without text and parser data.""" 773 774 query = db.query_property(SummarizedPostQuery) 775 776 def __init__(self): 777 raise TypeError('You cannot create %r instance' % type(self).__name__) 764 778 765 779 … … 1181 1195 query_class=PostQuery) 1182 1196 }, order_by=categories.c.name) 1183 db.mapper(Comment, comments, properties={1197 db.mapper(Comment, db.join(comments, texts), properties={ 1184 1198 'id': comments.c.comment_id, 1185 'text': db.synonym('_text', map_column=True), 1186 'author': db.synonym('_author', map_column=True), 1187 'email': db.synonym('_email', map_column=True), 1188 'www': db.synonym('_www', map_column=True), 1189 'status': db.synonym('_status', map_column=True), 1199 'text_id': [comments.c.text_id, texts.c.text_id], 1200 '_text': texts.c.text, 1201 'text': db.synonym('_text'), 1202 '_author': comments.c.author, 1203 'author': db.synonym('_author'), 1204 '_email': comments.c.email, 1205 'email': db.synonym('_email'), 1206 '_www': comments.c.www, 1207 'www': db.synonym('_www'), 1208 '_status': comments.c.status, 1209 'status': db.synonym('_status'), 1190 1210 'children': db.relation(Comment, 1191 1211 primaryjoin=comments.c.parent_id == comments.c.comment_id, … … 1204 1224 query_class=PostQuery) 1205 1225 }, order_by=tags.c.name) 1206 db.mapper(Post, posts, properties={1226 db.mapper(Post, db.join(posts, texts), properties={ 1207 1227 'id': posts.c.post_id, 1208 'text': db.synonym('_text', map_column=True), 1228 'text_id': [posts.c.text_id, texts.c.text_id], 1229 '_text': texts.c.text, 1230 'text': db.synonym('_text'), 1209 1231 'comments': db.relation(Comment, backref='post', 1210 1232 primaryjoin=posts.c.post_id == … … 1220 1242 'tags': db.relation(Tag, secondary=post_tags, lazy=False, 1221 1243 order_by=[tags.c.name]), 1244 '_comment_count': posts.c.comment_count, 1245 'comment_count': db.synonym('_comment_count') 1246 }, order_by=posts.c.pub_date.desc()) 1247 db.mapper(SummarizedPost, posts, properties={ 1248 'id': posts.c.post_id, 1249 'comments': db.relation(Comment, 1250 primaryjoin=posts.c.post_id == 1251 comments.c.post_id, 1252 order_by=[db.asc(comments.c.pub_date)], 1253 lazy=True, viewonly=True), 1254 'links': db.relation(PostLink, viewonly=True, lazy=True), 1255 'categories': db.relation(Category, secondary=post_categories, lazy=True, 1256 order_by=[db.asc(categories.c.name)], 1257 viewonly=True), 1258 'tags': db.relation(Tag, secondary=post_tags, lazy=True, 1259 viewonly=True, order_by=[tags.c.name]), 1222 1260 'comment_count': db.synonym('_comment_count', map_column=True) 1223 1261 }, order_by=posts.c.pub_date.desc()) -
zine/widgets.py
r1068 r1070 18 18 """ 19 19 from zine.application import render_template 20 from zine.models import Post, Category, Tag, Comment20 from zine.models import Post, SummarizedPost, Category, Tag, Comment 21 21 22 22 … … 49 49 50 50 def __init__(self, detail='months', limit=6, show_title=False): 51 self.__dict__.update( Post.query.summary_lightweight()52 .get_archive_summary(detail, limit))51 self.__dict__.update(SummarizedPost.query 52 .get_archive_summary(detail, limit)) 53 53 self.show_title = show_title 54 54 … … 61 61 62 62 def __init__(self, limit=5, show_title=False, content_types=None): 63 query = Post.query.summary_lightweight()64 63 if content_types is None: 65 query = query.for_index()64 query = SummarizedPost.query.for_index() 66 65 else: 67 query = query.filter(Post.content_type.in_(content_types)) 66 query = SummarizedPost.query.filter(SummarizedPost 67 .content_type.in_(content_types)) 68 68 self.posts = query.latest().limit(limit).all() 69 69 self.show_title = show_title
Note: See TracChangeset
for help on using the changeset viewer.