Skip to content

Commit 72959ad

Browse files
hassan-raza-1Hassan Raza
and
Hassan Raza
authored
feat: refactor country disable logic into the Embargo app (#36202)
* feat: add country disabling feature in embargo app * revert: disabled countries list in env * fix: resolved linter issues --------- Co-authored-by: Hassan Raza <[email protected]>
1 parent ee7fd49 commit 72959ad

File tree

10 files changed

+212
-62
lines changed

10 files changed

+212
-62
lines changed

cms/envs/common.py

-7
Original file line numberDiff line numberDiff line change
@@ -2919,13 +2919,6 @@ def _should_send_learning_badge_events(settings):
29192919
MEILISEARCH_INDEX_PREFIX = ""
29202920
MEILISEARCH_API_KEY = "devkey"
29212921

2922-
# .. setting_name: DISABLED_COUNTRIES
2923-
# .. setting_default: []
2924-
# .. setting_description: List of country codes that should be disabled
2925-
# .. for now it wil impact country listing in auth flow and user profile.
2926-
# .. eg ['US', 'CA']
2927-
DISABLED_COUNTRIES = []
2928-
29292922
# .. setting_name: LIBRARY_ENABLED_BLOCKS
29302923
# .. setting_default: ['problem', 'video', 'html', 'drag-and-drop-v2']
29312924
# .. setting_description: List of block types that are ready/enabled to be created/used

lms/envs/common.py

-9
Original file line numberDiff line numberDiff line change
@@ -5549,15 +5549,6 @@ def _should_send_learning_badge_events(settings):
55495549
# .. setting_description: Dictionary with additional information that you want to share in the report.
55505550
SURVEY_REPORT_EXTRA_DATA = {}
55515551

5552-
5553-
# .. setting_name: DISABLED_COUNTRIES
5554-
# .. setting_default: []
5555-
# .. setting_description: List of country codes that should be disabled
5556-
# .. for now it wil impact country listing in auth flow and user profile.
5557-
# .. eg ['US', 'CA']
5558-
DISABLED_COUNTRIES = []
5559-
5560-
55615552
LMS_COMM_DEFAULT_FROM_EMAIL = "[email protected]"
55625553

55635554

openedx/core/djangoapps/embargo/admin.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.contrib import admin
99

1010
from .forms import IPFilterForm, RestrictedCourseForm
11-
from .models import CountryAccessRule, IPFilter, RestrictedCourse
11+
from .models import CountryAccessRule, GlobalRestrictedCountry, IPFilter, RestrictedCourse
1212

1313

1414
class IPFilterAdmin(ConfigurationModelAdmin):
@@ -41,5 +41,20 @@ class RestrictedCourseAdmin(admin.ModelAdmin):
4141
search_fields = ('course_key',)
4242

4343

44+
class GlobalRestrictedCountryAdmin(admin.ModelAdmin):
45+
"""
46+
Admin configuration for the Global Country Restriction model.
47+
"""
48+
list_display = ("country",)
49+
50+
def delete_queryset(self, request, queryset):
51+
"""
52+
Override the delete_queryset method to clear the cache when objects are deleted in bulk.
53+
"""
54+
super().delete_queryset(request, queryset)
55+
GlobalRestrictedCountry.update_cache()
56+
57+
4458
admin.site.register(IPFilter, IPFilterAdmin)
4559
admin.site.register(RestrictedCourse, RestrictedCourseAdmin)
60+
admin.site.register(GlobalRestrictedCountry, GlobalRestrictedCountryAdmin)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 4.2.18 on 2025-01-29 08:19
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('embargo', '0002_data__add_countries'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='GlobalRestrictedCountry',
16+
fields=[
17+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('country', models.ForeignKey(help_text='The country to be restricted.', on_delete=django.db.models.deletion.CASCADE, to='embargo.country', unique=True)),
19+
],
20+
options={
21+
'verbose_name': 'Global Restricted Country',
22+
'verbose_name_plural': 'Global Restricted Countries',
23+
},
24+
),
25+
]

openedx/core/djangoapps/embargo/models.py

+75
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,81 @@ class Meta:
662662
get_latest_by = 'timestamp'
663663

664664

665+
class GlobalRestrictedCountry(models.Model):
666+
"""
667+
Model to restrict access to specific countries globally.
668+
"""
669+
country = models.ForeignKey(
670+
"Country",
671+
help_text="The country to be restricted.",
672+
on_delete=models.CASCADE,
673+
unique=True
674+
)
675+
676+
CACHE_KEY = "embargo.global.restricted_countries"
677+
678+
@classmethod
679+
def get_countries(cls):
680+
"""
681+
Retrieve the set of restricted country codes from the cache or refresh it if not available.
682+
683+
Returns:
684+
set: A set of restricted country codes.
685+
"""
686+
return cache.get_or_set(cls.CACHE_KEY, cls._fetch_restricted_countries)
687+
688+
@classmethod
689+
def is_country_restricted(cls, country_code):
690+
"""
691+
Check if the given country code is restricted.
692+
693+
Args:
694+
country_code (str): The country code to check.
695+
696+
Returns:
697+
bool: True if the country is restricted, False otherwise.
698+
"""
699+
return country_code in cls.get_countries()
700+
701+
@classmethod
702+
def _fetch_restricted_countries(cls):
703+
"""
704+
Fetch the set of restricted country codes from the database.
705+
706+
Returns:
707+
set: A set of restricted country codes.
708+
"""
709+
return set(cls.objects.values_list("country__country", flat=True))
710+
711+
@classmethod
712+
def update_cache(cls):
713+
"""
714+
Update the cache with the latest restricted country codes.
715+
"""
716+
cache.set(cls.CACHE_KEY, cls._fetch_restricted_countries())
717+
718+
def save(self, *args, **kwargs):
719+
"""
720+
Override save method to update cache on insert/update.
721+
"""
722+
super().save(*args, **kwargs)
723+
self.update_cache()
724+
725+
def delete(self, *args, **kwargs):
726+
"""
727+
Override delete method to update cache on deletion.
728+
"""
729+
super().delete(*args, **kwargs)
730+
self.update_cache()
731+
732+
def __str__(self):
733+
return f"{self.country.country.name} ({self.country.country})"
734+
735+
class Meta:
736+
verbose_name = "Global Restricted Country"
737+
verbose_name_plural = "Global Restricted Countries"
738+
739+
665740
# Connect the signals to the receivers so we record a history
666741
# of changes to the course access rules.
667742
post_save.connect(CourseAccessRuleHistory.snapshot_post_save_receiver, sender=RestrictedCourse)

openedx/core/djangoapps/user_api/accounts/api.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from django.conf import settings
1010
from django.core.exceptions import ObjectDoesNotExist
1111
from django.core.validators import ValidationError, validate_email
12-
from django.utils.translation import override as override_language
1312
from django.utils.translation import gettext as _
13+
from django.utils.translation import override as override_language
1414
from eventtracking import tracker
1515
from pytz import UTC
16+
1617
from common.djangoapps.student import views as student_views
1718
from common.djangoapps.student.models import (
1819
AccountRecovery,
@@ -25,7 +26,7 @@
2526
from common.djangoapps.util.password_policy_validators import validate_password
2627
from lms.djangoapps.certificates.api import get_certificates_for_user
2728
from lms.djangoapps.certificates.data import CertificateStatuses
28-
29+
from openedx.core.djangoapps.embargo.models import GlobalRestrictedCountry
2930
from openedx.core.djangoapps.enrollments.api import get_verified_enrollments
3031
from openedx.core.djangoapps.user_api import accounts, errors, helpers
3132
from openedx.core.djangoapps.user_api.errors import (
@@ -39,6 +40,7 @@
3940
from openedx.core.lib.api.view_utils import add_serializer_errors
4041
from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields
4142
from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed
43+
4244
from .serializers import AccountLegacyProfileSerializer, AccountUserSerializer, UserReadOnlySerializer, _visible_fields
4345

4446
name_affirmation_installed = is_name_affirmation_installed()
@@ -151,7 +153,10 @@ def update_account_settings(requesting_user, update, username=None):
151153

152154
_validate_email_change(user, update, field_errors)
153155
_validate_secondary_email(user, update, field_errors)
154-
if update.get('country', '') in settings.DISABLED_COUNTRIES:
156+
if (
157+
settings.FEATURES.get('EMBARGO', False) and
158+
GlobalRestrictedCountry.is_country_restricted(update.get('country', ''))
159+
):
155160
field_errors['country'] = {
156161
'developer_message': 'Country is disabled for registration',
157162
'user_message': 'This country cannot be selected for user registration'

openedx/core/djangoapps/user_api/accounts/tests/test_api.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@
77
import itertools
88
import unicodedata
99
from unittest.mock import Mock, patch
10-
import pytest
10+
1111
import ddt
12+
import pytest
1213
from django.conf import settings
1314
from django.contrib.auth.hashers import make_password
1415
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
1516
from django.http import HttpResponse
1617
from django.test import TestCase
1718
from django.test.client import RequestFactory
18-
from django.test.utils import override_settings
1919
from django.urls import reverse
2020
from pytz import UTC
2121
from social_django.models import UserSocialAuth
22+
2223
from common.djangoapps.student.models import (
2324
AccountRecovery,
2425
PendingEmailChange,
@@ -28,14 +29,14 @@
2829
from common.djangoapps.student.tests.factories import UserFactory
2930
from common.djangoapps.student.tests.tests import UserSettingsEventTestMixin
3031
from common.djangoapps.student.views.management import activate_secondary_email
31-
3232
from lms.djangoapps.certificates.data import CertificateStatuses
3333
from openedx.core.djangoapps.ace_common.tests.mixins import EmailTemplateTagMixin
34+
from openedx.core.djangoapps.embargo.models import Country, GlobalRestrictedCountry
3435
from openedx.core.djangoapps.user_api.accounts import PRIVATE_VISIBILITY
3536
from openedx.core.djangoapps.user_api.accounts.api import (
3637
get_account_settings,
37-
update_account_settings,
38-
get_name_validation_error
38+
get_name_validation_error,
39+
update_account_settings
3940
)
4041
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
4142
RetirementTestCase,
@@ -574,12 +575,14 @@ def test_change_country_removes_state(self):
574575
assert account_settings['country'] is None
575576
assert account_settings['state'] is None
576577

577-
@override_settings(DISABLED_COUNTRIES=['KP'])
578578
def test_change_to_disabled_country(self):
579579
"""
580580
Test that changing the country to a disabled country is not allowed
581581
"""
582582
# First set the country and state
583+
country = Country.objects.create(country="KP")
584+
GlobalRestrictedCountry.objects.create(country=country)
585+
583586
update_account_settings(self.user, {"country": UserProfile.COUNTRY_WITH_STATES, "state": "MA"})
584587
account_settings = get_account_settings(self.default_request)[0]
585588
assert account_settings['country'] == UserProfile.COUNTRY_WITH_STATES

openedx/core/djangoapps/user_authn/views/registration_form.py

+15-15
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
"""
44

55
import copy
6-
from importlib import import_module
7-
from eventtracking import tracker
86
import re
7+
from importlib import import_module
98

109
from django import forms
1110
from django.conf import settings
@@ -16,26 +15,25 @@
1615
from django.urls import reverse
1716
from django.utils.translation import gettext as _
1817
from django_countries import countries
18+
from eventtracking import tracker
1919

2020
from common.djangoapps import third_party_auth
2121
from common.djangoapps.edxmako.shortcuts import marketing_link
22+
from common.djangoapps.student.models import CourseEnrollmentAllowed, UserProfile, email_exists_or_retired
23+
from common.djangoapps.util.password_policy_validators import (
24+
password_validators_instruction_texts,
25+
password_validators_restrictions,
26+
validate_password
27+
)
28+
from openedx.core.djangoapps.embargo.models import GlobalRestrictedCountry
2229
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
2330
from openedx.core.djangoapps.user_api import accounts
2431
from openedx.core.djangoapps.user_api.helpers import FormDescription
25-
from openedx.core.djangoapps.user_authn.utils import check_pwned_password, is_registration_api_v1 as is_api_v1
32+
from openedx.core.djangoapps.user_authn.utils import check_pwned_password
33+
from openedx.core.djangoapps.user_authn.utils import is_registration_api_v1 as is_api_v1
2634
from openedx.core.djangoapps.user_authn.views.utils import remove_disabled_country_from_list
2735
from openedx.core.djangolib.markup import HTML, Text
2836
from openedx.features.enterprise_support.api import enterprise_customer_for_request
29-
from common.djangoapps.student.models import (
30-
CourseEnrollmentAllowed,
31-
UserProfile,
32-
email_exists_or_retired,
33-
)
34-
from common.djangoapps.util.password_policy_validators import (
35-
password_validators_instruction_texts,
36-
password_validators_restrictions,
37-
validate_password,
38-
)
3937

4038

4139
class TrueCheckbox(widgets.CheckboxInput):
@@ -306,7 +304,10 @@ def clean_country(self):
306304
Check if the user's country is in the embargoed countries list.
307305
"""
308306
country = self.cleaned_data.get("country")
309-
if country in settings.DISABLED_COUNTRIES:
307+
if (
308+
settings.FEATURES.get('EMBARGO', False) and
309+
country in GlobalRestrictedCountry.get_countries()
310+
):
310311
raise ValidationError(_("Registration from this country is not allowed due to restrictions."))
311312
return self.cleaned_data.get("country")
312313

@@ -981,7 +982,6 @@ def _add_country_field(self, form_desc, required=True):
981982
'country',
982983
default=default_country.upper()
983984
)
984-
985985
form_desc.add_field(
986986
"country",
987987
label=country_label,

0 commit comments

Comments
 (0)