Skip to content

Commit

Permalink
Merge branch 'refs/heads/main' into datetime-field-shortcuts
Browse files Browse the repository at this point in the history
  • Loading branch information
ramazan committed Aug 12, 2024
2 parents d934890 + c36f846 commit 37d3e69
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 9 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ repos:
- id: trailing-whitespace
exclude: CHANGELOG.md
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.5
rev: v0.5.6
hooks:
- id: ruff
args:
- --fix
- id: ruff-format
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.3.0
rev: v3.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,9 @@ class CustomAdminClass(ModelAdmin):
# Display fields in changeform in compressed mode
compressed_fields = True # Default: False

# Warn before leaving unsaved changes in changeform
warn_unsaved_form = True # Default: False

# Preprocess content of readonly fields before render
readonly_preprocess_fields = {
"model_field_name": "html.unescape",
Expand Down Expand Up @@ -621,7 +624,14 @@ The difference between them is that `ChoicesDropdownFilter` will collect a list
from django.contrib import admin
from django.contrib.auth.models import User
from unfold.admin import ModelAdmin
from unfold.contrib.filters.admin import ChoicesDropdownFilter, RelatedDropdownFilter, DropdownFilter
from unfold.contrib.filters.admin import (
ChoicesDropdownFilter,
MultipleChoicesDropdownFilter,
RelatedDropdownFilter,
MultipleRelatedDropdownFilter,
DropdownFilter,
MultipleDropdownFilter
)


class CustomDropdownFilter(DropdownFilter):
Expand All @@ -648,7 +658,9 @@ class MyAdmin(ModelAdmin):
list_filter = [
CustomDropdownFilter,
("modelfield_with_choices", ChoicesDropdownFilter),
("modelfield_with_choices_multiple", MultipleChoicesDropdownFilter),
("modelfield_with_foreign_key", RelatedDropdownFilter)
("modelfield_with_foreign_key_multiple", MultipleRelatedDropdownFilter)
]
```

Expand Down
1 change: 1 addition & 0 deletions src/unfold/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
list_disable_select_all = False
compressed_fields = False
readonly_preprocess_fields = {}
warn_unsaved_form = False
checks_class = UnfoldModelAdminChecks

@property
Expand Down
28 changes: 27 additions & 1 deletion src/unfold/contrib/filters/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,23 @@ def value(self) -> Optional[str]:
)


class MultiValueMixin:
def value(self) -> Optional[List[str]]:
return (
self.lookup_val
if self.lookup_val not in EMPTY_VALUES
and isinstance(self.lookup_val, List)
and len(self.lookup_val) > 0
else self.lookup_val
)


class DropdownMixin:
template = "unfold/filters/filters_field.html"
form_class = DropdownForm
all_option = ["", _("All")]

def queryset(self, request, queryset) -> QuerySet:
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
if self.value() not in EMPTY_VALUES:
return super().queryset(request, queryset)

Expand Down Expand Up @@ -112,11 +123,16 @@ def choices(self, changelist: ChangeList) -> Tuple[Dict[str, Any], ...]:
name=self.parameter_name,
choices=[self.all_option, *self.lookup_choices],
data={self.parameter_name: self.value()},
multiple=self.multiple if hasattr(self, "multiple") else False,
),
},
)


class MultipleDropdownFilter(MultiValueMixin, DropdownFilter):
multiple = True


class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFilter):
def choices(self, changelist: ChangeList):
choices = [self.all_option, *self.field.flatchoices]
Expand All @@ -127,10 +143,15 @@ def choices(self, changelist: ChangeList):
name=self.lookup_kwarg,
choices=choices,
data={self.lookup_kwarg: self.value()},
multiple=self.multiple if hasattr(self, "multiple") else False,
),
}


class MultipleChoicesDropdownFilter(MultiValueMixin, ChoicesDropdownFilter):
multiple = True


class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFilter):
def choices(self, changelist: ChangeList):
yield {
Expand All @@ -139,10 +160,15 @@ def choices(self, changelist: ChangeList):
name=self.lookup_kwarg,
choices=[self.all_option, *self.lookup_choices],
data={self.lookup_kwarg: self.value()},
multiple=self.multiple if hasattr(self, "multiple") else False,
),
}


class MultipleRelatedDropdownFilter(MultiValueMixin, RelatedDropdownFilter):
multiple = True


class SingleNumericFilter(admin.FieldListFilter):
request = None
parameter_name = None
Expand Down
39 changes: 36 additions & 3 deletions src/unfold/contrib/filters/forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django import forms
from django.forms import ChoiceField, MultipleChoiceField
from django.utils.translation import gettext_lazy as _

from ...widgets import (
INPUT_CLASSES,
UnfoldAdminSelectMultipleWidget,
UnfoldAdminSelectWidget,
UnfoldAdminSplitDateTimeVerticalWidget,
UnfoldAdminTextInputWidget,
Expand All @@ -21,15 +23,46 @@ def __init__(self, name, label, *args, **kwargs):


class DropdownForm(forms.Form):
def __init__(self, name, label, choices, *args, **kwargs):
widget = UnfoldAdminSelectWidget(
attrs={
"data-theme": "admin-autocomplete",
"class": "admin-autocomplete",
}
)
field = ChoiceField

def __init__(self, name, label, choices, multiple=False, *args, **kwargs):
super().__init__(*args, **kwargs)

self.fields[name] = forms.ChoiceField(
if multiple:
self.widget = UnfoldAdminSelectMultipleWidget(
attrs={
"data-theme": "admin-autocomplete",
"class": "admin-autocomplete",
}
)
self.field = MultipleChoiceField

self.fields[name] = self.field(
label=label,
required=False,
choices=choices,
widget=UnfoldAdminSelectWidget,
widget=self.widget,
)

class Media:
js = (
"admin/js/vendor/jquery/jquery.js",
"admin/js/vendor/select2/select2.full.js",
"admin/js/jquery.init.js",
"unfold/js/select2.init.js",
)
css = {
"screen": (
"admin/css/vendor/select2/select2.css",
"admin/css/autocomplete.css",
),
}


class SingleNumericForm(forms.Form):
Expand Down
23 changes: 23 additions & 0 deletions src/unfold/static/unfold/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,31 @@ window.addEventListener("load", (e) => {
renderCharts();

filterForm();

warnWithoutSaving();
});

/*************************************************************
* Warn without saving
*************************************************************/
const warnWithoutSaving = () => {
let formChanged = false;

Array.from(
document.querySelectorAll(
"form.warn-unsaved-form input, form.warn-unsaved-form select"
)
).forEach((field) => {
field.addEventListener("change", (e) => (formChanged = true));
});

window.addEventListener("beforeunload", (e) => {
if (formChanged) {
e.preventDefault();
}
});
};

/*************************************************************
* Filter form
*************************************************************/
Expand Down
15 changes: 15 additions & 0 deletions src/unfold/static/unfold/js/select2.init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";
{
const $ = django.jQuery;

$.fn.djangoCustomSelect2 = function () {
$.each(this, function (i, element) {
$(element).select2();
});
return this;
};

$(function () {
$(".admin-autocomplete").djangoCustomSelect2();
});
}
2 changes: 1 addition & 1 deletion src/unfold/templates/admin/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

{% block content %}
<div id="content-main">
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" novalidate>
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" {% if adminform.model_admin.warn_unsaved_form %}class="warn-unsaved-form"{% endif %} novalidate>
{% csrf_token %}

{% block form_top %}{% endblock %}
Expand Down
12 changes: 11 additions & 1 deletion src/unfold/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
NumberInput,
PasswordInput,
Select,
SelectMultiple,
)
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -500,7 +501,16 @@ def __init__(self, attrs=None, choices=()):
if attrs is None:
attrs = {}

attrs["class"] = " ".join(SELECT_CLASSES)
attrs["class"] = " ".join([*SELECT_CLASSES, attrs.get("class", "")])
super().__init__(attrs, choices)


class UnfoldAdminSelectMultipleWidget(SelectMultiple):
def __init__(self, attrs=None, choices=()):
if attrs is None:
attrs = {}

attrs["class"] = " ".join([*SELECT_CLASSES, attrs.get("class", "")])
super().__init__(attrs, choices)


Expand Down

0 comments on commit 37d3e69

Please sign in to comment.