[bec7760] | 1 | import collections |
---|
[a86a924] | 2 | import datetime |
---|
| 3 | |
---|
| 4 | from django.db import models |
---|
[bec7760] | 5 | from django.db.models import Q |
---|
[a86a924] | 6 | |
---|
| 7 | import reversion |
---|
| 8 | |
---|
| 9 | import groups.models |
---|
| 10 | |
---|
[9dfb3d5] | 11 | EXPIRE_OFFSET = datetime.timedelta(seconds=1) |
---|
[a86a924] | 12 | |
---|
[e2ceffa] | 13 | LOCK_DB_UPDATE_NONE = 'none' |
---|
[1eee8d1] | 14 | LOCK_DB_UPDATE_CAC_CARD = 'cac-card' |
---|
| 15 | lock_db_update_choices = ( |
---|
| 16 | (LOCK_DB_UPDATE_NONE, "No database management"), |
---|
| 17 | (LOCK_DB_UPDATE_CAC_CARD, "CAC-managed card-based access"), |
---|
| 18 | ) |
---|
| 19 | |
---|
[d0bfc27] | 20 | class LockType(models.Model): |
---|
| 21 | name = models.CharField(max_length=50) |
---|
| 22 | slug = models.SlugField(unique=True, ) |
---|
[1eee8d1] | 23 | description = models.TextField() |
---|
[00017c8] | 24 | info_addr = models.EmailField(default='asa-exec@mit.edu', help_text='Address groups should email to get more information about managing access through this lock type.') |
---|
[8aea837] | 25 | info_url = models.URLField(blank=True, help_text='URL that groups can visit to get more information about this lock type.') |
---|
[e2ceffa] | 26 | db_update = models.CharField(max_length=20, default='none', choices=lock_db_update_choices) |
---|
| 27 | |
---|
| 28 | def __unicode__(self, ): |
---|
| 29 | return self.name |
---|
| 30 | |
---|
[d0bfc27] | 31 | |
---|
[a86a924] | 32 | class Space(models.Model): |
---|
| 33 | number = models.CharField(max_length=20, unique=True, ) |
---|
| 34 | asa_owned = models.BooleanField(default=True, ) |
---|
[1eee8d1] | 35 | lock_type = models.ForeignKey(LockType) |
---|
[5680065] | 36 | merged_acl = models.BooleanField(default=False, help_text="Does this room have a single merged ACL, that combines all groups together, or CAC maintain a separate ACL per-group? Generally, the shared storage offices get a merged ACL and everything else doesn't.") |
---|
[a86a924] | 37 | notes = models.TextField(blank=True, ) |
---|
| 38 | |
---|
| 39 | def __unicode__(self, ): |
---|
| 40 | if self.asa_owned: |
---|
| 41 | asa_str = "ASA" |
---|
| 42 | else: |
---|
| 43 | asa_str = "Non-ASA" |
---|
| 44 | return u"%s (%s)" % (self.number, asa_str) |
---|
[bec7760] | 45 | |
---|
| 46 | def build_access(self, time=None, group=None, ): |
---|
| 47 | """Assemble a list of who had access to this Space. |
---|
| 48 | |
---|
| 49 | time: |
---|
| 50 | optional; indicate that you want access as of a particular time. |
---|
| 51 | If omitted, uses the present. |
---|
| 52 | group: |
---|
| 53 | optional; indicates that you want access via a particular group. |
---|
| 54 | If omitted, finds access via any group. |
---|
| 55 | |
---|
| 56 | Return value: |
---|
| 57 | tuple (access, assignments, aces, errors) |
---|
| 58 | access is the main field that matters, but the others are potentially useful supplementary information |
---|
| 59 | |
---|
| 60 | access: |
---|
| 61 | Group.pk -> (ID -> Set name) |
---|
| 62 | Indicates who has access. Grouped by group and ID number. |
---|
| 63 | Usually, the sets will each have one member, but ID 999999999 is decently likely to have several. |
---|
| 64 | The SpaceAccessListEntrys will be filtered to reflect assignments as of that time. |
---|
[94f3a39] | 65 | access_by_id: |
---|
| 66 | ID -> (Name -> (Set Group.pk)) |
---|
| 67 | Indicates who has access. Grouped by ID number and name. |
---|
| 68 | Usually, each ID dict will have one member, but ID 999999999 is, again, likely to have several. |
---|
| 69 | This is intended for rooms that have one access list (e.g., W20-437 and W20-441) |
---|
[bec7760] | 70 | assignments: |
---|
| 71 | [SpaceAssignment] |
---|
| 72 | QuerySet of all SpaceAssignments involving the space and group at the time |
---|
| 73 | aces: |
---|
| 74 | [SpaceAccessListEntry] |
---|
| 75 | QuerySet of all SpaceAccessListEntrys involving the space and group at the time. |
---|
| 76 | This is not filtered for the ace's group having a relevant SpaceAssignment. |
---|
| 77 | errors: |
---|
| 78 | [String] |
---|
| 79 | errors/warnings that occurred. |
---|
| 80 | Includes messages about groups no longer having access. |
---|
| 81 | """ |
---|
| 82 | |
---|
| 83 | if time is None: |
---|
| 84 | time = datetime.datetime.now() |
---|
| 85 | errors = [] |
---|
| 86 | time_q = Q(end__gte=time, start__lte=time) |
---|
| 87 | assignments = SpaceAssignment.objects.filter(time_q, space=self) |
---|
| 88 | aces = SpaceAccessListEntry.objects.filter(time_q, space=self) |
---|
| 89 | if group: |
---|
| 90 | assignments = assignments.filter(group=group) |
---|
| 91 | aces = aces.filter(group=group) |
---|
| 92 | access = {} # Group.pk -> (ID -> Set name) |
---|
[5680065] | 93 | # ID -> (Name -> (Set Group.pk)) |
---|
| 94 | access_by_id = collections.defaultdict(lambda: collections.defaultdict(set)) |
---|
[bec7760] | 95 | for assignment in assignments: |
---|
| 96 | if assignment.group.pk not in access: |
---|
| 97 | access[assignment.group.pk] = collections.defaultdict(set) |
---|
| 98 | for ace in aces: |
---|
| 99 | if ace.group.pk in access: |
---|
| 100 | access[ace.group.pk][ace.card_number].add(ace.name) |
---|
[94f3a39] | 101 | access_by_id[ace.card_number][ace.name].add(ace.group.pk) |
---|
[bec7760] | 102 | else: |
---|
| 103 | # This group appears to no longer have access... |
---|
| 104 | errors.append("Group %s no longer has access to %s, but has live ACEs." % (ace.group, self, )) |
---|
[94f3a39] | 105 | return access, access_by_id, assignments, aces, errors |
---|
[bec7760] | 106 | |
---|
[a86a924] | 107 | reversion.register(Space) |
---|
| 108 | |
---|
[6ff04b1] | 109 | |
---|
| 110 | class CurrentAssignmentManager(models.Manager): |
---|
| 111 | def get_query_set(self, ): |
---|
| 112 | return super(CurrentAssignmentManager, self).get_query_set().filter( |
---|
| 113 | start__lte=datetime.date.today, |
---|
| 114 | end__gte=datetime.date.today, |
---|
| 115 | ) |
---|
| 116 | |
---|
[a86a924] | 117 | class SpaceAssignment(models.Model): |
---|
| 118 | END_NEVER = datetime.datetime.max |
---|
| 119 | |
---|
[a03cb61] | 120 | group = models.ForeignKey(groups.models.Group, db_index=True, ) |
---|
| 121 | space = models.ForeignKey(Space, db_index=True, ) |
---|
| 122 | start = models.DateField(default=datetime.datetime.now, db_index=True, ) |
---|
| 123 | end = models.DateField(default=END_NEVER, db_index=True, ) |
---|
[a86a924] | 124 | |
---|
| 125 | notes = models.TextField(blank=True, ) |
---|
| 126 | locker_num = models.CharField(max_length=10, blank=True, help_text='Locker number. If set, will use the "locker-access" OfficerRole to maintain access. If unset/blank, uses "office-access" and SpaceAccessListEntry for access.') |
---|
| 127 | |
---|
[6ff04b1] | 128 | objects = models.Manager() |
---|
| 129 | current = CurrentAssignmentManager() |
---|
| 130 | |
---|
[a86a924] | 131 | def expire(self, ): |
---|
| 132 | self.end_time = datetime.datetime.now()-self.EXPIRE_OFFSET |
---|
| 133 | self.save() |
---|
| 134 | |
---|
[6ff04b1] | 135 | def is_locker(self, ): |
---|
| 136 | return bool(self.locker_num) |
---|
| 137 | |
---|
| 138 | def __unicode__(self, ): |
---|
| 139 | return u"<SpaceAssignment group=%s space=%s locker=%s start=%s end=%s>" % ( |
---|
| 140 | self.group, |
---|
| 141 | self.space, |
---|
| 142 | self.locker_num, |
---|
| 143 | self.start, |
---|
| 144 | self.end, |
---|
| 145 | ) |
---|
| 146 | |
---|
| 147 | |
---|
[d7557b8] | 148 | groups.models.filter_registry.register( |
---|
[62f73df] | 149 | category='space', |
---|
[d7557b8] | 150 | slug='space:owners', |
---|
| 151 | name='Space owners', |
---|
| 152 | desc='Groups with space', |
---|
| 153 | qs_thunk=lambda: SpaceAssignment.current.values('group'), |
---|
| 154 | ) |
---|
| 155 | |
---|
[2563230] | 156 | def assignment_filter(building=None, locker=None): |
---|
| 157 | assign = SpaceAssignment.current.all() |
---|
| 158 | if building: |
---|
| 159 | assign = assign.filter( |
---|
| 160 | space__number__startswith="%s-" % (building, ), |
---|
| 161 | ) |
---|
| 162 | if locker == True: |
---|
| 163 | assign = assign.exclude(locker_num="") |
---|
| 164 | elif locker == False: |
---|
| 165 | assign = assign.filter(locker_num="") |
---|
| 166 | owners = groups.models.Group.objects.filter(pk__in=assign.values('group')) |
---|
[d7557b8] | 167 | return owners |
---|
| 168 | |
---|
| 169 | groups.models.filter_registry.register( |
---|
[62f73df] | 170 | category='space', |
---|
[2563230] | 171 | slug='space:locker', |
---|
| 172 | name='Locker owners', |
---|
| 173 | desc='Owners of lockers', |
---|
| 174 | qs_thunk=lambda: assignment_filter(locker=True), |
---|
| 175 | ) |
---|
| 176 | groups.models.filter_registry.register( |
---|
| 177 | category='space', |
---|
| 178 | slug='space:office', |
---|
| 179 | name='Office owners', |
---|
| 180 | desc='Owners of offices', |
---|
| 181 | qs_thunk=lambda: assignment_filter(locker=False), |
---|
| 182 | ) |
---|
| 183 | groups.models.filter_registry.register( |
---|
| 184 | category='space', |
---|
[d7557b8] | 185 | slug='space:w20', |
---|
| 186 | name='W20 owners', |
---|
| 187 | desc='Owners of W20 space', |
---|
[2563230] | 188 | qs_thunk=lambda: assignment_filter(building='W20'), |
---|
[d7557b8] | 189 | ) |
---|
| 190 | groups.models.filter_registry.register( |
---|
[62f73df] | 191 | category='space', |
---|
[d7557b8] | 192 | slug='space:walker', |
---|
| 193 | name='Walker owners', |
---|
| 194 | desc='Owners of Walker space', |
---|
[2563230] | 195 | qs_thunk=lambda: assignment_filter(building='50'), |
---|
[d7557b8] | 196 | ) |
---|
| 197 | |
---|
[2563230] | 198 | |
---|
[6ff04b1] | 199 | class CurrentACLEntryManager(models.Manager): |
---|
| 200 | def get_query_set(self, ): |
---|
| 201 | return super(CurrentACLEntryManager, self).get_query_set().filter( |
---|
| 202 | start__lte=datetime.datetime.now, |
---|
| 203 | end__gte=datetime.datetime.now, |
---|
| 204 | ) |
---|
| 205 | |
---|
[9dfb3d5] | 206 | def now_offset(): |
---|
| 207 | return datetime.datetime.now()-EXPIRE_OFFSET |
---|
| 208 | |
---|
[a86a924] | 209 | class SpaceAccessListEntry(models.Model): |
---|
| 210 | END_NEVER = datetime.datetime.max |
---|
| 211 | |
---|
[a03cb61] | 212 | group = models.ForeignKey(groups.models.Group, db_index=True, ) |
---|
| 213 | space = models.ForeignKey(Space, db_index=True, ) |
---|
| 214 | start = models.DateTimeField(default=now_offset, db_index=True, ) |
---|
| 215 | end = models.DateTimeField(default=END_NEVER, db_index=True, ) |
---|
[a86a924] | 216 | |
---|
| 217 | name = models.CharField(max_length=50) |
---|
[7eea15c] | 218 | card_number = models.CharField(max_length=20, verbose_name="MIT ID", help_text="MIT ID number (as printed on, eg, the relevant ID card)") |
---|
[a86a924] | 219 | |
---|
[6ff04b1] | 220 | objects = models.Manager() |
---|
| 221 | current = CurrentACLEntryManager() |
---|
| 222 | |
---|
[a86a924] | 223 | def expire(self, ): |
---|
[9dfb3d5] | 224 | self.end = now_offset() |
---|
[a86a924] | 225 | self.save() |
---|
[6ff04b1] | 226 | |
---|
| 227 | def format_name(self, ): |
---|
| 228 | return u"%s (%s)" % (self.name, self.card_number, ) |
---|
| 229 | |
---|
| 230 | def __unicode__(self, ): |
---|
| 231 | return u"<SpaceAccessListEntry group=%s space=%s name=%s start=%s end=%s>" % ( |
---|
| 232 | self.group, |
---|
| 233 | self.space, |
---|
| 234 | self.name, |
---|
| 235 | self.start, |
---|
| 236 | self.end, |
---|
| 237 | ) |
---|