Browse Source
feat: implement authentication serializers and related account and meeting management modules
main
feat: implement authentication serializers and related account and meeting management modules
main
32 changed files with 3818 additions and 38 deletions
-
2090apps/account/admin/access_modal_widget.py
-
43apps/account/admin/user.py
-
82apps/account/admin/widgets.py
-
33apps/account/migrations/0006_user_managed_regions_user_regional_permissions.py
-
88apps/account/models/user.py
-
19apps/account/serializers/auth_serializers.py
-
11apps/geo_map/views.py
-
2apps/meetings/admin.py
-
18apps/meetings/migrations/0002_meetingrequest_rejection_reason.py
-
1apps/meetings/models/meeting.py
-
26apps/meetings/serializers/meeting_serializers.py
-
34apps/meetings/tests/test_phase7_meetings.py
-
2apps/meetings/views/meeting_views.py
-
82apps/profiles/admin.py
-
137apps/profiles/migrations/0002_region_and_institution_region.py
-
3apps/profiles/models/__init__.py
-
26apps/profiles/models/institution.py
-
84apps/profiles/models/region.py
-
8apps/profiles/permissions.py
-
5apps/profiles/serializers/__init__.py
-
92apps/profiles/serializers/institution_serializers.py
-
124apps/profiles/tests/test_phase2_profiles.py
-
256apps/profiles/tests/test_regional_admin.py
-
6apps/profiles/urls.py
-
5apps/profiles/views/__init__.py
-
134apps/profiles/views/institution_views.py
-
13apps/projects/models/project.py
-
2apps/projects/permissions.py
-
35apps/projects/serializers/project_serializers.py
-
5config/settings/base.py
-
BINdump.rdb
-
388schema.yml
2090
apps/account/admin/access_modal_widget.py
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,33 @@ |
|||
from django.db import migrations, models |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
('account', '0005_role_user_custom_permissions_user_role'), |
|||
('profiles', '0002_region_and_institution_region'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.AddField( |
|||
model_name='user', |
|||
name='managed_regions', |
|||
field=models.ManyToManyField( |
|||
blank=True, |
|||
help_text='Regions assigned to this regional administrator or staff member. Administrative actions and regional permissions apply only to these regions.', |
|||
related_name='regional_admins', |
|||
to='profiles.region', |
|||
verbose_name='Managed Regions', |
|||
), |
|||
), |
|||
migrations.AddField( |
|||
model_name='user', |
|||
name='regional_permissions', |
|||
field=models.JSONField( |
|||
blank=True, |
|||
default=list, |
|||
help_text="List of permission names that are restricted to the user's managed regions (e.g. ['can_manage_institutions', 'can_manage_tickets']).", |
|||
verbose_name='Regional Permission Overrides', |
|||
), |
|||
), |
|||
] |
|||
@ -0,0 +1,18 @@ |
|||
# Generated manually on 2026-10-08 |
|||
|
|||
from django.db import migrations, models |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
('meetings', '0001_initial'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.AddField( |
|||
model_name='meetingrequest', |
|||
name='rejection_reason', |
|||
field=models.TextField(blank=True, verbose_name='Rejection / Decline Reason'), |
|||
), |
|||
] |
|||
@ -0,0 +1,137 @@ |
|||
from django.db import migrations, models |
|||
import django.db.models.deletion |
|||
|
|||
|
|||
def create_initial_regions(apps, schema_editor): |
|||
Region = apps.get_model('profiles', 'Region') |
|||
regions = [ |
|||
{"name": "Global / Other", "slug": "global-other", "code": "GLB", "description": "Global or international scope"}, |
|||
{"name": "Middle East & North Africa", "slug": "middle-east-north-africa", "code": "MENA", "description": "Middle East, Gulf countries, and North Africa"}, |
|||
{"name": "Europe", "slug": "europe", "code": "EU", "description": "European continent and United Kingdom"}, |
|||
{"name": "North America", "slug": "north-america", "code": "NA", "description": "United States and Canada"}, |
|||
{"name": "Central & South Asia", "slug": "central-south-asia", "code": "CSA", "description": "Central and South Asian subcontinent"}, |
|||
{"name": "East & Southeast Asia", "slug": "east-southeast-asia", "code": "ESA", "description": "East Asia and ASEAN region"}, |
|||
{"name": "Latin America & Caribbean", "slug": "latin-america", "code": "LATAM", "description": "Central and South America and the Caribbean"}, |
|||
{"name": "Sub-Saharan Africa", "slug": "sub-saharan-africa", "code": "SSA", "description": "Central, Eastern, Western, and Southern Africa"}, |
|||
{"name": "Oceania", "slug": "oceania", "code": "OCN", "description": "Australia, New Zealand, and Pacific Islands"}, |
|||
] |
|||
for r in regions: |
|||
Region.objects.get_or_create( |
|||
name=r["name"], |
|||
defaults={ |
|||
"slug": r["slug"], |
|||
"code": r["code"], |
|||
"description": r["description"], |
|||
"is_active": True, |
|||
} |
|||
) |
|||
|
|||
|
|||
def populate_regions_and_backfill(apps, schema_editor): |
|||
Region = apps.get_model('profiles', 'Region') |
|||
Institution = apps.get_model('profiles', 'Institution') |
|||
|
|||
default_region = Region.objects.filter(slug="global-other").first() or Region.objects.first() |
|||
mena = Region.objects.filter(slug="middle-east-north-africa").first() or default_region |
|||
europe = Region.objects.filter(slug="europe").first() or default_region |
|||
na = Region.objects.filter(slug="north-america").first() or default_region |
|||
csa = Region.objects.filter(slug="central-south-asia").first() or default_region |
|||
esa = Region.objects.filter(slug="east-southeast-asia").first() or default_region |
|||
latam = Region.objects.filter(slug="latin-america").first() or default_region |
|||
ssa = Region.objects.filter(slug="sub-saharan-africa").first() or default_region |
|||
oceania = Region.objects.filter(slug="oceania").first() or default_region |
|||
|
|||
country_map = { |
|||
# MENA |
|||
"ایران": mena, "iran": mena, "iraq": mena, "عراق": mena, "syria": mena, "سوریه": mena, |
|||
"lebanon": mena, "لبنان": mena, "turkey": mena, "ترکیه": mena, "yemen": mena, "یمن": mena, |
|||
"saudi arabia": mena, "عربستان": mena, "uae": mena, "united arab emirates": mena, |
|||
"امارات": mena, "kuwait": mena, "کویت": mena, "qatar": mena, "قطر": mena, |
|||
"bahrain": mena, "بحرین": mena, "oman": mena, "عمان": mena, "jordan": mena, "اردن": mena, |
|||
"palestine": mena, "فلسطین": mena, "egypt": mena, "مصر": mena, "morocco": mena, "مراکش": mena, |
|||
"algeria": mena, "الجزایر": mena, "tunisia": mena, "تونس": mena, "libya": mena, "لیبی": mena, |
|||
# Europe |
|||
"russia": europe, "روسیه": europe, "sweden": europe, "سوئد": europe, "germany": europe, "آلمان": europe, |
|||
"united kingdom": europe, "uk": europe, "انگلیس": europe, "بریتانیا": europe, "france": europe, "فرانسه": europe, |
|||
"italy": europe, "ایتالیا": europe, "austria": europe, "اتریش": europe, "netherlands": europe, "هلند": europe, |
|||
"belgium": europe, "بلژیک": europe, "switzerland": europe, "سوئیس": europe, "spain": europe, "اسپانیا": europe, |
|||
"norway": europe, "نروژ": europe, "denmark": europe, "دانمارک": europe, "finland": europe, "فنلاند": europe, |
|||
"bosnia": europe, "bosnia and herzegovina": europe, "بوسنی": europe, "poland": europe, "لهستان": europe, |
|||
# North America |
|||
"canada": na, "کانادا": na, "united states": na, "usa": na, "آمریکا": na, "ایالات متحده": na, |
|||
# Central & South Asia |
|||
"pakistan": csa, "پاکستان": csa, "india": csa, "هند": csa, "هندوستان": csa, |
|||
"afghanistan": csa, "افغانستان": csa, "bangladesh": csa, "بنگلادش": csa, |
|||
"tajikistan": csa, "تاجیکستان": csa, "uzbekistan": csa, "ازبکستان": csa, |
|||
"azerbaijan": csa, "آذربایجان": csa, "turkmenistan": csa, "ترکمنستان": csa, |
|||
# East & Southeast Asia |
|||
"indonesia": esa, "اندونزی": esa, "malaysia": esa, "مالزی": esa, "china": esa, "چین": esa, |
|||
"japan": esa, "ژاپن": esa, "thailand": esa, "تایلند": esa, "philippines": esa, "فیلیپین": esa, |
|||
# Latin America |
|||
"brazil": latam, "برزیل": latam, "argentina": latam, "آرژانتین": latam, "colombia": latam, "کلمبیا": latam, |
|||
"chile": latam, "شیلی": latam, "venezuela": latam, "ونزوئلا": latam, "mexico": latam, "مکزیک": latam, |
|||
# Sub-Saharan Africa |
|||
"nigeria": ssa, "نیجریه": ssa, "ghana": ssa, "غنا": ssa, "kenya": ssa, "کنیا": ssa, |
|||
"tanzania": ssa, "تانزانیا": ssa, "south africa": ssa, "آفریقای جنوبی": ssa, |
|||
# Oceania |
|||
"australia": oceania, "استرالیا": oceania, "new zealand": oceania, "نیوزیلند": oceania, |
|||
} |
|||
|
|||
for inst in Institution.objects.all(): |
|||
c = (inst.country or "").strip().lower() |
|||
inst.region = country_map.get(c, default_region) |
|||
inst.save(update_fields=['region']) |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
('profiles', '0001_initial'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.CreateModel( |
|||
name='Region', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('name', models.CharField(help_text='Name of the geographical or administrative region (e.g. Middle East, Europe).', max_length=255, unique=True, verbose_name='Region Name')), |
|||
('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, unique=True, verbose_name='Slug / URL Identifier')), |
|||
('code', models.CharField(blank=True, help_text='Short code such as MENA, EU, NA, etc.', max_length=50, null=True, verbose_name='Region Code')), |
|||
('description', models.TextField(blank=True, null=True, verbose_name='Description')), |
|||
('is_active', models.BooleanField(default=True, verbose_name='Is Active')), |
|||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), |
|||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
|||
], |
|||
options={ |
|||
'verbose_name': 'Region', |
|||
'verbose_name_plural': 'Regions', |
|||
'ordering': ('name',), |
|||
}, |
|||
), |
|||
migrations.RunPython(create_initial_regions, reverse_code=migrations.RunPython.noop), |
|||
migrations.AddField( |
|||
model_name='institution', |
|||
name='region', |
|||
field=models.ForeignKey( |
|||
blank=True, |
|||
help_text='Geographical or administrative region of the institution.', |
|||
null=True, |
|||
on_delete=django.db.models.deletion.PROTECT, |
|||
related_name='institutions', |
|||
to='profiles.region', |
|||
verbose_name='Region', |
|||
), |
|||
), |
|||
migrations.RunPython(populate_regions_and_backfill, reverse_code=migrations.RunPython.noop), |
|||
migrations.AlterField( |
|||
model_name='institution', |
|||
name='region', |
|||
field=models.ForeignKey( |
|||
help_text='Geographical or administrative region of the institution.', |
|||
on_delete=django.db.models.deletion.PROTECT, |
|||
related_name='institutions', |
|||
to='profiles.region', |
|||
verbose_name='Region', |
|||
), |
|||
), |
|||
] |
|||
@ -0,0 +1,84 @@ |
|||
import random |
|||
from django.db import models |
|||
from django.utils.text import slugify |
|||
from django.utils.translation import gettext_lazy as _ |
|||
|
|||
|
|||
def get_default_region_pk(): |
|||
region = Region.get_default_region() |
|||
return region.pk if region else None |
|||
|
|||
|
|||
class Region(models.Model): |
|||
name = models.CharField( |
|||
max_length=255, |
|||
unique=True, |
|||
verbose_name=_('Region Name'), |
|||
help_text=_('Name of the geographical or administrative region (e.g. Middle East, Europe).') |
|||
) |
|||
slug = models.SlugField( |
|||
max_length=255, |
|||
unique=True, |
|||
blank=True, |
|||
null=True, |
|||
allow_unicode=True, |
|||
verbose_name=_('Slug / URL Identifier') |
|||
) |
|||
code = models.CharField( |
|||
max_length=50, |
|||
blank=True, |
|||
null=True, |
|||
verbose_name=_('Region Code'), |
|||
help_text=_('Short code such as MENA, EU, NA, etc.') |
|||
) |
|||
description = models.TextField( |
|||
blank=True, |
|||
null=True, |
|||
verbose_name=_('Description') |
|||
) |
|||
is_active = models.BooleanField( |
|||
default=True, |
|||
verbose_name=_('Is Active') |
|||
) |
|||
created_at = models.DateTimeField( |
|||
auto_now_add=True, |
|||
verbose_name=_('Created At') |
|||
) |
|||
updated_at = models.DateTimeField( |
|||
auto_now=True, |
|||
verbose_name=_('Updated At') |
|||
) |
|||
|
|||
class Meta: |
|||
ordering = ('name',) |
|||
verbose_name = _('Region') |
|||
verbose_name_plural = _('Regions') |
|||
|
|||
def __str__(self): |
|||
return self.name |
|||
|
|||
def save(self, *args, **kwargs): |
|||
if not self.slug: |
|||
base_slug = slugify(self.name, allow_unicode=True) or f"region-{random.randint(1000, 9999)}" |
|||
slug = base_slug |
|||
counter = 1 |
|||
while Region.objects.filter(slug=slug).exclude(pk=self.pk).exists(): |
|||
slug = f"{base_slug}-{counter}" |
|||
counter += 1 |
|||
self.slug = slug |
|||
super().save(*args, **kwargs) |
|||
|
|||
@classmethod |
|||
def get_default_region(cls): |
|||
region = cls.objects.filter(name="Global / Other").first() |
|||
if not region: |
|||
region, _ = cls.objects.get_or_create( |
|||
slug="global-other", |
|||
defaults={ |
|||
"name": "Global / Other", |
|||
"code": "GLB", |
|||
"description": "Default international / global region", |
|||
"is_active": True, |
|||
} |
|||
) |
|||
return region |
|||
@ -0,0 +1,256 @@ |
|||
from django.test import TestCase, RequestFactory |
|||
from django.contrib.auth import get_user_model |
|||
from rest_framework.test import APIClient |
|||
from rest_framework import status |
|||
|
|||
from apps.profiles.models import Region, Institution, InstitutionMember |
|||
from apps.profiles.admin import InstitutionAdmin, RegionAdmin |
|||
from apps.projects.models import Project |
|||
from apps.account.serializers.auth_serializers import UserMeSerializer |
|||
from utils.admin import project_admin_site |
|||
|
|||
User = get_user_model() |
|||
|
|||
|
|||
class RegionalAdminPermissionTests(TestCase): |
|||
""" |
|||
Comprehensive test suite verifying regional admin scoping and permissions: |
|||
- User model can_manage_region and has_regional_perm methods. |
|||
- Regional admin CRUD isolation for institutions (permitted within managed region, 403 outside). |
|||
- API managed_only filtering honoring admin jurisdiction. |
|||
- Project editing scoped to institution regions. |
|||
- Django Admin get_queryset and formfield_for_foreignkey scoping. |
|||
- UserMeSerializer exposure of managed_regions. |
|||
""" |
|||
|
|||
def setUp(self): |
|||
self.client = APIClient() |
|||
self.rf = RequestFactory() |
|||
|
|||
# Retrieve or create regions (Europe & Asia are pre-seeded by migration 0002) |
|||
self.region_europe, _ = Region.objects.get_or_create( |
|||
slug="europe", |
|||
defaults={"name": "Europe", "code": "EU", "description": "European Region"} |
|||
) |
|||
self.region_asia, _ = Region.objects.get_or_create( |
|||
slug="asia", |
|||
defaults={"name": "Asia", "code": "AS", "description": "Asian Region"} |
|||
) |
|||
|
|||
# Create Super Admin |
|||
self.super_admin = User.objects.create_superuser( |
|||
email="[email protected]", |
|||
password="SecurePassword123!", |
|||
fullname="Global Super Admin" |
|||
) |
|||
|
|||
# Create Regional Admin assigned to Europe only |
|||
self.eu_admin = User.objects.create_user( |
|||
email="[email protected]", |
|||
password="SecurePassword123!", |
|||
fullname="Europe Regional Admin", |
|||
user_type=User.UserType.REGIONAL_ADMIN, |
|||
is_staff=True, |
|||
regional_permissions={ |
|||
"institutions": ["view", "change", "delete"], |
|||
"projects": ["view", "change"] |
|||
} |
|||
) |
|||
self.eu_admin.managed_regions.add(self.region_europe) |
|||
|
|||
# Create Regular User |
|||
self.regular_user = User.objects.create_user( |
|||
email="[email protected]", |
|||
password="SecurePassword123!", |
|||
fullname="Regular User", |
|||
user_type=User.UserType.CLIENT |
|||
) |
|||
|
|||
# Create Institutions in Europe and Asia |
|||
self.inst_europe = Institution.objects.create( |
|||
name="Berlin Islamic Center", |
|||
slug="berlin-islamic-center", |
|||
type="mosque", |
|||
region=self.region_europe, |
|||
country="Germany", |
|||
city="Berlin", |
|||
address="Flughafenstr 43", |
|||
created_by=self.eu_admin |
|||
) |
|||
self.inst_asia = Institution.objects.create( |
|||
name="Tokyo Camii Center", |
|||
slug="tokyo-camii-center", |
|||
type="mosque", |
|||
region=self.region_asia, |
|||
country="Japan", |
|||
city="Tokyo", |
|||
address="1-19 Oyama-cho", |
|||
created_by=self.super_admin |
|||
) |
|||
|
|||
# Create Projects |
|||
self.proj_europe = Project.objects.create( |
|||
title="European Youth Conference", |
|||
owner_institution=self.inst_europe, |
|||
created_by=self.eu_admin, |
|||
status="active" |
|||
) |
|||
self.proj_asia = Project.objects.create( |
|||
title="Tokyo Intercultural Expo", |
|||
owner_institution=self.inst_asia, |
|||
created_by=self.super_admin, |
|||
status="active" |
|||
) |
|||
|
|||
# ------------------------------------------------------------------------- |
|||
# 1. Model-level jurisdiction methods |
|||
# ------------------------------------------------------------------------- |
|||
def test_user_can_manage_region(self): |
|||
# Super admin can manage all regions |
|||
self.assertTrue(self.super_admin.can_manage_region(self.region_europe)) |
|||
self.assertTrue(self.super_admin.can_manage_region(self.region_asia)) |
|||
|
|||
# Europe admin can manage Europe, but NOT Asia |
|||
self.assertTrue(self.eu_admin.can_manage_region(self.region_europe)) |
|||
self.assertFalse(self.eu_admin.can_manage_region(self.region_asia)) |
|||
|
|||
# Regular user cannot manage any region |
|||
self.assertFalse(self.regular_user.can_manage_region(self.region_europe)) |
|||
self.assertFalse(self.regular_user.can_manage_region(self.region_asia)) |
|||
|
|||
def test_user_has_regional_perm(self): |
|||
# EU admin has change permission on Europe |
|||
self.assertTrue(self.eu_admin.has_regional_perm("institutions.change", self.region_europe)) |
|||
# EU admin does NOT have permission on Asia |
|||
self.assertFalse(self.eu_admin.has_regional_perm("institutions.change", self.region_asia)) |
|||
# Super admin has all permissions on any region |
|||
self.assertTrue(self.super_admin.has_regional_perm("anything", self.region_asia)) |
|||
|
|||
# ------------------------------------------------------------------------- |
|||
# 2. Institution permission checks |
|||
# ------------------------------------------------------------------------- |
|||
def test_institution_is_admin_regional_scoping(self): |
|||
# EU admin is admin of European institution, NOT Asian institution |
|||
self.assertTrue(self.inst_europe.is_admin(self.eu_admin)) |
|||
self.assertFalse(self.inst_asia.is_admin(self.eu_admin)) |
|||
|
|||
# Super admin is admin of both |
|||
self.assertTrue(self.inst_europe.is_admin(self.super_admin)) |
|||
self.assertTrue(self.inst_asia.is_admin(self.super_admin)) |
|||
|
|||
def test_institution_edit_api_regional_scoping(self): |
|||
# Authenticate as EU admin |
|||
self.client.force_authenticate(user=self.eu_admin) |
|||
|
|||
# 1. Edit European institution -> Success (200) |
|||
res_eu = self.client.patch( |
|||
f"/api/v1/profiles/{self.inst_europe.id}/", |
|||
{"description": "Updated by EU admin"}, |
|||
format="json" |
|||
) |
|||
self.assertEqual(res_eu.status_code, status.HTTP_200_OK) |
|||
self.inst_europe.refresh_from_db() |
|||
self.assertEqual(self.inst_europe.description, "Updated by EU admin") |
|||
|
|||
# 2. Edit Asian institution -> Forbidden (403) |
|||
res_asia = self.client.patch( |
|||
f"/api/v1/profiles/{self.inst_asia.id}/", |
|||
{"description": "Attempted update outside jurisdiction"}, |
|||
format="json" |
|||
) |
|||
self.assertEqual(res_asia.status_code, status.HTTP_403_FORBIDDEN) |
|||
|
|||
def test_institution_list_managed_only_filter(self): |
|||
self.client.force_authenticate(user=self.eu_admin) |
|||
|
|||
# Query all institutions |
|||
res_all = self.client.get("/api/v1/profiles/") |
|||
self.assertEqual(res_all.status_code, status.HTTP_200_OK) |
|||
all_ids = [item["id"] for item in res_all.data["results"]] |
|||
self.assertIn(self.inst_europe.id, all_ids) |
|||
self.assertIn(self.inst_asia.id, all_ids) |
|||
|
|||
# Query with managed_only=true |
|||
res_managed = self.client.get("/api/v1/profiles/?managed_only=true") |
|||
self.assertEqual(res_managed.status_code, status.HTTP_200_OK) |
|||
managed_ids = [item["id"] for item in res_managed.data["results"]] |
|||
self.assertIn(self.inst_europe.id, managed_ids) |
|||
self.assertNotIn(self.inst_asia.id, managed_ids) |
|||
|
|||
# ------------------------------------------------------------------------- |
|||
# 3. Project Scoping |
|||
# ------------------------------------------------------------------------- |
|||
def test_project_can_user_edit_regional_scoping(self): |
|||
# EU admin can edit European project, but NOT Asian project |
|||
self.assertTrue(self.proj_europe.can_user_edit(self.eu_admin)) |
|||
self.assertFalse(self.proj_asia.can_user_edit(self.eu_admin)) |
|||
|
|||
# Super admin can edit both |
|||
self.assertTrue(self.proj_europe.can_user_edit(self.super_admin)) |
|||
self.assertTrue(self.proj_asia.can_user_edit(self.super_admin)) |
|||
|
|||
def test_project_edit_api_regional_scoping(self): |
|||
self.client.force_authenticate(user=self.eu_admin) |
|||
|
|||
# 1. Edit European project -> Success (200) |
|||
res_eu = self.client.patch( |
|||
f"/api/v1/projects/{self.proj_europe.id}/", |
|||
{"description": "Updated conference agenda"}, |
|||
format="json" |
|||
) |
|||
self.assertEqual(res_eu.status_code, status.HTTP_200_OK) |
|||
|
|||
# 2. Edit Asian project -> Forbidden (403) |
|||
res_asia = self.client.patch( |
|||
f"/api/v1/projects/{self.proj_asia.id}/", |
|||
{"description": "Unauthorized edit"}, |
|||
format="json" |
|||
) |
|||
self.assertEqual(res_asia.status_code, status.HTTP_403_FORBIDDEN) |
|||
|
|||
# ------------------------------------------------------------------------- |
|||
# 4. Django Admin Isolation |
|||
# ------------------------------------------------------------------------- |
|||
def test_admin_panel_institution_scoping(self): |
|||
admin_obj = InstitutionAdmin(Institution, project_admin_site) |
|||
|
|||
# Request by EU admin |
|||
req_eu = self.rf.get("/admin/profiles/institution/") |
|||
req_eu.user = self.eu_admin |
|||
|
|||
qs_eu = admin_obj.get_queryset(req_eu) |
|||
self.assertIn(self.inst_europe, qs_eu) |
|||
self.assertNotIn(self.inst_asia, qs_eu) |
|||
|
|||
# Request by Super Admin |
|||
req_super = self.rf.get("/admin/profiles/institution/") |
|||
req_super.user = self.super_admin |
|||
|
|||
qs_super = admin_obj.get_queryset(req_super) |
|||
self.assertIn(self.inst_europe, qs_super) |
|||
self.assertIn(self.inst_asia, qs_super) |
|||
|
|||
def test_admin_panel_foreign_key_formfield_scoping(self): |
|||
admin_obj = InstitutionAdmin(Institution, project_admin_site) |
|||
|
|||
req_eu = self.rf.get("/admin/profiles/institution/add/") |
|||
req_eu.user = self.eu_admin |
|||
|
|||
db_field = Institution._meta.get_field("region") |
|||
form_field = admin_obj.formfield_for_foreignkey(db_field, req_eu) |
|||
|
|||
# Dropdown queryset for regional admin should only contain Europe |
|||
region_ids = list(form_field.queryset.values_list("id", flat=True)) |
|||
self.assertIn(self.region_europe.id, region_ids) |
|||
self.assertNotIn(self.region_asia.id, region_ids) |
|||
|
|||
# ------------------------------------------------------------------------- |
|||
# 5. UserMeSerializer exposure |
|||
# ------------------------------------------------------------------------- |
|||
def test_user_me_serializer_exposes_managed_regions(self): |
|||
serializer = UserMeSerializer(self.eu_admin) |
|||
data = serializer.data |
|||
self.assertIn("managed_regions", data) |
|||
self.assertEqual(len(data["managed_regions"]), 1) |
|||
self.assertEqual(data["managed_regions"][0]["code"], "EU") |
|||
self.assertEqual(data["managed_regions"][0]["name"], "Europe") |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue