source: asadb/space/diffs.py @ 492251a

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

Comment space/diffs.py's locker code a bit better

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