diff --git a/credentials/apps/core/admin.py b/credentials/apps/core/admin.py index f4cd9284e..3400ebecb 100644 --- a/credentials/apps/core/admin.py +++ b/credentials/apps/core/admin.py @@ -142,6 +142,7 @@ class SiteConfigurationAdmin(admin.ModelAdmin): { "fields": ( "site", + "edx_org_short_name", "platform_name", "company_name", "segment_key", @@ -178,4 +179,9 @@ class SiteConfigurationAdmin(admin.ModelAdmin): ) }, ), + (_('Edly Settings'), { + 'fields': ( + 'edly_client_branding_and_django_settings', + ) + }), ) diff --git a/credentials/apps/core/migrations/0024_siteconfiguration_edly_client_branding_and_django_settings.py b/credentials/apps/core/migrations/0024_siteconfiguration_edly_client_branding_and_django_settings.py new file mode 100644 index 000000000..2f963b90e --- /dev/null +++ b/credentials/apps/core/migrations/0024_siteconfiguration_edly_client_branding_and_django_settings.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.20 on 2025-09-16 11:47 + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0023_add_core_user_indices'), + ] + + operations = [ + migrations.AddField( + model_name='siteconfiguration', + name='edly_client_branding_and_django_settings', + field=jsonfield.fields.JSONField(default={}, help_text='JSON string containing edly client theme branding & Django settings.', verbose_name='Edly client theme branding & Django settings'), + ), + ] diff --git a/credentials/apps/core/migrations/0025_siteconfiguration_edx_org_short_name_and_more.py b/credentials/apps/core/migrations/0025_siteconfiguration_edx_org_short_name_and_more.py new file mode 100644 index 000000000..306c17903 --- /dev/null +++ b/credentials/apps/core/migrations/0025_siteconfiguration_edx_org_short_name_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.20 on 2025-10-08 06:04 + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0024_siteconfiguration_edly_client_branding_and_django_settings'), + ] + + operations = [ + migrations.AddField( + model_name='siteconfiguration', + name='edx_org_short_name', + field=models.CharField(help_text='Unique, short string identifier for organization, same as LMS. Please do not use spaces or special characters. Only allowed special characters are period (.), hyphen (-) and underscore (_).', max_length=255, null=True, unique=True, verbose_name='Organization short name'), + ), + migrations.AlterField( + model_name='siteconfiguration', + name='edly_client_branding_and_django_settings', + field=jsonfield.fields.JSONField(default=dict, help_text='JSON string containing edly client theme branding & Django settings.', verbose_name='Edly client theme branding & Django settings'), + ), + ] diff --git a/credentials/apps/core/models.py b/credentials/apps/core/models.py index 32e237b15..9b5d4042d 100644 --- a/credentials/apps/core/models.py +++ b/credentials/apps/core/models.py @@ -2,6 +2,7 @@ import hashlib from urllib.parse import urljoin +import logging from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -10,7 +11,9 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from edx_rest_api_client.client import OAuthAPIClient +from jsonfield.fields import JSONField +log = logging.getLogger(__name__) class SiteConfiguration(models.Model): """ @@ -20,6 +23,17 @@ class SiteConfiguration(models.Model): """ site = models.OneToOneField(Site, null=False, blank=False, on_delete=models.CASCADE) + edx_org_short_name = models.CharField( + max_length=255, + unique=True, + verbose_name=u'Organization short name', + help_text=_( + 'Unique, short string identifier for organization, same as LMS. ' + 'Please do not use spaces or special characters. ' + 'Only allowed special characters are period (.), hyphen (-) and underscore (_).' + ), + null=True, + ) platform_name = models.CharField( verbose_name=_("Platform Name"), help_text=_("Name of your Open edX platform"), @@ -124,6 +138,12 @@ class SiteConfiguration(models.Model): enable_twitter_sharing = models.BooleanField( verbose_name=_("Enable Twitter sharing"), help_text=_("Enable sharing via Twitter"), default=True ) + edly_client_branding_and_django_settings = JSONField( + verbose_name=_('Edly client theme branding & Django settings'), + help_text=_('JSON string containing edly client theme branding & Django settings.'), + null=False, + default=dict + ) def __str__(self): return self.site.name @@ -188,6 +208,22 @@ def get_user_api_data(self, username): return user_data + def get_edly_configuration_value(self, name, default=None): + """ + Return Configuration value for the key specified as name argument. + Function logs a message if there is an error retrieving a key. + Arguments: + name (str): Name of the key for which to return configuration value. + default: default value to return if key is not found in the configuration + Returns: + Configuration value for the given key or returns `None` if default is not available. + """ + try: + return self.edly_client_branding_and_django_settings.get(name, default) + except AttributeError as error: + log.exception('Invalid JSON data. \n [%s]', error) + + return default class User(AbstractUser): """ diff --git a/credentials/apps/credentials/utils.py b/credentials/apps/credentials/utils.py index 03706ef32..2a73f9a6f 100644 --- a/credentials/apps/credentials/utils.py +++ b/credentials/apps/credentials/utils.py @@ -60,7 +60,7 @@ def filter_visible(qs: "QuerySet") -> "QuerySet": Filters a UserCredentials queryset by excluding credentials that aren't supposed to be visible yet. """ - visible_course_certs = _filter_visible_course_certificates(qs.filter(course_credentials__isnull=False)) + visible_course_certs = _filter_visible_course_certificates(qs) visible_program_certs = _filter_visible_program_certificates(qs.filter(program_credentials__isnull=False)) visible_certs = visible_course_certs | visible_program_certs @@ -121,7 +121,7 @@ def _get_program_certificate_visible_date(user_program_credential: UserCredentia (DateTime or None): The date on which the program credential should be visible. (It shouldn’t return None but is technically possible.) """ - last_date = None # type: Optional[datetime.datetime] + last_date = user_program_credential.created for course_run in user_program_credential.credential.program.course_runs.all(): # Does the user have a course cert for this course run? course_run_cert = UserCredential.objects.filter(