source: asadb/space/diffs.py

stable
Last change on this file was 3bca03c, checked in by Alex Dehnert <adehnert@…>, 10 years ago

Move emails off asa-db@, for easier filtering

Basically all mail to asa-db@ should be mail that really needs a human to look
at, not routine nightly emails.

  • Property mode set to 100755
File size: 13.8 KB
RevLine 
[bec7760]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), '..'))
[59506d6]10    django_dir_parent = os.path.abspath(os.path.join(os.path.dirname(cur_file), '../..'))
[bec7760]11    sys.path.append(django_dir)
[59506d6]12    sys.path.append(django_dir_parent)
[bec7760]13    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
14
15from django.core.mail import EmailMessage
[59506d6]16from django.core.urlresolvers import reverse
[bec7760]17from django.db import connection
18from django.db.models import Q
19from django.template import Context, Template
20from django.template.loader import get_template
21
22import groups.diffs
23import groups.models
24import space.models
25import util.emails
26
[57a2ad6]27role = {
[5680065]28    'office': groups.models.OfficerRole.objects.get(slug='office-access'),
29    'locker': groups.models.OfficerRole.objects.get(slug='locker-access'),
[57a2ad6]30}
31
[bec7760]32people_name = {} # username -> full name
33people_id = {} # username -> MIT ID
34
35all_spaces = {} # Space.pk -> Space
36
[57a2ad6]37def bulk_fill_people(times):
38    max_time = max(times)
39    min_time = min(times)
40    active_holders = groups.models.OfficeHolder.objects.filter(
41        start_time__lte=max_time,
42        end_time__gte=min_time,
43        role__in=role.values(),
44    )
45    usernames = active_holders.values_list('person', flat=True,)
46    people = groups.models.AthenaMoiraAccount.objects.filter(username__in=usernames)
47    for person in people:
48        people_name[person.username] = person.format()
49        people_id[person.username] = person.mit_id
50
[bec7760]51def fill_people(holder):
52    if not holder.person in people_name:
[57a2ad6]53        #print "Person %s not pre-cached" % (holder.person, )
[bec7760]54        try:
55            person = groups.models.AthenaMoiraAccount.objects.get(username=holder.person)
56            people_name[holder.person] = person.format()
57            people_id[holder.person] = person.mit_id
58        except groups.models.AthenaMoiraAccount.DoesNotExist:
59            people_name[holder.person] = "<%s>" % (holder.person, )
60            people_id[holder.person] = None
61
62class GroupInfo(object):
63    def __init__(self, group, ):
64        self.group = group
65        self.offices = {}  # Space.pk -> (ID -> (Set name, Set name))
[5680065]66        self.locker_acl = {}
67        self.locker_messages = []
68        self.changes = False
[bec7760]69
[5680065]70    def learn_office_access(self, space_pk, old, new):
[bec7760]71        group_pk = self.group.pk
72        if group_pk in old:
73            old_access = old[group_pk]
74        else: old_access = {}
75        if group_pk in new:
76            new_access = new[group_pk]
77        else: new_access = {}
78        assert space_pk not in self.offices
79
80        # Let's fill out the self.offices set.
81        self.offices[space_pk] = collections.defaultdict(lambda: (set(), set()))
82        space_data = self.offices[space_pk]
83        for mit_id, old_set in old_access.items():
[0d5dc3b]84            space_data[mit_id][0].update(old_set)
[bec7760]85        for mit_id, new_set in new_access.items():
[0d5dc3b]86            space_data[mit_id][1].update(new_set)
[bec7760]87
[6ae8c4a]88    def add_office_signatories_per_time(self, ind, time):
[bec7760]89        group = self.group
[6ae8c4a]90        people = group.officers(as_of=time, role=role['office'])
91        for holder in people:
[bec7760]92            fill_people(holder)
93        for office_id, office_data in self.offices.items():
[6ae8c4a]94            for holder in people:
[bec7760]95                holder_name = people_name[holder.person]
96                holder_id = people_id[holder.person]
[6ae8c4a]97                office_data[holder_id][ind].add(holder_name)
98
99    def add_office_signatories(self, old_time, new_time, ):
100        group = self.group
101        self.add_office_signatories_per_time(0, old_time)
102        self.add_office_signatories_per_time(1, new_time)
[bec7760]103
[5680065]104    def list_office_changes(self, ):
[59506d6]105        systems_lines = {
106            'cac-card': [],
107            'none': [],
108        }
[bec7760]109        group_lines = []
110        for space_pk, space_data in self.offices.items():
[59506d6]111            lock_type = all_spaces[space_pk].lock_type
112            system_lines = systems_lines[lock_type.db_update]
113            def append_change(mit_id, verb, name):
114                system_lines.append("%s:\t%s:\t%s" % (mit_id, verb, name))
115                group_lines.append("%s:\t%s" % (verb, name))
116
[bec7760]117            line = "Changes in %s:" % (all_spaces[space_pk].number, )
[59506d6]118            system_lines.append(line)
[bec7760]119            group_lines.append(line)
[59506d6]120
121            if lock_type.db_update == 'none':
[27e09a9]122                tmpl =  'Warning: You submitted changes affecting this space, but this space is ' + \
[59506d6]123                        'a "%s" space, and is not managed through the ASA DB. See ' + \
[6aa3517]124                        'https://asa.mit.edu%s for details on how to update spaces of this type.'
[59506d6]125                line = tmpl % (lock_type.name, reverse('space-lock-type'), )
126                group_lines.append(line)
127
[bec7760]128            for mit_id, (old_names, new_names) in space_data.items():
129                if mit_id is None: mit_id = "ID unknown"
130                if old_names == new_names:
131                    pass
132                else:
[5680065]133                    self.changes = True
[bec7760]134                    for name in old_names:
135                        if name in new_names:
136                            append_change(mit_id, "Keep", name)
137                        else:
138                            append_change(mit_id, "Remove", name)
139                    for name in new_names:
140                        if name in old_names:
141                            pass
142                        else:
143                            append_change(mit_id, "Add", name)
[59506d6]144            system_lines.append("")
[bec7760]145            group_lines.append("")
146
[59506d6]147        systems_msg = dict([
148            (system, '\n'.join(lines), ) for (system, lines) in systems_lines.items()
149        ])
[bec7760]150        group_msg = "\n".join(group_lines)
[59506d6]151        return systems_msg, group_msg
[5680065]152
153    def add_locker_signatories(self, space_access, time):
154        # space_access: ID -> (Name -> (Set Group.pk))
155        if time in self.locker_acl:
156            locker_acl = self.locker_acl[time]
157        else:
158            locker_acl = self.group.officers(as_of=time, role=role['locker'])
159            self.locker_acl[time] = locker_acl
160        for holder in locker_acl:
161            fill_people(holder)
162            holder_name = people_name[holder.person]
163            holder_id = people_id[holder.person]
164            space_access[holder_id][holder_name].add(self.group.pk)
[bec7760]165
166def init_groups(the_groups, assignments):
167    for assignment in assignments:
168        group = assignment.group
169        if group.id not in the_groups:
170            the_groups[group.id] = GroupInfo(group)
171
[5680065]172def flip_dict(dct):
173    new = collections.defaultdict(set)
174    for key, vals in dct.items():
175        for val in vals:
176            new[val].add(key)
177    return new
178
179def joint_keys(dct1, dct2):
180    return set(dct1.keys()).union(dct2.keys())
181
182class LockerAccessChangeEntry(object):
183    def __init__(self, mit_id, verb, name, groups):
184        self.mit_id = mit_id
185        self.verb = verb
186        self.name = name
187        self.cac_msgs = ""
188        self.group_msgs = ""
189        self.groups = groups
[bec7760]190
[5680065]191    def cac_format(self):
192        return "%s\t%s\t%s\t%s" % (self.mit_id, self.verb, self.name, self.cac_msgs)
193
194    def group_format(self):
195        return "%s\t%s\t%s" % (self.verb, self.name, self.group_msgs)
196
197def safe_add_change_real(change_by_name, change):
[492251a]198    """Add a new change to our dict of pending changes.
199
200    If a different change has already been added for this person (eg, "Remove"
201    instead of "Keep", or with a different list of groups), error.  This should
202    always succeed; if it doesn't, the code is buggy. We worry about this
203    because we want to be really sure that the email that goes to just CAC is
204    compatible with the emails that go to each groups. Since we iterate over
205    the changes once per group, we want to be sure that for each group
206    iteration we're building compatible information.
207    """
208
[5680065]209    name = change.name
210    if name in change_by_name:
211        if change_by_name[name].verb != change.verb or change_by_name[name].groups != change.groups:
212            print "change_by_name=%s" % (change_by_name, )
213            print "change: old=%s; new=%s" % (change_by_name[name], change)
214            assert False
215    else:
216        change_by_name[name] = change
217
218def locker_access_diff(the_space, group_data, old_access, new_access, ):
219    cac_msgs = [] # [String]
220    for mit_id in joint_keys(old_access, new_access):
221        change_by_name = {} # name -> LockerAccessChangeEntry
222        def safe_add(change):
223            safe_add_change_real(change_by_name, change)
224        old_by_names = old_access[mit_id]
225        new_by_names = new_access[mit_id]
226        old_by_group = flip_dict(old_by_names)
227        new_by_group = flip_dict(new_by_names)
228        unchanged = (old_by_names == new_by_names)
229        if unchanged: continue
230        print "ID=%s (%s):\n\t%s\t(%s)\n\t%s\t(%s)\n" % (mit_id, unchanged, old_by_names, old_by_group, new_by_names, new_by_group, ),
231        for group_pk in joint_keys(old_by_group, new_by_group):
[492251a]232            # TODO: Do we need to do an iteration for each group? This seems
233            # slightly questionable. Can we just loop over all known names?
234
[5680065]235            old_names = old_by_group[group_pk]
236            new_names = new_by_group[group_pk]
237            for name in old_names.union(new_names):
238                changed_groups = old_by_names[name] ^ new_by_names[name]
239
240                def mkchange(verb):
241                    change = LockerAccessChangeEntry(
242                        mit_id=mit_id,
243                        verb=verb,
244                        name=name,
245                        groups=changed_groups,
246                    )
247                    if verb == "Keep":
248                        change.group_msgs = "(other groups involved)"
249                    change.cac_msgs = "(groups: %s -> %s)" % (old_by_names[name], new_by_names[name])
250                    return change
251
252                if name in old_names and name in new_names: # keep
253                    safe_add(mkchange("Keep"))
254                elif name in old_names: # remove from this group
255                    if new_by_names[name]: # keep b/c other groups
256                        safe_add(mkchange("Keep"))
257                    else:
258                        safe_add(mkchange("Remove"))
259                elif name in new_names: # add for this group
260                    if old_by_names[name]: # keep b/c other groups
261                        safe_add(mkchange("Keep"))
262                    else:
263                        safe_add(mkchange("Add"))
264                else:
265                    assert False, "in old_names or new_names, but not in both, one, or the other..."
266
267        # Handle reporting the results...
268        for change in change_by_name.values():
269            cac_msgs.append(change.cac_format())
270            group_msg = "%s\t%s" % (the_space.number, change.group_format())
271            for group_pk in change.groups:
272                group_data[group_pk].locker_messages.append(group_msg)
273                group_data[group_pk].changes = True
274
275    return cac_msgs
276
277def space_specific_access(the_space, group_data, old_time, new_time, ):
278    old_data = the_space.build_access(time=old_time)
279    new_data = the_space.build_access(time=new_time)
280    all_spaces[the_space.pk] = the_space
281    init_groups(group_data, old_data[2])
282    init_groups(group_data, new_data[2])
283    for group_pk, group_info in group_data.items():
284        if group_pk in old_data[0] or group_pk in new_data[0]:
285            if the_space.merged_acl:
286                group_info.add_locker_signatories(old_data[1], old_time)
287                group_info.add_locker_signatories(new_data[1], new_time)
288            else:
289                group_info.learn_office_access(the_space.pk, old_data[0], new_data[0])
290    cac_msgs = []
291    if the_space.merged_acl:
292        cac_msgs = locker_access_diff(the_space, group_data, old_data[1], new_data[1])
293    return cac_msgs
[bec7760]294
295def space_access_diffs():
296    new_time = datetime.datetime.utcnow()
[fb54faf]297    old_time = new_time - datetime.timedelta(days=1, minutes=15)
[57a2ad6]298    bulk_fill_people([old_time, new_time])
[bec7760]299    group_data = {} # Group.pk -> GroupInfo
[5680065]300    cac_locker_msgs = []
301
[59506d6]302    process_spaces =  space.models.Space.objects.all().select_related('lock_type')
[5680065]303    for the_space in process_spaces:
304        new_cac_msgs = space_specific_access(the_space, group_data, old_time, new_time)
305        if new_cac_msgs:
306            cac_locker_msgs.append("%s\n%s\n" % (the_space.number, "\n".join(new_cac_msgs)))
307
[bec7760]308    changed_groups = []
[59506d6]309    cac_chars = 0
[bec7760]310    for group_pk, group_info in group_data.items():
[6ae8c4a]311        group_info.add_office_signatories(old_time, new_time)
[59506d6]312        systems_changes, group_office_changes = group_info.list_office_changes()
[5680065]313        if group_info.changes:
[59506d6]314            cac_chars += len(systems_changes['cac-card'])
315            changed_groups.append((group_info.group, systems_changes['cac-card'], group_office_changes, group_info.locker_messages, ))
[bec7760]316
[3bca03c]317    asa_rcpts = ['asa-space-access@mit.edu']
[59506d6]318    if cac_chars > 0 or cac_locker_msgs:
[a53f8ee]319        util.emails.email_from_template(
320            tmpl='space/cac-change-email.txt',
321            context={'changed_groups': changed_groups, 'locker_msgs':cac_locker_msgs, },
322            subject="Space access updates",
323            to=['caclocks@mit.edu'],
324            cc=asa_rcpts,
[3bca03c]325            from_email='asa-db-auto@mit.edu',
[a53f8ee]326        ).send()
[a255a66]327    group_email_cc = asa_rcpts + ['caclocks@mit.edu']
[5680065]328    for group, cac_msg, group_office_msg, group_locker_msgs in changed_groups:
[bec7760]329        util.emails.email_from_template(
330            tmpl='space/group-change-email.txt',
331            context={
332                'group':group,
[5680065]333                'office_msg':group_office_msg,
334                'locker_msgs':group_locker_msgs,
[bec7760]335            },
[c8f4eea]336            subject="[ASA DB] Space access updates for %s" % (group.name, ),
[bec7760]337            to=[group.officer_email],
[a255a66]338            cc=group_email_cc,
[3bca03c]339            from_email='asa-db-auto@mit.edu',
[bec7760]340        ).send()
[5680065]341
[bec7760]342
343if __name__ == "__main__":
344    space_access_diffs()
Note: See TracBrowser for help on using the repository browser.