diff --git a/apps/dobodbi_calendar/admin.py b/apps/dobodbi_calendar/admin.py index 8c38f3f..eafc931 100644 --- a/apps/dobodbi_calendar/admin.py +++ b/apps/dobodbi_calendar/admin.py @@ -1,3 +1,135 @@ +import json from django.contrib import admin +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.safestring import mark_safe +from django.utils.html import format_html +from dj_language.models import Language +from django import forms +from import_export import fields, widgets, resources +from import_export.admin import ImportExportModelAdmin +from unfold.decorators import display +from unfold.admin import ModelAdmin, TabularInline +from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm +from unfold.contrib.filters.admin import ( + RangeDateFilter, + RangeNumericFilter, + SingleNumericFilter, + ChoicesDropdownFilter +) +from apps.dobodbi_calendar.models import CalendarOccasions +from utils.json_editor_field import JsonEditorWidget +from utils.admin import project_admin_site +from utils.schema import get_calender_dates_schema -# Register your models here. + + + + +class CalendarOccasionsForm(forms.ModelForm): + + class Meta: + model = CalendarOccasions + fields = '__all__' + + widgets = { + 'dates': JsonEditorWidget(attrs={ + 'schema': get_calender_dates_schema(), + 'title': _('Dates'), + }), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + + +class CalendarOccasionsAdmin(ModelAdmin): + form = CalendarOccasionsForm + ordering = ('-id',) + list_display = [ + "title", + "display_occasion_type", + "display_event_type", + "is_global", + "is_yearly", + "display_dates", + ] + + list_filter = [ + ("occasion_type", ChoicesDropdownFilter), + ("event_type", ChoicesDropdownFilter), + "is_global", + "is_yearly", + ("created_at", RangeDateFilter), + ] + + search_fields = [ + "title", + "dates", + ] + + fieldsets = ( + ("Basic Information", { + "fields": ( + "title", + "is_global", + "is_yearly", + "occasion_type", + "event_type", + ), + "description": "Main information about the calendar occasion", + }), + ("Dates Configuration", { + "fields": ("dates",), + "classes": ("collapse",), + "description": "Configure dates for this occasion", + }), + ("Metadata", { + "fields": ("created_at", "updated_at"), + "classes": ("collapse",), + "description": "Metadata information", + }), + ) + readonly_fields = ["created_at", "updated_at"] + + # سفارشی‌سازی اکشن‌ها + actions = ["make_global", "make_not_global"] + + # متدهای اکشن + @admin.action(description="Mark selected occasions as global") + def make_global(self, request, queryset): + queryset.update(is_global=True) + + @admin.action(description="Mark selected occasions as not global") + def make_not_global(self, request, queryset): + queryset.update(is_global=False) + + + @display(description="Occasion Type", label=True) + def display_occasion_type(self, obj): + return obj.get_occasion_type_display() + + @display(description="Event Type", label=True) + def display_event_type(self, obj): + return obj.get_event_type_display() + + @display(description="Dates") + def display_dates(self, obj): + from django.utils.html import format_html + return "\n".join([f"{i['month']}/{i['day']}" for i in obj.dates]) + + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super().get_search_results(request, queryset, search_term) + try: + # امکان جستجو در فیلد JSON + import json + json.loads(search_term) + queryset |= self.model.objects.filter(dates__contains=search_term) + except ValueError: + pass + return queryset, use_distinct + + +project_admin_site.register(CalendarOccasions, CalendarOccasionsAdmin) diff --git a/apps/dobodbi_calendar/admin/calendar.html b/apps/dobodbi_calendar/admin/calendar.html new file mode 100644 index 0000000..8f5f4df --- /dev/null +++ b/apps/dobodbi_calendar/admin/calendar.html @@ -0,0 +1,63 @@ +{% extends 'admin/change_form.html' %} +{% block scripts %} + {{ block.super }} + + + +{% endblock %} \ No newline at end of file diff --git a/apps/dobodbi_calendar/admin/json_date_field.html b/apps/dobodbi_calendar/admin/json_date_field.html new file mode 100644 index 0000000..b86766c --- /dev/null +++ b/apps/dobodbi_calendar/admin/json_date_field.html @@ -0,0 +1,2 @@ + diff --git a/apps/dobodbi_calendar/migrations/0001_initial.py b/apps/dobodbi_calendar/migrations/0001_initial.py new file mode 100644 index 0000000..197753b --- /dev/null +++ b/apps/dobodbi_calendar/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.8 on 2025-05-04 08:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CalendarOccasions', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('is_global', models.BooleanField(default=False, help_text='check this field if event is global', verbose_name='is global')), + ('occasion_type', models.CharField(choices=[('georgian', 'georgian'), ('lunar', 'lunar')], default='georgian', help_text='Choose between georgian or lunar. default to georgian', max_length=12, verbose_name='occasion type')), + ('dates', models.JSONField(verbose_name='dates')), + ('is_yearly', models.BooleanField(default=True, help_text='check this field if event is annually', verbose_name='is yearly')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('event_type', models.CharField(choices=[('national', 'National'), ('international', 'International'), ('religious', 'Religious')], max_length=16, null=True, verbose_name='event type')), + ], + options={ + 'ordering': ('-updated_at',), + }, + ), + ] diff --git a/apps/dobodbi_calendar/models.py b/apps/dobodbi_calendar/models.py index 328e555..e241725 100644 --- a/apps/dobodbi_calendar/models.py +++ b/apps/dobodbi_calendar/models.py @@ -1,6 +1,6 @@ from django.db import models -# Create your models here. +from django.utils.translation import gettext_lazy as _ class CalendarOccasions(models.Model): @@ -36,4 +36,13 @@ class CalendarOccasions(models.Model): help_text=_('check this field if event is annually') ) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + event_type = models.CharField(max_length=16, choices=EventType.choices, null=True, verbose_name=_('event type')) + class Meta: + ordering = ('-updated_at',) + + def __str__(self) -> str: + return self.title + + \ No newline at end of file diff --git a/apps/dobodbi_calendar/serializer.py b/apps/dobodbi_calendar/serializer.py new file mode 100644 index 0000000..2a65c4d --- /dev/null +++ b/apps/dobodbi_calendar/serializer.py @@ -0,0 +1,34 @@ +from rest_framework import serializers + +from apps.dobodbi_calendar.models import CalendarOccasions + + +class CalendarSerializer(serializers.ModelSerializer): + type = serializers.CharField(source='occasion_type') + dates = serializers.SerializerMethodField() + + + # def get_countries(self, obj): + # if not obj.countries or obj.countries[0] == 'ALL': + # return ["All"] + + # return [country.name or country.code for country in obj.countries] + + # def get_holiday_in_countries(self, obj): + # return [country.name or country.code for country in obj.holiday_in_countries] + + def get_dates(self, obj): + dates = [] + for date in obj.dates: + dates.append({ + 'day': str(date['day']), + 'month': str(date['month']), + 'year': str(date.get('year', '')), + }) + return dates + + + class Meta: + model = CalendarOccasions + fields = ('id', 'title', 'type', 'event_type', 'dates', 'is_yearly',) + diff --git a/apps/dobodbi_calendar/urls.py b/apps/dobodbi_calendar/urls.py new file mode 100644 index 0000000..57dfc8c --- /dev/null +++ b/apps/dobodbi_calendar/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from apps.dobodbi_calendar.views import CalendarList, AdjustmentConfigView + + +urlpatterns = [ + path('occasions/', CalendarList.as_view()), + path('adjustemnts/', AdjustmentConfigView.as_view()), + +] diff --git a/apps/dobodbi_calendar/views.py b/apps/dobodbi_calendar/views.py index 91ea44a..3dca2a9 100644 --- a/apps/dobodbi_calendar/views.py +++ b/apps/dobodbi_calendar/views.py @@ -1,3 +1,64 @@ from django.shortcuts import render -# Create your views here. +import datetime +import json +from collections import OrderedDict + +from django.db.models import Q +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page +from rest_framework.generics import ListAPIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView, status + +from apps.account.models import User +from apps.dobodbi_calendar.models import CalendarOccasions +from apps.dobodbi_calendar.serializer import CalendarSerializer +from utils.config_getter import get_config + + +class CalendarList(ListAPIView): + serializer_class = CalendarSerializer + pagination_class = None + + permission_classes = (IsAuthenticated,) + + # @method_decorator(cache_page(60 * 15)) # Cache for 1 Hour + # def dispatch(self, *args, **kwargs): + # return super().dispatch(*args, **kwargs) + + def get_queryset(self): + queryset = CalendarOccasions.objects.all() + req = self.request + + if v := req.query_params.get('last_updated'): + query &= Q( + updated_at__gte=v + ) + + return queryset + + + def list(self, request, *args, **kwargs): + q = self.get_queryset() + last_item_date = q.first() + if last_item_date: + last_updated = last_item_date.updated_at + datetime.timedelta(microseconds=1) + last_updated = str(last_updated) + else: + last_updated = None + + d = self.get_serializer(q, many=True).data + data = OrderedDict({ + 'last_updated': last_updated, + 'total': len(d), + 'data': d, + }) + return Response(data) + + +class AdjustmentConfigView(APIView): + def get(self, request): + adjustment_config = get_config('calendar__Adjustment') + return Response(json.loads(adjustment_config)) diff --git a/config/settings/base.py b/config/settings/base.py index 81931f6..4eda092 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -52,6 +52,7 @@ LOCAL_APPS = [ 'apps.library.apps.LibraryConfig', 'apps.video.apps.VideoConfig', 'apps.bookmark.apps.BookmarkConfig', + 'apps.dobodbi_calendar.apps.DobodbiCalendarConfig', 'dynamic_preferences', ] @@ -555,10 +556,21 @@ UNFOLD = { ] }, + { + "title": _(""), + "items": [ + { + "title": _("Calender"), + "icon": "calendar_today", + "link": reverse_lazy("admin:dobodbi_calendar_calendaroccasions_changelist"), + "permission": lambda request: request.user.is_staff, + }, + ], + }, { "title": _("Courses"), "collapsible": True, - # "separator": True, + "separator": True, "items": [ { "title": _("Categories"), @@ -595,7 +607,7 @@ UNFOLD = { { "title": _("Transactions"), "collapsible": True, - # "separator": True, + "separator": True, "items": [ { "title": _("Transactions"), diff --git a/config/urls.py b/config/urls.py index 286f433..2be20aa 100644 --- a/config/urls.py +++ b/config/urls.py @@ -72,6 +72,7 @@ api_patterns = [ path('library/', include('apps.library.urls')), path('videos/', include('apps.video.urls')), path('bookmarks/', include('apps.bookmark.urls')), + path('calendar/', include('apps.dobodbi_calendar.urls')), path('settings/', include('dynamic_preferences.urls')), diff --git a/dynamic_preferences/dynamic_preferences_registry.py b/dynamic_preferences/dynamic_preferences_registry.py index 9e05574..4ab5f10 100644 --- a/dynamic_preferences/dynamic_preferences_registry.py +++ b/dynamic_preferences/dynamic_preferences_registry.py @@ -10,12 +10,15 @@ from dynamic_preferences.registries import global_preferences_registry from dynamic_preferences.types import BasePreferenceType, BaseSerializer, LongStringPreference, StringPreference, \ FilePreference from utils.json_editor_field import JsonEditorWidget -from unfold.contrib.forms.widgets import WysiwygWidget +from unfold.contrib.forms.widgets import WysiwygWidget, ArrayWidget +from unfold.widgets import UnfoldAdminTextareaWidget class EditorPreferences(LongStringPreference): widget = WysiwygWidget(attrs={'class': 'editor-field'}) +class EditorTextPreferences(LongStringPreference): + widget = UnfoldAdminTextareaWidget(attrs={'class': 'editor-field', 'rows': 20}) @global_preferences_registry.register class AboutUsConfig(EditorPreferences): @@ -181,3 +184,12 @@ class SupportConfig(JsonFieldCard): verbose_name = 'Card Detail' default = {} + + +@global_preferences_registry.register +class CalendarAdjustmentConfig(EditorTextPreferences): + section = Section('calendar', verbose_name='CalendarAdjustmentConfig') + name = 'Adjustment' + required = False + verbose_name = 'Calendar Adjustment Config' + default = '' diff --git a/utils/config_getter.py b/utils/config_getter.py new file mode 100644 index 0000000..4eaf50d --- /dev/null +++ b/utils/config_getter.py @@ -0,0 +1,13 @@ +import logging + +from dynamic_preferences.registries import global_preferences_registry + +global_preferences = global_preferences_registry.manager() + + +def get_config(key): + try: + return global_preferences[key] + except Exception as e: + logging.error(f"error gettings config {key}: {e}") + return None diff --git a/utils/schema.py b/utils/schema.py index 110a701..4183b56 100644 --- a/utils/schema.py +++ b/utils/schema.py @@ -36,7 +36,6 @@ def get_weekly_timing_schema(): } - def get_course_feature_schema(): return { 'type': "array", @@ -50,3 +49,20 @@ def get_course_feature_schema(): } } } + + +def get_calender_dates_schema(): + return { + 'type': "array", + 'format': 'table', + 'title': ' ', + 'items': { + 'type': 'object', + 'title': str(_('')), + 'properties': { + 'year': {'type': 'string', 'format': 'number', 'title': str(_('year'))}, + 'month': {'type': 'string', 'format': 'number', 'title': str(_('month'))}, + 'day': {'type': 'string', 'format': 'number', 'title': str(_('day'))}, + } + } + }