[geany/www.geany.org] f68d9c: Add simple News app for Geany project news posts

Enrico Tröger git-noreply at xxxxx
Sun Aug 9 11:13:20 UTC 2015


Branch:      refs/heads/master
Author:      Enrico Tröger <enrico.troeger at uvena.de>
Committer:   Enrico Tröger <enrico.troeger at uvena.de>
Date:        Sun, 09 Aug 2015 11:13:20 UTC
Commit:      f68d9cba272a11b481ff29fe8ca2f97737b7fdbd
             https://github.com/geany/www.geany.org/commit/f68d9cba272a11b481ff29fe8ca2f97737b7fdbd

Log Message:
-----------
Add simple News app for Geany project news posts


Modified Paths:
--------------
    news/__init__.py
    news/admin.py
    news/feeds.py
    news/models.py
    news/sitemaps.py
    news/static/css/newspost.css
    news/templates/news/detail.html
    news/templates/news/list.html
    news/templates/news/list_embedded.html
    news/templatetags/__init__.py
    news/templatetags/news_tags.py
    news/urls.py
    news/views.py

Modified: news/__init__.py
0 lines changed, 0 insertions(+), 0 deletions(-)
===================================================================
No diff available, check online


Modified: news/admin.py
102 lines changed, 102 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.conf.urls import patterns, url
+from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.templatetags.static import static
+from mezzanine.core.models import CONTENT_STATUS_DRAFT, CONTENT_STATUS_PUBLISHED
+from news.models import NewsPost
+
+
+########################################################################
+class NewsPostAdmin(admin.ModelAdmin):
+    list_display = ('title', 'user', '_is_published_switch', 'publish_date')
+    date_hierarchy = 'publish_date'
+    list_filter = ('publish_date', 'status')
+    exclude = ('slug', 'user', 'entry_date')
+    actions = ['_toggle_many_published']
+
+    #----------------------------------------------------------------------
+    def save_model(self, request, obj, form, change):
+        if not obj.user_id:
+            # set logged in user as author
+            obj.user = request.user
+        obj.save()
+
+    #----------------------------------------------------------------------
+    def get_urls(self):
+        urls = admin.ModelAdmin.get_urls(self)
+        my_urls = patterns(
+            '',
+            url(
+                r'^toggle_published/([0-9]+)/$',
+                self.admin_site.admin_view(self._toggle_published),
+                name='news_post_toggle_published'),)
+        return my_urls + urls
+
+    #----------------------------------------------------------------------
+    def _is_published_switch(self, obj):
+        toggle_published_url = reverse('admin:news_post_toggle_published', args=(obj.id,))
+        yes_no = 'yes' if obj.status == CONTENT_STATUS_PUBLISHED else 'no'
+        static_path = static('admin/img/icon-{}.gif'.format(yes_no))
+        value = obj.status
+        return '<a href="{}"><img src="{}" alt="{}"/></a>'.format(
+            toggle_published_url,
+            static_path,
+            value)
+
+    #----------------------------------------------------------------------
+    def _toggle_published(self, request, newspost_id):
+        newspost = NewsPost.objects.get(pk=newspost_id)
+        self._toggle_newspost_published_status(newspost)
+        self.message_user(
+            request,
+            u'News post public status have been changed.',
+            fail_silently=True)
+        # redirect back to the changelist page
+        changelist_url = self._admin_url('changelist')
+        return HttpResponseRedirect(changelist_url)
+
+    #----------------------------------------------------------------------
+    def _toggle_newspost_published_status(self, newspost):
+        if newspost.status == CONTENT_STATUS_PUBLISHED:
+            newspost.status = CONTENT_STATUS_DRAFT
+        else:
+            newspost.status = CONTENT_STATUS_PUBLISHED
+        newspost.save()
+
+    #----------------------------------------------------------------------
+    def _admin_url(self, target_url):
+        opts = self.model._meta
+        url_ = "admin:%s_%s_%s" % (opts.app_label, opts.object_name.lower(), target_url)
+        return reverse(url_)
+
+    #----------------------------------------------------------------------
+    def _toggle_many_published(self, request, queryset):
+        # this is not really as efficient as it could be as the query is performed, but I don't know
+        # a way to get the primary keys in the queryset without executing it
+        rows_updated = 0
+        for newspost in queryset:
+            self._toggle_newspost_published_status(newspost)
+            rows_updated += 1
+        self.message_user(request, "{} News posts were successfully changed.".format(rows_updated))
+
+    _is_published_switch.allow_tags = True
+    _is_published_switch.short_description = 'Is published'
+    _toggle_many_published.short_description = "Toggle the published status of selected News posts"
+
+
+admin.site.register(NewsPost, NewsPostAdmin)


Modified: news/feeds.py
55 lines changed, 55 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,55 @@
+# coding: utf-8
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.contrib.syndication.views import Feed
+from django.core.urlresolvers import reverse
+from mezzanine.core.templatetags.mezzanine_tags import richtext_filters
+from mezzanine.utils.html import absolute_urls
+from mezzanine.conf import settings
+from news.models import NewsPost
+
+
+########################################################################
+class LatestNewsPostsFeed(Feed):
+
+    title = "Geany project news"
+    description = "News feed for the Geany project"
+
+    #----------------------------------------------------------------------
+    def link(self):
+        return reverse("news_list")
+
+    #----------------------------------------------------------------------
+    def items(self):
+        return NewsPost.objects.recently_published(count=20)
+
+    #----------------------------------------------------------------------
+    def item_title(self, item):
+        return item.title
+
+    #----------------------------------------------------------------------
+    def item_description(self, item):
+        description = richtext_filters(item.content)
+        absolute_urls_name = "mezzanine.utils.html.absolute_urls"
+        if absolute_urls_name not in settings.RICHTEXT_FILTERS:
+            description = absolute_urls(description)
+        return description
+
+    #----------------------------------------------------------------------
+    def item_pubdate(self, item):
+        return item.publish_date
+
+    #----------------------------------------------------------------------
+    def item_author_name(self, item):
+        return item.user.get_full_name() or item.user.username


Modified: news/models.py
101 lines changed, 101 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.core.urlresolvers import reverse
+from django.db import models
+from django.utils import timezone
+from django.utils.timezone import now
+from django.utils.translation import ugettext_lazy as _
+from mezzanine.core.fields import RichTextField
+from mezzanine.core.models import CONTENT_STATUS_CHOICES, CONTENT_STATUS_PUBLISHED
+from mezzanine.utils.models import get_user_model_name
+from mezzanine.utils.urls import slugify
+
+
+########################################################################
+class PublishedManager(models.Manager):
+    """
+    Provides filter for restricting items returned by status and
+    publish date when the given user is not a staff member.
+    """
+    # this is a clone of mezzanine.core.managers.PublishedManager but with the
+    # 'expiry_date' field removed
+
+    #----------------------------------------------------------------------
+    def published(self, for_user=None):
+        """
+        For non-staff users, return items with a published status and
+        whose publish and expiry dates fall before and after the
+        current date when specified.
+        """
+        if for_user is not None and for_user.is_staff:
+            return self.all()
+        return self.filter(
+            models.Q(publish_date__lte=now()) | models.Q(publish_date__isnull=True),
+            models.Q(status=CONTENT_STATUS_PUBLISHED))
+
+    #----------------------------------------------------------------------
+    def recently_published(self, count=5, for_user=None):
+        return self.published(for_user).order_by('-publish_date')[:count]
+
+
+########################################################################
+class NewsPost(models.Model):
+
+    slug = models.CharField(_('Slug'), max_length=255, editable=False, db_index=True)
+    title = models.CharField(_(u'Title'), max_length=255, blank=True)
+    content = RichTextField(_('Content'))
+    user = models.ForeignKey(
+        get_user_model_name(),
+        verbose_name=_('Author'),
+        related_name='%(class)ss')
+    status = models.IntegerField(
+        _('Status'),
+        choices=CONTENT_STATUS_CHOICES,
+        default=CONTENT_STATUS_PUBLISHED,
+        db_index=True,
+        help_text=_('With Draft chosen, will only be shown for admin users on the site.'))
+    entry_date = models.DateTimeField(
+        _(u'Published'),
+        editable=False,
+        auto_now_add=True,
+        db_index=True)
+    publish_date = models.DateTimeField(
+        _(u'Published on'),
+        blank=True,
+        db_index=True,
+        default=timezone.now)
+
+    # add a 'published' method to the Manager to filter by published status
+    objects = PublishedManager()
+
+    ########################################################################
+    class Meta:
+        ordering = ('-publish_date',)
+        verbose_name = _(u'News')
+        verbose_name_plural = _(u'News')
+
+    #----------------------------------------------------------------------
+    def save(self, *args, **kwargs):
+        if not self.slug:
+            self.slug = slugify(self.title)
+        super(NewsPost, self).save(*args, **kwargs)
+
+    #----------------------------------------------------------------------
+    def get_absolute_url(self):
+        return reverse('news_detail', kwargs={'newspost_slug': self.slug})
+
+    #----------------------------------------------------------------------
+    def __unicode__(self):
+        return self.title


Modified: news/sitemaps.py
25 lines changed, 25 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,25 @@
+# coding: utf-8
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from geany.sitemaps import StaticSitemap
+from news.models import NewsPost
+
+
+########################################################################
+class NewsPostSitemap(StaticSitemap):
+    """Return the static sitemap items + the last five most recent News posts"""
+
+    #----------------------------------------------------------------------
+    def get_dynamic_items(self):
+        return list(NewsPost.objects.recently_published(count=10))


Modified: news/static/css/newspost.css
5 lines changed, 5 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,5 @@
+
+.list-group-item {
+    margin-top: 5px;
+    margin-bottom: 5px;
+}


Modified: news/templates/news/detail.html
31 lines changed, 31 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,31 @@
+{% extends "pages/richtextpage.html" %}
+{% load i18n mezzanine_tags  %}
+
+{% block extra_css %}
+{{ super }}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/newspost.css">
+{% endblock %}
+
+
+{% block meta_title %}{{ newspost.title }}{% endblock %}
+
+{% block meta_description %}{% metablock %}
+{{ newspost.content|striptags|truncatewords:15 }}
+{% endmetablock %}{% endblock %}
+
+
+{% block richtext_content %}
+
+<div>
+    <div class="pull-left">
+        <h2>{{ newspost.title }}</h2>
+    </div>
+    <div class="pull-right">
+        {{ newspost.publish_date|date:"F dS, Y" }}
+    </div>
+    <div class="clearfix"></div>
+</div>
+
+{{ newspost.content|richtext_filter|safe }}
+
+{% endblock %}


Modified: news/templates/news/list.html
43 lines changed, 43 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,43 @@
+{% extends "pages/richtextpage.html" %}
+{% load i18n mezzanine_tags  %}
+
+{% block extra_css %}
+{{ super }}
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/newspost.css">
+{% endblock %}
+
+
+{% block meta_title %}{% if page %}{{ page.richtextpage.meta_title }}{% else %}{% trans "News" %}{% endif %}{% endblock %}
+
+{% block meta_description %}{% metablock %}
+Latest news of the Geany project.
+{% endmetablock %}{% endblock %}
+
+
+{% block richtext_content %}
+
+<h2>News</h2>
+
+<div class="list-group">
+{% for newspost in object_list %}
+    <div class="list-group-item">
+        <div class="list-group-item-heading">
+            <div class="pull-left">
+                <h4>
+                    <a href="{{ newspost.get_absolute_url }}">{{ newspost.title }}</a>
+                </h4>
+            </div>
+            <div class="pull-right">
+                {{ newspost.publish_date|date:"F dS, Y" }}
+            </div>
+            <div class="clearfix"></div>
+        </div>
+        <div class="list-group-item-text">
+            {{ newspost.content|striptags|truncatechars:75 }}
+            <a href="{{ newspost.get_absolute_url }}" class="small">more</a>
+        </div>
+    </div>
+{% endfor %}
+</div>
+
+{% endblock %}


Modified: news/templates/news/list_embedded.html
105 lines changed, 105 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,105 @@
+{% load static from staticfiles %}
+
+<ul class="list-group">
+{% for newspost in recent_news_posts %}
+    <li class="list-group-item">
+        <span class="glyphicon glyphicon-calendar" aria-hidden="true"></span>
+        <a href="#" data-toggle="modal" data-target="#news-modal" id="news-post-{{ newspost.slug }}">
+            {{ newspost.title }}
+        </a>
+        <span class="small">- {{ newspost.publish_date|date:"F Y" }}</span>
+    </li>
+{% endfor %}
+</ul>
+
+{% comment %}
+<ul class="list-group">
+{% for newspost in recent_news_posts %}
+    <li class="list-group-item">
+        <a href="#" data-toggle="modal" data-target="#news-modal" id="news-post-{{ newspost.slug }}">
+            {{ newspost.title }}
+        </a>
+    </li>
+{% endfor %}
+</ul>
+{% endcomment %}
+
+<!-- Modal -->
+<div class="modal fade" id="news-modal" tabindex="-1" role="dialog" aria-labelledby="news-modal-label">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+            <span aria-hidden="true">×</span>
+        </button>
+        <h4 class="modal-title" id="news-modal-title">title-dummy</h4>
+      </div>
+      <div class="modal-body" id="news-modal-body">
+        body-dummy
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+      </div>
+    </div>
+  </div>
+</div>
+
+
+<script>
+function fetch_newspost(newspost_slug) {
+    $("#news-modal").modal();
+    $("#news-modal-title").html("News loading...");
+    $("#news-modal-body").html("<p><img src=\"{% static 'mezzanine/img/loadingAnimation.gif' %}\"></p>");
+    $.ajax({
+        {# slightly hacky: we use the base URL pattern but add a fake slug to trick Django into the "news_detail" URL pattern #}
+        url: "{% url 'news_list' %}json/",
+        dataType: "json",
+        timeout: 30 * 1000,
+        data: {newspost_slug: newspost_slug},
+        type: "POST",
+        crossDomain: false,
+        success: function (newspost) {
+            var result_html;
+            var title;
+            if (newspost.error == null) {
+                title = '';
+                title += '<div>';
+                title += '<div class="pull-right small">';
+                title += newspost.publish_date;
+                title += ' </div>';
+                title += newspost.title;
+                title += '</div>';
+                result_html = newspost.content;
+            } else {
+                title = 'Error';
+                result_html = newspost.error;
+            }
+            $("#news-modal-body").html(result_html);
+            $("#news-modal-title").html(title);
+        },
+        error: function (jqxhr, text_status, error) {
+            var result_html;
+            if (error === "timeout") {
+                result_html = "<p><strong>An error occurred: " + text_status + "</strong></p>";
+            } else {
+                result_html = "<p><strong>An error occurred: " + text_status + ": " + error + "</strong></p>";
+                result_html += "<p><pre>" + jqxhr.responseText + "</pre></p>";
+            }
+            $("#news-modal-title").html('Error');
+            $("#news-modal-body").html(result_html);
+        }
+    });
+}
+
+function show_modal_for_news() {
+    var newspost_slug;
+    // strip off the prefix "news-post-"
+    newspost_slug = this.id.substring(10);
+    fetch_newspost(newspost_slug);
+    return false;
+}
+
+$(document).ready(function () {
+    $("[id^=news-post-]").click(show_modal_for_news);
+});
+</script>


Modified: news/templatetags/__init__.py
0 lines changed, 0 insertions(+), 0 deletions(-)
===================================================================
No diff available, check online


Modified: news/templatetags/news_tags.py
26 lines changed, 26 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django import template
+from news.models import NewsPost
+
+register = template.Library()
+
+
+#----------------------------------------------------------------------
+ at register.inclusion_tag("news/list_embedded.html", takes_context=True)
+def get_recent_news(context):
+    user = context.request.user
+    context["recent_news_posts"] = NewsPost.objects.recently_published(for_user=user)
+    return context


Modified: news/urls.py
29 lines changed, 29 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,29 @@
+# coding: utf-8
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.conf.urls import url
+from geany.sitemaps import sitemap_registry
+from news.feeds import LatestNewsPostsFeed
+from news.sitemaps import NewsPostSitemap
+from news.views import NewsListView, NewsDetailView
+
+
+urlpatterns = (
+    url(r'^$', NewsListView.as_view(), name='news_list'),
+    url(r'^feed/$', LatestNewsPostsFeed(), name='news_feed'),
+    url(r'^(?P<newspost_slug>.+)$', NewsDetailView.as_view(), name='news_detail'),
+)
+
+# register our urlpatterns to the global sitemap generator
+sitemap_registry.add(NewsPostSitemap, urlpatterns)


Modified: news/views.py
85 lines changed, 85 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.http import JsonResponse
+from django.shortcuts import render, get_object_or_404
+from django.template.defaultfilters import date, safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import DetailView, ListView, View
+from mezzanine.core.templatetags.mezzanine_tags import richtext_filters
+from news.models import NewsPost
+
+
+########################################################################
+class NewsPostPublishedMixin(object):
+
+    #----------------------------------------------------------------------
+    def get_queryset(self):
+        """ filter non-published news posts except for staff users """
+        user = self.request.user
+        return NewsPost.objects.\
+            published(for_user=user).\
+            order_by('-publish_date')
+
+
+########################################################################
+class NewsListView(NewsPostPublishedMixin, ListView):
+
+    model = NewsPost
+    template_name = 'news/list.html'
+
+
+########################################################################
+class NewsDetailView2(NewsPostPublishedMixin, DetailView):
+
+    model = NewsPost
+    template_name = 'news/detail.html'
+
+
+########################################################################
+class NewsDetailView(NewsPostPublishedMixin, View):
+    template_name = 'news/detail.html'
+
+    #----------------------------------------------------------------------
+    @method_decorator(csrf_exempt)
+    def dispatch(self, *args, **kwargs):
+        return super(NewsDetailView, self).dispatch(*args, **kwargs)
+
+    #----------------------------------------------------------------------
+    def get(self, request, newspost_slug):
+        newspost = get_object_or_404(NewsPost, slug=newspost_slug)
+        return render(request, self.template_name, {'newspost': newspost})
+
+    #----------------------------------------------------------------------
+    def post(self, request, *args, **kwargs):
+        newspost_slug = request.POST.get('newspost_slug')
+        try:
+            newspost = NewsPost.objects.get(slug=newspost_slug)
+        except NewsPost.DoesNotExist:
+            error_message = u'News post item for "{}" could not be found'.format(newspost_slug)
+            result = dict(error=error_message)
+        else:
+            # adapt to dict
+            user_name = newspost.user.get_full_name()
+            publish_date = date(newspost.publish_date, 'F dS, Y')
+            content = safe(richtext_filters(newspost.content))
+            result = dict(
+                error=None,
+                title=newspost.title,
+                content=content,
+                user=user_name,
+                publish_date=publish_date)
+
+        return JsonResponse(result, safe=True)



--------------
This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).


More information about the Commits mailing list