| 1 | from django.db import models |
|---|
| 2 | from django.contrib.auth.models import User |
|---|
| 3 | |
|---|
| 4 | import datetime |
|---|
| 5 | |
|---|
| 6 | import settings |
|---|
| 7 | |
|---|
| 8 | # Create your models here. |
|---|
| 9 | |
|---|
| 10 | class ActiveGroupManager(models.Manager): |
|---|
| 11 | def get_query_set(self, ): |
|---|
| 12 | return super(ActiveGroupManager, self).get_query_set().filter( |
|---|
| 13 | group_status__slug='active', |
|---|
| 14 | ) |
|---|
| 15 | |
|---|
| 16 | class Group(models.Model): |
|---|
| 17 | name = models.CharField(max_length=100) |
|---|
| 18 | abbreviation = models.CharField(max_length=10, blank=True) |
|---|
| 19 | description = models.TextField() |
|---|
| 20 | activity_category = models.ForeignKey('ActivityCategory', null=True, blank=True, ) |
|---|
| 21 | group_class = models.ForeignKey('GroupClass') |
|---|
| 22 | group_status = models.ForeignKey('GroupStatus') |
|---|
| 23 | group_funding = models.ForeignKey('GroupFunding', null=True, blank=True, ) |
|---|
| 24 | website_url = models.URLField() |
|---|
| 25 | constitution_url = models.CharField(max_length=200, blank=True) |
|---|
| 26 | meeting_times = models.TextField(blank=True) |
|---|
| 27 | advisor_name = models.CharField(max_length=100, blank=True) |
|---|
| 28 | num_undergrads = models.IntegerField(null=True, blank=True, ) |
|---|
| 29 | num_grads = models.IntegerField(null=True, blank=True, ) |
|---|
| 30 | num_community = models.IntegerField(null=True, blank=True, ) |
|---|
| 31 | num_other = models.IntegerField(null=True, blank=True, ) |
|---|
| 32 | group_email = models.EmailField(blank=True, ) |
|---|
| 33 | officer_email = models.EmailField() |
|---|
| 34 | main_account_id = models.IntegerField(null=True, blank=True, ) |
|---|
| 35 | funding_account_id = models.IntegerField(null=True, blank=True, ) |
|---|
| 36 | athena_locker = models.CharField(max_length=20, blank=True) |
|---|
| 37 | recognition_date = models.DateField() |
|---|
| 38 | update_date = models.DateTimeField(editable=False, ) |
|---|
| 39 | updater = models.CharField(max_length=30, editable=False, null=True, ) # match Django username field |
|---|
| 40 | _updater_set = False |
|---|
| 41 | |
|---|
| 42 | objects = models.Manager() |
|---|
| 43 | active_groups = ActiveGroupManager() |
|---|
| 44 | |
|---|
| 45 | def update_string(self, ): |
|---|
| 46 | updater = self.updater or "unknown" |
|---|
| 47 | return "%s by %s" % (self.update_date.strftime(settings.DATETIME_FORMAT_PYTHON), updater, ) |
|---|
| 48 | |
|---|
| 49 | def set_updater(self, who): |
|---|
| 50 | if hasattr(who, 'username'): |
|---|
| 51 | self.updater = who.username |
|---|
| 52 | else: |
|---|
| 53 | self.updater = who |
|---|
| 54 | self._updater_set = True |
|---|
| 55 | |
|---|
| 56 | def save(self, ): |
|---|
| 57 | if not self._updater_set: |
|---|
| 58 | self.updater = None |
|---|
| 59 | self.update_date = datetime.datetime.now() |
|---|
| 60 | super(Group, self).save() |
|---|
| 61 | |
|---|
| 62 | def viewable_notes(self, user): |
|---|
| 63 | return GroupNote.viewable_notes(self, user) |
|---|
| 64 | |
|---|
| 65 | def officers(self, role=None, person=None, as_of="now",): |
|---|
| 66 | """Get the set of people holding some office. |
|---|
| 67 | |
|---|
| 68 | If None is passed for role, person, or as_of, that field will not |
|---|
| 69 | be constrained. If as_of is "now" (default) the status will be |
|---|
| 70 | required to be current. If any of the three parameters are set |
|---|
| 71 | to another value, the corresponding filter will be applied. |
|---|
| 72 | """ |
|---|
| 73 | office_holders = OfficeHolder.objects.filter(group=self,) |
|---|
| 74 | if role: |
|---|
| 75 | if isinstance(role, str): |
|---|
| 76 | office_holders = office_holders.filter(role__slug=role) |
|---|
| 77 | else: |
|---|
| 78 | office_holders = office_holders.filter(role=role) |
|---|
| 79 | if person: |
|---|
| 80 | office_holders = office_holders.filter(person=person) |
|---|
| 81 | if as_of: |
|---|
| 82 | if as_of == "now": as_of = datetime.datetime.now() |
|---|
| 83 | office_holders = office_holders.filter(start_time__lte=as_of, end_time__gte=as_of) |
|---|
| 84 | return office_holders |
|---|
| 85 | |
|---|
| 86 | def __str__(self, ): |
|---|
| 87 | return self.name |
|---|
| 88 | |
|---|
| 89 | class Meta: |
|---|
| 90 | ordering = ('name', ) |
|---|
| 91 | permissions = ( |
|---|
| 92 | ('view_group_private_info', 'View private group information'), |
|---|
| 93 | # ability to update normal group info or people |
|---|
| 94 | # this is weaker than change_group, which is the built-in |
|---|
| 95 | # permission that controls the admin interface |
|---|
| 96 | ('admin_group', 'Administer basic group information'), |
|---|
| 97 | ('view_signatories', 'View signatory information for all groups'), |
|---|
| 98 | ) |
|---|
| 99 | |
|---|
| 100 | |
|---|
| 101 | class GroupNote(models.Model): |
|---|
| 102 | author = models.CharField(max_length=30, ) # match Django username field |
|---|
| 103 | timestamp = models.DateTimeField(default=datetime.datetime.now, editable=False, ) |
|---|
| 104 | body = models.TextField() |
|---|
| 105 | acl_read_group = models.BooleanField(default=True, help_text='Can the group read this note') |
|---|
| 106 | acl_read_offices = models.BooleanField(default=True, help_text='Can "offices" that interact with groups (SAO, CAC, and funding boards) read this note') |
|---|
| 107 | group = models.ForeignKey(Group) |
|---|
| 108 | |
|---|
| 109 | def __str__(self, ): |
|---|
| 110 | return "Note by %s on %s" % (self.author, self.timestamp, ) |
|---|
| 111 | |
|---|
| 112 | @classmethod |
|---|
| 113 | def viewable_notes(cls, group, user): |
|---|
| 114 | notes = cls.objects.filter(group=group) |
|---|
| 115 | if not user.has_perm('groups.view_note_all'): |
|---|
| 116 | q = models.Q(pk=0) |
|---|
| 117 | if user.has_perm('groups.view_note_group', group): |
|---|
| 118 | q |= models.Q(acl_read_group=True) |
|---|
| 119 | if user.has_perm('groups.view_note_office'): |
|---|
| 120 | q |= models.Q(acl_read_offices=True) |
|---|
| 121 | notes = notes.filter(q) |
|---|
| 122 | return notes |
|---|
| 123 | |
|---|
| 124 | class Meta: |
|---|
| 125 | permissions = ( |
|---|
| 126 | ('view_note_group', 'View notes intended for the group to see', ), |
|---|
| 127 | ('view_note_office', 'View notes intended for "offices" to see', ), |
|---|
| 128 | ('view_note_all', 'View all notes', ), |
|---|
| 129 | ) |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | class OfficerRole(models.Model): |
|---|
| 133 | UNLIMITED = 10000 |
|---|
| 134 | |
|---|
| 135 | display_name = models.CharField(max_length=50) |
|---|
| 136 | slug = models.SlugField() |
|---|
| 137 | description = models.TextField() |
|---|
| 138 | max_count = models.IntegerField(default=UNLIMITED, help_text='Maximum number of holders of this role. Use %d for no limit.' % UNLIMITED) |
|---|
| 139 | require_student = models.BooleanField(default=False) |
|---|
| 140 | grant_user = models.ForeignKey(User, null=True, blank=True, |
|---|
| 141 | limit_choices_to={ 'username__endswith': '@SYSTEM'}) |
|---|
| 142 | publicly_visible = models.BooleanField(default=True, help_text='Can everyone see the holders of this office.') |
|---|
| 143 | |
|---|
| 144 | def max_count_str(self, ): |
|---|
| 145 | if self.max_count == self.UNLIMITED: |
|---|
| 146 | return "unlimited" |
|---|
| 147 | else: |
|---|
| 148 | return str(self.max_count) |
|---|
| 149 | |
|---|
| 150 | def __str__(self, ): |
|---|
| 151 | return self.display_name |
|---|
| 152 | |
|---|
| 153 | @classmethod |
|---|
| 154 | def getGrantUsers(cls, roles): |
|---|
| 155 | ret = set([role.grant_user for role in roles]) |
|---|
| 156 | if None in ret: ret.remove(None) |
|---|
| 157 | return ret |
|---|
| 158 | |
|---|
| 159 | @classmethod |
|---|
| 160 | def retrieve(cls, slug, ): |
|---|
| 161 | return cls.objects.get(slug=slug) |
|---|
| 162 | |
|---|
| 163 | |
|---|
| 164 | class OfficeHolder_CurrentManager(models.Manager): |
|---|
| 165 | def get_query_set(self, ): |
|---|
| 166 | return super(OfficeHolder_CurrentManager, self).get_query_set().filter( |
|---|
| 167 | start_time__lte=datetime.datetime.now, |
|---|
| 168 | end_time__gte=datetime.datetime.now, |
|---|
| 169 | ) |
|---|
| 170 | |
|---|
| 171 | class OfficeHolder(models.Model): |
|---|
| 172 | EXPIRE_OFFSET = datetime.timedelta(seconds=1) |
|---|
| 173 | END_NEVER = datetime.datetime.max |
|---|
| 174 | |
|---|
| 175 | person = models.CharField(max_length=30) |
|---|
| 176 | role = models.ForeignKey('OfficerRole') |
|---|
| 177 | group = models.ForeignKey('Group') |
|---|
| 178 | start_time = models.DateTimeField(default=datetime.datetime.now) |
|---|
| 179 | end_time = models.DateTimeField(default=datetime.datetime.max) |
|---|
| 180 | |
|---|
| 181 | objects = models.Manager() |
|---|
| 182 | current_holders = OfficeHolder_CurrentManager() |
|---|
| 183 | |
|---|
| 184 | def expire(self, ): |
|---|
| 185 | self.end_time = datetime.datetime.now()-self.EXPIRE_OFFSET |
|---|
| 186 | self.save() |
|---|
| 187 | |
|---|
| 188 | def __str__(self, ): |
|---|
| 189 | return "<OfficeHolder: person=%s, role=%s, group=%s, start_time=%s, end_time=%s>" % ( |
|---|
| 190 | self.person, self.role, self.group, self.start_time, self.end_time, ) |
|---|
| 191 | |
|---|
| 192 | def __repr__(self, ): |
|---|
| 193 | return str(self) |
|---|
| 194 | |
|---|
| 195 | |
|---|
| 196 | class PerGroupAuthz: |
|---|
| 197 | supports_anonymous_user = True |
|---|
| 198 | supports_inactive_user = True |
|---|
| 199 | supports_object_permissions = True |
|---|
| 200 | |
|---|
| 201 | def authenticate(self, username=None, password=None, ): |
|---|
| 202 | return None # we don't do authn |
|---|
| 203 | def get_user(user_id, ): |
|---|
| 204 | return None # we don't do authn |
|---|
| 205 | |
|---|
| 206 | def has_perm(self, user_obj, perm, obj=None, ): |
|---|
| 207 | print "Checking user %s for perm %s on obj %s" % (user_obj, perm, obj) |
|---|
| 208 | if not user_obj.is_active: |
|---|
| 209 | return False |
|---|
| 210 | if not user_obj.is_authenticated(): |
|---|
| 211 | return False |
|---|
| 212 | if obj is None: |
|---|
| 213 | return False |
|---|
| 214 | # Great, we're active, authenticated, and not in a recursive call |
|---|
| 215 | # Check that we've got a reasonable object |
|---|
| 216 | if getattr(user_obj, 'is_system', False): |
|---|
| 217 | return False |
|---|
| 218 | if isinstance(obj, Group): |
|---|
| 219 | # Now we can do the real work |
|---|
| 220 | holders = obj.officers(person=user_obj.username).select_related('role__grant_user') |
|---|
| 221 | sys_users = OfficerRole.getGrantUsers([holder.role for holder in holders]) |
|---|
| 222 | for sys_user in sys_users: |
|---|
| 223 | sys_user.is_system = True |
|---|
| 224 | if sys_user.has_perm(perm): |
|---|
| 225 | print "While checking user %s for perm %s on obj %s: implicit user %s has perms" % (user_obj, perm, obj, sys_user, ) |
|---|
| 226 | return True |
|---|
| 227 | print "While checking user %s for perm %s on obj %s: no perms found (implicit: %s)" % (user_obj, perm, obj, sys_users) |
|---|
| 228 | return False |
|---|
| 229 | |
|---|
| 230 | |
|---|
| 231 | |
|---|
| 232 | class ActivityCategory(models.Model): |
|---|
| 233 | name = models.CharField(max_length=50) |
|---|
| 234 | |
|---|
| 235 | def __str__(self, ): |
|---|
| 236 | return self.name |
|---|
| 237 | |
|---|
| 238 | class Meta: |
|---|
| 239 | verbose_name_plural = "activity categories" |
|---|
| 240 | |
|---|
| 241 | |
|---|
| 242 | class GroupClass(models.Model): |
|---|
| 243 | name = models.CharField(max_length=50) |
|---|
| 244 | slug = models.SlugField(unique=True, ) |
|---|
| 245 | description = models.TextField() |
|---|
| 246 | gets_publicity = models.BooleanField(help_text="Gets publicity resources such as FYSM or Activities Midway") |
|---|
| 247 | |
|---|
| 248 | def __str__(self, ): |
|---|
| 249 | return self.name |
|---|
| 250 | |
|---|
| 251 | class Meta: |
|---|
| 252 | verbose_name_plural = "group classes" |
|---|
| 253 | |
|---|
| 254 | |
|---|
| 255 | class GroupStatus(models.Model): |
|---|
| 256 | name = models.CharField(max_length=50) |
|---|
| 257 | slug = models.SlugField(unique=True, ) |
|---|
| 258 | description = models.TextField() |
|---|
| 259 | is_active = models.BooleanField(default=True, help_text="This status represents an active group") |
|---|
| 260 | |
|---|
| 261 | def __str__(self, ): |
|---|
| 262 | active = "" |
|---|
| 263 | if not self.is_active: |
|---|
| 264 | active = " (inactive)" |
|---|
| 265 | return "%s%s" % (self.name, active, ) |
|---|
| 266 | |
|---|
| 267 | class Meta: |
|---|
| 268 | verbose_name_plural= "group statuses" |
|---|
| 269 | |
|---|
| 270 | |
|---|
| 271 | class GroupFunding(models.Model): |
|---|
| 272 | name = models.CharField(max_length=50) |
|---|
| 273 | slug = models.SlugField(unique=True, ) |
|---|
| 274 | contact_email = models.EmailField() |
|---|
| 275 | funding_list = models.CharField(max_length=32, blank=True, help_text="List that groups receiving funding emails should be on. The database will attempt to make sure that ONLY those groups are on it.") |
|---|
| 276 | |
|---|
| 277 | def __str__(self, ): |
|---|
| 278 | return "%s (%s)" % (self.name, self.contact_email, ) |
|---|
| 279 | |
|---|
| 280 | |
|---|
| 281 | class AthenaMoiraAccount_ActiveManager(models.Manager): |
|---|
| 282 | def get_query_set(self, ): |
|---|
| 283 | return super(AthenaMoiraAccount_ActiveManager, self).get_query_set().filter(del_date=None) |
|---|
| 284 | |
|---|
| 285 | class AthenaMoiraAccount(models.Model): |
|---|
| 286 | username = models.CharField(max_length=8) |
|---|
| 287 | mit_id = models.CharField(max_length=15) |
|---|
| 288 | first_name = models.CharField(max_length=45) |
|---|
| 289 | last_name = models.CharField(max_length=45) |
|---|
| 290 | account_class = models.CharField(max_length=10) |
|---|
| 291 | mutable = models.BooleanField(default=True) |
|---|
| 292 | add_date = models.DateField(help_text="Date when this person was added to the dump.", ) |
|---|
| 293 | del_date = models.DateField(help_text="Date when this person was removed from the dump.", blank=True, null=True, ) |
|---|
| 294 | mod_date = models.DateField(help_text="Date when this person's record was last changed.", blank=True, null=True, ) |
|---|
| 295 | |
|---|
| 296 | objects = models.Manager() |
|---|
| 297 | active_accounts = AthenaMoiraAccount_ActiveManager() |
|---|
| 298 | |
|---|
| 299 | def is_student(self, ): |
|---|
| 300 | # XXX: Is this... right? |
|---|
| 301 | return self.account_class == 'G' or self.account_class.isdigit() |
|---|
| 302 | |
|---|
| 303 | def __str__(self, ): |
|---|
| 304 | if self.mutable: |
|---|
| 305 | mutable_str = "" |
|---|
| 306 | else: |
|---|
| 307 | mutable_str = " (immutable)" |
|---|
| 308 | return "<AthenaMoiraAccount: username=%s name='%s, %s' account_class=%s%s>" % ( |
|---|
| 309 | self.username, self.last_name, self.first_name, |
|---|
| 310 | self.account_class, mutable_str, |
|---|
| 311 | ) |
|---|
| 312 | |
|---|
| 313 | def __repr__(self, ): |
|---|
| 314 | return str(self) |
|---|
| 315 | |
|---|
| 316 | class Meta: |
|---|
| 317 | verbose_name = "Athena (Moira) account" |
|---|