Browse Source

courses can have multi professors update

master
Mohsen Taba 1 month ago
parent
commit
1aba40c433
  1. 8
      apps/account/admin/user.py
  2. 4
      apps/account/models/user.py
  3. 2
      apps/account/views/user.py
  4. 2
      apps/api/views/admin_dashboard.py
  5. 16
      apps/api/views/professor_dashboard.py
  6. 2
      apps/certificate/views.py
  7. 10
      apps/chat/admin.py
  8. 2
      apps/chat/admin_views.py
  9. 2
      apps/course/access.py
  10. 14
      apps/course/admin/course.py
  11. 6
      apps/course/admin/professor_base.py
  12. 4
      apps/course/management/commands/ensure_course_rooms.py
  13. 23
      apps/course/migrations/0016_remove_course_professor_course_professors.py
  14. 5
      apps/course/models/course.py
  15. 49
      apps/course/serializers/admin.py
  16. 19
      apps/course/serializers/course.py
  17. 34
      apps/course/signals.py
  18. 24
      apps/course/views/admin.py
  19. 15
      apps/course/views/course.py
  20. 5
      apps/course/views/professor.py
  21. 4
      apps/quiz/admin/quiz.py
  22. 8
      apps/quiz/views/admin.py
  23. 2
      apps/transaction/views.py

8
apps/account/admin/user.py

@ -211,7 +211,7 @@ class StudentParticipantInline(StackedInline):
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.select_related('course', 'course__professor')
return qs.select_related('course').prefetch_related('course__professors')
@admin.display(description=_('Course Status')) @admin.display(description=_('Course Status'))
def course_status(self, obj): def course_status(self, obj):
@ -221,8 +221,10 @@ class StudentParticipantInline(StackedInline):
@admin.display(description=_('Professor')) @admin.display(description=_('Professor'))
def course_professor(self, obj): def course_professor(self, obj):
if obj.course and obj.course.professor:
return obj.course.professor.fullname or obj.course.professor.email
if obj.course:
professors = obj.course.professors.all()
if professors:
return ", ".join(p.fullname or p.email for p in professors)
return '-' return '-'
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):

4
apps/account/models/user.py

@ -261,7 +261,6 @@ class User(AbstractUser):
self.is_admin_panel_user() or self.is_admin_panel_user() or
self.is_professor_panel_user() self.is_professor_panel_user()
) )
def can_enroll_course(self): def can_enroll_course(self):
"""آیا می‌تواند در دوره ثبت‌نام کند؟""" """آیا می‌تواند در دوره ثبت‌نام کند؟"""
return True # همه می‌توانند دانش‌آموز باشند return True # همه می‌توانند دانش‌آموز باشند
@ -273,7 +272,7 @@ class User(AbstractUser):
return True return True
# اولویت دوم: professor - فقط دوره‌های خودش # اولویت دوم: professor - فقط دوره‌های خودش
if course and self.has_role('professor'): if course and self.has_role('professor'):
return course.professor == self
return course.professors.filter(id=self.id).exists()
return False return False
def ensure_professor_profile(self, commit: bool = True) -> bool: def ensure_professor_profile(self, commit: bool = True) -> bool:
@ -305,7 +304,6 @@ class User(AbstractUser):
def _ensure_professor_slug(self): def _ensure_professor_slug(self):
if self.slug: if self.slug:
return return
base_candidates = [ base_candidates = [
self.fullname, self.fullname,
(self.email.split('@')[0] if self.email else None), (self.email.split('@')[0] if self.email else None),

2
apps/account/views/user.py

@ -661,7 +661,7 @@ class AdminUserViewSet(ModelViewSet):
if self.request.user.user_type == 'professor': if self.request.user.user_type == 'professor':
queryset = queryset.filter( queryset = queryset.filter(
Q(user_type='student') | Q(user_type='client'), Q(user_type='student') | Q(user_type='client'),
participated_courses__course__professor_id=self.request.user.id
participated_courses__course__professors=self.request.user
) )
# Handle Search # Handle Search

2
apps/api/views/admin_dashboard.py

@ -115,7 +115,7 @@ class AdminDashboardStatsView(APIView):
{ {
"id": c.id, "id": c.id,
"title": extract_text_from_json(c.title), "title": extract_text_from_json(c.title),
"professor": c.professor.fullname if c.professor else "Unknown",
"professor": ", ".join(p.fullname for p in c.professors.all()) or "Unknown",
"participants": c.participant_count "participants": c.participant_count
} for c in top_courses_qs } for c in top_courses_qs
] ]

16
apps/api/views/professor_dashboard.py

@ -27,20 +27,20 @@ class ProfessorDashboardStatsView(APIView):
# 1. CARDS & BASIC STATS # 1. CARDS & BASIC STATS
# --------------------------------------------------------- # ---------------------------------------------------------
# Total courses taught by this professor # Total courses taught by this professor
courses_count = Course.objects.filter(professor=request.user).count()
courses_count = Course.objects.filter(professors=request.user).count()
# Total unique active students enrolled in this professor's courses # Total unique active students enrolled in this professor's courses
students_count = Participant.objects.filter( students_count = Participant.objects.filter(
course__professor=request.user,
course__professors=request.user,
is_active=True is_active=True
).values('student').distinct().count() ).values('student').distinct().count()
# Total quizzes created in this professor's courses # Total quizzes created in this professor's courses
quizzes_count = Quiz.objects.filter(course__professor=request.user).count()
quizzes_count = Quiz.objects.filter(course__professors=request.user).count()
# Chat rooms count (group rooms for professor's courses + private chats with professor) # Chat rooms count (group rooms for professor's courses + private chats with professor)
chat_rooms_count = RoomMessage.objects.filter( chat_rooms_count = RoomMessage.objects.filter(
Q(course__professor=request.user) |
Q(course__professors=request.user) |
(Q(room_type=RoomMessage.RoomTypeChoices.PRIVATE) & (Q(initiator=request.user) | Q(recipient=request.user))) (Q(room_type=RoomMessage.RoomTypeChoices.PRIVATE) & (Q(initiator=request.user) | Q(recipient=request.user)))
).distinct().count() ).distinct().count()
@ -49,7 +49,7 @@ class ProfessorDashboardStatsView(APIView):
# --------------------------------------------------------- # ---------------------------------------------------------
# Chat Volume Trend (30 days) # Chat Volume Trend (30 days)
professor_rooms = RoomMessage.objects.filter( professor_rooms = RoomMessage.objects.filter(
Q(course__professor=request.user) |
Q(course__professors=request.user) |
(Q(room_type=RoomMessage.RoomTypeChoices.PRIVATE) & (Q(initiator=request.user) | Q(recipient=request.user))) (Q(room_type=RoomMessage.RoomTypeChoices.PRIVATE) & (Q(initiator=request.user) | Q(recipient=request.user)))
) )
chat_volume = ChatMessage.objects.filter( chat_volume = ChatMessage.objects.filter(
@ -62,7 +62,7 @@ class ProfessorDashboardStatsView(APIView):
chat_chart = [{"date": item['date'].strftime('%Y-%m-%d'), "messages": item['count']} for item in chat_volume] chat_chart = [{"date": item['date'].strftime('%Y-%m-%d'), "messages": item['count']} for item in chat_volume]
# Course Efficacy: Completed vs Enrolled (Bar Chart for Top 5 Courses) # Course Efficacy: Completed vs Enrolled (Bar Chart for Top 5 Courses)
top_courses_qs = Course.objects.filter(professor=request.user).annotate(
top_courses_qs = Course.objects.filter(professors=request.user).annotate(
participant_count=Count('participants') participant_count=Count('participants')
).order_by('-participant_count')[:5] ).order_by('-participant_count')[:5]
@ -81,7 +81,7 @@ class ProfessorDashboardStatsView(APIView):
}) })
# Quiz Participation statistics (all quizzes of the professor) # Quiz Participation statistics (all quizzes of the professor)
quizzes = Quiz.objects.filter(course__professor=request.user)
quizzes = Quiz.objects.filter(course__professors=request.user)
quizzes_stats = [] quizzes_stats = []
for quiz in quizzes: for quiz in quizzes:
course = quiz.course course = quiz.course
@ -112,7 +112,7 @@ class ProfessorDashboardStatsView(APIView):
# Leaderboard (Top 5 Performing Students in professor's quizzes) # Leaderboard (Top 5 Performing Students in professor's quizzes)
top_students = QuizParticipant.objects.filter( top_students = QuizParticipant.objects.filter(
quiz__course__professor=request.user
quiz__course__professors=request.user
).select_related('user').order_by('-total_score', 'total_timing')[:5] ).select_related('user').order_by('-total_score', 'total_timing')[:5]
leaderboard = [ leaderboard = [

2
apps/certificate/views.py

@ -98,7 +98,7 @@ class AdminCertificateViewSet(ModelViewSet):
# Professors can only see certificates for students in their courses # Professors can only see certificates for students in their courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
# Search query # Search query
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)

10
apps/chat/admin.py

@ -32,9 +32,9 @@ def get_allowed_users_for_room(room):
allowed_ids.add(room.recipient_id) allowed_ids.add(room.recipient_id)
if room.room_type == 'group' and room.course_id: if room.room_type == 'group' and room.course_id:
# Add Professor
if room.course.professor_id:
allowed_ids.add(room.course.professor_id)
# Add all Professors
professor_ids = room.course.professors.values_list('id', flat=True)
allowed_ids.update(professor_ids)
# Add Active Participants # Add Active Participants
from apps.course.models import Participant from apps.course.models import Participant
@ -67,7 +67,7 @@ def get_rooms_queryset_for_user(user, queryset):
# اساتید و دانشجویان: چت‌های خودشان + چت‌های گروهی که در آن عضو هستند # اساتید و دانشجویان: چت‌های خودشان + چت‌های گروهی که در آن عضو هستند
return queryset.filter( return queryset.filter(
personal_q | personal_q |
Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__professor=user) |
Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__professors=user) |
Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__participants__student=user, course__participants__is_active=True) Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__participants__student=user, course__participants__is_active=True)
).distinct() ).distinct()
@ -305,7 +305,7 @@ class ChatMessageAdmin(ModelAdmin):
# اساتید و دانشجویان # اساتید و دانشجویان
return queryset.filter( return queryset.filter(
personal_q | personal_q |
Q(room__room_type=RoomMessage.RoomTypeChoices.GROUP, room__course__professor=user) |
Q(room__room_type=RoomMessage.RoomTypeChoices.GROUP, room__course__professors=user) |
Q(room__room_type=RoomMessage.RoomTypeChoices.GROUP, room__course__participants__student=user, room__course__participants__is_active=True) Q(room__room_type=RoomMessage.RoomTypeChoices.GROUP, room__course__participants__student=user, room__course__participants__is_active=True)
).distinct() ).distinct()

2
apps/chat/admin_views.py

@ -57,7 +57,7 @@ def api_get_rooms(request):
else: else:
rooms = queryset.filter( rooms = queryset.filter(
personal_q | personal_q |
Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__professor=user) |
Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__professors=user) |
Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__participants__student=user, course__participants__is_active=True) Q(room_type=RoomMessage.RoomTypeChoices.GROUP, course__participants__student=user, course__participants__is_active=True)
).distinct()[:50] ).distinct()[:50]

2
apps/course/access.py

@ -8,7 +8,7 @@ def user_has_course_access(user, course):
if user.is_staff or user.is_superuser: if user.is_staff or user.is_superuser:
return True return True
if course.professor_id == user.id:
if course.professors.filter(id=user.id).exists():
return True return True
return Participant.objects.filter( return Participant.objects.filter(

14
apps/course/admin/course.py

@ -270,7 +270,7 @@ class CourseAdmin(DirectCourseAdmin):
search_fields = ('id','title', 'description') search_fields = ('id','title', 'description')
exclude = ('slug', ) exclude = ('slug', )
readonly_fields = ('final_price', 'final_price_rub') readonly_fields = ('final_price', 'final_price_rub')
autocomplete_fields = ('category', 'professor',)
autocomplete_fields = ('category', 'professors',)
list_filter_submit = True list_filter_submit = True
change_form_show_cancel_button = True change_form_show_cancel_button = True
@ -291,7 +291,7 @@ class CourseAdmin(DirectCourseAdmin):
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('title', 'category', 'professor', 'thumbnail', 'description', 'short_description')
'fields': ('title', 'category', 'professors', 'thumbnail', 'description', 'short_description')
}), }),
(_('Settings & Status'), { (_('Settings & Status'), {
'fields': ( 'fields': (
@ -340,7 +340,7 @@ class CourseAdmin(DirectCourseAdmin):
@display(description=_("Professor")) @display(description=_("Professor"))
def display_professor(self, instance): def display_professor(self, instance):
return instance.professor.fullname
return ", ".join([p.fullname for p in instance.professors.all()])
@display(description=_("Price")) @display(description=_("Price"))
def display_price(self, instance): def display_price(self, instance):
@ -433,10 +433,10 @@ class GlossaryAdmin(AttachmentGlossaryBaseAdmin):
ordering = ('-id',) ordering = ('-id',)
def is_used_in_professor_courses(self, user, obj): def is_used_in_professor_courses(self, user, obj):
return obj.courseglossary_set.filter(course__professor=user).exists()
return obj.courseglossary_set.filter(course__professors=user).exists()
def filter_by_professor_usage(self, user, queryset): def filter_by_professor_usage(self, user, queryset):
return queryset.filter(courseglossary__course__professor=user).distinct()
return queryset.filter(courseglossary__course__professors=user).distinct()
class CourseGlossaryAdmin(CourseRelatedAdmin): class CourseGlossaryAdmin(CourseRelatedAdmin):
@ -495,10 +495,10 @@ class AttachmentAdmin(AttachmentGlossaryBaseAdmin):
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
def is_used_in_professor_courses(self, user, obj): def is_used_in_professor_courses(self, user, obj):
return obj.courseattachment_set.filter(course__professor=user).exists()
return obj.courseattachment_set.filter(course__professors=user).exists()
def filter_by_professor_usage(self, user, queryset): def filter_by_professor_usage(self, user, queryset):
return queryset.filter(courseattachment__course__professor=user).distinct()
return queryset.filter(courseattachment__course__professors=user).distinct()
class CourseAttachmentAdmin(CourseRelatedAdmin): class CourseAttachmentAdmin(CourseRelatedAdmin):

6
apps/course/admin/professor_base.py

@ -121,7 +121,7 @@ class CourseRelatedAdmin(ProfessorBaseAdmin):
def filter_queryset_for_professor(self, request, queryset): def filter_queryset_for_professor(self, request, queryset):
"""فیلتر کردن بر اساس دوره‌های استاد""" """فیلتر کردن بر اساس دوره‌های استاد"""
return queryset.filter(course__professor=request.user)
return queryset.filter(course__professors=request.user)
def get_course_from_object(self, obj): def get_course_from_object(self, obj):
"""دریافت Course از object""" """دریافت Course از object"""
@ -140,7 +140,7 @@ class DirectCourseAdmin(ProfessorBaseAdmin):
def filter_queryset_for_professor(self, request, queryset): def filter_queryset_for_professor(self, request, queryset):
"""فقط دوره‌های خود استاد""" """فقط دوره‌های خود استاد"""
return queryset.filter(professor=request.user)
return queryset.filter(professors=request.user)
class AttachmentGlossaryBaseAdmin(ProfessorBaseAdmin): class AttachmentGlossaryBaseAdmin(ProfessorBaseAdmin):
@ -178,4 +178,4 @@ class CertificateBaseAdmin(ProfessorBaseAdmin):
def filter_queryset_for_professor(self, request, queryset): def filter_queryset_for_professor(self, request, queryset):
"""فقط certificate های دانش‌آموزان دوره‌های استاد""" """فقط certificate های دانش‌آموزان دوره‌های استاد"""
return queryset.filter(course__professor=request.user)
return queryset.filter(course__professors=request.user)

4
apps/course/management/commands/ensure_course_rooms.py

@ -26,7 +26,7 @@ class Command(BaseCommand):
room_type=RoomMessage.RoomTypeChoices.GROUP, room_type=RoomMessage.RoomTypeChoices.GROUP,
) )
courses_without_group_room = Course.objects.select_related('professor').annotate(
courses_without_group_room = Course.objects.prefetch_related('professors').annotate(
has_group_room=Exists(group_room_exists) has_group_room=Exists(group_room_exists)
).filter( ).filter(
has_group_room=False has_group_room=False
@ -53,7 +53,7 @@ class Command(BaseCommand):
RoomMessage.objects.create( RoomMessage.objects.create(
name=f"{course.title} - Group", name=f"{course.title} - Group",
description=f"Group chat for course: {course.title}", description=f"Group chat for course: {course.title}",
initiator=course.professor,
initiator=course.professors.first(),
course=course, course=course,
room_type=RoomMessage.RoomTypeChoices.GROUP, room_type=RoomMessage.RoomTypeChoices.GROUP,
is_locked=course.is_group_chat_locked, is_locked=course.is_group_chat_locked,

23
apps/course/migrations/0016_remove_course_professor_course_professors.py

@ -0,0 +1,23 @@
# Generated by Django 5.2.12 on 2026-06-17 10:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0005_user_password_enc'),
('course', '0015_rename_course_cour_ended_a_0f47b4_idx_course_cour_ended_a_32eaaa_idx_and_more'),
]
operations = [
migrations.RemoveField(
model_name='course',
name='professor',
),
migrations.AddField(
model_name='course',
name='professors',
field=models.ManyToManyField(related_name='courses', to='account.professoruser', verbose_name='Professors'),
),
]

5
apps/course/models/course.py

@ -333,11 +333,10 @@ class Course(models.Model):
title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Course Title')) title = models.JSONField(default=list, null=False, blank=False, verbose_name=_('Course Title'))
slug = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Slug')) slug = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Slug'))
category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE, related_name='courses', verbose_name=_('Category')) category = models.ForeignKey(CourseCategory, on_delete=models.CASCADE, related_name='courses', verbose_name=_('Category'))
professor = models.ForeignKey(
professors = models.ManyToManyField(
ProfessorUser, ProfessorUser,
on_delete=models.CASCADE,
related_name="courses", related_name="courses",
verbose_name=_("Professor")
verbose_name=_("Professors")
) )
thumbnail = models.ImageField(upload_to="courses/thumbnails/", verbose_name=_('Thumbnail')) thumbnail = models.ImageField(upload_to="courses/thumbnails/", verbose_name=_('Thumbnail'))

49
apps/course/serializers/admin.py

@ -122,7 +122,9 @@ class AdminCourseCategorySerializer(serializers.ModelSerializer):
class AdminCourseListSerializer(serializers.ModelSerializer): class AdminCourseListSerializer(serializers.ModelSerializer):
category_name = serializers.SerializerMethodField() category_name = serializers.SerializerMethodField()
professor_name = serializers.CharField(source='professor.fullname', read_only=True)
professor_name = serializers.SerializerMethodField(read_only=True)
professors = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), many=True, required=False)
professor = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), required=False, allow_null=True, write_only=True)
participant_count = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
@ -135,6 +137,7 @@ class AdminCourseListSerializer(serializers.ModelSerializer):
'category', 'category',
'category_name', 'category_name',
'professor', 'professor',
'professors',
'professor_name', 'professor_name',
'thumbnail', 'thumbnail',
'is_online', 'is_online',
@ -152,6 +155,9 @@ class AdminCourseListSerializer(serializers.ModelSerializer):
'created_at' 'created_at'
] ]
def get_professor_name(self, obj):
return ", ".join([p.fullname for p in obj.professors.all()])
def get_participant_count(self, obj): def get_participant_count(self, obj):
return obj.participants.count() return obj.participants.count()
@ -171,13 +177,21 @@ class AdminCourseListSerializer(serializers.ModelSerializer):
ret['slug'] = get_localized_field(lang, instance.slug) ret['slug'] = get_localized_field(lang, instance.slug)
ret['level'] = get_localized_field(lang, instance.level) ret['level'] = get_localized_field(lang, instance.level)
ret['status'] = get_localized_field(lang, instance.status) ret['status'] = get_localized_field(lang, instance.status)
# Inject legacy fields
first_prof = instance.professors.first()
ret['professor'] = first_prof.id if first_prof else None
ret['professor_name'] = first_prof.fullname if first_prof else ""
ret['professors'] = [p.id for p in instance.professors.all()]
return ret return ret
class AdminCourseDetailSerializer(serializers.ModelSerializer): class AdminCourseDetailSerializer(serializers.ModelSerializer):
slug = serializers.JSONField(required=False, allow_null=True) slug = serializers.JSONField(required=False, allow_null=True)
category_name = serializers.SerializerMethodField() category_name = serializers.SerializerMethodField()
professor_name = serializers.CharField(source='professor.fullname', read_only=True)
professor_name = serializers.SerializerMethodField(read_only=True)
professors = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), many=True, required=False)
professor = serializers.PrimaryKeyRelatedField(queryset=ProfessorUser.objects.all(), required=False, allow_null=True, write_only=True)
thumbnail = FileFieldSerializer(required=False, allow_null=True) thumbnail = FileFieldSerializer(required=False, allow_null=True)
video_file = FileFieldSerializer(required=False, allow_null=True) video_file = FileFieldSerializer(required=False, allow_null=True)
participant_count = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField()
@ -191,6 +205,7 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
'category', 'category',
'category_name', 'category_name',
'professor', 'professor',
'professors',
'professor_name', 'professor_name',
'thumbnail', 'thumbnail',
'video_type', 'video_type',
@ -220,6 +235,9 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
] ]
read_only_fields = ['id', 'final_price', 'final_price_rub', 'created_at', 'updated_at'] read_only_fields = ['id', 'final_price', 'final_price_rub', 'created_at', 'updated_at']
def get_professor_name(self, obj):
return ", ".join([p.fullname for p in obj.professors.all()])
def get_participant_count(self, obj): def get_participant_count(self, obj):
return obj.participants.count() return obj.participants.count()
@ -229,11 +247,34 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
lang = get_request_lang(self) lang = get_request_lang(self)
return get_localized_field(lang, obj.category.name) return get_localized_field(lang, obj.category.name)
def create(self, validated_data):
professor = validated_data.pop('professor', None)
professors = validated_data.pop('professors', None)
instance = super().create(validated_data)
if professors is not None:
instance.professors.set(professors)
elif professor is not None:
instance.professors.set([professor])
return instance
def update(self, instance, validated_data):
professor = validated_data.pop('professor', None)
professors = validated_data.pop('professors', None)
instance = super().update(instance, validated_data)
if professors is not None:
instance.professors.set(professors)
elif professor is not None:
instance.professors.set([professor])
return instance
def to_representation(self, instance): def to_representation(self, instance):
ret = super().to_representation(instance) ret = super().to_representation(instance)
lang = get_request_lang(self) lang = get_request_lang(self)
# Keep raw JSON values here so the admin form can edit all language variants,
# matching the blog admin flow.
# Inject legacy fields
first_prof = instance.professors.first()
ret['professor'] = first_prof.id if first_prof else None
ret['professor_name'] = first_prof.fullname if first_prof else ""
ret['professors'] = [p.id for p in instance.professors.all()]
return ret return ret

19
apps/course/serializers/course.py

@ -176,7 +176,7 @@ class CourseListSerializer(serializers.ModelSerializer):
class CourseDetailSerializer(serializers.ModelSerializer): class CourseDetailSerializer(serializers.ModelSerializer):
category = CourseCategorySerializer() category = CourseCategorySerializer()
professor = serializers.SerializerMethodField()
professors = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
participant_count = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField()
access = serializers.SerializerMethodField() access = serializers.SerializerMethodField()
@ -209,7 +209,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'category', 'category',
'access', 'access',
'participant_count', 'participant_count',
'professor',
'professors',
'is_professor', 'is_professor',
'thumbnail', 'thumbnail',
'video_type', 'video_type',
@ -344,15 +344,14 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return self._has_access(user, obj) return self._has_access(user, obj)
return False return False
def get_professor(self, obj):
def get_professors(self, obj):
"""Return the course professor's profile using UserProfileSerializer""" """Return the course professor's profile using UserProfileSerializer"""
if obj.professor:
return UserProfileSerializer(obj.professor, context=self.context).data
return None
return UserProfileSerializer(obj.professors.all(), many=True, context=self.context).data
def get_is_professor(self, obj): def get_is_professor(self, obj):
if professor := self._get_authenticated_user():
return obj.professor == professor
if user := self._get_authenticated_user():
# چک کردن اینکه آیا کاربر در لیست اساتید این دوره هست یا نه
return obj.professors.filter(id=user.id).exists()
return False return False
def get_lessons_count(self, obj): def get_lessons_count(self, obj):
@ -389,7 +388,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
if user.is_staff or user.is_superuser: if user.is_staff or user.is_superuser:
return True return True
if course.professor_id == user.id:
if course.professors.filter(id=user.id).exists():
return True return True
return Participant.objects.filter( return Participant.objects.filter(
@ -601,7 +600,7 @@ class MyCourseListSerializer(serializers.ModelSerializer):
return True return True
# اگر کاربر استاد دوره است، دسترسی کامل دارد # اگر کاربر استاد دوره است، دسترسی کامل دارد
if course.professor_id == student.id:
if course.professors.filter(id=student.id).exists():
return True return True
# در غیر این صورت چک می‌کنیم که آیا participant است یا خیر # در غیر این صورت چک می‌کنیم که آیا participant است یا خیر

34
apps/course/signals.py

@ -20,30 +20,32 @@ UserModel = get_user_model()
@receiver(post_save, sender=Course) @receiver(post_save, sender=Course)
def handle_room_message_for_course(sender, instance, created, **kwargs): def handle_room_message_for_course(sender, instance, created, **kwargs):
title_str = extract_text_from_json(instance.title) title_str = extract_text_from_json(instance.title)
first_professor = instance.professors.first()
if created: # فقط برای موارد جدید اجرا شود if created: # فقط برای موارد جدید اجرا شود
RoomMessage.objects.create( RoomMessage.objects.create(
name=f"{title_str} - Group", name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}", description=f"Group chat for course: {title_str}",
initiator=instance.professor, # استاد به‌عنوان سازنده اتاق
initiator=first_professor, # استاد اول به‌عنوان سازنده اتاق
course=instance, course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP room_type=RoomMessage.RoomTypeChoices.GROUP
) )
else: # این بخش در زمان آپدیت دوره اجرا می‌شود else: # این بخش در زمان آپدیت دوره اجرا می‌شود
# Find the existing group room for this course and update its details # Find the existing group room for this course and update its details
update_kwargs = {
'name': f"{title_str} - Group",
'description': f"Group chat for course: {title_str}",
}
if first_professor:
update_kwargs['initiator'] = first_professor
RoomMessage.objects.filter( RoomMessage.objects.filter(
course=instance, course=instance,
room_type=RoomMessage.RoomTypeChoices.GROUP room_type=RoomMessage.RoomTypeChoices.GROUP
).update(
name=f"{title_str} - Group",
description=f"Group chat for course: {title_str}",
initiator=instance.professor
)
).update(**update_kwargs)
@receiver(post_save, sender=Course) @receiver(post_save, sender=Course)
def ensure_professor_role(sender, instance, **kwargs): def ensure_professor_role(sender, instance, **kwargs):
professor = getattr(instance, 'professor', None)
if professor:
for professor in instance.professors.all():
professor.ensure_professor_profile() professor.ensure_professor_profile()
@receiver([post_save, post_delete], sender=Course) @receiver([post_save, post_delete], sender=Course)
@ -52,9 +54,8 @@ def invalidate_professor_course_cache(sender, instance, **kwargs):
Clears the cached professor detail page AND their course list Clears the cached professor detail page AND their course list
whenever a course assigned to them is created, updated, or deleted. whenever a course assigned to them is created, updated, or deleted.
""" """
if instance.professor:
detail_cache_key = f"professor_detail_{instance.professor.slug}"
for professor in instance.professors.all():
detail_cache_key = f"professor_detail_{professor.slug}"
cache.delete(detail_cache_key) cache.delete(detail_cache_key)
# Optional: If you update a Professor's profile in the admin directly # Optional: If you update a Professor's profile in the admin directly
@ -76,17 +77,18 @@ def sync_course_chat_locks(sender, instance, **kwargs):
room_type=RoomMessage.RoomTypeChoices.GROUP room_type=RoomMessage.RoomTypeChoices.GROUP
).update(is_locked=instance.is_group_chat_locked) ).update(is_locked=instance.is_group_chat_locked)
# 2. Update the Private Chats between the Professor and Students of this course
# 2. Update the Private Chats between the Professors and Students of this course
# Get all student IDs enrolled in this course # Get all student IDs enrolled in this course
student_ids = instance.participants.values_list('student_id', flat=True) student_ids = instance.participants.values_list('student_id', flat=True)
professor_ids = list(instance.professors.values_list('id', flat=True))
if student_ids:
if student_ids and professor_ids:
RoomMessage.objects.filter( RoomMessage.objects.filter(
room_type=RoomMessage.RoomTypeChoices.PRIVATE room_type=RoomMessage.RoomTypeChoices.PRIVATE
).filter( ).filter(
# Find rooms where initiator is prof and recipient is student, OR vice versa
Q(initiator_id=instance.professor_id, recipient_id__in=student_ids) |
Q(initiator_id__in=student_ids, recipient_id=instance.professor_id)
# Find rooms where initiator is any professor and recipient is student, OR vice versa
Q(initiator_id__in=professor_ids, recipient_id__in=student_ids) |
Q(initiator_id__in=student_ids, recipient_id__in=professor_ids)
).update(is_locked=instance.is_professor_chat_locked) ).update(is_locked=instance.is_professor_chat_locked)

24
apps/course/views/admin.py

@ -65,11 +65,11 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
return AdminCourseDetailSerializer return AdminCourseDetailSerializer
def get_queryset(self): def get_queryset(self):
queryset = Course.objects.all().select_related('category', 'professor')
queryset = Course.objects.all().select_related('category').prefetch_related('professors')
# Professors can only see their own courses # Professors can only see their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(professor_id=self.request.user.id)
queryset = queryset.filter(professors=self.request.user)
# Search Query # Search Query
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)
@ -120,7 +120,7 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
# If the logged-in user is a professor, force the professor field to themselves # If the logged-in user is a professor, force the professor field to themselves
if is_professor(self.request): if is_professor(self.request):
serializer.save(professor_id=self.request.user.id)
serializer.save(professor=self.request.user)
else: else:
serializer.save() serializer.save()
@ -128,9 +128,9 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
# Prevent professors from reassigning a course to another professor # Prevent professors from reassigning a course to another professor
if is_professor(self.request): if is_professor(self.request):
instance = self.get_object() instance = self.get_object()
if instance.professor_id != self.request.user.id:
if not instance.professors.filter(id=self.request.user.id).exists():
raise PermissionDenied("You can only edit your own courses.") raise PermissionDenied("You can only edit your own courses.")
serializer.save(professor_id=self.request.user.id)
serializer.save(professor=self.request.user)
else: else:
serializer.save() serializer.save()
@ -150,7 +150,7 @@ class AdminChapterViewSet(viewsets.ModelViewSet):
# Professors can only see chapters of their own courses # Professors can only see chapters of their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
course_id = self.request.query_params.get('course', None) course_id = self.request.query_params.get('course', None)
if course_id: if course_id:
@ -190,7 +190,7 @@ class AdminCourseLessonViewSet(viewsets.ModelViewSet):
# Professors can only see lessons of their own courses # Professors can only see lessons of their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
course_id = self.request.query_params.get('course', None) course_id = self.request.query_params.get('course', None)
if course_id: if course_id:
@ -242,7 +242,7 @@ class AdminCourseAttachmentViewSet(viewsets.ModelViewSet):
# Professors can only see attachments of their own courses # Professors can only see attachments of their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
course_id = self.request.query_params.get('course', None) course_id = self.request.query_params.get('course', None)
if course_id: if course_id:
@ -258,7 +258,7 @@ class AdminCourseAttachmentViewSet(viewsets.ModelViewSet):
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
if is_professor(request): if is_professor(request):
course = Course.objects.filter(id=course_id, professor_id=request.user.id).first()
course = Course.objects.filter(id=course_id, professors=request.user).first()
else: else:
course = Course.objects.filter(id=course_id).first() course = Course.objects.filter(id=course_id).first()
@ -342,7 +342,7 @@ class AdminCourseGlossaryViewSet(viewsets.ModelViewSet):
# Professors can only see glossaries of their own courses # Professors can only see glossaries of their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
course_id = self.request.query_params.get('course', None) course_id = self.request.query_params.get('course', None)
if course_id: if course_id:
@ -358,7 +358,7 @@ class AdminCourseGlossaryViewSet(viewsets.ModelViewSet):
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
if is_professor(request): if is_professor(request):
course = Course.objects.filter(id=course_id, professor_id=request.user.id).first()
course = Course.objects.filter(id=course_id, professors=request.user).first()
else: else:
course = Course.objects.filter(id=course_id).first() course = Course.objects.filter(id=course_id).first()
@ -547,7 +547,7 @@ class AdminLiveSessionViewSet(viewsets.ModelViewSet):
# Professors can only see live sessions of their own courses # Professors can only see live sessions of their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
search = self.request.query_params.get('search', None) search = self.request.query_params.get('search', None)
if search: if search:

15
apps/course/views/course.py

@ -97,7 +97,7 @@ from utils.pagination import StandardResultsSetPagination
class CourseListAPIView(ListAPIView): class CourseListAPIView(ListAPIView):
serializer_class = CourseListSerializer serializer_class = CourseListSerializer
filter_backends = [SearchFilter] filter_backends = [SearchFilter]
search_fields = ['title', 'category__name', 'professor__fullname']
search_fields = ['title', 'category__name', 'professors__fullname']
pagination_class = StandardResultsSetPagination pagination_class = StandardResultsSetPagination
permission_classes = [AllowAny] permission_classes = [AllowAny]
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
@ -148,7 +148,8 @@ class CourseListAPIView(ListAPIView):
""" """
queryset = Course.objects.select_related( queryset = Course.objects.select_related(
'category', 'category',
'professor'
).prefetch_related(
'professors',
).exclude( ).exclude(
status__contains=[{"title": Course.StatusChoices.INACTIVE}] status__contains=[{"title": Course.StatusChoices.INACTIVE}]
).order_by('-created_at', '-id') ).order_by('-created_at', '-id')
@ -212,8 +213,8 @@ class CourseDetailAPIView(RetrieveAPIView):
""" """
return Course.objects.select_related( return Course.objects.select_related(
'category', 'category',
'professor'
).prefetch_related( ).prefetch_related(
'professors',
'chapters__lessons__lesson', 'chapters__lessons__lesson',
'chapters__lessons__completions', 'chapters__lessons__completions',
'attachments__attachment', 'attachments__attachment',
@ -272,8 +273,8 @@ class MyCourseListAPIView(ListAPIView):
""" """
queryset = Course.objects.select_related( queryset = Course.objects.select_related(
'category', 'category',
'professor'
).prefetch_related( ).prefetch_related(
'professors',
'chapters__lessons__lesson', 'chapters__lessons__lesson',
'chapters__lessons__completions', 'chapters__lessons__completions',
'participants__student' 'participants__student'
@ -283,8 +284,8 @@ class MyCourseListAPIView(ListAPIView):
filters = request.query_params filters = request.query_params
user = self.request.user user = self.request.user
# Include courses where user is a student OR the professor
qs = queryset.filter(Q(participants__student=user) | Q(professor=user)).distinct()
# Include courses where user is a student OR one of the professors
qs = queryset.filter(Q(participants__student=user) | Q(professors=user)).distinct()
completed_param = filters.get('completed') completed_param = filters.get('completed')
if completed_param is not None: if completed_param is not None:
@ -485,7 +486,7 @@ class CourseOnlineClassTokenAPIView(GenericAPIView):
@staticmethod @staticmethod
def _user_has_access(user, course: Course) -> bool: def _user_has_access(user, course: Course) -> bool:
if user.is_staff or course.professor_id == user.id:
if user.is_staff or course.professors.filter(id=user.id).exists():
return True return True
return Participant.objects.filter(course=course, student=user).exists() return Participant.objects.filter(course=course, student=user).exists()

5
apps/course/views/professor.py

@ -198,8 +198,9 @@ class ProfessorCourseListAPIView(ListAPIView):
def get_queryset(self): def get_queryset(self):
slug = self.kwargs.get('slug') slug = self.kwargs.get('slug')
professor = get_object_or_404(UserModel.objects.filter(user_type=UserModel.UserType.PROFESSOR, slug=slug)) professor = get_object_or_404(UserModel.objects.filter(user_type=UserModel.UserType.PROFESSOR, slug=slug))
return Course.objects.select_related('category', 'professor').prefetch_related(
return Course.objects.select_related('category').prefetch_related(
'professors',
'lessons__lesson', 'lessons__lesson',
'lessons__completions', 'lessons__completions',
'participants__student', 'participants__student',
).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}]).filter(professor=professor)
).exclude(status__contains=[{"title": Course.StatusChoices.INACTIVE}]).filter(professors=professor)

4
apps/quiz/admin/quiz.py

@ -41,7 +41,7 @@ class QuizAdmin(ModelAdmin):
# اولویت دوم: professor - فقط کوئیزهای دوره‌های خود # اولویت دوم: professor - فقط کوئیزهای دوره‌های خود
if request.user.has_role('professor'): if request.user.has_role('professor'):
return queryset.filter(lesson__course__professor=request.user)
return queryset.filter(lesson__course__professors=request.user)
return queryset.none() return queryset.none()
@ -104,7 +104,7 @@ class QuizAdmin(ModelAdmin):
form.base_fields['lesson'].queryset = CourseLesson.objects.all() form.base_fields['lesson'].queryset = CourseLesson.objects.all()
elif request.user.has_role('professor'): elif request.user.has_role('professor'):
# اولویت دوم: professor - فقط CourseLesson های دوره‌های خود # اولویت دوم: professor - فقط CourseLesson های دوره‌های خود
form.base_fields['lesson'].queryset = CourseLesson.objects.filter(course__professor=request.user)
form.base_fields['lesson'].queryset = CourseLesson.objects.filter(course__professors=request.user)
else: else:
# سایر کاربران - عدم دسترسی # سایر کاربران - عدم دسترسی
form.base_fields['lesson'].queryset = CourseLesson.objects.none() form.base_fields['lesson'].queryset = CourseLesson.objects.none()

8
apps/quiz/views/admin.py

@ -102,7 +102,7 @@ class AdminQuizViewSet(ModelViewSet):
# Professors can only see quizzes of their own courses # Professors can only see quizzes of their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
# Handle Search # Handle Search
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)
@ -216,7 +216,7 @@ class AdminQuestionViewSet(ModelViewSet):
# Professors can only see questions of their own courses' quizzes # Professors can only see questions of their own courses' quizzes
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(quiz__course__professor_id=self.request.user.id)
queryset = queryset.filter(quiz__course__professors=self.request.user)
quiz_id = self.request.query_params.get('quiz_id', None) quiz_id = self.request.query_params.get('quiz_id', None)
if not quiz_id: if not quiz_id:
@ -270,7 +270,7 @@ class AdminCourseListView(ListAPIView):
def get_queryset(self): def get_queryset(self):
queryset = Course.objects.all().order_by('-id') queryset = Course.objects.all().order_by('-id')
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(professor_id=self.request.user.id)
queryset = queryset.filter(professors=self.request.user)
return queryset return queryset
@ -293,6 +293,6 @@ class AdminLessonListView(ListAPIView):
# Professors only see lessons of their own courses # Professors only see lessons of their own courses
if is_professor(self.request): if is_professor(self.request):
qs = qs.filter(course__professor_id=self.request.user.id)
qs = qs.filter(course__professors=self.request.user)
return qs return qs

2
apps/transaction/views.py

@ -487,7 +487,7 @@ class AdminTransactionViewSet(ModelViewSet):
# Professors can only see transactions for their own courses # Professors can only see transactions for their own courses
if is_professor(self.request): if is_professor(self.request):
queryset = queryset.filter(course__professor_id=self.request.user.id)
queryset = queryset.filter(course__professors=self.request.user)
# Search # Search
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)

Loading…
Cancel
Save