[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