diff --git a/apps/hadis/migrations/0065_country_and_publisher_models.py b/apps/hadis/migrations/0065_country_and_publisher_models.py new file mode 100644 index 0000000..65d1a13 --- /dev/null +++ b/apps/hadis/migrations/0065_country_and_publisher_models.py @@ -0,0 +1,61 @@ +# Generated by Django 4.2.30 on 2026-10-21 10:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0064_transmitter_profession_and_relations'), + ] + + operations = [ + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.JSONField(default=list, verbose_name='Title')), + ('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug')), + ('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': 'Country', + 'verbose_name_plural': 'Countries', + 'ordering': ('-created_at', '-id'), + }, + ), + migrations.CreateModel( + name='BookPublisher', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.JSONField(default=list, verbose_name='Title')), + ('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('city', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publishers', to='hadis.city', verbose_name='City')), + ('country', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publishers', to='hadis.country', verbose_name='Country')), + ], + options={ + 'verbose_name': 'Book Publisher', + 'verbose_name_plural': 'Book Publishers', + 'ordering': ('-created_at', '-id'), + }, + ), + migrations.AddField( + model_name='bookedition', + name='country_of_publication_country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publication_editions', to='hadis.country', verbose_name='Country of Publication (Country)'), + ), + migrations.AddField( + model_name='bookedition', + name='publisher_item', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='editions', to='hadis.bookpublisher', verbose_name='Publisher Entity'), + ), + migrations.AddField( + model_name='city', + name='country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cities', to='hadis.country', verbose_name='Country'), + ), + ] diff --git a/apps/hadis/models/reference.py b/apps/hadis/models/reference.py index d62b422..d75fea3 100644 --- a/apps/hadis/models/reference.py +++ b/apps/hadis/models/reference.py @@ -239,12 +239,69 @@ class BookReference(LowercaseSlugMixin, models.Model): + +class BookPublisher(LowercaseSlugMixin, models.Model): + slug_source_field = 'title' + + title = models.JSONField(default=list, verbose_name=_('Title')) + slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, null=True, allow_unicode=True) + city = models.ForeignKey( + 'hadis.City', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='publishers', + verbose_name=_('City') + ) + country = models.ForeignKey( + 'hadis.Country', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='publishers', + verbose_name=_('Country') + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + + class Meta: + ordering = ('-created_at', '-id') + verbose_name = _('Book Publisher') + verbose_name_plural = _('Book Publishers') + + def __str__(self): + if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict): + return self.title[0].get('text') or self.title[0].get('title') or str(self.id) + return str(self.id) + + def get_title(self, lang: str = 'en') -> Optional[str]: + if not self.title or not isinstance(self.title, list): + return None + for tr in self.title: + if isinstance(tr, dict) and tr.get('language_code') == lang: + return tr.get('text', '') or tr.get('title', '') + for tr in self.title: + if isinstance(tr, dict) and tr.get('language_code') == 'en': + return tr.get('text', '') or tr.get('title', '') + if isinstance(self.title[0], dict): + return self.title[0].get('text', '') or self.title[0].get('title', '') + return None + + class BookEdition(models.Model): """ Model representing a specific edition or print of a book reference. """ book_reference = models.ForeignKey(BookReference, on_delete=models.CASCADE, related_name='editions', verbose_name=_('book reference')) publisher = models.JSONField(default=list, blank=True, verbose_name=_('Publisher')) + publisher_item = models.ForeignKey( + 'hadis.BookPublisher', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='editions', + verbose_name=_('Publisher Entity') + ) city_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('City of Publication')) city_of_publication_city = models.ForeignKey( 'hadis.City', @@ -255,6 +312,14 @@ class BookEdition(models.Model): verbose_name=_('City of Publication (City)'), ) country_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('Country of Publication')) + country_of_publication_country = models.ForeignKey( + 'hadis.Country', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='publication_editions', + verbose_name=_('Country of Publication (Country)'), + ) edition_number = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Edition Number')) year_of_publication = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('year of publication')) number_of_volumes = models.PositiveIntegerField(null=True, blank=True, verbose_name=_('Total Number of Volumes')) diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index aa96f64..318552e 100644 --- a/apps/hadis/models/transmitter.py +++ b/apps/hadis/models/transmitter.py @@ -143,6 +143,15 @@ class City(LowercaseSlugMixin, models.Model): created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + country = models.ForeignKey( + 'hadis.Country', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='cities', + verbose_name=_('Country') + ) + class Meta: ordering = ('-created_at', '-id') verbose_name = _('City') @@ -166,6 +175,38 @@ class City(LowercaseSlugMixin, models.Model): return self.title[0].get('text', '') or self.title[0].get('title', '') return None +class Country(LowercaseSlugMixin, models.Model): + slug_source_field = 'title' + + title = models.JSONField(default=list, verbose_name=_('Title')) + slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, null=True, allow_unicode=True) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + + class Meta: + ordering = ('-created_at', '-id') + verbose_name = _('Country') + verbose_name_plural = _('Countries') + + def __str__(self): + if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict): + return self.title[0].get('text') or self.title[0].get('title') or str(self.id) + return str(self.id) + + def get_title(self, lang: str = 'en') -> Optional[str]: + if not self.title or not isinstance(self.title, list): + return None + for tr in self.title: + if isinstance(tr, dict) and tr.get('language_code') == lang: + return tr.get('text', '') or tr.get('title', '') + for tr in self.title: + if isinstance(tr, dict) and tr.get('language_code') == 'en': + return tr.get('text', '') or tr.get('title', '') + if isinstance(self.title[0], dict): + return self.title[0].get('text', '') or self.title[0].get('title', '') + return None + + class TransmitterProfession(LowercaseSlugMixin, models.Model): slug_source_field = 'title' diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 41c7ec3..df68777 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -8,9 +8,9 @@ from utils.image_compression import maybe_compress_uploaded_file from utils import absolute_https_url from ..models import ( Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, ReferenceImage, - BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, + BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookPublisher, BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, ScholarlyOpinion, TransmitterOpinion, TransmitterOriginalText, - City, Kunya, TransmitterFeature, TransmitterProfession, SahabiStatus, Madhhab, RelationType, YearValue, NarratorDate, + City, Country, Kunya, TransmitterFeature, TransmitterProfession, SahabiStatus, Madhhab, RelationType, YearValue, NarratorDate, CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, OriginalTextReference, OriginalTextReferenceImage, ContentRelease @@ -2017,15 +2017,46 @@ class AdminYearValueSerializer(serializers.ModelSerializer): return instance -class AdminCitySerializer(serializers.ModelSerializer): +class AdminCountrySerializer(serializers.ModelSerializer): title = serializers.JSONField() class Meta: - model = City + model = Country fields = ["id", "title", "slug", "created_at", "updated_at"] read_only_fields = ["id", "slug", "created_at", "updated_at"] +class AdminCitySerializer(serializers.ModelSerializer): + title = serializers.JSONField() + country_detail = AdminCountrySerializer(source="country", read_only=True) + + class Meta: + model = City + fields = ["id", "title", "slug", "country", "country_detail", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "country_detail", "created_at", "updated_at"] + + +class AdminBookPublisherSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + city_detail = AdminCitySerializer(source="city", read_only=True) + country_detail = AdminCountrySerializer(source="country", read_only=True) + + class Meta: + model = BookPublisher + fields = [ + "id", + "title", + "slug", + "city", + "city_detail", + "country", + "country_detail", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "slug", "city_detail", "country_detail", "created_at", "updated_at"] + + class AdminKunyaSerializer(serializers.ModelSerializer): title = serializers.JSONField() @@ -3645,6 +3676,12 @@ class AdminBookEditorSerializer(serializers.ModelSerializer): class AdminBookEditionSerializer(serializers.ModelSerializer): publisher = serializers.JSONField(required=False, default=list) city_of_publication = serializers.JSONField(required=False, default=list) + publisher_item = serializers.PrimaryKeyRelatedField( + queryset=BookPublisher.objects.all(), + required=False, + allow_null=True, + ) + publisher_item_detail = AdminBookPublisherSerializer(source="publisher_item", read_only=True) city_of_publication_city = serializers.PrimaryKeyRelatedField( queryset=City.objects.all(), required=False, @@ -3652,6 +3689,12 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): ) city_of_publication_city_detail = AdminCitySerializer(source="city_of_publication_city", read_only=True) country_of_publication = serializers.JSONField(required=False, default=list) + country_of_publication_country = serializers.PrimaryKeyRelatedField( + queryset=Country.objects.all(), + required=False, + allow_null=True, + ) + country_of_publication_country_detail = AdminCountrySerializer(source="country_of_publication_country", read_only=True) notes = serializers.JSONField(required=False, default=list) tag = serializers.PrimaryKeyRelatedField( queryset=BookTag.objects.all(), @@ -3672,10 +3715,14 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): "id", "book_reference", "publisher", + "publisher_item", + "publisher_item_detail", "city_of_publication", "city_of_publication_city", "city_of_publication_city_detail", "country_of_publication", + "country_of_publication_country", + "country_of_publication_country_detail", "edition_number", "year_of_publication", "number_of_volumes", diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index cb0badf..0c2f6a4 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -45,6 +45,8 @@ from .views_admin import ( AdminContentReleaseViewSet, DovodiCityViewSet, + DovodiCountryViewSet, + DovodiBookPublisherViewSet, DovodiKunyaViewSet, DovodiTransmitterFeatureViewSet, DovodiTransmitterProfessionViewSet, @@ -149,6 +151,10 @@ admin_router.register(r'content-version', AdminContentReleaseViewSet, basename=' admin_router.register(r'dovodi-cities', DovodiCityViewSet, basename='dovodi-cities') admin_router.register(r'cities', DovodiCityViewSet, basename='admin-cities') +admin_router.register(r'dovodi-countries', DovodiCountryViewSet, basename='dovodi-countries') +admin_router.register(r'countries', DovodiCountryViewSet, basename='admin-countries') +admin_router.register(r'dovodi-publishers', DovodiBookPublisherViewSet, basename='dovodi-publishers') +admin_router.register(r'publishers', DovodiBookPublisherViewSet, basename='admin-publishers') admin_router.register(r'dovodi-kunyas', DovodiKunyaViewSet, basename='dovodi-kunyas') admin_router.register(r'kunyas', DovodiKunyaViewSet, basename='admin-kunyas') admin_router.register(r'dovodi-transmitter-features', DovodiTransmitterFeatureViewSet, basename='dovodi-transmitter-features') diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 5172ea1..ba645d7 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -37,6 +37,8 @@ from .models import ( CategoryQuranVerse, City, + Country, + BookPublisher, Kunya, TransmitterFeature, @@ -130,6 +132,8 @@ from .serializers.serializers_admin import ( AdminContentReleaseSerializer, AdminCitySerializer, + AdminCountrySerializer, + AdminBookPublisherSerializer, AdminKunyaSerializer, AdminTransmitterFeatureSerializer, @@ -2878,6 +2882,50 @@ class AdminContentReleaseViewSet(ModelViewSet): + +class DovodiCountryViewSet(ModelViewSet): + serializer_class = AdminCountrySerializer + permission_classes = [IsAuthenticated, IsSuperAdmin] + authentication_classes = [TokenAuthentication] + pagination_class = OptionalPagination + parser_classes = (JSONParser, MultiPartParser, FormParser) + + def get_queryset(self): + queryset = Country.objects.all().order_by("slug") + search_query = self.request.query_params.get("search") + if search_query: + queryset = queryset.filter( + Q(title__icontains=search_query) + | Q(slug__icontains=search_query) + ) + return queryset + + +class DovodiBookPublisherViewSet(ModelViewSet): + serializer_class = AdminBookPublisherSerializer + permission_classes = [IsAuthenticated, IsSuperAdmin] + authentication_classes = [TokenAuthentication] + pagination_class = OptionalPagination + parser_classes = (JSONParser, MultiPartParser, FormParser) + + def get_queryset(self): + queryset = BookPublisher.objects.select_related("city", "country").all().order_by("slug") + search_query = self.request.query_params.get("search") + if search_query: + queryset = queryset.filter( + Q(title__icontains=search_query) + | Q(slug__icontains=search_query) + | Q(city__title__icontains=search_query) + | Q(country__title__icontains=search_query) + ) + city_id = self.request.query_params.get("city") + if city_id and city_id != "all": + queryset = queryset.filter(city_id=city_id) + country_id = self.request.query_params.get("country") + if country_id and country_id != "all": + queryset = queryset.filter(country_id=country_id) + return queryset + class DovodiCityViewSet(ModelViewSet): serializer_class = AdminCitySerializer @@ -2894,7 +2942,7 @@ class DovodiCityViewSet(ModelViewSet): def get_queryset(self): - queryset = City.objects.all().order_by("slug") + queryset = City.objects.all().select_related("country").order_by("slug") search_query = self.request.query_params.get("search") @@ -2908,6 +2956,10 @@ class DovodiCityViewSet(ModelViewSet): ) + country_id = self.request.query_params.get("country") or self.request.query_params.get("country_id") + if country_id: + queryset = queryset.filter(country_id=country_id) + return queryset