source: asadb/groups/diffs.py @ bda4d86

space-accessstablestage
Last change on this file since bda4d86 was ac9b167, checked in by Alex Dehnert <adehnert@…>, 13 years ago

Update finboard-groups-only (ASA-#121)

Note that this doesn't bother to kinit, so it depends on KRB5CCNAME being set
already. Fixing that will come next.

  • Property mode set to 100755
File size: 14.7 KB
Line 
1#!/usr/bin/python
2import collections
3import datetime
4import os
5import sys
6
7if __name__ == '__main__':
8    cur_file = os.path.abspath(__file__)
9    django_dir = os.path.abspath(os.path.join(os.path.dirname(cur_file), '..'))
10    sys.path.append(django_dir)
11    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
12
13from django.contrib.contenttypes.models import ContentType
14from django.core.mail import EmailMessage, mail_admins
15from django.db import connection
16from django.db.models import Q
17from django.template import Context, Template
18from django.template.loader import get_template
19
20import reversion.models
21
22import groups.models
23import settings
24import util.emails
25import util.mailinglist
26
27if settings.PRODUCTION_DEPLOYMENT:
28    asa_all_groups_list = util.mailinglist.MailmanList('asa-official')
29    finboard_groups_list = util.mailinglist.MoiraList('finboard-groups-only')
30else:
31    asa_all_groups_list = util.mailinglist.MailmanList('asa-test-mailman')
32    finboard_groups_list = util.mailinglist.MoiraList('asa-test-moira')
33
34class DiffCallback(object):
35    def start_run(self, since, now, ):
36        pass
37    def end_run(self, ):
38        pass
39    def handle_group(self, before, after, before_fields, after_fields, ):
40        pass
41    def handle_signatories(self, signatories, ):
42        pass
43    def new_group(self, after, after_fields, ):
44        pass
45
46class StaticMailCallback(DiffCallback):
47    def __init__(self, fields, address, template, signatories=[]):
48        self.fields = fields
49        self.address = address
50        self.template = template
51        self.interesting_signatories = signatories
52        self.care_about_groups = True
53        self.care_about_signatories = True
54
55    def start_run(self, since, now, ):
56        self.updates = []
57        self.signatory_updates = []
58        self.signatory_type_counts = {
59            'Added': collections.defaultdict(lambda: 0),
60            'Expired': collections.defaultdict(lambda: 0),
61        }
62        self.signatory_types_seen = set()
63        self.since = since
64        self.now = now
65
66    def handle_group(self, before, after, before_fields, after_fields, ):
67        after_revision = after.revision
68        update = "Group: %s (ID #%d)\n" % (after_fields['name'], after_fields['id'], )
69        update += "  At %s by %s (and possibly other people or times)\n" % (
70            after_revision.date_created, after_revision.user, )
71        for field in self.fields:
72            if before_fields[field] != after_fields[field]:
73                update += %18s: %12s -> %12s\n' % (
74                    field, before_fields[field], after_fields[field], )
75        self.updates.append(update)
76
77    def handle_signatories(self, signatories, ):
78        prev_group = None
79        for signatory in signatories:
80            if signatory.end_time > self.now:
81                change_type = "Added"
82            else:
83                change_type = "Expired"
84            counter = self.signatory_type_counts[change_type]
85            counter[signatory.role.slug] += 1
86            self.signatory_types_seen.add(signatory.role.slug)
87            if signatory.role.slug in self.interesting_signatories:
88                if signatory.group != prev_group:
89                    self.signatory_updates.append("")
90                self.signatory_updates.append(
91                    "%s: %s: %s: %s:\n\trange %s to %s" % (
92                        change_type,
93                        signatory.group,
94                        signatory.role,
95                        signatory.person,
96                        signatory.start_time.strftime(settings.DATETIME_FORMAT_PYTHON),
97                        signatory.end_time.strftime(settings.DATETIME_FORMAT_PYTHON),
98                    ))
99                prev_group = signatory.group
100            else:
101                pass
102                #print "Ignoring role %s (signatory %s)" % (signatory.role.slug, signatory, )
103
104    def build_change_stats(self, ):
105        lines = []
106        care_about = 0
107
108        line = "%20s" % ("", )
109        change_types = self.signatory_type_counts.keys()
110        for change_type in change_types:
111            line += "\t%s" % (change_type, )
112        lines.append(line); line = ""
113
114        for sig_type in self.signatory_types_seen:
115            anno_sig = sig_type
116            if sig_type in self.interesting_signatories:
117                anno_sig += " "
118            else:
119                anno_sig += "*"
120            line += "%20s" % (anno_sig, )
121            for change_type in change_types:
122                if sig_type in self.signatory_type_counts[change_type]:
123                    count = self.signatory_type_counts[change_type][sig_type]
124                else:
125                    count = 0
126                if sig_type in self.interesting_signatories:
127                    care_about += count
128                out = "\t%4d" % (count, )
129                line += out
130            lines.append(line); line = ""
131
132        line = "* Details for this signatory type not included in email."
133        lines.append(line)
134
135        return "\n".join(lines), care_about
136
137    def end_run(self, ):
138        change_stats, care_about = self.build_change_stats()
139        print "\nChange stats for email to %s:" % (self.address, )
140        print change_stats
141
142        message = "\n\n".join(self.updates)
143        signatories_message = "\n".join(self.signatory_updates)
144        if (self.care_about_groups and self.updates) or (self.care_about_signatories and self.signatory_updates):
145            pass
146        else:
147            return
148        tmpl = get_template(self.template)
149        ctx = Context({
150            'num_groups': len(self.updates),
151            'groups_message': message,
152            'care_about': care_about,
153            'change_stats': change_stats,
154            'signatory_types': self.interesting_signatories,
155            'signatories_message': signatories_message,
156        })
157        body = tmpl.render(ctx)
158        email = EmailMessage(
159            subject='ASA Database Updates',
160            body=body,
161            from_email='asa-db@mit.edu',
162            to=[self.address, ],
163            bcc=['asa-db-outgoing@mit.edu', ]
164        )
165        email.send()
166        self.updates = []
167        self.signatory_updates = []
168
169
170class UpdateOfficerListCallback(DiffCallback):
171    def __init__(self, listobj, include_pred=None):
172        self.listobj = listobj
173        if not include_pred:
174            include_pred = lambda version, fields: True
175        self.include_pred = include_pred
176
177    def start_run(self, since, now, ):
178        self.add = []
179        self.delete = []
180        self.notes = []
181
182    def end_run(self, ):
183        if self.add or self.delete:
184            errors = self.listobj.change_members(self.add, self.delete)
185            listname = self.listobj.name
186            subject = "[ASA DB] %s updater" % (listname, )
187            if errors:
188                subject = "ERROR: " + subject
189            context = {
190                'listname': listname,
191                'add': self.add,
192                'delete': self.delete,
193                'errors': errors,
194                'notes': self.notes,
195            }
196            util.emails.email_from_template(
197                tmpl='groups/diffs/asa-official-update.txt',
198                context=context, subject=subject,
199                to=['asa-db@mit.edu'],
200            ).send()
201
202    def update_changes(self, change_list, phase_name, name, addr, include, force_note=False, ):
203        """
204        Given an address and whether to process this item, update a list as appropriate and supply appropriate diagnostic notes.
205        """
206
207        note = None
208        if addr and include:
209            change_list.append((name, addr, ))
210            if force_note:
211                note = "email address is %s" % (addr, )
212        elif not include:
213            note = "doesn't pass predicate"
214        elif not addr:
215            note = "address is blank"
216        else:
217            note = "Something weird happened while adding (addr='%s', include=%s)" % (addr, include)
218        if note:
219            self.notes.append("%8s: %s: %s" % (phase_name, name, note, ))
220
221    def handle_group(self, before, after, before_fields, after_fields, ):
222        before_include = self.include_pred(before, before_fields)
223        after_include = self.include_pred(after, after_fields)
224        before_addr = before_fields['officer_email']
225        after_addr  = after_fields['officer_email']
226
227        # check if a change is appropriate
228        effective_before_addr = before_addr if before_include else None
229        effective_after_addr = after_addr if after_include else None
230
231        if effective_before_addr != effective_after_addr:
232            name = after_fields['name']
233            self.update_changes(self.delete, "Delete", name, before_addr, before_include)
234            self.update_changes(self.add, "Add", name, after_addr, after_include)
235
236    def new_group(self, after, after_fields, ):
237        name = after_fields['name']
238        email = after_fields['officer_email']
239        include = self.include_pred(after, after_fields)
240        self.update_changes(self.add, "New", name, email, include, force_note=True)
241
242
243def funded_pred(funding_slug):
244    classes = groups.models.GroupClass.objects
245    class_pk = classes.get(slug='mit-funded').pk
246    fundings = groups.models.GroupFunding.objects
247    fund_pk = fundings.get(slug=funding_slug).pk
248    print "funded_pred: %s %s" % (class_pk, fund_pk)
249    def pred(version, fields):
250        return fields['group_class'] == class_pk and fields['group_funding'] == fund_pk
251    return pred
252
253
254# Note: these aren't actually used (but might have some utility in telling what
255# should be diffed)
256update_asa_exec = 'asa-exec@mit.edu'
257update_funding_board = 'asa-db@mit.edu'
258update_constitution_archive = 'asa-db@mit.edu'
259
260diff_fields = {
261    'name' :            [ update_asa_exec, ],
262    'abbreviation' :    [ update_asa_exec, ],
263    'officer_email' :   [ update_asa_exec, update_funding_board ],
264    'constitution_url': [ update_asa_exec, update_constitution_archive ],
265}
266
267# This is used, OTOH
268def build_callbacks():
269    callbacks = []
270    callbacks.append(StaticMailCallback(
271        fields=[
272            'name', 'abbreviation',
273            'officer_email', 'group_email', 'athena_locker',
274            'website_url', 'constitution_url',
275            'activity_category', 'group_class', 'group_status', 'group_funding',
276            'advisor_name',
277            'num_undergrads', 'num_grads', 'num_community', 'num_other',
278            'main_account_id', 'funding_account_id',
279        ],
280        address='asa-internal@mit.edu',     # some of these fields aren't public
281        template='groups/diffs/asa-update-mail.txt',
282        signatories=['president', 'treasurer', 'financial', 'group-admin', 'temp-admin', ]
283    ))
284    sao_callback = StaticMailCallback(
285        fields=['name', 'abbreviation', 'officer_email', ],
286        address='funds@mit.edu',
287        template='groups/diffs/sao-update-mail.txt',
288        signatories=['president', 'treasurer', 'financial', ]
289    )
290    sao_callback.care_about_groups = False
291    callbacks.append(sao_callback)
292    callbacks.append(UpdateOfficerListCallback(asa_all_groups_list))
293    callbacks.append(UpdateOfficerListCallback(
294        listobj=finboard_groups_list,
295        include_pred=funded_pred('undergrad'),
296    ))
297    return callbacks
298
299def recent_groups(since):
300    group_type = ContentType.objects.get_by_natural_key(app_label='groups', model='group')
301    revisions = reversion.models.Revision.objects.filter(date_created__gte=since)
302    versions = reversion.models.Version.objects.filter(revision__in=revisions, content_type=group_type)
303    objs = versions.values("content_type", "object_id").distinct()
304    return objs
305
306def diff_objects(objs, since, callbacks, stats, ):
307    revs  = reversion.models.Revision.objects.all()
308    old_revs = revs.filter(date_created__lte=since)
309    new_revs = revs.filter(date_created__gte=since)
310    for obj in objs:
311        all_versions = reversion.models.Version.objects.filter(content_type=obj['content_type'], object_id=obj['object_id']).order_by('-revision__date_created')
312        before_versions = all_versions.filter(revision__in=old_revs)[:1]
313        # This object being passed in means that some version changed it.
314        after_versions = all_versions.filter(revision__in=new_revs).select_related('revision__user')
315        after = after_versions[0]
316        after_fields = after.field_dict
317
318        if len(before_versions) > 0 or len(after_versions) > 1:
319            if len(before_versions) > 0:
320                before = before_versions[0]
321                stats['change_old'] += 1
322            else:
323                # New group that's been edited since. Diff against the creation
324                # (since creation sent mail, but later changes haven't)
325                before = after_versions.reverse()[0]
326                stats['change_new'] += 1
327            stats['change_total'] += 1
328            #print "Change?: before=%s (%d), after=%s (%d), type=%s, new=%s" % (
329            #    before, before.pk,
330            #    after, after.pk,
331            #    after.type, after.field_dict,
332            #)
333            before_fields = before.field_dict
334            for callback in callbacks:
335                callback.handle_group(before, after, before_fields, after_fields)
336        else:
337            # New group that's only been edited once
338            # Note that "normal" new groups will have their startup form
339            # (which creates the Group object) and the approval (which makes
340            # more changes, so this is group startups + NGEs, not actually
341            # normal new groups)
342            stats['new_group'] += 1
343            callback.new_group(after, after_fields)
344
345def diff_signatories(since, now, callbacks):
346    # First: still around; then added recently
347    qobj_added = Q(end_time__gte=now, start_time__gte=since)
348    # First: already gone; then it existed for a while; finally expired recently
349    qobj_expired = Q(end_time__lte=now, start_time__lte=since, end_time__gte=since)
350    changed_signatories = groups.models.OfficeHolder.objects.filter(qobj_added|qobj_expired)
351    changed_signatories = changed_signatories.order_by('group__name', 'role__display_name', 'person', )
352    changed_signatories = changed_signatories.select_related('role', 'group')
353    for callback in callbacks: callback.handle_signatories(changed_signatories)
354
355def generate_diffs():
356    now = datetime.datetime.now()
357    recent = now - datetime.timedelta(hours=24, minutes=15)
358    objs = recent_groups(since=recent)
359    callbacks = build_callbacks()
360    stats = collections.defaultdict(lambda: 0)
361    for callback in callbacks: callback.start_run(since=recent, now=now, )
362    diff_objects(objs, since=recent, callbacks=callbacks, stats=stats)
363    diff_signatories(recent, now, callbacks=callbacks, )
364    for callback in callbacks: callback.end_run()
365
366    print "\nOverall change stats:"
367    for stat_key, stat_val in stats.items():
368        print "%20s:\t%6d" % (stat_key, stat_val, )
369    print ""
370
371if __name__ == '__main__':
372    generate_diffs()
Note: See TracBrowser for help on using the repository browser.