""" Provide django models to back the discussions app """ from __future__ import annotations import logging from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from django_mysql.models import ListCharField from jsonfield import JSONField from model_utils.models import TimeStampedModel from opaque_keys.edx.django.models import LearningContextKeyField from opaque_keys.edx.keys import CourseKey from simple_history.models import HistoricalRecords from lti_consumer.models import LtiConfiguration from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel from openedx.core.djangoapps.content.course_overviews.models import CourseOverview log = logging.getLogger(__name__) def get_supported_providers() -> list[str]: """ Return the list of supported discussion providers TODO: Load this from entry points? """ providers = [ 'legacy', 'piazza', ] return providers class ProviderFilter(StackedConfigurationModel): """ Associate allow/deny-lists of discussions providers with courses/orgs """ allow = ListCharField( base_field=models.CharField( choices=[ (provider, provider) for provider in get_supported_providers() ], max_length=20, ), blank=True, help_text=_("Comma-separated list of providers to allow, eg: {choices}").format( choices=','.join(get_supported_providers()), ), # max_length = (size * (max_length + len(',')) # max_length = ( 3 * ( 20 + 1)) max_length=63, # size = len(get_supported_providers()) size=3, verbose_name=_('Allow List'), ) deny = ListCharField( base_field=models.CharField( choices=[ (provider, provider) for provider in get_supported_providers() ], max_length=20, ), blank=True, help_text=_("Comma-separated list of providers to deny, eg: {choices}").format( choices=','.join(get_supported_providers()), ), # max_length = (size * (max_length + len(',')) # max_length = ( 3 * ( 20 + 1)) max_length=63, # size = len(get_supported_providers()) size=3, verbose_name=_('Deny List'), ) STACKABLE_FIELDS = ( 'enabled', 'allow', 'deny', ) def __str__(self): return 'ProviderFilter(org="{org}", course="{course}", allow={allow}, deny={deny})'.format( allow=self.allow, course=self.course or '', deny=self.deny, org=self.org or '', ) @property def available_providers(self) -> list[str]: """ Return a filtered list of available providers """ _providers = get_supported_providers() if self.allow: _providers = [ provider for provider in _providers if provider in self.allow ] if self.deny: _providers = [ provider for provider in _providers if provider not in self.deny ] return _providers @classmethod def get_available_providers(cls, course_key: CourseKey) -> list[str]: _filter = cls.current(course_key=course_key) providers = _filter.available_providers return providers class DiscussionsConfiguration(TimeStampedModel): """ Associates a learning context with discussion provider and configuration """ context_key = LearningContextKeyField( primary_key=True, db_index=True, unique=True, max_length=255, # Translators: A key specifying a course, library, program, # website, or some other collection of content where learning # happens. verbose_name=_("Learning Context Key"), ) enabled = models.BooleanField( default=True, help_text=_("If disabled, the discussions in the associated learning context/course will be disabled.") ) lti_configuration = models.ForeignKey( LtiConfiguration, on_delete=models.SET_NULL, blank=True, null=True, help_text=_("The LTI configuration data for this context/provider."), ) plugin_configuration = JSONField( blank=True, default={}, help_text=_("The plugin configuration data for this context/provider."), ) provider_type = models.CharField( blank=False, max_length=100, verbose_name=_("Discussion provider"), help_text=_("The discussion tool/provider's id"), ) history = HistoricalRecords() def clean(self): """ Validate the model Currently, this only support courses, this can be extended whenever discussions are available in other contexts """ if not CourseOverview.course_exists(self.context_key): raise ValidationError('Context Key should be an existing learning context.') def __str__(self): return "DiscussionsConfiguration(context_key='{context_key}', provider='{provider}', enabled={enabled})".format( context_key=self.context_key, provider=self.provider_type, enabled=self.enabled, ) @classmethod def is_enabled(cls, context_key: CourseKey) -> bool: """ Check if there is an active configuration for a given course key Default to False, if no configuration exists """ configuration = cls.get(context_key) return configuration.enabled # pylint: disable=undefined-variable @classmethod def get(cls, context_key: CourseKey) -> cls: """ Lookup a model by context_key """ try: configuration = cls.objects.get(context_key=context_key) except cls.DoesNotExist: configuration = cls(context_key=context_key, enabled=False) return configuration # pylint: enable=undefined-variable @property def available_providers(self) -> list[str]: return ProviderFilter.current(course_key=self.context_key).available_providers @classmethod def get_available_providers(cls, context_key: CourseKey) -> list[str]: return ProviderFilter.current(course_key=context_key).available_providers