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'))},
+ }
+ }
+ }