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.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 |
|||
|
|||
# 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