Changeset ef118cf


Ignore:
Timestamp:
Feb 11, 2014, 2:22:25 AM (12 years ago)
Author:
Alex Dehnert <adehnert@…>
Branches:
master, stable, stage
Children:
d4f571c
Parents:
2563230 (diff), e5c5180 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
git-author:
Alex Dehnert <adehnert@…> (02/11/14 02:22:25)
git-committer:
Alex Dehnert <adehnert@…> (02/11/14 02:22:25)
Message:

Merge branch 'master' into filters

  • master: (107 commits) People lookup: Handle more name formats (ASA-#253) People lookup: specify intended format (ASA-#253) People lookup: handle round numbers (ASA-#253) People lookup: force names to lowercase (ASA-#252) Add hazing and non-discrimination links (ASA-#255) Correct description of submitted link Add missing column heading (ASA-#258) Re-wrap anti-hazing email (ASA-#256) Add recognition date to issues.csv People lookup: fix *small* groups... Add docs for setting up chosen.js Link people lookup tool from membership update page People lookup: mention the data source People lookup: Support large numbers of people People lookup: make the UI more reasonable Basic people lookup support (ASA-#186) Add pre-group-approval warnings Reword membership definition text More thoroughly purge mention of membership lists Standarize on UK-style ndashes ...

Conflicts:

asadb/groups/models.py

Files:
19 added
38 edited

Legend:

Unmodified
Added
Removed
  • .gitignore

    r7ef7143 r7d7801f  
    77asadb/media/fysm/slides/
    88asadb/media/page-previews/fysm/
     9asadb/media/midway/maps/
    910asadb/util/saved-data/
    1011asadb/util/static-data/
  • asadb/forms/admin.py

    rbda4d86 r26826c9  
     1from django.contrib import admin
     2
    13import forms.models
    2 from django.contrib import admin
    34
    45class FYSMAdmin(admin.ModelAdmin):
     
    6667    search_fields = ('username', 'groups__officer_email', 'groups__name', 'groups__abbreviation', )
    6768admin.site.register(forms.models.PersonMembershipUpdate, Admin_PersonMembershipUpdate)
     69
     70class Admin_Midway(admin.ModelAdmin):
     71    list_display = (
     72        'pk',
     73        'name',
     74        'slug',
     75        'date',
     76    )
     77    list_display_links = list_display
     78    search_fields = ('name', )
     79    prepopulated_fields = {"slug": ("name",)}
     80admin.site.register(forms.models.Midway, Admin_Midway)
     81
     82class Admin_MidwayAssignment(admin.ModelAdmin):
     83    list_display = (
     84        'pk',
     85        'midway',
     86        'location',
     87        'group',
     88    )
     89    list_display_links = list_display
     90    list_filter = ('midway', )
     91    search_fields = ('location', 'group__name', )
     92admin.site.register(forms.models.MidwayAssignment, Admin_MidwayAssignment)
  • asadb/forms/models.py

    rafc5348 r86986f5  
     1import datetime
     2import errno
     3import json
     4import os
     5import re
     6
     7import ldap
     8
    19from django.conf import settings
     10from django.contrib.auth.models import User
    211from django.db import models
    3 
    4 import datetime
    5 import os, errno
    612
    713import groups.models
     
    1824    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.")
    1925    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.")
    20     slide = models.ImageField(upload_to='fysm/slides', 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    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.")
    2127    tags = models.CharField(max_length=100, blank=True, help_text="Specify some free-form, comma-delimited tags for your group", )
    2228    categories = models.ManyToManyField('FYSMCategory', blank=True, help_text="Put your group into whichever of our categories seem applicable.", )
     
    168174    num_other = models.IntegerField(verbose_name="Num non-MIT")
    169175
    170     membership_list = models.TextField(help_text="Member emails on separate lines (Athena usernames where applicable)")
     176    membership_list = models.TextField(blank=True, help_text="Member emails on separate lines (Athena usernames where applicable)")
    171177
    172178    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.")
    173179
    174     hazing_statement = "By checking this, I hereby affirm that I have read and understand Chapter 269: Sections 17, 18, and 19 of Massachusetts Law. 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    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.)"
    175181    no_hazing = models.BooleanField(help_text=hazing_statement)
    176182
    177     discrimination_statement = "By checking this, I hereby affirm that I have read and understand the MIT Non-Discrimination Policy.  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    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."
    178184    no_discrimination = models.BooleanField(help_text=discrimination_statement)
    179185
     
    205211    def __unicode__(self, ):
    206212        return "PersonMembershipUpdate for %s" % (self.username, )
     213
     214
     215class PeopleStatusLookup(models.Model):
     216    people = models.TextField(help_text="Enter some usernames or email addresses, separated by newlines, to look up here.")
     217    requestor = models.ForeignKey(User, null=True, blank=True, )
     218    referer = models.URLField(blank=True)
     219    time = models.DateTimeField(default=datetime.datetime.now)
     220    classified_people_json = models.TextField()
     221    _classified_people = None
     222
     223    def ldap_classify(self, usernames, ):
     224        con = ldap.open('ldap-too.mit.edu')
     225        con.simple_bind_s("", "")
     226        dn = "ou=users,ou=moira,dc=mit,dc=edu"
     227        fields = ['uid', 'eduPersonAffiliation', 'mitDirStudentYear']
     228
     229        chunk_size = 100
     230        username_chunks = []
     231        ends = range(chunk_size, len(usernames), chunk_size)
     232        start = 0
     233        end = 0
     234        for end in ends:
     235            username_chunks.append(usernames[start:end])
     236            start = end
     237        extra = usernames[end:]
     238        if extra:
     239            username_chunks.append(extra)
     240
     241        results = []
     242        for chunk in username_chunks:
     243            filters = [ldap.filter.filter_format('(uid=%s)', [u]) for u in chunk]
     244            userfilter = "(|%s)" % (''.join(filters), )
     245            batch_results = con.search_s(dn, ldap.SCOPE_SUBTREE, userfilter, fields)
     246            results.extend(batch_results)
     247
     248        left = set([u.lower() for u in usernames])
     249        undergrads = []
     250        grads = []
     251        staff = []
     252        secret = []
     253        other = []
     254        info = {
     255            'undergrads': undergrads,
     256            'grads': grads,
     257            'staff': staff,
     258            'secret': secret,
     259            'affiliate': other,
     260        }
     261        for result in results:
     262            username = result[1]['uid'][0]
     263            left.remove(username.lower())
     264            affiliation = result[1].get('eduPersonAffiliation', ['secret'])[0]
     265            if affiliation == 'student':
     266                year = result[1].get('mitDirStudentYear', [None])[0]
     267                if year == 'G':
     268                    grads.append((username, None))
     269                elif year.isdigit():
     270                    undergrads.append((username, year))
     271                else:
     272                    other.append((username, year))
     273            else:
     274                info[affiliation].append((username, None, ))
     275        info['unknown'] = [(u, None) for u in left]
     276        return info
     277
     278    def classify_people(self, people):
     279        mit_usernames = []
     280        alum_addresses = []
     281        other_mit_addresses = []
     282        nonmit_addresses = []
     283
     284        for name in people:
     285            local, at, domain = name.partition('@')
     286            if domain.lower() == 'mit.edu' or domain == '':
     287                mit_usernames.append(local)
     288            elif domain.lower() == 'alum.mit.edu':
     289                alum_addresses.append((name, None))
     290            elif domain.endswith('.mit.edu'):
     291                other_mit_addresses.append((name, None))
     292            else:
     293                nonmit_addresses.append((name, None))
     294
     295        results = self.ldap_classify(mit_usernames)
     296        results['alum'] = alum_addresses
     297        results['other-mit'] = other_mit_addresses
     298        results['non-mit'] = nonmit_addresses
     299        return results
     300
     301    def split_people(self):
     302        splitted = re.split(r'[\n,]+', self.people)
     303        people = []
     304        for name in splitted:
     305            name = name.strip()
     306            if len(name) > 2 and (name[0] == '(') and (name[-1] == ')'):
     307                name = name[1:-1]
     308            name = name.replace(' at ', '@')
     309            if name:
     310                people.append(name)
     311        return people
     312
     313    def update_classified_people(self):
     314        people = self.split_people()
     315        self._classified_people = self.classify_people(people)
     316        self.classified_people_json = json.dumps(self._classified_people)
     317        return self._classified_people
     318
     319    @property
     320    def classified_people(self):
     321        if self._classified_people is None:
     322            self._classified_people = json.loads(self.classified_people_json)
     323        return self._classified_people
     324
     325    def classifications_with_descriptions(self):
     326        descriptions = {
     327            'undergrads':   'Undergraduate students (class year in parentheses)',
     328            'grads':        'Graduate students',
     329            'alum':         "Alumni Association addresses",
     330            'staff':        'MIT Staff (including faculty)',
     331            'affiliate':    'This includes some alumni, group members with Athena accounts sponsored through SAO, and many others.',
     332            '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.',
     333            '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.",
     334            'other-mit':    ".mit.edu addresses that aren't @mit.edu or @alum.mit.edu.",
     335            'non-mit':      "Non-MIT addresses, including outside addresses of MIT students.",
     336        }
     337
     338        names = (
     339            ('undergrads', 'Undergrads', ),
     340            ('grads', 'Grad students', ),
     341            ('alum', 'Alumni', ),
     342            ('staff', 'Staff', ),
     343            ('affiliate', 'Affiliates', ),
     344            ('secret', 'Secret', ),
     345            ('unknown', 'Unknown', ),
     346            ('other-mit', 'Other MIT addresses', ),
     347            ('non-mit', 'Non-MIT addresses', ),
     348        )
     349
     350        classifications = self.classified_people
     351        sorted_results = []
     352        for k, label in names:
     353            sorted_results.append({
     354                'label': label,
     355                'description': descriptions[k],
     356                'people': sorted(classifications[k]),
     357            })
     358        return sorted_results
     359
     360
     361##########
     362# MIDWAY #
     363##########
     364
     365
     366class Midway(models.Model):
     367    name = models.CharField(max_length=50)
     368    slug = models.SlugField()
     369    date = models.DateTimeField()
     370    table_map = models.ImageField(upload_to='midway/maps')
     371
     372    def __str__(self, ):
     373        return "%s" % (self.name, )
     374
     375class MidwayAssignment(models.Model):
     376    midway = models.ForeignKey(Midway)
     377    location = models.CharField(max_length=20)
     378    group = models.ForeignKey(groups.models.Group)
     379
     380    def __str__(self, ):
     381        return "<MidwayAssignment: %s at %s at %s>" % (self.group, self.location, self.midway, )
  • asadb/forms/views.py

    rafc5348 r9605a6a  
    1 import forms.models
    2 import groups.models
    3 import groups.views
    4 import util.emails
     1import collections
     2import csv
     3import datetime
     4import StringIO
    55
    66from django.conf import settings
     
    1515from django.core.urlresolvers import reverse
    1616from django.core.mail import EmailMessage, mail_admins
     17from django.forms import FileField
    1718from django.forms import Form
    1819from django.forms import ModelForm
     
    2223from django.db.models import Q, Count
    2324
    24 import csv
    25 import datetime
    26 import StringIO
     25import django_filters
     26
     27import forms.models
     28import groups.models
     29import groups.views
     30import util.emails
    2731
    2832#################
     
    149153            'description',
    150154            'logo',
    151             'slide',
    152155            'tags',
    153156            'categories',
     
    249252    cycle = forms.models.GroupConfirmationCycle.latest()
    250253
    251     users_groups = groups.models.Group.involved_groups(request.user.username)
     254    users_groups = groups.models.Group.admin_groups(request.user.username)
    252255    qs = membership_update_qs.filter(pk__in=users_groups)
    253256
     
    258261        queryset=qs,
    259262        title="Submit membership update for...",
    260         msg="The list below contains only groups that list you as being involved. You must be an administrator of a group to submit an update.",
    261263    )
    262264
     
    265267        super(Form_GroupMembershipUpdate, self).__init__(*args, **kwargs)
    266268        self.fields['no_hazing'].required = True
     269        help_text = "If you have a membership list, you can get help turning it into membership breakdown using our <a href='%s'>people lookup</a> tool. You'll need to copy the numbers back over, though." % (reverse('membership-people-lookup'), )
     270        self.fields['num_undergrads'].help_text = help_text
    267271
    268272    class Meta:
     
    281285            'num_other_affiliate',
    282286            'num_other',
    283             'membership_list',
     287            #'membership_list',
    284288        ]
    285289
     
    415419            role_groups[office_holder.group.pk] = (office_holder.group, set())
    416420        role_groups[office_holder.group.pk][1].add(office_holder.role.display_name)
    417 
    418     # Find groups the user searched for
    419     filterset = groups.views.GroupFilter(request.GET, membership_update_qs)
    420     filtered_groups = filterset.qs.all()
    421     show_filtered_groups = ('search' in request.GET)
    422421
    423422    message = ""
     
    466465        'role_groups':role_groups,
    467466        'form':form,
    468         'filter':filterset,
    469         'show_filtered_groups':show_filtered_groups,
    470         'filtered_groups':filtered_groups,
    471467        'member_groups':selected_groups,
    472468        'message': message,
     
    496492
    497493
     494class View_GroupConfirmationCyclesList(ListView):
     495    context_object_name = "cycle_list"
     496    template_name = "membership/admin.html"
     497    model = forms.models.GroupConfirmationCycle
     498
     499    def get_context_data(self, **kwargs):
     500        context = super(View_GroupConfirmationCyclesList, self).get_context_data(**kwargs)
     501        context['pagename'] = 'groups'
     502        return context
     503
     504
    498505@permission_required('groups.view_group_private_info')
    499 def group_confirmation_issues(request, ):
     506def group_confirmation_issues(request, slug, ):
    500507    account_numbers = ("accounts" in request.GET) and request.GET['accounts'] == "1"
    501508
    502     active_groups = groups.models.Group.active_groups
    503     group_updates = forms.models.GroupMembershipUpdate.objects.all()
     509    check_groups = groups.models.Group.objects.filter(group_status__slug__in=('active', 'suspended', ))
     510    check_groups = check_groups.select_related('group_status')
     511    group_updates = forms.models.GroupMembershipUpdate.objects.filter(cycle__slug=slug, )
     512    group_updates = group_updates.select_related('group', 'group__group_status')
    504513    people_confirmations = forms.models.PersonMembershipUpdate.objects.filter(
    505514        deleted__isnull=True,
    506515        valid__gt=0,
     516        cycle__slug=slug,
    507517    )
    508518
    509519    buf = StringIO.StringIO()
    510520    output = csv.writer(buf)
    511     fields = ['group_id', 'group_name', 'issue', 'num_confirm', 'officer_email', ]
     521    fields = ['group_id', 'group_name', 'group_status', 'recognition_date', 'issue', 'num_confirm', 'officer_email', ]
    512522    if account_numbers: fields.append("main_account")
    513523    output.writerow(fields)
    514524
    515     q_present = Q(id__in=group_updates.values('group'))
    516     missing_groups = active_groups.filter(~q_present)
    517     #print len(list(group_updates))
    518     for group in missing_groups:
    519         num_confirms = len(people_confirmations.filter(groups=group))
     525    def output_issue(group, issue, num_confirms):
    520526        fields = [
    521527            group.id,
    522528            group.name,
    523             'unsubmitted',
     529            group.group_status.slug,
     530            group.recognition_date,
     531            issue,
    524532            num_confirms,
    525533            group.officer_email,
     
    528536        output.writerow(fields)
    529537
     538    q_present = Q(id__in=group_updates.values('group'))
     539    missing_groups = check_groups.filter(~q_present)
     540    #print len(list(group_updates))
     541    for group in missing_groups:
     542        #num_confirms = len(people_confirmations.filter(groups=group))
     543        output_issue(group, 'unsubmitted', '')
     544
    530545    for group_update in group_updates:
    531546        group = group_update.group
    532         num_confirms = len(people_confirmations.filter(groups=group))
     547        num_confirms = people_confirmations.filter(groups=group).count()
    533548        problems = []
    534549
     
    542557
    543558        for problem in problems:
    544             fields = [
    545                 group.id,
    546                 group.name,
    547                 problem,
    548                 num_confirms,
    549                 group.officer_email,
    550             ]
    551             if account_numbers: fields.append(group.main_account_id)
    552             output.writerow(fields)
    553 
     559            output_issue(group, problem, num_confirms)
    554560
    555561    return HttpResponse(buf.getvalue(), mimetype='text/csv', )
     562
     563
     564class PeopleStatusLookupForm(ModelForm):
     565    class Meta:
     566        model = forms.models.PeopleStatusLookup
     567        fields = ('people', )
     568
     569def people_status_lookup(request, pk=None, ):
     570    if pk is None:
     571        if request.method == 'POST':
     572            form = PeopleStatusLookupForm(request.POST, request.FILES, )
     573            if form.is_valid(): # All validation rules pass
     574                lookup = form.save(commit=False)
     575                lookup.requestor = request.user
     576                lookup.referer = request.META['HTTP_REFERER']
     577                lookup.update_classified_people()
     578                results = lookup.classifications_with_descriptions()
     579                lookup.save()
     580        else:
     581            form = PeopleStatusLookupForm()
     582            results = None
     583    else:
     584        if request.user.has_perm('forms.view_peoplestatusupdate'):
     585            lookup = get_object_or_404(forms.models.PeopleStatusLookup, pk=int(pk))
     586            results = lookup.classifications_with_descriptions()
     587            form = None
     588        else:
     589            raise PermissionDenied("You don't have permission to view old lookup requests.")
     590
     591    context = {
     592        'form': form,
     593        'results': results,
     594    }
     595
     596    return render_to_response('membership/people-lookup.html', context, context_instance=RequestContext(request), )
     597
     598##########
     599# Midway #
     600##########
     601
     602
     603class View_Midways(ListView):
     604    context_object_name = "midway_list"
     605    template_name = "midway/midway_list.html"
     606
     607    def get_queryset(self):
     608        midways = forms.models.Midway.objects.order_by('date')
     609        return midways
     610
     611    def get_context_data(self, **kwargs):
     612        context = super(View_Midways, self).get_context_data(**kwargs)
     613        context['pagename'] = 'midway'
     614        return context
     615
     616def midway_map_latest(request, ):
     617    midways = forms.models.Midway.objects.order_by('-date')[:1]
     618    if len(midways) == 0:
     619        raise Http404("No midways found.")
     620    else:
     621        url = reverse('midway-map', args=(midways[0].slug, ))
     622        return HttpResponseRedirect(url)
     623
     624
     625class MidwayAssignmentFilter(django_filters.FilterSet):
     626    name = django_filters.CharFilter(name='group__name', lookup_type='icontains', label="Name contains")
     627    abbreviation = django_filters.CharFilter(name='group__abbreviation', lookup_type='iexact', label="Abbreviation is")
     628    activity_category = django_filters.ModelChoiceFilter(
     629        label='Activity category',
     630        name='group__activity_category',
     631        queryset=groups.models.ActivityCategory.objects,
     632    )
     633
     634    class Meta:
     635        model = forms.models.MidwayAssignment
     636        fields = [
     637            'name',
     638            'abbreviation',
     639            'activity_category',
     640        ]
     641        order_by = (
     642            ('group__name', 'Name', ),
     643            ('group__abbreviation', 'Abbreviation', ),
     644            ('group__activity_category__name', 'Activity category', ),
     645            ('location', 'Location', ),
     646        )
     647
     648
     649class MidwayMapView(DetailView):
     650    context_object_name = "midway"
     651    model = forms.models.Midway
     652    template_name = 'midway/map.html'
     653
     654    def get_context_data(self, **kwargs):
     655        # Call the base implementation first to get a context
     656        context = super(MidwayMapView, self).get_context_data(**kwargs)
     657       
     658        filterset = MidwayAssignmentFilter(self.request.GET)
     659        context['assignments'] = filterset.qs
     660        context['filter'] = filterset
     661        context['pagename'] = 'midway'
     662
     663        return context
     664
     665
     666class MidwayAssignmentsUploadForm(Form):
     667    def validate_csv_fields(upload_file):
     668        reader = csv.reader(upload_file)
     669        row = reader.next()
     670        for col in ('Group', 'officers', 'Table', ):
     671            if col not in row:
     672                raise ValidationError('Please upload a CSV file with (at least) columns "Group", "officers", and "Table". (Missing at least "%s".)' % (col, ))
     673
     674    assignments = FileField(validators=[validate_csv_fields])
     675
     676@permission_required('forms.add_midwayassignment')
     677def midway_assignment_upload(request, slug, ):
     678    midway = get_object_or_404(forms.models.Midway, slug=slug, )
     679
     680    uploaded = False
     681    found = []
     682    issues = collections.defaultdict(list)
     683
     684    if request.method == 'POST': # If the form has been submitted...
     685        form = MidwayAssignmentsUploadForm(request.POST, request.FILES, ) # A form bound to the POST data
     686
     687        if form.is_valid(): # All validation rules pass
     688            uploaded = True
     689            reader = csv.DictReader(request.FILES['assignments'])
     690            for row in reader:
     691                group_name = row['Group']
     692                group_officers = row['officers']
     693                table = row['Table']
     694                issue = False
     695                try:
     696                    group = groups.models.Group.objects.get(name=group_name)
     697                    assignment = forms.models.MidwayAssignment(
     698                        midway=midway,
     699                        location=table,
     700                        group=group,
     701                    )
     702                    assignment.save()
     703                    found.append(assignment)
     704                    status = group.group_status.slug
     705                    if status != 'active':
     706                        issue = 'status=%s (added anyway)' % (status, )
     707                except groups.models.Group.DoesNotExist:
     708                    issue = 'unknown group (ignored)'
     709                except groups.models.Group.MultipleObjectsReturned:
     710                    issue = 'multiple groups found (ignored)'
     711                if issue:
     712                    issues[issue].append((group_name, group_officers, table))
     713            for issue in issues:
     714                issues[issue] = sorted(issues[issue], key=lambda x: x[0])
     715
     716    else:
     717        form = MidwayAssignmentsUploadForm() # An unbound form
     718
     719    context = {
     720        'midway':midway,
     721        'form':form,
     722        'uploaded': uploaded,
     723        'found': found,
     724        'issues': dict(issues),
     725        'pagename':'midway',
     726    }
     727    return render_to_response('midway/upload.html', context, context_instance=RequestContext(request), )
  • asadb/groups/admin.py

    rcbffe98 r89165c1  
     1import datetime
     2
     3from django.contrib import admin
     4from django.utils.translation import ugettext_lazy
     5
     6from reversion.admin import VersionAdmin
     7
    18import groups.models
    2 from django.contrib import admin
    3 from reversion.admin import VersionAdmin
     9import util.admin
    410
    511class GroupAdmin(VersionAdmin):
     
    98104
    99105class OfficeHolderAdmin(VersionAdmin):
     106    class OfficeHolderPeriodFilter(util.admin.TimePeriodFilter):
     107        start_field = 'start_time'
     108        end_field = 'end_time'
     109
     110    def expire_holders(self, request, queryset):
     111        rows_updated = queryset.update(end_time=datetime.datetime.now())
     112        if rows_updated == 1:
     113            message_bit = "1 entry was"
     114        else:
     115            message_bit = "%s entries were" % rows_updated
     116        self.message_user(request, "%s successfully expired." % message_bit)
     117    expire_holders.short_description = ugettext_lazy("Expire selected %(verbose_name_plural)s")
     118
     119    actions = ['expire_holders']
     120
    100121    list_display = (
    101122        'id',
     
    121142    list_filter = [
    122143        'role',
     144        OfficeHolderPeriodFilter,
    123145    ]
    124146admin.site.register(groups.models.OfficeHolder, OfficeHolderAdmin)
     
    181203        'last_name',
    182204        'account_class',
     205        'affiliation_basic',
     206        'loose_student',
    183207        'mutable',
    184208        'add_date',
     
    188212    list_display_links = ( 'id', 'username', )
    189213    search_fields = ( 'username', 'mit_id', 'first_name', 'last_name', 'account_class', )
     214    list_filter = (
     215        'account_class',
     216        'affiliation_basic', 'affiliation_detailed',
     217        'loose_student',
     218        'mutable',
     219    )
    190220admin.site.register(groups.models.AthenaMoiraAccount, Admin_AthenaMoiraAccount)
  • asadb/groups/diffs.py

    rafc5348 r9af1bb4  
    244244
    245245def default_active_pred():
    246     status_objs = groups.models.GroupStatus.objects.filter(slug__in=['active', 'suspended', 'nge'])
     246    status_objs = groups.models.GroupStatus.objects.filter(slug__in=['active', 'suspended', ])
    247247    status_pks = [status.pk for status in status_objs]
    248248    def pred(version, fields):
  • asadb/groups/load_people.py

    r161ce5f r89165c1  
    2626    'last_name',
    2727    'account_class',
     28    'affiliation_basic',
     29    'affiliation_detailed',
    2830]
    2931
  • asadb/groups/models.py

    r62f73df ref118cf  
    11# -*- coding: utf8 -*-
    22
    3 from django.conf import settings
    4 from django.db import models
    5 from django.core.exceptions import ValidationError
    6 from django.core.validators import RegexValidator
    7 from django.contrib.auth.models import User
    8 from django.template.defaultfilters import slugify
    9 import reversion
    10 
     3import collections
    114import datetime
    125import filecmp
     
    2013import urllib2
    2114
     15from django.conf import settings
     16from django.db import models
     17from django.db.models import Q
     18from django.core.exceptions import ValidationError
     19from django.core.validators import RegexValidator
     20from django.contrib.auth.models import User, Permission
     21from django.contrib.contenttypes.models import ContentType
     22from django.template.defaultfilters import slugify
     23
     24import reversion
     25
    2226import mit
    2327
     
    3034        )
    3135
    32 locker_validator = RegexValidator(regex=r'^[-A-Za-z0-9_.]+$', message='Enter a valid Athena locker.')
     36locker_validator = RegexValidator(regex=r'^[-A-Za-z0-9_.]+$', message='Enter a valid Athena locker. This should be the single "word" that appears in "/mit/word/" or "web.mit.edu/word/", with no slashes, spaces, etc..')
    3337
    3438class Group(models.Model):
     
    100104            if as_of == "now": as_of = datetime.datetime.now()
    101105            office_holders = office_holders.filter(start_time__lte=as_of, end_time__gte=as_of)
     106        office_holders = office_holders.order_by('role', 'person')
    102107        return office_holders
    103108
     
    112117        current_officers = OfficeHolder.current_holders.filter(person=username)
    113118        users_groups = Group.objects.filter(officeholder__in=current_officers).distinct()
     119
     120    @staticmethod
     121    def admin_groups(username, codename='admin_group'):
     122        holders = OfficeHolder.current_holders.filter_perm(codename=codename).filter(person=username)
     123        users_groups = Group.objects.filter(officeholder__in=holders).distinct()
    114124        return users_groups
    115125
     
    395405
    396406    @classmethod
     407    def getRolesGrantingPerm(cls, perm=None, model=Group, codename=None, ):
     408        """Get all OfficerRole objects granting a permission
     409
     410        Either `perm` or `codename` must be supplied, but not both. If
     411        `codename` is provided (and `perm` is None), then `perm` the
     412        permission corresponding to `model` (default: `Group`) and `codename`
     413        will be found and used."""
     414
     415        if perm is None:
     416            ct = ContentType.objects.get_for_model(model)
     417            print ct
     418            print Permission.objects.filter(content_type=ct)
     419            perm = Permission.objects.get(content_type=ct, codename=codename)
     420
     421        Q_user = Q(user_permissions=perm)
     422        Q_group = Q(groups__permissions=perm)
     423        users = User.objects.filter(Q_user|Q_group)
     424        roles = cls.objects.filter(grant_user__in=users)
     425        return roles
     426
     427    @classmethod
    397428    def retrieve(cls, slug, ):
    398429        return cls.objects.get(slug=slug)
     430
    399431reversion.register(OfficerRole)
    400432
     
    407439        )
    408440
     441    def filter_perm(self, perm=None, model=Group, codename=None, ):
     442        roles = OfficerRole.getRolesGrantingPerm(perm=perm, model=model, codename=codename)
     443        return self.get_query_set().filter(role__in=roles)
     444
    409445class OfficeHolder(models.Model):
    410446    EXPIRE_OFFSET   = datetime.timedelta(seconds=1)
    411447    END_NEVER       = datetime.datetime.max
    412448
    413     person = models.CharField(max_length=30, db_index=True, )
     449    person = models.CharField(max_length=30, db_index=True, help_text='Athena username')
    414450    role = models.ForeignKey('OfficerRole', db_index=True, )
    415451    group = models.ForeignKey('Group', db_index=True, )
     
    433469    def __repr__(self, ):
    434470        return str(self)
     471
    435472reversion.register(OfficeHolder)
    436473
     
    506543
    507544    def __str__(self, ):
    508         active = ""
    509         if not self.is_active:
    510             active = " (inactive)"
    511         return "%s%s" % (self.name, active, )
     545        return self.name
    512546
    513547    class Meta:
     
    528562    def get_query_set(self, ):
    529563        return super(AthenaMoiraAccount_ActiveManager, self).get_query_set().filter(del_date=None)
     564
     565def student_account_classes():
     566    year = datetime.datetime.now().year
     567    return ["G"] + [str(yr) for yr in range(year-5, year+10)]
    530568
    531569class AthenaMoiraAccount(models.Model):
     
    535573    last_name       = models.CharField(max_length=45)
    536574    account_class   = models.CharField(max_length=10)
     575    affiliation_basic       = models.CharField(max_length=10)
     576    affiliation_detailed    = models.CharField(max_length=40)
     577    loose_student   = models.BooleanField(default=False, help_text='Whether to use loose or strict determination of student status. Loose means that either the account class or the affiliation should indicate student status; strict means that the affiliation must be student. In general, we use strict; for some people ("secret people") directory information is suppressed and the affiliation will be misleading.')
    537578    mutable         = models.BooleanField(default=True)
    538579    add_date        = models.DateField(help_text="Date when this person was added to the dump.", )
     
    544585
    545586    def is_student(self, ):
    546         # XXX: Is this... right?
    547         return self.account_class == 'G' or self.account_class.isdigit()
     587        student_affiliation = (self.affiliation_basic == 'student')
     588        student_class = (self.account_class in student_account_classes())
     589        return student_affiliation or (student_class and self.loose_student)
     590
     591    @staticmethod
     592    def student_q():
     593        q_affiliation = Q(affiliation_basic='student')
     594        q_class = Q(account_class__in=student_account_classes())
     595        return q_affiliation | (q_class & Q(loose_student=True))
    548596
    549597    def format(self, ):
  • asadb/groups/urls.py

    r532a8e9 ra7c08e4  
    11from django.conf.urls.defaults import *
     2
     3import django.shortcuts
    24
    35import groups.views
    46import space.views
    57
     8def redirect_view(to):
     9    def _redirect_view(request, **kwargs):
     10        return django.shortcuts.redirect(to, **kwargs)
     11    return _redirect_view
     12
    613group_patterns = patterns('',
    714    url(r'^$', groups.views.GroupDetailView.as_view(), name='group-detail', ),
    815    url(r'^edit/main$', groups.views.manage_main, name='group-manage-main', ),
    9     url(r'^edit/officers$', groups.views.manage_officers, name='group-manage-officers', ),
     16    url(r'^edit/people$', groups.views.manage_officers, name='group-manage-officers', ),
     17    url(r'^edit/officers$', redirect_view('groups:group-manage-officers'), ),
    1018    url(r'^history/$', groups.views.GroupHistoryView.as_view(), name='group-manage-history', ),
    1119    url(r'^space/$', space.views.manage_access, name='group-space-access', ),
  • asadb/groups/views.py

    rd7557b8 ref118cf  
    44import csv
    55import datetime
    6 
    7 import groups.models
    86
    97from django.contrib.auth.decorators import user_passes_test, login_required, permission_required
     
    3028import django_filters
    3129
     30import groups.models
    3231from util.db_form_utils import StaticWidget
     32import util.db_filters
    3333from util.emails import email_from_template
    3434
     
    129129        for field in self.force_required:
    130130            self.fields[field].required = True
    131         self.fields['constitution_url'].help_text = mark_safe("""Please put your current constitution URL or AFS path.<br>If you don't currently know where your constitution is, put "http://mit.edu/asa/start/constitution-req.html" and draft a constitution soon.""")
     131        self.fields['constitution_url'].help_text = mark_safe("Please put your current constitution URL or AFS path.")
    132132
    133133    exec_only_fields = [
     
    221221def manage_officers_load_officers(group, ):
    222222    officers = group.officers()
    223     people = list(set([ officer.person for officer in officers ]))
     223    people = sorted(set([ officer.person for officer in officers ]))
    224224    roles  = groups.models.OfficerRole.objects.all()
    225225
     
    573573    treasurer_name = forms.CharField(max_length=50)
    574574    treasurer_kerberos = forms.CharField(min_length=3, max_length=8, )
    575     def clean_president(self, ):
     575    def clean_president_kerberos(self, ):
    576576        username = self.cleaned_data['president_kerberos']
    577577        validate_athena(username, True, )
    578578        return username
    579579
    580     def clean_treasurer(self, ):
     580    def clean_treasurer_kerberos(self, ):
    581581        username = self.cleaned_data['treasurer_kerberos']
    582582        validate_athena(username, True, )
     
    630630        self.fields['constitution_url'].required = True
    631631        self.fields['constitution_url'].help_text = "Please put a copy of your finalized constitution on a publicly-accessible website (e.g. your group's, or your own, Public folder), and link to it in the box above."
     632        self.fields['group_email'].required = True
    632633        self.fields['athena_locker'].required = True
     634        self.fields['athena_locker'].help_text = "In general, this is limited to twelve characters. You should stick to letters, numbers, and hyphens. (Underscores and dots are also acceptable, but may cause problems in some situations.)"
     635
     636        # Specifically, if the group ends up wanting to use scripts.mit.edu,
     637        # they will currently be assigned locker.scripts.mit.edu. If they try
     638        # to use foo.bar, then https://foo.bar.scripts.mit.edu/ will produce a
     639        # certificate name mismatch. Officially, underscores are not allowed in
     640        # hostnames, so foo_.scripts.mit.edu may fail with some software.
    633641
    634642    class Meta(GroupCreateForm.Meta):
     
    688696            from_email='asa-admin@mit.edu',
    689697        )
    690         # XXX: Handle this better
    691         if officer_domain != 'mit.edu' or (create_group_list and group_domain != 'mit.edu'):
    692             accounts_mail.to = ['asa-groups@mit.edu']
    693             accounts_mail.cc = ['asa-db@mit.edu']
    694             accounts_mail.subject = "ERROR: " + accounts_mail.subject
    695             accounts_mail.body = "Bad domain on officer or group list\n\n" + accounts_mail.body
    696698
    697699    else:
     
    820822    return render_to_response('groups/create/startup.html', context, context_instance=RequestContext(request), )
    821823
     824def review_group_check_warnings(group_startup, group, ):
     825    warnings = []
     826
     827    if group.name.startswith("MIT "):
     828        warnings.append('Group name starts with "MIT". Generally, we prefer "Foo, MIT" instead.')
     829    if "mit" in group.athena_locker.lower():
     830        warnings.append('Athena locker name contains "mit", which may be redundant with paths like "http://web.mit.edu/mitfoo" or "/mit/foo/".')
     831
     832    if group_startup.president_kerberos == group_startup.treasurer_kerberos:
     833        warnings.append('President matches Treasurer.')
     834    if "%s@mit.edu" % (group_startup.president_kerberos, ) in (group.officer_email, group.group_email):
     835        warnings.append('President email matches officer and/or group email.')
     836    if group.officer_email == group.group_email:
     837        warnings.append('Officer email matches group email.')
     838
     839    if '@mit.edu' not in group.officer_email or '@mit.edu' not in group.group_email:
     840        warnings.append('Officer and/or group email are non-MIT. Ensure that they are not requesting the addresses be created, and consider suggesting they use an MIT list instead.')
     841
     842    if '.' in group.athena_locker:
     843        warnings.append('Athena locker contains a ".". This is not compatible with scripts.mit.edu\'s wildcard certificate, and may cause other problems.')
     844    if '_' in group.athena_locker:
     845        warnings.append('Athena locker contains a "_". If this locker name gets used in a URL (for example, locker.scripts.mit.edu), it will technically violate the hostname specification and may not work in some clients.')
     846    if len(group.athena_locker) > 12:
     847        warnings.append('Athena locker is more than twelve characters long. In general, twelve characters is the longest Athena locker an ASA-recognized group can get.')
     848
     849    return warnings
     850
    822851@permission_required('groups.recognize_group')
    823852def recognize_normal_group(request, pk, ):
     
    836865        return render_to_response('groups/create/err.not-applying.html', context, context_instance=RequestContext(request), )
    837866
     867    context['warnings'] = review_group_check_warnings(group_startup, group)
     868
    838869    context['msg'] = ""
    839870    if request.method == 'POST':
     
    842873            group_startup.save()
    843874
    844             group.group_status = groups.models.GroupStatus.objects.get(slug='active')
     875            group.group_status = groups.models.GroupStatus.objects.get(slug='suspended')
    845876            group.constitution_url = ""
    846877            group.recognition_date = datetime.datetime.now()
    847878            group.set_updater(request.user)
     879
     880            note = groups.models.GroupNote(
     881                author=request.user.username,
     882                body="Approved group for recognition.",
     883                acl_read_group=True,
     884                acl_read_offices=True,
     885                group=group,
     886            ).save()
    848887
    849888            group.save()
     
    900939    name = django_filters.CharFilter(lookup_type='icontains', label="Name contains")
    901940    abbreviation = django_filters.CharFilter(lookup_type='iexact', label="Abbreviation is")
     941    officer_email = django_filters.CharFilter(lookup_type='icontains', label="Officers' list contains")
     942
     943    account_filter = util.db_filters.MultiNumberFilter(
     944        lookup_type='exact', label="Account number",
     945        names=('main_account_id', 'funding_account_id', ),
     946    )
    902947
    903948    class Meta:
     
    906951            'name',
    907952            'abbreviation',
     953            'officer_email',
    908954            'activity_category',
    909955            'group_class',
    910956            'group_status',
    911957            'group_funding',
     958            'account_filter',
    912959        ]
    913960
     
    945992    groups_filterset = GroupFilter(request.GET, the_groups)
    946993    the_groups = groups_filterset.qs
     994
    947995    officers = groups.models.OfficeHolder.objects.filter(start_time__lte=datetime.datetime.now(), end_time__gte=datetime.datetime.now())
    948996    officers = officers.filter(group__in=the_groups)
    949997    officers = officers.select_related(depth=1)
     998
    950999    role_slugs = ['president', 'treasurer', 'financial', 'reservation']
    9511000    roles = groups.models.OfficerRole.objects.filter(slug__in=role_slugs)
    9521001    roles = sorted(roles, key=lambda r: role_slugs.index(r.slug))
     1002
    9531003    officers_map = collections.defaultdict(lambda: collections.defaultdict(set))
    9541004    for officer in officers:
     
    9581008        role_list = []
    9591009        for role in roles:
    960             role_list.append(officers_map[group][role])
     1010            role_list.append(sorted(officers_map[group][role]))
    9611011        officers_data.append((group, role_list))
    9621012
     
    10041054        if 'pk' in self.kwargs:
    10051055            group = get_object_or_404(groups.models.Group, pk=self.kwargs['pk'])
    1006             history_entries = reversion.models.Version.objects.get_for_object(group)
     1056            history_entries = reversion.get_for_object(group)
    10071057        else:
    10081058            history_entries = reversion.models.Version.objects.all()
     
    12881338def show_nonstudent_officers(request, ):
    12891339    student_roles  = groups.models.OfficerRole.objects.filter(require_student=True, )
    1290     year = datetime.datetime.now().year
    1291     account_classes = ["G"] + [str(yr) for yr in range(year-5, year+10)]
    1292     students = groups.models.AthenaMoiraAccount.active_accounts.filter(account_class__in=account_classes)
     1340    student_q = groups.models.AthenaMoiraAccount.student_q()
     1341    students = groups.models.AthenaMoiraAccount.active_accounts.filter(student_q)
    12931342    office_holders = groups.models.OfficeHolder.current_holders.order_by('group__name', 'role', )
    12941343    office_holders = office_holders.filter(role__in=student_roles)
    12951344    office_holders = office_holders.exclude(person__in=students.values('username'))
    1296     office_holders = office_holders.select_related('group', 'role')
     1345    office_holders = office_holders.select_related('group', 'group__group_status', 'role')
    12971346
    12981347    msg = None
     
    13001349    if 'sort' in request.GET:
    13011350        if request.GET['sort'] == 'group':
    1302             office_holders = office_holders.order_by('group__name', 'role', 'person', )
     1351            office_holders = office_holders.order_by('group__name', 'group__group_status', 'role', 'person', )
     1352        elif request.GET['sort'] == 'status':
     1353            office_holders = office_holders.order_by('group__group_status', 'group__name', 'role', 'person', )
    13031354        elif request.GET['sort'] == 'role':
    1304             office_holders = office_holders.order_by('role', 'group__name', 'person', )
     1355            office_holders = office_holders.order_by('role', 'group__group_status', 'group__name', 'person', )
    13051356        elif request.GET['sort'] == 'person':
    1306             office_holders = office_holders.order_by('person', 'group__name', 'role', )
     1357            office_holders = office_holders.order_by('person', 'group__group_status', 'group__name', 'role', )
    13071358        else:
    13081359            msg = 'Unknown sort key "%s".' % (request.GET['sort'], )
  • asadb/media/style/style.css

    rd6f8984 r53e624e  
    234234.group-detail-page.group-status-active h1          { color: black; }
    235235.group-detail-page.group-status-applying h1        { color: red; }
    236 .group-detail-page.group-status-nge h1             { color: yellow; }
     236.group-detail-page.group-status-nge h1             { color: #00ffff; }
    237237.group-detail-page.group-status-provisional h1     { color: yellow; }
    238238.group-detail-page.group-status-derecognized h1    { color: red; }
     
    254254    color: black;
    255255}
     256.group-list-page tr.group-status-nge td.group-status
     257{
     258    background-color: #00ffff;
     259    color: black;
     260}
    256261
    257262
     
    267272}
    268273
    269 table.group-change-officers-change td
     274table.group-update-people-change td
    270275{
    271276    text-align: center;
  • asadb/mit/__init__.py

    rd0c7563 r8c0a2ff  
    130130def scripts_login(request, **kwargs):
    131131    host = request.META['HTTP_HOST'].split(':')[0]
    132     if host == 'localhost':
     132    if host in ('localhost', '127.0.0.1'):
    133133        return login(request, **kwargs)
    134134    elif request.META['SERVER_PORT'] == '444':
  • asadb/settings.py

    red5797d rb4cc0cc  
    121121    # Always use forward slashes, even on Windows.
    122122    # Don't forget to use absolute paths, not relative paths.
    123     'template',
     123    os.path.join(SITE_ROOT, 'template'),
    124124)
    125125
  • asadb/space/admin.py

    r465eb7a r3c1b20b  
     1from django.contrib import admin
     2
     3from reversion.admin import VersionAdmin
     4
    15import space.models
    2 from django.contrib import admin
    3 from reversion.admin import VersionAdmin
     6import util.admin
     7
     8class Admin_LockType(VersionAdmin):
     9    list_display = (
     10        'id',
     11        'name',
     12        'slug',
     13        'info_addr',
     14        'info_url',
     15        'db_update',
     16    )
     17    list_display_links = ( 'id', 'name', 'slug', )
     18    search_fields = ('name', 'slug', 'info_addr', 'info_url', 'db_update', )
     19admin.site.register(space.models.LockType, Admin_LockType)
    420
    521class Admin_Space(VersionAdmin):
     
    824        'number',
    925        'asa_owned',
     26        'lock_type',
    1027        'merged_acl',
    1128    )
    1229    list_display_links = ( 'id', 'number', )
     30    list_filter = ('lock_type', )
    1331    search_fields = ('number', )
    1432admin.site.register(space.models.Space, Admin_Space)
    1533
    1634class Admin_SpaceAssignment(admin.ModelAdmin):
     35    class AssignmentPeriodFilter(util.admin.TimePeriodFilter):
     36        start_field = 'start'
     37        end_field = 'end'
     38
    1739    list_max_show_all = 500
    1840    list_display = (
     
    2446    )
    2547    list_display_links = list_display
    26     list_filter = ('space', )
     48    list_filter = (AssignmentPeriodFilter, 'space', )
    2749    search_fields = ( 'group__name', 'group__officer_email', 'space__number', )
    2850admin.site.register(space.models.SpaceAssignment, Admin_SpaceAssignment)
    2951
    3052class Admin_SpaceAccessListEntry(admin.ModelAdmin):
     53    class AccessPeriodFilter(util.admin.TimePeriodFilter):
     54        start_field = 'start'
     55        end_field = 'end'
     56
    3157    list_display = (
    3258        'group',
     
    3763    )
    3864    list_display_links = list_display
     65    list_filter = (AccessPeriodFilter, 'space', )
    3966    search_fields = (
    4067        'group__name', 'group__officer_email',
  • asadb/space/diffs.py

    rc8f4eea r27e09a9  
    88    cur_file = os.path.abspath(__file__)
    99    django_dir = os.path.abspath(os.path.join(os.path.dirname(cur_file), '..'))
     10    django_dir_parent = os.path.abspath(os.path.join(os.path.dirname(cur_file), '../..'))
    1011    sys.path.append(django_dir)
     12    sys.path.append(django_dir_parent)
    1113    os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
    1214
    1315from django.core.mail import EmailMessage
     16from django.core.urlresolvers import reverse
    1417from django.db import connection
    1518from django.db.models import Q
     
    100103
    101104    def list_office_changes(self, ):
    102         cac_lines = []
     105        systems_lines = {
     106            'cac-card': [],
     107            'none': [],
     108        }
    103109        group_lines = []
    104         def append_change(mit_id, verb, name):
    105             cac_lines.append("%s:\t%s:\t%s" % (mit_id, verb, name))
    106             group_lines.append("%s:\t%s" % (verb, name))
    107110        for space_pk, space_data in self.offices.items():
     111            lock_type = all_spaces[space_pk].lock_type
     112            system_lines = systems_lines[lock_type.db_update]
     113            def append_change(mit_id, verb, name):
     114                system_lines.append("%s:\t%s:\t%s" % (mit_id, verb, name))
     115                group_lines.append("%s:\t%s" % (verb, name))
     116
    108117            line = "Changes in %s:" % (all_spaces[space_pk].number, )
    109             cac_lines.append(line)
     118            system_lines.append(line)
    110119            group_lines.append(line)
     120
     121            if lock_type.db_update == 'none':
     122                tmpl =  'Warning: You submitted changes affecting this space, but this space is ' + \
     123                        'a "%s" space, and is not managed through the ASA DB. See ' + \
     124                        'https://asa.mit.edu%s for details on how to update spaces of this type.'
     125                line = tmpl % (lock_type.name, reverse('space-lock-type'), )
     126                group_lines.append(line)
     127
    111128            for mit_id, (old_names, new_names) in space_data.items():
    112129                if mit_id is None: mit_id = "ID unknown"
     
    125142                        else:
    126143                            append_change(mit_id, "Add", name)
    127             cac_lines.append("")
     144            system_lines.append("")
    128145            group_lines.append("")
    129146
    130         cac_msg = "\n".join(cac_lines)
     147        systems_msg = dict([
     148            (system, '\n'.join(lines), ) for (system, lines) in systems_lines.items()
     149        ])
    131150        group_msg = "\n".join(group_lines)
    132         return cac_msg, group_msg
     151        return systems_msg, group_msg
    133152
    134153    def add_locker_signatories(self, space_access, time):
     
    177196
    178197def safe_add_change_real(change_by_name, change):
     198    """Add a new change to our dict of pending changes.
     199
     200    If a different change has already been added for this person (eg, "Remove"
     201    instead of "Keep", or with a different list of groups), error.  This should
     202    always succeed; if it doesn't, the code is buggy. We worry about this
     203    because we want to be really sure that the email that goes to just CAC is
     204    compatible with the emails that go to each groups. Since we iterate over
     205    the changes once per group, we want to be sure that for each group
     206    iteration we're building compatible information.
     207    """
     208
    179209    name = change.name
    180210    if name in change_by_name:
     
    200230        print "ID=%s (%s):\n\t%s\t(%s)\n\t%s\t(%s)\n" % (mit_id, unchanged, old_by_names, old_by_group, new_by_names, new_by_group, ),
    201231        for group_pk in joint_keys(old_by_group, new_by_group):
     232            # TODO: Do we need to do an iteration for each group? This seems
     233            # slightly questionable. Can we just loop over all known names?
     234
    202235            old_names = old_by_group[group_pk]
    203236            new_names = new_by_group[group_pk]
     
    267300    cac_locker_msgs = []
    268301
    269     process_spaces =  space.models.Space.objects.all()
     302    process_spaces =  space.models.Space.objects.all().select_related('lock_type')
    270303    for the_space in process_spaces:
    271304        new_cac_msgs = space_specific_access(the_space, group_data, old_time, new_time)
     
    274307
    275308    changed_groups = []
     309    cac_chars = 0
    276310    for group_pk, group_info in group_data.items():
    277311        group_info.add_office_signatories(old_time, new_time)
    278         cac_changes, group_office_changes = group_info.list_office_changes()
     312        systems_changes, group_office_changes = group_info.list_office_changes()
    279313        if group_info.changes:
    280             changed_groups.append((group_info.group, cac_changes, group_office_changes, group_info.locker_messages, ))
     314            cac_chars += len(systems_changes['cac-card'])
     315            changed_groups.append((group_info.group, systems_changes['cac-card'], group_office_changes, group_info.locker_messages, ))
    281316
    282317    asa_rcpts = ['asa-space@mit.edu', 'asa-db@mit.edu', ]
    283     if changed_groups:
     318    if cac_chars > 0 or cac_locker_msgs:
    284319        util.emails.email_from_template(
    285320            tmpl='space/cac-change-email.txt',
  • asadb/space/models.py

    r2563230 ref118cf  
    1010
    1111EXPIRE_OFFSET   = datetime.timedelta(seconds=1)
     12
     13LOCK_DB_UPDATE_NONE = 'none'
     14LOCK_DB_UPDATE_CAC_CARD = 'cac-card'
     15lock_db_update_choices = (
     16    (LOCK_DB_UPDATE_NONE, "No database management"),
     17    (LOCK_DB_UPDATE_CAC_CARD, "CAC-managed card-based access"),
     18)
     19
     20class LockType(models.Model):
     21    name = models.CharField(max_length=50)
     22    slug = models.SlugField(unique=True, )
     23    description = models.TextField()
     24    info_addr = models.EmailField(default='asa-exec@mit.edu', help_text='Address groups should email to get more information about managing access through this lock type.')
     25    info_url = models.URLField(blank=True, help_text='URL that groups can visit to get more information about this lock type.')
     26    db_update = models.CharField(max_length=20, default='none', choices=lock_db_update_choices)
     27
     28    def __unicode__(self, ):
     29        return self.name
     30
    1231
    1332class Space(models.Model):
    1433    number = models.CharField(max_length=20, unique=True, )
    1534    asa_owned = models.BooleanField(default=True, )
     35    lock_type = models.ForeignKey(LockType)
    1636    merged_acl = models.BooleanField(default=False, help_text="Does this room have a single merged ACL, that combines all groups together, or CAC maintain a separate ACL per-group? Generally, the shared storage offices get a merged ACL and everything else doesn't.")
    1737    notes = models.TextField(blank=True, )
  • asadb/space/views.py

    rde2f0ac rf6982d4  
    9090        'allow_edit': allow_edit,
    9191        'extras_indices': extras_indices,
    92         'pagename':'group',
     92        'pagename':'groups',
    9393    }
    9494    return render_to_response('space/manage-access.html', context, context_instance=RequestContext(request), )
     
    112112        'locker_num',
    113113        'group__name',
    114     ).select_related('space', 'group')
     114    ).select_related('space', 'space__lock_type', 'group')
    115115    office_assignments = assignments.filter(locker_num='')
    116116
     
    127127        'offices': office_assignments,
    128128        'lockers': locker_rooms,
    129         'pagename':'group',
     129        'pagename':'groups',
    130130    }
    131131    return render_to_response('space/summary.html', context, context_instance=RequestContext(request), )
     132
     133def lock_types(request, ):
     134    lock_types = space.models.LockType.objects.order_by('name')
     135    context = {
     136        'lock_types': lock_types,
     137        'pagename': 'groups',
     138    }
     139    return render_to_response('space/lock_types.html', context, context_instance=RequestContext(request), )
  • asadb/template/base.html

    rcec082b r5560f6d  
    77    <link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}style/style.css" />
    88    <link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}style/page-frame.css" />
     9    {% block extrahead %}
     10    {% endblock %}
    911
    1012  </head>
     
    1416        <li{% ifequal pagename "groups"   %} class='selected'{% endifequal %}><a href="{% url groups:list %}">Groups</a></li>
    1517        <li{% ifequal pagename "fysm"     %} class='selected'{% endifequal %}><a href="{% url fysm       %}">FYSM</a></li>
     18        <li{% ifequal pagename "midway"     %} class='selected'{% endifequal %}><a href="{% url midway-list %}">Midway</a></li>
    1619        <li{% ifequal pagename "about"    %} class='selected'{% endifequal %}><a href="{% url about %}">Help &amp; About</a></li>
    1720        {% if user.is_staff %}<li><a href='{% url admin:index %}'>Admin</a></li>{% endif %}
  • asadb/template/fysm/fysm_listing.html

    rbd2bc21 r21ab6b3  
    1212<p>Hello incoming MIT students!</p>
    1313
    14 <p>Student activities play an important role in student life at MIT &mdash; they're a
     14<p>Student activities play an important role in student life at MIT &ndash; they're a
    1515great place to meet people, practice old skills and learn new ones, have fun,
    1616and give back to the community.  Undergraduate and graduate students, along
     
    5959{% for fysm in fysm_list %}
    6060<div class='single-fysm-entry'>
    61     <h3><a href='{% url fysm-view year fysm.pk %}'>{{fysm.display_name}}</a></h3>
     61    {% comment %}
     62    If we were cooler, years when we had slides would still link them here.
     63    We're not that cool, though, so we just always link the join page.
     64    {% endcomment %}
     65    <h3><a href='{% url fysm-link year "join" fysm.pk %}'>{{fysm.display_name}}</a></h3>
    6266    <div class='fysm-body'>
    6367    <div class='logo'>
     
    6973    </div>
    7074    <p class='data'>{{fysm.description}}</p>
    71     <p class='join'><a href='{% url fysm-link year "join" fysm.pk %}'>Join {{fysm.display_name}}!</a> Have questions? Send them to <a href='mailto:{{fysm.contact_email}}'>{{fysm.contact_email}}</a>!</p>
     75    <p class='join'><a href='{% url fysm-link year "join" fysm.pk %}'>Learn more about {{fysm.display_name}}!</a> Have questions? Send them to <a href='mailto:{{fysm.contact_email}}'>{{fysm.contact_email}}</a>!</p>
    7276    </div>
    7377    {% if fysm.tags %}
  • asadb/template/groups/account_lookup.html

    rb928edc r9469a73  
    1717    <th>Group</th>
    1818    <td>{{group}}</td>
     19</tr>
     20<tr>
     21    <th>Status</th>
     22    <td>{{group.group_status}}</td>
    1923</tr>
    2024<tr>
     
    3943{% endif %}
    4044
     45{% if account_number %}
     46<p>You may be able to find information about account {{account_number}} in <a href='https://rolesweb.mit.edu/cgi-bin/roleauth2.pl?category=SAP+SAP-related&amp;func_name=CAN+SPEND+OR+COMMIT+FUNDS&amp;qual_code=F{{account_number}}&amp;skip_root=Y'>Roles</a>.</p>
     47{% endif %}
     48
    4149<table class='pretty-table'>
    4250<form enctype="multipart/form-data" method="get" action="">
     
    4553</form>
    4654</table>
     55
     56<h2>Group Status</h2>
     57
     58<table class='pretty-table'>
     59<tr>
     60    <th>Status</th>
     61    <th>Description</th>
     62</tr>
     63<tr>
     64    <th>Active</th>
     65    <td>Active groups are normal ASA groups in good standing.</td>
     66</tr>
     67<tr>
     68    <th>Suspended and Derecognized</th>
     69    <td>Suspended and derecognized groups are <strong>not</strong> in good standing, and should generally not be permitted to spend money, reserve rooms, etc..</td>
     70</tr>
     71<tr>
     72    <th>Non-Group Entity</th>
     73    <td>Non-Group Entities do not necessarily have any ASA recognition, but are included in the ASA Database as a courtesy to other MIT offices. You may assume the roles listed are authoritative. However, the ASA does not grant them any privileges.</td>
     74</tr>
     75</table>
     76
    4777
    4878<h2>Available Roles</h2>
  • asadb/template/groups/create/startup_review.html

    rac93a0c r7dde669  
    1414<tr><th>Description</th><td>{{group.description}}</td></tr>
    1515
    16 <tr><th colspan='2'>Type</th></tr>
     16<tr><th colspan='2'>Officers</th></tr>
    1717<tr><th>President</th><td>{{startup.president_name}} ({{startup.president_kerberos}})</td></tr>
    1818<tr><th>Treasurer</th><td>{{startup.treasurer_name}} ({{startup.treasurer_kerberos}})</td></tr>
     
    5151{% endif %}
    5252
     53{% if warnings %}
     54<div class='messagebox warnbox'>
     55<h3>Potential issues</h3>
     56
     57<p>The following potential issues were identified with this group's startup application:</p>
     58
     59<ul>
     60{% for warning in warnings %}
     61    <li>{{warning}}</li>
     62{%endfor%}
     63</ul>
     64
     65<p>Please look over these issues. Usually, you should <strong>reach out to the group</strong> and ask them to fix (or consider fixing) each issue before you approve the group. In some cases, it may be appropriate to ignore an issue. (For example, "MIT" is a central part of some group's acronyms (like "HTGAMIT"). In these cases, confirming with the group may not be necessary.)</p>
     66</div>
     67{%endif%}
     68
    5369{% if disp_form %}
    5470<form enctype="multipart/form-data" method="post" action="">
    5571{% csrf_token %}
     72{% if warnings %}
     73<input type='submit' name='approve' value='Approve DESPITE WARNINGS' />
     74{% else %}
    5675<input type='submit' name='approve' value='Approve' />
     76{% endif %}
    5777<input type='submit' name='reject' value='Reject' />
    5878</form>
  • asadb/template/groups/diffs/new-group-announce.txt

    r264efa0 rf38b6d0  
    55to the MIT community!
    66
    7 The last step to finalize the group recognition is for you to finish entering the
    8 group's complete information into the ASA Database:
     7Your group is currently *suspended*. The last step to finalize the group
     8recognition is for you to finish entering the group's complete information into
     9the ASA Database:
    910
    1011      http://web.mit.edu/asa/www/asa-db.shtml
     
    1213Your group's entry is at:
    1314
    14       http://asa.mit.edu/{% url groups:group-detail group.pk %}
     15      https://asa.mit.edu{% url groups:group-detail group.pk %}
    1516
    1617You should make sure to do the following:
     
    2930can modify the information and that MIT personal certificates are
    3031required to access the group's account in the ASA Database.
     32
     33Once you have completed these steps, you must *notify asa-groups@mit.edu* to
     34become an active group.
    3135
    3236*** Please keep this e-mail for further reference. ***
  • asadb/template/groups/group_change_main.html

    rcb9b105 r4752b74  
    99{% include "groups/group_tools.part.html" %}
    1010
    11 <p>We're using the transition from the old ASA Database to this new one as an opportunity to verify that old information is still accurate. Thus, we have intentionally limited the amount of information we copied from the old database. While filling this out, feel free to refer to the <a href='https://sisapp2.mit.edu/asa/student_group_detail.do?action=detail&amp;studentGroupId={{group.pk}}'>old database</a>. However, please verify that information is accurate as you copy it over. As always, if you have trouble, please <a href='mailto:asa-exec@mit.edu'>contact us</a>.</p>
     11<p>Please make sure to keep the information about your group up-to-date. As always, if you have trouble (or need to update a field that you don't have access to), please <a href='mailto:asa-exec@mit.edu'>contact us</a>.</p>
    1212
    1313{% if msg %}
  • asadb/template/groups/group_change_officers.html

    rbb1a40e r21ab6b3  
    11{% extends "base.html" %}
    22
    3 {% block title %}{{group.name}}: Change people{% endblock %}
     3{% block title %}{{group.name}}: Update people{% endblock %}
    44{% block content %}
    55
    6 <h1>{{group.name}}: Change people</h1>
     6<h1>{{group.name}}: Update people</h1>
    77
    88{% include "groups/group_tools.part.html" %}
    99
    10 <p>Please adjust your signatories below.</p>
     10<p>Please adjust the people associated with your group below.</p>
    1111
    1212<h2>Available roles</h2>
     
    5252{% endif %}
    5353
    54 <h2>Option 1: View and update signatories one at a time</h2>
     54<h2>Option 1: View and update people one at a time</h2>
    5555
    5656<p>Please note:</p>
    5757<ul>
    58     <li>We don't track "group membership" &mdash; people will show up in the list below only if they have one of the roles listed</li>
    59     <li>Type only the <em>Athena username</em> to add people &mdash; do not type full names or names plus username</li>
     58    <li>We don't track "group membership" &ndash; people will show up in the list below only if they have one of the roles listed</li>
     59    <li>Type only the <em>Athena username</em> to add people &ndash; do not type full names or names plus username</li>
    6060    <li>To add <em>more than four</em> new people, add four at a time and <em>submit multiple times</em></li>
    6161</ul>
     
    6464<input type='hidden' name='opt-mode' value='table' />
    6565{% csrf_token %}
    66 <table class='pretty-table group-change-officers-change'>
     66<table class='pretty-table group-update-people-change'>
    6767<thead>
    6868<tr>
  • asadb/template/groups/group_list.html

    r8f46374 r58fe19c  
    1717
    1818<h2>The Groups</h2>
     19
     20<p>Found {{group_list|length}} groups:</p>
    1921
    2022<table class='pretty-table group-list'>
  • asadb/template/groups/reporting.html

    rea8e8b6 r58fe19c  
    1717<h2><a name='results'></a>Results</h2>
    1818{% if run_report %}
     19
     20<p>Found {{report_groups|length}} groups:</p>
     21
    1922<table class='pretty-table'>
    2023<thead>
  • asadb/template/groups/reporting/non-students.html

    r213dd57 r89165c1  
    3535<tr>
    3636    <th><a href='?sort=group'>Group</a></th>
     37    <th><a href='?sort=status'>Status</a></th>
    3738    <th><a href='?sort=role'>Role</a></th>
    3839    <th><a href='?sort=person'>Person</a></th>
     
    4142<tr>
    4243    <td><a href='{% url groups:group-detail holder.group.pk %}'>{{holder.group.name}}</a></td>
     44    <td>{{holder.group.group_status}}</td>
    4345    <td>{{holder.role.display_name}}</td>
    44     <td>{{holder.person}}</td>
     46    <td>{% if perms.groups.change_athenamoiraaccount %}<a href='{% url admin:groups_athenamoiraaccount_changelist %}?username={{holder.person}}'>{{holder.person}}</a>{% else %}{{holder.person}}{% endif %}</td>
    4547</tr>
    4648{% endfor %}
  • asadb/template/index.html

    rde2f0ac r3c1b20b  
    4444        <li><a href='{%url fysm-select%}'>Submit an entry</a></li>
    4545    </ul></li>
     46    <li><a href='{% url midway-list %}'>Midways</a>: <a href='{% url midway-map-latest %}'>latest map</a></li>
    4647    <li>Membership updates<ul>
    4748        <li><a href='{%url membership-update-cycle %}'>Group update</a></li>
     
    5758    </ul></li>
    5859    <li><a href='{% url space-summary %}'>Space assignments</a>
     60        <ul>
     61        <li><a href='{% url space-lock-type %}'>Lock Types</a></li>
    5962        {% if perms.groups.view_group_private_info %}
    60         <ul>
    6163        <li><a href='{% url space-dump-locker-access %}'>Locker access (CSV)</a></li>
    6264        <li><a href='{% url space-dump-office-access %}'>Office access (CSV)</a></li>
     65        {% endif %}
    6366        </ul>
    64         {% endif %}
    6567    </li>
    6668    <li><a href='{% url about %}'>About the ASA Database</a><ul>
  • asadb/template/membership/admin.html

    rd6f8984 r21ab6b3  
    11{% extends "base.html" %}
    22
    3 {% block title %}Membership confirmations &mdash; ASA Exec pages{% endblock %}
     3{% block title %}Membership confirmations &ndash; ASA Exec pages{% endblock %}
    44{% block content %}
    55
    6 <h2>Membership confirmations &mdash; ASA Exec pages</h2>
     6<h2>Membership confirmations &ndash; ASA Exec pages</h2>
    77
    8 <p>Exec members can check the <a href='{% url membership-issues %}'>list of groups with issues</a> (or a version <a href='{% url membership-issues %}?accounts=1'>with account numbers</a>)</p>
     8<p>Exec members can check the issues:</p>
     9
     10<table class='pretty-table'>
     11<tr>
     12    <th>Name</th>
     13    <th>Create date</th>
     14    <th>Issues</th>
     15    <th>(with accounts)</th>
     16</tr>
     17{% for cycle in cycle_list %}
     18<tr>
     19    <th>{{cycle.name}}</th>
     20    <td>{{cycle.create_date}}</td>
     21    <td><a href='{% url membership-issues cycle.slug %}'>Issues</a></td>
     22    <td><a href='{% url membership-issues cycle.slug %}?accounts=1'>(with accounts)</a></td>
     23</tr>
     24{% endfor %}
     25</table>
    926
    1027<p>The list includes the following types of issues:</p>
    1128<dl>
    12     <dt>unsubmitted</dt><dd>active groups ("group status" is "Active") who have not submitted an update (ever &mdash; see ASA-#191)</dd>
    13     <dt>confirmations</dt><dd>groups that have submitted an update (ever) but had less than five members confirm membership (again, ever &mdash; ASA-#191)</dd>
    14     <dt>50%</dt><dd>groups that (ever) submitted a confirmation with more non-students (alum, other affiliates, or other) than students (grad or undergrad)</dd>
     29    <dt>unsubmitted</dt><dd>active or suspended groups who have not submitted an update
     30    <dt>confirmations</dt><dd>groups that have submitted an update but had less than five members confirm membership</dd>
     31    <dt>50%</dt><dd>groups that submitted a confirmation with more non-students (alum, other affiliates, or other) than students (grad or undergrad)</dd>
    1532</dl>
    1633
  • asadb/template/membership/anti-hazing.txt

    r43434f6 r70a96d4  
    1515Text of Anti-Hazing Law:
    1616
    17 Section 17. Hazing; organizing or participating; hazing defined: Whoever is a principal organizer or
    18 participant in the crime of hazing, as defined herein, shall be punished by a fine of not more than three thousand
    19 dollars or by imprisonment in a house of correction for not more than one year, or both such fine and imprisonment.
     17Section 17. Hazing; organizing or participating; hazing defined: Whoever is a principal organizer or participant in the crime of hazing, as defined herein, shall be punished by a fine of not more than three thousand dollars or by imprisonment in a house of correction for not more than one year, or both such fine and imprisonment.
    2018
    21 The term hazing as used in this section and in sections eighteen and nineteen, shall mean any conduct or method of
    22 initiation into any student organization, whether on public or private property, which willfully or recklessly
    23 endangers the physical or mental health of any student or other person. Such conduct shall include whipping, beating,
    24 branding, forced calisthenics, exposure to the weather, forced consumption of any food, liquor, beverage, drug or
    25 other substance, or any other brutal treatment or forced physical activity which is likely to adversely affect the
    26 physical health or safety of any such student or other person, or which subjects such student or other person to
    27 extreme mental stress, including extended deprivation of sleep or rest or extended isolation.
     19The term hazing as used in this section and in sections eighteen and nineteen, shall mean any conduct or method of initiation into any student organization, whether on public or private property, which willfully or recklessly endangers the physical or mental health of any student or other person. Such conduct shall include whipping, beating, branding, forced calisthenics, exposure to the weather, forced consumption of any food, liquor, beverage, drug or other substance, or any other brutal treatment or forced physical activity which is likely to adversely affect the physical health or safety of any such student or other person, or which subjects such student or other person to extreme mental stress, including extended deprivation of sleep or rest or extended isolation.
    2820
    29 Notwithstanding any other provisions of this section to the contrary, consent shall not be available as a defense to
    30 any prosecution under this action.
     21Notwithstanding any other provisions of this section to the contrary, consent shall not be available as a defense to any prosecution under this action.
    3122
    32 Section 18. Failure to report hazing: Whoever knows that another person is the victim of hazing as defined in
    33 section seventeen and is at the scene of such crime shall, to the extent that such person can do so without danger
    34 or peril to himself or others, report such crime to an appropriate law enforcement official as soon as reasonably
    35 practicable. Whoever fails to report such crime shall be punished by a fine of not more than one thousand dollars.
     23Section 18. Failure to report hazing: Whoever knows that another person is the victim of hazing as defined in section seventeen and is at the scene of such crime shall, to the extent that such person can do so without danger or peril to himself or others, report such crime to an appropriate law enforcement official as soon as reasonably practicable. Whoever fails to report such crime shall be punished by a fine of not more than one thousand dollars.
    3624
    37 Section 19. Copy of Secs. 17 to 19; issuance to students and student groups, teams and organizations; report:
    38 Each institution of secondary education and each public and private institution of post secondary education shall
    39 issue to every student group, student team or student organization which is part of such institution or is
    40 recognized by the institution or permitted by the institution to use its name or facilities or is known by the
    41 institution to exist as an unaffiliated student group, student team or student organization, a copy of this section
    42 and sections seventeen and eighteen; provided, however, that an institution's compliance with this section's
    43 requirements that an institution issue copies of this section and sections seventeen and eighteen to unaffiliated
    44 student groups, teams or organizations shall not constitute evidence of the institution's recognition or endorsement
    45 of said unaffiliated student groups, teams or organizations.
     25Section 19. Copy of Secs. 17 to 19; issuance to students and student groups, teams and organizations; report: Each institution of secondary education and each public and private institution of post secondary education shall issue to every student group, student team or student organization which is part of such institution or is recognized by the institution or permitted by the institution to use its name or facilities or is known by the institution to exist as an unaffiliated student group, student team or student organization, a copy of this section and sections seventeen and eighteen; provided, however, that an institution's compliance with this section's requirements that an institution issue copies of this section and sections seventeen and eighteen to unaffiliated student groups, teams or organizations shall not constitute evidence of the institution's recognition or endorsement of said unaffiliated student groups, teams or organizations.
    4626
    47 Each such group, team or organization shall distribute a copy of this section and sections seventeen and eighteen to
    48 each of its members, plebes, pledges or applicants for membership. It shall be the duty of each such group, team or
    49 organization, acting through its designated officer, to deliver annually, to the institution [in MIT's case the
    50 Office of Student Life Programs (with exception of varsity teams and club sports, who will deliver attested
    51 acknowledgements to the Department of Athletics, Physical Education and Recreation)] an attested acknowledgement
    52 stating that such group, team or organization has received a copy of this section and said sections seventeen and
    53 eighteen, that each of its members, plebes, pledges, or applicants has received a copy of sections seventeen and
    54 eighteen, and that such group, team or organization understands and agrees to comply with the provisions of this
    55 section and sections seventeen and eighteen.
     27Each such group, team or organization shall distribute a copy of this section and sections seventeen and eighteen to each of its members, plebes, pledges or applicants for membership. It shall be the duty of each such group, team or organization, acting through its designated officer, to deliver annually, to the institution [in MIT's case the Office of Student Life Programs (with exception of varsity teams and club sports, who will deliver attested acknowledgements to the Department of Athletics, Physical Education and Recreation)] an attested acknowledgement stating that such group, team or organization has received a copy of this section and said sections seventeen and eighteen, that each of its members, plebes, pledges, or applicants has received a copy of sections seventeen and eighteen, and that such group, team or organization understands and agrees to comply with the provisions of this section and sections seventeen and eighteen.
    5628
    57 Each institution of secondary education and each public or private institution of post secondary education shall, at
    58 least annually, before or at the start of enrollment, deliver to each person who enrolls as a full time student in
    59 such institution a copy of this section and sections seventeen and eighteen.
     29Each institution of secondary education and each public or private institution of post secondary education shall, at least annually, before or at the start of enrollment, deliver to each person who enrolls as a full time student in such institution a copy of this section and sections seventeen and eighteen.
    6030
    61 Each institution of secondary education and each public or private institution of post secondary education shall
    62 file, at least annually, a report with the board of higher education and in the case of secondary institutions, the
    63 board of education, certifying that such institution has complied with its responsibility to inform student groups,
    64 teams or organizations and to notify each full time student enrolled by it of the provisions of this section and
    65 sections seventeen and eighteen and also certifying that said institution has adopted a disciplinary policy with
    66 regard to the organizers and participants of hazing, and that such policy has been set forth with appropriate
    67 emphasis in the student handbook or similar means of communicating the institution's policies to its students. The
    68 board of higher education and, in the case of secondary institutions, the board of education shall promulgate
    69 regulations governing the content and frequency of such reports, and shall forthwith report to the attorney general
    70 any such institution which fails to make such report.
     31Each institution of secondary education and each public or private institution of post secondary education shall file, at least annually, a report with the board of higher education and in the case of secondary institutions, the board of education, certifying that such institution has complied with its responsibility to inform student groups, teams or organizations and to notify each full time student enrolled by it of the provisions of this section and sections seventeen and eighteen and also certifying that said institution has adopted a disciplinary policy with regard to the organizers and participants of hazing, and that such policy has been set forth with appropriate emphasis in the student handbook or similar means of communicating the institution's policies to its students. The board of higher education and, in the case of secondary institutions, the board of education shall promulgate regulations governing the content and frequency of such reports, and shall forthwith report to the attorney general any such institution which fails to make such report.
    7132
    7233
  • asadb/template/membership/confirm.html

    r11941e1 r5560f6d  
    11{% extends "base.html" %}
     2
     3{% block extrahead %}
     4<link rel="stylesheet" href="{{MEDIA_URL}}js/libs/chosen/chosen.css">
     5{% endblock %}
    26
    37{% block title %}Membership update{% endblock %}
     
    7781</table>
    7882
    79 <h3>Option 2: Add or remove groups by searching</h3>
     83<h3>Option 2: Select all your groups at once</h3>
    8084
    81 <form action="" method="get">
    82     <table class='pretty-table'>
    83     {{ filter.form.as_table }}
    84     </table>
    85     <input type="submit" name='search' value="Search" />
    86     <input type="submit" name='stop-search' value="Stop searching" />
    87 </form>
    88 
    89 {% if show_filtered_groups %}
    90 <table class='pretty-table'>
    91 <thead>
    92     <tr>
    93         <th>Name</th>
    94         <th>Website</th>
    95         <th>ASA DB</th>
    96         <th>Description</th>
    97         <th>Add/Remove</th>
    98     </tr>
    99 </thead>
    100 <tbody>
    101 {% for group in filtered_groups %}
    102     <tr>
    103         <th>{{group.name}}</th>
    104         <td>{% if group.website_url %}<a href='{{group.website_url}}'>Website</a>{%endif%}</td>
    105         <td><a href='{% url groups:group-detail group.pk %}'>DB Entry</a></td>
    106         <td>{{group.description}}</td>
    107         <td>
    108             <form action="" method="post">
    109             {% csrf_token %}
    110             <input type="hidden" name="group" value="{{group.pk}}">
    111             {% if group in member_groups %}
    112             <input type="hidden" name="action" value="remove">
    113             <input type="submit" name="add-remove" value="Remove">
    114             {% else %}
    115             <input type="hidden" name="action" value="add">
    116             <input type="submit" name="add-remove" value="Add">
    117             {% endif %}
    118             </form>
    119         </td>
    120     </tr>
    121 {% endfor %}
    122 </tbody>
    123 </table>
    124 {% else %}
    125 <p>Hit "search" to see matching groups.</p>
    126 {% endif %}
    127 
    128 
    129 <h3>Option 3: Select all your groups at once</h3>
    130 
    131 <p>Below is a list of all recognized groups. You can go through picking out the groups you are a member of. However, you should be careful not to deselect any groups (unless you aren't a member of them, of course).</p>
     85<p>Below, you can add or remove any recognized group from the list of groups you are a member of.</p>
    13286
    13387<form enctype="multipart/form-data" method="post" action="">
     
    13993</form>
    14094
     95
     96<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script>
     97<script src="{{MEDIA_URL}}js/libs/chosen/chosen.jquery.js" type="text/javascript"></script>
     98<script type="text/javascript">
     99    $("#id_groups").chosen();
     100</script>
     101
    141102{% endblock %}
  • asadb/template/membership/thanks.html

    r1ed7e51 r21ab6b3  
    11{% extends "base.html" %}
    22
    3 {% block title %}Membership update &mdash; thanks{% endblock %}
     3{% block title %}Membership update &ndash; thanks{% endblock %}
    44{% block content %}
    55
    6 <h2>Membership update &mdash; thanks</h2>
     6<h2>Membership update &ndash; thanks</h2>
    77
    88<p>Thanks for confirming your group's activity. You should receive a confirmation email to your officer's list, and a copy of the anti-hazing law to your group list.</p>
  • asadb/template/membership/update.html

    r6e247d3 r1a5ed66  
    55
    66
    7 <h1>ASA Group Registration &amp; Anti-Hazing Form</h1>
     7<h1>ASA Group Registration &amp; Anti-Hazing Form for <a href='{%url groups:group-detail group.pk%}'>{{group.name}}</a></h1>
    88
    99<p>The purpose of this electronic registration is for the ASA to determine which groups are still active and which are not. There are four components to this process:</p>
     
    3535<h3><a name='components-membership'></a>3. Providing Membership Information</h3>
    3636
    37 <p>We are requiring that groups provide membership information &mdash; both membership numbers and a membership list. This information is important for our understanding of your group and ensuring that groups are meeting our membership standards. We will also share the membership numbers with the funding boards (GSC, UA, and LEF/ARCADE) for groups that apply to those sources. <strong>It is very important you are honest when answering these questions</strong>, even if you are concerned your membership does not meet our standards. We will provide time and support for groups to improve their student participation as needed &mdash; both in terms of number and proportion of students. Groups that are shown to be dishonest in this process, particularly with regards to meeting our standards, <strong>may have funding and space eligibility revoked or be suspended or derecognized</strong>.</p>
     37<p>We are requiring that groups provide membership information &ndash; both membership definition and the corresponding numbers. This information is important for our understanding of your group and ensuring that groups are meeting our membership standards. We will also share the membership numbers with the funding boards (GSC, UA, and LEF/ARCADE) for groups that apply to those sources. <strong>It is very important you are honest when answering these questions</strong>, even if you are concerned your membership does not meet our standards. We will provide time and support for groups to improve their student participation as needed &ndash; both in terms of number and proportion of students. Groups that are shown to be dishonest in this process, particularly with regards to meeting our standards, <strong>may have funding and space eligibility revoked or be suspended or derecognized</strong>.</p>
    3838
    3939<dl>
    40 <dt>Minimum definition of member</dt><dd><p>For the purposes of this form, the minimum definition of membership you should use is people you would expect to confirm membership in your group (not that they all have to) and people that you consider to be regular or active members of your group. (You may be more stringent, based on how your group views or defines membership internally.) You must state the specific definition of membership you are using below. This does not need to be a membership definition that is in your constitution.</p></dd>
     40<dt>Minimum definition of member</dt><dd><p>For the purposes of this form, the minimum definition of membership you use should include only people who (a) you would expect to confirm membership in your group (not that they all have to) if asked and (b) you consider to be regular or active members of your group. Members should satisfy both (a) and (b); you should not count members who satisfy only one or the other. (You may be more stringent, based on how your group views or defines membership internally.) You must state the specific definition of membership you are using below. This does not need to be a membership definition that is in your constitution.</p></dd>
    4141
    42 <dt>Privacy</dt><dd><p>The list of members you provide will not be shared. If we need to contact them we will contact your officer/exec list first &mdash; this would only be if we have concerns about the information provided or to spot-check the information provided. If feasible, we will ask your officer/exec list to contact your members directly.</p></dd>
    43 
    44 <dt>Recruitment</dt><dd><p>If you are still in the midst of or leading up to your major recruitment period, we encourage you to submit current numbers and a list soon to ensure you have something submitted before the deadline. Then you can submit an updated form later, either before or after the deadline. You don't need to do this if your membership changes slightly, but we recommend you do so if your numbers change significantly.</p></dd>
     42<dt>Recruitment</dt><dd><p>If you are still in the midst of or leading up to your major recruitment period, we encourage you to submit current numbers soon to ensure you have something submitted before the deadline. Then you can submit an updated form later, either before or after the deadline. You don't need to do this if your membership changes slightly, but we recommend you do so if your numbers change significantly.</p></dd>
    4543
    4644</dl>
     
    4846<h3><a name='components-confirm'></a>4. Membership Confirmation</h3>
    4947
    50 <p>In addition to the membership info provided by you above, please have at least five student members confirm membership using the link below. You do not need to have everyone do this; we are simply looking for <em>five students'</em> confirmation. You can <a href='{% url membership-submitted %}'>see how many have confirmed already</a>.</p>
     48<p>In addition to the membership info provided by you above, please have at least five student members confirm membership using the link below. You do not need to have everyone do this; we are simply looking for <em>five students'</em> confirmation. Once you submit this form, you will be able to <a href='{% url membership-submitted %}'>see how many have confirmed already</a>.</p>
    5149
    5250<p>Link to provide to members: <a href='{{confirm_uri}}'>{{confirm_uri}}</a>.</p>
  • asadb/template/space/group-change-email.txt

    rd768e47 r72a3d90  
    11{% autoescape off %}Hi {{group.name}},
    2     Thank you for updating space access on the ASA database today. We've forwarded the following changes on to CAC:
     2
     3Thank you for updating space access on the ASA database today. We are forwarding these changes to CAC, which generally updates access within one week. They will reply when the update has been done; if you don't get a reply, the message may have been lost and you should reply-all to this email reminding them.
     4
     5We believe you made the following changes:
    36
    47{% if office_msg %}
  • asadb/template/space/manage-access.html

    r78de8cb r1927d79  
    11{% extends "base.html" %}
    22
    3 {% block title %}{{group.name}}: Office Access{% endblock %}
     3{% block title %}{{group.name}}: Space Access{% endblock %}
    44{% block content %}
    55
    6 <h1>{{group.name}}: Office Access</h1>
     6<h1>{{group.name}}: Space Access</h1>
    77
    88{% include "groups/group_tools.part.html" %}
     
    4848    <th>{{assignment.space}}</th>
    4949    <td>
     50        {% with assignment.space.lock_type as lock_type %}
     51        {% if lock_type.db_update == "none" %}
     52        <p><strong>Warning: Access to this office is not managed through the ASA DB.</strong></p>
     53        <p><em><a href='{% url space-lock-type %}'>{{lock_type.name}}</a></em>: {{lock_type.description}}{% if lock_type.info_url %} <a href='{{lock_type.info_url}}'>Details.</a>{%endif%}</p>
     54        <p>Contact <a href='mailto:{{lock_type.info_addr}}'>{{lock_type.info_addr}}</a> for more information.</p>
     55
     56        {% else %}
     57
     58        <p>We recommend managing access on the <a href='{% url groups:group-manage-officers group.id %}'>update people</a> page if possible. You should only need to use this page if:</p>
     59        <ul>
     60            <li>You need to grant access to somebody who does not have an Athena account, or</li>
     61            <li>Your group has several offices, and somebody needs access to one or more of the offices, but should not have access to all of them</li>
     62        </ul>
     63
    5064        <table class='pretty-table'>
    5165            <tr>
     
    6781            </tr>{% endfor %}
    6882        </table>
     83
     84        {% endif %}
     85        {% endwith %}
     86
    6987    </td>
    7088</tr>
  • asadb/template/space/summary.html

    rde2f0ac rf6982d4  
    2727    <th>Room</th>
    2828    <th>Group</th>
     29    <th><a href='{% url space-lock-type %}'>Lock Type</a></th>
    2930    <th>Access</th>
    3031</tr>
     
    3334    <td>{{office.space.number}}</td>
    3435    <td><a href='{% url groups:group-detail office.group.pk %}'>{{office.group}}</a></td>
     36    <td>{{office.space.lock_type.name}}</td>
    3537    <td><a href='{% url groups:group-space-access office.group.pk %}'>Access</a></td>
    3638</tr>
  • asadb/urls.py

    rafc5348 r88a856b  
    6565    ),
    6666    url(r'^membership/submitted/$', forms.views.View_GroupMembershipList.as_view(), name='membership-submitted', ),
    67     url(
    68         r'^membership/admin/$',
    69         'django.views.generic.simple.direct_to_template',
    70         {'template': 'membership/admin.html', 'extra_context': { 'pagename':'groups' }, },
    71         name='membership-admin',
    72     ),
    73     url(r'^membership/admin/issues.csv$', forms.views.group_confirmation_issues, name='membership-issues', ),
     67    url(r'^membership/admin/$', forms.views.View_GroupConfirmationCyclesList.as_view(), name='membership-admin', ),
     68    url(r'^membership/admin/issues/(?P<slug>[\w-]+).csv$', forms.views.group_confirmation_issues, name='membership-issues', ),
     69    url(r'^membership/people-lookup/((?P<pk>\d+)/)?$', forms.views.people_status_lookup, name='membership-people-lookup', ),
     70
     71    # Midway
     72    url(r'^midway/$', forms.views.View_Midways.as_view(), name='midway-list', ),
     73    url(r'^midway/latest/$', forms.views.midway_map_latest, name='midway-map-latest', ),
     74    url(r'^midway/(?P<slug>[\w-]+)/$', forms.views.MidwayMapView.as_view(), name='midway-map', ),
     75    url(r'^midway/(?P<slug>[\w-]+)/assign/$', forms.views.midway_assignment_upload, name='midway-assign', ),
    7476
    7577    # Group list
     
    8082    url(r'^space/dump/office-access.csv$', space.views.dump_office_access, name='space-dump-office-access', ),
    8183    url(r'^space/$', space.views.summary, name='space-summary', ),
     84    url(r'^space/lock_types.html$', space.views.lock_types, name='space-lock-type', ),
    8285
    8386    # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
  • asadb/util/diff_static_data.sh

    rc060d8c r3c1b20b  
    55date
    66
    7 cd static-data
     7cd "$(dirname "$0")/static-data"
    88
    99../dump_group_perms.py > group-perms.py
     
    1212../../manage.py dumpdata --format=xml --indent=4 groups.ActivityCategory > groups_initial_data.xml
    1313../../manage.py dumpdata --format=xml --indent=4 forms.FYSMCategory > forms_initial_data.xml
    14 git add {groups,forms}_initial_data.xml
     14../../manage.py dumpdata --format=xml --indent=4 space.LockType --format=xml --indent=4 > space_lock_types.xml
     15git add {groups,forms}_initial_data.xml space_lock_types.xml
    1516
    1617echo
Note: See TracChangeset for help on using the changeset viewer.