| 1 | from django.conf import settings |
|---|
| 2 | from django.db import models |
|---|
| 3 | from django.core.validators import RegexValidator |
|---|
| 4 | from django.contrib.auth.models import User |
|---|
| 5 | from django.template.defaultfilters import slugify |
|---|
| 6 | import reversion |
|---|
| 7 | |
|---|
| 8 | import datetime |
|---|
| 9 | import filecmp |
|---|
| 10 | import mimetypes |
|---|
| 11 | import os |
|---|
| 12 | import re |
|---|
| 13 | import shutil |
|---|
| 14 | import subprocess |
|---|
| 15 | import urlparse |
|---|
| 16 | import urllib |
|---|
| 17 | import urllib2 |
|---|
| 18 | |
|---|
| 19 | import mit |
|---|
| 20 | |
|---|
| 21 | # Create your models here. |
|---|
| 22 | |
|---|
| 23 | class ActiveGroupManager(models.Manager): |
|---|
| 24 | def get_query_set(self, ): |
|---|
| 25 | return super(ActiveGroupManager, self).get_query_set().filter( |
|---|
| 26 | group_status__slug='active', |
|---|
| 27 | ) |
|---|
| 28 | |
|---|
| 29 | locker_validator = RegexValidator(regex=r'^[-A-Za-z0-9_.]+$', message='Enter a valid Athena locker.') |
|---|
| 30 | |
|---|
| 31 | class Group(models.Model): |
|---|
| 32 | name = models.CharField(max_length=100, db_index=True, ) |
|---|
| 33 | abbreviation = models.CharField(max_length=10, blank=True, db_index=True, ) |
|---|
| 34 | description = models.TextField(blank=True, ) |
|---|
| 35 | activity_category = models.ForeignKey('ActivityCategory', null=True, blank=True, db_index=True, ) |
|---|
| 36 | group_class = models.ForeignKey('GroupClass', db_index=True, ) |
|---|
| 37 | group_status = models.ForeignKey('GroupStatus', db_index=True, ) |
|---|
| 38 | group_funding = models.ForeignKey('GroupFunding', null=True, blank=True, db_index=True, ) |
|---|
| 39 | website_url = models.URLField(blank=True, ) |
|---|
| 40 | constitution_url = models.CharField(max_length=200, blank=True, validators=[mit.UrlOrAfsValidator]) |
|---|
| 41 | meeting_times = models.TextField(blank=True) |
|---|
| 42 | advisor_name = models.CharField(max_length=100, blank=True) |
|---|
| 43 | num_undergrads = models.IntegerField(null=True, blank=True, ) |
|---|
| 44 | num_grads = models.IntegerField(null=True, blank=True, ) |
|---|
| 45 | num_community = models.IntegerField(null=True, blank=True, ) |
|---|
| 46 | num_other = models.IntegerField(null=True, blank=True, ) |
|---|
| 47 | group_email = models.EmailField(verbose_name="group email list", blank=True, ) |
|---|
| 48 | officer_email = models.EmailField(verbose_name="officers' email list") |
|---|
| 49 | main_account_id = models.IntegerField(null=True, blank=True, ) |
|---|
| 50 | funding_account_id = models.IntegerField(null=True, blank=True, ) |
|---|
| 51 | athena_locker = models.CharField(max_length=20, blank=True, validators=[locker_validator]) |
|---|
| 52 | recognition_date = models.DateTimeField() |
|---|
| 53 | update_date = models.DateTimeField(editable=False, ) |
|---|
| 54 | updater = models.CharField(max_length=30, editable=False, null=True, ) # match Django username field |
|---|
| 55 | _updater_set = False |
|---|
| 56 | |
|---|
| 57 | objects = models.Manager() |
|---|
| 58 | active_groups = ActiveGroupManager() |
|---|
| 59 | |
|---|
| 60 | def update_string(self, ): |
|---|
| 61 | updater = self.updater or "unknown" |
|---|
| 62 | return "%s by %s" % (self.update_date.strftime(settings.DATETIME_FORMAT_PYTHON), updater, ) |
|---|
| 63 | |
|---|
| 64 | def set_updater(self, who): |
|---|
| 65 | if hasattr(who, 'username'): |
|---|
| 66 | self.updater = who.username |
|---|
| 67 | else: |
|---|
| 68 | self.updater = who |
|---|
| 69 | self._updater_set = True |
|---|
| 70 | |
|---|
| 71 | def save(self, ): |
|---|
| 72 | if not self._updater_set: |
|---|
| 73 | self.updater = None |
|---|
| 74 | self.update_date = datetime.datetime.now() |
|---|
| 75 | super(Group, self).save() |
|---|
| 76 | |
|---|
| 77 | def viewable_notes(self, user): |
|---|
| 78 | return GroupNote.viewable_notes(self, user) |
|---|
| 79 | |
|---|
| 80 | def officers(self, role=None, person=None, as_of="now",): |
|---|
| 81 | """Get the set of people holding some office. |
|---|
| 82 | |
|---|
| 83 | If None is passed for role, person, or as_of, that field will not |
|---|
| 84 | be constrained. If as_of is "now" (default) the status will be |
|---|
| 85 | required to be current. If any of the three parameters are set |
|---|
| 86 | to another value, the corresponding filter will be applied. |
|---|
| 87 | """ |
|---|
| 88 | office_holders = OfficeHolder.objects.filter(group=self,) |
|---|
| 89 | if role: |
|---|
| 90 | if isinstance(role, str): |
|---|
| 91 | office_holders = office_holders.filter(role__slug=role) |
|---|
| 92 | else: |
|---|
| 93 | office_holders = office_holders.filter(role=role) |
|---|
| 94 | if person: |
|---|
| 95 | office_holders = office_holders.filter(person=person) |
|---|
| 96 | if as_of: |
|---|
| 97 | if as_of == "now": as_of = datetime.datetime.now() |
|---|
| 98 | office_holders = office_holders.filter(start_time__lte=as_of, end_time__gte=as_of) |
|---|
| 99 | return office_holders |
|---|
| 100 | |
|---|
| 101 | def slug(self, ): |
|---|
| 102 | return slugify(self.name) |
|---|
| 103 | |
|---|
| 104 | def __str__(self, ): |
|---|
| 105 | return self.name |
|---|
| 106 | |
|---|
| 107 | @staticmethod |
|---|
| 108 | def involved_groups(username): |
|---|
| 109 | current_officers = OfficeHolder.current_holders.filter(person=username) |
|---|
| 110 | users_groups = Group.objects.filter(officeholder__in=current_officers).distinct() |
|---|
| 111 | return users_groups |
|---|
| 112 | |
|---|
| 113 | @classmethod |
|---|
| 114 | def reporting_prefetch(cls, ): |
|---|
| 115 | return set(['activity_category', 'group_class', 'group_status', 'group_funding']) |
|---|
| 116 | |
|---|
| 117 | @classmethod |
|---|
| 118 | def reporting_fields(cls, ): |
|---|
| 119 | fields = cls._meta.fields |
|---|
| 120 | return [(f.name, f.verbose_name) for f in fields] |
|---|
| 121 | |
|---|
| 122 | class Meta: |
|---|
| 123 | ordering = ('name', ) |
|---|
| 124 | permissions = ( |
|---|
| 125 | ('view_group_private_info', 'View private group information'), |
|---|
| 126 | # ability to update normal group info or people |
|---|
| 127 | # this is weaker than change_group, which is the built-in |
|---|
| 128 | # permission that controls the admin interface |
|---|
| 129 | ('admin_group', 'Administer basic group information'), |
|---|
| 130 | ('view_signatories', 'View signatory information for all groups'), |
|---|
| 131 | ('recognize_nge', 'Recognize Non-Group Entity'), |
|---|
| 132 | ('recognize_group', 'Recognize groups'), |
|---|
| 133 | ) |
|---|
| 134 | reversion.register(Group) |
|---|
| 135 | |
|---|
| 136 | |
|---|
| 137 | constitution_dir = os.path.join(settings.SITE_ROOT, '..', 'constitutions') |
|---|
| 138 | |
|---|
| 139 | class GroupConstitution(models.Model): |
|---|
| 140 | group = models.ForeignKey(Group, unique=True, ) |
|---|
| 141 | source_url = models.URLField() |
|---|
| 142 | dest_file = models.CharField(max_length=100) |
|---|
| 143 | last_update = models.DateTimeField(help_text='Last time when this constitution actually changed.') |
|---|
| 144 | last_download = models.DateTimeField(help_text='Last time we downloaded this constitution to see if it had changed.') |
|---|
| 145 | failure_date = models.DateTimeField(null=True, blank=True, default=None, help_text='Time this URL started failing to download. (Null if currently working.)') |
|---|
| 146 | status_msg = models.CharField(max_length=100) |
|---|
| 147 | failure_reason = models.CharField(max_length=100, blank=True, default="") |
|---|
| 148 | |
|---|
| 149 | def record_failure(self, msg): |
|---|
| 150 | now = datetime.datetime.now() |
|---|
| 151 | if not self.failure_date: |
|---|
| 152 | self.failure_date = now |
|---|
| 153 | self.status_msg = msg |
|---|
| 154 | self.failure_reason = self.status_msg |
|---|
| 155 | self.save() |
|---|
| 156 | |
|---|
| 157 | def record_success(self, msg, updated): |
|---|
| 158 | now = datetime.datetime.now() |
|---|
| 159 | if updated: |
|---|
| 160 | self.last_update = now |
|---|
| 161 | self.status_msg = msg |
|---|
| 162 | self.last_download = now |
|---|
| 163 | self.failure_date = None |
|---|
| 164 | self.failure_reason = "" |
|---|
| 165 | self.save() |
|---|
| 166 | |
|---|
| 167 | def update(self, ): |
|---|
| 168 | url = self.source_url |
|---|
| 169 | success = None |
|---|
| 170 | old_success = (self.failure_date is None) |
|---|
| 171 | if url and '/asa/start/constitution-req.html' not in url: |
|---|
| 172 | # Fetch the file |
|---|
| 173 | error_msg = None |
|---|
| 174 | try: |
|---|
| 175 | new_mimetype = None |
|---|
| 176 | if url.startswith('/afs/') or url.startswith('/mit/'): |
|---|
| 177 | new_data = mit.pag_check_output(['/bin/cat', url], aklog=False, stderr=subprocess.STDOUT) |
|---|
| 178 | else: |
|---|
| 179 | new_fp = urllib2.urlopen(url) |
|---|
| 180 | if new_fp.info().getheader('Content-Type'): |
|---|
| 181 | new_mimetype = new_fp.info().gettype() |
|---|
| 182 | new_data = new_fp.read() |
|---|
| 183 | new_fp.close() |
|---|
| 184 | except urllib2.HTTPError, e: |
|---|
| 185 | error_msg = "HTTPError: %s %s" % (e.code, e.msg) |
|---|
| 186 | except urllib2.URLError, e: |
|---|
| 187 | error_msg = "URLError: %s" % (e.reason) |
|---|
| 188 | except subprocess.CalledProcessError, e: |
|---|
| 189 | results = e.output.split(": ") |
|---|
| 190 | if len(results) == 3 and results[0] == '/bin/cat' and results[1] == url: |
|---|
| 191 | cat_err = results[2] |
|---|
| 192 | else: |
|---|
| 193 | cat_err = e.output |
|---|
| 194 | error_msg = "CalledProcessError %d: %s" % (e.returncode, cat_err) |
|---|
| 195 | except IOError: |
|---|
| 196 | error_msg = "IOError" |
|---|
| 197 | except ValueError, e: |
|---|
| 198 | if e.args[0].startswith('unknown url type'): |
|---|
| 199 | error_msg = "unknown url type" |
|---|
| 200 | else: |
|---|
| 201 | raise |
|---|
| 202 | if error_msg: |
|---|
| 203 | self.record_failure(error_msg) |
|---|
| 204 | return (False, self.status_msg, old_success, ) |
|---|
| 205 | |
|---|
| 206 | # At this point, failures are our fault, not the group's. |
|---|
| 207 | # We can let any errors bubble all the way up, rather than |
|---|
| 208 | # trying to catch and neatly record them |
|---|
| 209 | success = True |
|---|
| 210 | |
|---|
| 211 | # Find a destination, and how to put it there |
|---|
| 212 | old_path = self.path_from_filename(self.dest_file) |
|---|
| 213 | new_filename = self.compute_filename(url, new_mimetype, ) |
|---|
| 214 | |
|---|
| 215 | # Process the update |
|---|
| 216 | if new_filename != self.dest_file: # new filename |
|---|
| 217 | if self.dest_file: |
|---|
| 218 | if os.path.exists(old_path): |
|---|
| 219 | os.remove(old_path) |
|---|
| 220 | else: |
|---|
| 221 | print "Warning: %s doesn't exist, but is referenced by dest_file" % (old_path, ) |
|---|
| 222 | self.dest_file = new_filename |
|---|
| 223 | new_path = self.path_from_filename(new_filename) |
|---|
| 224 | with open(new_path, 'wb') as fp: |
|---|
| 225 | fp.write(new_data) |
|---|
| 226 | self.record_success("new path", updated=True) |
|---|
| 227 | else: # old filename |
|---|
| 228 | with open(old_path, 'rb') as old_fp: |
|---|
| 229 | old_data = old_fp.read() |
|---|
| 230 | if old_data == new_data: # unchanged |
|---|
| 231 | self.record_success("no change", updated=False) |
|---|
| 232 | else: # changed |
|---|
| 233 | with open(old_path, 'wb') as fp: |
|---|
| 234 | fp.write(new_data) |
|---|
| 235 | self.record_success("updated in place", updated=True) |
|---|
| 236 | |
|---|
| 237 | elif '/asa/start/constitution-req.html' in url: |
|---|
| 238 | self.record_failure("constitution-req used") |
|---|
| 239 | success = False |
|---|
| 240 | else: |
|---|
| 241 | self.record_failure("no url") |
|---|
| 242 | success = False |
|---|
| 243 | |
|---|
| 244 | return (success, self.status_msg, old_success, ) |
|---|
| 245 | |
|---|
| 246 | def compute_filename(self, url, mimetype): |
|---|
| 247 | slug = self.group.slug() |
|---|
| 248 | known_ext = set([ |
|---|
| 249 | '.pdf', |
|---|
| 250 | '.ps', |
|---|
| 251 | '.doc', |
|---|
| 252 | '.rtf', |
|---|
| 253 | '.html', |
|---|
| 254 | '.tex', |
|---|
| 255 | '.txt' |
|---|
| 256 | ]) |
|---|
| 257 | |
|---|
| 258 | # This probably breaks on Windows. But that's probably true of |
|---|
| 259 | # everything... |
|---|
| 260 | path = urlparse.urlparse(url).path |
|---|
| 261 | basename, fileext = os.path.splitext(path) |
|---|
| 262 | |
|---|
| 263 | if fileext: |
|---|
| 264 | ext = fileext |
|---|
| 265 | else: |
|---|
| 266 | if mimetype: |
|---|
| 267 | extensions = mimetypes.guess_all_extensions(mimetype) |
|---|
| 268 | for extension in extensions: |
|---|
| 269 | if extension in known_ext: |
|---|
| 270 | ext = extension |
|---|
| 271 | break |
|---|
| 272 | else: |
|---|
| 273 | if len(extensions) > 0: |
|---|
| 274 | ext = extensions[0] |
|---|
| 275 | else: |
|---|
| 276 | ext = '' |
|---|
| 277 | else: |
|---|
| 278 | ext = '' |
|---|
| 279 | |
|---|
| 280 | extmap = { |
|---|
| 281 | '.htm': '.html', |
|---|
| 282 | '.php': '.html', |
|---|
| 283 | '.PS': '.ps', |
|---|
| 284 | '.shtml': '.html', |
|---|
| 285 | '.text': '.txt', |
|---|
| 286 | } |
|---|
| 287 | # we have no real handling of no extension, .old, and .ksh |
|---|
| 288 | if ext in extmap: ext = extmap[ext] |
|---|
| 289 | if ext not in known_ext: ext = ext + '.unknown' |
|---|
| 290 | |
|---|
| 291 | return "%04d-%s%s" % (self.group.pk, slug, ext, ) |
|---|
| 292 | |
|---|
| 293 | def path_from_filename(self, filename): |
|---|
| 294 | path = os.path.join(constitution_dir, filename) |
|---|
| 295 | return path |
|---|
| 296 | |
|---|
| 297 | def webstat(self, ): |
|---|
| 298 | url = self.source_url |
|---|
| 299 | if url: |
|---|
| 300 | try: |
|---|
| 301 | stream = urllib.urlopen(self.source_url) |
|---|
| 302 | return stream.getcode() |
|---|
| 303 | except IOError: |
|---|
| 304 | return "IOError" |
|---|
| 305 | else: |
|---|
| 306 | return "no-url" |
|---|
| 307 | |
|---|
| 308 | reversion.register(GroupConstitution) |
|---|
| 309 | |
|---|
| 310 | GROUP_STARTUP_STAGE_SUBMITTED = 10 |
|---|
| 311 | GROUP_STARTUP_STAGE_APPROVED = 20 |
|---|
| 312 | GROUP_STARTUP_STAGE_REJECTED = -10 |
|---|
| 313 | GROUP_STARTUP_STAGE = ( |
|---|
| 314 | (GROUP_STARTUP_STAGE_SUBMITTED, 'submitted'), |
|---|
| 315 | (GROUP_STARTUP_STAGE_APPROVED, 'approved'), |
|---|
| 316 | (GROUP_STARTUP_STAGE_REJECTED, 'rejected'), |
|---|
| 317 | ) |
|---|
| 318 | |
|---|
| 319 | |
|---|
| 320 | class GroupStartup(models.Model): |
|---|
| 321 | group = models.ForeignKey(Group, unique=True, ) |
|---|
| 322 | stage = models.IntegerField(choices=GROUP_STARTUP_STAGE) |
|---|
| 323 | submitter = models.CharField(max_length=30, editable=False, ) |
|---|
| 324 | create_officer_list = models.BooleanField() |
|---|
| 325 | create_group_list = models.BooleanField() |
|---|
| 326 | create_athena_locker = models.BooleanField() |
|---|
| 327 | president_name = models.CharField(max_length=50) |
|---|
| 328 | president_kerberos = models.CharField(max_length=8) |
|---|
| 329 | treasurer_name = models.CharField(max_length=50) |
|---|
| 330 | treasurer_kerberos = models.CharField(max_length=8) |
|---|
| 331 | reversion.register(GroupStartup) |
|---|
| 332 | |
|---|
| 333 | |
|---|
| 334 | class GroupNote(models.Model): |
|---|
| 335 | author = models.CharField(max_length=30, db_index=True, ) # match Django username field |
|---|
| 336 | timestamp = models.DateTimeField(default=datetime.datetime.now, editable=False, ) |
|---|
| 337 | body = models.TextField() |
|---|
| 338 | acl_read_group = models.BooleanField(default=True, help_text='Can the group read this note') |
|---|
| 339 | acl_read_offices = models.BooleanField(default=True, help_text='Can "offices" that interact with groups (SAO, CAC, and funding boards) read this note') |
|---|
| 340 | group = models.ForeignKey(Group, db_index=True, ) |
|---|
| 341 | |
|---|
| 342 | def __str__(self, ): |
|---|
| 343 | return "Note by %s on %s" % (self.author, self.timestamp, ) |
|---|
| 344 | |
|---|
| 345 | @classmethod |
|---|
| 346 | def viewable_notes(cls, group, user): |
|---|
| 347 | notes = cls.objects.filter(group=group) |
|---|
| 348 | if not user.has_perm('groups.view_note_all'): |
|---|
| 349 | q = models.Q(pk=0) |
|---|
| 350 | if user.has_perm('groups.view_note_group', group): |
|---|
| 351 | q |= models.Q(acl_read_group=True) |
|---|
| 352 | if user.has_perm('groups.view_note_office'): |
|---|
| 353 | q |= models.Q(acl_read_offices=True) |
|---|
| 354 | notes = notes.filter(q) |
|---|
| 355 | return notes |
|---|
| 356 | |
|---|
| 357 | class Meta: |
|---|
| 358 | permissions = ( |
|---|
| 359 | ('view_note_group', 'View notes intended for the group to see', ), |
|---|
| 360 | ('view_note_office', 'View notes intended for "offices" to see', ), |
|---|
| 361 | ('view_note_all', 'View all notes', ), |
|---|
| 362 | ) |
|---|
| 363 | reversion.register(GroupNote) |
|---|
| 364 | |
|---|
| 365 | |
|---|
| 366 | class OfficerRole(models.Model): |
|---|
| 367 | UNLIMITED = 10000 |
|---|
| 368 | |
|---|
| 369 | display_name = models.CharField(max_length=50) |
|---|
| 370 | slug = models.SlugField(unique=True, ) |
|---|
| 371 | description = models.TextField() |
|---|
| 372 | max_count = models.IntegerField(default=UNLIMITED, help_text='Maximum number of holders of this role. Use %d for no limit.' % UNLIMITED) |
|---|
| 373 | require_student = models.BooleanField(default=False) |
|---|
| 374 | grant_user = models.ForeignKey(User, null=True, blank=True, |
|---|
| 375 | limit_choices_to={ 'username__endswith': '@SYSTEM'}) |
|---|
| 376 | publicly_visible = models.BooleanField(default=True, help_text='Can everyone see the holders of this office.') |
|---|
| 377 | |
|---|
| 378 | def max_count_str(self, ): |
|---|
| 379 | if self.max_count == self.UNLIMITED: |
|---|
| 380 | return "unlimited" |
|---|
| 381 | else: |
|---|
| 382 | return str(self.max_count) |
|---|
| 383 | |
|---|
| 384 | def __str__(self, ): |
|---|
| 385 | return self.display_name |
|---|
| 386 | |
|---|
| 387 | @classmethod |
|---|
| 388 | def getGrantUsers(cls, roles): |
|---|
| 389 | ret = set([role.grant_user for role in roles]) |
|---|
| 390 | if None in ret: ret.remove(None) |
|---|
| 391 | return ret |
|---|
| 392 | |
|---|
| 393 | @classmethod |
|---|
| 394 | def retrieve(cls, slug, ): |
|---|
| 395 | return cls.objects.get(slug=slug) |
|---|
| 396 | reversion.register(OfficerRole) |
|---|
| 397 | |
|---|
| 398 | |
|---|
| 399 | class OfficeHolder_CurrentManager(models.Manager): |
|---|
| 400 | def get_query_set(self, ): |
|---|
| 401 | return super(OfficeHolder_CurrentManager, self).get_query_set().filter( |
|---|
| 402 | start_time__lte=datetime.datetime.now, |
|---|
| 403 | end_time__gte=datetime.datetime.now, |
|---|
| 404 | ) |
|---|
| 405 | |
|---|
| 406 | class OfficeHolder(models.Model): |
|---|
| 407 | EXPIRE_OFFSET = datetime.timedelta(seconds=1) |
|---|
| 408 | END_NEVER = datetime.datetime.max |
|---|
| 409 | |
|---|
| 410 | person = models.CharField(max_length=30, db_index=True, ) |
|---|
| 411 | role = models.ForeignKey('OfficerRole', db_index=True, ) |
|---|
| 412 | group = models.ForeignKey('Group', db_index=True, ) |
|---|
| 413 | start_time = models.DateTimeField(default=datetime.datetime.now, db_index=True, ) |
|---|
| 414 | end_time = models.DateTimeField(default=datetime.datetime.max, db_index=True, ) |
|---|
| 415 | |
|---|
| 416 | objects = models.Manager() |
|---|
| 417 | current_holders = OfficeHolder_CurrentManager() |
|---|
| 418 | |
|---|
| 419 | def format_person(self, ): |
|---|
| 420 | return AthenaMoiraAccount.try_format_by_username(self.person, ) |
|---|
| 421 | |
|---|
| 422 | def expire(self, ): |
|---|
| 423 | self.end_time = datetime.datetime.now()-self.EXPIRE_OFFSET |
|---|
| 424 | self.save() |
|---|
| 425 | |
|---|
| 426 | def __str__(self, ): |
|---|
| 427 | return "<OfficeHolder: person=%s, role=%s, group=%s, start_time=%s, end_time=%s>" % ( |
|---|
| 428 | self.person, self.role, self.group, self.start_time, self.end_time, ) |
|---|
| 429 | |
|---|
| 430 | def __repr__(self, ): |
|---|
| 431 | return str(self) |
|---|
| 432 | reversion.register(OfficeHolder) |
|---|
| 433 | |
|---|
| 434 | |
|---|
| 435 | class PerGroupAuthz: |
|---|
| 436 | supports_anonymous_user = True |
|---|
| 437 | supports_inactive_user = True |
|---|
| 438 | supports_object_permissions = True |
|---|
| 439 | |
|---|
| 440 | def authenticate(self, username=None, password=None, ): |
|---|
| 441 | return None # we don't do authn |
|---|
| 442 | def get_user(user_id, ): |
|---|
| 443 | return None # we don't do authn |
|---|
| 444 | |
|---|
| 445 | def has_perm(self, user_obj, perm, obj=None, ): |
|---|
| 446 | print "Checking user %s for perm %s on obj %s" % (user_obj, perm, obj) |
|---|
| 447 | if not user_obj.is_active: |
|---|
| 448 | return False |
|---|
| 449 | if not user_obj.is_authenticated(): |
|---|
| 450 | return False |
|---|
| 451 | if obj is None: |
|---|
| 452 | return False |
|---|
| 453 | # Great, we're active, authenticated, and not in a recursive call |
|---|
| 454 | # Check that we've got a reasonable object |
|---|
| 455 | if getattr(user_obj, 'is_system', False): |
|---|
| 456 | return False |
|---|
| 457 | if isinstance(obj, Group): |
|---|
| 458 | # Having the unqualified perm means that you should have it |
|---|
| 459 | # on any object |
|---|
| 460 | if user_obj.has_perm(perm): |
|---|
| 461 | return True |
|---|
| 462 | # Now we can do the real work |
|---|
| 463 | holders = obj.officers(person=user_obj.username).select_related('role__grant_user') |
|---|
| 464 | sys_users = OfficerRole.getGrantUsers([holder.role for holder in holders]) |
|---|
| 465 | for sys_user in sys_users: |
|---|
| 466 | sys_user.is_system = True |
|---|
| 467 | if sys_user.has_perm(perm): |
|---|
| 468 | print "While checking user %s for perm %s on obj %s: implicit user %s has perms" % (user_obj, perm, obj, sys_user, ) |
|---|
| 469 | return True |
|---|
| 470 | print "While checking user %s for perm %s on obj %s: no perms found (implicit: %s)" % (user_obj, perm, obj, sys_users) |
|---|
| 471 | return False |
|---|
| 472 | |
|---|
| 473 | |
|---|
| 474 | |
|---|
| 475 | class ActivityCategory(models.Model): |
|---|
| 476 | name = models.CharField(max_length=50) |
|---|
| 477 | |
|---|
| 478 | def __str__(self, ): |
|---|
| 479 | return self.name |
|---|
| 480 | |
|---|
| 481 | class Meta: |
|---|
| 482 | verbose_name_plural = "activity categories" |
|---|
| 483 | |
|---|
| 484 | |
|---|
| 485 | class GroupClass(models.Model): |
|---|
| 486 | name = models.CharField(max_length=50) |
|---|
| 487 | slug = models.SlugField(unique=True, ) |
|---|
| 488 | description = models.TextField() |
|---|
| 489 | gets_publicity = models.BooleanField(help_text="Gets publicity resources such as FYSM or Activities Midway") |
|---|
| 490 | |
|---|
| 491 | def __str__(self, ): |
|---|
| 492 | return self.name |
|---|
| 493 | |
|---|
| 494 | class Meta: |
|---|
| 495 | verbose_name_plural = "group classes" |
|---|
| 496 | |
|---|
| 497 | |
|---|
| 498 | class GroupStatus(models.Model): |
|---|
| 499 | name = models.CharField(max_length=50) |
|---|
| 500 | slug = models.SlugField(unique=True, ) |
|---|
| 501 | description = models.TextField() |
|---|
| 502 | is_active = models.BooleanField(default=True, help_text="This status represents an active group") |
|---|
| 503 | |
|---|
| 504 | def __str__(self, ): |
|---|
| 505 | active = "" |
|---|
| 506 | if not self.is_active: |
|---|
| 507 | active = " (inactive)" |
|---|
| 508 | return "%s%s" % (self.name, active, ) |
|---|
| 509 | |
|---|
| 510 | class Meta: |
|---|
| 511 | verbose_name_plural= "group statuses" |
|---|
| 512 | |
|---|
| 513 | |
|---|
| 514 | class GroupFunding(models.Model): |
|---|
| 515 | name = models.CharField(max_length=50) |
|---|
| 516 | slug = models.SlugField(unique=True, ) |
|---|
| 517 | contact_email = models.EmailField() |
|---|
| 518 | 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.") |
|---|
| 519 | |
|---|
| 520 | def __str__(self, ): |
|---|
| 521 | return "%s (%s)" % (self.name, self.contact_email, ) |
|---|
| 522 | |
|---|
| 523 | |
|---|
| 524 | class AthenaMoiraAccount_ActiveManager(models.Manager): |
|---|
| 525 | def get_query_set(self, ): |
|---|
| 526 | return super(AthenaMoiraAccount_ActiveManager, self).get_query_set().filter(del_date=None) |
|---|
| 527 | |
|---|
| 528 | class AthenaMoiraAccount(models.Model): |
|---|
| 529 | username = models.CharField(max_length=8, unique=True, ) |
|---|
| 530 | mit_id = models.CharField(max_length=15) |
|---|
| 531 | first_name = models.CharField(max_length=45) |
|---|
| 532 | last_name = models.CharField(max_length=45) |
|---|
| 533 | account_class = models.CharField(max_length=10) |
|---|
| 534 | mutable = models.BooleanField(default=True) |
|---|
| 535 | add_date = models.DateField(help_text="Date when this person was added to the dump.", ) |
|---|
| 536 | del_date = models.DateField(help_text="Date when this person was removed from the dump.", blank=True, null=True, ) |
|---|
| 537 | mod_date = models.DateField(help_text="Date when this person's record was last changed.", blank=True, null=True, ) |
|---|
| 538 | |
|---|
| 539 | objects = models.Manager() |
|---|
| 540 | active_accounts = AthenaMoiraAccount_ActiveManager() |
|---|
| 541 | |
|---|
| 542 | def is_student(self, ): |
|---|
| 543 | # XXX: Is this... right? |
|---|
| 544 | return self.account_class == 'G' or self.account_class.isdigit() |
|---|
| 545 | |
|---|
| 546 | def format(self, ): |
|---|
| 547 | return "%s %s <%s>" % (self.first_name, self.last_name, self.username, ) |
|---|
| 548 | |
|---|
| 549 | def __str__(self, ): |
|---|
| 550 | if self.mutable: |
|---|
| 551 | mutable_str = "" |
|---|
| 552 | else: |
|---|
| 553 | mutable_str = " (immutable)" |
|---|
| 554 | return "<AthenaMoiraAccount: username=%s name='%s, %s' account_class=%s%s>" % ( |
|---|
| 555 | self.username, self.last_name, self.first_name, |
|---|
| 556 | self.account_class, mutable_str, |
|---|
| 557 | ) |
|---|
| 558 | |
|---|
| 559 | def __repr__(self, ): |
|---|
| 560 | return str(self) |
|---|
| 561 | |
|---|
| 562 | @classmethod |
|---|
| 563 | def try_format_by_username(cls, username): |
|---|
| 564 | try: |
|---|
| 565 | moira = AthenaMoiraAccount.objects.get(username=username) |
|---|
| 566 | return moira.format() |
|---|
| 567 | except AthenaMoiraAccount.DoesNotExist: |
|---|
| 568 | return "%s (name not available)" % (username) |
|---|
| 569 | |
|---|
| 570 | class Meta: |
|---|
| 571 | verbose_name = "Athena (Moira) account" |
|---|