Commit 19f676a8 by Will Daly

Merge pull request #6845 from edx/will/country-access-models

Add new models to embargo to support country access
parents 3570e474 a5867da9
...@@ -5,8 +5,14 @@ from django.contrib import admin ...@@ -5,8 +5,14 @@ from django.contrib import admin
import textwrap import textwrap
from config_models.admin import ConfigurationModelAdmin from config_models.admin import ConfigurationModelAdmin
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter from embargo.models import (
from embargo.forms import EmbargoedCourseForm, EmbargoedStateForm, IPFilterForm EmbargoedCourse, EmbargoedState, IPFilter,
CountryAccessRule, RestrictedCourse
)
from embargo.forms import (
EmbargoedCourseForm, EmbargoedStateForm, IPFilterForm,
RestrictedCourseForm
)
class EmbargoedCourseAdmin(admin.ModelAdmin): class EmbargoedCourseAdmin(admin.ModelAdmin):
...@@ -59,6 +65,23 @@ class IPFilterAdmin(ConfigurationModelAdmin): ...@@ -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(EmbargoedCourse, EmbargoedCourseAdmin)
admin.site.register(EmbargoedState, EmbargoedStateAdmin) admin.site.register(EmbargoedState, EmbargoedStateAdmin)
admin.site.register(IPFilter, IPFilterAdmin) admin.site.register(IPFilter, IPFilterAdmin)
admin.site.register(RestrictedCourse, RestrictedCourseAdmin)
...@@ -3,48 +3,82 @@ Defines forms for providing validation of embargo admin details. ...@@ -3,48 +3,82 @@ Defines forms for providing validation of embargo admin details.
""" """
from django import forms from django import forms
from django.utils.translation import ugettext as _
from embargo.models import EmbargoedCourse, EmbargoedState, IPFilter
from embargo.fixtures.country_codes import COUNTRY_CODES
import ipaddr import ipaddr
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey 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 class CourseKeyValidationForm(forms.ModelForm):
model = EmbargoedCourse """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): 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: try:
course_key = CourseKey.from_string(cleaned_id) course_key = CourseKey.from_string(cleaned_id)
except InvalidKeyError: except InvalidKeyError:
try: raise forms.ValidationError(error_msg)
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)
if not modulestore().has_course(course_key): if not modulestore().has_course(course_key):
msg = 'COURSE NOT FOUND' raise forms.ValidationError(error_msg)
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)
return course_key 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 class EmbargoedStateForm(forms.ModelForm): # pylint: disable=incomplete-protocol
"""Form validating entry of states to embargo""" """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, ...@@ -14,10 +14,15 @@ file and check it in at the same time as your model changes. To do that,
import ipaddr import ipaddr
from django.db import models 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 config_models.models import ConfigurationModel
from xmodule_django.models import CourseKeyField, NoneToEmptyManager from xmodule_django.models import CourseKeyField, NoneToEmptyManager
from embargo.messages import ENROLL_MESSAGES, ACCESS_MESSAGES
class EmbargoedCourse(models.Model): class EmbargoedCourse(models.Model):
""" """
...@@ -72,6 +77,146 @@ class EmbargoedState(ConfigurationModel): ...@@ -72,6 +77,146 @@ class EmbargoedState(ConfigurationModel):
return [country.strip().upper() for country in self.embargoed_countries.split(',')] # pylint: disable=no-member 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): class IPFilter(ConfigurationModel):
""" """
Register specific IP addresses to explicitly block or unblock. Register specific IP addresses to explicitly block or unblock.
......
...@@ -70,9 +70,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase): ...@@ -70,9 +70,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
msg = 'COURSE NOT FOUND' msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(bad_id) self.assertIn(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
msg += 'Please recheck that you have supplied a valid course id.'
self.assertEquals(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."): with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save() form.save()
...@@ -87,9 +85,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase): ...@@ -87,9 +85,7 @@ class EmbargoCourseFormTest(ModuleStoreTestCase):
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
msg = 'COURSE NOT FOUND' msg = 'COURSE NOT FOUND'
msg += u' --- Entered course id was: "{0}". '.format(bad_id) self.assertIn(msg, form._errors['course_id'][0]) # pylint: disable=protected-access
msg += 'Please recheck that you have supplied a valid course id.'
self.assertEquals(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."): with self.assertRaisesRegexp(ValueError, "The EmbargoedCourse could not be created because the data didn't validate."):
form.save() 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