source: asadb/forms/models.py @ 4217b13

stablestage
Last change on this file since 4217b13 was 4217b13, checked in by Alex Dehnert <adehnert@…>, 12 years ago

People lookup: specify intended format (ASA-#253)

  • Property mode set to 100644
File size: 16.3 KB
Line 
1import datetime
2import errno
3import json
4import os
5
6import ldap
7
8from django.conf import settings
9from django.contrib.auth.models import User
10from django.db import models
11
12import groups.models
13from util.misc import log_and_ignore_failures, mkdir_p
14import util.previews
15
16class FYSM(models.Model):
17    group = models.ForeignKey(groups.models.Group, db_index=True, )
18    display_name = models.CharField(max_length=50, help_text="""Form of your name suitable for display (for example, don't end your name with ", MIT")""")
19    year = models.IntegerField(db_index=True, )
20    website = models.URLField()
21    join_url = models.URLField(verbose_name="recruiting URL", help_text="""<p>If you have a specific web page for recruiting new members of your group, you can link to it here. It will be used as the destination for most links about your group (join link on the main listing page and when clicking on the slide, but not the "website" link on the slide page). If you do not have such a page, use your main website's URL.</p>""")
22    contact_email = models.EmailField(help_text="Give an address for students interested in joining the group to email (e.g., an officers list)")
23    description = models.TextField(help_text="Explain, in no more than 400 characters (including spaces), what your group does and why incoming students should get involved.")
24    logo = models.ImageField(upload_to='fysm/logos', blank=True, help_text="Upload a logo (JPG, GIF, or PNG) to display on the main FYSM page as well as the group detail page. This will be scaled to be 100px wide.")
25    slide = models.ImageField(upload_to='fysm/slides', blank=True, default="", help_text="Upload a slide (JPG, GIF, or PNG) to display on the group detail page. This will be scaled to be at most 600x600 pixels. We recommend making it exactly that size.")
26    tags = models.CharField(max_length=100, blank=True, help_text="Specify some free-form, comma-delimited tags for your group", )
27    categories = models.ManyToManyField('FYSMCategory', blank=True, help_text="Put your group into whichever of our categories seem applicable.", )
28    join_preview = models.ForeignKey('PagePreview', null=True, )
29
30    def save(self, *args, **kwargs):
31        if self.join_preview is None or self.join_url != self.join_preview.url:
32            self.join_preview = PagePreview.allocate_page_preview(
33                filename='fysm/%d/group%d'%(self.year, self.group.pk, ),
34                url=self.join_url,
35            )
36        super(FYSM, self).save(*args, **kwargs) # Call the "real" save() method.
37
38    def __str__(self, ):
39        return "%s (%d)" % (self.display_name, self.year, )
40
41    class Meta:
42        verbose_name = "FYSM submission"
43
44class FYSMCategory(models.Model):
45    name = models.CharField(max_length=25)
46    slug = models.SlugField(unique=True, )
47    blurb = models.TextField()
48
49    def __str__(self, ):
50        return self.name
51
52    class Meta:
53        verbose_name = "FYSM category"
54        verbose_name_plural = "FYSM categories"
55        ordering = ['name', ]
56
57class FYSMView(models.Model):
58    when = models.DateTimeField(default=datetime.datetime.now)
59    fysm = models.ForeignKey(FYSM, null=True, blank=True, )
60    year = models.IntegerField(null=True, blank=True, )
61    page = models.CharField(max_length=20, blank=True, )
62    referer = models.URLField(verify_exists=False, null=True, )
63    user_agent = models.CharField(max_length=255)
64    source_ip = models.IPAddressField()
65    source_user = models.CharField(max_length=30, blank=True, )
66
67    @staticmethod
68    @log_and_ignore_failures(logfile=settings.LOGFILE)
69    def record_metric(request, fysm=None, year=None, page=None, ):
70        record = FYSMView()
71        record.fysm = fysm
72        record.year = year
73        record.page = page
74        if 'HTTP_REFERER' in request.META:
75            record.referer = request.META['HTTP_REFERER']
76        record.user_agent = request.META['HTTP_USER_AGENT']
77        record.source_ip = request.META['REMOTE_ADDR']
78        record.source_user = request.user.username
79        record.save()
80
81class PagePreview(models.Model):
82    update_time = models.DateTimeField(default=datetime.datetime.utcfromtimestamp(0))
83    url = models.URLField()
84    image = models.ImageField(upload_to='page-previews', blank=True, )
85
86    never_updated = datetime.datetime.utcfromtimestamp(0) # Never updated
87    update_interval = datetime.timedelta(hours=23)
88
89    def image_filename(self, ):
90        return os.path.join(settings.MEDIA_ROOT, self.image.name)
91
92
93    @classmethod
94    def allocate_page_preview(cls, filename, url, ):
95        preview = PagePreview()
96        preview.update_time = cls.never_updated
97        preview.url = url
98        preview.image = 'page-previews/%s.jpg' % (filename, )
99        image_filename = preview.image_filename()
100        mkdir_p(os.path.dirname(image_filename))
101        try:
102            os.symlink('no-preview.jpg', image_filename)
103        except OSError as exc:
104            if exc.errno == errno.EEXIST:
105                pass
106            else: raise
107        preview.save()
108        return preview
109
110    def update_preview(self, ):
111        self.update_time = datetime.datetime.now()
112        self.save()
113        failure = util.previews.generate_webpage_preview(self.url, self.image_filename(), )
114        if failure:
115            self.update_time = self.never_updated
116            self.save()
117
118    @classmethod
119    def previews_needing_updates(cls, interval=None, ):
120        if interval is None:
121            interval = cls.update_interval
122        before = datetime.datetime.now() - interval
123        return cls.objects.filter(update_time__lte=before)
124
125    @classmethod
126    def update_outdated_previews(cls, interval=None, ):
127        previews = cls.previews_needing_updates(interval)
128        now = datetime.datetime.now()
129        update_list = []
130        previews_dict = {}
131        for preview in previews:
132            update_list.append((preview.url, preview.image_filename(), ))
133            previews_dict[preview.url] = preview
134            preview.update_time = now
135            preview.save()
136        failures = util.previews.generate_webpage_previews(update_list)
137        for url, msg in failures:
138            print "%s: %s" % (url, msg, )
139            preview = previews_dict[url]
140            preview.update_time = cls.never_updated
141            preview.save()
142
143
144class GroupConfirmationCycle(models.Model):
145    name = models.CharField(max_length=30)
146    slug = models.SlugField(unique=True, )
147    create_date = models.DateTimeField(default=datetime.datetime.now)
148    deadlines = models.TextField(blank=True)
149
150    def __unicode__(self, ):
151        return u"GroupConfirmationCycle %d: %s" % (self.id, self.name, )
152
153    @classmethod
154    def latest(cls, ):
155        return cls.objects.order_by('-create_date')[0]
156
157
158class GroupMembershipUpdate(models.Model):
159    update_time = models.DateTimeField(default=datetime.datetime.utcfromtimestamp(0))
160    updater_name = models.CharField(max_length=30)
161    updater_title = models.CharField(max_length=30, help_text="You need not hold any particular title in the group, but we like to know who is completing the form.")
162   
163    cycle = models.ForeignKey(GroupConfirmationCycle)
164    group = models.ForeignKey(groups.models.Group, help_text="If your group does not appear in the list above, then please email asa-exec@mit.edu.", db_index=True, )
165    group_email = models.EmailField(help_text="The text of the law will be automatically distributed to your members via this list, in order to comply with the law.")
166    officer_email = models.EmailField()
167
168    membership_definition = models.TextField()
169    num_undergrads = models.IntegerField()
170    num_grads = models.IntegerField()
171    num_alum = models.IntegerField()
172    num_other_affiliate = models.IntegerField(verbose_name="Num other MIT affiliates")
173    num_other = models.IntegerField(verbose_name="Num non-MIT")
174
175    membership_list = models.TextField(blank=True, help_text="Member emails on separate lines (Athena usernames where applicable)")
176
177    email_preface = models.TextField(blank=True, help_text="If you would like, you may add text here that will preface the text of the policies when it is sent out to the group membership list provided above.")
178
179    hazing_statement = "By checking this, I hereby affirm that I have read and understand <a href='http://web.mit.edu/asa/rules/ma-hazing-law.html'>Chapter 269: Sections 17, 18, and 19 of Massachusetts Law</a>. I furthermore attest that I have provided the appropriate address or will otherwise distribute to group members, pledges, and/or applicants, copies of Massachusetts Law 269: 17, 18, 19 and that our organization, group, or team agrees to comply with the provisions of that law. (See below for text.)"
180    no_hazing = models.BooleanField(help_text=hazing_statement)
181
182    discrimination_statement = "By checking this, I hereby affirm that I have read and understand the <a href='http://web.mit.edu/referencepubs/nondiscrimination/'>MIT Non-Discrimination Policy</a>.  I furthermore attest that our organization, group, or team agrees to not discriminate against individuals on the basis of race, color, sex, sexual orientation, gender identity, religion, disability, age, genetic information, veteran status, ancestry, or national or ethnic origin."
183    no_discrimination = models.BooleanField(help_text=discrimination_statement)
184
185    def __unicode__(self, ):
186        return "GroupMembershipUpdate for %s" % (self.group, )
187
188
189VALID_UNSET         = 0
190VALID_AUTOVALIDATED = 10
191VALID_OVERRIDDEN    = 20    # confirmed by an admin
192VALID_AUTOREJECTED      = -10
193VALID_HANDREJECTED      = -20
194VALID_CHOICES = (
195    (VALID_UNSET,           "unvalidated"),
196    (VALID_AUTOVALIDATED,   "autovalidated"),
197    (VALID_OVERRIDDEN,      "hand-validated"),
198    (VALID_AUTOREJECTED,    "autorejected"),
199    (VALID_HANDREJECTED,    "hand-rejected"),
200)
201
202class PersonMembershipUpdate(models.Model):
203    update_time = models.DateTimeField(default=datetime.datetime.utcfromtimestamp(0))
204    username = models.CharField(max_length=30)
205    cycle = models.ForeignKey(GroupConfirmationCycle, db_index=True, )
206    deleted = models.DateTimeField(default=None, null=True, blank=True, )
207    valid = models.IntegerField(choices=VALID_CHOICES, default=VALID_UNSET)
208    groups = models.ManyToManyField(groups.models.Group, help_text="By selecting a group here, you indicate that you are an active member of the group in question.<br>If your group does not appear in the list above, then please email asa-exec@mit.edu.<br>")
209
210    def __unicode__(self, ):
211        return "PersonMembershipUpdate for %s" % (self.username, )
212
213
214class PeopleStatusLookup(models.Model):
215    people = models.TextField(help_text="Enter some usernames or email addresses, separated by newlines, to look up here.")
216    requestor = models.ForeignKey(User, null=True, blank=True, )
217    referer = models.URLField(blank=True)
218    time = models.DateTimeField(default=datetime.datetime.now)
219    classified_people_json = models.TextField()
220    _classified_people = None
221
222    def ldap_classify(self, usernames, ):
223        con = ldap.open('ldap-too.mit.edu')
224        con.simple_bind_s("", "")
225        dn = "ou=users,ou=moira,dc=mit,dc=edu"
226        fields = ['uid', 'eduPersonAffiliation', 'mitDirStudentYear']
227
228        chunk_size = 100
229        username_chunks = []
230        ends = range(chunk_size, len(usernames), chunk_size)
231        start = 0
232        end = 0
233        for end in ends:
234            username_chunks.append(usernames[start:end])
235            start = end
236        extra = usernames[end:]
237        if extra:
238            username_chunks.append(extra)
239
240        results = []
241        for chunk in username_chunks:
242            filters = [ldap.filter.filter_format('(uid=%s)', [u]) for u in chunk]
243            userfilter = "(|%s)" % (''.join(filters), )
244            batch_results = con.search_s(dn, ldap.SCOPE_SUBTREE, userfilter, fields)
245            results.extend(batch_results)
246
247        left = set([u.lower() for u in usernames])
248        undergrads = []
249        grads = []
250        staff = []
251        secret = []
252        other = []
253        info = {
254            'undergrads': undergrads,
255            'grads': grads,
256            'staff': staff,
257            'secret': secret,
258            'affiliate': other,
259        }
260        for result in results:
261            username = result[1]['uid'][0]
262            left.remove(username.lower())
263            affiliation = result[1].get('eduPersonAffiliation', ['secret'])[0]
264            if affiliation == 'student':
265                year = result[1].get('mitDirStudentYear', [None])[0]
266                if year == 'G':
267                    grads.append((username, None))
268                elif year.isdigit():
269                    undergrads.append((username, year))
270                else:
271                    other.append((username, year))
272            else:
273                info[affiliation].append((username, None, ))
274        info['unknown'] = [(u, None) for u in left]
275        return info
276
277    def classify_people(self, people):
278        mit_usernames = []
279        alum_addresses = []
280        other_mit_addresses = []
281        nonmit_addresses = []
282
283        for name in people:
284            local, at, domain = name.partition('@')
285            if domain.lower() == 'mit.edu' or domain == '':
286                mit_usernames.append(local)
287            elif domain.lower() == 'alum.mit.edu':
288                alum_addresses.append((name, None))
289            elif domain.endswith('.mit.edu'):
290                other_mit_addresses.append((name, None))
291            else:
292                nonmit_addresses.append((name, None))
293
294        results = self.ldap_classify(mit_usernames)
295        results['alum'] = alum_addresses
296        results['other-mit'] = other_mit_addresses
297        results['non-mit'] = nonmit_addresses
298        return results
299
300    def update_classified_people(self):
301        people = [p for p in [p.strip() for p in self.people.split('\n')] if p]
302        self._classified_people = self.classify_people(people)
303        self.classified_people_json = json.dumps(self._classified_people)
304        return self._classified_people
305
306    @property
307    def classified_people(self):
308        if self._classified_people is None:
309            self._classified_people = json.loads(self.classified_people_json)
310        return self._classified_people
311
312    def classifications_with_descriptions(self):
313        descriptions = {
314            'undergrads':   'Undergraduate students (class year in parentheses)',
315            'grads':        'Graduate students',
316            'alum':         "Alumni Association addresses",
317            'staff':        'MIT Staff (including faculty)',
318            'affiliate':    'This includes some alumni, group members with Athena accounts sponsored through SAO, and many others.',
319            'secret':       'People with directory information suppressed. These people have Athena accounts, but they could have any MIT affiliation, including just being a student group member.',
320            'unknown':      "While this looks like an Athena account, we couldn't find it. This could be a deactivated account, or it might never have existed.",
321            'other-mit':    ".mit.edu addresses that aren't @mit.edu or @alum.mit.edu.",
322            'non-mit':      "Non-MIT addresses, including outside addresses of MIT students.",
323        }
324
325        names = (
326            ('undergrads', 'Undergrads', ),
327            ('grads', 'Grad students', ),
328            ('alum', 'Alumni', ),
329            ('staff', 'Staff', ),
330            ('affiliate', 'Affiliates', ),
331            ('secret', 'Secret', ),
332            ('unknown', 'Unknown', ),
333            ('other-mit', 'Other MIT addresses', ),
334            ('non-mit', 'Non-MIT addresses', ),
335        )
336
337        classifications = self.classified_people
338        sorted_results = []
339        for k, label in names:
340            sorted_results.append({
341                'label': label,
342                'description': descriptions[k],
343                'people': sorted(classifications[k]),
344            })
345        return sorted_results
346
347
348##########
349# MIDWAY #
350##########
351
352
353class Midway(models.Model):
354    name = models.CharField(max_length=50)
355    slug = models.SlugField()
356    date = models.DateTimeField()
357    table_map = models.ImageField(upload_to='midway/maps')
358
359    def __str__(self, ):
360        return "%s" % (self.name, )
361
362class MidwayAssignment(models.Model):
363    midway = models.ForeignKey(Midway)
364    location = models.CharField(max_length=20)
365    group = models.ForeignKey(groups.models.Group)
366
367    def __str__(self, ):
368        return "<MidwayAssignment: %s at %s at %s>" % (self.group, self.location, self.midway, )
Note: See TracBrowser for help on using the repository browser.