13 changed files with 402 additions and 7 deletions
-
134apps/dobodbi_calendar/admin.py
-
63apps/dobodbi_calendar/admin/calendar.html
-
2apps/dobodbi_calendar/admin/json_date_field.html
-
31apps/dobodbi_calendar/migrations/0001_initial.py
-
11apps/dobodbi_calendar/models.py
-
34apps/dobodbi_calendar/serializer.py
-
9apps/dobodbi_calendar/urls.py
-
63apps/dobodbi_calendar/views.py
-
16config/settings/base.py
-
1config/urls.py
-
14dynamic_preferences/dynamic_preferences_registry.py
-
13utils/config_getter.py
-
18utils/schema.py
@ -1,3 +1,135 @@ |
|||||
|
import json |
||||
from django.contrib import admin |
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) |
||||
@ -0,0 +1,63 @@ |
|||||
|
{% extends 'admin/change_form.html' %} |
||||
|
{% block scripts %} |
||||
|
{{ block.super }} |
||||
|
<style> |
||||
|
.json-view-editor .row .form-group { |
||||
|
display: flex; |
||||
|
align-items: baseline; |
||||
|
} |
||||
|
|
||||
|
.json-view-editor .row .form-group label { |
||||
|
margin-bottom: 0; |
||||
|
margin-right: 10px; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<script> |
||||
|
|
||||
|
let editor = document.getElementById('id_dates') |
||||
|
let schema_str = window.sc = JSON.parse(editor.value) |
||||
|
$(editor).addClass("hidden") |
||||
|
let json_viewer_div = $(`<div class="json-view-editor" id='date-view-editor'></div>`) |
||||
|
$(editor).parent().append(json_viewer_div) |
||||
|
|
||||
|
|
||||
|
function init(start_value = []) { |
||||
|
if (window.jsoneditor) { |
||||
|
window.jsoneditor.destroy() |
||||
|
} |
||||
|
window.jsoneditor = new JSONEditor( |
||||
|
json_viewer_div[0], { |
||||
|
theme: 'bootstrap4', |
||||
|
schema: { |
||||
|
type: "array", |
||||
|
format: 'table', |
||||
|
title: ' ', |
||||
|
items: { |
||||
|
type: 'object', |
||||
|
title: 'date', |
||||
|
properties: { |
||||
|
year: {type: 'string', format: 'number'}, |
||||
|
month: {type: 'string', format: 'number'}, |
||||
|
day: {type: 'string', format: 'number'}, |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
disable_edit_json: true, |
||||
|
disable_properties: true, |
||||
|
disable_array_delete_all_rows: true, |
||||
|
disable_array_delete_last_row: true, |
||||
|
disable_array_reorder: true, |
||||
|
grid_columns: 3, |
||||
|
prompt_before_delete: false, |
||||
|
disable_collapse: true, |
||||
|
startval: start_value |
||||
|
}) |
||||
|
window.jsoneditor.on('change', () => { |
||||
|
$(editor).val(JSON.stringify(jsoneditor.getValue())) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
init(schema_str || []) |
||||
|
</script> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,2 @@ |
|||||
|
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}> |
||||
|
{% if widget.value %}{{ widget.value }}{% endif %}</textarea> |
||||
@ -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',), |
||||
|
}, |
||||
|
), |
||||
|
] |
||||
@ -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',) |
||||
|
|
||||
@ -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()), |
||||
|
|
||||
|
] |
||||
@ -1,3 +1,64 @@ |
|||||
from django.shortcuts import render |
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)) |
||||
@ -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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue