Browse Source

feat: implement LMS course models, serializers, views, and migrations

main
sina_sajjadi 6 days ago
parent
commit
01891790c1
  1. 8
      apps/lms/admin.py
  2. 1
      apps/lms/management/__init__.py
  3. 1
      apps/lms/management/commands/__init__.py
  4. 271
      apps/lms/management/commands/seed_online_courses.py
  5. 28
      apps/lms/migrations/0002_course_adobe_connect_url_course_course_type_and_more.py
  6. 3
      apps/lms/models/__init__.py
  7. 24
      apps/lms/models/course.py
  8. 32
      apps/lms/serializers/course_serializers.py
  9. 59
      apps/lms/tests/test_phase8_lms.py
  10. 8
      apps/lms/views/course_views.py
  11. 8
      apps/profiles/management/commands/seed_almadinah_rich_data.py

8
apps/lms/admin.py

@ -31,10 +31,10 @@ class QuizQuestionInline(StackedInline):
@admin.register(Course) @admin.register(Course)
class CourseAdmin(ModelAdmin): class CourseAdmin(ModelAdmin):
list_display = ('title', 'category', 'language', 'level', 'instructor_name', 'is_published', 'is_featured', 'created_at')
list_display_links = ('title', 'category', 'instructor_name')
list_filter = ('category', 'language', 'level', 'is_published', 'is_featured')
search_fields = ('title', 'description', 'instructor_name', 'organization_name')
list_display = ('title', 'course_type', 'category', 'language', 'level', 'instructor_name', 'is_published', 'is_featured', 'created_at')
list_display_links = ('title', 'course_type', 'category', 'instructor_name')
list_filter = ('course_type', 'category', 'language', 'level', 'is_published', 'is_featured')
search_fields = ('title', 'description', 'instructor_name', 'organization_name', 'adobe_connect_url')
prepopulated_fields = {'slug': ('title',)} prepopulated_fields = {'slug': ('title',)}
inlines = [CourseModuleInline] inlines = [CourseModuleInline]

1
apps/lms/management/__init__.py

@ -0,0 +1 @@
# LMS management package

1
apps/lms/management/commands/__init__.py

@ -0,0 +1 @@
# LMS management commands

271
apps/lms/management/commands/seed_online_courses.py

@ -0,0 +1,271 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from apps.profiles.models import Institution
from apps.lms.models import (
Course,
CourseType,
CourseCategory,
CourseLevel,
CourseLanguage,
CourseModule,
Lesson,
QuizQuestion,
)
User = get_user_model()
class Command(BaseCommand):
help = "Seeds initial sample online and hybrid courses with Adobe Connect links and live schedules."
def handle(self, *args, **options):
self.stdout.write(self.style.NOTICE("Seeding sample online courses..."))
admin_user = User.objects.filter(is_staff=True).first() or User.objects.first()
institution = Institution.objects.first()
sample_courses = [
{
"title": "Interactive Workshop: International Cultural Diplomacy & Interfaith Dialogue",
"slug": "online-cultural-diplomacy-interfaith",
"course_type": CourseType.ONLINE,
"adobe_connect_url": "https://live.astan.ir/cultural-diplomacy",
"live_schedule": "Sundays & Tuesdays at 17:00 UTC",
"category": CourseCategory.CULTURAL_DIPLOMACY,
"language": CourseLanguage.EN,
"level": CourseLevel.INTERMEDIATE,
"instructor_name": "Dr. Mohammad Javad Karimi",
"instructor_title": "Senior Fellow at Razavi Islamic Studies Institute",
"organization_name": "Razavi International Academy",
"description": (
"A high-impact live online workshop series exploring strategic communication, "
"interfaith dialogue methodologies, and cultural diplomacy across global institutions. "
"All sessions are hosted in real-time via Adobe Connect virtual classroom with interactive Q&A."
),
"duration_hours": 16.0,
"rating": 4.95,
"reviews_count": 38,
"is_published": True,
"is_featured": True,
"modules": [
{
"title": "Module 1: Foundations of Cultural Diplomacy",
"order": 1,
"lessons": [
{
"title": "Live Orientation & Syllabus Walkthrough",
"lesson_type": Lesson.LessonType.READING,
"duration_minutes": 30,
"content": (
"# Live Course Welcome & Guidelines\n\n"
"Welcome to the **International Cultural Diplomacy** live series.\n\n"
"### How to Join the Live Classroom\n"
"- The live sessions take place every **Sunday and Tuesday at 17:00 UTC**.\n"
"- Please ensure your Adobe Connect client is updated.\n"
"- Interactive microphones and webcam access will be enabled during designated Q&A segments."
),
"is_preview": True,
"order": 1,
},
{
"title": "Session 1 Live Stream Recording & Summary",
"lesson_type": Lesson.LessonType.VIDEO,
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"duration_minutes": 60,
"content": "Archived lecture notes and summary discussion points from the first live lecture.",
"is_preview": False,
"order": 2,
},
],
},
{
"title": "Module 2: Practical Interfaith Case Studies",
"order": 2,
"lessons": [
{
"title": "Live Workshop: Interfaith Crisis Management",
"lesson_type": Lesson.LessonType.ASSIGNMENT,
"duration_minutes": 90,
"content": "Collaborative case study solving during the live Adobe Connect breakout rooms.",
"is_preview": False,
"order": 1,
},
],
},
],
},
{
"title": "سیره و سبک زندگی رضوی در جامعه معاصر (دوره برخط زنده)",
"slug": "online-razavi-lifestyle-modern-society",
"course_type": CourseType.ONLINE,
"adobe_connect_url": "https://live.astan.ir/razavi-ethics",
"live_schedule": "پنجشنبه‌ها ساعت ۱۸:۳۰ به وقت تهران",
"category": CourseCategory.RAZAVI_TEACHINGS,
"language": CourseLanguage.FA,
"level": CourseLevel.INTRODUCTORY,
"instructor_name": "استاد سید علیرضا حسینی",
"instructor_title": "مدرس حوزه و پژوهشگر سیره و معارف اهل‌بیت (ع)",
"organization_name": "مرکز ارتباطات بین‌الملل آستان قدس رضوی",
"description": (
"دوره تعاملی و برخط پیرامون آموزه‌های کاربردی امام رضا (ع) در مدیریت خانواده، اخلاق حرفه‌ای، "
"و مسئولیت اجتماعی در بستر کلاس مجازی ادوبی کانکت با پاسخگویی زنده به پرسش‌های دانشجویان."
),
"duration_hours": 10.0,
"rating": 5.0,
"reviews_count": 64,
"is_published": True,
"is_featured": True,
"modules": [
{
"title": "فصل اول: مبانی اخلاق گفت‌وگو و مدارا در سیره امام رضا (ع)",
"order": 1,
"lessons": [
{
"title": "راهنمای ورود به کلاس مجازی ادوبی کانکت و تقویم جلسات",
"lesson_type": Lesson.LessonType.READING,
"duration_minutes": 20,
"content": (
"# راهنمای شرکت در جلسات برخط\n\n"
"دانش‌پژوهان گرامی، کلاس‌های زنده هفتگی از طریق لینک ادوبی کانکت اختصاصی دوره برگزار می‌گردد.\n"
"جهت شرکت در مباحث، پس از ورود به سامانه ادوبی، با نام کامل خود وارد اتاق جلسه شوید."
),
"is_preview": True,
"order": 1,
},
{
"title": "جلسه زنده اول: مناظرات رضوی و درس‌های آن برای دنیای امروز",
"lesson_type": Lesson.LessonType.READING,
"duration_minutes": 75,
"content": "متن و نکات کلیدی جلسه برخط اول همراه با فایل اسلایدها و منابع تکمیلی.",
"is_preview": False,
"order": 2,
},
],
},
],
},
{
"title": "Advanced Islamic Center & Community Leadership (Live Cohort)",
"slug": "online-center-community-leadership",
"course_type": CourseType.ONLINE,
"adobe_connect_url": "https://live.astan.ir/center-leadership",
"live_schedule": "Every Saturday at 14:00 GMT",
"category": CourseCategory.CENTER_MANAGEMENT,
"language": CourseLanguage.EN,
"level": CourseLevel.ADVANCED,
"instructor_name": "Sheikh Bilal Al-Kazemi",
"instructor_title": "Director of International Community Engagement",
"organization_name": "Federation of Global Islamic Centers",
"description": (
"A specialized leadership masterclass for center directors, mosque trustees, and community organizers. "
"Features live panel discussions, strategic planning exercises, and real-time governance case studies."
),
"duration_hours": 18.0,
"rating": 4.88,
"reviews_count": 29,
"is_published": True,
"is_featured": False,
"modules": [
{
"title": "Module 1: Strategic Governance and Crisis Leadership",
"order": 1,
"lessons": [
{
"title": "Cohort Onboarding & Live Room Setup",
"lesson_type": Lesson.LessonType.READING,
"duration_minutes": 25,
"content": (
"# Live Cohort Setup\n\n"
"Sessions convene every Saturday at 14:00 GMT via Adobe Connect.\n"
"Please review the community governance code before attending Session 1."
),
"is_preview": True,
"order": 1,
},
],
},
],
},
{
"title": "Comprehensive Quranic Arabic for Researchers (Hybrid Course)",
"slug": "hybrid-quranic-arabic-researchers",
"course_type": CourseType.HYBRID,
"adobe_connect_url": "https://live.astan.ir/quranic-arabic",
"live_schedule": "Mondays & Wednesdays at 19:00 Mecca Time",
"category": CourseCategory.SPECIALIZED_LANGUAGES,
"language": CourseLanguage.AR,
"level": CourseLevel.INTERMEDIATE,
"instructor_name": "د. طارق الهاشمي",
"instructor_title": "أستاذ اللغويات والترجمة التخصصية",
"organization_name": "معهد الدراسات اللغوية التخصصية",
"description": (
"دورة هجينة تجمع بين الدروس المسجلة الذاتية وورش العمل الحية الأسبوعية عبر أدوبي كونكت "
"لتحليل النصوص الدينية والمصطلحات القرآنية التخصصية."
),
"duration_hours": 24.0,
"rating": 4.92,
"reviews_count": 45,
"is_published": True,
"is_featured": True,
"modules": [
{
"title": "الوحدة الأولى: البنية الصرفية والنحوية المتقدمة",
"order": 1,
"lessons": [
{
"title": "مقدمة البرنامج وجدول البث المباشر",
"lesson_type": Lesson.LessonType.READING,
"duration_minutes": 20,
"content": (
"# جدول الجلسات التفاعلية المباشرة\n\n"
"تنعقد الورش الحية يومي الإثنين والأربعاء الساعة 19:00 بتوقيت مكة المكرمة عبر قاعة أدوبي كونكت."
),
"is_preview": True,
"order": 1,
},
],
},
],
},
]
created_count = 0
for data in sample_courses:
modules_data = data.pop("modules", [])
slug = data["slug"]
course, created = Course.objects.update_or_create(
slug=slug,
defaults={
**data,
"institution": institution,
"created_by": admin_user,
}
)
if created:
created_count += 1
self.stdout.write(self.style.SUCCESS(f"Created course: {course.title} ({course.course_type})"))
else:
self.stdout.write(self.style.WARNING(f"Updated course: {course.title} ({course.course_type})"))
# Create modules and lessons
for m_data in modules_data:
lessons_data = m_data.pop("lessons", [])
module, _ = CourseModule.objects.update_or_create(
course=course,
order=m_data["order"],
defaults={"title": m_data["title"]},
)
for l_data in lessons_data:
Lesson.objects.update_or_create(
module=module,
order=l_data["order"],
defaults=l_data,
)
self.stdout.write(
self.style.SUCCESS(
f"Successfully seeded {len(sample_courses)} online/hybrid courses (New: {created_count})!"
)
)

28
apps/lms/migrations/0002_course_adobe_connect_url_course_course_type_and_more.py

@ -0,0 +1,28 @@
# Generated by Django 4.2.30 on 2026-10-22 06:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lms', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='course',
name='adobe_connect_url',
field=models.URLField(blank=True, max_length=500, null=True, verbose_name='Adobe Connect / Online Meeting Link'),
),
migrations.AddField(
model_name='course',
name='course_type',
field=models.CharField(choices=[('recorded', 'Recorded / Self-Paced'), ('online', 'Live Online (Adobe Connect)'), ('hybrid', 'Hybrid (Recorded & Live Online)')], default='recorded', max_length=20, verbose_name='Course Delivery Type'),
),
migrations.AddField(
model_name='course',
name='live_schedule',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Live Schedule (e.g. Saturdays at 18:00 UTC)'),
),
]

3
apps/lms/models/__init__.py

@ -1,5 +1,6 @@
from apps.lms.models.course import ( from apps.lms.models.course import (
Course, Course,
CourseType,
CourseModule, CourseModule,
Lesson, Lesson,
QuizQuestion, QuizQuestion,
@ -13,6 +14,7 @@ from apps.lms.models.course import (
__all__ = [ __all__ = [
'Course', 'Course',
'CourseType',
'CourseModule', 'CourseModule',
'Lesson', 'Lesson',
'QuizQuestion', 'QuizQuestion',
@ -23,3 +25,4 @@ __all__ = [
'CourseLevel', 'CourseLevel',
'CourseLanguage', 'CourseLanguage',
] ]

24
apps/lms/models/course.py

@ -31,6 +31,12 @@ class CourseLanguage(models.TextChoices):
FR = 'fr', _('French') FR = 'fr', _('French')
class CourseType(models.TextChoices):
RECORDED = 'recorded', _('Recorded / Self-Paced')
ONLINE = 'online', _('Live Online (Adobe Connect)')
HYBRID = 'hybrid', _('Hybrid (Recorded & Live Online)')
class Course(models.Model): class Course(models.Model):
title = models.CharField(max_length=255, verbose_name=_('Course Title')) title = models.CharField(max_length=255, verbose_name=_('Course Title'))
slug = models.SlugField( slug = models.SlugField(
@ -41,6 +47,24 @@ class Course(models.Model):
allow_unicode=True, allow_unicode=True,
verbose_name=_('Slug / URL Identifier') verbose_name=_('Slug / URL Identifier')
) )
course_type = models.CharField(
max_length=20,
choices=CourseType.choices,
default=CourseType.RECORDED,
verbose_name=_('Course Delivery Type')
)
adobe_connect_url = models.URLField(
max_length=500,
blank=True,
null=True,
verbose_name=_('Adobe Connect / Online Meeting Link')
)
live_schedule = models.CharField(
max_length=255,
blank=True,
null=True,
verbose_name=_('Live Schedule (e.g. Saturdays at 18:00 UTC)')
)
category = models.CharField( category = models.CharField(
max_length=50, max_length=50,
choices=CourseCategory.choices, choices=CourseCategory.choices,

32
apps/lms/serializers/course_serializers.py

@ -102,9 +102,9 @@ class CourseListSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Course model = Course
fields = [ fields = [
'id', 'title', 'slug', 'category', 'language',
'level', 'cover_image', 'instructor_name',
'instructor_title', 'instructor_avatar',
'id', 'title', 'slug', 'course_type', 'live_schedule',
'category', 'language', 'level', 'cover_image',
'instructor_name', 'instructor_title', 'instructor_avatar',
'organization_name', 'duration_hours', 'rating', 'organization_name', 'duration_hours', 'rating',
'reviews_count', 'is_featured', 'total_lessons', 'reviews_count', 'is_featured', 'total_lessons',
'total_students', 'is_enrolled', 'progress_percentage', 'total_students', 'is_enrolled', 'progress_percentage',
@ -134,11 +134,13 @@ class CourseDetailSerializer(serializers.ModelSerializer):
is_enrolled = serializers.SerializerMethodField() is_enrolled = serializers.SerializerMethodField()
progress_percentage = serializers.SerializerMethodField() progress_percentage = serializers.SerializerMethodField()
certificate_code = serializers.SerializerMethodField() certificate_code = serializers.SerializerMethodField()
adobe_connect_url = serializers.SerializerMethodField()
class Meta: class Meta:
model = Course model = Course
fields = [ fields = [
'id', 'title', 'slug', 'category', 'language',
'id', 'title', 'slug', 'course_type', 'live_schedule',
'adobe_connect_url', 'category', 'language',
'level', 'cover_image', 'instructor_name', 'level', 'cover_image', 'instructor_name',
'instructor_title', 'instructor_avatar', 'instructor_title', 'instructor_avatar',
'institution', 'organization_name', 'description', 'institution', 'organization_name', 'description',
@ -172,12 +174,32 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return cert.certificate_code if cert else None return cert.certificate_code if cert else None
return None return None
@extend_schema_field(OpenApiTypes.STR)
def get_adobe_connect_url(self, obj):
"""Only enrolled students, course creator, or admins can see the Adobe Connect link."""
if not obj.adobe_connect_url:
return None
request = self.context.get('request')
if not request or not request.user or not request.user.is_authenticated:
return None
user = request.user
if getattr(user, 'is_super_admin', False) or user.is_staff:
return obj.adobe_connect_url
if obj.created_by == user:
return obj.adobe_connect_url
if obj.institution and obj.institution.is_editor(user):
return obj.adobe_connect_url
if CourseEnrollment.objects.filter(user=user, course=obj).exists():
return obj.adobe_connect_url
return None
class CourseCreateUpdateSerializer(serializers.ModelSerializer): class CourseCreateUpdateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Course model = Course
fields = [ fields = [
'id', 'title', 'category', 'language', 'level',
'id', 'title', 'course_type', 'adobe_connect_url', 'live_schedule',
'category', 'language', 'level',
'cover_image', 'instructor_name', 'instructor_title', 'cover_image', 'instructor_name', 'instructor_title',
'instructor_avatar', 'institution', 'organization_name', 'instructor_avatar', 'institution', 'organization_name',
'description', 'duration_hours', 'is_published', 'is_featured', 'description', 'duration_hours', 'is_published', 'is_featured',

59
apps/lms/tests/test_phase8_lms.py

@ -6,6 +6,7 @@ from rest_framework import status
from apps.profiles.models import Institution from apps.profiles.models import Institution
from apps.lms.models import ( from apps.lms.models import (
Course, Course,
CourseType,
CourseModule, CourseModule,
Lesson, Lesson,
QuizQuestion, QuizQuestion,
@ -294,3 +295,61 @@ class Phase8LMSAPITests(TestCase):
self.assertEqual(my_certs_res.status_code, status.HTTP_200_OK) self.assertEqual(my_certs_res.status_code, status.HTTP_200_OK)
self.assertEqual(len(my_certs_res.data), 1) self.assertEqual(len(my_certs_res.data), 1)
self.assertEqual(my_certs_res.data[0]["certificate_code"], cert_code) self.assertEqual(my_certs_res.data[0]["certificate_code"], cert_code)
def test_online_course_adobe_connect_flow(self):
"""
Verify:
1. Course can be created with course_type=online, adobe_connect_url, and live_schedule.
2. Non-enrolled user cannot see adobe_connect_url (returns None).
3. Enrolling in course returns the adobe_connect_url in response.
4. Enrolled user can now see adobe_connect_url in course details.
5. Filtering by course_type=online works.
"""
online_course = Course.objects.create(
title="Live Webinar on Islamic Jurisprudence",
course_type=CourseType.ONLINE,
adobe_connect_url="https://live.razaviacademy.org/fiqh-live",
live_schedule="Every Friday at 16:00 UTC",
category=CourseCategory.GENERAL,
language=CourseLanguage.EN,
level=CourseLevel.ADVANCED,
instructor_name="Ayatollah Mohammadi",
institution=self.institution,
description="Live interactive lectures via Adobe Connect virtual classroom.",
is_published=True,
created_by=self.admin_user,
)
# 1. Unauthenticated or non-enrolled user cannot see the Adobe Connect URL
anon_client = APIClient()
detail_res_anon = anon_client.get(f"/api/v1/lms/courses/{online_course.slug}/")
self.assertEqual(detail_res_anon.status_code, status.HTTP_200_OK)
self.assertEqual(detail_res_anon.data["course_type"], "online")
self.assertEqual(detail_res_anon.data["live_schedule"], "Every Friday at 16:00 UTC")
self.assertIsNone(detail_res_anon.data["adobe_connect_url"])
# Authenticated student before enrollment
self.client.force_authenticate(user=self.student_user)
detail_res_student = self.client.get(f"/api/v1/lms/courses/{online_course.slug}/")
self.assertEqual(detail_res_student.status_code, status.HTTP_200_OK)
self.assertIsNone(detail_res_student.data["adobe_connect_url"])
# 2. Student enrolls in the online course
enroll_res = self.client.post(f"/api/v1/lms/courses/{online_course.slug}/enroll/")
self.assertEqual(enroll_res.status_code, status.HTTP_201_CREATED)
self.assertEqual(enroll_res.data["course_type"], "online")
self.assertEqual(enroll_res.data["adobe_connect_url"], "https://live.razaviacademy.org/fiqh-live")
self.assertEqual(enroll_res.data["live_schedule"], "Every Friday at 16:00 UTC")
# 3. After enrollment, student can access the Adobe Connect URL in course details
detail_res_enrolled = self.client.get(f"/api/v1/lms/courses/{online_course.slug}/")
self.assertEqual(detail_res_enrolled.status_code, status.HTTP_200_OK)
self.assertTrue(detail_res_enrolled.data["is_enrolled"])
self.assertEqual(detail_res_enrolled.data["adobe_connect_url"], "https://live.razaviacademy.org/fiqh-live")
# 4. Catalog filtering by course_type
list_res = self.client.get("/api/v1/lms/courses/?course_type=online")
self.assertEqual(list_res.status_code, status.HTTP_200_OK)
self.assertTrue(any(c["id"] == online_course.id for c in list_res.data))
self.assertTrue(all(c["course_type"] == "online" for c in list_res.data))

8
apps/lms/views/course_views.py

@ -55,6 +55,7 @@ class CourseListCreateView(GenericAPIView):
description="Retrieves a list of published courses with category, language, level, and keyword filtering.", description="Retrieves a list of published courses with category, language, level, and keyword filtering.",
parameters=[ parameters=[
OpenApiParameter('category', str, description="Filter by course category (e.g. razavi_teachings, cultural_diplomacy)"), OpenApiParameter('category', str, description="Filter by course category (e.g. razavi_teachings, cultural_diplomacy)"),
OpenApiParameter('course_type', str, description="Filter by delivery type (recorded, online, hybrid)"),
OpenApiParameter('language', str, description="Filter by language code (fa, ar, en, ur, fr)"), OpenApiParameter('language', str, description="Filter by language code (fa, ar, en, ur, fr)"),
OpenApiParameter('level', str, description="Filter by difficulty level (introductory, intermediate, advanced)"), OpenApiParameter('level', str, description="Filter by difficulty level (introductory, intermediate, advanced)"),
OpenApiParameter('search', str, description="Keyword search in title, instructor, or description"), OpenApiParameter('search', str, description="Keyword search in title, instructor, or description"),
@ -69,6 +70,10 @@ class CourseListCreateView(GenericAPIView):
if not (request.user and request.user.is_authenticated and request.user.is_staff): if not (request.user and request.user.is_authenticated and request.user.is_staff):
qs = qs.filter(is_published=True) qs = qs.filter(is_published=True)
course_type = request.query_params.get('course_type')
if course_type:
qs = qs.filter(course_type=course_type)
category = request.query_params.get('category') category = request.query_params.get('category')
if category: if category:
qs = qs.filter(category=category) qs = qs.filter(category=category)
@ -201,6 +206,9 @@ class CourseEnrollView(GenericAPIView):
'message': _("Enrolled successfully.") if created else _("Already enrolled in this course."), 'message': _("Enrolled successfully.") if created else _("Already enrolled in this course."),
'course_id': course.id, 'course_id': course.id,
'course_title': course.title, 'course_title': course.title,
'course_type': course.course_type,
'adobe_connect_url': course.adobe_connect_url,
'live_schedule': course.live_schedule,
'enrolled_at': enrollment.enrolled_at, 'enrolled_at': enrollment.enrolled_at,
'progress_percentage': enrollment.progress_percentage, 'progress_percentage': enrollment.progress_percentage,
}, status=status_code) }, status=status_code)

8
apps/profiles/management/commands/seed_almadinah_rich_data.py

@ -287,12 +287,12 @@ class Command(BaseCommand):
inst.save() inst.save()
self.stdout.write(self.style.SUCCESS(f"Updated institution profile: {inst.name} (ID: {inst.id})")) self.stdout.write(self.style.SUCCESS(f"Updated institution profile: {inst.name} (ID: {inst.id})"))
# Ensure members are assigned for permissions
for u in User.objects.all():
# Ensure designated admin is assigned for permissions
if admin_user:
InstitutionMember.objects.get_or_create( InstitutionMember.objects.get_or_create(
institution=inst, institution=inst,
user=u,
defaults={"role": InstitutionMember.MemberRole.ADMIN, "title": "Staff / Researcher"}
user=admin_user,
defaults={"role": InstitutionMember.MemberRole.ADMIN, "title": "Director of Administration"}
) )
# Populate Timeline entries # Populate Timeline entries

Loading…
Cancel
Save