Merge pull request #11806 from edx/saleem-latif/WL-327
WL-327: Add SiteConfiguration and SiteConfigurationHistory models to LMS
This commit is contained in:
@@ -823,6 +823,9 @@ INSTALLED_APPS = (
|
||||
# Theming
|
||||
'openedx.core.djangoapps.theming',
|
||||
|
||||
# Site configuration for theming and behavioral modification
|
||||
'openedx.core.djangoapps.site_configuration',
|
||||
|
||||
# comment common
|
||||
'django_comment_common',
|
||||
|
||||
|
||||
@@ -1829,6 +1829,9 @@ INSTALLED_APPS = (
|
||||
# Theming
|
||||
'openedx.core.djangoapps.theming',
|
||||
|
||||
# Site configuration for theming and behavioral modification
|
||||
'openedx.core.djangoapps.site_configuration',
|
||||
|
||||
# Our courseware
|
||||
'courseware',
|
||||
'student',
|
||||
|
||||
26
openedx/core/djangoapps/site_configuration/__init__.py
Normal file
26
openedx/core/djangoapps/site_configuration/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
This app is used for creating/updating site's configuration. This app encapsulate configuration related logic for sites
|
||||
and provides a way for sites to override default/system behavioural or presentation logic.
|
||||
|
||||
Models:
|
||||
SiteConfiguration (models.Model):
|
||||
This model contains configuration for a site and can be used to override OpenEdx configurations.
|
||||
|
||||
Fields:
|
||||
site (OneToOneField): one to one field relating each configuration to a single site
|
||||
values (JSONField): json field to store configurations for a site
|
||||
Usage:
|
||||
configuration of each site would be available as `configuration` attribute to django site.
|
||||
If you want to access current site's configuration simply access it as `request.site.configuration`.
|
||||
|
||||
SiteConfigurationHistory (TimeStampedModel):
|
||||
This model keeps a track of all the changes made to SiteConfiguration with time stamps.
|
||||
|
||||
Fields:
|
||||
site (ForeignKey): foreign-key to django Site
|
||||
values (JSONField): json field to store configurations for a site
|
||||
Usage:
|
||||
configuration history of each site would be available as `configuration_histories` attribute to django site.
|
||||
If you want to access a list of current site's configuration history simply access it as
|
||||
`request.site.configuration_histories.all()`.
|
||||
"""
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import jsonfield.fields
|
||||
import django_extensions.db.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sites', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SiteConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('values', jsonfield.fields.JSONField(blank=True)),
|
||||
('site', models.OneToOneField(related_name='configuration', to='sites.Site')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SiteConfigurationHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
||||
('values', jsonfield.fields.JSONField(blank=True)),
|
||||
('site', models.ForeignKey(related_name='configuration_histories', to='sites.Site')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-modified', '-created'),
|
||||
'abstract': False,
|
||||
'get_latest_by': 'modified',
|
||||
},
|
||||
),
|
||||
]
|
||||
77
openedx/core/djangoapps/site_configuration/models.py
Normal file
77
openedx/core/djangoapps/site_configuration/models.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Django models for site configurations.
|
||||
"""
|
||||
import collections
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from django_extensions.db.models import TimeStampedModel
|
||||
from jsonfield.fields import JSONField
|
||||
|
||||
|
||||
class SiteConfiguration(models.Model):
|
||||
"""
|
||||
Model for storing site configuration. These configuration override OpenEdx configurations and settings.
|
||||
e.g. You can override site name, logo image, favicon etc. using site configuration.
|
||||
|
||||
Fields:
|
||||
site (OneToOneField): one to one field relating each configuration to a single site
|
||||
values (JSONField): json field to store configurations for a site
|
||||
"""
|
||||
site = models.OneToOneField(Site, related_name='configuration')
|
||||
values = JSONField(
|
||||
null=False,
|
||||
blank=True,
|
||||
load_kwargs={'object_pairs_hook': collections.OrderedDict}
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<SiteConfiguration: {site} >".format(site=self.site)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
|
||||
class SiteConfigurationHistory(TimeStampedModel):
|
||||
"""
|
||||
This is an archive table for SiteConfiguration, so that we can maintain a history of
|
||||
changes. Note that the site field is not unique in this model, compared to SiteConfiguration.
|
||||
|
||||
Fields:
|
||||
site (ForeignKey): foreign-key to django Site
|
||||
values (JSONField): json field to store configurations for a site
|
||||
"""
|
||||
site = models.ForeignKey(Site, related_name='configuration_histories')
|
||||
values = JSONField(
|
||||
null=False,
|
||||
blank=True,
|
||||
load_kwargs={'object_pairs_hook': collections.OrderedDict}
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"<SiteConfigurationHistory: {site}, Last Modified: {modified} >".format(
|
||||
modified=self.modified,
|
||||
site=self.site,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
|
||||
@receiver(post_save, sender=SiteConfiguration)
|
||||
def update_site_configuration_history(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Add site configuration changes to site configuration history.
|
||||
|
||||
Args:
|
||||
sender: sender of the signal i.e. SiteConfiguration model
|
||||
instance: SiteConfiguration instance associated with the current signal
|
||||
**kwargs: extra key word arguments
|
||||
"""
|
||||
SiteConfigurationHistory.objects.create(
|
||||
site=instance.site,
|
||||
values=instance.values,
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Model factories for unit testing views or models.
|
||||
"""
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
|
||||
|
||||
|
||||
class SiteConfigurationFactory(DjangoModelFactory):
|
||||
"""
|
||||
Factory class for SiteConfiguration model
|
||||
"""
|
||||
class Meta(object):
|
||||
model = SiteConfiguration
|
||||
|
||||
values = {}
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Tests for site configuration's django models.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from openedx.core.djangoapps.site_configuration.models import SiteConfigurationHistory
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
|
||||
|
||||
|
||||
class SiteConfigurationTests(TestCase):
|
||||
"""
|
||||
Tests for SiteConfiguration and its signals/receivers.
|
||||
"""
|
||||
domain = 'site_configuration_post_save_receiver_example.com'
|
||||
name = 'site_configuration_post_save_receiver_example'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(SiteConfigurationTests, cls).setUpClass()
|
||||
cls.site, _ = Site.objects.get_or_create(domain=cls.domain, name=cls.domain)
|
||||
|
||||
def test_site_configuration_post_save_receiver(self):
|
||||
"""
|
||||
Test that and entry is added to SiteConfigurationHistory model each time a new
|
||||
SiteConfiguration is added.
|
||||
"""
|
||||
# add SiteConfiguration to database
|
||||
site_configuration = SiteConfigurationFactory.create(
|
||||
site=self.site,
|
||||
)
|
||||
|
||||
# Verify an entry to SiteConfigurationHistory was added.
|
||||
site_configuration_history = SiteConfigurationHistory.objects.filter(
|
||||
site=site_configuration.site,
|
||||
).all()
|
||||
|
||||
# Make sure an entry (and only one entry) is saved for SiteConfiguration
|
||||
self.assertEqual(len(site_configuration_history), 1)
|
||||
|
||||
def test_site_configuration_post_update_receiver(self):
|
||||
"""
|
||||
Test that and entry is added to SiteConfigurationHistory each time a
|
||||
SiteConfiguration is updated.
|
||||
"""
|
||||
# add SiteConfiguration to database
|
||||
site_configuration = SiteConfigurationFactory.create(
|
||||
site=self.site,
|
||||
)
|
||||
|
||||
site_configuration.values = {'test': 'test'}
|
||||
site_configuration.save()
|
||||
|
||||
# Verify an entry to SiteConfigurationHistory was added.
|
||||
site_configuration_history = SiteConfigurationHistory.objects.filter(
|
||||
site=site_configuration.site,
|
||||
).all()
|
||||
|
||||
# Make sure two entries (one for save and one for update) are saved for SiteConfiguration
|
||||
self.assertEqual(len(site_configuration_history), 2)
|
||||
|
||||
def test_no_entry_is_saved_for_errors(self):
|
||||
"""
|
||||
Test that and entry is not added to SiteConfigurationHistory if there is an error while
|
||||
saving SiteConfiguration.
|
||||
"""
|
||||
# add SiteConfiguration to database
|
||||
site_configuration = SiteConfigurationFactory.create(
|
||||
site=self.site,
|
||||
)
|
||||
|
||||
# Verify an entry to SiteConfigurationHistory was added.
|
||||
site_configuration_history = SiteConfigurationHistory.objects.filter(
|
||||
site=site_configuration.site,
|
||||
).all()
|
||||
|
||||
# Make sure entry is saved if there is no error
|
||||
self.assertEqual(len(site_configuration_history), 1)
|
||||
|
||||
with transaction.atomic():
|
||||
with self.assertRaises(IntegrityError):
|
||||
# try to add a duplicate entry
|
||||
site_configuration = SiteConfigurationFactory.create(
|
||||
site=self.site,
|
||||
)
|
||||
site_configuration_history = SiteConfigurationHistory.objects.filter(
|
||||
site=site_configuration.site,
|
||||
).all()
|
||||
|
||||
# Make sure no entry is saved if there an error
|
||||
self.assertEqual(len(site_configuration_history), 1)
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Comprehensive Theme related models.
|
||||
Django models supporting the Comprehensive Theming subsystem
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
Reference in New Issue
Block a user