From 67bfbc6a37c4657a988975810d6eb9b8c48d83c7 Mon Sep 17 00:00:00 2001 From: gfawcett Date: Fri, 16 Jul 2010 17:38:23 +0000 Subject: [PATCH] Access-control model changes. Simplistic site-permisisons screen. Schema changes. The site-permissions screen is unfinished; need ways to add individuals and external groups. Right now it's mainly informational. I've taken out the passkey (invitation-code) system for now; it could easily be reimplemented in terms of a Group with a 'passkey:NNN' external ID. git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@927 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- .../0003_auto__del_field_site_passkey.py | 170 +++++++++++++++++++++ conifer/syrup/models.py | 24 +-- conifer/syrup/views/sites.py | 58 ++----- conifer/templates/edit_site_permissions.xhtml | 104 ++++++------- 4 files changed, 244 insertions(+), 112 deletions(-) create mode 100644 conifer/syrup/migrations/0003_auto__del_field_site_passkey.py diff --git a/conifer/syrup/migrations/0003_auto__del_field_site_passkey.py b/conifer/syrup/migrations/0003_auto__del_field_site_passkey.py new file mode 100644 index 0000000..806b87f --- /dev/null +++ b/conifer/syrup/migrations/0003_auto__del_field_site_passkey.py @@ -0,0 +1,170 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting field 'Site.passkey' + db.delete_column('syrup_site', 'passkey') + + + def backwards(self, orm): + + # Adding field 'Site.passkey' + db.add_column('syrup_site', 'passkey', self.gf('django.db.models.fields.CharField')(blank=True, max_length=256, null=True, db_index=True), keep_default=False) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'syrup.config': { + 'Meta': {'object_name': 'Config'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '8192'}) + }, + 'syrup.course': { + 'Meta': {'object_name': 'Course'}, + 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Department']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}) + }, + 'syrup.department': { + 'Meta': {'object_name': 'Department'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'service_desk': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.ServiceDesk']"}) + }, + 'syrup.group': { + 'Meta': {'object_name': 'Group'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'external_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '2048', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Site']"}) + }, + 'syrup.item': { + 'Meta': {'object_name': 'Item'}, + 'author': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8192', 'null': 'True', 'blank': 'True'}), + 'bib_id': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'fileobj': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'fileobj_mimetype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item_type': ('django.db.models.fields.CharField', [], {'max_length': '7'}), + 'itemtype': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1', 'null': 'True', 'blank': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'marcxml': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'parent_heading': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Item']", 'null': 'True', 'blank': 'True'}), + 'published': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '8192', 'null': 'True', 'blank': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Site']"}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '8192', 'db_index': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + 'syrup.membership': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'Membership'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'role': ('django.db.models.fields.CharField', [], {'default': "'STUDT'", 'max_length': '6'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'syrup.servicedesk': { + 'Meta': {'object_name': 'ServiceDesk'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'external_id': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}) + }, + 'syrup.site': { + 'Meta': {'unique_together': "(('course', 'term', 'owner'),)", 'object_name': 'Site'}, + 'access': ('django.db.models.fields.CharField', [], {'default': "'LOGIN'", 'max_length': '5'}), + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Course']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'service_desk': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.ServiceDesk']"}), + 'term': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Term']"}) + }, + 'syrup.term': { + 'Meta': {'object_name': 'Term'}, + 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'finish': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'start': ('django.db.models.fields.DateField', [], {}) + }, + 'syrup.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ils_userid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'last_email_notice': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'blank': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'wants_email_notices': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}) + }, + 'syrup.z3950target': { + 'Meta': {'object_name': 'Z3950Target'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'database': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'host': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'port': ('django.db.models.fields.IntegerField', [], {'default': '210'}), + 'syntax': ('django.db.models.fields.CharField', [], {'default': "'USMARC'", 'max_length': '10'}) + } + } + + complete_apps = ['syrup'] diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py index afdffe0..ec75f1c 100644 --- a/conifer/syrup/models.py +++ b/conifer/syrup/models.py @@ -48,7 +48,7 @@ class UserExtensionMixin(object): bool(callhook('can_create_sites', self)) def get_list_name(self): - return '%s, %s (%s)' % (self.last_name, self.first_name, self.username) + return '%s, %s' % (self.last_name, self.first_name) @classmethod def active_instructors(cls): @@ -164,19 +164,23 @@ class Site(BaseModel): choices = [ ('ANON', _('World-accessible')), ('LOGIN', _('Accessible to all logged-in users')), - ('STUDT', _('Accessible to course students (by section)')), - ('INVIT', _('Accessible to course students (by invitation code)')), + ('MEMBR', _('Accessible to course-site members')), ('CLOSE', _('Accessible only to course-site owners'))]) - # For sites that use a passkey as an invitation (INVIT access). - # Note: only set this value using 'generate_new_passkey'. - # TODO: for postgres, add UNIQUE constraint on 'passkey'. - passkey = m.CharField(db_index=True, blank=True, null=True, max_length=256) - class Meta: unique_together = (('course', 'term', 'owner')) ordering = ['-term__start', 'course__code'] + def save(self, *args, **kwargs): + # Ensure there is always an internal Group. + super(Site, self).save(*args, **kwargs) + internal, just_created = Group.objects.get_or_create( + site=self, external_id=None) + # ..and that the owner is an instructor in the site. + Membership.objects.get_or_create(group = internal, + user = self.owner, + defaults = {'role':'INSTR'}) + def __unicode__(self): return u'%s: %s (%s, %s)' % ( self.course.code, self.course.name, @@ -184,7 +188,8 @@ class Site(BaseModel): self.term.name) def list_display(self): - return '%s [%s, %s]' % (self.course.name, self.course.code, self.term.name) + return '%s [%s, %s]' % (self.course.name, self.course.code, + self.term.name) def items(self): return self.item_set.all() @@ -293,7 +298,6 @@ class Site(BaseModel): or user.is_staff \ or self.is_member(user) - #-------------------------------------------------- @classmethod diff --git a/conifer/syrup/views/sites.py b/conifer/syrup/views/sites.py index ffc95b0..a29ec8a 100644 --- a/conifer/syrup/views/sites.py +++ b/conifer/syrup/views/sites.py @@ -7,7 +7,7 @@ from search import * class NewSiteForm(ModelForm): class Meta: model = models.Site - exclude = ('passkey','access') + exclude = ('access',) def clean_code(self): v = (self.cleaned_data.get('code') or '').strip() @@ -53,9 +53,6 @@ def _add_or_edit_site(request, instance=None): else: form.save() site = form.instance - if site.access == u'INVIT' and not site.passkey: - site.generate_new_passkey() - site.save() assert site.id if is_add or (current_access_level != site.access): @@ -70,31 +67,20 @@ def edit_site_permissions(request, site_id): # choices: make the access-choice labels more personalized than # the ones in 'models'. choices = [ - # note: I'm leaving ANON out for now, until we discuss it further. - (u'ANON', _(u'Anyone on the planet may access this site.')), - (u'CLOSE', _(u'No students: this site is closed.')), - (u'STUDT', _(u'Students in my course -- I will provide section numbers')), - (u'INVIT', _(u'Students in my course -- I will share an Invitation Code with them')), - (u'LOGIN', _(u'All Reserves patrons'))] - # TODO: fixme, campus module no longer exists. - if models.campus.sections_tuple_delimiter is None: - # no course-sections support? Then STUDT cannot be an option. - del choices[1] - choose_access = django.forms.Select(choices=choices) + (u'ANON', _(u'Everyone: no login required.')), + (u'LOGIN', _(u'Members and non-members: login required.')), + (u'MEMBR', _(u'Members only.')), + (u'CLOSE', _(u'Instructors only: this site is closed.')), + ] + + choose_access = django.forms.RadioSelect(choices=choices) if request.method != 'POST': return g.render('edit_site_permissions.xhtml', **locals()) else: POST = request.POST - if 'action_change_code' in POST: - # update invitation code ------------------------------------- - site.generate_new_passkey() - site.access = u'INVIT' - site.save() - return HttpResponseRedirect('.#student_access') - - elif 'action_save_instructor' in POST: + if 'action_save_instructor' in POST: # update instructor details ---------------------------------- iname = POST.get('new_instructor_name','').strip() irole = POST.get('new_instructor_role') @@ -181,31 +167,7 @@ def delete_site(request, site_id): @login_required # must be, to avoid/audit brute force attacks. def site_invitation(request): - if request.method != 'POST': - return g.render('site_invitation.xhtml', code='', error='', - **locals()) - else: - code = request.POST.get('code', '').strip() - # todo, a pluggable passkey implementation would normalize the code here. - if not code: - return HttpResponseRedirect('.') - try: - # note, we only allow the passkey if access='INVIT'. - crs = models.Site.objects.filter(access='INVIT').get(passkey=code) - except models.Site.DoesNotExist: - # todo, do we need a formal logging system? Or a table for - # invitation failures? They should be captured somehow, I - # think. Should we temporarily disable accounts after - # multiple failures? - error = _('The code you provided is not valid.') - return g.render('site_invitation.xhtml', **locals()) - - # the passkey is good; add the user if not already a member. - if not models.Membership.objects.filter(user=request.user, site=crs): - mbr = models.Membership.objects.create(user=request.user, site=crs, - role='STUDT') - mbr.save() - return HttpResponseRedirect(crs.site_url()) + raise NotImplementedError #----------------------------------------------------------------------------- # Site-instance handlers diff --git a/conifer/templates/edit_site_permissions.xhtml b/conifer/templates/edit_site_permissions.xhtml index d6d0987..6090d27 100644 --- a/conifer/templates/edit_site_permissions.xhtml +++ b/conifer/templates/edit_site_permissions.xhtml @@ -1,6 +1,8 @@ ${title}