diff --git a/kotti_blog/populate.py b/kotti_blog/populate.py index d9a9a22..1a3c958 100644 --- a/kotti_blog/populate.py +++ b/kotti_blog/populate.py @@ -54,5 +54,55 @@ class KottiBlogSettingsSchema(colander.MappingSchema): } +class UseSidebarCategories(colander.SchemaNode): + name = 'use_sidebar_categories' + title = _(u'Show categories in sidebar') + description = _(u'Show categories sorted by number of posts on which you ' + u'can filter.') + missing = True + default = True + + +class SidebarCategoriesNumber(colander.SchemaNode): + name = 'sidebar_categories_number' + title = _(u'The number of categories shown') + description = _(u'Choose how many categories are shown in the sidebar. ' + + u'Set to 0 to show all categories.') + default = 5 + + +class UseSidebarArchives(colander.SchemaNode): + name = 'use_sidebar_archives' + title = _(u'Show archives in sidebar') + description = _(u'Show categories sorted by date on which you can filter.') + missing = True + default = True + + +class SidebarArchivesNumber(colander.SchemaNode): + name = 'sidebar_archives_number' + title = _(u'The number of archives shown') + description = _(u'Choose how many archives are shown in the sidebar. ' + + u'Set to 0 to show all archives.') + default = 5 + + +class KottiBlogSidebarSettingsSchema(colander.MappingSchema): + use_sidebar_categories = UseSidebarCategories(colander.Boolean()) + sidebar_categories_number = SidebarCategoriesNumber(colander.Integer()) + use_sidebar_archives = UseSidebarArchives(colander.Boolean()) + sidebar_archives_number = SidebarArchivesNumber(colander.Integer()) + + +KottiBlogSidebarSettings = { + 'name': 'kotti_blog_sidebar_settings', + 'title': _(u'Blog sidebar settings'), + 'description': _(u"Settings for kotti_blog sidebar"), + 'success_message': _(u"Successfully saved blog sidebar settings."), + 'schema_factory': KottiBlogSidebarSettingsSchema, +} + + def populate_settings(): add_settings(KottiBlogSettings) + add_settings(KottiBlogSidebarSettings) diff --git a/kotti_blog/resources.py b/kotti_blog/resources.py index c70e33d..0c479bb 100644 --- a/kotti_blog/resources.py +++ b/kotti_blog/resources.py @@ -1,4 +1,5 @@ import datetime +from datetime import date from dateutil.tz import tzutc from sqlalchemy import Column from sqlalchemy import ForeignKey @@ -33,6 +34,38 @@ class Blog(Document): addable_to=[u'Document'], ) + def get_children_with_permission(self, request): + return self.children_with_permission(request) + + def get_unique_tags(self, request): + children = self.get_children_with_permission(request) + + unique_tags_dict = {} + for child in children: + for tag in child.tags: + try: + unique_tags_dict[tag] += 1 + except KeyError: + unique_tags_dict[tag] = 1 + + return sorted( + unique_tags_dict.items(), + key=lambda x: x[1], + reverse=True + ) + + def get_archives(self, request): + children = self.get_children_with_permission(request) + + dates_dict = {} + for child in children: + try: + dates_dict[date(child.date.year, child.date.month, 1)] += 1 + except KeyError: + dates_dict[date(child.date.year, child.date.month, 1)] = 1 + + return sorted(dates_dict.items(), reverse=True) + class BlogEntry(Document): id = Column(Integer, ForeignKey('documents.id'), primary_key=True) diff --git a/kotti_blog/static/style.css b/kotti_blog/static/style.css index e43a694..c7a0e66 100644 --- a/kotti_blog/static/style.css +++ b/kotti_blog/static/style.css @@ -1,9 +1,19 @@ .blog-view .blogentry { margin-top: 2em; } -.blogentry-view h1, .blog-view .blogentry h2 { border-top: 1px solid black; margin-top: 0.75em; padding-top: 0.5em; } +.blogentry-view h1 { + margin-top: 0.5em; +} +.blog-view .description, +.blogentry-view .description { + font-style: italic; +} +.blog-metadata { + margin-bottom: 20px; + border-bottom: 1px solid #EEE; +} diff --git a/kotti_blog/templates/blog-archives.pt b/kotti_blog/templates/blog-archives.pt new file mode 100644 index 0000000..8d5a0d8 --- /dev/null +++ b/kotti_blog/templates/blog-archives.pt @@ -0,0 +1,27 @@ + + + +
+ +

${context.title} archives

+

+ ${context.description} +

+

+ ${structure: context.body} +

+ +
+ +
+ +
+ diff --git a/kotti_blog/templates/blog-categories.pt b/kotti_blog/templates/blog-categories.pt new file mode 100644 index 0000000..54e235e --- /dev/null +++ b/kotti_blog/templates/blog-categories.pt @@ -0,0 +1,27 @@ + + + +
+ +

${context.title} categories

+

+ ${context.description} +

+

+ ${structure: context.body} +

+ +
+ +
+ +
+ diff --git a/kotti_blog/templates/blog-sidebar.pt b/kotti_blog/templates/blog-sidebar.pt new file mode 100644 index 0000000..e0f3484 --- /dev/null +++ b/kotti_blog/templates/blog-sidebar.pt @@ -0,0 +1,22 @@ +
+
+
Categories:
+ + +
+ +
+
Archives:
+ + +
+
+ diff --git a/kotti_blog/templates/blog-view.pt b/kotti_blog/templates/blog-view.pt index 243df80..4fb8c46 100644 --- a/kotti_blog/templates/blog-view.pt +++ b/kotti_blog/templates/blog-view.pt @@ -7,7 +7,7 @@
-

${api.context.title}

+

${api.context.title}

${api.context.description}

@@ -17,12 +17,19 @@
-

${item.title}

+

${structure: item.body}

diff --git a/kotti_blog/templates/blogentry-view.pt b/kotti_blog/templates/blogentry-view.pt index 964ead3..6c31a1d 100644 --- a/kotti_blog/templates/blogentry-view.pt +++ b/kotti_blog/templates/blogentry-view.pt @@ -8,8 +8,15 @@
-

${api.context.title}

+

${api.context.description}

diff --git a/kotti_blog/templates/macros.pt b/kotti_blog/templates/macros.pt index f9b8ddd..2c5a213 100644 --- a/kotti_blog/templates/macros.pt +++ b/kotti_blog/templates/macros.pt @@ -13,33 +13,33 @@ tal:condition="batch.multiple_pages">
  • - 1 + 1 ...
  • - +
  • Current page number
  • + tal:attributes="href string:${url}${url_parameters}${page}"/>
  • ... last page
  • diff --git a/kotti_blog/views.py b/kotti_blog/views.py index b2e37d1..b56afc4 100644 --- a/kotti_blog/views.py +++ b/kotti_blog/views.py @@ -1,26 +1,24 @@ -import datetime from dateutil.tz import tzutc - -import colander from deform.widget import DateTimeInputWidget +from pyramid.exceptions import PredicateMismatch from pyramid.renderers import get_renderer +from pyramid.view import render_view_to_response from pyramid.view import view_config from pyramid.view import view_defaults +import colander +import datetime - -from kotti import DBSession -from kotti.security import has_permission from kotti.views.edit import DocumentSchema from kotti.views.form import AddFormView from kotti.views.form import EditFormView +from kotti.views.slots import assign_slot from kotti.views.util import template_api - from kotti_settings.util import get_setting +from kotti_blog import _ from kotti_blog.batch import Batch from kotti_blog.resources import Blog from kotti_blog.resources import BlogEntry -from kotti_blog import _ class BlogSchema(DocumentSchema): @@ -77,29 +75,197 @@ def __init__(self, context, request): self.context = context self.request = request + self.selected_tag = self.request.GET.get("selected-tag") + self.selected_month = self.selected_year = None + selected_date = self.request.GET.get("selected-date") + + if selected_date: + self.selected_year, self.selected_month = map( + int, + selected_date.split('_') + ) + + def filter_blog_entry(self, entry): + # Filter on date + if self.selected_year and self.selected_month: + if(entry.date.year != self.selected_year or + entry.date.month != self.selected_month): + return False + + # Filter on the tag + if self.selected_tag: + if self.selected_tag not in entry.tags: + return False + + return True + + @view_config(context=Blog) + def view_blog_super(self): + """A super view for the blog, used to handle pagination. + """ + api = template_api(self.context, self.request) + + # Remove everything but the extra parameters + page = self.request.url.replace(api.url(self.context).strip('/'), '') + page = page.strip('/') + + # Pagination only works when specifying the view + if page.startswith('view/'): + page = page[5:] + + if page: + self.request.GET['page'] = page + + url_parameters_get = 'view/' + self.request.GET['url_parameters'] = url_parameters_get + + return render_view_to_response( + self.context, + self.request, + name='blog-view' + ) + @view_config(context=Blog, + name='blog-view', renderer='kotti_blog:templates/blog-view.pt') def view_blog(self): macros = get_renderer('templates/macros.pt').implementation() - query = DBSession.query(BlogEntry) - query = query.filter(BlogEntry.parent_id == self.context.id) - query = query.order_by(BlogEntry.date.desc()) - items = query.all() - items = [item for item in items if has_permission('view', item, self.request)] + + items = [child for child in + self.context.get_children_with_permission(self.request) if + self.filter_blog_entry(child)] + items.sort(key=lambda x: x.date, reverse=True) + page = self.request.params.get('page', 1) use_pagination = get_setting('use_pagination') if use_pagination: items = Batch.fromPagenumber(items, pagesize=get_setting('pagesize'), pagenumber=int(page)) + return { 'api': template_api(self.context, self.request), 'macros': macros, 'items': items, 'use_pagination': use_pagination, 'link_headline': get_setting('link_headline'), + 'url_parameters': self.request.GET.get("url_parameters") } + @view_config(context=Blog, + name="categories") + def view_categories_super(self): + """A super view that either shows the list or filters by the provided + category/tag, which we get from the URL. We use this so we can have + URL's like: + + blog/categories (shows the list) + blog/categories/tag1 (shows articles, filtered by tag 'tag1') + ... + + """ + api = template_api(self.context, self.request) + + # Remove everything but the extra parameters + url_parameters = self.request.url.replace( + api.url(self.context) + 'categories', + '' + ) + url_parameters = url_parameters.strip('/') + + # If we have url parameters, take first one as tag, second one as page, + # ignore the rest + if url_parameters: + url_parameters = url_parameters.split('/') + url_parameters_get = 'categories/' + try: + self.request.GET['selected-tag'] = url_parameters[0] + url_parameters_get += url_parameters[0] + '/' + self.request.GET['page'] = url_parameters[1] + except IndexError: + pass + + self.request.GET['url_parameters'] = url_parameters_get + + return render_view_to_response( + self.context, + self.request, + name='blog-view' + ) + + # If no tag was provided, return the categories list + return render_view_to_response( + self.context, + self.request, + name='categories-list' + ) + + @view_config(context=Blog, + name="categories-list", + renderer='kotti_blog:templates/blog-categories.pt') + def view_categories_list(self): + return { + 'api': template_api(self.context, self.request), + 'items': self.context.get_unique_tags(self.request) + } + + @view_config(context=Blog, + name="archives") + def view_archives_super(self): + """A super view that either shows the list or filters by the provided + category/tag, which we get from the URL. We use this so we can have + URL's like: + + blog/categories (shows the list) + blog/categories/tag1 (shows articles, filtered by tag 'tag1') + ... + + """ + api = template_api(self.context, self.request) + + # Remove everything but the extra parameters + url_parameters = self.request.url.replace( + api.url(self.context) + 'archives', + '' + ) + url_parameters = url_parameters.strip('/') + + # If we have url parameters, take first one as tag, second one as page, + # ignore the rest + if url_parameters: + url_parameters = url_parameters.split('/') + url_parameters_get = 'archives/' + try: + self.request.GET['selected-date'] = url_parameters[0] + url_parameters_get += url_parameters[0] + '/' + self.request.GET['page'] = url_parameters[1] + except IndexError: + pass + + self.request.GET['url_parameters'] = url_parameters_get + + return render_view_to_response( + self.context, + self.request, + name='blog-view' + ) + + # If no tag was provided, return the categories list + return render_view_to_response( + self.context, + self.request, + name='archives-list' + ) + + @view_config(context=Blog, + name="archives-list", + renderer='kotti_blog:templates/blog-archives.pt') + def view_archives_list(self): + return { + 'api': template_api(self.context, self.request), + 'items': self.context.get_archives(self.request) + } + @view_config(context=BlogEntry, renderer='kotti_blog:templates/blogentry-view.pt') def view_blogentry(self): @@ -113,6 +279,49 @@ def use_auto_pagination(context, request): return {'use_auto_pagination': get_setting('use_auto_pagination')} +@view_config(name="blog_sidebar", + renderer="kotti_blog:templates/blog-sidebar.pt") +def blog_sidebar_view(context, request): + # Only show sidebar on Blog and Blog Entries + if not (isinstance(context, Blog) or isinstance(context, BlogEntry)): + raise PredicateMismatch() + + # Find the blog + + if isinstance(context, Blog): + blog = context + else: + blog = context.parent + + api = template_api(context, request) + + use_categories = get_setting('use_sidebar_categories') + unique_tags = None + if use_categories: + number = get_setting('sidebar_categories_number') + if number: + unique_tags = blog.get_unique_tags(request)[:number] + else: + unique_tags = blog.get_unique_tags(request) + + use_archives = get_setting('use_sidebar_archives') + archives = None + if use_archives: + number = get_setting('sidebar_archives_number') + if number: + archives = blog.get_archives(request)[:number] + else: + archives = blog.get_archives(request) + + return { + 'blog_url': api.url(blog), + 'unique_tags': unique_tags, + 'use_categories': use_categories, + 'archives': archives, + 'use_archives': use_archives, + } + + def includeme_edit(config): config.add_view( @@ -155,4 +364,5 @@ def includeme(config): config.override_asset(to_override='kotti_blog', override_with=override) includeme_edit(config) config.add_static_view('static-kotti_blog', 'kotti_blog:static') + assign_slot('blog_sidebar', 'right') config.scan(__name__)