summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2024-11-14 11:54:58 +0100
committerÉtienne Loks <etienne.loks@iggdrasil.net>2025-02-19 14:45:55 +0100
commit56beeee5f8be48cc513e28721e9c17aa75fdd793 (patch)
treefae7c455c76ab7b14d123d72d9d7706fb0de21f5
parent4426024156e1bff42cbd37f0c0e273c369c3773f (diff)
downloadIshtar-56beeee5f8be48cc513e28721e9c17aa75fdd793.tar.bz2
Ishtar-56beeee5f8be48cc513e28721e9c17aa75fdd793.zip
✨ admin page: filtered sheets - filters, sheets
✨ admin page: filtered sheets - filters, sheets
-rw-r--r--ishtar_common/admin.py205
-rw-r--r--ishtar_common/models_common.py17
2 files changed, 185 insertions, 37 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py
index 5ca7ef105..7a621f601 100644
--- a/ishtar_common/admin.py
+++ b/ishtar_common/admin.py
@@ -836,6 +836,172 @@ class PermissionRequestAdmin(admin.ModelAdmin):
form = PermissionRequestAdminForm
+def get_content_types_with_sheet(with_empty=False):
+ choices = []
+ for ct in ContentType.objects.all():
+ klass = ct.model_class()
+ if not klass:
+ continue
+ if getattr(klass, "SHOW_URL", None):
+ choices.append((ct.pk, ct.name))
+ lst = list(
+ sorted(choices, key=lambda x: x[1])
+ )
+ if with_empty:
+ lst = [("", "-" * 9)] + lst
+ return lst
+
+
+class ContentTypeChoice:
+ def set_content_types_choices(self, field_name):
+ self.fields[field_name].choices = get_content_types_with_sheet()
+
+
+class ContentTypeListFilter(admin.SimpleListFilter):
+ # Only display content types with sheet attached
+ title = _("content type")
+ parameter_name = 'content_type'
+
+ def lookups(self, request, model_admin):
+ return get_content_types_with_sheet(with_empty=False)
+
+ def queryset(self, request, queryset):
+ value = self.value()
+ if value:
+ query = {f"{self.parameter_name}_id": self.value()}
+ return queryset.filter(**query)
+
+
+class BaseSheetFilterForm(forms.ModelForm, ContentTypeChoice):
+ content_type_field = ""
+ key = forms.CharField(
+ label=_("Key"),
+ initial="-",
+ help_text=_("Save first to choose a key"),
+ max_length=200,
+ )
+
+ def _get_content_type_model(self, instance):
+ raise NotImplementedError()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set_content_types_choices(self.content_type_field)
+ instance = kwargs.get("instance")
+ if not instance:
+ return
+ if args:
+ query_dict = args[0]
+ query_dict._mutable = True
+ query_dict.setlist(
+ self.content_type_field,
+ [getattr(instance, self.content_type_field).pk]
+ )
+ query_dict._mutable = False
+ self.fields[self.content_type_field].widget.attrs = {"disabled": "disabled"}
+ keys = instance.get_keys()
+ model = self._get_content_type_model(instance)
+ help_text = []
+ for key in keys:
+ try:
+ field = model._meta.get_field(key)
+ except FieldDoesNotExist:
+ field = None
+ if field and getattr(field, "verbose_name", None):
+ key += f" ({field.verbose_name})"
+ help_text.append(key)
+ self.fields["key"].help_text = str(_("Available keys: ")) + " ; ".join(
+ help_text
+ )
+
+
+class SheetFilterForm(BaseSheetFilterForm):
+ class Meta:
+ model = models_common.SheetFilter
+ exclude = []
+
+ content_type_field = "content_type"
+
+ def _get_content_type_model(self, instance):
+ return instance.content_type.model_class()
+
+
+class ContentTypeSearchAdmin:
+ def search_results(self, queryset, search_term):
+ # search with real model name
+ # not efficient but searching only in ~10 models
+ search_terms = search_term.split(" ")
+ for ct in ContentType.objects.all():
+ klass = ct.model_class()
+ if not klass:
+ continue
+ if getattr(klass, "SHOW_URL", None):
+ name = ct.name.lower()
+ for idx, k in enumerate(reversed(search_terms[:])):
+ if k in name:
+ queryset |= self.model.objects.filter(
+ content_type_id=ct.pk)
+ return queryset
+
+ def get_search_results(self, request, queryset, search_term):
+ queryset, may_have_duplicates = super().get_search_results(
+ request,
+ queryset,
+ search_term,
+ )
+ queryset = self.search_results(queryset, search_term)
+ return queryset, may_have_duplicates
+
+
+@admin.register(models_common.SheetFilter, site=admin_site)
+class SheetFilterAdmin(ContentTypeSearchAdmin, admin.ModelAdmin):
+ form = SheetFilterForm
+ model = models_common.SheetFilter
+ list_display = ("content_type", "exclude_or_include", "key")
+ list_filter = (ContentTypeListFilter,)
+ search_fields = ("key",)
+
+
+class FilteredSheetForm(forms.ModelForm, ContentTypeChoice):
+ class Meta:
+ model = models_common.FilteredSheet
+ exclude = []
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set_content_types_choices("content_type")
+
+ def clean(self):
+ filters = self.cleaned_data.get("filters", [])
+ content_type = self.cleaned_data.get("content_type", None)
+ if not filters:
+ return self.cleaned_data
+ exc = None
+ for filtr in filters.all():
+ if content_type != filtr.content_type:
+ raise forms.ValidationError(
+ _("Bad filter configuration. "
+ "Only use filters of the associacted content type.")
+ )
+ if not exc:
+ exc = filtr.exclude_or_include
+ elif exc != filtr.exclude_or_include:
+ raise forms.ValidationError(
+ _("You cannot mix exclude and include filters.")
+ )
+ return self.cleaned_data
+
+
+@admin.register(models_common.FilteredSheet, site=admin_site)
+class FilteredSheetAdmin(ContentTypeSearchAdmin, admin.ModelAdmin):
+ form = FilteredSheetForm
+ model = models_common.FilteredSheet
+ list_display = ("name", "content_type")
+ list_filter = (ContentTypeListFilter,)
+ autocomplete_fields = ("filters",)
+ search_fields = ("name",)
+
+
MAIN_ITEM_READONLY_FIELDS = [
"history_creator",
"history_modifier",
@@ -1659,7 +1825,7 @@ admin_site.register(models.Area, AreaAdmin)
class ProfileTypeAdmin(GeneralTypeAdmin):
model = models.ProfileType
filter_vertical = ("groups",)
- autocomplete_fields = ("permission_requests",)
+ autocomplete_fields = ("permission_requests", "filtered_sheets")
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
@@ -2927,47 +3093,20 @@ class ApiKeyMatchAdmin(admin.ModelAdmin):
admin_site.register(models_rest.ApiKeyMatch, ApiKeyMatchAdmin)
-class ApiSheetFilterForm(forms.ModelForm):
+class ApiSheetFilterForm(BaseSheetFilterForm):
api_search_model = forms.ModelChoiceField(
models_rest.ApiSearchModel.objects,
label=_("API - Remote access - Search model"),
)
- key = forms.CharField(
- label=_("Key"),
- initial="-",
- help_text=_("Save first to choose a key"),
- max_length=200,
- )
class Meta:
model = models_rest.ApiSheetFilter
fields = ["api_search_model", "key"]
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- instance = kwargs.get("instance")
- if not instance:
- return
- if args:
- query_dict = args[0]
- query_dict._mutable = True
- query_dict.setlist("api_search_model", [instance.api_search_model.pk])
- query_dict._mutable = False
- self.fields["api_search_model"].widget.attrs = {"disabled": "disabled"}
- keys = instance.get_keys()
- model = instance.api_search_model.content_type.model_class()
- help_text = []
- for key in keys:
- try:
- field = model._meta.get_field(key)
- except FieldDoesNotExist:
- field = None
- if field and getattr(field, "verbose_name", None):
- key += f" ({field.verbose_name})"
- help_text.append(key)
- self.fields["key"].help_text = str(_("Available keys: ")) + " ; ".join(
- help_text
- )
+ content_type_field = "api_search_model"
+
+ def _get_content_type_model(self, instance):
+ return instance.api_search_model.content_type.model_class()
class ApiSheetFilterAdmin(admin.ModelAdmin):
diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py
index 613c54fac..cd74ca068 100644
--- a/ishtar_common/models_common.py
+++ b/ishtar_common/models_common.py
@@ -885,10 +885,16 @@ class SheetFilter(BaseSheetFilter):
)
class Meta:
- verbose_name = _("Sheet filter")
- verbose_name_plural = _("Sheet filters")
+ verbose_name = _("Filtered sheet - Filter")
+ verbose_name_plural = _("Filtered sheet - Filters")
ADMIN_SECTION = _("Account")
+ def __str__(self):
+ exc = _("exclude")
+ if self.exclude_or_include == "I":
+ exc = _("include")
+ return f"{self.content_type.model_class()._meta.verbose_name} | {exc} | {self.key}"
+
def get_template(self):
ct = self.content_type
model = apps.get_model(ct.app_label, ct.model)
@@ -907,10 +913,13 @@ class FilteredSheet(models.Model):
)
class Meta:
- verbose_name = _("Filtered sheet")
- verbose_name_plural = _("Filtered sheets")
+ verbose_name = _("Filtered sheet - Sheet")
+ verbose_name_plural = _("Filtered sheet - Sheets")
ADMIN_SECTION = _("Account")
+ def __str__(self):
+ return f"{self.content_type.model_class()._meta.verbose_name} | {self.name}"
+
class FullSearch(models.Model):
search_vector = SearchVectorField(