From 1bb29289f2d199c1756bad6130c1560b3e2e249b Mon Sep 17 00:00:00 2001 From: jsa Date: Sat, 24 Oct 2015 22:24:07 -0400 Subject: [PATCH] Programs: added platform support package and configuration model. ECOM-2657 --- cms/envs/common.py | 3 + lms/envs/common.py | 3 + openedx/core/djangoapps/programs/__init__.py | 0 openedx/core/djangoapps/programs/admin.py | 15 ++++ .../programs/migrations/0001_initial.py | 80 +++++++++++++++++ .../programs/migrations/__init__.py | 0 openedx/core/djangoapps/programs/models.py | 44 ++++++++++ .../djangoapps/programs/tests/__init__.py | 0 .../djangoapps/programs/tests/test_models.py | 86 +++++++++++++++++++ 9 files changed, 231 insertions(+) create mode 100644 openedx/core/djangoapps/programs/__init__.py create mode 100644 openedx/core/djangoapps/programs/admin.py create mode 100644 openedx/core/djangoapps/programs/migrations/0001_initial.py create mode 100644 openedx/core/djangoapps/programs/migrations/__init__.py create mode 100644 openedx/core/djangoapps/programs/models.py create mode 100644 openedx/core/djangoapps/programs/tests/__init__.py create mode 100644 openedx/core/djangoapps/programs/tests/test_models.py diff --git a/cms/envs/common.py b/cms/envs/common.py index c753a7031d..59b51c200b 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -790,6 +790,9 @@ INSTALLED_APPS = ( # edX Proctoring 'edx_proctoring', + + # programs support + 'openedx.core.djangoapps.programs', ) diff --git a/lms/envs/common.py b/lms/envs/common.py index 9b761399ac..3d3900cb9b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1964,6 +1964,9 @@ INSTALLED_APPS = ( 'teams', 'xblock_django', + + # programs support + 'openedx.core.djangoapps.programs', ) ######################### CSRF ######################################### diff --git a/openedx/core/djangoapps/programs/__init__.py b/openedx/core/djangoapps/programs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/programs/admin.py b/openedx/core/djangoapps/programs/admin.py new file mode 100644 index 0000000000..0676e93ad4 --- /dev/null +++ b/openedx/core/djangoapps/programs/admin.py @@ -0,0 +1,15 @@ +""" +django admin pages for program support models +""" +from django.contrib import admin + +from config_models.admin import ConfigurationModelAdmin + +from openedx.core.djangoapps.programs.models import ProgramsApiConfig + + +class ProgramsApiConfigAdmin(ConfigurationModelAdmin): # pylint: disable=missing-docstring + pass + + +admin.site.register(ProgramsApiConfig, ProgramsApiConfigAdmin) diff --git a/openedx/core/djangoapps/programs/migrations/0001_initial.py b/openedx/core/djangoapps/programs/migrations/0001_initial.py new file mode 100644 index 0000000000..5bc39e7274 --- /dev/null +++ b/openedx/core/djangoapps/programs/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as 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 'ProgramsApiConfig' + db.create_table('programs_programsapiconfig', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('internal_service_url', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('public_service_url', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('api_version_number', self.gf('django.db.models.fields.IntegerField')()), + ('enable_student_dashboard', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('programs', ['ProgramsApiConfig']) + + + def backwards(self, orm): + # Deleting model 'ProgramsApiConfig' + db.delete_table('programs_programsapiconfig') + + + 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'}) + }, + 'programs.programsapiconfig': { + 'Meta': {'ordering': "('-change_date',)", 'object_name': 'ProgramsApiConfig'}, + 'api_version_number': ('django.db.models.fields.IntegerField', [], {}), + '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'}), + 'enable_student_dashboard': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'internal_service_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'public_service_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + } + } + + complete_apps = ['programs'] \ No newline at end of file diff --git a/openedx/core/djangoapps/programs/migrations/__init__.py b/openedx/core/djangoapps/programs/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/programs/models.py b/openedx/core/djangoapps/programs/models.py new file mode 100644 index 0000000000..846b227717 --- /dev/null +++ b/openedx/core/djangoapps/programs/models.py @@ -0,0 +1,44 @@ +""" +Models providing Programs support for the LMS and Studio. +""" + +from urlparse import urljoin + +from django.db.models import BooleanField, IntegerField, URLField +from django.utils.translation import ugettext_lazy as _ + +from config_models.models import ConfigurationModel + + +class ProgramsApiConfig(ConfigurationModel): + """ + Manages configuration for connecting to the Programs service and using its + API. + """ + + internal_service_url = URLField(verbose_name=_("Internal Service URL")) + public_service_url = URLField(verbose_name=_("Public Service URL")) + api_version_number = IntegerField(verbose_name=_("API Version")) + enable_student_dashboard = BooleanField(verbose_name=_("Enable Student Dashboard Displays")) + + @property + def internal_api_url(self): + """ + Generate a URL based on internal service URL and api version number. + """ + return urljoin(self.internal_service_url, "/api/v{}/".format(self.api_version_number)) + + @property + def public_api_url(self): + """ + Generate a URL based on public service URL and api version number. + """ + return urljoin(self.public_service_url, "/api/v{}/".format(self.api_version_number)) + + @property + def is_student_dashboard_enabled(self): + """ + Indicate whether LMS dashboard functionality related to Programs should + be enabled or not. + """ + return self.enabled and self.enable_student_dashboard diff --git a/openedx/core/djangoapps/programs/tests/__init__.py b/openedx/core/djangoapps/programs/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/core/djangoapps/programs/tests/test_models.py b/openedx/core/djangoapps/programs/tests/test_models.py new file mode 100644 index 0000000000..5cf09e57bf --- /dev/null +++ b/openedx/core/djangoapps/programs/tests/test_models.py @@ -0,0 +1,86 @@ +""" +Tests for models supporting Program-related functionality. +""" + +from django.test import TestCase +from mock import patch + +from openedx.core.djangoapps.programs.models import ProgramsApiConfig + + +@patch('config_models.models.cache.get', return_value=None) # during tests, make every cache get a miss. +class ProgramsApiConfigTest(TestCase): + """ + Tests for the ProgramsApiConfig model. + """ + + INTERNAL_URL = "http://internal/" + PUBLIC_URL = "http://public/" + + DEFAULTS = dict( + internal_service_url=INTERNAL_URL, + public_service_url=PUBLIC_URL, + api_version_number=1, + ) + + def create_config(self, **kwargs): + """ + DRY helper. Create a new ProgramsApiConfig with self.DEFAULTS, updated + with any kwarg overrides. + """ + ProgramsApiConfig(**dict(self.DEFAULTS, **kwargs)).save() + + def test_default_state(self, _mock_cache): + """ + Ensure the config stores empty values when no data has been inserted, + and is completely disabled. + """ + self.assertFalse(ProgramsApiConfig.is_enabled()) + api_config = ProgramsApiConfig.current() + self.assertEqual(api_config.internal_service_url, '') + self.assertEqual(api_config.public_service_url, '') + self.assertEqual(api_config.api_version_number, None) + self.assertFalse(api_config.is_student_dashboard_enabled) + + def test_created_state(self, _mock_cache): + """ + Ensure the config stores correct values when created with them, but + remains disabled. + """ + self.create_config() + self.assertFalse(ProgramsApiConfig.is_enabled()) + api_config = ProgramsApiConfig.current() + self.assertEqual(api_config.internal_service_url, self.INTERNAL_URL) + self.assertEqual(api_config.public_service_url, self.PUBLIC_URL) + self.assertEqual(api_config.api_version_number, 1) + self.assertFalse(api_config.is_student_dashboard_enabled) + + def test_api_urls(self, _mock_cache): + """ + Ensure the api url methods return correct concatenations of service + URLs and version numbers. + """ + self.create_config() + api_config = ProgramsApiConfig.current() + self.assertEqual(api_config.internal_api_url, "{}api/v1/".format(self.INTERNAL_URL)) + self.assertEqual(api_config.public_api_url, "{}api/v1/".format(self.PUBLIC_URL)) + + def test_is_student_dashboard_enabled(self, _mock_cache): + """ + Ensure that is_student_dashboard_enabled only returns True when the + current config has both 'enabled' and 'enable_student_dashboard' set to + True. + """ + self.assertFalse(ProgramsApiConfig.current().is_student_dashboard_enabled) + + self.create_config() + self.assertFalse(ProgramsApiConfig.current().is_student_dashboard_enabled) + + self.create_config(enabled=True) + self.assertFalse(ProgramsApiConfig.current().is_student_dashboard_enabled) + + self.create_config(enable_student_dashboard=True) + self.assertFalse(ProgramsApiConfig.current().is_student_dashboard_enabled) + + self.create_config(enabled=True, enable_student_dashboard=True) + self.assertTrue(ProgramsApiConfig.current().is_student_dashboard_enabled)