source: asadb/groups/models.py @ 68c93e8

space-accessstablestage
Last change on this file since 68c93e8 was a03cb61, checked in by ASA Group Database <asa-db@…>, 14 years ago

Add indices to database tables

Fixes ASA Trac #38. Also, makes space-related scripts quick instead of
stupidly slow...

  • Property mode set to 100644
File size: 18.3 KB
Line 
1from django.db import models
2from django.contrib.auth.models import User
3from django.template.defaultfilters import slugify
4import reversion
5
6import datetime
7import filecmp
8import mimetypes
9import os
10import re
11import shutil
12import urllib
13
14import settings
15
16# Create your models here.
17
18class ActiveGroupManager(models.Manager):
19    def get_query_set(self, ):
20        return super(ActiveGroupManager, self).get_query_set().filter(
21            group_status__slug='active',
22        )
23
24class Group(models.Model):
25    name = models.CharField(max_length=100, db_index=True, )
26    abbreviation = models.CharField(max_length=10, blank=True, db_index=True, )
27    description = models.TextField()
28    activity_category = models.ForeignKey('ActivityCategory', null=True, blank=True, db_index=True, )
29    group_class = models.ForeignKey('GroupClass', db_index=True, )
30    group_status = models.ForeignKey('GroupStatus', db_index=True, )
31    group_funding = models.ForeignKey('GroupFunding', null=True, blank=True, db_index=True, )
32    website_url = models.URLField()
33    constitution_url = models.CharField(max_length=200, blank=True)
34    meeting_times = models.TextField(blank=True)
35    advisor_name = models.CharField(max_length=100, blank=True)
36    num_undergrads = models.IntegerField(null=True, blank=True, )
37    num_grads = models.IntegerField(null=True, blank=True, )
38    num_community = models.IntegerField(null=True, blank=True, )
39    num_other = models.IntegerField(null=True, blank=True, )
40    group_email = models.EmailField(verbose_name="group email list", blank=True, )
41    officer_email = models.EmailField(verbose_name="officers' email list")
42    main_account_id = models.IntegerField(null=True, blank=True, )
43    funding_account_id = models.IntegerField(null=True, blank=True, )
44    athena_locker = models.CharField(max_length=20, blank=True, help_text="Often, this will appear in your group's URL --- http://web.mit.edu/SOME_VARIANT_OF_GROUP_NAME/, http://www.mit.edu/~SOME_VARIANT_OF_GROUP_NAME/ and http://SOME_VARIANT_OF_GROUP_NAME.scripts.mit.edu/ all correspond to a locker name of SOME_VARIANT_OF_GROUP_NAME. Also, you can probably find this in your entry in the old database.")
45    recognition_date = models.DateTimeField()
46    update_date = models.DateTimeField(editable=False, )
47    updater = models.CharField(max_length=30, editable=False, null=True, ) # match Django username field
48    _updater_set = False
49
50    objects = models.Manager()
51    active_groups = ActiveGroupManager()
52
53    def update_string(self, ):
54        updater = self.updater or "unknown"
55        return "%s by %s" % (self.update_date.strftime(settings.DATETIME_FORMAT_PYTHON), updater, )
56
57    def set_updater(self, who):
58        if hasattr(who, 'username'):
59            self.updater = who.username
60        else:
61            self.updater = who
62        self._updater_set = True
63
64    def save(self, ):
65        if not self._updater_set:
66            self.updater = None
67        self.update_date = datetime.datetime.now()
68        super(Group, self).save()
69
70    def viewable_notes(self, user):
71        return GroupNote.viewable_notes(self, user)
72
73    def officers(self, role=None, person=None, as_of="now",):
74        """Get the set of people holding some office.
75
76        If None is passed for role, person, or as_of, that field will not
77        be constrained. If as_of is "now" (default) the status will be
78        required to be current. If any of the three parameters are set
79        to another value, the corresponding filter will be applied.
80        """
81        office_holders = OfficeHolder.objects.filter(group=self,)
82        if role:
83            if isinstance(role, str):
84                office_holders = office_holders.filter(role__slug=role)
85            else:
86                office_holders = office_holders.filter(role=role)
87        if person:
88            office_holders = office_holders.filter(person=person)
89        if as_of:
90            if as_of == "now": as_of = datetime.datetime.now()
91            office_holders = office_holders.filter(start_time__lte=as_of, end_time__gte=as_of)
92        return office_holders
93
94    def slug(self, ):
95        return slugify(self.name)
96
97    def __str__(self, ):
98        return self.name
99
100    @classmethod
101    def reporting_prefetch(cls, ):
102        return set(['activity_category', 'group_class', 'group_status', 'group_funding'])
103
104    @classmethod
105    def reporting_fields(cls, ):
106        fields = cls._meta.fields
107        return [(f.name, f.verbose_name) for f in fields]
108
109    class Meta:
110        ordering = ('name', )
111        permissions = (
112            ('view_group_private_info', 'View private group information'),
113            # ability to update normal group info or people
114            # this is weaker than change_group, which is the built-in
115            # permission that controls the admin interface
116            ('admin_group', 'Administer basic group information'),
117            ('view_signatories', 'View signatory information for all groups'),
118            ('recognize_nge', 'Recognize Non-Group Entity'),
119            ('recognize_group', 'Recognize groups'),
120        )
121reversion.register(Group)
122
123
124constitution_dir = os.path.join(settings.SITE_ROOT, '..', 'constitutions')
125
126class GroupConstitution(models.Model):
127    group = models.ForeignKey(Group, unique=True, )
128    source_url = models.URLField()
129    dest_file = models.CharField(max_length=100)
130    last_update = models.DateTimeField(help_text='Last time when this constitution actually changed.')
131    last_download = models.DateTimeField(help_text='Last time we downloaded this constitution to see if it had changed.')
132    failure_date = models.DateTimeField(null=True, blank=True, default=None, help_text='Time this URL started failing to download. (Null if currently working.)')
133    status_msg = models.CharField(max_length=20)
134    failure_reason = models.CharField(max_length=100, blank=True, default="")
135
136    def update(self, ):
137        url = self.source_url
138        success = None
139        old_success = (self.failure_date is None)
140        if url:
141            url_opener = urllib.FancyURLopener()
142            now = datetime.datetime.now()
143            try:
144                tmp_path, headers = url_opener.retrieve(url)
145            except IOError:
146                self.failure_date = now
147                self.save()
148                success = False
149                self.status_msg = "retrieval failed"
150                self.failure_reason = self.status_msg
151                return (success, self.status_msg, old_success, )
152            if tmp_path == url:
153                mover = shutil.copyfile
154            else:
155                mover = shutil.move
156            save_filename = self.compute_filename(tmp_path, headers, )
157            dest_path = self.path_from_filename(self.dest_file)
158            if save_filename != self.dest_file:
159                if self.dest_file: os.remove(dest_path)
160                mover(tmp_path, self.path_from_filename(save_filename))
161                self.dest_file = save_filename
162                self.last_update = now
163                self.status_msg = "new path"
164            else:
165                if filecmp.cmp(tmp_path, dest_path, shallow=False, ):
166                    self.status_msg = "no change"
167                else:
168                    # changed
169                    mover(tmp_path, dest_path)
170                    self.last_update = now
171                    self.status_msg = "updated in place"
172            self.last_download = now
173            self.failure_date = None
174            self.failure_reason = ""
175            self.save()
176            success = True
177        else:
178            success = False
179            self.status_msg = "no url"
180            self.failure_reason = self.status_msg
181        return (success, self.status_msg, old_success, )
182
183    def compute_filename(self, tmp_path, headers, ):
184        slug = self.group.slug()
185        basename, fileext = os.path.splitext(tmp_path)
186        if fileext:
187            ext = fileext
188        else:
189            if headers.getheader('Content-Type'):
190                mimeext = mimetypes.guess_extension(headers.gettype())
191                if mimeext:
192                    ext = mimeext
193                else:
194                    ext = ''
195            else:
196                ext = ''
197        return "%04d-%s%s" % (self.group.pk, slug, ext, )
198
199    def path_from_filename(self, filename):
200        path = os.path.join(constitution_dir, filename)
201        return path
202
203    def webstat(self, ):
204        url = self.source_url
205        if url:
206            try:
207                stream = urllib.urlopen(self.source_url)
208                return stream.getcode()
209            except:
210                return "IOError"
211        else:
212            return "no-url"
213
214reversion.register(GroupConstitution)
215
216GROUP_STARTUP_STAGE_SUBMITTED = 10
217GROUP_STARTUP_STAGE_APPROVED = 20
218GROUP_STARTUP_STAGE_REJECTED = -10
219GROUP_STARTUP_STAGE = (
220    (GROUP_STARTUP_STAGE_SUBMITTED,     'submitted'),
221    (GROUP_STARTUP_STAGE_APPROVED,      'approved'),
222    (GROUP_STARTUP_STAGE_REJECTED,      'rejected'),
223)
224
225
226class GroupStartup(models.Model):
227    group = models.ForeignKey(Group, unique=True, )
228    stage = models.IntegerField(choices=GROUP_STARTUP_STAGE)
229    submitter = models.CharField(max_length=30, editable=False, )
230    create_officer_list = models.BooleanField()
231    create_group_list = models.BooleanField()
232    create_athena_locker = models.BooleanField()
233    president_name = models.CharField(max_length=50)
234    president_kerberos = models.CharField(max_length=8)
235    treasurer_name = models.CharField(max_length=50)
236    treasurer_kerberos = models.CharField(max_length=8)
237reversion.register(GroupStartup)
238
239
240class GroupNote(models.Model):
241    author = models.CharField(max_length=30, db_index=True, ) # match Django username field
242    timestamp = models.DateTimeField(default=datetime.datetime.now, editable=False, )
243    body = models.TextField()
244    acl_read_group = models.BooleanField(default=True, help_text='Can the group read this note')
245    acl_read_offices = models.BooleanField(default=True, help_text='Can "offices" that interact with groups (SAO, CAC, and funding boards) read this note')
246    group = models.ForeignKey(Group, db_index=True, )
247
248    def __str__(self, ):
249        return "Note by %s on %s" % (self.author, self.timestamp, )
250
251    @classmethod
252    def viewable_notes(cls, group, user):
253        notes = cls.objects.filter(group=group)
254        if not user.has_perm('groups.view_note_all'):
255            q = models.Q(pk=0)
256            if user.has_perm('groups.view_note_group', group):
257                q |= models.Q(acl_read_group=True)
258            if user.has_perm('groups.view_note_office'):
259                q |= models.Q(acl_read_offices=True)
260            notes = notes.filter(q)
261        return notes
262
263    class Meta:
264        permissions = (
265            ('view_note_group',     'View notes intended for the group to see', ),
266            ('view_note_office',    'View notes intended for "offices" to see', ),
267            ('view_note_all',       'View all notes', ),
268        )
269reversion.register(GroupNote)
270
271
272class OfficerRole(models.Model):
273    UNLIMITED = 10000
274
275    display_name = models.CharField(max_length=50)
276    slug = models.SlugField(unique=True, )
277    description = models.TextField()
278    max_count = models.IntegerField(default=UNLIMITED, help_text='Maximum number of holders of this role. Use %d for no limit.' % UNLIMITED)
279    require_student = models.BooleanField(default=False)
280    grant_user = models.ForeignKey(User, null=True, blank=True,
281        limit_choices_to={ 'username__endswith': '@SYSTEM'})
282    publicly_visible = models.BooleanField(default=True, help_text='Can everyone see the holders of this office.')
283
284    def max_count_str(self, ):
285        if self.max_count == self.UNLIMITED:
286            return "unlimited"
287        else:
288            return str(self.max_count)
289
290    def __str__(self, ):
291        return self.display_name
292
293    @classmethod
294    def getGrantUsers(cls, roles):
295        ret = set([role.grant_user for role in roles])
296        if None in ret: ret.remove(None)
297        return ret
298
299    @classmethod
300    def retrieve(cls, slug, ):
301        return cls.objects.get(slug=slug)
302reversion.register(OfficerRole)
303
304
305class OfficeHolder_CurrentManager(models.Manager):
306    def get_query_set(self, ):
307        return super(OfficeHolder_CurrentManager, self).get_query_set().filter(
308            start_time__lte=datetime.datetime.now,
309            end_time__gte=datetime.datetime.now,
310        )
311
312class OfficeHolder(models.Model):
313    EXPIRE_OFFSET   = datetime.timedelta(seconds=1)
314    END_NEVER       = datetime.datetime.max
315
316    person = models.CharField(max_length=30, db_index=True, )
317    role = models.ForeignKey('OfficerRole', db_index=True, )
318    group = models.ForeignKey('Group', db_index=True, )
319    start_time = models.DateTimeField(default=datetime.datetime.now, db_index=True, )
320    end_time = models.DateTimeField(default=datetime.datetime.max, db_index=True, )
321
322    objects = models.Manager()
323    current_holders = OfficeHolder_CurrentManager()
324
325    def format_person(self, ):
326        return AthenaMoiraAccount.try_format_by_username(self.person, )
327
328    def expire(self, ):
329        self.end_time = datetime.datetime.now()-self.EXPIRE_OFFSET
330        self.save()
331
332    def __str__(self, ):
333        return "<OfficeHolder: person=%s, role=%s, group=%s, start_time=%s, end_time=%s>" % (
334            self.person, self.role, self.group, self.start_time, self.end_time, )
335
336    def __repr__(self, ):
337        return str(self)
338reversion.register(OfficeHolder)
339
340
341class PerGroupAuthz:
342    supports_anonymous_user = True
343    supports_inactive_user = True
344    supports_object_permissions = True
345
346    def authenticate(self, username=None, password=None, ):
347        return None # we don't do authn
348    def get_user(user_id, ):
349        return None # we don't do authn
350
351    def has_perm(self, user_obj, perm, obj=None, ):
352        print "Checking user %s for perm %s on obj %s" % (user_obj, perm, obj)
353        if not user_obj.is_active:
354            return False
355        if not user_obj.is_authenticated():
356            return False
357        if obj is None:
358            return False
359        # Great, we're active, authenticated, and not in a recursive call
360        # Check that we've got a reasonable object
361        if getattr(user_obj, 'is_system', False):
362            return False
363        if isinstance(obj, Group):
364            # Having the unqualified perm means that you should have it
365            # on any object
366            if user_obj.has_perm(perm):
367                return True
368            # Now we can do the real work
369            holders = obj.officers(person=user_obj.username).select_related('role__grant_user')
370            sys_users = OfficerRole.getGrantUsers([holder.role for holder in holders])
371            for sys_user in sys_users:
372                sys_user.is_system = True
373                if sys_user.has_perm(perm):
374                    print "While checking user %s for perm %s on obj %s: implicit user %s has perms" % (user_obj, perm, obj, sys_user, )
375                    return True
376        print "While checking user %s for perm %s on obj %s: no perms found (implicit: %s)" % (user_obj, perm, obj, sys_users)
377        return False
378
379
380
381class ActivityCategory(models.Model):
382    name = models.CharField(max_length=50)
383
384    def __str__(self, ):
385        return self.name
386
387    class Meta:
388        verbose_name_plural = "activity categories"
389
390
391class GroupClass(models.Model):
392    name = models.CharField(max_length=50)
393    slug = models.SlugField(unique=True, )
394    description = models.TextField()
395    gets_publicity = models.BooleanField(help_text="Gets publicity resources such as FYSM or Activities Midway")
396
397    def __str__(self, ):
398        return self.name
399
400    class Meta:
401        verbose_name_plural = "group classes"
402
403
404class GroupStatus(models.Model):
405    name = models.CharField(max_length=50)
406    slug = models.SlugField(unique=True, )
407    description = models.TextField()
408    is_active = models.BooleanField(default=True, help_text="This status represents an active group")
409
410    def __str__(self, ):
411        active = ""
412        if not self.is_active:
413            active = " (inactive)"
414        return "%s%s" % (self.name, active, )
415
416    class Meta:
417        verbose_name_plural= "group statuses"
418
419
420class GroupFunding(models.Model):
421    name = models.CharField(max_length=50)
422    slug = models.SlugField(unique=True, )
423    contact_email = models.EmailField()
424    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.")
425
426    def __str__(self, ):
427        return "%s (%s)" % (self.name, self.contact_email, )
428
429
430class AthenaMoiraAccount_ActiveManager(models.Manager):
431    def get_query_set(self, ):
432        return super(AthenaMoiraAccount_ActiveManager, self).get_query_set().filter(del_date=None)
433
434class AthenaMoiraAccount(models.Model):
435    username = models.CharField(max_length=8, unique=True, )
436    mit_id = models.CharField(max_length=15)
437    first_name      = models.CharField(max_length=45)
438    last_name       = models.CharField(max_length=45)
439    account_class   = models.CharField(max_length=10)
440    mutable         = models.BooleanField(default=True)
441    add_date        = models.DateField(help_text="Date when this person was added to the dump.", )
442    del_date        = models.DateField(help_text="Date when this person was removed from the dump.", blank=True, null=True, )
443    mod_date        = models.DateField(help_text="Date when this person's record was last changed.", blank=True, null=True, )
444
445    objects = models.Manager()
446    active_accounts = AthenaMoiraAccount_ActiveManager()
447
448    def is_student(self, ):
449        # XXX: Is this... right?
450        return self.account_class == 'G' or self.account_class.isdigit()
451
452    def format(self, ):
453        return "%s %s <%s>" % (self.first_name, self.last_name, self.username, )
454
455    def __str__(self, ):
456        if self.mutable:
457            mutable_str = ""
458        else:
459            mutable_str = " (immutable)"
460        return "<AthenaMoiraAccount: username=%s name='%s, %s' account_class=%s%s>" % (
461            self.username, self.last_name, self.first_name,
462            self.account_class, mutable_str,
463        )
464
465    def __repr__(self, ):
466        return str(self)
467
468    @classmethod
469    def try_format_by_username(cls, username):
470        try:
471            moira = AthenaMoiraAccount.objects.get(username=username)
472            return moira.format()
473        except AthenaMoiraAccount.DoesNotExist:
474            return "%s (name not available)" % (username)
475
476    class Meta:
477        verbose_name = "Athena (Moira) account"
Note: See TracBrowser for help on using the repository browser.