diff --git a/apps/account/serializers/user.py b/apps/account/serializers/user.py index e565768..255a3c3 100644 --- a/apps/account/serializers/user.py +++ b/apps/account/serializers/user.py @@ -43,19 +43,16 @@ class UserProfileSerializer(serializers.ModelSerializer): class UserRegisterSerializer(serializers.ModelSerializer): - password_confirmation = serializers.CharField(write_only=True) fcm = serializers.CharField(required=False) device_id = serializers.CharField(required=True) email = serializers.EmailField() class Meta: model = User - fields = ['id','fullname', 'email', 'password', 'password_confirmation', 'fcm', 'device_id'] + fields = ['id','fullname', 'email', 'fcm', 'device_id'] extra_kwargs = { 'fullname': {'required': True,}, 'email': {'required': True,}, - 'password': {'required': True,}, - 'password_confirmation': {'required': True,}, 'device_id': {'required': True,}, } @@ -65,35 +62,15 @@ class UserRegisterSerializer(serializers.ModelSerializer): return value - def validate(self, data): - password = data.get('password') - password_confirmation = data.get('password_confirmation') - errors = {} - - if password and password_confirmation and password != password_confirmation: - raise serializers.ValidationError({"password_confirmation": "Passwords do not match."}) - if len(password) < 8: - raise serializers.ValidationError({"password": "Password must be at least 8 characters long."}) - # If there are any errors, raise ValidationError - data.pop('password_confirmation', None) - return data - - -class UserVerifySerializer(serializers.ModelSerializer): +class UserVerifySerializer(serializers.Serializer): code = serializers.CharField(max_length=5, validators=[validate_type_code]) email = serializers.EmailField() - - class Meta: - model = User - fields = ["email", "code"] - extra_kwargs = { - 'email': {'required': True,}, - 'code': {'required': True,}, - } + device_id = serializers.CharField(max_length=255, required=False) + -class UserLoginSerializer(serializers.ModelSerializer): +class UserLoginSerializer(serializers.Serializer): password = serializers.CharField(write_only=True) token = serializers.CharField(allow_null=True, read_only=True, required=False) fullname = serializers.CharField(allow_null=True, read_only=True, required=False) @@ -104,20 +81,23 @@ class UserLoginSerializer(serializers.ModelSerializer): device_id = serializers.CharField(required=False) timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) - - class Meta: - model = User - fields = ['id', 'phone_number', 'password', 'fullname', 'avatar', 'email', 'token', 'fcm', 'device_id', 'timezone'] - - def get_token(self, obj): - token, created = Token.objects.get_or_create(user=obj) - return token.key - def validate(self, data): - data.pop('fcm', None) - data.pop('device_id', None) + # Custom validation logic can be added here if needed + # data.pop('fcm', None) + # data.pop('device_id', None) return data +# class UserLoginSerializer(serializers.Serializer): +# password = serializers.CharField(write_only=True) +# token = serializers.CharField(allow_null=True, read_only=True, required=False) +# fullname = serializers.CharField(allow_null=True, read_only=True, required=False) +# avatar = serializers.CharField(allow_null=True, read_only=True, required=False) +# email = serializers.EmailField(write_only=True) +# password = serializers.CharField(style={'input_type': 'password'}, trim_whitespace=False) +# fcm = serializers.CharField(required=False) +# device_id = serializers.CharField(required=False) +# timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) + @@ -134,30 +114,15 @@ class UserRecoverPasswordSerializer(serializers.ModelSerializer): class UserResetPasswordSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) - password_confirmation = serializers.CharField(write_only=True) class Meta: model = User - fields = ['password', 'password_confirmation'] + fields = ['password', ] extra_kwargs = { 'password': {'required': True,}, - 'password_confirmation': {'required': True,}, } - def validate(self, data): - password = data.get('password') - password_confirmation = data.get('password_confirmation') - errors = {} - - if password and password_confirmation and password != password_confirmation: - raise serializers.ValidationError({"password_confirmation": "Passwords do not match."}) - if len(password) < 8: - raise serializers.ValidationError({"password": "Password must be at least 8 characters long."}) - # If there are any errors, raise ValidationError - - data.pop('password_confirmation', None) - return data class UserGuestSerializer(serializers.ModelSerializer): diff --git a/apps/account/views/user.py b/apps/account/views/user.py index 134b2bc..5553186 100644 --- a/apps/account/views/user.py +++ b/apps/account/views/user.py @@ -41,7 +41,18 @@ class UserGuestView(CreateAPIView): @swagger_auto_schema( operation_description="Create a guest user account with device information", - request_body=UserGuestSerializer, + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "device_id": openapi.Schema(type=openapi.TYPE_STRING, default="c9f0c1f4f5cee3d7"), + "fcm": openapi.Schema(type=openapi.TYPE_STRING, default=""), + "device_os": openapi.Schema(type=openapi.TYPE_STRING, default="android"), + "lat": openapi.Schema(type=openapi.TYPE_STRING, default="56"), + "lon": openapi.Schema(type=openapi.TYPE_STRING, default="44"), + "timezone": openapi.Schema(type=openapi.TYPE_STRING, default="1.0"), + }, + required=["device_id"], + ), ) def post(self, request, *args, **kwargs): logger.info(f'GuestAuthView--> {request.data}') @@ -75,7 +86,8 @@ class UserGuestView(CreateAPIView): device_id = serializer.validated_data.get('device_id') device_os = serializer.validated_data.get('device_os') fcm = serializer.validated_data.get('fcm') - lat, lon = serializer.validated_data.get('lat'), serializer.validated_data.get('lon') + lat = serializer.validated_data.pop('lat', None) + lon = serializer.validated_data.pop('lon', None) user_timezone = serializer.validated_data.pop('timezone', None) serializer_data = dict(serializer.validated_data) @@ -122,7 +134,6 @@ class UserRegisterView(CreateAPIView): phone_number = RedisManager().add_to_redis(code, **data) send_email([data['email']], code) - password = data.pop('password') return Response( data= { "user": data, @@ -142,12 +153,14 @@ class UserVerifyView(CreateAPIView): request_body=UserVerifySerializer, ) def post(self, request, *args, **kwargs): + print(f'-UserVerifyView-> {request.data}') return super().post(request, *args, **kwargs) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.data + print(f'--UserVerifyView---1--') try: verify_data = RedisManager().get_by_redis(data['email']) if not verify_data: @@ -159,18 +172,16 @@ class UserVerifyView(CreateAPIView): # raise ExpiredCodeException("The verification code has expired.") raise ValidationError({"code": "The verification code has expired."}) - code = self.valied_code(data['code'], verify_data['code']) del verify_data['code'] user = self.perform_create( - email=serializer.data['email'],**verify_data + email=serializer.data['email'], device_id=serializer.data['device_id'], **verify_data ) - # Token.objects.filter(user=user).delete() - token = Token.objects.get_or_create(user=user) + token, _ = Token.objects.get_or_create(user=user) return Response(data={ - 'token': str(token), + 'token': str(token.key), 'user_id': user.id, - 'phone_number': str(user.phone_number), + 'phone_number': str(user.phone_number) if user.phone_number else None, 'email': str(user.email), 'fullname': str(user.fullname), 'avatar': str(user.avatar) if user.avatar else None @@ -184,20 +195,23 @@ class UserVerifyView(CreateAPIView): def perform_create(self, *args, **kwargs): email = kwargs.get('email') + device_id = kwargs.get('device_id') user = User.objects.filter(email=email).first() if user: if kwargs['password']: user.is_active = True user.deletion_date = None + user.device_id = device_id user.last_login = timezone.now() - user.set_password(kwargs['password']) user.save() else: - user = User.objects.filter(device_id=kwargs['device_id']).first() + user = User.objects.filter(device_id=device_id, email__isnull=True).first() if not user: user = User.objects.create(**kwargs) - - user.set_password(kwargs['password']) + else: + user.email = email + user.fullname = kwargs['fullname'] + user.device_id = device_id user.last_login = timezone.now() user.is_active = True user.save() @@ -215,8 +229,6 @@ class UserLoginView(CreateAPIView): ) def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs) - - @staticmethod def get_client_ip(self): request = self.request x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') @@ -241,7 +253,7 @@ class UserLoginView(CreateAPIView): serializer_data = serializer.data serializer_data['token'] = token.key - login_history_obj = obj.login_history.create( + login_history_obj = user.login_history.create( ip=self.get_client_ip(), timezone=user_timezone, ) @@ -295,7 +307,7 @@ class UserRecoverPassword(CreateAPIView): data= { "id": user.id, "fullname": user.fullname, - "phone_number": str(user.phone_number), + "phone_number": str(user.phone_number) if user.phone_number else None, "email": user.email if user.email else None, "avatar": user.avatar if user.avatar else None, "message": "Forgot password code sent" diff --git a/apps/hadis/__init__.py b/apps/hadis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/hadis/admin/__init__.py b/apps/hadis/admin/__init__.py new file mode 100644 index 0000000..ba8ce70 --- /dev/null +++ b/apps/hadis/admin/__init__.py @@ -0,0 +1,3 @@ +from .category import * +from .hadis import * +from .transmitter import * \ No newline at end of file diff --git a/apps/hadis/admin/category.py b/apps/hadis/admin/category.py new file mode 100644 index 0000000..4ffcefa --- /dev/null +++ b/apps/hadis/admin/category.py @@ -0,0 +1,217 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from dj_category.admin import BaseCategoryAdmin +from ajaxdatatable.admin import AjaxDatatable +from django.http import JsonResponse +from django.urls import path + +from apps.hadis.models import * +from django import forms + +from django.db.models import Case, When, Value + + + +@admin.register(HadisCategory) +class HadisCategoryAdmin(BaseCategoryAdmin): + change_form_template = 'admin/hadiscategory/change_form.html' + change_list_template = 'admin/category_index.html' + fields = ( + 'name', 'source_type', 'category_type' , 'parent', 'is_active', 'order' + ) + search_fields = ['name'] + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + return form + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('categories-ajax/hadiscategory/', self.admin_site.admin_view(self.ajax_categories), name='hadiscategory_ajax_categories'), + ] + return custom_urls + urls + def get_categories_groupby_language(self, request=None, selected_values=(), is_multiple=False): + print(f'--get_categories_groupby_language-> {selected_values}') + return super().get_categories(request, selected_values, is_multiple) + + def ajax_update(self, request): + data = request.POST + src_node = self.model.objects.get(pk=int(data['srcNode'])) + other_node = self.model.objects.get(pk=int(data['otherNode'])) + print(f'--ajax_update-> {data}') + if src_node.slug in self.base_categories or other_node.slug in self.base_categories: + return JsonResponse({'data': _('This item can not be modifed')}, status=401) + + mode = data['hitMode'] + if mode == 'over': + src_node.move_to(other_node, 'first-child') + elif mode == 'after': + src_node.move_to(other_node, 'right') + elif mode == 'before': + src_node.move_to(other_node, 'left') + + return JsonResponse({'data': 'ok'}, safe=False) + + def get_categories(self, request=None, selected_values=(), is_multiple=False): + """ + Override the get_categories method to filter by source_type if provided in the request + """ + categories = super().get_categories(request, selected_values, is_multiple) + + # If request has source_type parameter, filter the categories + if request and request.GET.get('source_type'): + source_type = request.GET.get('source_type') + # Filter the categories by source_type + filtered_categories = [] + for category in categories: + # If it's a dictionary (serialized category) + if isinstance(category, dict) and category.get('source_type') == source_type: + filtered_categories.append(category) + # If it's a model instance + elif hasattr(category, 'source_type') and getattr(category, 'source_type') == source_type: + filtered_categories.append(category) + return filtered_categories + + return categories + + def ajax_categories(self, request): + """ + Handle AJAX request for categories with source_type filtering and search + """ + # Get source_type from request + source_type = request.GET.get('source_type') + + # Get node_id if provided (for single node data) + node_id = request.GET.get('node_id') + + # Get search term if provided + search = request.GET.get('search') + + # Get parent level filter if provided + parent_level = request.GET.get('parent_level') + + if node_id: + # Return data for a specific node + try: + node = self.model.objects.get(pk=int(node_id)) + return JsonResponse({ + 'id': node.id, + 'source_type': node.source_type, + 'category_type': node.category_type, + 'parent': node.parent_id, + 'level': node.level_p # Add the level_p property + }) + except self.model.DoesNotExist: + return JsonResponse({'error': 'Node not found'}, status=404) + + # Get all categories + queryset = self.model.objects.all() + + # Annotate queryset with level_p + queryset = queryset.annotate( + level_pp=Case( + When(parent=None, then=Value(1)), + When(parent__isnull=False, parent__parent=None, then=Value(2)), + default=Value(3), + output_field=models.IntegerField() + ) + ) + + # Filter by source_type if provided + if source_type: + queryset = queryset.filter(source_type=source_type) + + # Filter by search term if provided + if search: + queryset = queryset.filter(name__icontains=search) + + # Filter by parent_level if provided + if parent_level and parent_level.isdigit(): + # Convert to integer + level = int(parent_level) + # Filter categories by level_p + queryset = queryset.filter(level_pp=level) + + # Convert queryset to list of dictionaries for JSON response + categories = [] + for category in queryset: + categories.append({ + 'key': category.id, + 'title': category.name, + 'parent': category.parent_id, + 'source_type': category.source_type, + 'category_type': category.category_type, + 'level': category.level_p, + # Add data property to store additional information + 'data': { + 'parent': category.parent_id, + 'level': category.level_p + } + }) + print(f'-categories-->{categories}') + return JsonResponse(categories, safe=False) + + def save_model(self, request, obj, form, change): + print(f'SAVE_MODEL CALLED: {request}/ {obj} / {form} / {change}') + print(f'POST DATA: {request.POST}') + + # Get the level choice from the form data + level_choice = request.POST.get('level_choice_hidden') + print(f'LEVEL CHOICE: {level_choice}') + + # Get the parent from AJAX selection if provided + ajax_parent = request.POST.get('ajax_parent') + if ajax_parent and ajax_parent.isdigit(): + # Set the parent for the object + try: + parent_category = self.model.objects.get(pk=int(ajax_parent)) + obj.parent = parent_category + + # If parent is level 1, inherit its source_type + # if parent_category.level_p == 1 and level_choice == '2': + # obj.source_type = parent_category.source_type + + print(f'AJAX PARENT SET: {parent_category.id} - {parent_category.name}') + except self.model.DoesNotExist: + print(f'PARENT CATEGORY NOT FOUND: {ajax_parent}') + + # Debug form validation + if form.is_valid(): + print("FORM IS VALID") + else: + print(f"FORM ERRORS: {form.errors}") + print(f'---> {obj}') + + # Let the parent class handle the save + super().save_model(request, obj, form, change) + + # Add a message to trigger tree reload via JavaScript + from django.contrib import messages + messages.success(request, "Category saved successfully. Tree will be reloaded.") + + # Set a flag in the request to redirect back to the category index page + request._category_saved = True + + def response_add(self, request, obj, post_url_continue=None): + """ + Override to redirect back to the category index page after adding a new category + """ + if hasattr(request, '_category_saved') and request._category_saved: + from django.http import HttpResponseRedirect + from django.urls import reverse + return HttpResponseRedirect(reverse('admin:hadis_hadiscategory_changelist')) + return super().response_add(request, obj, post_url_continue) + + def response_change(self, request, obj): + """ + Override to redirect back to the category index page after editing a category + """ + if hasattr(request, '_category_saved') and request._category_saved: + from django.http import HttpResponseRedirect + from django.urls import reverse + return HttpResponseRedirect(reverse('admin:hadis_hadiscategory_changelist')) + return super().response_change(request, obj) + + + diff --git a/apps/hadis/admin/hadis.py b/apps/hadis/admin/hadis.py new file mode 100644 index 0000000..444918c --- /dev/null +++ b/apps/hadis/admin/hadis.py @@ -0,0 +1,161 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from dj_category.admin import BaseCategoryAdmin +from ajaxdatatable.admin import AjaxDatatable +from django.http import JsonResponse +from django.urls import path +from django.db.models import Q +from django.utils.safestring import mark_safe +from django.forms.widgets import RadioSelect + +from apps.hadis.models import * +from django import forms +from utils.json_editor_field import JsonEditorWidget + +# Define color choices +COLOR_CHOICES = [ + ('red', _('Red')), + ('blue', _('Blue')), + ('green', _('Green')), + ('yellow', _('Yellow')), + ('orange', _('Orange')), + ('purple', _('Purple')), + ('pink', _('Pink')), + ('brown', _('Brown')), + ('gray', _('Gray')), + ('black', _('Black')), +] + +class ColorRadioSelect(RadioSelect): + template_name = 'admin/widgets/color_radio.html' + option_template_name = 'admin/widgets/color_radio_option.html' + + +def get_links_schema(): + return { + 'type': "array", + 'format': 'table', + 'title': ' ', + 'items': { + 'type': 'object', + 'title': str(_('Link')), + 'properties': { + 'text': {'type': 'string', "format": "textarea",'title': str(_('text'))}, + 'link': {'type': 'string', "format": "textarea", 'title': str(_('link'))}, + } + } + } + +class HadisOverviewForm(forms.ModelForm): + status_color = forms.ChoiceField( + choices=COLOR_CHOICES, + widget=ColorRadioSelect(), + required=False + ) + + class Meta: + model = HadisOverview + fields = '__all__' + widgets = { + 'links': JsonEditorWidget(attrs={'schema': get_links_schema}), + } + + + + + +@admin.register(HadisTag) +class HadisTagAdmin(AjaxDatatable): + list_display = ['title', 'status'] + search_fields = ['title'] + + +class ReferenceImageInline(admin.TabularInline): + model = ReferenceImage + extra = 1 + verbose_name_plural = _('Reference Images') + fields = ('thumbnail', 'priority') + + +@admin.register(HadisReference) +class HadisReferenceAdmin(AjaxDatatable): + list_display = ['hadis', 'book', 'created_at'] + list_filter = ['book'] + search_fields = ['hadis__title', 'hadis__number', 'description'] + autocomplete_fields = ['hadis', 'book'] + readonly_fields = ['created_at'] + inlines = [ReferenceImageInline] + fieldsets = ( + (None, { + 'fields': ('hadis', 'book', 'description') + }), + ) + + + + +@admin.register(HadisOverview) +class HadisOverviewAdmin(AjaxDatatable): + change_form_template = 'admin/hadisowerview_change_form.html' + form = HadisOverviewForm + ordering = ['hadis__number'] + list_display = ['hadis', 'status', 'created_at'] + search_fields = ['hadis__title', 'hadis__number', 'status_text',] + autocomplete_fields = ['hadis', 'tags'] + fieldsets = ( + (None, { + 'fields': ('hadis', 'status', 'status_color', 'status_text') + }), + (_('Reference Information'), { + 'fields': ('address', 'share_link',), + }), + (_('Additional Information'), { + 'fields': ('links', 'tags', 'created_at'), + 'classes': ('collapse',), + }), + ) + + +class HadisOverviewInline(admin.StackedInline): + change_form_template = 'admin/hadisowerview_change_form.html' + form = HadisOverviewForm + model = HadisOverview + autocomplete_fields = ['tags', ] + can_delete = False + verbose_name_plural = _('Hadis Overview') + fieldsets = ( + (None, { + 'fields': ('status', 'status_color', 'status_text', 'address', 'share_link', 'links', 'tags',), + }), + ) + extra = 1 + min_num = 1 + + +@admin.register(Hadis) +class HadisAdmin(AjaxDatatable): + # form = HadisForm + list_display = ['number', 'title', 'category', 'status', 'created_at'] + list_filter = ['status', 'category'] + search_fields = ['title', 'text', 'number'] + readonly_fields = ['created_at', 'updated_at'] + autocomplete_fields = ['category'] + inlines = [HadisOverviewInline] + fieldsets = ( + (None, { + 'fields': ('number', 'title', 'category', 'status') + }), + (_('Content'), { + 'fields': ('text', 'translation'), + 'classes': ('collapse',), + }), + ) + + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if obj is None: + form.base_fields['category'].widget.can_add_related = False + + return form + diff --git a/apps/hadis/admin/transmitter.py b/apps/hadis/admin/transmitter.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/hadis/apps.py b/apps/hadis/apps.py new file mode 100644 index 0000000..47fcf3d --- /dev/null +++ b/apps/hadis/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HadisConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.hadis' diff --git a/apps/hadis/doc.py b/apps/hadis/doc.py new file mode 100644 index 0000000..6976da7 --- /dev/null +++ b/apps/hadis/doc.py @@ -0,0 +1,159 @@ +""" +Swagger documentation for the Hadis API endpoints. + +This module provides Swagger documentation for the Hadis API endpoints using drf-yasg. +It defines the request parameters, response schemas, and decorators for the views. +""" + +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + +from apps.hadis.models import HadisCategory + +# Parameter definitions +source_type_param = openapi.Parameter( + 'source_type', + openapi.IN_QUERY, + description="Filter categories by source type (shia or sunni)", + type=openapi.TYPE_STRING, + enum=[HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI], + required=False +) + +# Response schemas +tag_schema = openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Unique identifier for the tag" + ), + 'title': openapi.Schema( + type=openapi.TYPE_STRING, + description="Title of the tag" + ) + }, + required=['id', 'title'] +) + +category_schema = openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Unique identifier for the category" + ), + 'name': openapi.Schema( + type=openapi.TYPE_STRING, + description="Name of the category" + ), + 'hadis_count': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Number of hadis items in this category" + ), + 'source_type': openapi.Schema( + type=openapi.TYPE_STRING, + enum=[HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI], + description="Source type of the category (shia or sunni)" + ), + 'category_type': openapi.Schema( + type=openapi.TYPE_STRING, + enum=[HadisCategory.ContentType.QURAN, HadisCategory.ContentType.HADITH], + description="Content type of the category (quran or hadith)", + nullable=True + ), + 'children': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema(type=openapi.TYPE_OBJECT), # Recursive reference + description="List of child categories" + ) + }, + required=['id', 'name', 'hadis_count', 'source_type', 'children'] +) + +categories_response = openapi.Response( + description="Tree structure of hadis categories", + schema=openapi.Schema( + type=openapi.TYPE_ARRAY, + items=category_schema + ) +) + +hadis_schema = openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'number': openapi.Schema( + type=openapi.TYPE_INTEGER, + description="Unique number identifier for the hadis" + ), + 'title': openapi.Schema( + type=openapi.TYPE_STRING, + description="Title of the hadis" + ), + 'text': openapi.Schema( + type=openapi.TYPE_STRING, + description="Original text of the hadis" + ), + 'translation': openapi.Schema( + type=openapi.TYPE_STRING, + description="Translation of the hadis text" + ), + 'tags': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=tag_schema, + description="List of tags associated with this hadis" + ) + }, + required=['number', 'title', 'text', 'translation', 'tags'] +) + +hadis_list_response = openapi.Response( + description="List of hadis items in the specified category", + schema=openapi.Schema( + type=openapi.TYPE_ARRAY, + items=hadis_schema + ) +) + +# Swagger decorators for views +category_list_swagger = swagger_auto_schema( + operation_id="list_hadis_categories", + operation_description=""" + Retrieve a hierarchical tree structure of hadis categories. + + This endpoint returns all hadis categories in a tree structure, with parent categories + containing their child categories. Each category includes its ID, name, source type, + category type, and the count of hadis items it contains. + + The response can be filtered by source type (shia or sunni) using the query parameter. + If no source type is specified, all categories are returned. + """, + operation_summary="List Hadis Categories", + tags=["Hadis"], + manual_parameters=[source_type_param], + responses={ + 200: categories_response, + 401: "Authentication credentials were not provided or are invalid.", + 500: "Internal server error occurred." + } +) + +category_hadis_list_swagger = swagger_auto_schema( + operation_id="list_hadis_in_category", + operation_description=""" + Retrieve a list of hadis items belonging to a specific category. + + This endpoint returns all hadis items that belong to the specified category. + Each hadis item includes its number, title, original text, translation, and associated tags. + + The category is specified by its ID in the URL path. + """, + operation_summary="List Hadis Items in Category", + tags=["Hadis"], + responses={ + 200: hadis_list_response, + 401: "Authentication credentials were not provided or are invalid.", + 404: "The specified category does not exist.", + 500: "Internal server error occurred." + } +) \ No newline at end of file diff --git a/apps/hadis/migrations/0001_initial.py b/apps/hadis/migrations/0001_initial.py new file mode 100644 index 0000000..0cd0e90 --- /dev/null +++ b/apps/hadis/migrations/0001_initial.py @@ -0,0 +1,87 @@ +# Generated by Django 3.2.7 on 2025-03-16 23:50 + +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Hadis', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.PositiveIntegerField(unique=True, verbose_name='number')), + ('title', models.CharField(max_length=355, verbose_name='title')), + ('text', models.TextField(verbose_name='text')), + ('translation', models.TextField(blank=True, default='', verbose_name='translation')), + ('status', models.BooleanField(default=True, verbose_name='visibility')), + ('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': 'hadis', + 'verbose_name_plural': 'hadises', + }, + ), + migrations.CreateModel( + name='HadisTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=355, verbose_name='title')), + ], + ), + migrations.CreateModel( + name='HadisTagRelation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('priority', models.IntegerField(default=0, verbose_name='priority')), + ('hadis', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hadis.hadis', verbose_name='hadis')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hadis.hadistag', verbose_name='tag')), + ], + options={ + 'verbose_name': 'hadis tag relation', + 'verbose_name_plural': 'hadis tag relations', + 'unique_together': {('tag', 'hadis')}, + }, + ), + migrations.CreateModel( + name='HadisCategory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=512, verbose_name='name')), + ('is_active', models.BooleanField(default=True, verbose_name='is active')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('source_type', models.CharField(blank=True, choices=[('shia', 'Shia Sources'), ('sunni', 'Sunni Sources')], default='shia', max_length=10, verbose_name='Source Type')), + ('category_type', models.CharField(blank=True, choices=[('quran', 'Quran'), ('hadith', 'Hadith')], max_length=10, null=True, verbose_name='Category Content Type')), + ('title', models.CharField(max_length=355, verbose_name='title')), + ('order', models.IntegerField(default=0, verbose_name='order')), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='hadis.hadiscategory')), + ], + options={ + 'verbose_name': 'Hadis Category', + 'verbose_name_plural': 'Hadis Categories', + 'ordering': ('order',), + }, + ), + migrations.AddField( + model_name='hadis', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.hadiscategory', verbose_name='category'), + ), + migrations.AddField( + model_name='hadis', + name='tags', + field=models.ManyToManyField(related_name='hadises', through='hadis.HadisTagRelation', to='hadis.HadisTag', verbose_name='tags'), + ), + ] diff --git a/apps/hadis/migrations/0002_auto_20250317_0055.py b/apps/hadis/migrations/0002_auto_20250317_0055.py new file mode 100644 index 0000000..819cede --- /dev/null +++ b/apps/hadis/migrations/0002_auto_20250317_0055.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.7 on 2025-03-17 00:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='hadiscategory', + name='name', + ), + migrations.AlterField( + model_name='hadiscategory', + name='source_type', + field=models.CharField(blank=True, choices=[('shia', 'Shia'), ('sunni', 'Sunni')], default='shia', max_length=10, verbose_name='Source Type'), + ), + ] diff --git a/apps/hadis/migrations/0003_auto_20250317_0102.py b/apps/hadis/migrations/0003_auto_20250317_0102.py new file mode 100644 index 0000000..12cabcb --- /dev/null +++ b/apps/hadis/migrations/0003_auto_20250317_0102.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.7 on 2025-03-17 01:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0002_auto_20250317_0055'), + ] + + operations = [ + migrations.RemoveField( + model_name='hadiscategory', + name='title', + ), + migrations.AddField( + model_name='hadiscategory', + name='name', + field=models.CharField(default='1', max_length=355, verbose_name='name'), + preserve_default=False, + ), + ] diff --git a/apps/hadis/migrations/0004_auto_20250321_0119.py b/apps/hadis/migrations/0004_auto_20250321_0119.py new file mode 100644 index 0000000..1f4decf --- /dev/null +++ b/apps/hadis/migrations/0004_auto_20250321_0119.py @@ -0,0 +1,121 @@ +# Generated by Django 3.2.7 on 2025-03-21 01:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import filer.fields.image + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), + ('library', '0003_auto_20250321_0119'), + ('hadis', '0003_auto_20250317_0102'), + ] + + operations = [ + migrations.CreateModel( + name='HadisOverview', + fields=[ + ('hadis', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='hadis.hadis')), + ('status', models.CharField(max_length=50, verbose_name='status')), + ('status_color', models.CharField(max_length=25, verbose_name='Display Status Color')), + ('status_text', models.TextField(verbose_name='address')), + ('address', models.TextField(verbose_name='address')), + ('links', models.JSONField(blank=True, default=dict, null=True, verbose_name='title')), + ('share_link', models.CharField(blank=True, max_length=255, null=True, verbose_name='share link')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('book_reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='library.book', verbose_name='book reference')), + ], + ), + migrations.CreateModel( + name='HadisReference', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('book', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hadis_references', to='library.book', verbose_name='book')), + ], + options={ + 'verbose_name': 'Hadis Reference', + 'verbose_name_plural': 'Hadis References', + }, + ), + migrations.CreateModel( + name='HadisTransmitter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(default=0, help_text='Order in the chain of transmission', verbose_name='Order')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ], + options={ + 'verbose_name': 'Hadis Transmitter', + 'verbose_name_plural': 'Hadis Transmitters', + 'ordering': ('hadis', 'order'), + }, + ), + migrations.CreateModel( + name='ReferenceImage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('priority', models.IntegerField(default=0, help_text='Priority of the image, lower values mean higher priority.', verbose_name='Priority')), + ('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hadis.hadisreference', verbose_name='Hadis Reference')), + ('thumbnail', filer.fields.image.FilerImageField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.FILER_IMAGE_MODEL, verbose_name='thumbnail')), + ], + options={ + 'verbose_name': 'Reference Image', + 'verbose_name_plural': 'Reference Images', + 'ordering': ('priority',), + }, + ), + migrations.CreateModel( + name='Transmitters', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=255)), + ('birth_year_hijri', models.IntegerField(verbose_name='Birth Year (Hijri)')), + ('death_year_hijri', models.IntegerField(verbose_name='Death Year (Hijri)')), + ('description', models.TextField(blank=True, null=True, verbose_name='Description')), + ('status', models.CharField(max_length=50, verbose_name='status')), + ('status_color', models.CharField(max_length=25, verbose_name='Display Status Color')), + ('thumbnail', filer.fields.image.FilerImageField(blank=True, help_text='image allowed', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.FILER_IMAGE_MODEL)), + ], + ), + migrations.RemoveField( + model_name='hadis', + name='tags', + ), + migrations.AddField( + model_name='hadistag', + name='status', + field=models.BooleanField(default=True, verbose_name='status'), + ), + migrations.DeleteModel( + name='HadisTagRelation', + ), + migrations.AddField( + model_name='hadistransmitter', + name='hadis', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transmitters', to='hadis.hadis', verbose_name='hadis'), + ), + migrations.AddField( + model_name='hadistransmitter', + name='transmitter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hadises', to='hadis.transmitters', verbose_name='transmitter'), + ), + migrations.AddField( + model_name='hadisreference', + name='hadis', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='references', to='hadis.hadis', verbose_name='hadis'), + ), + migrations.AddField( + model_name='hadisoverview', + name='tags', + field=models.ManyToManyField(blank=True, related_name='hadises', to='hadis.HadisTag', verbose_name='tags'), + ), + migrations.AlterUniqueTogether( + name='hadistransmitter', + unique_together={('hadis', 'transmitter', 'order')}, + ), + ] diff --git a/apps/hadis/migrations/0005_auto_20250321_1550.py b/apps/hadis/migrations/0005_auto_20250321_1550.py new file mode 100644 index 0000000..1a8af1e --- /dev/null +++ b/apps/hadis/migrations/0005_auto_20250321_1550.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.7 on 2025-03-21 15:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0004_auto_20250321_0119'), + ] + + operations = [ + migrations.AlterModelOptions( + name='referenceimage', + options={'verbose_name': 'Reference Image', 'verbose_name_plural': 'Reference Images'}, + ), + migrations.RemoveField( + model_name='hadisoverview', + name='book_reference', + ), + ] diff --git a/apps/hadis/migrations/0006_auto_20250321_1600.py b/apps/hadis/migrations/0006_auto_20250321_1600.py new file mode 100644 index 0000000..d314b59 --- /dev/null +++ b/apps/hadis/migrations/0006_auto_20250321_1600.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.7 on 2025-03-21 16:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0005_auto_20250321_1550'), + ] + + operations = [ + migrations.AlterField( + model_name='hadisoverview', + name='address', + field=models.TextField(blank=True, null=True, verbose_name='address'), + ), + migrations.AlterField( + model_name='hadisoverview', + name='status_text', + field=models.TextField(blank=True, null=True, verbose_name='Status Text'), + ), + ] diff --git a/apps/hadis/migrations/__init__.py b/apps/hadis/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/hadis/models/__init__.py b/apps/hadis/models/__init__.py new file mode 100644 index 0000000..ba8ce70 --- /dev/null +++ b/apps/hadis/models/__init__.py @@ -0,0 +1,3 @@ +from .category import * +from .hadis import * +from .transmitter import * \ No newline at end of file diff --git a/apps/hadis/models/category.py b/apps/hadis/models/category.py new file mode 100644 index 0000000..7fde53e --- /dev/null +++ b/apps/hadis/models/category.py @@ -0,0 +1,105 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from dj_category.models import BaseCategoryAbstract + + +class HadisCategory(BaseCategoryAbstract): + class SourceType(models.TextChoices): + SHIA = 'shia', _('Shia') + SUNNI = 'sunni', _('Sunni') + + class ContentType(models.TextChoices): + QURAN = 'quran', _('Quran') + HADITH = 'hadith', _('Hadith') + + class LevelChoices(models.IntegerChoices): + LEVEL_1 = 1, _('Level 1 (Root)') + LEVEL_2 = 2, _('Level 2 (Child)') + LEVEL_3 = 3, _('Level 3 (Grandchild)') + + source_type = models.CharField(max_length=10, choices=SourceType.choices, default=SourceType.SHIA, verbose_name=_('Source Type'), blank=True) + category_type = models.CharField(max_length=10, choices=ContentType.choices, verbose_name=_('Category Content Type'), blank=True, null=True) + name = models.CharField(max_length=355, verbose_name=_('name')) + order = models.IntegerField(default=0, verbose_name=_('order')) + slug = None + content_type = None + language = None + language_id = None + + # This field is not stored in the database, it's only used for the form + level_choice = None + + class Meta: + verbose_name = _('Hadis Category') + verbose_name_plural = _('Hadis Categories') + ordering = ('order',) + + def __str__(self): + return f'<{str(self.level_p)}>{self.name}' + + def __repr__(self): + return f'<{str(self.level_p)}>{self.name}' + + def clean(self): + super().clean() + + # Skip validation for new objects that haven't been saved yet + # This allows the admin form to set these values properly + if self.pk is None: + return + + # For existing objects, apply the validation rules + if self.level_p == 1 and self.category_type: + raise ValidationError(_("Level 1 cannot have content type")) + + if self.level_p == 2 and not self.category_type: + raise ValidationError(_("Level 2 must have content type")) + + if self.level_p == 3 and (self.source_type or self.category_type): + raise ValidationError(_("Level 3 cannot have source/content type")) + + + def save(self, *args, **kwargs): + self.clean() + + # Get the level from the parent structure + level = self.level_p + + # Apply level-specific logic + # if level == 2 and self.parent: + # For level 2, inherit source_type from parent + # self.source_type = self.parent.source_type + # elif level == 3: + # For level 3, inherit both from parent + # if self.parent and self.parent.parent: + # self.source_type = self.parent.source_type + # self.category_type = self.parent.category_type + + # Call the parent class's save method + super().save(*args, **kwargs) + + @property + def level_p(self): + if not self.parent: + return 1 + elif not self.parent.parent: + return 2 + else: + return 3 + def get_level_info(self): + info = { + 'level': self.level_p, + 'source_type': None, + 'category_type': None, + } + if self.level_p == 1: + info['source_type'] = self.source_type + elif self.level_p == 2: + info['source_type'] = self.parent.source_type + info['category_type'] = self.category_type + return info + + + + diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py new file mode 100644 index 0000000..91c8523 --- /dev/null +++ b/apps/hadis/models/hadis.py @@ -0,0 +1,102 @@ + + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from filer.fields.image import FilerImageField + + +class HadisTag(models.Model): + title = models.CharField(max_length=355, verbose_name=_('title')) + status = models.BooleanField(default=True, verbose_name=_('status')) + + def __str__(self): + return f"{self.title}" + + + + +class Hadis(models.Model): + number = models.PositiveIntegerField(verbose_name=_('number'), unique=True) + title = models.CharField(max_length=355, verbose_name=_('title')) + text = models.TextField(verbose_name=_('text')) + translation = models.TextField(verbose_name=_('translation'), blank=True, default='') + + category = models.ForeignKey("hadis.HadisCategory", null=True, on_delete=models.SET_NULL, verbose_name=_('category'), ) + + status = models.BooleanField(default=True, verbose_name=_('visibility')) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + + def __str__(self): + return f"<{self.number}> {self.title[:32]}" + + @property + def get_tags(self): + return self.tags.all().order_by('hadistagrelation__priority') + + class Meta: + verbose_name = _('hadis') + verbose_name_plural = _('hadises') + + +class HadisOverview(models.Model): + hadis = models.OneToOneField(Hadis, on_delete=models.CASCADE, primary_key=True) + status = models.CharField(max_length=50, verbose_name=_('status')) + status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color')) + status_text = models.TextField(verbose_name=_('Status Text'), null=True, blank=True) + address = models.TextField(verbose_name=_('address'), null=True, blank=True) + links = models.JSONField(verbose_name=_('title'), null=True, blank=True, default=dict) + tags = models.ManyToManyField("HadisTag", related_name="hadises", verbose_name=_('tags'), blank=True) + share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + + +class HadisReference(models.Model): + hadis = models.ForeignKey( + Hadis, + on_delete=models.CASCADE, + verbose_name=_('hadis'), + related_name='references' + ) + book = models.ForeignKey("library.Book", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('book'), related_name='hadis_references') + description = models.TextField(verbose_name=_('description'), blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + + class Meta: + verbose_name = _('Hadis Reference') + verbose_name_plural = _('Hadis References') + + def __str__(self): + return f'{self.hadis.number}-{self.book.title}' + +class ReferenceImage(models.Model): + reference = models.ForeignKey(HadisReference, verbose_name="Hadis Reference", on_delete=models.CASCADE) + thumbnail = FilerImageField( + related_name='+', on_delete=models.PROTECT, null=True, blank=True, + verbose_name=_('thumbnail') + ) + priority = models.IntegerField( + default=0, + verbose_name=_("Priority"), + help_text=_("Priority of the image, lower values mean higher priority.") + ) + + + class Meta: + verbose_name = _('Reference Image') + verbose_name_plural = _('Reference Images') + + def __str__(self): + return f'{self.reference.title}-{self.id}' + + def save(self, *args, **kwargs): + if ReferenceImage.objects.filter(reference=self.reference, priority=self.priority).exists(): + ReferenceImage.objects.filter( + reference=self.reference, + priority__gte=self.priority + ).update(priority=F('priority') + 1) + + super().save(*args, **kwargs) + diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py new file mode 100644 index 0000000..08d7e48 --- /dev/null +++ b/apps/hadis/models/transmitter.py @@ -0,0 +1,52 @@ + + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from filer.fields.image import FilerImageField + + + +class Transmitters(models.Model): + full_name = models.CharField(max_length=255) + birth_year_hijri = models.IntegerField(verbose_name="Birth Year (Hijri)") + death_year_hijri = models.IntegerField(verbose_name="Death Year (Hijri)") + description = models.TextField(blank=True, null=True, verbose_name="Description") + status = models.CharField(max_length=50, verbose_name=_('status')) + status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color')) + thumbnail = FilerImageField(related_name="+", on_delete=models.CASCADE, help_text=_( + 'image allowed' + ), null=True, blank=True) + + def __str__(self): + return self.full_name + + +class HadisTransmitter(models.Model): + hadis = models.ForeignKey( + "hadis.Hadis", + on_delete=models.CASCADE, + verbose_name=_('hadis'), + related_name='transmitters' + ) + transmitter = models.ForeignKey( + Transmitters, + on_delete=models.CASCADE, + verbose_name=_('transmitter'), + related_name='hadises' + ) + order = models.PositiveIntegerField( + default=0, + verbose_name=_('Order'), + help_text=_('Order in the chain of transmission') + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + + class Meta: + verbose_name = _('Hadis Transmitter') + verbose_name_plural = _('Hadis Transmitters') + ordering = ('hadis', 'order') + unique_together = ('hadis', 'transmitter', 'order') + + def __str__(self): + return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order})' diff --git a/apps/hadis/serializers.py b/apps/hadis/serializers.py new file mode 100644 index 0000000..3ee2628 --- /dev/null +++ b/apps/hadis/serializers.py @@ -0,0 +1,50 @@ + +from rest_framework import serializers + +from apps.hadis.models import * + + +class HadisCategorySerializer(serializers.ModelSerializer): + children = serializers.SerializerMethodField('get_children') + name = serializers.SerializerMethodField() + hadis_count = serializers.SerializerMethodField() + source_type = serializers.CharField(read_only=True) + + + def get_children(self, obj): + return [self.to_dict(cat) for cat in obj.get_children()] + + def to_dict(self, c): + children = c.get_children() + + return { + 'id': c.id, + 'name': c.name, + 'hadis_count': c.hadis_count, + 'source_type': c.source_type, + 'category_type': c.category_type, + 'children': [] if not children else [self.to_dict(i) for i in children], + } + + class Meta: + model = HadisCategory + fields = ['id', 'name', 'hadis_count', 'source_type','children'] + + +class HadisTagSerializer(serializers.ModelSerializer): + class Meta: + model = HadisTag + fields = ('id', 'title') + + +class HadisSerializer(serializers.ModelSerializer): + translation = serializers.CharField(source='translation') + text = serializers.CharField(source='text') + tags = serializers.SerializerMethodField() + + def get_tags(self, obj): + return HadisTagSerializer(obj.get_tags, many=True).data + + class Meta: + model = Hadis + fields = ('number', 'title', 'text', 'translation', 'tags') \ No newline at end of file diff --git a/apps/hadis/templates/admin/category_index.html b/apps/hadis/templates/admin/category_index.html new file mode 100644 index 0000000..a3653c0 --- /dev/null +++ b/apps/hadis/templates/admin/category_index.html @@ -0,0 +1,2343 @@ +{% extends 'admin/change_form.html' %} +{% load i18n static admin_modify mptt_tags %} + +{% block content %} +
+ {% trans "Make your category and sort it by drag and drop . and try to edit items by double click." %} +
+ + +