diff --git a/apps/course/admin/course.py b/apps/course/admin/course.py
index 04904b8..03b0ac5 100644
--- a/apps/course/admin/course.py
+++ b/apps/course/admin/course.py
@@ -269,7 +269,7 @@ class CourseAdmin(DirectCourseAdmin):
warn_unsaved_form = True
search_fields = ('id','title', 'description')
exclude = ('slug', )
- readonly_fields = ('final_price',)
+ readonly_fields = ('final_price', 'final_price_rub')
autocomplete_fields = ('category', 'professor',)
list_filter_submit = True
change_form_show_cancel_button = True
@@ -280,8 +280,10 @@ class CourseAdmin(DirectCourseAdmin):
conditional_fields = {
'price': "is_free == false",
+ 'price_rub': "is_free == false",
'discount_percentage': "is_free == false",
'final_price': "is_free == false",
+ 'final_price_rub': "is_free == false",
'online_link': "is_online",
'video_file': "video_type == 'video_file'",
'video_link': "video_type == 'youtube_link'",
@@ -308,8 +310,8 @@ class CourseAdmin(DirectCourseAdmin):
}),
(_('Pricing'), {
'fields': (
- ('is_free', 'price'),
- ('discount_percentage', 'final_price')
+ ('is_free', 'price', 'price_rub'),
+ ('discount_percentage', 'final_price', 'final_price_rub')
),
'classes': ['tab'],
}),
@@ -347,12 +349,18 @@ class CourseAdmin(DirectCourseAdmin):
if instance.discount_percentage > 0:
return format_html(
- '${}'
- '${}',
+ 'USD: ${} -> ${}'
+ 'RUB: ₽{} -> ₽{}',
instance.price,
- instance.final_price
+ instance.final_price,
+ instance.price_rub,
+ instance.final_price_rub,
)
- return format_html('${}', instance.final_price)
+ return format_html(
+ 'USD: ${}RUB: ₽{}',
+ instance.final_price,
+ instance.final_price_rub,
+ )
actions_row = ["add_student_to_course"]
@@ -592,4 +600,4 @@ class HiddenCourseAdmin(ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
-dovoodi_admin_site.register(Course, HiddenCourseAdmin)
\ No newline at end of file
+dovoodi_admin_site.register(Course, HiddenCourseAdmin)
diff --git a/apps/course/migrations/0010_course_rub_prices.py b/apps/course/migrations/0010_course_rub_prices.py
new file mode 100644
index 0000000..3d21519
--- /dev/null
+++ b/apps/course/migrations/0010_course_rub_prices.py
@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0009_alter_course_description_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='course',
+ name='final_price_rub',
+ field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='This field is automatically calculated based on the discount percentage.', max_digits=10, verbose_name='Course Final Price (RUB)'),
+ ),
+ migrations.AddField(
+ model_name='course',
+ name='price_rub',
+ field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Course Price (RUB)'),
+ ),
+ ]
diff --git a/apps/course/models/course.py b/apps/course/models/course.py
index c90e15b..9976dc9 100644
--- a/apps/course/models/course.py
+++ b/apps/course/models/course.py
@@ -151,6 +151,9 @@ STATUS_TRANSLATIONS = {
}
}
+CURRENCY_USD = 'USD'
+CURRENCY_RUB = 'RUB'
+
def normalize_level(value):
if not value:
return 'beginner'
@@ -292,11 +295,16 @@ class Course(models.Model):
status = models.JSONField(default=list, null=True, blank=True, verbose_name=_('Course Status'))
is_free = models.BooleanField(default=True, verbose_name=_('Is Free'))
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name=_('Course Price'))
+ price_rub = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name=_('Course Price (RUB)'))
discount_percentage = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], verbose_name=_('Discount Percentage'))
final_price = models.DecimalField(
verbose_name=_('Course Final Price'), decimal_places=2, max_digits=10, default=0.00, blank=True,
help_text=_('This field is automatically calculated based on the discount percentage.')
)
+ final_price_rub = models.DecimalField(
+ verbose_name=_('Course Final Price (RUB)'), decimal_places=2, max_digits=10, default=0.00, blank=True,
+ help_text=_('This field is automatically calculated based on the discount percentage.')
+ )
is_group_chat_locked = models.BooleanField(
default=False,
@@ -339,6 +347,26 @@ class Course(models.Model):
def recalculate_lessons_count(self):
return self.__class__.recalculate_lessons_count_for_course(self.pk)
+ def get_currency_for_language(self, lang):
+ normalized = (lang or 'en').lower()
+ if normalized.startswith('ru'):
+ return CURRENCY_RUB
+ return CURRENCY_USD
+
+ def get_price_for_language(self, lang):
+ if self.is_free:
+ return Decimal('0.00')
+ if self.get_currency_for_language(lang) == CURRENCY_RUB:
+ return Decimal(self.price_rub or 0)
+ return Decimal(self.price or 0)
+
+ def get_final_price_for_language(self, lang):
+ if self.is_free:
+ return Decimal('0.00')
+ if self.get_currency_for_language(lang) == CURRENCY_RUB:
+ return Decimal(self.final_price_rub or 0)
+ return Decimal(self.final_price or 0)
+
def save(self, *args, **kwargs):
self.title = format_multilingual_field(self.title)
@@ -424,21 +452,31 @@ class Course(models.Model):
]
self.features = format_multilingual_field(self.features)
- # Ensure consistency: if price is 0, set is_free to True and discount_percentage to 0
- if self.price == 0:
+ usd_price = Decimal(self.price or 0)
+ rub_price = Decimal(self.price_rub or 0)
+
+ # Ensure consistency: if both prices are 0, set is_free to True and discount_percentage to 0
+ if usd_price == 0 and rub_price == 0:
self.is_free = True
self.discount_percentage = 0
self.final_price = Decimal('0.00')
+ self.final_price_rub = Decimal('0.00')
elif self.is_free:
self.price = Decimal('0.00')
+ self.price_rub = Decimal('0.00')
self.discount_percentage = 0
self.final_price = Decimal('0.00')
+ self.final_price_rub = Decimal('0.00')
elif self.discount_percentage > 0:
- discount_amount = (self.price * self.discount_percentage) / 100
- final_price = self.price - discount_amount
- self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00'))
+ usd_discount_amount = (usd_price * self.discount_percentage) / 100
+ rub_discount_amount = (rub_price * self.discount_percentage) / 100
+ usd_final_price = usd_price - usd_discount_amount
+ rub_final_price = rub_price - rub_discount_amount
+ self.final_price = Decimal(math.ceil(usd_final_price)).quantize(Decimal('0.00'))
+ self.final_price_rub = Decimal(math.ceil(rub_final_price)).quantize(Decimal('0.00'))
else:
- self.final_price = Decimal(math.ceil(self.price)).quantize(Decimal('0.00'))
+ self.final_price = Decimal(math.ceil(usd_price)).quantize(Decimal('0.00'))
+ self.final_price_rub = Decimal(math.ceil(rub_price)).quantize(Decimal('0.00'))
super().save(*args, **kwargs)
diff --git a/apps/course/serializers/admin.py b/apps/course/serializers/admin.py
index 468956b..8597a92 100644
--- a/apps/course/serializers/admin.py
+++ b/apps/course/serializers/admin.py
@@ -20,7 +20,21 @@ from apps.course.models.course import extract_text_from_json, get_localized_fiel
def get_request_lang(serializer_instance):
request = serializer_instance.context.get('request')
- return getattr(request, 'LANGUAGE_CODE', None) or 'en'
+ language_code = getattr(request, 'LANGUAGE_CODE', None)
+ if language_code:
+ return str(language_code).lower()
+
+ header_value = ''
+ if request is not None:
+ if hasattr(request, 'headers'):
+ header_value = request.headers.get('Accept-Language', '')
+ if not header_value and hasattr(request, 'META'):
+ header_value = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
+
+ if header_value:
+ return header_value.split(',')[0].strip().lower()
+
+ return 'en'
class AdminLessonSerializer(serializers.ModelSerializer):
class Meta:
@@ -102,8 +116,10 @@ class AdminCourseListSerializer(serializers.ModelSerializer):
'status',
'is_free',
'price',
+ 'price_rub',
'discount_percentage',
'final_price',
+ 'final_price_rub',
'participant_count',
'created_at'
]
@@ -162,8 +178,10 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
'status',
'is_free',
'price',
+ 'price_rub',
'discount_percentage',
'final_price',
+ 'final_price_rub',
'is_group_chat_locked',
'is_professor_chat_locked',
'timing',
@@ -172,7 +190,7 @@ class AdminCourseDetailSerializer(serializers.ModelSerializer):
'created_at',
'updated_at'
]
- read_only_fields = ['id', 'final_price', 'created_at', 'updated_at']
+ read_only_fields = ['id', 'final_price', 'final_price_rub', 'created_at', 'updated_at']
def get_participant_count(self, obj):
return obj.participants.count()
diff --git a/apps/course/serializers/course.py b/apps/course/serializers/course.py
index eb848c4..954279a 100644
--- a/apps/course/serializers/course.py
+++ b/apps/course/serializers/course.py
@@ -5,10 +5,31 @@ from apps.course.models import Course, CourseCategory, Attachment, Glossary, Les
from apps.course.models.course import get_localized_field
from apps.chat.models import RoomMessage
from apps.account.serializers import UserProfileSerializer
+from decimal import Decimal
import ast
import json
+def resolve_request_lang(request, default='en'):
+ language_code = getattr(request, 'LANGUAGE_CODE', None)
+ if language_code:
+ return str(language_code).lower()
+
+ if not request:
+ return default
+
+ header_value = ''
+ if hasattr(request, 'headers'):
+ header_value = request.headers.get('Accept-Language', '')
+ if not header_value and hasattr(request, 'META'):
+ header_value = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
+
+ if header_value:
+ return header_value.split(',')[0].strip().lower()
+
+ return default
+
+
class CourseCategorySerializer(serializers.ModelSerializer):
course_count = serializers.SerializerMethodField()
@@ -21,7 +42,7 @@ class CourseCategorySerializer(serializers.ModelSerializer):
def _lang(self):
request = self.context.get('request')
- return getattr(request, 'LANGUAGE_CODE', None) or 'en'
+ return resolve_request_lang(request)
def get_name(self, obj):
return get_localized_field(self._lang(), obj.name)
@@ -41,6 +62,7 @@ class CourseListSerializer(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
+ currency = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
slug = serializers.SerializerMethodField()
@@ -67,12 +89,13 @@ class CourseListSerializer(serializers.ModelSerializer):
'is_free',
'price',
'discount_percentage',
- 'final_price',
+ 'final_price',
+ 'currency',
]
def _lang(self):
request = self.context.get('request')
- return getattr(request, 'LANGUAGE_CODE', None) or 'en'
+ return resolve_request_lang(request)
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
@@ -110,22 +133,27 @@ class CourseListSerializer(serializers.ModelSerializer):
return max(lessons_count, obj.lessons_count)
def get_price(self, obj):
- if obj.is_free or obj.price == 0:
+ amount = obj.get_price_for_language(self._lang())
+ if obj.is_free or amount == 0:
return "0.00"
- return str(obj.price)
+ return str(amount)
def get_discount_percentage(self, obj):
- if obj.is_free or obj.price == 0:
+ if obj.is_free or obj.get_price_for_language(self._lang()) == 0:
return 0
return obj.discount_percentage
def get_final_price(self, obj):
- if obj.is_free or obj.price == 0:
+ amount = obj.get_final_price_for_language(self._lang())
+ if obj.is_free or amount == 0:
return "0.00"
- return str(obj.final_price)
+ return str(amount)
+
+ def get_currency(self, obj):
+ return obj.get_currency_for_language(self._lang())
def get_is_free(self, obj):
- return obj.is_free or obj.price == 0
+ return obj.is_free or (Decimal(obj.price or 0) == 0 and Decimal(obj.price_rub or 0) == 0)
class CourseDetailSerializer(serializers.ModelSerializer):
@@ -142,6 +170,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
+ currency = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
is_professor = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
@@ -181,6 +210,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'price',
'discount_percentage',
'final_price',
+ 'currency',
'timing',
'features',
'last_lesson_id',
@@ -389,25 +419,29 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return obj.participants.count()
def get_price(self, obj):
- if obj.is_free or obj.price == 0:
+ amount = obj.get_price_for_language(self._lang())
+ if obj.is_free or amount == 0:
return "0.00"
- return str(obj.price)
+ return str(amount)
def get_discount_percentage(self, obj):
- if obj.is_free or obj.price == 0:
+ if obj.is_free or obj.get_price_for_language(self._lang()) == 0:
return 0
return obj.discount_percentage
def get_final_price(self, obj):
- if obj.is_free or obj.price == 0:
+ amount = obj.get_final_price_for_language(self._lang())
+ if obj.is_free or amount == 0:
return "0.00"
- return str(obj.final_price)
+ return str(amount)
+ def get_currency(self, obj):
+ return obj.get_currency_for_language(self._lang())
def get_is_free(self, obj):
- return obj.is_free or obj.price == 0
+ return obj.is_free or (Decimal(obj.price or 0) == 0 and Decimal(obj.price_rub or 0) == 0)
def _lang(self):
request = self.context.get('request')
- return getattr(request, 'LANGUAGE_CODE', None) or 'en'
+ return resolve_request_lang(request)
def get_title(self, obj):
return get_localized_field(self._lang(), obj.title)
diff --git a/apps/course/serializers/professor.py b/apps/course/serializers/professor.py
index a82b212..b0bea9b 100644
--- a/apps/course/serializers/professor.py
+++ b/apps/course/serializers/professor.py
@@ -15,7 +15,7 @@ class ProfessorListSerializer(serializers.ModelSerializer):
class Meta:
model = User
- fields = ['id', 'slug', 'fullname', 'avatar','experience_years', 'course_count', 'lesson_count']
+ fields = ['id', 'slug', 'fullname', 'email', 'avatar', 'experience_years', 'course_count', 'lesson_count']
class ProfessorDetailSerializer(UserProfileSerializer):
diff --git a/apps/course/tests/test_multiple_roles_api.py b/apps/course/tests/test_multiple_roles_api.py
index 12be84d..9eacac5 100644
--- a/apps/course/tests/test_multiple_roles_api.py
+++ b/apps/course/tests/test_multiple_roles_api.py
@@ -6,6 +6,7 @@ from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
from django.contrib.auth.models import Group
+from decimal import Decimal
from apps.account.models import User
from apps.course.models import Course, CourseCategory, Participant
from apps.transaction.models import TransactionParticipant
@@ -214,3 +215,40 @@ class MultipleRolesAPITestCase(TestCase):
# user_type field باید بروزرسانی شود
self.assertEqual(self.user.user_type, User.UserType.PROFESSOR)
+
+ def test_course_detail_serializer_uses_rub_price_for_russian_language(self):
+ """برای زبان روسی باید قیمت روبلی برگردانده شود، نه دلار"""
+ self.user.add_role('professor')
+
+ course = Course.objects.create(
+ title=[
+ {"title": "English Title", "language_code": "en"},
+ {"title": "Русский заголовок", "language_code": "ru"},
+ ],
+ slug='localized-course',
+ category=self.category,
+ professor=self.user,
+ level='beginner',
+ duration=10,
+ lessons_count=5,
+ description='Test description',
+ is_free=False,
+ price=Decimal('100.00'),
+ price_rub=Decimal('7500.00'),
+ discount_percentage=10,
+ )
+
+ from apps.course.serializers import CourseDetailSerializer
+ from django.test import RequestFactory
+
+ factory = RequestFactory()
+ request = factory.get('/', HTTP_ACCEPT_LANGUAGE='ru')
+ request.user = self.user
+ request.LANGUAGE_CODE = 'ru'
+
+ serializer = CourseDetailSerializer(course, context={'request': request})
+ data = serializer.data
+
+ self.assertEqual(data['currency'], 'RUB')
+ self.assertEqual(data['price'], '7500.00')
+ self.assertEqual(data['final_price'], '6750.00')
diff --git a/apps/course/views/admin.py b/apps/course/views/admin.py
index f1125e6..7d2a174 100644
--- a/apps/course/views/admin.py
+++ b/apps/course/views/admin.py
@@ -64,11 +64,20 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
# Search Query
search_query = self.request.query_params.get('search', None)
+ has_search = False
if search_query:
+ has_search = True
+ from django.db.models import Case, When, Value, IntegerField
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(description__icontains=search_query) |
Q(short_description__icontains=search_query)
+ ).annotate(
+ search_relevance=Case(
+ When(title__icontains=search_query, then=Value(2)),
+ default=Value(1),
+ output_field=IntegerField()
+ )
)
# Filters
@@ -95,6 +104,8 @@ class AdminCourseViewSet(viewsets.ModelViewSet):
if student_id:
queryset = queryset.filter(participants__student_id=student_id)
+ if has_search:
+ return queryset.order_by('-search_relevance', '-id')
return queryset.order_by('-id')
def perform_create(self, serializer):