From ecbfad9bf54f1711737614f69aa5ca581ffb9d5e Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 4 Jun 2013 15:54:40 +0200 Subject: [PATCH] finish views and cms plugins --- portail_citoyen_announces/admin.py | 4 +- portail_citoyen_announces/app_settings.py | 15 ++- portail_citoyen_announces/cms_plugins.py | 79 +++++++++++++ portail_citoyen_announces/forms.py | 16 +-- portail_citoyen_announces/models.py | 14 ++- .../announce_homepage.html | 5 + .../announce_list.html | 12 ++ .../announce_list_plugin.html | 1 + .../form_plugin.html | 33 ++++++ .../subscription_edit.html | 26 ++++- portail_citoyen_announces/transports.py | 70 +++++++----- portail_citoyen_announces/urls.py | 10 ++ portail_citoyen_announces/views.py | 69 +++++++++++- portail_citoyen_announces/widgets.py | 104 ++++++++++++++++++ 14 files changed, 418 insertions(+), 40 deletions(-) create mode 100644 portail_citoyen_announces/cms_plugins.py create mode 100644 portail_citoyen_announces/templates/portail_citoyen_announces/announce_homepage.html create mode 100644 portail_citoyen_announces/templates/portail_citoyen_announces/announce_list.html create mode 100644 portail_citoyen_announces/templates/portail_citoyen_announces/announce_list_plugin.html create mode 100644 portail_citoyen_announces/templates/portail_citoyen_announces/form_plugin.html create mode 100644 portail_citoyen_announces/urls.py create mode 100644 portail_citoyen_announces/widgets.py diff --git a/portail_citoyen_announces/admin.py b/portail_citoyen_announces/admin.py index dd345ae..ba94b4f 100644 --- a/portail_citoyen_announces/admin.py +++ b/portail_citoyen_announces/admin.py @@ -5,7 +5,6 @@ from django.contrib.sites.models import get_current_site import models import transports -import app_settings from django.utils.translation import ugettext_lazy as _ @@ -31,7 +30,8 @@ class SendingAction(object): def transport_actions(): - return [SendingAction(mode) for mode in app_settings.transport_modes] + return [SendingAction(transport.identifier) + for transport in transports.get_transports() if hasattr(transport, 'send')] class SentInlineAdmin(admin.TabularInline): diff --git a/portail_citoyen_announces/app_settings.py b/portail_citoyen_announces/app_settings.py index fb3871b..092a14c 100644 --- a/portail_citoyen_announces/app_settings.py +++ b/portail_citoyen_announces/app_settings.py @@ -1,10 +1,19 @@ from django.conf import settings -default_transport_modes = { - 'email': 'portail_citoyen_announces.transports.EmailTransport', -} +default_transport_modes = ( + 'portail_citoyen_announces.transports.EmailTransport', + 'portail_citoyen_announces.transports.HomepageTransport', +) transport_modes = getattr(settings, 'ANNOUNCES_TRANSPORTS', default_transport_modes) default_from = getattr(settings, 'ANNOUNCES_DEFAULT_FROM_EMAIL', None) + +feed_title = getattr(settings, 'ANNOUNCES_FEED_TITLE', u'Announces') +feed_description = getattr(settings, 'ANNOUNCES_FEED_TITLE', + u'') +feed_link = getattr(settings, 'ANNOUNCES_FEED_LINK', '') +feed_item_link_template = getattr(settings, + 'ANNOUNCES_FEED_ITEM_LINK_TEMPLATE', '#announce-item-{0}') +feed_homepage_limit = getattr(settings, 'ANNOUNCE_FEED_HOMEPAGE_LIMIT', 10) diff --git a/portail_citoyen_announces/cms_plugins.py b/portail_citoyen_announces/cms_plugins.py new file mode 100644 index 0000000..cc7064f --- /dev/null +++ b/portail_citoyen_announces/cms_plugins.py @@ -0,0 +1,79 @@ +from cms.plugin_base import CMSPluginBase + + +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django.contrib.sites.models import get_current_site + + +from cms.plugin_pool import plugin_pool + + +import forms +import models +import transports +import app_settings + + +class AnnounceListPlugin(CMSPluginBase): + model = models.AnnounceListPlugin + name = _('Announce List Plugin') + render_template = "portail_citoyen_announces/announce_list_plugin.html" + text_enabled = True + transport_identifier = transports.HomepageTransport.identifier + + def get_queryset(self): + qs = models.Announce.objects.all() + qs = qs.filter(category__subscription__transport=self.transport_identifier) + if app_settings.feed_homepage_limit: + qs = qs[:app_settings.feed_homepage_limit] + return qs + + def render(self, context, instance, placeholder): + request = context['request'] + context['object_list'] = self.get_queryset() + context['id_prefix'] = 'announce-item-' + subscriptions = models.Subscription.objects.filter( + transport=self.transport_identifier, + user=request.user).select_related('category') + context['subscriptions'] = [ sub.category.name for sub in subscriptions ] + return context + + def icon_src(self, instance): + return settings.STATIC_URL + u"compte_meyzieu/meyzieu_newsletters_plugin.png" + +class FormPluginMixin(object): + form_class = None + success_msg = None + render_template = 'portail_citoyen_announces/form_plugin.html' + + def get_form_kwargs(self, context, instance, placeholder): + request = context['request'] + return dict(user=request.user, site=get_current_site(request)) + + def render(self, context, instance, placeholder): + request = context['request'] + context['submit'] = submit = 'cms-form-plugin-%s' % instance.id + if request.method == 'POST' and submit in request.POST: + form = self.form_class(data=request.POST, + **self.get_form_kwargs(context, instance, placeholder)) + if form.is_valid(): + form.save() + context['success'] = self.success_msg + else: + form = self.form_class(**self.get_form_kwargs(context, instance, + placeholder)) + context['form'] = form + return context + + +class AnnounceSubscribePlugin(FormPluginMixin, CMSPluginBase): + model = models.AnnounceSubscribePlugin + form_class = forms.SubscriptionForm + name = _('Announce Subscribe Plugin') + sucess_msg = _(u'Your subscriptions were saved') + text_enabled = True + + +plugin_pool.register_plugin(AnnounceSubscribePlugin) +plugin_pool.register_plugin(AnnounceListPlugin) diff --git a/portail_citoyen_announces/forms.py b/portail_citoyen_announces/forms.py index acae039..187574d 100644 --- a/portail_citoyen_announces/forms.py +++ b/portail_citoyen_announces/forms.py @@ -6,6 +6,7 @@ from django import forms import models import transports +import widgets class SubscriptionForm(forms.Form): @@ -16,18 +17,19 @@ class SubscriptionForm(forms.Form): self.subscriptions = models.Subscription.objects.filter(user=self.user) sub_by_category = defaultdict(lambda: []) for sub in self.subscriptions: - sub_by_category[sub].append(sub.transport) + sub_by_category[sub.category].append(sub.transport) super(SubscriptionForm, self).__init__(*args, **kwargs) - choices = transports.get_transport_choices() + self.choices = list(transports.get_transport_choices()) for category in self.categories: field = forms.MultipleChoiceField( - label=category.name, choices=choices, - widget=forms.CheckboxSelectMultiple, - initial=sub_by_category[category]) + label=category.name, choices=self.choices, + widget=widgets.CheckboxMultipleSelect, + initial=sub_by_category[category], + required=False) self.fields['category_%s' % category.identifier] = field def save(self): - cleaned_data = self.cleaned_data() + cleaned_data = self.cleaned_data wanted_subscriptions = set() for category in self.categories: field_id = 'category_%s' % category.identifier @@ -41,7 +43,7 @@ class SubscriptionForm(forms.Form): subscription.delete() # create or update new ones for category, transport in wanted_subscriptions: - models.SubscriptionForm.objects.get_or_create( + models.Subscription.objects.get_or_create( user=self.user, category=category, transport=transport) diff --git a/portail_citoyen_announces/models.py b/portail_citoyen_announces/models.py index 1141eb7..1c1ede0 100644 --- a/portail_citoyen_announces/models.py +++ b/portail_citoyen_announces/models.py @@ -2,6 +2,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.core.validators import RegexValidator +from django.utils import timezone from model_utils.models import TimeStampedModel @@ -41,7 +42,7 @@ class Announce(models.Model): hidden = models.BooleanField(_('hidden'), blank=True, default=False) publication_time = models.DateTimeField(_('publication time'), blank=True, - null=True) + null=True, default=timezone.now) expiration_time = models.DateTimeField(_('expiration time'), blank=True, null=True) creation_time = models.DateTimeField(_('creation time'), auto_now_add=True) @@ -96,3 +97,14 @@ class Subscription(TimeStampedModel): class Meta: verbose_name = _('subscription') ordering = ('-created',) + +try: + from cms.models import CMSPlugin +except: + pass +else: + class AnnounceListPlugin(CMSPlugin): + pass + + class AnnounceSubscribePlugin(CMSPlugin): + pass diff --git a/portail_citoyen_announces/templates/portail_citoyen_announces/announce_homepage.html b/portail_citoyen_announces/templates/portail_citoyen_announces/announce_homepage.html new file mode 100644 index 0000000..519dc8a --- /dev/null +++ b/portail_citoyen_announces/templates/portail_citoyen_announces/announce_homepage.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +{% include "portail_citoyen_announces/announce_list.html" %} +{% endblock %} diff --git a/portail_citoyen_announces/templates/portail_citoyen_announces/announce_list.html b/portail_citoyen_announces/templates/portail_citoyen_announces/announce_list.html new file mode 100644 index 0000000..0db6ec7 --- /dev/null +++ b/portail_citoyen_announces/templates/portail_citoyen_announces/announce_list.html @@ -0,0 +1,12 @@ +{% load i18n %} +
+ {% for announce in object_list %} +
+ {{ announce.title }} +
+ {% if announce.text %} +
{{ announce.text }}
+ {% endif %} + {% endfor %} +
+

{% trans "Subscriptions" %}: {{ subscriptions|join:", " }}

diff --git a/portail_citoyen_announces/templates/portail_citoyen_announces/announce_list_plugin.html b/portail_citoyen_announces/templates/portail_citoyen_announces/announce_list_plugin.html new file mode 100644 index 0000000..d428985 --- /dev/null +++ b/portail_citoyen_announces/templates/portail_citoyen_announces/announce_list_plugin.html @@ -0,0 +1 @@ +{% include "portail_citoyen_announces/announce_list.html" %} diff --git a/portail_citoyen_announces/templates/portail_citoyen_announces/form_plugin.html b/portail_citoyen_announces/templates/portail_citoyen_announces/form_plugin.html new file mode 100644 index 0000000..ad86acb --- /dev/null +++ b/portail_citoyen_announces/templates/portail_citoyen_announces/form_plugin.html @@ -0,0 +1,33 @@ +{% load i18n %} + +
+ {% csrf_token %} + {{ form.non_field_errors }} + + + + + {% for choice_key, choice_name in form.choices %} + + {% endfor %} + + + + {% for field in form %} + + + {% for subwidget in field %} + + {% endfor %} + + {% if field.error %} + + {% endif %} + {% endfor %} + +
{% trans "Catégorie" %}{{ choice_name }}
{{ field.label }}{{ subwidget.tag }}
{{ field.error }}
+ +
+{% if success_msg %} +

{{ success_msg }}

+{% endif %} diff --git a/portail_citoyen_announces/templates/portail_citoyen_announces/subscription_edit.html b/portail_citoyen_announces/templates/portail_citoyen_announces/subscription_edit.html index 121b7d7..2827e2c 100644 --- a/portail_citoyen_announces/templates/portail_citoyen_announces/subscription_edit.html +++ b/portail_citoyen_announces/templates/portail_citoyen_announces/subscription_edit.html @@ -3,7 +3,31 @@ {% block content %}
- {{ form }} + {% csrf_token %} + {{ form.non_field_errors }} + + + + + {% for choice_key, choice_name in form.choices %} + + {% endfor %} + + + + {% for field in form %} + + + {% for subwidget in field %} + + {% endfor %} + + {% if field.error %} + + {% endif %} + {% endfor %} + +
{% trans "Catégorie" %}{{ choice_name }}
{{ field.label }}{{ subwidget.tag }}
{{ field.error }}
{% endblock %} diff --git a/portail_citoyen_announces/transports.py b/portail_citoyen_announces/transports.py index 838823e..d3830ee 100644 --- a/portail_citoyen_announces/transports.py +++ b/portail_citoyen_announces/transports.py @@ -15,31 +15,41 @@ import models logger = logging.getLogger() -__transport_choices = None - -def get_transport_choices(): - global __transport_choices - if __transport_choices is None: - __transport_choices = [] - for mode in app_settings.transport_modes: - transport = get_transport(mode) - identifier = transport.identifier - display_name = getattr(transport, 'display_name', identifier) - __transport_choices.append((identifier, display_name)) - return __transport_choices +def get_transport_choices(include=[], exclude=[]): + for transport in get_transports(): + if include and transport.identifer not in include: + continue + if exclude and transport.identifer in exclude: + continue + for identifier, display_name in transport.get_choices(): + yield (identifier, display_name) -def get_transport(mode): - if mode not in app_settings.transport_modes: - return None - module_path, class_name = app_settings.transport_modes[mode].rsplit('.', 1) - try: - module = import_module(module_path) - return getattr(module, class_name)() - except (ImportError, AttributeError): - logger.error('unable to load transport class %s.%s', - module_path, class_name) - return None +def get_transport(identifier): + transports = get_transports() + for transport in transports: + if identifier == transport.identifier: + return transport + return None + + +__TRANSPORTS = None + + +def get_transports(): + global __TRANSPORTS + + if __TRANSPORTS is None: + transports = [] + for class_path in app_settings.transport_modes: + module_path, class_name = class_path.rsplit('.', 1) + try: + module = import_module(module_path) + transports.append(getattr(module, class_name)()) + except (ImportError, AttributeError), e: + raise ImportError('Unable to load transport class %s' % class_path, e) + __TRANSPORTS = transports + return __TRANSPORTS def get_template_list(template_list, category): @@ -53,7 +63,16 @@ def get_template(template_list, category): return select_template(template_list) +class HomepageTransport(object): + identifier = 'homepage' + + def get_choices(self): + return (('homepage', _('homepage')),) + + class EmailTransport(object): + identifier = 'email' + subject_template_list = [ 'portail_citoyen_announces/email/subject_{category}.txt', 'portail_citoyen_announces/email/subject.txt', @@ -67,8 +86,9 @@ class EmailTransport(object): 'portail_citoyen_announces/body_{category}.txt', 'portail_citoyen_announces/body.txt', ] - identifier = 'email' - display_name = _('email') + + def get_choices(self): + return (('email', _('email')),) def get_subscriptions(self, category): return models.Subscription.objects.filter(category=category, diff --git a/portail_citoyen_announces/urls.py b/portail_citoyen_announces/urls.py new file mode 100644 index 0000000..6b702f9 --- /dev/null +++ b/portail_citoyen_announces/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import patterns, include, url + +urlpatterns = patterns('', + url(r'^$', 'portail_citoyen_announces.views.homepage_view', + name='announce-homepage'), + url(r'^subscribe/$', 'portail_citoyen_announces.views.subscription_view', + name='announce-subscribe'), + url(r'^feed/$', 'portail_citoyen_announces.views.feed', + name='announce-feed'), +) diff --git a/portail_citoyen_announces/views.py b/portail_citoyen_announces/views.py index 7e3075c..698ee38 100644 --- a/portail_citoyen_announces/views.py +++ b/portail_citoyen_announces/views.py @@ -1,7 +1,16 @@ from django.views.generic.edit import FormView +from django.views.generic.list import ListView +from django.contrib.syndication.views import Feed +from django.contrib.sites.models import get_current_site + + +from django.utils.feedgenerator import Atom1Feed import forms +import models +import transports +import app_settings class SubscriptionView(FormView): @@ -9,9 +18,67 @@ class SubscriptionView(FormView): form_class = forms.SubscriptionForm success_url = '..' + def get_form_kwargs(self, **kwargs): + kwargs = super(SubscriptionView, self).get_form_kwargs(**kwargs) + kwargs['user'] = self.request.user + kwargs['site'] = get_current_site(self.request) + return kwargs + def form_valid(self, form): form.save() - return super(SubscriptionView, self).form_valid(self, form) + return super(SubscriptionView, self).form_valid(form) subscription_view = SubscriptionView.as_view() + +class AnnounceHomepageView(ListView): + model = models.Announce + template_name = 'portail_citoyen_announces/announce_homepage.html' + transport_identifier = transports.HomepageTransport.identifier + + def get_queryset(self): + qs = models.Announce.objects.all() + qs = qs.filter(category__subscription__transport=self.transport_identifier) + if app_settings.feed_homepage_limit: + qs = qs[:app_settings.feed_homepage_limit] + return qs + + def get_context_data(self, **kwargs): + ctx = super(AnnounceHomepageView, self).get_context_data(**kwargs) + ctx['id_prefix'] = 'announce-item-' + subscriptions = models.Subscription.objects.filter( + transport=self.transport_identifier, + user=self.request.user).select_related('category') + ctx['subscriptions'] = [ sub.category.name for sub in subscriptions ] + return ctx + +homepage_view = AnnounceHomepageView.as_view() + + +class AnnounceFeed(Feed): + title = app_settings.feed_title + description = app_settings.feed_description + link = app_settings.feed_link + feed_item_link_template = app_settings.feed_item_link_template + feed_type = Atom1Feed + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def items(self): + return models.Announce.objects.order_by('-publication_time') + + def item_title(self, item): + return item.title + + def item_description(self, item): + return item.text + + def item_link(self, item): + return self.feed_item_link_template.format(item.pk) + + def item_pubdate(self, item): + return item.publication_time or item.modification_time + + +feed = AnnounceFeed() diff --git a/portail_citoyen_announces/widgets.py b/portail_citoyen_announces/widgets.py new file mode 100644 index 0000000..30a7262 --- /dev/null +++ b/portail_citoyen_announces/widgets.py @@ -0,0 +1,104 @@ +from itertools import chain + +from django.forms.widgets import SubWidget, SelectMultiple +from django.forms.util import flatatt +from django.utils.html import conditional_escape +from django.utils.encoding import StrAndUnicode, force_unicode +from django.utils.safestring import mark_safe + +class CheckboxInput(SubWidget): + """ + An object used by CheckboxRenderer that represents a single + . + """ + def __init__(self, name, value, attrs, choice, index): + self.name, self.value = name, value + self.attrs = attrs + self.choice_value = force_unicode(choice[0]) + self.choice_label = force_unicode(choice[1]) + self.index = index + + def __unicode__(self): + return self.render() + + def render(self, name=None, value=None, attrs=None, choices=()): + name = name or self.name + value = value or self.value + attrs = attrs or self.attrs + + if 'id' in self.attrs: + label_for = ' for="%s_%s"' % (self.attrs['id'], self.index) + else: + label_for = '' + choice_label = conditional_escape(force_unicode(self.choice_label)) + return mark_safe(u'%s %s' % (label_for, self.tag(), choice_label)) + + def is_checked(self): + return self.choice_value in self.value + + def tag(self): + if 'id' in self.attrs: + self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) + final_attrs = dict(self.attrs, type='checkbox', name=self.name, value=self.choice_value) + if self.is_checked(): + final_attrs['checked'] = 'checked' + return mark_safe(u'' % flatatt(final_attrs)) + +class CheckboxRenderer(StrAndUnicode): + def __init__(self, name, value, attrs, choices): + self.name, self.value, self.attrs = name, value, attrs + self.choices = choices + + def __iter__(self): + for i, choice in enumerate(self.choices): + yield CheckboxInput(self.name, self.value, self.attrs.copy(), choice, i) + + def __getitem__(self, idx): + choice = self.choices[idx] # Let the IndexError propogate + return CheckboxInput(self.name, self.value, self.attrs.copy(), choice, idx) + + def __unicode__(self): + return self.render() + + def render(self): + """Outputs a
    for this set of checkbox fields.""" + return mark_safe(u'
      \n%s\n
    ' % u'\n'.join([u'
  • %s
  • ' + % force_unicode(w) for w in self])) + +class CheckboxMultipleSelect(SelectMultiple): + """ + Checkbox multi select field that enables iteration of each checkbox + Similar to django.forms.widgets.RadioSelect + """ + renderer = CheckboxRenderer + + def __init__(self, *args, **kwargs): + # Override the default renderer if we were passed one. + renderer = kwargs.pop('renderer', None) + if renderer: + self.renderer = renderer + super(CheckboxMultipleSelect, self).__init__(*args, **kwargs) + + def subwidgets(self, name, value, attrs=None, choices=()): + for widget in self.get_renderer(name, value, attrs, choices): + yield widget + + def get_renderer(self, name, value, attrs=None, choices=()): + """Returns an instance of the renderer.""" + if value is None: value = '' + str_values = set([force_unicode(v) for v in value]) # Normalize to string. + if attrs is None: + attrs = {} + if 'id' not in attrs: + attrs['id'] = name + final_attrs = self.build_attrs(attrs) + choices = list(chain(self.choices, choices)) + return self.renderer(name, str_values, final_attrs, choices) + + def render(self, name, value, attrs=None, choices=()): + return self.get_renderer(name, value, attrs, choices).render() + + def id_for_label(self, id_): + if id_: + id_ += '_0' + return id_