Commits

Colin Copeland committed 1188c3c

initial import

Comments (0)

Files changed (12)

members/__init__.py

Empty file added.
+from django.contrib import admin
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+
+from members import models as memberships
+
+
+def send_account_activation_email(modeladmin, request, queryset):
+    selected = queryset.values_list('contact__id', flat=True)
+    selected = ["ids=%d" % pk for pk in selected]
+    url = reverse('create_registration')
+    return HttpResponseRedirect("%s?%s" % (
+        url,
+        "&".join(selected)
+    ))
+
+
+class MembershipAdmin(admin.ModelAdmin):
+    list_display = ('contact', 'type', 'status')
+    list_filter = ('type', 'status')
+    raw_id_fields = ('contact',)
+    search_fields = (
+        'contact__first_name',
+        'contact__last_name',
+        'contact__email',
+    )
+    ordering = ('contact__sort_name',)
+    actions = [send_account_activation_email]
+admin.site.register(memberships.Membership, MembershipAdmin)
+
+
+class MembershipTypeAdmin(admin.ModelAdmin):
+    prepopulated_fields = {'slug': ('name',)}
+    list_display = ('name', 'slug', 'minimum_dues', 'order')
+    ordering = ('order', 'name')
+admin.site.register(memberships.MembershipType, MembershipTypeAdmin)
+
+
+class MembershipStatusAdmin(admin.ModelAdmin):
+    prepopulated_fields = {'slug': ('name',)}
+    list_display = ('name', 'slug', 'order', 'is_current_member')
+    ordering = ('order',)
+admin.site.register(memberships.MembershipStatus, MembershipStatusAdmin)
+
+
+class MembershipComparisonAdmin(admin.ModelAdmin):
+    list_display = ('id', 'date_uploaded', 'file', 'compare')
+    ordering = ('-date_uploaded',)
+    
+    def compare(self, obj):
+        return "<a href='%s'>Compare</a>" % reverse(
+            'compare_membership_list',
+            args=[obj.pk],
+        )
+    compare.allow_tags = True
+admin.site.register(memberships.MembershipComparison, MembershipComparisonAdmin)
+from django import forms
+
+
+class SearchForm(forms.Form):
+    search = forms.CharField(required=False)
+    letter = forms.CharField(required=False, widget=forms.HiddenInput())
+    
+    def clean_letter(self):
+        letter = self.cleaned_data['letter']
+        if letter and not ord(letter) in range(ord('A'), ord('Z') + 1):
+            raise forms.ValidationError('Not a valid letter')
+        return letter
+
+
+FIELDS = ('first_name', 'last_name', 'email', 'membership_type')
+FIELD_MAP = {
+    'first_name': ('contact', 'first_name'),
+    'last_name': ('contact', 'last_name'),
+    'email': ('contact', 'email'),
+    'membership_type': ('membership', 'type'),
+}
+
+class ColumnMatchForm(forms.Form):
+    def __init__(self, *args, **kwargs):
+        count = kwargs.pop('column_count')
+        choices = [(i + 1, 'Column %d' % (i + 1)) for i in range(count)]
+        choices.insert(0, ('', '------'))
+        super(ColumnMatchForm, self).__init__(*args, **kwargs)
+        for field in FIELDS:
+            self.fields[field] = forms.ChoiceField(
+                choices=choices,
+                required=False,
+            )
+    
+    def save(self):
+        columns = []
+        for key in FIELDS:
+            if key in self.cleaned_data and self.cleaned_data[key] != '':
+                index = int(self.cleaned_data[key]) - 1
+                columns.append((FIELD_MAP[key], index))
+        return columns

members/management/__init__.py

Empty file added.

members/management/commands/__init__.py

Empty file added.

members/management/commands/import_memberships.py

+import csv
+from decimal import Decimal
+
+from django.core.management.base import BaseCommand
+from django.db import transaction
+from django.template.defaultfilters import slugify
+from django.db.models import Q
+from django.db import connection
+
+from caktus.django.db.util import slugify_uniquely
+
+from crm.models import Contact
+
+from members import models as memberships
+
+
+class Command(BaseCommand):
+    help = "Import CSV Membership List"
+
+    @transaction.commit_on_success
+    def handle(self, csv_file_name, **kwargs):
+        total_contacts = Contact.objects.count()
+        types = {}
+        status, created = memberships.MembershipStatus.objects.get_or_create(
+            name='Active',
+            slug='active',
+            is_current_member=True,
+            order=1,
+        )
+        mreader = csv.reader(
+            open(csv_file_name),
+            delimiter=',',
+            quotechar='"'
+        )
+        
+        for row in mreader:
+            # print row[0], row[2]
+            contact = None
+            location = None
+            type = None
+            query = ''
+            if row[11] == 'Club' or row[11] == 'Institutional':
+                ctype = 'business'
+            else:
+                ctype = 'individual'
+            if ctype == 'individual':
+                try:
+                    lookup = Q(type='individual')
+                    lookup &= (Q(email=row[10]) & ~Q(email='')) | (Q(first_name=row[0]) & Q(last_name=row[2]))
+                    contact = list(Contact.objects.filter(lookup))
+                    #query = connection.queries[-1]
+                    #print query
+                    contact = contact[0]
+                except (Contact.DoesNotExist, IndexError):
+                    contact = None
+            else:
+                contact = Contact.objects.create(
+                    type=ctype,
+                    name=row[0],
+                    slug=slugify_uniquely(row[0], Contact.objects.all()),
+                    sort_name=slugify(row[0]),
+                    description='',
+                    notes='',
+                )
+            if not contact:
+                contact = Contact.objects.create(
+                    type=ctype,
+                    first_name=row[0],
+                    middle_name=row[1],
+                    last_name=row[2],
+                    slug=slugify_uniquely("%s %s" % (row[0], row[2]), Contact.objects.all()),
+                    sort_name=slugify("%s %s" % (row[2], row[0])),
+                    email=row[10],
+                    description='',
+                    notes='',
+                )
+            try:
+                type = types[row[11]]
+            except KeyError:
+                type = types[row[11]] = memberships.MembershipType.objects.create(
+                    name=row[11],
+                    slug=slugify(row[11]),
+                    minimum_dues=Decimal('50.00'),
+                    description='Fill me in',
+                )
+            existing = memberships.Membership.objects.filter(
+                contact=contact,
+                type=type,
+            )
+            
+            if existing.count() > 0:
+                print row
+                print existing
+                for m in existing:
+                    print m.contact.__dict__
+                print query
+                continue
+            
+            names = Contact.objects.filter(first_name=contact.first_name, last_name=contact.last_name)
+            if  ctype == 'individual' and names.count() > 1:
+                print 'MORE THAN 1'
+                for name in names:
+                    print name, name.email
+                print query
+
+            location = contact.locations.create()
+            location.addresses.create(
+                street=', '.join([row[3], row[4], row[5]]),
+                city=row[6],
+                state_province=row[7],
+                postal_code=row[8],
+            )
+            location.phones.create(number=row[9])
+            memberships.Membership.objects.create(
+                contact=contact,
+                type=type,
+                status=status,
+            )
+        print 'Total Contacts Before:', total_contacts
+        print 'Total Contacts Now:', Contact.objects.count()
+        print 'Total Memberships', memberships.Membership.objects.count()
+import datetime
+
+from django.db import models
+from django.core.urlresolvers import reverse
+
+from crm.models import Contact
+
+
+ORDER_CHOICES = [(x, x) for x in range(-10, 11)]
+
+
+class Membership(models.Model):
+    type = models.ForeignKey('MembershipType', related_name='memberships')
+    status = models.ForeignKey('MembershipStatus', related_name='memberships')
+    contact = models.ForeignKey(Contact, related_name='memberships')
+    
+    class Meta:
+        unique_together = ('contact', 'type')
+        permissions = (
+            ("can_print_membership_list", "Can print membership list"),
+            ("can_export_membership_list", "Can export membership list"),
+        )
+    
+    def __unicode__(self):
+        return "%s membership: %s" % (self.type, self.contact)
+
+
+class MembershipType(models.Model):
+    name = models.CharField(max_length=255, unique=True)
+    slug = models.SlugField(max_length=255, unique=True)
+    minimum_dues = models.DecimalField(max_digits=10, decimal_places=2)
+    description = models.TextField()
+    order = models.SmallIntegerField(
+        null=True,
+        blank=True,
+        choices=ORDER_CHOICES,
+    )
+    
+    def __unicode__(self):
+        return self.name
+
+
+class MembershipStatus(models.Model):
+    name = models.CharField(max_length=255, unique=True)
+    slug = models.SlugField(max_length=255, unique=True)
+    is_current_member = models.BooleanField(default=True)
+    order = models.SmallIntegerField(choices=ORDER_CHOICES)
+    
+    class Meta:
+        verbose_name_plural = 'Membership statuses'
+    
+    def __unicode__(self):
+        return self.name
+
+
+class MembershipComparison(models.Model):
+    file = models.FileField(
+        upload_to='membership-comparison/',
+        help_text="Comma-separated Values (CSV) files only.  Must be sorted last name, first name."
+    )
+    date_uploaded = models.DateTimeField(default=datetime.datetime.now)
+    
+    class Meta:
+        permissions = (
+            ("can_compare_membership_list", "Can compare membership list"),
+        )
+    
+    def __unicode__(self):
+        return self.file.name
+    
+    def get_absolute_url(self):
+        return reverse('compare_membership_list', args=(self.pk,))

members/templatetags/__init__.py

Empty file added.

members/templatetags/members_tags.py

+from django import template
+from django.template.defaultfilters import capfirst
+
+register = template.Library()
+
+
+@register.simple_tag
+def format_phones(membership):
+    phones = []
+    for type_, number in zip(membership.phone_type, membership.phone_number):
+        phones.append("%s: %s" % (capfirst(type_), number))
+    return ', '.join(phones)
+import datetime
+from decimal import Decimal
+
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User, Permission
+from django.core import mail
+
+from crm import models as crm
+
+from members import models as members
+
+
+class MembersTestCase(TestCase):
+    def setUp(self):
+        self.john_user = User.objects.create_user(
+            'john',
+            'john@doe.com',
+            'abc123',
+        )
+        self.john_contact = crm.Contact.objects.create(
+            first_name='John',
+            last_name='Doe',
+            email='john@doe.com',
+            slug='john-doe',
+            description='',
+            sort_name='doe-john',
+            user=self.john_user,
+            type='individual',
+        )
+        self.jane_user = User.objects.create_user(
+            'jane',
+            'jane@doe.com',
+            'abc123',
+        )
+        self.jane_contact = crm.Contact.objects.create(
+            first_name='Jane',
+            last_name='Doe',
+            email='jane@doe.com',
+            slug='jane-doe',
+            description='',
+            sort_name='doe-jane',
+            user=self.jane_user,
+            type='individual',
+        )
+        membership_type = members.MembershipType.objects.create(
+            name='Member',
+            slug='member',
+            minimum_dues=Decimal('0.0'),
+            description='',
+        )
+        membership_status = members.MembershipStatus.objects.create(
+            name='Active',
+            slug='active',
+            is_current_member=True,
+            order=1,
+        )
+        members.Membership.objects.create(
+            contact=self.john_contact,
+            type=membership_type,
+            status=membership_status,
+        )
+    
+    def testViewPerson(self):
+        # anonymous users cannot see profile
+        response = self.client.get(
+            reverse('view_person', args=[self.john_contact.pk]),
+        )
+        self.assertEquals(response.status_code, 302)
+        
+        # authenticated users can see profile
+        self.client.login(username='john@doe.com', password='abc123')
+        response = self.client.get(
+            reverse('view_person', args=[self.john_contact.pk]),
+        )
+        self.assertEquals(response.status_code, 200)
+    
+    def testEditPerson(self):
+        self.client.login(username='john@doe.com', password='abc123')
+        response = self.client.get(
+            reverse('edit_person', args=[self.john_contact.pk]),
+            follow=True,
+        )
+        self.assertEquals(response.status_code, 200)
+        self.client.logout()
+        
+        self.client.login(username='jane@doe.com', password='abc123')
+        response = self.client.get(
+            reverse('edit_person', args=[self.john_contact.pk]),
+        )
+        self.assertEquals(response.status_code, 302)
+    
+    def testMembershipListRequiresLogin(self):
+        response = self.client.get(reverse('list_memberships'), follow=True)
+        self.assertEqual(response.status_code, 200)
+        url, status_code = response.redirect_chain.pop()
+        self.assertEqual(status_code, 302)
+        self.assertTrue('login' in url)
+    
+    def testPrintMembershipList(self):
+        p = Permission.objects.get(codename='can_print_membership_list')
+        self.john_user.user_permissions.add(p)
+        response = self.client.get(reverse('print_membership_list'))
+        self.assertEqual(response.status_code, 302)
+        self.client.login(username='john@doe.com', password='abc123')
+        response = self.client.get(reverse('print_membership_list'))
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(response.context['memberships'])
+
+    def testPrintMembershipList(self):
+        p = Permission.objects.get(codename='can_export_membership_list')
+        self.john_user.user_permissions.add(p)
+        response = self.client.get(reverse('export_membership_list'))
+        self.assertEqual(response.status_code, 302)
+        self.client.login(username='john@doe.com', password='abc123')
+        response = self.client.get(reverse('export_membership_list'))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response['Content-Type'], 'text/csv')
+        self.assertEqual(
+            response['Content-Disposition'],
+            'attachment; filename=memberships.csv',
+        )
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('members.views',
+    url(
+        r'^membership/$',
+        'list_memberships',
+        name='list_memberships',
+    ),
+    url(
+        r'^membership/print/$',
+        'print_membership_list',
+        name='print_membership_list',
+    ),
+    url(
+        r'^membership/export/$',
+        'export_membership_list',
+        name='export_membership_list',
+    ),
+    url(
+        r'^membership/compare/(?P<compare_id>\d+)/$',
+        'compare_membership_list',
+        name='compare_membership_list',
+    ),
+)
+import csv
+import random
+import difflib
+
+from pygments import highlight
+from pygments.lexers import DiffLexer
+from pygments.formatters import HtmlFormatter
+
+from django.shortcuts import get_object_or_404
+from django.db import transaction
+from django.http import HttpResponseRedirect, HttpResponse
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.contrib.auth.decorators import login_required, permission_required
+from django.core.cache import cache
+
+from caktus.django.decorators import render_with
+from caktus.iter import previous_and_next, collapse
+
+from members import models as members
+from members import forms as member_forms
+from crm import models as crm
+from contactinfo import models as contactinfo
+
+
+@login_required
+@render_with('members/membership/list.html')
+def list_memberships(request):
+    letter = ''
+    letters = cache.get('membership-letter-nav')
+    if not letters:
+        letters = members.Membership.objects.distinct().exclude(
+            contact__last_name=''
+        ).extra(
+            select={
+                'letter': 'SUBSTR(crm_contact.last_name, 1, 1)'
+            }
+        ).order_by('letter').values_list('letter', flat=True)
+        cache.set('membership-letter-nav', letters)
+    
+    memberships = members.Membership.objects.filter(
+        status__is_current_member=True
+    )
+    total_current_members = cache.get('membership-current-count')
+    if not total_current_members:
+        total_current_members = memberships.count()
+        cache.set('membership-current-count', total_current_members)
+    filtering = False
+    form = member_forms.SearchForm(request.GET)
+    if form.is_valid():
+        if 'search' in form.cleaned_data and form.cleaned_data['search']:
+            search = form.cleaned_data['search']
+            memberships = memberships.filter(
+                Q(contact__first_name__icontains=search)
+                | Q(contact__last_name__icontains=search)
+                | Q(contact__email__icontains=search)
+                | Q(contact__name=search)
+            )
+            filtering = True
+        if 'letter' in form.cleaned_data and form.cleaned_data['letter']:
+            letter = form.cleaned_data['letter']
+            memberships = memberships.filter(
+                contact__last_name__istartswith=letter,
+            )
+            filtering = True
+    
+    memberships = memberships.select_related(
+        'contact',
+        'type',
+    ).order_by('contact__sort_name')
+    
+    return {
+        'form': form,
+        'filtering': filtering,
+        'letters': letters,
+        'selected_letter': letter,
+        'memberships': memberships,
+        'total_current_members': total_current_members,
+    }
+
+
+def get_memberships_with_relationships():
+    memberships = members.Membership.objects.filter(
+        status__is_current_member=True,
+    ).select_related(
+        'contact',
+        'type',
+    ).extra(
+        select={
+            'street': 'contactinfo_address.street',
+            'city': 'contactinfo_address.city',
+            'state_province': 'contactinfo_address.state_province',
+            'postal_code': 'contactinfo_address.postal_code',
+            'phone_number': 'contactinfo_phone.number',
+            'phone_type': 'contactinfo_phone.type',
+            'location_type': 'contactinfo_locationtype.name',
+        },
+    ).order_by('contact__sort_name')
+    connection = (
+        members.Membership._meta.db_table,
+        crm.Contact._meta.db_table,
+        members.Membership.contact.field.column,
+        crm.Contact._meta.pk.name,
+    )
+    memberships.query.join(connection)
+    connection = (
+        crm.Contact._meta.db_table,
+        crm.Contact.locations.field.m2m_db_table(),
+        crm.Contact._meta.pk.column,
+        crm.Contact.locations.field.m2m_column_name(),
+    )
+    memberships.query.join(connection, promote=True)
+    connection = (
+        crm.Contact.locations.field.m2m_db_table(),
+        contactinfo.Location._meta.db_table,
+        crm.Contact.locations.field.m2m_reverse_name(),
+        # or crm.Location.contact_set.related.field.m2m_reverse_name()?
+        contactinfo.Location._meta.pk.column,
+    )
+    memberships.query.join(connection, promote=True)
+    connection = (
+        contactinfo.Location._meta.db_table,
+        contactinfo.LocationType._meta.db_table,
+        contactinfo.Location.type.field.column,
+        contactinfo.LocationType._meta.pk.column,
+    )
+    memberships.query.join(connection, promote=True)
+    connection = (
+        contactinfo.Location._meta.db_table,
+        contactinfo.Address._meta.db_table,
+        contactinfo.Location._meta.pk.column,
+        contactinfo.Address.location.field.column,
+    )
+    memberships.query.join(connection, promote=True)
+    connection = (
+        contactinfo.Location._meta.db_table,
+        contactinfo.Phone._meta.db_table,
+        contactinfo.Location._meta.pk.column,
+        contactinfo.Phone.location.field.column,
+    )
+    memberships.query.join(connection, promote=True)
+    memberships = collapse(
+        memberships,
+        ['id'],
+        ['phone_number', 'phone_type'],
+        sort=False,
+    )
+    return memberships
+
+
+@permission_required('members.can_print_membership_list')
+@render_with('members/membership/print.html')
+def print_membership_list(request):
+    return {
+        'memberships': get_memberships_with_relationships(),
+    }
+
+
+@permission_required('members.can_export_membership_list')
+def export_membership_list(request):
+    response = HttpResponse(mimetype='text/csv')
+    response['Content-Disposition'] = 'attachment; filename=memberships.csv'
+    writer = csv.writer(response)
+    writer.writerow((
+        'Name',
+        'Contact Type',
+        'Membership Type',
+        'Membership Status',
+        'Email',
+        'Location Type',
+        'Street',
+        'City',
+        'State/Province',
+        'Postal Code',
+        'Phone Number(s)',
+    ))
+    memberships = get_memberships_with_relationships()
+    for membership in memberships:
+        data = [
+            membership.contact,
+            membership.contact.type,
+            membership.type,
+            membership.status,
+            membership.contact.email,
+            membership.location_type,
+        ]
+        street = membership.street or ''
+        if street:
+            street = street.replace('\n', ' ').replace('\r', ' ')
+        data.extend((
+            street,
+            membership.city,
+            membership.state_province,
+            membership.postal_code,
+            ', '.join([unicode(n) for n in membership.phone_number if n])
+        ))
+        writer.writerow((data))
+    return response
+
+
+@permission_required('members.can_compare_membership_list')
+@render_with('members/membership/compare/prepare.html')
+def compare_membership_list(request, compare_id):
+    compare = get_object_or_404(members.MembershipComparison, pk=compare_id)
+    fh = open(compare.file.path)
+    has_header = csv.Sniffer().sniff(fh.read(1024))
+    fh.seek(0)
+    csv_reader = csv.reader(fh)
+    lines = []
+    for line in csv_reader:
+        lines.append(line)
+        if len(lines) == 7:
+            break
+    output = ''
+    column_count = len(lines[0])
+    if request.POST:
+        form = member_forms.ColumnMatchForm(
+            request.POST,
+            column_count=column_count,
+        )
+        if form.is_valid():
+            columns = form.save()
+            
+            memberships = members.Membership.objects.filter(
+                status__is_current_member=True,
+            ).select_related(
+                'contact',
+                'type',
+            ).order_by('contact__last_name', 'contact__first_name')
+            fh.seek(0)
+            first = []
+            for membership in memberships:
+                row = []
+                for (type_, attr), index in columns:
+                    if type_ == 'membership':
+                        col = getattr(membership, attr)
+                    elif type_ == 'contact':
+                        col = getattr(membership.contact, attr)
+                    row.append(unicode(col))
+                first.append(', '.join(row))
+            
+            fh.seek(0)
+            second = []
+            for line in csv_reader:
+                row = []
+                for attr, index in columns:
+                    row.append(line[index])
+                second.append(', '.join(row))
+            if has_header:
+                second.pop(0)
+            
+            diff = '\n'.join(difflib.ndiff(first, second))
+            output = highlight(diff, DiffLexer(), HtmlFormatter())
+            
+    else:
+        form = member_forms.ColumnMatchForm(column_count=column_count)
+    
+    fh.close()
+    
+    return {
+        'output': output,
+        'lines': lines,
+        'has_header': has_header,
+        'form': form,
+        'compare': compare,
+    }
+