Initial
This commit is contained in:
0
app/base/views/model_views/__init__.py
Executable file
0
app/base/views/model_views/__init__.py
Executable file
215
app/base/views/model_views/base.py
Normal file
215
app/base/views/model_views/base.py
Normal file
@@ -0,0 +1,215 @@
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Model, Q, QuerySet, Value as V
|
||||
from django.db.models.functions import Concat
|
||||
from django.views.generic import (
|
||||
ListView, DetailView, CreateView, UpdateView, DeleteView)
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
from app.base.templatetags.url_utils import url_with_formatter, UrlFormatObject
|
||||
|
||||
from typing import (
|
||||
Generic, TypeVar, Union, Type, List, Dict, Any, Tuple, Optional,
|
||||
Sequence, Literal, TypedDict)
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Types
|
||||
# --------------------------------------------
|
||||
|
||||
M = TypeVar('M', bound=Model)
|
||||
ListFilterOption = Dict[str, str] # {GET-key: model-query-filter-key}
|
||||
ListColumnsOption = List[str]
|
||||
RenderOptions = Dict[str, Dict[str, Any]]
|
||||
SearchQueryOption = List[str]
|
||||
ViewsOption = Dict[str, str]
|
||||
FieldsOption = Union[Sequence[str], Literal['__all__'], None]
|
||||
OnSuccessOption = UrlFormatObject
|
||||
BreadcrumbEntry = Tuple[str, Optional[UrlFormatObject]] # (title, href-url)
|
||||
|
||||
|
||||
class ToolbarButtonType(TypedDict, total=False):
|
||||
icon: Optional[str]
|
||||
label: str
|
||||
href: str # use either href or path, not both. href takes precedence
|
||||
path: UrlFormatObject
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Base Options
|
||||
#
|
||||
# - ViewOptions (ListView)
|
||||
# - SubviewOfListOptions (DetailView)
|
||||
# - SubviewWithEditOptions (CreateView, UpdateView, DeleteView)
|
||||
# --------------------------------------------
|
||||
|
||||
class ViewOptions(Generic[M], ContextMixin):
|
||||
model: Type[M] # must be declared for each View separately
|
||||
object: M # populated by django
|
||||
title: str = '' # used for h1 and button titles
|
||||
icon = 'database' # please override
|
||||
views: ViewsOption = {}
|
||||
detail_fields: FieldsOption = '__all__'
|
||||
detail_render: RenderOptions = {}
|
||||
list_filter: ListFilterOption = {}
|
||||
list_columns: ListColumnsOption = []
|
||||
list_render: RenderOptions = {}
|
||||
toolbar_buttons: List[ToolbarButtonType] = []
|
||||
|
||||
def get_breadcrumbs(self) -> List[BreadcrumbEntry]:
|
||||
return [(self.get_title(), None)]
|
||||
|
||||
def get_title(self) -> str:
|
||||
return self.title.format(*[self] * 9) if self.title else \
|
||||
(self.model._meta.verbose_name_plural or '')
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs) # type: ignore
|
||||
context['model_verbose_name'] = self.model._meta.verbose_name \
|
||||
if hasattr(self, 'model') else None
|
||||
context['breadcrumbs'] = self.get_breadcrumbs()
|
||||
context['icon'] = self.icon
|
||||
context['title'] = self.title.format(*[self] * 9) if self.title else \
|
||||
self.model._meta.verbose_name_plural
|
||||
context['views'] = self.views
|
||||
context['toolbar_buttons'] = self.toolbar_buttons
|
||||
context['detail_fields'] = self.detail_fields
|
||||
context['detail_render'] = self.detail_render
|
||||
context['list_render'] = self.list_render
|
||||
context['list_columns'] = self.list_columns
|
||||
context['list_filter'] = self.list_filter
|
||||
return context
|
||||
|
||||
|
||||
class SubviewOfListOptions(ViewOptions[M]):
|
||||
def get_breadcrumbs(self) -> List[BreadcrumbEntry]:
|
||||
active_page_breadcrumb = super().get_breadcrumbs()
|
||||
parent = self.views.get('list')
|
||||
if not parent:
|
||||
return active_page_breadcrumb
|
||||
title = self.model._meta.verbose_name_plural or '-?-'
|
||||
return [(title, parent)] + active_page_breadcrumb
|
||||
|
||||
|
||||
class SubviewWithEditOptions(
|
||||
SubviewOfListOptions[M], TemplateResponseMixin, FormMixin):
|
||||
on_success: OnSuccessOption = '' # format source is: self.object
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
prev = self.request.GET.get('prev')
|
||||
if isinstance(self, DeleteView):
|
||||
obj_url = getattr(self.object, 'get_absolute_url', lambda: None)()
|
||||
if obj_url == prev:
|
||||
prev = None # do not redirect to detail page after delete
|
||||
if prev:
|
||||
return prev
|
||||
target = self.on_success
|
||||
if not target:
|
||||
return super().get_success_url()
|
||||
return url_with_formatter(target, self.object)
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# List Options
|
||||
# --------------------------------------------
|
||||
|
||||
class ModelListView(ViewOptions[M], ListView):
|
||||
template_name = 'generic/list.html'
|
||||
paginate_by = 50
|
||||
search_fields: SearchQueryOption = []
|
||||
|
||||
def get_list_filter(self) -> Optional[Dict[str, str]]:
|
||||
rv = {}
|
||||
for get_key, model_key in self.list_filter.items():
|
||||
val = self.request.GET.get(get_key, None)
|
||||
if val is not None:
|
||||
rv[model_key] = val or None
|
||||
# allow to search records with ?q=
|
||||
search_query = self.request.GET.get('q')
|
||||
if search_query and self.search_fields:
|
||||
rv['search_qry_fld__icontains'] = search_query
|
||||
return rv
|
||||
|
||||
def _annotate_with_search_query(self) -> QuerySet:
|
||||
''' called if and only if filters are active '''
|
||||
# self.get_queryset() or self.model.objects ?
|
||||
query = self.get_queryset()
|
||||
if self.search_fields:
|
||||
search_field = Concat(*[
|
||||
y for x in self.search_fields for y in (V(' '), x)
|
||||
][1:]) # ignore first space
|
||||
return query.annotate(search_qry_fld=search_field)
|
||||
return query
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs) # type: ignore
|
||||
|
||||
# Apply filter if specified & active
|
||||
list_filter = self.get_list_filter()
|
||||
if list_filter:
|
||||
sort_order = self.ordering or self.model._meta.ordering or []
|
||||
filtered_objs = self._annotate_with_search_query().filter(
|
||||
Q(**list_filter)).order_by(*sort_order)
|
||||
pagination = Paginator(filtered_objs, self.paginate_by)
|
||||
preferred_page = int(self.request.GET.get('page', 1))
|
||||
actual_page = min(max(preferred_page, 1), pagination.num_pages)
|
||||
page_obj = pagination.page(actual_page)
|
||||
context['object_list'] = page_obj
|
||||
context['page_obj'] = page_obj
|
||||
search_query = list_filter.get('search_qry_fld__icontains')
|
||||
if search_query:
|
||||
context['active_search_query'] = search_query
|
||||
context['breadcrumbs'] = [
|
||||
(context['title'], self.views.get('list', 'index')),
|
||||
(f'Suchergebnisse für "{search_query}"', None)
|
||||
]
|
||||
return context
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Detail Options
|
||||
# --------------------------------------------
|
||||
|
||||
class ModelDetailView(SubviewOfListOptions[M], DetailView):
|
||||
template_name = 'generic/detail.html'
|
||||
title = '{.object}'
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Create Options
|
||||
# --------------------------------------------
|
||||
|
||||
class ModelCreateView(SubviewWithEditOptions[M], CreateView):
|
||||
template_name = 'generic/create.html'
|
||||
title = '{.model._meta.verbose_name} anlegen'
|
||||
fields: FieldsOption = '__all__' # you probably want to override that
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['submit_label'] = f'{self.model._meta.verbose_name} anlegen'
|
||||
return context
|
||||
|
||||
# def get_success_url(self) -> str:
|
||||
# prev = self.request.GET.get('prev')
|
||||
# if prev:
|
||||
# return prev
|
||||
# return super().get_success_url()
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Update Options
|
||||
# --------------------------------------------
|
||||
|
||||
class ModelUpdateView(SubviewWithEditOptions[M], UpdateView):
|
||||
template_name = 'generic/update.html'
|
||||
title = '"{.object}" bearbeiten'
|
||||
fields: FieldsOption = '__all__' # you probably want to override that
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# Delete Options
|
||||
# --------------------------------------------
|
||||
|
||||
class ModelDeleteView(SubviewWithEditOptions[M], DeleteView):
|
||||
template_name = 'generic/delete.html'
|
||||
title = '"{.object}" löschen'
|
||||
83
app/base/views/model_views/booking.py
Executable file
83
app/base/views/model_views/booking.py
Executable file
@@ -0,0 +1,83 @@
|
||||
from django.db.models import Q
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models.booking import Booking
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class BookingOptions(ViewOptions[Booking], LoginRequired):
|
||||
model = Booking
|
||||
icon = 'clock'
|
||||
views = {
|
||||
'list': 'booking:list',
|
||||
'detail': 'booking:detail',
|
||||
'create': 'booking:create',
|
||||
'update': 'booking:update',
|
||||
'delete': 'booking:delete'
|
||||
}
|
||||
list_filter = {'user': 'user__pk'}
|
||||
list_columns = ['begin_time', 'end_time', 'user', 'type']
|
||||
list_render = {
|
||||
'begin_time': {
|
||||
'date_format': 'D. d.m.y, H:i',
|
||||
'width': '11rem',
|
||||
},
|
||||
'end_time': {
|
||||
'date_format': 'H:i',
|
||||
'width': '4rem',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class BookingListView(BookingOptions, ModelListView):
|
||||
template_name = 'booking_list.html'
|
||||
ordering = ('-end_time',)
|
||||
|
||||
def get_context_data(self, *, object_list=None, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = self.request.GET.get('user')
|
||||
|
||||
if not user:
|
||||
context['open_bookings'] = Booking.objects.filter(
|
||||
Q(end_time__isnull=True)
|
||||
# | Q(begin_time__gte=date)
|
||||
).order_by('-begin_time')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class BookingDetailView(BookingOptions, ModelDetailView):
|
||||
pass
|
||||
|
||||
|
||||
class BookingCreateView(BookingOptions, ModelCreateView):
|
||||
on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
initial['user'] = self.request.GET.get('user') or None
|
||||
initial['comment'] = self.request.GET.get('comment') or None
|
||||
return initial
|
||||
|
||||
|
||||
class BookingUpdateView(BookingOptions, ModelUpdateView):
|
||||
on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
|
||||
class BookingDeleteView(BookingOptions, ModelDeleteView):
|
||||
on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'booking'
|
||||
urlpatterns = [
|
||||
path('', BookingListView.as_view(), name='list'),
|
||||
path('<int:pk>/', BookingDetailView.as_view(), name='detail'),
|
||||
path('new/', BookingCreateView.as_view(), name='create'),
|
||||
path('update/<int:pk>/', BookingUpdateView.as_view(), name='update'),
|
||||
path('delete/<int:pk>/', BookingDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
25
app/base/views/model_views/booking_type.py
Executable file
25
app/base/views/model_views/booking_type.py
Executable file
@@ -0,0 +1,25 @@
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models.booking_type import BookingType
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import ModelUpdateView, ViewOptions
|
||||
|
||||
|
||||
class BookingTypeOptions(ViewOptions[BookingType], LoginRequired):
|
||||
model = BookingType
|
||||
icon = 'business-time'
|
||||
views = {
|
||||
'update': 'booking-type:update',
|
||||
}
|
||||
|
||||
|
||||
class BookingTypeUpdateView(BookingTypeOptions, ModelUpdateView):
|
||||
on_success = 'settings'
|
||||
fields = ['label', 'price', 'interval', 'is_checkin']
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'booking-type'
|
||||
urlpatterns = [
|
||||
path('update/<str:pk>/', BookingTypeUpdateView.as_view(), name='update'),
|
||||
]
|
||||
74
app/base/views/model_views/course.py
Executable file
74
app/base/views/model_views/course.py
Executable file
@@ -0,0 +1,74 @@
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models import Course
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class CourseOptions(ViewOptions[Course], LoginRequired):
|
||||
model = Course
|
||||
icon = 'graduation-cap'
|
||||
views = {
|
||||
'list': 'course:list',
|
||||
'detail': 'course:detail',
|
||||
'create': 'course:create',
|
||||
'update': 'course:update',
|
||||
'delete': 'course:delete'
|
||||
}
|
||||
detail_fields = ['mandatory', 'description']
|
||||
list_filter = {
|
||||
'mandatory': 'mandatory',
|
||||
'user': 'visits__participant__pk',
|
||||
'teacher': 'visits__teacher__pk',
|
||||
}
|
||||
list_columns = ['mandatory', 'title']
|
||||
list_render = {
|
||||
'mandatory': {'width': 2}
|
||||
}
|
||||
|
||||
|
||||
class CourseListView(CourseOptions, ModelListView):
|
||||
ordering = ('title',)
|
||||
|
||||
|
||||
class CourseDetailView(CourseOptions, ModelDetailView):
|
||||
toolbar_buttons = [
|
||||
{
|
||||
'label': 'Teilnehmer:in hinzufügen',
|
||||
'icon': 'user-plus', # user-plus, person-circle-plus
|
||||
'path': ('course-visit:create', '',
|
||||
'?course={[object].pk}'),
|
||||
},
|
||||
{
|
||||
'label': 'Teilnahmen anzeigen',
|
||||
'icon': 'person-chalkboard',
|
||||
'path': ('course-visit:list', '',
|
||||
'?course={[object].pk}')
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class CourseCreateView(CourseOptions, ModelCreateView):
|
||||
on_success = 'course:list'
|
||||
|
||||
|
||||
class CourseUpdateView(CourseOptions, ModelUpdateView):
|
||||
on_success = 'course:list'
|
||||
|
||||
|
||||
class CourseDeleteView(CourseOptions, ModelDeleteView):
|
||||
on_success = 'course:list'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'course'
|
||||
urlpatterns = [
|
||||
path('', CourseListView.as_view(), name='list'),
|
||||
path('<int:pk>/', CourseDetailView.as_view(), name='detail'),
|
||||
path('new/', CourseCreateView.as_view(), name='create'),
|
||||
path('update/<int:pk>/', CourseUpdateView.as_view(), name='update'),
|
||||
path('delete/<int:pk>/', CourseDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
66
app/base/views/model_views/course_visit.py
Executable file
66
app/base/views/model_views/course_visit.py
Executable file
@@ -0,0 +1,66 @@
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models import CourseVisit
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, # ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class CourseVisitOptions(ViewOptions[CourseVisit], LoginRequired):
|
||||
model = CourseVisit
|
||||
icon = 'person-chalkboard'
|
||||
views = {
|
||||
'list': 'course-visit:list',
|
||||
'detail': 'course-visit:detail',
|
||||
'create': 'course-visit:create',
|
||||
# 'update': 'course-visit:update',
|
||||
'delete': 'course-visit:delete'
|
||||
}
|
||||
# detail_fields = []
|
||||
list_filter = {
|
||||
'course': 'course__pk',
|
||||
'user': 'participant__pk',
|
||||
'teacher': 'teacher__pk',
|
||||
}
|
||||
list_columns = ['date', 'course', 'participant', 'teacher']
|
||||
list_render = {'date': {'date_format': 'd. M y'}}
|
||||
|
||||
|
||||
class CourseVisitListView(CourseVisitOptions, ModelListView):
|
||||
ordering = ('-date',)
|
||||
|
||||
|
||||
class CourseVisitDetailView(CourseVisitOptions, ModelDetailView):
|
||||
pass
|
||||
|
||||
|
||||
class CourseVisitCreateView(CourseVisitOptions, ModelCreateView):
|
||||
# on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
initial['course'] = self.request.GET.get('course') or None
|
||||
initial['participant'] = self.request.GET.get('user') or None
|
||||
initial['teacher'] = self.request.GET.get('teacher') or None
|
||||
return initial
|
||||
|
||||
|
||||
# class CourseVisitUpdateView(CourseVisitOptions, ModelUpdateView):
|
||||
# on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
|
||||
class CourseVisitDeleteView(CourseVisitOptions, ModelDeleteView):
|
||||
on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'course-visit'
|
||||
urlpatterns = [
|
||||
path('', CourseVisitListView.as_view(), name='list'),
|
||||
path('<int:pk>/', CourseVisitDetailView.as_view(), name='detail'),
|
||||
path('new/', CourseVisitCreateView.as_view(), name='create'),
|
||||
# path('update/<int:pk>/', CourseVisitUpdateView.as_view(), name='update'),
|
||||
path('delete/<int:pk>/', CourseVisitDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
151
app/base/views/model_views/person.py
Executable file
151
app/base/views/model_views/person.py
Executable file
@@ -0,0 +1,151 @@
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models.booking import Booking
|
||||
from app.base.models.transaction import Transaction
|
||||
from app.base.models.person import Person
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
FORM_FIELDS = [ # hide UUID, and many-to-many mappings
|
||||
'first_name', 'last_name', 'birth_date', 'street', 'house_nr',
|
||||
'zip_code', 'city', 'email', 'phone', 'identified',
|
||||
'agreed_to_terms_of_service',
|
||||
]
|
||||
|
||||
|
||||
class PersonOptions(ViewOptions[Person], LoginRequired):
|
||||
model = Person
|
||||
icon = 'user'
|
||||
views = {
|
||||
'list': 'person:list',
|
||||
'detail': 'person:detail',
|
||||
'create': 'person:create',
|
||||
'update': 'person:update',
|
||||
'delete': 'person:delete'
|
||||
}
|
||||
detail_fields = ['birth_date', 'city']
|
||||
detail_render = {
|
||||
'birth_date': {
|
||||
'verbose_name': 'Geburtsjahr',
|
||||
'date_format': 'Y',
|
||||
},
|
||||
}
|
||||
list_filter = {
|
||||
'trait': 'traits__pk',
|
||||
'course': 'courses__pk',
|
||||
}
|
||||
list_columns = ['display_name', 'birth_date', 'last_check_in']
|
||||
list_render = {
|
||||
'display_name': {'verbose_name': 'Nutzer:in'},
|
||||
'birth_date': {
|
||||
'verbose_name': 'Geburtsjahr',
|
||||
'date_format': 'Y',
|
||||
},
|
||||
'last_check_in': {
|
||||
'verbose_name': 'Letzter Besuch',
|
||||
'date_format': 'D. d. M y, H:i',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class PersonListView(PersonOptions, ModelListView):
|
||||
icon = 'users'
|
||||
ordering = ('-last_check_in',)
|
||||
search_fields = ['uuid', 'first_name', 'last_name']
|
||||
|
||||
def get_queryset(self):
|
||||
query = Subquery(Booking.latest_checkin_query(OuterRef('pk')))
|
||||
rv = Person.objects.annotate(last_check_in=query)
|
||||
return rv.order_by(*self.ordering)
|
||||
|
||||
|
||||
class PersonCreateView(PersonOptions, ModelCreateView):
|
||||
# on_success = 'person:detail', '{.pk}' # the default anyway
|
||||
fields = FORM_FIELDS
|
||||
|
||||
|
||||
class PersonUpdateView(PersonOptions, ModelUpdateView):
|
||||
fields = ['uuid'] + FORM_FIELDS
|
||||
|
||||
|
||||
class PersonDeleteView(PersonOptions, ModelDeleteView):
|
||||
on_success = 'person:list'
|
||||
|
||||
|
||||
class PersonDetailView(PersonOptions, ModelDetailView):
|
||||
template_name = 'person_detail.html'
|
||||
|
||||
def get_context_data(self, **kwargs: 'dict[str, object]'):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# context['prevname'] = context['title']
|
||||
|
||||
context['bookings'] = {
|
||||
'objects': Booking.objects.filter(
|
||||
user=self.object).order_by('-begin_time')[:5],
|
||||
'views': {
|
||||
'detail': 'booking:detail',
|
||||
'update': 'booking:update',
|
||||
'delete': 'booking:delete'
|
||||
},
|
||||
'columns': ['begin_time', 'end_time', 'duration', 'type'],
|
||||
'render': {
|
||||
'begin_time': {
|
||||
'date_format': 'D. d.m.y, H:i',
|
||||
'width': '11rem',
|
||||
},
|
||||
'end_time': {
|
||||
'date_format': 'H:i',
|
||||
'width': '4rem',
|
||||
},
|
||||
'duration': {
|
||||
'verbose_name': 'Dauer',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
context['course_list'] = {
|
||||
'objects': self.object.courses.all(),
|
||||
'views': {
|
||||
'delete': 'course-visit:delete',
|
||||
'detail': 'course-visit:detail',
|
||||
},
|
||||
'columns': ['date', 'course'],
|
||||
'render': {
|
||||
'date': {'date_format': 'd. M Y'}
|
||||
},
|
||||
}
|
||||
|
||||
context['transaction_list'] = {
|
||||
'objects': Transaction.objects.filter(
|
||||
account__user=self.object).order_by("-time_stamp")[:5],
|
||||
'views': {
|
||||
'detail': 'transaction:detail',
|
||||
},
|
||||
'columns': ['time_stamp', 'amount'],
|
||||
'render': {
|
||||
'time_stamp': {
|
||||
'date_format': 'd.m.y - H:i',
|
||||
},
|
||||
'amount': {
|
||||
'is_price': True,
|
||||
'class': 'text-right',
|
||||
},
|
||||
},
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'person'
|
||||
urlpatterns = [
|
||||
path('', PersonListView.as_view(), name='list'),
|
||||
path('<int:pk>/', PersonDetailView.as_view(), name='detail'),
|
||||
path('new/', PersonCreateView.as_view(), name='create'),
|
||||
path('update/<int:pk>/', PersonUpdateView.as_view(), name='update'),
|
||||
path('delete/<int:pk>/', PersonDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
56
app/base/views/model_views/sys_user.py
Executable file
56
app/base/views/model_views/sys_user.py
Executable file
@@ -0,0 +1,56 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import path
|
||||
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class SysUserOptions(ViewOptions[User], LoginRequired):
|
||||
model = User
|
||||
icon = 'user-cog'
|
||||
views = {
|
||||
'list': 'sys-user:list',
|
||||
'detail': 'sys-user:detail',
|
||||
'create': 'sys-user:create',
|
||||
'update': 'sys-user:update',
|
||||
'delete': 'sys-user:delete'
|
||||
}
|
||||
# detail_fields = ['username', 'email', 'last_login', 'is_superuser']
|
||||
list_filter = {'user': 'account__pk'}
|
||||
list_columns = ['username', 'email']
|
||||
list_render = {}
|
||||
|
||||
|
||||
class SysUserListView(SysUserOptions, ModelListView):
|
||||
# title = 'Thekenkräfte'
|
||||
ordering = ('id',)
|
||||
|
||||
|
||||
class SysUserDetailView(SysUserOptions, ModelDetailView):
|
||||
pass
|
||||
|
||||
|
||||
class SysUserCreateView(SysUserOptions, ModelCreateView):
|
||||
on_success = 'person:detail', '{.account.user.pk}'
|
||||
|
||||
|
||||
class SysUserUpdateView(SysUserOptions, ModelUpdateView):
|
||||
on_success = 'person:detail', '{.account.user.pk}'
|
||||
|
||||
|
||||
class SysUserDeleteView(SysUserOptions, ModelDeleteView):
|
||||
on_success = 'sys-user:list'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'sys-user'
|
||||
urlpatterns = [
|
||||
# path('', SysUserListView.as_view(), name='list'),
|
||||
# path('<int:pk>/', SysUserDetailView.as_view(), name='detail'),
|
||||
# path('new/', SysUserCreateView.as_view(), name='create'),
|
||||
# path('update/<int:pk>/', SysUserUpdateView.as_view(), name='update'),
|
||||
# path('delete/<int:pk>/', SysUserDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
70
app/base/views/model_views/trait.py
Executable file
70
app/base/views/model_views/trait.py
Executable file
@@ -0,0 +1,70 @@
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models import Trait
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class TraitOptions(ViewOptions[Trait], LoginRequired):
|
||||
model = Trait
|
||||
icon = 'tags'
|
||||
views = {
|
||||
'list': 'trait:list',
|
||||
'detail': 'trait:detail',
|
||||
# 'create': 'trait:create',
|
||||
'update': 'trait:update',
|
||||
# 'delete': 'trait:delete'
|
||||
}
|
||||
# detail_fields = ['key', 'label', 'description'] # hide id
|
||||
list_filter = {'user': 'person__pk'}
|
||||
list_columns = ['label', 'description']
|
||||
list_render = {
|
||||
# 'key': {'width': 2}
|
||||
}
|
||||
|
||||
|
||||
class TraitListView(TraitOptions, ModelListView):
|
||||
ordering = ('label',)
|
||||
|
||||
|
||||
class TraitDetailView(TraitOptions, ModelDetailView):
|
||||
toolbar_buttons = [
|
||||
{
|
||||
'label': 'Nutzer:in zuweisen',
|
||||
'icon': 'user-plus', # user-plus, person-circle-plus
|
||||
'path': ('trait-mapping:create', '',
|
||||
'?trait={[object].pk}'),
|
||||
},
|
||||
{
|
||||
'label': 'Zuweisungen anzeigen',
|
||||
'icon': 'user-tag',
|
||||
'path': ('trait-mapping:list', '',
|
||||
'?trait={[object].pk}')
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TraitCreateView(TraitOptions, ModelCreateView):
|
||||
on_success = 'trait:list'
|
||||
|
||||
|
||||
class TraitUpdateView(TraitOptions, ModelUpdateView):
|
||||
fields = ['label', 'description']
|
||||
|
||||
|
||||
class TraitDeleteView(TraitOptions, ModelDeleteView):
|
||||
on_success = 'trait:list'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'trait'
|
||||
urlpatterns = [
|
||||
path('', TraitListView.as_view(), name='list'),
|
||||
path('detail/<str:pk>/', TraitDetailView.as_view(), name='detail'),
|
||||
# path('new/', TraitCreateView.as_view(), name='create'),
|
||||
path('update/<str:pk>/', TraitUpdateView.as_view(), name='update'),
|
||||
# path('delete/<str:pk>/', TraitDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
67
app/base/views/model_views/trait_mapping.py
Executable file
67
app/base/views/model_views/trait_mapping.py
Executable file
@@ -0,0 +1,67 @@
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models import TraitMapping
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class TraitMappingOptions(ViewOptions[TraitMapping], LoginRequired):
|
||||
model = TraitMapping
|
||||
icon = 'user-tag'
|
||||
views = {
|
||||
'list': 'trait-mapping:list',
|
||||
'detail': 'trait-mapping:detail',
|
||||
'create': 'trait-mapping:create',
|
||||
'update': 'trait-mapping:update',
|
||||
'delete': 'trait-mapping:delete'
|
||||
}
|
||||
# detail_fields = []
|
||||
list_filter = {
|
||||
'trait': 'trait__pk',
|
||||
'user': 'user__pk',
|
||||
}
|
||||
list_columns = ['trait', 'user', 'valid_from', 'valid_until']
|
||||
list_render = {
|
||||
'valid_from': {'date_format': 'd. M y'},
|
||||
'valid_until': {'date_format': 'd. M y'},
|
||||
}
|
||||
|
||||
|
||||
class TraitMappingListView(TraitMappingOptions, ModelListView):
|
||||
ordering = ('-valid_from',)
|
||||
|
||||
|
||||
class TraitMappingDetailView(TraitMappingOptions, ModelDetailView):
|
||||
pass
|
||||
|
||||
|
||||
class TraitMappingCreateView(TraitMappingOptions, ModelCreateView):
|
||||
# on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
initial['user'] = self.request.GET.get('user') or None
|
||||
initial['trait'] = self.request.GET.get('trait') or None
|
||||
return initial
|
||||
|
||||
|
||||
class TraitMappingUpdateView(TraitMappingOptions, ModelUpdateView):
|
||||
pass
|
||||
|
||||
|
||||
class TraitMappingDeleteView(TraitMappingOptions, ModelDeleteView):
|
||||
on_success = 'person:detail', '{.user.pk}'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'trait-mapping'
|
||||
urlpatterns = [
|
||||
path('', TraitMappingListView.as_view(), name='list'),
|
||||
path('<int:pk>/', TraitMappingDetailView.as_view(), name='detail'),
|
||||
path('new/', TraitMappingCreateView.as_view(), name='create'),
|
||||
path('update/<int:pk>/', TraitMappingUpdateView.as_view(), name='update'),
|
||||
path('delete/<int:pk>/', TraitMappingDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
90
app/base/views/model_views/transaction.py
Executable file
90
app/base/views/model_views/transaction.py
Executable file
@@ -0,0 +1,90 @@
|
||||
from django.urls import path
|
||||
|
||||
from app.base.models import Transaction
|
||||
from app.base.views.login import LoginRequired
|
||||
from app.base.views.model_views.base import (
|
||||
ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView,
|
||||
ViewOptions, ModelDeleteView
|
||||
)
|
||||
|
||||
|
||||
class TransactionOptions(ViewOptions[Transaction], LoginRequired):
|
||||
model = Transaction
|
||||
icon = 'euro-sign'
|
||||
views = {
|
||||
'list': 'transaction:list',
|
||||
'detail': 'transaction:detail',
|
||||
'create': 'transaction:create',
|
||||
'update': 'transaction:update',
|
||||
'delete': 'transaction:delete'
|
||||
}
|
||||
list_filter = {'user': 'account__pk'}
|
||||
list_columns = ['time_stamp', 'amount', 'account', 'description']
|
||||
list_render = {
|
||||
'time_stamp': {
|
||||
'date_format': 'D. d.m.y, H:i',
|
||||
'width': '11rem',
|
||||
},
|
||||
'amount': {
|
||||
'is_price': True,
|
||||
'class': 'text-right',
|
||||
'width': '6rem',
|
||||
},
|
||||
'account': {
|
||||
'verbose_name': 'Nutzer:in',
|
||||
'format': '{.user}',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TransactionListView(TransactionOptions, ModelListView):
|
||||
ordering = ('-time_stamp',)
|
||||
|
||||
|
||||
class TransactionDetailView(TransactionOptions, ModelDetailView):
|
||||
pass
|
||||
|
||||
|
||||
class TransactionCreateView(TransactionOptions, ModelCreateView):
|
||||
on_success = 'person:detail', '{.account.user.pk}'
|
||||
fields = ['account', 'amount', 'description']
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
amount = float(self.request.GET.get('amount') or 0)
|
||||
typ = self.request.GET.get('_type')
|
||||
if typ == 'deposit-plus':
|
||||
desc = 'Einzahlung'
|
||||
elif typ == 'deposit-minus':
|
||||
desc = 'Auszahlung'
|
||||
if amount > 0:
|
||||
amount *= -1
|
||||
else:
|
||||
desc = self.request.GET.get('description')
|
||||
|
||||
initial['account'] = self.request.GET.get('account') or None
|
||||
initial['amount'] = amount
|
||||
initial['description'] = desc or None
|
||||
return initial
|
||||
|
||||
|
||||
class TransactionUpdateView(TransactionOptions, ModelUpdateView):
|
||||
fields = ['account', 'amount', 'description']
|
||||
on_success = 'person:detail', '{.account.user.pk}'
|
||||
# on_success = (
|
||||
# 'transaction:list', '', '?user={.account.user.pk}&selected={.pk}')
|
||||
|
||||
|
||||
class TransactionDeleteView(TransactionOptions, ModelDeleteView):
|
||||
on_success = 'transaction:list'
|
||||
|
||||
|
||||
# URL paths
|
||||
app_name = 'transaction'
|
||||
urlpatterns = [
|
||||
path('', TransactionListView.as_view(), name='list'),
|
||||
path('<int:pk>/', TransactionDetailView.as_view(), name='detail'),
|
||||
path('new/', TransactionCreateView.as_view(), name='create'),
|
||||
path('update/<int:pk>/', TransactionUpdateView.as_view(), name='update'),
|
||||
path('delete/<int:pk>/', TransactionDeleteView.as_view(), name='delete'),
|
||||
]
|
||||
Reference in New Issue
Block a user