Branch: refs/heads/master Author: Enrico Tröger enrico.troeger@uvena.de Committer: Enrico Tröger enrico.troeger@uvena.de Date: Sun, 09 Aug 2015 11:13:20 UTC Commit: f68d9cba272a11b481ff29fe8ca2f97737b7fdbd https://github.com/geany/www.geany.org/commit/f68d9cba272a11b481ff29fe8ca2f9...
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() + + +#---------------------------------------------------------------------- +@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).