source: asadb/groups/models.py @ 0270ed7

space-accessstablestage
Last change on this file since 0270ed7 was 0270ed7, checked in by Alex Dehnert <adehnert@…>, 14 years ago

Display full names of signatories

This displays the full name of signatories (not just the username) in the
group display page and the signatory change page (but not the big list
signatories page). It also requires a database query per signatory being
looked up. (This is one reason for not doing it on the big list signatories
page, the other being that full names take more space.)

  • Property mode set to 100644
File size: 17.4 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)
26    abbreviation = models.CharField(max_length=10, blank=True)
27    description = models.TextField()
28    activity_category = models.ForeignKey('ActivityCategory', null=True, blank=True, )
29    group_class = models.ForeignKey('GroupClass')
30    group_status = models.ForeignKey('GroupStatus')
31    group_funding = models.ForeignKey('GroupFunding', null=True, blank=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(blank=True, )
41    officer_email = models.EmailField()
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)
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    class Meta:
101        ordering = ('name', )
102        permissions = (
103            ('view_group_private_info', 'View private group information'),
104            # ability to update normal group info or people
105            # this is weaker than change_group, which is the built-in
106            # permission that controls the admin interface
107            ('admin_group', 'Administer basic group information'),
108            ('view_signatories', 'View signatory information for all groups'),
109            ('recognize_nge', 'Recognize Non-Group Entity'),
110            ('recognize_group', 'Recognize groups'),
111        )
112reversion.register(Group)
113
114
115GROUP_STARTUP_STAGE_SUBMITTED = 10
116GROUP_STARTUP_STAGE_APPROVED = 20
117GROUP_STARTUP_STAGE_REJECTED = -10
118GROUP_STARTUP_STAGE = (
119    (GROUP_STARTUP_STAGE_SUBMITTED,     'submitted'),
120    (GROUP_STARTUP_STAGE_APPROVED,      'approved'),
121    (GROUP_STARTUP_STAGE_REJECTED,      'rejected'),
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
216
217class GroupStartup(models.Model):
218    group = models.ForeignKey(Group)
219    stage = models.IntegerField(choices=GROUP_STARTUP_STAGE)
220    submitter = models.CharField(max_length=30, editable=False, )
221    create_officer_list = models.BooleanField()
222    create_group_list = models.BooleanField()
223    create_athena_locker = models.BooleanField()
224    president_name = models.CharField(max_length=50)
225    president_kerberos = models.CharField(max_length=8)
226    treasurer_name = models.CharField(max_length=50)
227    treasurer_kerberos = models.CharField(max_length=8)
228reversion.register(GroupStartup)
229
230
231class GroupNote(models.Model):
232    author = models.CharField(max_length=30, ) # match Django username field
233    timestamp = models.DateTimeField(default=datetime.datetime.now, editable=False, )
234    body = models.TextField()
235    acl_read_group = models.BooleanField(default=True, help_text='Can the group read this note')
236    acl_read_offices = models.BooleanField(default=True, help_text='Can "offices" that interact with groups (SAO, CAC, and funding boards) read this note')
237    group = models.ForeignKey(Group)
238
239    def __str__(self, ):
240        return "Note by %s on %s" % (self.author, self.timestamp, )
241
242    @classmethod
243    def viewable_notes(cls, group, user):
244        notes = cls.objects.filter(group=group)
245        if not user.has_perm('groups.view_note_all'):
246            q = models.Q(pk=0)
247            if user.has_perm('groups.view_note_group', group):
248                q |= models.Q(acl_read_group=True)
249            if user.has_perm('groups.view_note_office'):
250                q |= models.Q(acl_read_offices=True)
251            notes = notes.filter(q)
252        return notes
253
254    class Meta:
255        permissions = (
256            ('view_note_group',     'View notes intended for the group to see', ),
257            ('view_note_office',    'View notes intended for "offices" to see', ),
258            ('view_note_all',       'View all notes', ),
259        )
260reversion.register(GroupNote)
261
262
263class OfficerRole(models.Model):
264    UNLIMITED = 10000
265
266    display_name = models.CharField(max_length=50)
267    slug = models.SlugField()
268    description = models.TextField()
269    max_count = models.IntegerField(default=UNLIMITED, help_text='Maximum number of holders of this role. Use %d for no limit.' % UNLIMITED)
270    require_student = models.BooleanField(default=False)
271    grant_user = models.ForeignKey(User, null=True, blank=True,
272        limit_choices_to={ 'username__endswith': '@SYSTEM'})
273    publicly_visible = models.BooleanField(default=True, help_text='Can everyone see the holders of this office.')
274
275    def max_count_str(self, ):
276        if self.max_count == self.UNLIMITED:
277            return "unlimited"
278        else:
279            return str(self.max_count)
280
281    def __str__(self, ):
282        return self.display_name
283
284    @classmethod
285    def getGrantUsers(cls, roles):
286        ret = set([role.grant_user for role in roles])
287        if None in ret: ret.remove(None)
288        return ret
289
290    @classmethod
291    def retrieve(cls, slug, ):
292        return cls.objects.get(slug=slug)
293reversion.register(OfficerRole)
294
295
296class OfficeHolder_CurrentManager(models.Manager):
297    def get_query_set(self, ):
298        return super(OfficeHolder_CurrentManager, self).get_query_set().filter(
299            start_time__lte=datetime.datetime.now,
300            end_time__gte=datetime.datetime.now,
301        )
302
303class OfficeHolder(models.Model):
304    EXPIRE_OFFSET   = datetime.timedelta(seconds=1)
305    END_NEVER       = datetime.datetime.max
306
307    person = models.CharField(max_length=30)
308    role = models.ForeignKey('OfficerRole')
309    group = models.ForeignKey('Group')
310    start_time = models.DateTimeField(default=datetime.datetime.now)
311    end_time = models.DateTimeField(default=datetime.datetime.max)
312
313    objects = models.Manager()
314    current_holders = OfficeHolder_CurrentManager()
315
316    def format_person(self, ):
317        return AthenaMoiraAccount.try_format_by_username(self.person, )
318
319    def expire(self, ):
320        self.end_time = datetime.datetime.now()-self.EXPIRE_OFFSET
321        self.save()
322
323    def __str__(self, ):
324        return "<OfficeHolder: person=%s, role=%s, group=%s, start_time=%s, end_time=%s>" % (
325            self.person, self.role, self.group, self.start_time, self.end_time, )
326
327    def __repr__(self, ):
328        return str(self)
329reversion.register(OfficeHolder)
330
331
332class PerGroupAuthz:
333    supports_anonymous_user = True
334    supports_inactive_user = True
335    supports_object_permissions = True
336
337    def authenticate(self, username=None, password=None, ):
338        return None # we don't do authn
339    def get_user(user_id, ):
340        return None # we don't do authn
341
342    def has_perm(self, user_obj, perm, obj=None, ):
343        print "Checking user %s for perm %s on obj %s" % (user_obj, perm, obj)
344        if not user_obj.is_active:
345            return False
346        if not user_obj.is_authenticated():
347            return False
348        if obj is None:
349            return False
350        # Great, we're active, authenticated, and not in a recursive call
351        # Check that we've got a reasonable object
352        if getattr(user_obj, 'is_system', False):
353            return False
354        if isinstance(obj, Group):
355            # Having the unqualified perm means that you should have it
356            # on any object
357            if user_obj.has_perm(perm):
358                return True
359            # Now we can do the real work
360            holders = obj.officers(person=user_obj.username).select_related('role__grant_user')
361            sys_users = OfficerRole.getGrantUsers([holder.role for holder in holders])
362            for sys_user in sys_users:
363                sys_user.is_system = True
364                if sys_user.has_perm(perm):
365                    print "While checking user %s for perm %s on obj %s: implicit user %s has perms" % (user_obj, perm, obj, sys_user, )
366                    return True
367        print "While checking user %s for perm %s on obj %s: no perms found (implicit: %s)" % (user_obj, perm, obj, sys_users)
368        return False
369
370
371
372class ActivityCategory(models.Model):
373    name = models.CharField(max_length=50)
374
375    def __str__(self, ):
376        return self.name
377
378    class Meta:
379        verbose_name_plural = "activity categories"
380
381
382class GroupClass(models.Model):
383    name = models.CharField(max_length=50)
384    slug = models.SlugField(unique=True, )
385    description = models.TextField()
386    gets_publicity = models.BooleanField(help_text="Gets publicity resources such as FYSM or Activities Midway")
387
388    def __str__(self, ):
389        return self.name
390
391    class Meta:
392        verbose_name_plural = "group classes"
393
394
395class GroupStatus(models.Model):
396    name = models.CharField(max_length=50)
397    slug = models.SlugField(unique=True, )
398    description = models.TextField()
399    is_active = models.BooleanField(default=True, help_text="This status represents an active group")
400
401    def __str__(self, ):
402        active = ""
403        if not self.is_active:
404            active = " (inactive)"
405        return "%s%s" % (self.name, active, )
406
407    class Meta:
408        verbose_name_plural= "group statuses"
409
410
411class GroupFunding(models.Model):
412    name = models.CharField(max_length=50)
413    slug = models.SlugField(unique=True, )
414    contact_email = models.EmailField()
415    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.")
416
417    def __str__(self, ):
418        return "%s (%s)" % (self.name, self.contact_email, )
419
420
421class AthenaMoiraAccount_ActiveManager(models.Manager):
422    def get_query_set(self, ):
423        return super(AthenaMoiraAccount_ActiveManager, self).get_query_set().filter(del_date=None)
424
425class AthenaMoiraAccount(models.Model):
426    username = models.CharField(max_length=8)
427    mit_id = models.CharField(max_length=15)
428    first_name      = models.CharField(max_length=45)
429    last_name       = models.CharField(max_length=45)
430    account_class   = models.CharField(max_length=10)
431    mutable         = models.BooleanField(default=True)
432    add_date        = models.DateField(help_text="Date when this person was added to the dump.", )
433    del_date        = models.DateField(help_text="Date when this person was removed from the dump.", blank=True, null=True, )
434    mod_date        = models.DateField(help_text="Date when this person's record was last changed.", blank=True, null=True, )
435
436    objects = models.Manager()
437    active_accounts = AthenaMoiraAccount_ActiveManager()
438
439    def is_student(self, ):
440        # XXX: Is this... right?
441        return self.account_class == 'G' or self.account_class.isdigit()
442
443    def format(self, ):
444        return "%s %s <%s>" % (self.first_name, self.last_name, self.username, )
445
446    def __str__(self, ):
447        if self.mutable:
448            mutable_str = ""
449        else:
450            mutable_str = " (immutable)"
451        return "<AthenaMoiraAccount: username=%s name='%s, %s' account_class=%s%s>" % (
452            self.username, self.last_name, self.first_name,
453            self.account_class, mutable_str,
454        )
455
456    def __repr__(self, ):
457        return str(self)
458
459    @classmethod
460    def try_format_by_username(cls, username):
461        try:
462            moira = AthenaMoiraAccount.objects.get(username=username)
463            return moira.format()
464        except AthenaMoiraAccount.DoesNotExist:
465            return "%s (name not available)" % (username)
466
467    class Meta:
468        verbose_name = "Athena (Moira) account"
Note: See TracBrowser for help on using the repository browser.