source: asadb/space/diffs.py @ a53f8ee

space-accessstablestage
Last change on this file since a53f8ee was a53f8ee, checked in by MIT Association of Student Activities <asa@…>, 14 years ago

Don't email CAC if they don't need to do anything

  • Property mode set to 100755
File size: 11.8 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.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
24role = {
25    'office': groups.models.OfficerRole.objects.get(slug='office-access'),
26    'locker': groups.models.OfficerRole.objects.get(slug='locker-access'),
27}
28
29people_name = {} # username -> full name
30people_id = {} # username -> MIT ID
31
32all_spaces = {} # Space.pk -> Space
33
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
48def fill_people(holder):
49    if not holder.person in people_name:
50        #print "Person %s not pre-cached" % (holder.person, )
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))
63        self.locker_acl = {}
64        self.locker_messages = []
65        self.changes = False
66
67    def learn_office_access(self, space_pk, old, new):
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():
81            space_data[mit_id][0].update(old_set)
82        for mit_id, new_set in new_access.items():
83            space_data[mit_id][1].update(new_set)
84
85    def add_office_signatories_per_time(self, ind, time):
86        group = self.group
87        people = group.officers(as_of=time, role=role['office'])
88        for holder in people:
89            fill_people(holder)
90        for office_id, office_data in self.offices.items():
91            for holder in people:
92                holder_name = people_name[holder.person]
93                holder_id = people_id[holder.person]
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)
100
101    def list_office_changes(self, ):
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:
116                    self.changes = True
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)
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)
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
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
171
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):
179    name = change.name
180    if name in change_by_name:
181        if change_by_name[name].verb != change.verb or change_by_name[name].groups != change.groups:
182            print "change_by_name=%s" % (change_by_name, )
183            print "change: old=%s; new=%s" % (change_by_name[name], change)
184            assert False
185    else:
186        change_by_name[name] = change
187
188def locker_access_diff(the_space, group_data, old_access, new_access, ):
189    cac_msgs = [] # [String]
190    for mit_id in joint_keys(old_access, new_access):
191        change_by_name = {} # name -> LockerAccessChangeEntry
192        def safe_add(change):
193            safe_add_change_real(change_by_name, change)
194        old_by_names = old_access[mit_id]
195        new_by_names = new_access[mit_id]
196        old_by_group = flip_dict(old_by_names)
197        new_by_group = flip_dict(new_by_names)
198        unchanged = (old_by_names == new_by_names)
199        if unchanged: continue
200        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, ),
201        for group_pk in joint_keys(old_by_group, new_by_group):
202            old_names = old_by_group[group_pk]
203            new_names = new_by_group[group_pk]
204            for name in old_names.union(new_names):
205                changed_groups = old_by_names[name] ^ new_by_names[name]
206
207                def mkchange(verb):
208                    change = LockerAccessChangeEntry(
209                        mit_id=mit_id,
210                        verb=verb,
211                        name=name,
212                        groups=changed_groups,
213                    )
214                    if verb == "Keep":
215                        change.group_msgs = "(other groups involved)"
216                    change.cac_msgs = "(groups: %s -> %s)" % (old_by_names[name], new_by_names[name])
217                    return change
218
219                if name in old_names and name in new_names: # keep
220                    safe_add(mkchange("Keep"))
221                elif name in old_names: # remove from this group
222                    if new_by_names[name]: # keep b/c other groups
223                        safe_add(mkchange("Keep"))
224                    else:
225                        safe_add(mkchange("Remove"))
226                elif name in new_names: # add for this group
227                    if old_by_names[name]: # keep b/c other groups
228                        safe_add(mkchange("Keep"))
229                    else:
230                        safe_add(mkchange("Add"))
231                else:
232                    assert False, "in old_names or new_names, but not in both, one, or the other..."
233
234        # Handle reporting the results...
235        for change in change_by_name.values():
236            cac_msgs.append(change.cac_format())
237            group_msg = "%s\t%s" % (the_space.number, change.group_format())
238            for group_pk in change.groups:
239                group_data[group_pk].locker_messages.append(group_msg)
240                group_data[group_pk].changes = True
241
242    return cac_msgs
243
244def space_specific_access(the_space, group_data, old_time, new_time, ):
245    old_data = the_space.build_access(time=old_time)
246    new_data = the_space.build_access(time=new_time)
247    all_spaces[the_space.pk] = the_space
248    init_groups(group_data, old_data[2])
249    init_groups(group_data, new_data[2])
250    for group_pk, group_info in group_data.items():
251        if group_pk in old_data[0] or group_pk in new_data[0]:
252            if the_space.merged_acl:
253                group_info.add_locker_signatories(old_data[1], old_time)
254                group_info.add_locker_signatories(new_data[1], new_time)
255            else:
256                group_info.learn_office_access(the_space.pk, old_data[0], new_data[0])
257    cac_msgs = []
258    if the_space.merged_acl:
259        cac_msgs = locker_access_diff(the_space, group_data, old_data[1], new_data[1])
260    return cac_msgs
261
262def space_access_diffs():
263    new_time = datetime.datetime.utcnow()
264    old_time = new_time - datetime.timedelta(days=1)
265    bulk_fill_people([old_time, new_time])
266    group_data = {} # Group.pk -> GroupInfo
267    cac_locker_msgs = []
268
269    process_spaces =  space.models.Space.objects.all()
270    for the_space in process_spaces:
271        new_cac_msgs = space_specific_access(the_space, group_data, old_time, new_time)
272        if new_cac_msgs:
273            cac_locker_msgs.append("%s\n%s\n" % (the_space.number, "\n".join(new_cac_msgs)))
274
275    changed_groups = []
276    for group_pk, group_info in group_data.items():
277        group_info.add_office_signatories(old_time, new_time)
278        cac_changes, group_office_changes = group_info.list_office_changes()
279        if group_info.changes:
280            changed_groups.append((group_info.group, cac_changes, group_office_changes, group_info.locker_messages, ))
281
282    asa_rcpts = ['asa-space@mit.edu', 'asa-db@mit.edu', ]
283    if changed_groups:
284        util.emails.email_from_template(
285            tmpl='space/cac-change-email.txt',
286            context={'changed_groups': changed_groups, 'locker_msgs':cac_locker_msgs, },
287            subject="Space access updates",
288            to=['caclocks@mit.edu'],
289            cc=asa_rcpts,
290        ).send()
291    for group, cac_msg, group_office_msg, group_locker_msgs in changed_groups:
292        util.emails.email_from_template(
293            tmpl='space/group-change-email.txt',
294            context={
295                'group':group,
296                'office_msg':group_office_msg,
297                'locker_msgs':group_locker_msgs,
298            },
299            subject="[ASA DB] Space access updates",
300            to=[group.officer_email],
301            cc=asa_rcpts,
302        ).send()
303
304
305if __name__ == "__main__":
306    space_access_diffs()
Note: See TracBrowser for help on using the repository browser.