Permalink
Browse files

renewal workflow implemented

  • Loading branch information...
1 parent b875372 commit 3dc0b15f4f4a7df0e8a3c4c95f6be81a68f8e670 @guglielmo guglielmo committed Feb 13, 2012
View
@@ -17,3 +17,6 @@ urls_local.py
urls_prod.py
apache
site-packages
+
+soci.sql
+transfer_dati_soci.py
View
@@ -1,17 +1,63 @@
from django.contrib import admin
from op_associazione.models import Membership, Associate, Citizen, Organization, Politician
-class MembershipInline(admin.StackedInline):
+
+#override of the InlineModelAdmin to support the link in the tabular inline
+class TabularLinkedInline(admin.options.InlineModelAdmin):
+ template = "admin/edit_inline/tabular_linked.html"
+ admin_model_path = None
+
+ def __init__(self, *args):
+ super(TabularLinkedInline, self).__init__(*args)
+ if self.admin_model_path is None:
+ self.admin_model_path = self.model.__name__.lower()
+
+
+
+class MembershipInline(TabularLinkedInline):
+ fields = ['associate', 'type_of_membership', 'fee', 'payed', 'payed_at', 'expire_at', 'sent_card_at', 'is_active', 'public_subscription', 'created_at', 'updated_at']
+ readonly_fields = ['associate', 'fee', 'type_of_membership', 'created_at', 'updated_at']
model = Membership
extra = 0
+class MembershipAdmin(admin.ModelAdmin):
+ fields = ['associate', 'type_of_membership', 'fee', 'payed', 'payed_at', 'expire_at', 'sent_card_at', 'is_active', 'notes', 'public_subscription', 'created_at', 'updated_at']
+ readonly_fields = ['associate', 'fee', 'type_of_membership', 'created_at', 'updated_at']
+ search_fields = ('associate__last_name', 'associate__firs_name', 'associate__email')
+ list_filter = ('is_active', 'expire_at', 'payed_at')
+ list_display = ('associate', 'created_at', 'payed_at', 'expire_at', 'is_active')
+
+ def notify(self, request, queryset):
+ for obj in queryset:
+ obj.notify()
+ notify.short_description = "Notifica disattivazione e rinnovo via email"
+
+ actions = [notify]
+
+
class AssociateAdmin(admin.ModelAdmin):
inlines = [MembershipInline,]
-
-class MembershipAdmin(admin.ModelAdmin):
- fields = ['type_of_membership']
+ search_fields = ('first_name', 'last_name', 'email' )
+ fieldsets = (
+ (None, {
+ 'fields': ('first_name', 'last_name', 'birth_date', 'gender')
+ }),
+ ('Contatti', {
+ 'classes': ('collapse',),
+ 'fields': ('phone_number', 'email', 'wants_newsletter')
+ }),
+ ('Indirizzo', {
+ 'classes': ('collapse',),
+ 'fields': ('street', 'civic_nb', 'zip_code', 'location', 'province', 'country')
+ }),
+ ('Indirizzo di spedizione', {
+ 'classes': ('collapse',),
+ 'fields': ('exp_street', 'exp_civic_nb', 'exp_zip_code', 'exp_location', 'exp_province', 'exp_country')
+ }),
+ )
+
-admin.site.register(Membership)
+admin.site.register(Membership, MembershipAdmin)
admin.site.register(Associate, AssociateAdmin)
admin.site.register(Citizen)
admin.site.register(Organization)
View
No changes.
No changes.
@@ -0,0 +1,66 @@
+from optparse import make_option
+import datetime
+from django.core.management.base import BaseCommand, CommandError
+from django.db.models import Count
+
+from op_associazione.models import Associate, Membership
+from op_associazione import notifications
+
+class Command(BaseCommand):
+ help = 'Check memberhips for expiration and optionally notify consequently (send reminder to expiring, send report to site managers for both expiring and expired)'
+
+ option_list = BaseCommand.option_list + (
+ make_option('--dry-run',
+ action='store_true',
+ dest='dryrun',
+ default=False,
+ help='List expiring and expired memberships and send mild notifications'),
+ ),
+
+ def handle(self, *args, **options):
+ """
+ Extracts expiring (in N days) and expired memberships.
+ Send a report to site managers.
+ Send a notification to expiring memberships related associates.
+
+ De-activation must be manually performed by administrators.
+ """
+
+ expiring = []
+ expired = []
+
+ # extracts associates having at least one membership
+ associates = Associate.objects.annotate(n_memberships=Count('membership')).filter(n_memberships__gt=0)
+ for associate in associates:
+ # filter active memberships
+ active_memberships = associate.memberships.order_by('-payed_at').filter(is_active=True)
+ if len(active_memberships):
+ # get last payed membership
+ last_membership = active_memberships[0]
+ # check if expire_at was filled, else fill it now (1 year )
+ if last_membership.expire_at is None:
+ last_membership.expire_at = last_membership.payed_at + datetime.timedelta(days=365)
+ last_membership.save()
+
+ # check if membership is going to expire in 15 days
+ if last_membership.expire_at == datetime.date.today() + datetime.timedelta(days=15):
+ # append to axpiring array, to send summary to managers
+ expiring.append(last_membership)
+ if not options['dryrun']:
+ # send mail to associate
+ notifications.send_expiring_warning_email(membership)
+
+ # log
+ self.stdout.write('Associate: %s - payed_at: %s, expire_at: %s - Expiring!\n' %
+ (associate, last_membership.payed_at, last_membership.expire_at))
+
+ # check if membership is expired
+ if last_membership.expire_at < datetime.date.today():
+ # append to expired array, to send summary to managers
+ expired.append(last_membership)
+
+ self.stdout.write('Associate: %s - payed_at: %s, expire_at: %s - EXPIRED!\n' %
+ (associate, last_membership.payed_at, last_membership.expire_at))
+
+ if not options['dryrun']:
+ notifications.send_checksubscriptions_report(expiring, expired)
View
@@ -3,6 +3,8 @@
from django.core.urlresolvers import reverse
from django.db import models
+from op_associazione import notifications
+
class Membership(models.Model):
MEMBER_TYPE = (
('fondatore', 'Socio fondatore'),
@@ -38,6 +40,15 @@ def get_typename(member_type):
if entry[0] == member_type:
return entry[1]
return u'Tipologia di utente %s non trovata' % member_type
+
+ def notify(self):
+ """
+ notify owner of the subscription that the subscription is being
+ deactivated; invite to renew
+ """
+ self.is_active = False
+ self.save()
+ notifications.send_expired_email(self)
def __unicode__(self):
return "%s at %s" % (self.associate, self.created_at.isoformat())
View
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+import datetime
from django.core.mail import EmailMultiAlternatives, mail_managers
from django.template.loader import get_template
from django.template import Context
@@ -17,12 +18,53 @@ def send_mail(subject, txt_content, from_address, to_addresses, html_content=Non
msg.attach_alternative(html_content, "text/html")
msg.send()
-def send_renewal_email(associate):
+
+def send_checksubscriptions_report(expiring, expired):
+ d = Context({ 'current_site': Site.objects.get(id=settings.SITE_ID),
+ 'expiring': expiring, 'expired': expired, 'expiring_date': datetime.date.today() + datetime.timedelta(days=15) })
+
+ # send notification to managers
+ plaintext = get_template('email/checksubscriptions_report.txt')
+ mail_managers('[openpolis] report controllo iscrizioni', plaintext.render(d))
+
+
+def send_expiring_warning_email(membership):
+ d = Context({ 'current_site': Site.objects.get(id=settings.SITE_ID),
+ 'associate': membership.associate,
+ 'expire_at': membership.expire_at.strftime("%d/%m/%Y"),
+ 'renewal_url': reverse('subscribe-renewal', args=[membership.associate.hash_key]) })
+ plaintext = get_template('email/expiring_warning_email.txt')
+ htmly = get_template('email/expiring_warning_email.html')
+
+ send_mail(
+ '[openpolis] Tua iscrizione in scadenza il %s' % (membership.expire_at.strftime("%d/%m/%Y")),
+ plaintext.render(d),
+ settings.SERVER_EMAIL,
+ membership.associate.email,
+ html_content=htmly.render(d)
+ )
+
+def send_expired_email(membership):
+ d = Context({ 'current_site': Site.objects.get(id=settings.SITE_ID),
+ 'associate': membership.associate,
+ 'renewal_url': reverse('subscribe-renewal', args=[membership.associate.hash_key]) })
+ plaintext = get_template('email/expired_email.txt')
+ htmly = get_template('email/expired_email.html')
+
+ send_mail(
+ '[openpolis] Tua iscrizione scaduta!',
+ plaintext.render(d),
+ settings.SERVER_EMAIL,
+ membership.associate.email,
+ html_content=htmly.render(d)
+ )
+
+def send_renewal_verification_email(associate):
d = Context({ 'current_site': Site.objects.get(id=settings.SITE_ID),
'associate': associate,
'renewal_url': reverse('subscribe-renewal', args=[associate.hash_key]) })
- plaintext = get_template('email/renewal_email.txt')
- htmly = get_template('email/renewal_email.html')
+ plaintext = get_template('email/renewal_verification_email.txt')
+ htmly = get_template('email/renewal_verification_email.html')
send_mail(
'[openpolis] Resta qui',
@@ -55,4 +97,6 @@ def subscription_received(membership, request_type):
# send notification to managers
plaintext = get_template('email/new_subscription_received.txt')
- mail_managers('[openpolis] nuova iscrizione ricevuta', plaintext.render(d))
+ mail_managers('[openpolis] nuova iscrizione ricevuta', plaintext.render(d))
+
+
@@ -0,0 +1,132 @@
+{% load i18n adminmedia admin_modify %}
+<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
+ <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
+{{ inline_admin_formset.formset.management_form }}
+<fieldset class="module">
+ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
+ {{ inline_admin_formset.formset.non_form_errors }}
+ <table>
+ <thead><tr>
+ {% for field in inline_admin_formset.fields %}
+ {% if not field.widget.is_hidden %}
+ <th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
+ {% endif %}
+ {% endfor %}
+ {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
+ </tr></thead>
+
+ <tbody>
+ {% for inline_admin_form in inline_admin_formset %}
+ {% if inline_admin_form.form.non_field_errors %}
+ <tr><td colspan="{{ inline_admin_form|cell_count }}">{{ inline_admin_form.form.non_field_errors }}</td></tr>
+ {% endif %}
+ <tr class="{% cycle "row1" "row2" %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}{% if forloop.last %} empty-form{% endif %}"
+ id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
+ <td class="original">
+ {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
+ {% if inline_admin_form.original %} <a href="/admin/{{ app_label }}/{{ inline_admin_formset.opts.admin_model_path }}/{{ inline_admin_form.original.pk }}/">{{ inline_admin_form.original }}</a>{% endif %}
+ {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original_content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
+ </p>{% endif %}
+ {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
+ {{ inline_admin_form.fk_field.field }}
+ {% spaceless %}
+ {% for fieldset in inline_admin_form %}
+ {% for line in fieldset %}
+ {% for field in line %}
+ {% if field.is_hidden %} {{ field.field }} {% endif %}
+ {% endfor %}
+ {% endfor %}
+ {% endfor %}
+ {% endspaceless %}
+ </td>
+ {% for fieldset in inline_admin_form %}
+ {% for line in fieldset %}
+ {% for field in line %}
+ <td class="{{ field.field.name }}">
+ {% if field.is_readonly %}
+ <p>{{ field.contents }}</p>
+ {% else %}
+ {{ field.field.errors.as_ul }}
+ {{ field.field }}
+ {% endif %}
+ </td>
+ {% endfor %}
+ {% endfor %}
+ {% endfor %}
+
+ {% if inline_admin_formset.formset.can_delete %}
+ <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
+ {% endif %}
+
+
+
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</fieldset>
+ </div>
+</div>
+
+<script type="text/javascript">
+(function($) {
+ $(document).ready(function($) {
+ var rows = "#{{ inline_admin_formset.formset.prefix }}-group .tabular.inline-related tbody tr";
+ var alternatingRows = function(row) {
+ $(rows).not(".add-row").removeClass("row1 row2")
+ .filter(":even").addClass("row1").end()
+ .filter(rows + ":odd").addClass("row2");
+ }
+ var reinitDateTimeShortCuts = function() {
+ // Reinitialize the calendar and clock widgets by force
+ if (typeof DateTimeShortcuts != "undefined") {
+ $(".datetimeshortcuts").remove();
+ DateTimeShortcuts.init();
+ }
+ }
+ var updateSelectFilter = function() {
+ // If any SelectFilter widgets are a part of the new form,
+ // instantiate a new SelectFilter instance for it.
+ if (typeof SelectFilter != "undefined"){
+ $(".selectfilter").each(function(index, value){
+ var namearr = value.name.split('-');
+ SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}");
+ });
+ $(".selectfilterstacked").each(function(index, value){
+ var namearr = value.name.split('-');
+ SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}");
+ });
+ }
+ }
+ var initPrepopulatedFields = function(row) {
+ row.find('.prepopulated_field').each(function() {
+ var field = $(this);
+ var input = field.find('input, select, textarea');
+ var dependency_list = input.data('dependency_list') || [];
+ var dependencies = [];
+ $.each(dependency_list, function(i, field_name) {
+ dependencies.push('#' + row.find(field_name).find('input, select, textarea').attr('id'));
+ });
+ if (dependencies.length) {
+ input.prepopulate(dependencies, input.attr('maxlength'));
+ }
+ });
+ }
+ $(rows).formset({
+ prefix: "{{ inline_admin_formset.formset.prefix }}",
+ addText: "{% blocktrans with inline_admin_formset.opts.verbose_name|title as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}",
+ formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}",
+ deleteCssClass: "inline-deletelink",
+ deleteText: "{% trans "Remove" %}",
+ emptyCssClass: "empty-form",
+ removed: alternatingRows,
+ added: (function(row) {
+ initPrepopulatedFields(row);
+ reinitDateTimeShortCuts();
+ updateSelectFilter();
+ alternatingRows(row);
+ })
+ });
+ });
+})(django.jQuery);
+</script>
@@ -0,0 +1,20 @@
+Questo è il report quotidiano del controllo delle iscrizioni all'associazione.
+
+In scadenza tra 15 giorni ({{ expiring_date|date:"d/m/Y" }})
+======================================
+{% for membership in expiring %}{{ membership.associate }} - {{ membership.associate.email }} - {{ membership.fee }} euri
+{% endfor %}
+
+Questi utenti hanno gia' ricevuto l'email di avviso.
+
+
+
+
+
+Scadute oggi
+============
+{% for membership in expired %}{{ membership.associate }} - {{ membership.associate.email }} - {{ membership.fee }} euri ({{ membership.expire_at }})
+{% endfor %}
+
+Questi utenti non hanno ricevuto alcuna email,
+l'intervento di disattivazione deve essere manuale.
Oops, something went wrong.

0 comments on commit 3dc0b15

Please sign in to comment.