Commit a5867da9 by Will Daly

Add new models to embargo to support country access

Add Django admin UI for configuring country access

Migrate existing embargo rules into the new tables.
parent 0a0d306b
......@@ -5,8 +5,14 @@ from django.contrib import admin
import textwrap
from config_models.admin import ConfigurationModelAdmin
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
from embargo.forms import EmbargoedCourseForm, EmbargoedStateForm, IPFilterForm
from embargo.models import (
EmbargoedCourse, EmbargoedState, IPFilter,
CountryAccessRule, RestrictedCourse
)
from embargo.forms import (
EmbargoedCourseForm, EmbargoedStateForm, IPFilterForm,
RestrictedCourseForm
)
class EmbargoedCourseAdmin(admin.ModelAdmin):
......@@ -59,6 +65,23 @@ class IPFilterAdmin(ConfigurationModelAdmin):
}),
)
class CountryAccessRuleInline(admin.StackedInline):
"""Inline editor for country access rules. """
model = CountryAccessRule
extra = 1
def has_delete_permission(self, request, obj=None):
return True
class RestrictedCourseAdmin(admin.ModelAdmin):
"""Admin for configuring course restrictions. """
inlines = [CountryAccessRuleInline]
form = RestrictedCourseForm
admin.site.register(EmbargoedCourse, EmbargoedCourseAdmin)
admin.site.register(EmbargoedState, EmbargoedStateAdmin)
admin.site.register(IPFilter, IPFilterAdmin)
admin.site.register(RestrictedCourse, RestrictedCourseAdmin)
......@@ -3,48 +3,82 @@ Defines forms for providing validation of embargo admin details.
"""
from django import forms
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
from embargo.fixtures.country_codes import COUNTRY_CODES
from django.utils.translation import ugettext as _
import ipaddr
from xmodule.modulestore.django import modulestore
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from embargo.models import (
EmbargoedCourse, EmbargoedState, IPFilter,
RestrictedCourse
)
from embargo.fixtures.country_codes import COUNTRY_CODES
class EmbargoedCourseForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form providing validation of entered Course IDs."""
class Meta: # pylint: disable=missing-docstring
model = EmbargoedCourse
class CourseKeyValidationForm(forms.ModelForm):
"""Base class for validating the "course_key" (or "course_id") field.
The default behavior in Django admin is to:
* Save course keys for courses that do not exist.
* Return a 500 response if the course key format is invalid.
Using this form ensures that we display a user-friendly
error message instead.
"""
def clean_course_id(self):
"""Validate the course id"""
"""Clean the 'course_id' field in the form. """
return self._clean_course_key("course_id")
def clean_course_key(self):
"""Clean the 'course_key' field in the form. """
return self._clean_course_key("course_key")
def _clean_course_key(self, field_name):
"""Validate the course key.
Checks that the key format is valid and that
the course exists. If not, displays an error message.
Arguments:
field_name (str): The name of the field to validate.
Returns:
CourseKey
"""
cleaned_id = self.cleaned_data[field_name]
error_msg = _('COURSE NOT FOUND. Please check that the course ID is valid.')
cleaned_id = self.cleaned_data["course_id"]
try:
course_key = CourseKey.from_string(cleaned_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(cleaned_id)
except InvalidKeyError:
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(cleaned_id)
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
raise forms.ValidationError(error_msg)
if not modulestore().has_course(course_key):
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(course_key.to_deprecated_string())
msg += 'Please recheck that you have supplied a valid course id.'
raise forms.ValidationError(msg)
raise forms.ValidationError(error_msg)
return course_key
class EmbargoedCourseForm(CourseKeyValidationForm):
"""Validate course keys for the EmbargoedCourse model. """
class Meta: # pylint: disable=missing-docstring
model = EmbargoedCourse
class RestrictedCourseForm(CourseKeyValidationForm):
"""Validate course keys for the RestirctedCourse model. """
class Meta: # pylint: disable=missing-docstring
model = RestrictedCourse
class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form validating entry of states to embargo"""
......
"""Define messages for restricted courses.
These messages are displayed to users when they are blocked
from either enrolling in or accessing a course.
"""
from collections import namedtuple
BlockedMessage = namedtuple('BlockedMessage', [
# A user-facing description of the message
'description',
])
ENROLL_MESSAGES = {
'default': BlockedMessage(
description='Default',
),
}
ACCESS_MESSAGES = {
'default': BlockedMessage(
description='Default',
)
}
# -*- coding: 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):
# Adding model 'Country'
db.create_table('embargo_country', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('country', self.gf('django_countries.fields.CountryField')(unique=True, max_length=2, db_index=True)),
))
db.send_create_signal('embargo', ['Country'])
# Adding model 'RestrictedCourse'
db.create_table('embargo_restrictedcourse', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_key', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=255, db_index=True)),
('enroll_msg_key', self.gf('django.db.models.fields.CharField')(default='default', max_length=255)),
('access_msg_key', self.gf('django.db.models.fields.CharField')(default='default', max_length=255)),
))
db.send_create_signal('embargo', ['RestrictedCourse'])
# Adding model 'CountryAccessRule'
db.create_table('embargo_countryaccessrule', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('rule_type', self.gf('django.db.models.fields.CharField')(default='blacklist', max_length=255)),
('restricted_course', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['embargo.RestrictedCourse'])),
('country', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['embargo.Country'])),
))
db.send_create_signal('embargo', ['CountryAccessRule'])
# Adding unique constraint on 'CountryAccessRule', fields ['restricted_course', 'country']
db.create_unique('embargo_countryaccessrule', ['restricted_course_id', 'country_id'])
# Changing field 'EmbargoedCourse.course_id'
db.alter_column('embargo_embargoedcourse', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=255))
def backwards(self, orm):
# Removing unique constraint on 'CountryAccessRule', fields ['restricted_course', 'country']
db.delete_unique('embargo_countryaccessrule', ['restricted_course_id', 'country_id'])
# Deleting model 'Country'
db.delete_table('embargo_country')
# Deleting model 'RestrictedCourse'
db.delete_table('embargo_restrictedcourse')
# Deleting model 'CountryAccessRule'
db.delete_table('embargo_countryaccessrule')
# Changing field 'EmbargoedCourse.course_id'
db.alter_column('embargo_embargoedcourse', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255, unique=True))
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']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", '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']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'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']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", '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'})
},
'embargo.country': {
'Meta': {'ordering': "['country']", 'object_name': 'Country'},
'country': ('django_countries.fields.CountryField', [], {'unique': 'True', 'max_length': '2', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.countryaccessrule': {
'Meta': {'unique_together': "(('restricted_course', 'country'),)", 'object_name': 'CountryAccessRule'},
'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['embargo.Country']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'restricted_course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['embargo.RestrictedCourse']"}),
'rule_type': ('django.db.models.fields.CharField', [], {'default': "'blacklist'", 'max_length': '255'})
},
'embargo.embargoedcourse': {
'Meta': {'object_name': 'EmbargoedCourse'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'embargoed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.embargoedstate': {
'Meta': {'object_name': 'EmbargoedState'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'embargoed_countries': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.ipfilter': {
'Meta': {'object_name': 'IPFilter'},
'blacklist': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'whitelist': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'embargo.restrictedcourse': {
'Meta': {'object_name': 'RestrictedCourse'},
'access_msg_key': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '255'}),
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'enroll_msg_key': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}
complete_apps = ['embargo']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from django_countries import countries
class Migration(DataMigration):
def forwards(self, orm):
"""Populate the available countries with all 2-character ISO country codes. """
for country_code, __ in list(countries):
orm.Country.objects.get_or_create(country=country_code)
def backwards(self, orm):
"""Clear all available countries. """
orm.Country.objects.all().delete()
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']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", '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']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'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']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", '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'})
},
'embargo.country': {
'Meta': {'object_name': 'Country'},
'country': ('django_countries.fields.CountryField', [], {'unique': 'True', 'max_length': '2', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.countryaccessrule': {
'Meta': {'unique_together': "(('restricted_course', 'rule_type'),)", 'object_name': 'CountryAccessRule'},
'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['embargo.Country']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'restricted_course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['embargo.RestrictedCourse']"}),
'rule_type': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'embargo.embargoedcourse': {
'Meta': {'object_name': 'EmbargoedCourse'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'embargoed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.embargoedstate': {
'Meta': {'object_name': 'EmbargoedState'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'embargoed_countries': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.ipfilter': {
'Meta': {'object_name': 'IPFilter'},
'blacklist': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'whitelist': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'embargo.restrictedcourse': {
'Meta': {'object_name': 'RestrictedCourse'},
'access_msg_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'enroll_msg_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}
complete_apps = ['embargo']
symmetrical = True
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
def forwards(self, orm):
"""Move the current course embargo configuration to the new models. """
for old_course in orm.EmbargoedCourse.objects.all():
new_course, __ = orm.RestrictedCourse.objects.get_or_create(course_key=old_course.course_id)
for country in self._embargoed_countries_list(orm):
country_model = orm.Country.objects.get(country=country)
orm.CountryAccessRule.objects.get_or_create(
country=country_model,
rule_type='blacklist',
restricted_course=new_course
)
def backwards(self, orm):
"""No backwards migration required since the forward migration is idempotent. """
pass
def _embargoed_countries_list(self, orm):
"""Retrieve the list of embargoed countries from the existing tables. """
# We need to replicate some application logic here, because South
# doesn't give us access to class methods on the Django model objects.
try:
current_config = orm.EmbargoedState.objects.order_by('-change_date')[0]
return [
country.strip().upper() for country
in current_config.embargoed_countries.split(',')
]
except IndexError:
return []
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']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", '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']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'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']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", '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'})
},
'embargo.country': {
'Meta': {'ordering': "['country']", 'object_name': 'Country'},
'country': ('django_countries.fields.CountryField', [], {'unique': 'True', 'max_length': '2', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.countryaccessrule': {
'Meta': {'unique_together': "(('restricted_course', 'country'),)", 'object_name': 'CountryAccessRule'},
'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['embargo.Country']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'restricted_course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['embargo.RestrictedCourse']"}),
'rule_type': ('django.db.models.fields.CharField', [], {'default': "'blacklist'", 'max_length': '255'})
},
'embargo.embargoedcourse': {
'Meta': {'object_name': 'EmbargoedCourse'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'embargoed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.embargoedstate': {
'Meta': {'object_name': 'EmbargoedState'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'embargoed_countries': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'embargo.ipfilter': {
'Meta': {'object_name': 'IPFilter'},
'blacklist': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'whitelist': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'embargo.restrictedcourse': {
'Meta': {'object_name': 'RestrictedCourse'},
'access_msg_key': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '255'}),
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
'enroll_msg_key': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
}
}
complete_apps = ['embargo']
symmetrical = True
......@@ -14,10 +14,15 @@ file and check it in at the same time as your model changes. To do that,
import ipaddr
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from django_countries.fields import CountryField
from config_models.models import ConfigurationModel
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
from embargo.messages import ENROLL_MESSAGES, ACCESS_MESSAGES
class EmbargoedCourse(models.Model):
"""
......@@ -72,6 +77,146 @@ class EmbargoedState(ConfigurationModel):
return [country.strip().upper() for country in self.embargoed_countries.split(',')] # pylint: disable=no-member
class RestrictedCourse(models.Model):
"""Course with access restrictions.
Restricted courses can block users at two points:
1) When enrolling in a course.
2) When attempting to access a course the user is already enrolled in.
The second case can occur when new restrictions
are put into place; for example, when new countries
are embargoed.
Restricted courses can be configured to display
messages to users when they are blocked.
These displayed on pages served by the embargo app.
"""
ENROLL_MSG_KEY_CHOICES = tuple([
(msg_key, msg.description)
for msg_key, msg in ENROLL_MESSAGES.iteritems()
])
ACCESS_MSG_KEY_CHOICES = tuple([
(msg_key, msg.description)
for msg_key, msg in ACCESS_MESSAGES.iteritems()
])
course_key = CourseKeyField(
max_length=255, db_index=True, unique=True,
help_text=ugettext_lazy(u"The course key for the restricted course.")
)
enroll_msg_key = models.CharField(
max_length=255,
choices=ENROLL_MSG_KEY_CHOICES,
default='default',
help_text=ugettext_lazy(u"The message to show when a user is blocked from enrollment.")
)
access_msg_key = models.CharField(
max_length=255,
choices=ACCESS_MSG_KEY_CHOICES,
default='default',
help_text=ugettext_lazy(u"The message to show when a user is blocked from accessing a course.")
)
def __unicode__(self):
return unicode(self.course_key)
class Country(models.Model):
"""Representation of a country.
This is used to define country-based access rules.
There is a data migration that creates entries for
each country code.
"""
country = CountryField(
db_index=True, unique=True,
help_text=ugettext_lazy(u"Two character ISO country code.")
)
def __unicode__(self):
return u"{name} ({code})".format(
name=unicode(self.country.name),
code=unicode(self.country)
)
class Meta:
# Default ordering is ascending by country code
ordering = ['country']
class CountryAccessRule(models.Model):
"""Course access rule based on the user's country.
The rule applies to a particular course-country pair.
Countries can either be whitelisted or blacklisted,
but not both.
To determine whether a user has access to a course
based on the user's country:
1) Retrieve the list of whitelisted countries for the course.
(If there aren't any, then include every possible country.)
2) From the initial list, remove all blacklisted countries
for the course.
"""
RULE_TYPE_CHOICES = (
('whitelist', 'Whitelist (allow only these countries)'),
('blacklist', 'Blacklist (block these countries)'),
)
rule_type = models.CharField(
max_length=255,
choices=RULE_TYPE_CHOICES,
default='blacklist',
help_text=ugettext_lazy(
u"Whether to include or exclude the given course. "
u"If whitelist countries are specified, then ONLY users from whitelisted countries "
u"will be able to access the course. If blacklist countries are specified, then "
u"users from blacklisted countries will NOT be able to access the course."
)
)
restricted_course = models.ForeignKey(
"RestrictedCourse",
help_text=ugettext_lazy(u"The course to which this rule applies.")
)
country = models.ForeignKey(
"Country",
help_text=ugettext_lazy(u"The country to which this rule applies.")
)
def __unicode__(self):
if self.rule_type == 'whitelist':
return _(u"Whitelist {country} for {course}").format(
course=unicode(self.restricted_course.course_key),
country=unicode(self.country),
)
elif self.rule_type == 'blacklist':
return _(u"Blacklist {country} for {course}").format(
course=unicode(self.restricted_course.course_key),
country=unicode(self.country),
)
class Meta:
unique_together = (
# This restriction ensures that a country is on
# either the whitelist or the blacklist, but
# not both (for a particular course).
("restricted_course", "country")
)
class IPFilter(ConfigurationModel):
"""
Register specific IP addresses to explicitly block or unblock.
......
......@@ -70,9 +70,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
self.assertFalse(form.is_valid())
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(bad_id)
msg += 'Please recheck that you have supplied a valid course id.'
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
self.assertIn(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save()
......@@ -87,9 +85,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
self.assertFalse(form.is_valid())
msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(bad_id)
msg += 'Please recheck that you have supplied a valid course id.'
self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
self.assertIn(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment