diff --git a/apps/library/admin.py b/apps/library/admin.py index 6a89fd3..2cceaf1 100644 --- a/apps/library/admin.py +++ b/apps/library/admin.py @@ -253,7 +253,10 @@ class AuthorAdmin(ModelAdmin): fieldsets = ( (None, { - 'fields': ('full_name', 'slug', 'thumbnail', 'birth_date', 'death_date') + 'fields': ('full_name', 'slug', 'thumbnail', 'image', 'birth_date', 'death_date', 'date_of_birth', 'date_of_death') + }), + (_('Publication Info'), { + 'fields': ('researcher_editor', 'publisher', 'publication_place', 'edition_statement', 'year', 'number_of_volumes', 'online_source', 'tag', 'notes') }), (_('Timestamps'), { 'fields': ('created_at', 'updated_at'), diff --git a/apps/library/doc.py b/apps/library/doc.py index 99ead32..b566da9 100644 --- a/apps/library/doc.py +++ b/apps/library/doc.py @@ -57,6 +57,14 @@ search_param = openapi.Parameter( required=False ) +author_search_param = openapi.Parameter( + 'search', + openapi.IN_QUERY, + description="Search authors by full name", + type=openapi.TYPE_STRING, + required=False +) + thumbs_schema = openapi.Schema( type=openapi.TYPE_OBJECT, @@ -75,6 +83,7 @@ author_summary_schema = openapi.Schema( 'full_name': openapi.Schema(type=openapi.TYPE_STRING), 'slug': openapi.Schema(type=openapi.TYPE_STRING), 'thumbnail': thumbs_schema, + 'image': thumbs_schema, } ) @@ -143,8 +152,20 @@ author_schema = openapi.Schema( 'full_name': openapi.Schema(type=openapi.TYPE_STRING, description="Full name of the author"), 'slug': openapi.Schema(type=openapi.TYPE_STRING, description="URL-friendly slug for the author"), 'thumbnail': thumbs_schema, + 'image': thumbs_schema, 'birth_date': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE, nullable=True, description="Birth date"), 'death_date': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE, nullable=True, description="Death date"), + 'date_of_birth': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Date of birth (text/historical format)"), + 'date_of_death': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Date of death (text/historical format)"), + 'researcher_editor': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Researcher / Editor"), + 'publisher': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Publisher"), + 'publication_place': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Publication Place"), + 'edition_statement': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Edition Statement"), + 'year': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Year"), + 'number_of_volumes': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Number of Volumes"), + 'notes': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Notes"), + 'online_source': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI, nullable=True, description="Online Source"), + 'tag': openapi.Schema(type=openapi.TYPE_STRING, nullable=True, description="Tag"), 'books_count': openapi.Schema(type=openapi.TYPE_INTEGER, description="Number of active books"), }, required=['id', 'full_name', 'slug', 'books_count'] @@ -263,9 +284,12 @@ author_list_swagger = swagger_auto_schema( Retrieve a paginated list of library authors. Each author includes thumbnail, full name, and number of active books. + + You can search authors by their full name using the `search` query parameter. """, operation_summary="List Authors", tags=["Dobodbi - Library"], + manual_parameters=[author_search_param], responses={ 200: authors_response, 401: "Authentication credentials were not provided or are invalid.", diff --git a/apps/library/management/commands/populate_author_fields.py b/apps/library/management/commands/populate_author_fields.py new file mode 100644 index 0000000..bcc516f --- /dev/null +++ b/apps/library/management/commands/populate_author_fields.py @@ -0,0 +1,79 @@ +from django.core.management.base import BaseCommand +from django.db import transaction +from apps.library.models import Author + +class Command(BaseCommand): + help = 'Populates the newly added fields for all existing authors with sample English data' + + def handle(self, *args, **options): + self.stdout.write(self.style.WARNING('--- Starting Author Fields Population ---')) + + authors = Author.objects.all() + if not authors.exists(): + self.stdout.write(self.style.ERROR('No authors found in the database.')) + return + + self.stdout.write(self.style.SUCCESS(f'Found {authors.count()} authors. Commencing update...')) + + updated_count = 0 + with transaction.atomic(): + for author in authors: + # 1. Researcher / Editor + if not author.researcher_editor: + author.researcher_editor = f"Edited and researched by {author.full_name}" + + # 2. Publisher + if not author.publisher: + author.publisher = "Imam Javad Publishing Institute" + + # 3. Publication Place + if not author.publication_place: + author.publication_place = "Qom, Iran" + + # 4. Edition Statement + if not author.edition_statement: + author.edition_statement = "First Edition (Revised)" + + # 5. Year + if not author.year: + author.year = "1448 AH" + + # 6. Number of Volumes + if not author.number_of_volumes: + author.number_of_volumes = 5 + + # 7. Notes + if not author.notes: + author.notes = f"This edition contains annotations and key commentaries regarding the works of {author.full_name}." + + # 8. Online Source + if not author.online_source: + author.online_source = "https://imamjavad.nwhco.ir" + + # 9. Tag + if not author.tag: + author.tag = "Reference Books" + + # 10. Copy thumbnail to image if thumbnail exists and image doesn't + if author.thumbnail and not author.image: + author.image = author.thumbnail + + # 11. Date of Birth (text) + if not author.date_of_birth: + if author.birth_date: + author.date_of_birth = f"{author.birth_date.year} AD" + else: + author.date_of_birth = "Around 4th Century AH" + + # 12. Date of Death (text) + if not author.date_of_death: + if author.death_date: + author.date_of_death = f"{author.death_date.year} AD" + else: + author.date_of_death = "Around 5th Century AH" + + author.save() + updated_count += 1 + self.stdout.write(f"Updated author: {author.full_name}") + + self.stdout.write(self.style.SUCCESS(f'--- Finished! Successfully updated {updated_count} authors ---')) diff --git a/apps/library/migrations/0005_author_date_of_birth_author_date_of_death_and_more.py b/apps/library/migrations/0005_author_date_of_birth_author_date_of_death_and_more.py new file mode 100644 index 0000000..514af86 --- /dev/null +++ b/apps/library/migrations/0005_author_date_of_birth_author_date_of_death_and_more.py @@ -0,0 +1,73 @@ +# Generated by Django 4.2.30 on 2026-07-16 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0004_alter_author_slug_alter_book_slug_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='author', + name='date_of_birth', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Date of Birth'), + ), + migrations.AddField( + model_name='author', + name='date_of_death', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Date of Death'), + ), + migrations.AddField( + model_name='author', + name='edition_statement', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Edition Statement'), + ), + migrations.AddField( + model_name='author', + name='image', + field=models.ImageField(blank=True, help_text='image allowed', null=True, upload_to='author_images/', verbose_name='Image'), + ), + migrations.AddField( + model_name='author', + name='notes', + field=models.TextField(blank=True, null=True, verbose_name='Notes'), + ), + migrations.AddField( + model_name='author', + name='number_of_volumes', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Number of Volumes'), + ), + migrations.AddField( + model_name='author', + name='online_source', + field=models.URLField(blank=True, max_length=512, null=True, verbose_name='Online Source'), + ), + migrations.AddField( + model_name='author', + name='publication_place', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Publication Place'), + ), + migrations.AddField( + model_name='author', + name='publisher', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Publisher'), + ), + migrations.AddField( + model_name='author', + name='researcher_editor', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Researcher / Editor'), + ), + migrations.AddField( + model_name='author', + name='tag', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Tag'), + ), + migrations.AddField( + model_name='author', + name='year', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Year'), + ), + ] diff --git a/apps/library/migrations/0006_alter_author_number_of_volumes.py b/apps/library/migrations/0006_alter_author_number_of_volumes.py new file mode 100644 index 0000000..7c06a0f --- /dev/null +++ b/apps/library/migrations/0006_alter_author_number_of_volumes.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.30 on 2026-07-16 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0005_author_date_of_birth_author_date_of_death_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='author', + name='number_of_volumes', + field=models.IntegerField(blank=True, null=True, verbose_name='Number of Volumes'), + ), + ] diff --git a/apps/library/models.py b/apps/library/models.py index f50b8cf..2acc6ab 100644 --- a/apps/library/models.py +++ b/apps/library/models.py @@ -97,6 +97,24 @@ class Author(LowercaseSlugMixin, models.Model): ) birth_date = models.DateField(null=True, blank=True, verbose_name=_('Birth Date')) death_date = models.DateField(null=True, blank=True, verbose_name=_('Death Date')) + researcher_editor = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Researcher / Editor')) + publisher = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Publisher')) + publication_place = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Publication Place')) + edition_statement = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Edition Statement')) + year = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Year')) + number_of_volumes = models.IntegerField(null=True, blank=True, verbose_name=_('Number of Volumes')) + notes = models.TextField(null=True, blank=True, verbose_name=_('Notes')) + online_source = models.URLField(max_length=512, null=True, blank=True, verbose_name=_('Online Source')) + tag = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Tag')) + image = models.ImageField( + upload_to='author_images/', + null=True, + blank=True, + help_text=_('image allowed'), + verbose_name=_('Image') + ) + date_of_birth = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Date of Birth')) + date_of_death = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Date of Death')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) diff --git a/apps/library/serializers.py b/apps/library/serializers.py index 67ca24d..2b0d085 100644 --- a/apps/library/serializers.py +++ b/apps/library/serializers.py @@ -49,30 +49,47 @@ class PinnedBookCollectionSerializer(serializers.ModelSerializer): class AuthorSummarySerializer(serializers.ModelSerializer): thumbnail = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() class Meta: model = Author - fields = ('id', 'full_name', 'slug', 'thumbnail') + fields = ('id', 'full_name', 'slug', 'thumbnail', 'image') def get_thumbnail(self, obj): if obj.thumbnail: return get_thumbs(obj.thumbnail, self.context.get('request')) return None + def get_image(self, obj): + if obj.image: + return get_thumbs(obj.image, self.context.get('request')) + return None + class AuthorListSerializer(serializers.ModelSerializer): thumbnail = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() books_count = serializers.SerializerMethodField() class Meta: model = Author - fields = ('id', 'full_name', 'slug', 'thumbnail', 'books_count') + fields = ( + 'id', 'full_name', 'slug', 'thumbnail', 'image', 'books_count', + 'birth_date', 'death_date', 'date_of_birth', 'date_of_death', + 'researcher_editor', 'publisher', 'publication_place', 'edition_statement', + 'year', 'number_of_volumes', 'notes', 'online_source', 'tag' + ) def get_thumbnail(self, obj): if obj.thumbnail: return get_thumbs(obj.thumbnail, self.context.get('request')) return None + def get_image(self, obj): + if obj.image: + return get_thumbs(obj.image, self.context.get('request')) + return None + def get_books_count(self, obj): if hasattr(obj, 'books_count_annotation'): return obj.books_count_annotation @@ -81,17 +98,28 @@ class AuthorListSerializer(serializers.ModelSerializer): class AuthorDetailSerializer(serializers.ModelSerializer): thumbnail = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() books_count = serializers.SerializerMethodField() class Meta: model = Author - fields = ('id', 'full_name', 'slug', 'thumbnail', 'birth_date', 'death_date', 'books_count') + fields = ( + 'id', 'full_name', 'slug', 'thumbnail', 'image', 'birth_date', 'death_date', + 'date_of_birth', 'date_of_death', 'researcher_editor', 'publisher', + 'publication_place', 'edition_statement', 'year', 'number_of_volumes', + 'notes', 'online_source', 'tag', 'books_count' + ) def get_thumbnail(self, obj): if obj.thumbnail: return get_thumbs(obj.thumbnail, self.context.get('request')) return None + def get_image(self, obj): + if obj.image: + return get_thumbs(obj.image, self.context.get('request')) + return None + def get_books_count(self, obj): if hasattr(obj, 'books_count_annotation'): return obj.books_count_annotation diff --git a/apps/library/serializers_admin.py b/apps/library/serializers_admin.py index 531a407..8e2b96b 100644 --- a/apps/library/serializers_admin.py +++ b/apps/library/serializers_admin.py @@ -63,12 +63,20 @@ class AdminLibraryCollectionSerializer(serializers.ModelSerializer): class AdminAuthorSerializer(serializers.ModelSerializer): thumbnail = AbsoluteImageField(required=False, allow_null=True) + image = AbsoluteImageField(required=False, allow_null=True) books_count = serializers.SerializerMethodField() remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) + remove_image = serializers.BooleanField(write_only=True, required=False, default=False) class Meta: model = Author - fields = ["id", "full_name", "slug", "thumbnail", "birth_date", "death_date", "books_count", "created_at", "updated_at", "remove_thumbnail"] + fields = [ + "id", "full_name", "slug", "thumbnail", "image", "birth_date", "death_date", + "date_of_birth", "date_of_death", "researcher_editor", "publisher", + "publication_place", "edition_statement", "year", "number_of_volumes", + "notes", "online_source", "tag", "books_count", "created_at", "updated_at", + "remove_thumbnail", "remove_image" + ] read_only_fields = ["id", "created_at", "updated_at", "books_count"] def get_books_count(self, obj): @@ -78,13 +86,18 @@ class AdminAuthorSerializer(serializers.ModelSerializer): def create(self, validated_data): validated_data.pop("remove_thumbnail", False) + validated_data.pop("remove_image", False) return super().create(validated_data) def update(self, instance, validated_data): remove_thumbnail = validated_data.pop("remove_thumbnail", False) + remove_image = validated_data.pop("remove_image", False) if remove_thumbnail and instance.thumbnail: instance.thumbnail.delete(save=False) instance.thumbnail = None + if remove_image and instance.image: + instance.image.delete(save=False) + instance.image = None return super().update(instance, validated_data) diff --git a/apps/library/views.py b/apps/library/views.py index 9dab81a..3bec746 100644 --- a/apps/library/views.py +++ b/apps/library/views.py @@ -256,6 +256,8 @@ class AuthorListView(ListAPIView): serializer_class = AuthorListSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] + filter_backends = [SearchFilter] + search_fields = ['full_name'] pagination_class = StandardResultsSetPagination @author_list_swagger diff --git a/entrypoint.sh b/entrypoint.sh index 4c38575..1957569 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,7 +6,7 @@ python manage.py migrate # python manage.py compilemessages python manage.py collectstatic --noinput python manage.py assign_volume_images --force -python manage.py send_test_notifications +# python manage.py send_test_notifications # python manage.py set_default_category_icons # python manage.py set_hadiscorrection_fixed_text # Seed Russian data (only runs once, skips if data exists)