├── .flake8 ├── .gitignore ├── CHANGES.md ├── Dockerfile ├── LICENSE ├── README.md ├── demo ├── .env.sample ├── coffee │ ├── 1.jpeg │ ├── 10.jpeg │ ├── 11.jpeg │ ├── 12.jpeg │ ├── 13.jpeg │ ├── 14.jpeg │ ├── 15.jpeg │ ├── 16.jpeg │ ├── 17.jpeg │ ├── 18.jpeg │ ├── 19.jpeg │ ├── 2.jpeg │ ├── 20.jpeg │ ├── 21.jpeg │ ├── 22.jpeg │ ├── 23.jpeg │ ├── 24.jpeg │ ├── 25.jpeg │ ├── 26.jpeg │ ├── 27.jpeg │ ├── 28.jpeg │ ├── 29.jpeg │ ├── 3.jpeg │ ├── 30.jpeg │ ├── 31.jpeg │ ├── 32.jpeg │ ├── 33.jpeg │ ├── 34.jpeg │ ├── 35.jpeg │ ├── 36.jpeg │ ├── 37.jpeg │ ├── 38.jpeg │ ├── 39.jpeg │ ├── 4.jpeg │ ├── 40.jpeg │ ├── 41.jpeg │ ├── 42.jpeg │ ├── 43.jpeg │ ├── 44.jpeg │ ├── 45.jpeg │ ├── 46.jpeg │ ├── 47.jpeg │ ├── 48.jpeg │ ├── 49.jpeg │ ├── 5.jpeg │ ├── 50.jpeg │ ├── 6.jpeg │ ├── 7.jpeg │ ├── 8.jpeg │ └── 9.jpeg ├── demo │ ├── __init__.py │ ├── asgi.py │ ├── forms.py │ ├── middleware.py │ ├── settings │ │ ├── base.py │ │ ├── development.py │ │ └── production.py │ ├── urls.py │ └── wsgi.py ├── demo_app │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── factory.py │ ├── filters.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ └── tests.py ├── manage.py ├── tasks.py └── templates │ └── admin │ ├── index.html │ └── login.html ├── docs ├── awesome.md ├── changes.md ├── contributing.md ├── customization.md ├── index.md ├── install.md ├── notes.md ├── screenshots.md └── screenshots │ ├── change-form-collapse-menu.png │ ├── change-form-stacked.png │ ├── change-form.png │ ├── change-list-collapse-menu.png │ ├── change-list-stacked.png │ ├── change-list.png │ ├── django-filter.png │ ├── django-import-export.png │ ├── html5-autocomplete.png │ └── pony-powered.png ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml └── semantic_admin ├── __init__.py ├── admin.py ├── awesomesearch.py ├── contrib ├── __init__.py └── import_export │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ └── widgets.py ├── helpers.py ├── static ├── admin │ └── js │ │ ├── actions.js │ │ ├── admin │ │ ├── DateTimeShortcuts.js │ │ └── RelatedObjectLookups.js │ │ └── inlines.js └── semantic_admin │ ├── django-white.svg │ ├── semantic-admin.css │ └── unsemantic.css ├── templates ├── admin │ ├── actions.html │ ├── auth │ │ └── user │ │ │ └── add_form.html │ ├── base.html │ ├── base_site.html │ ├── change_form.html │ ├── change_list.html │ ├── change_list_results.html │ ├── date_hierarchy.html │ ├── delete_button.html │ ├── delete_confirmation.html │ ├── delete_selected_confirmation.html │ ├── edit_inline │ │ ├── stacked.html │ │ └── tabular.html │ ├── filter.html │ ├── import_export │ │ ├── base.html │ │ ├── change_list.html │ │ ├── change_list_export_item.html │ │ ├── change_list_import_item.html │ │ ├── export.html │ │ ├── export_action_select.html │ │ ├── import.html │ │ └── resource_fields_list.html │ ├── includes │ │ ├── fieldset.html │ │ ├── fieldset_content.html │ │ └── object_delete_summary.html │ ├── index.html │ ├── login.html │ ├── main.html │ ├── menu.html │ ├── object_history.html │ ├── pagination.html │ ├── save_buttons.html │ ├── search_form.html │ ├── submit_line.html │ └── widgets │ │ └── related_widget_wrapper.html ├── registration │ ├── logged_out.html │ ├── login.html │ ├── password_change_done.html │ ├── password_change_form.html │ ├── password_reset_complete.html │ ├── password_reset_confirm.html │ ├── password_reset_done.html │ └── password_reset_form.html └── semantic_admin │ ├── action_checkbox.html │ └── changelist_checkbox.html ├── templatetags ├── __init__.py ├── awesomesearch.py ├── semantic_admin_list.py ├── semantic_app_list.py ├── semantic_filters.py └── semantic_utils.py ├── utils.py ├── views ├── __init__.py └── autocomplete.py └── widgets ├── __init__.py ├── admin.py └── autocomplete.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 89 3 | ignore = W291,W503 # Trailing whitespace, line break before binary operator 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.sqlite3 3 | .env 4 | .coverage 5 | .DS_Store 6 | dist/ 7 | static/ 8 | media/ 9 | site/ 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | v0.6.0 2 | ------ 3 | * Require Python >= 3.10 4 | * Replace deprecated length_is filter with length for Django 5.1 5 | 6 | v0.5.0 7 | ------ 8 | * Forms were moved to [django-semantic-forms](https://github.com/globophobe/django-semantic-forms) so that they may be used outside of the admin. 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | ARG WHEEL 4 | ARG POETRY_EXPORT 5 | ARG SECRET_KEY 6 | ARG SENTRY_DSN 7 | 8 | ENV SECRET_KEY $SECRET_KEY 9 | ENV SENTRY_DSN $SENTRY_DSN 10 | 11 | COPY dist/$WHEEL / 12 | COPY demo/demo /demo/demo 13 | COPY demo/demo_app /demo/demo_app 14 | COPY demo/static /demo/static 15 | COPY demo/media /demo/media 16 | COPY demo/templates /demo/templates 17 | COPY demo/db.sqlite3 /demo/db.sqlite3 18 | 19 | RUN pip install --no-cache-dir wheel 20 | RUN pip install $WHEEL 21 | RUN pip install --no-cache-dir $POETRY_EXPORT sentry-sdk 22 | RUN rm $WHEEL 23 | 24 | ENTRYPOINT ["gunicorn", "--chdir", "/demo", "--bind", "0.0.0.0:8080", "--threads", "2", "--timeout", "0", "--preload", "demo.wsgi:application"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2024 Alex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Semantic UI admin theme 2 | ------------------------------ 3 | django-semantic-admin 4 | 5 | A completely free (MIT) [Semantic UI](https://semantic-ui.com/) admin theme for Django. Actually, this is my 3rd admin theme for Django. The first was forgettable, and the second was with [Pure CSS](https://purecss.io/). Pure CSS was great, but lacked JavaScript components. 6 | 7 | Semantic UI looks professional, and has great JavaScript components. 8 | 9 | Log in to the demo with username `admin` and password `semantic`: https://semantic-admin.com 10 | 11 | Documentation is on [GitHub Pages](https://globophobe.github.io/django-semantic-admin/). 12 | 13 | 14 | Django Semantic Forms 15 | --------------------- 16 | 🎉 As of v0.5.0, forms were moved to [django-semantic-forms](https://github.com/globophobe/django-semantic-forms). `semantic_forms` must be added to INSTALLED_APPS. 17 | 18 | ```python 19 | INSTALLED_APPS = [ 20 | "semantic_admin", 21 | "semantic_forms", 22 | ... 23 | ] 24 | ``` 25 | 26 | You may use `semantic_forms` outside of the admin. 27 | 28 | 29 | Why? 30 | ---- 31 | * Looks professional, with a nice sidebar. 32 | * Responsive design, even [tables can stack](https://semantic-ui.com/collections/table.html#stacking) responsively on mobile. 33 | * JavaScript datepicker and timepicker components. 34 | * JavaScript selects, including multiple selections, which integrate well with Django autocomplete fields. 35 | * Semantic UI has libraries for [React](https://react.semantic-ui.com/) and [Vue](https://semantic-ui-vue.github.io/#/), in addition to jQuery. This means this package can be used to style the admin, and custom views can be added with React or Vue components with the same style. 36 | 37 | 38 | Install 39 | ------- 40 | 41 | Install from PyPI: 42 | 43 | ``` 44 | pip install django-semantic-admin 45 | ``` 46 | 47 | Add to `settings.py` before `django.contrib.admin`: 48 | 49 | ```python 50 | INSTALLED_APPS = [ 51 | "semantic_admin", 52 | "semantic_forms", 53 | "django.contrib.admin", 54 | ... 55 | ] 56 | ``` 57 | 58 | Please remember to run `python manage.py collectstatic` for production deployments. 59 | 60 | Usage 61 | ----- 62 | 63 | Instead of `admin.ModelAdmin`, `admin.StackedInline`, or `admin.TabularInline`: 64 | 65 | ```python 66 | class ExampleStackedInline(admin.StackedInline): 67 | pass 68 | 69 | class ExampleTabularInline(admin.TabularInline): 70 | pass 71 | 72 | class ExampleAdmin(admin.ModelAdmin): 73 | inlines = (ExampleStackedInline, ExampleTabularInline) 74 | ``` 75 | 76 | Inherit from their `Semantic` equivalents: 77 | 78 | ```python 79 | from semantic_admin import SemanticModelAdmin, SemanticStackedInline, SemanticTabularInline 80 | 81 | class ExampleStackedInline(SemanticStackedInline): 82 | pass 83 | 84 | class ExampleTabularInline(SemanticTabularInline): 85 | pass 86 | 87 | class ExampleAdmin(SemanticModelAdmin): 88 | inlines = (ExampleStackedInline, ExampleTabularInline) 89 | ``` 90 | 91 | Awesome optional features 92 | ------------------------- 93 | 94 | 1. Optional integration with [django-filter](https://github.com/carltongibson/django-filter): 95 | 96 | django-filter 97 | 98 | To enable this awesome feature, add `filterset_class` to your Django admin: 99 | 100 | ```python 101 | from semantic_forms.filters import SemanticFilterSet 102 | 103 | class DemoFilter(SemanticFilterSet): 104 | class Meta: 105 | model = Demo 106 | fields = ("demo_field",) 107 | 108 | class DemoAdmin(SemanticModelAdmin): 109 | filterset_class = DemoFilter 110 | ``` 111 | 112 | 2. HTML preview in Django `autocomplete_fields`: 113 | 114 | html5-autocomplete 115 | 116 | To enable this awesome feature, add the `semantic_autocomplete` property to your Django model: 117 | 118 | ```python 119 | class DemoModel(models.Model): 120 | @property 121 | def semantic_autocomplete(self): 122 | html = self.get_img() 123 | return format_html(html) 124 | ``` 125 | 126 | 3. Optional integration with [django-import-export](https://github.com/django-import-export/django-import-export): 127 | 128 | django-import-export 129 | 130 | To enable this awesome feature, instead of `ImportExportModelAdmin`, etc: 131 | 132 | ```python 133 | from import_export.admin import ImportExportModelAdmin 134 | 135 | class ExampleImportExportAdmin(ImportExportModelAdmin): 136 | pass 137 | ``` 138 | 139 | Inherit from their `Semantic` equivalents: 140 | 141 | ```python 142 | from semantic_admin.contrib.import_export.admin import SemanticImportExportModelAdmin 143 | 144 | class ExampleImportExportAdmin(SemanticImportExportModelAdmin): 145 | pass 146 | ``` 147 | 148 | Contributing 149 | ------------ 150 | 151 | Install dependencies with `poetry install`. The demo is built with [invoke tasks](https://github.com/globophobe/django-semantic-admin/blob/master/demo/tasks.py). For example, `cd demo; invoke build`. 152 | 153 | 154 | Notes 155 | ----- 156 | Please note, this package uses [Fomantic UI](https://fomantic-ui.com/) the official community fork of Semantic UI. 157 | -------------------------------------------------------------------------------- /demo/.env.sample: -------------------------------------------------------------------------------- 1 | [settings] 2 | SECRET_KEY= 3 | SENTRY_DSN= 4 | -------------------------------------------------------------------------------- /demo/coffee/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/1.jpeg -------------------------------------------------------------------------------- /demo/coffee/10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/10.jpeg -------------------------------------------------------------------------------- /demo/coffee/11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/11.jpeg -------------------------------------------------------------------------------- /demo/coffee/12.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/12.jpeg -------------------------------------------------------------------------------- /demo/coffee/13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/13.jpeg -------------------------------------------------------------------------------- /demo/coffee/14.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/14.jpeg -------------------------------------------------------------------------------- /demo/coffee/15.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/15.jpeg -------------------------------------------------------------------------------- /demo/coffee/16.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/16.jpeg -------------------------------------------------------------------------------- /demo/coffee/17.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/17.jpeg -------------------------------------------------------------------------------- /demo/coffee/18.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/18.jpeg -------------------------------------------------------------------------------- /demo/coffee/19.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/19.jpeg -------------------------------------------------------------------------------- /demo/coffee/2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/2.jpeg -------------------------------------------------------------------------------- /demo/coffee/20.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/20.jpeg -------------------------------------------------------------------------------- /demo/coffee/21.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/21.jpeg -------------------------------------------------------------------------------- /demo/coffee/22.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/22.jpeg -------------------------------------------------------------------------------- /demo/coffee/23.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/23.jpeg -------------------------------------------------------------------------------- /demo/coffee/24.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/24.jpeg -------------------------------------------------------------------------------- /demo/coffee/25.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/25.jpeg -------------------------------------------------------------------------------- /demo/coffee/26.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/26.jpeg -------------------------------------------------------------------------------- /demo/coffee/27.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/27.jpeg -------------------------------------------------------------------------------- /demo/coffee/28.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/28.jpeg -------------------------------------------------------------------------------- /demo/coffee/29.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/29.jpeg -------------------------------------------------------------------------------- /demo/coffee/3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/3.jpeg -------------------------------------------------------------------------------- /demo/coffee/30.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/30.jpeg -------------------------------------------------------------------------------- /demo/coffee/31.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/31.jpeg -------------------------------------------------------------------------------- /demo/coffee/32.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/32.jpeg -------------------------------------------------------------------------------- /demo/coffee/33.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/33.jpeg -------------------------------------------------------------------------------- /demo/coffee/34.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/34.jpeg -------------------------------------------------------------------------------- /demo/coffee/35.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/35.jpeg -------------------------------------------------------------------------------- /demo/coffee/36.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/36.jpeg -------------------------------------------------------------------------------- /demo/coffee/37.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/37.jpeg -------------------------------------------------------------------------------- /demo/coffee/38.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/38.jpeg -------------------------------------------------------------------------------- /demo/coffee/39.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/39.jpeg -------------------------------------------------------------------------------- /demo/coffee/4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/4.jpeg -------------------------------------------------------------------------------- /demo/coffee/40.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/40.jpeg -------------------------------------------------------------------------------- /demo/coffee/41.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/41.jpeg -------------------------------------------------------------------------------- /demo/coffee/42.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/42.jpeg -------------------------------------------------------------------------------- /demo/coffee/43.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/43.jpeg -------------------------------------------------------------------------------- /demo/coffee/44.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/44.jpeg -------------------------------------------------------------------------------- /demo/coffee/45.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/45.jpeg -------------------------------------------------------------------------------- /demo/coffee/46.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/46.jpeg -------------------------------------------------------------------------------- /demo/coffee/47.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/47.jpeg -------------------------------------------------------------------------------- /demo/coffee/48.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/48.jpeg -------------------------------------------------------------------------------- /demo/coffee/49.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/49.jpeg -------------------------------------------------------------------------------- /demo/coffee/5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/5.jpeg -------------------------------------------------------------------------------- /demo/coffee/50.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/50.jpeg -------------------------------------------------------------------------------- /demo/coffee/6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/6.jpeg -------------------------------------------------------------------------------- /demo/coffee/7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/7.jpeg -------------------------------------------------------------------------------- /demo/coffee/8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/8.jpeg -------------------------------------------------------------------------------- /demo/coffee/9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/coffee/9.jpeg -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for demo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /demo/demo/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.admin.forms import AdminAuthenticationForm 3 | from django.contrib.auth.forms import UsernameField 4 | from django.utils.html import format_html 5 | from django.views.decorators.debug import sensitive_variables 6 | 7 | try: 8 | from django.utils.translation import gettext_lazy as _ # Django >= 4 9 | except ImportError: 10 | from django.utils.translation import ugettext_lazy as _ 11 | 12 | 13 | class LoginForm(AdminAuthenticationForm): 14 | """Login form.""" 15 | 16 | error_messages = { 17 | **AdminAuthenticationForm.error_messages, 18 | "invalid_login": format_html( 19 | _("Please enter username admin and password semantic.") 20 | ), 21 | } 22 | 23 | username = UsernameField( 24 | widget=forms.TextInput(attrs={"placeholder": "admin", "autofocus": True}), 25 | ) 26 | password = forms.CharField( 27 | label=_("Password"), 28 | strip=False, 29 | widget=forms.PasswordInput(attrs={"placeholder": "semantic"}), 30 | ) 31 | -------------------------------------------------------------------------------- /demo/demo/middleware.py: -------------------------------------------------------------------------------- 1 | from django.urls import get_script_prefix 2 | 3 | try: 4 | from urlparse.parse import urlparse 5 | except ImportError: 6 | from urllib.parse import urlparse 7 | 8 | from whitenoise.middleware import WhiteNoiseMiddleware 9 | from whitenoise.string_utils import ensure_leading_trailing_slash 10 | 11 | 12 | class WhiteNoiseMediaMiddleware(WhiteNoiseMiddleware): 13 | def configure_from_settings(self, settings): 14 | # Default configuration 15 | self.autorefresh = settings.DEBUG 16 | self.use_finders = settings.DEBUG 17 | self.static_prefix = urlparse(settings.MEDIA_URL or "").path 18 | script_prefix = get_script_prefix().rstrip("/") 19 | if script_prefix: 20 | if self.static_prefix.startswith(script_prefix): 21 | self.static_prefix = self.static_prefix[len(script_prefix) :] # noqa 22 | if settings.DEBUG: 23 | self.max_age = 0 24 | # Allow settings to override default attributes 25 | for attr in self.config_attrs: 26 | settings_key = "WHITENOISE_{0}".format(attr.upper()) 27 | try: 28 | value = getattr(settings, settings_key) 29 | except AttributeError: 30 | pass 31 | else: 32 | value = value 33 | setattr(self, attr, value) 34 | self.static_prefix = ensure_leading_trailing_slash(self.static_prefix) 35 | self.static_root = settings.MEDIA_ROOT 36 | -------------------------------------------------------------------------------- /demo/demo/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | from decouple import config 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = config("SECRET_KEY") 25 | 26 | MEDIA_ROOT = BASE_DIR / "media" 27 | MEDIA_URL = "/media/" 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | "semantic_admin", 33 | "semantic_forms", 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | # 3rd party 41 | "taggit", 42 | "whitenoise", 43 | "demo_app", 44 | ] 45 | 46 | SEMANTIC_APP_LIST = [ 47 | { 48 | "app_label": "demo_app", 49 | "models": [{"object_name": "Person"}, {"object_name": "Picture"}], 50 | }, 51 | ] 52 | 53 | MIDDLEWARE = [ 54 | "django.middleware.security.SecurityMiddleware", 55 | # Whitenoise for static 56 | "whitenoise.middleware.WhiteNoiseMiddleware", 57 | # Use Whitenoise to serve media to reduce complexity 58 | # User uploads are not permitted 59 | "demo.middleware.WhiteNoiseMediaMiddleware", 60 | "django.contrib.sessions.middleware.SessionMiddleware", 61 | # For internationalization 62 | "django.middleware.locale.LocaleMiddleware", 63 | "django.middleware.common.CommonMiddleware", 64 | "django.middleware.csrf.CsrfViewMiddleware", 65 | "django.contrib.auth.middleware.AuthenticationMiddleware", 66 | "django.contrib.messages.middleware.MessageMiddleware", 67 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 68 | ] 69 | 70 | ROOT_URLCONF = "demo.urls" 71 | 72 | TEMPLATES = [ 73 | { 74 | "BACKEND": "django.template.backends.django.DjangoTemplates", 75 | "DIRS": [BASE_DIR / "templates"], 76 | "APP_DIRS": True, 77 | "OPTIONS": { 78 | "context_processors": [ 79 | "django.template.context_processors.debug", 80 | "django.template.context_processors.request", 81 | "django.contrib.auth.context_processors.auth", 82 | "django.contrib.messages.context_processors.messages", 83 | ], 84 | }, 85 | }, 86 | ] 87 | 88 | WSGI_APPLICATION = "demo.wsgi.application" 89 | 90 | 91 | # Database 92 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 93 | 94 | DATABASES = { 95 | "default": { 96 | "ENGINE": "django.db.backends.sqlite3", 97 | "NAME": BASE_DIR / "db.sqlite3", 98 | } 99 | } 100 | 101 | 102 | # Password validation 103 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | "NAME": ( 108 | "django.contrib.auth." 109 | "password_validation.UserAttributeSimilarityValidator" 110 | ), 111 | }, 112 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 113 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 114 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "UTC" 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 134 | 135 | STATIC_URL = "/static/" 136 | STATIC_ROOT = BASE_DIR / "static" 137 | 138 | # Default primary key field type 139 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 140 | 141 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 142 | -------------------------------------------------------------------------------- /demo/demo/settings/development.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | 3 | # SECURITY WARNING: don't run with debug turned on in production! 4 | DEBUG = True 5 | 6 | INSTALLED_APPS.append("debug_toolbar") # noqa: F405 7 | MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 8 | INTERNAL_IPS = ["127.0.0.1"] 9 | -------------------------------------------------------------------------------- /demo/demo/settings/production.py: -------------------------------------------------------------------------------- 1 | import sentry_sdk 2 | from decouple import config 3 | from sentry_sdk.integrations.django import DjangoIntegration 4 | 5 | from .base import * # noqa 6 | 7 | # SECURITY WARNING: don't run with debug turned on in production! 8 | DEBUG = False 9 | 10 | ALLOWED_HOSTS = ["semantic-admin.com"] 11 | 12 | USE_X_FORWARDED_HOST = True 13 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 14 | 15 | sentry_sdk.init( 16 | dsn=config("SENTRY_DSN"), 17 | integrations=[DjangoIntegration()], 18 | # Less transactions 19 | traces_sample_rate=0.01, 20 | ) 21 | 22 | STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" 23 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | import re 18 | 19 | from django.conf import settings 20 | from django.contrib import admin 21 | from django.contrib.auth.views import LoginView 22 | from django.urls import include, path, re_path 23 | from django.views.generic.base import RedirectView 24 | from django.views.i18n import JavaScriptCatalog 25 | from django.views.static import serve 26 | from semantic_forms.docs.views import semantic_forms_kitchen_sink 27 | 28 | from demo.forms import LoginForm 29 | 30 | urlpatterns = [ 31 | path( 32 | "login/", 33 | LoginView.as_view(authentication_form=LoginForm), 34 | ), 35 | path( 36 | "password_change/", 37 | RedirectView.as_view(url="/"), 38 | name="change-password-redirect", 39 | ), 40 | path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), 41 | re_path( 42 | r"^%s(?P.*)$" % re.escape(settings.MEDIA_URL.lstrip("/")), 43 | serve, 44 | kwargs={"document_root": settings.MEDIA_ROOT}, 45 | ), 46 | path("forms/", semantic_forms_kitchen_sink, name="semantic-forms-kitchen-sink"), 47 | path("", admin.site.urls), 48 | ] 49 | 50 | if settings.DEBUG: 51 | urlpatterns.insert(0, path("__debug__/", include("debug_toolbar.urls"))) 52 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings.production") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/demo_app/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "demo_app.apps.DemoAppConfig" 2 | -------------------------------------------------------------------------------- /demo/demo_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.contrib.admin import ModelAdmin as DefaultModelAdmin 4 | from django.contrib.admin import StackedInline as DefaultStackedInline 5 | from django.contrib.admin import TabularInline as DefaultTabularInline 6 | from django.contrib.auth.models import Group, User 7 | from django.db.models import Count, QuerySet 8 | from django.http import HttpRequest 9 | from django.urls import reverse 10 | from django.utils.html import format_html 11 | from django.utils.safestring import mark_safe 12 | from taggit.models import Tag 13 | 14 | from semantic_admin import ( 15 | SemanticModelAdmin, 16 | SemanticStackedInline, 17 | SemanticTabularInline, 18 | ) 19 | 20 | from .filters import PersonFilter 21 | from .models import Favorite, Person, Picture 22 | 23 | try: 24 | from django.utils.translation import gettext_lazy as _ # Django >= 4 25 | except ImportError: 26 | from django.utils.translation import ugettext_lazy as _ 27 | 28 | 29 | admin.site.unregister(User) 30 | admin.site.unregister(Group) 31 | admin.site.unregister(Tag) 32 | 33 | if "semantic_admin" in settings.INSTALLED_APPS: 34 | ModelAdmin = SemanticModelAdmin 35 | StackedInline = SemanticStackedInline 36 | TabularInline = SemanticTabularInline 37 | else: 38 | ModelAdmin = DefaultModelAdmin 39 | StackedInline = DefaultStackedInline 40 | TabularInline = DefaultTabularInline 41 | 42 | 43 | def html5_picture(obj: Picture, css: str = "") -> str: 44 | """HTML5 picture.""" 45 | name = str(obj) 46 | img = obj.get_img(css=css) 47 | html = f"{img}{name}" 48 | return format_html(mark_safe(html)) 49 | 50 | 51 | class PictureStackedInline(StackedInline): 52 | """Picture stacked inline.""" 53 | 54 | model = Picture 55 | fields = ( 56 | ("date_and_time", "tags"), 57 | "inline_picture", 58 | "is_color", 59 | ) 60 | readonly_fields = ("inline_picture",) 61 | show_change_link = True 62 | extra = 0 63 | 64 | def inline_picture(self, obj: Picture) -> str: 65 | """Inline picture.""" 66 | return html5_picture(obj, css="large rounded") 67 | 68 | inline_picture.short_description = _("picture").capitalize() # type: ignore 69 | 70 | def has_add_permission(self, *args, **kwargs) -> bool: 71 | """Has add permission.""" 72 | return False 73 | 74 | 75 | class PersonFavoriteTabularInline(TabularInline): 76 | """Person favorite tabular inline.""" 77 | 78 | model = Favorite 79 | autocomplete_fields = fields = ("picture",) 80 | extra = 0 81 | 82 | 83 | @admin.register(Person) 84 | class PersonAdmin(ModelAdmin): 85 | """Person admin.""" 86 | 87 | search_fields = ("name",) 88 | filterset_class = PersonFilter 89 | list_display = ("name", "birthday", "list_friends", "list_favorites") 90 | list_editable = ("birthday",) 91 | fieldsets = ( 92 | (None, {"fields": (("name", "birthday"),)}), 93 | (_("extra").capitalize(), {"fields": (("slug", "url", "email"),)}), 94 | (None, {"fields": ("friends",)}), 95 | ) 96 | prepopulated_fields = {"slug": ("name",)} 97 | autocomplete_fields = ("friends",) 98 | list_per_page = 10 99 | actions = ("send_friend_request",) 100 | inlines = (PictureStackedInline, PersonFavoriteTabularInline) 101 | 102 | def list_friends(self, obj: Person) -> str: 103 | """List friends.""" 104 | friends = [] 105 | for friend in obj.friends.all(): 106 | url = reverse("admin:demo_app_person_change", args=(friend.pk,)) 107 | a = f"{friend.name}" 108 | friends.append(a) 109 | html = ", ".join(friends) 110 | return format_html(mark_safe(html)) 111 | 112 | list_friends.short_description = _("friends").capitalize() # type: ignore 113 | 114 | def list_favorites(self, obj: Person) -> str: 115 | """List favorites.""" 116 | favorites = [] 117 | for favorite in obj.favorites.all(): 118 | picture = favorite.picture 119 | name = str(picture) 120 | url = reverse("admin:demo_app_picture_change", args=(picture.pk,)) 121 | img = picture.get_img(css="tiny rounded") 122 | a = f"{img}{name}" 123 | favorites.append(a) 124 | html = "".join(favorites) 125 | return format_html(mark_safe(html)) 126 | 127 | list_favorites.short_description = _("favorites").capitalize() # type: ignore 128 | 129 | def send_friend_request(self, request: HttpRequest, queryset: QuerySet) -> None: 130 | """Send friend request.""" 131 | msg = _("You are now friends with {friends}.") 132 | format_dict = {"friends": ", ".join(obj.name for obj in queryset)} 133 | self.message_user(request, msg.format(**format_dict)) 134 | 135 | def get_queryset(self, request: HttpRequest) -> QuerySet: 136 | """Get queryset.""" 137 | queryset = super().get_queryset(request) 138 | return queryset.prefetch_related("friends", "favorites__picture") 139 | 140 | 141 | class PictureFavoriteTabularInline(TabularInline): 142 | """Picture favorite tabular inline.""" 143 | 144 | model = Favorite 145 | autocomplete_fields = fields = ("person",) 146 | extra = 0 147 | 148 | 149 | @admin.register(Picture) 150 | class PictureAdmin(ModelAdmin): 151 | """Picture admin.""" 152 | 153 | search_fields = ("tags__name",) 154 | list_filter = ("person",) 155 | list_display = ( 156 | "list_picture", 157 | "person", 158 | "date_and_time", 159 | "is_color", 160 | "has_favorites", 161 | ) 162 | list_editable = ( 163 | "person", 164 | "date_and_time", 165 | "is_color", 166 | ) 167 | fields = ( 168 | ("date_and_time", "tags", "is_color"), 169 | "detail_picture", 170 | ) 171 | readonly_fields = ( 172 | "list_picture", 173 | "person_changelink", 174 | "has_favorites", 175 | "detail_picture", 176 | ) 177 | date_hierarchy = "date_and_time" 178 | list_per_page = 10 179 | inlines = (PictureFavoriteTabularInline,) 180 | 181 | def list_picture(self, obj: Picture) -> str: 182 | """List picture.""" 183 | return html5_picture(obj, css="medium rounded") 184 | 185 | list_picture.short_description = _("picture").capitalize() # type: ignore 186 | list_picture.admin_order_field = "date_and_time" # type: ignore 187 | 188 | def detail_picture(self, obj: Picture) -> str: 189 | """Detail picture.""" 190 | return html5_picture(obj, css="large rounded") 191 | 192 | detail_picture.short_description = _("picture").capitalize() # type: ignore 193 | 194 | def person_changelink(self, obj: Picture) -> str: 195 | """Person change link.""" 196 | url = reverse("admin:demo_app_person_change", args=(obj.pk,)) 197 | a = f"{obj.person.name}" 198 | return format_html(mark_safe(a)) 199 | 200 | person_changelink.short_description = _("person").capitalize() # type: ignore 201 | person_changelink.admin_order_field = "person" # type: ignore 202 | 203 | def has_favorites(self, obj: Picture) -> bool: 204 | """Has favorites.""" 205 | return obj.total_favorites > 1 206 | 207 | has_favorites.short_description = _("has favorites").capitalize() # type: ignore 208 | has_favorites.admin_order_field = "total_favorites" 209 | has_favorites.boolean = True # type: ignore 210 | 211 | def has_add_permission(self, *args, **kwargs) -> bool: 212 | """Has add permission.""" 213 | return False 214 | 215 | def get_queryset(self, request: HttpRequest) -> QuerySet: 216 | """Get queryset.""" 217 | queryset = super().get_queryset(request) 218 | queryset = queryset.select_related("person") 219 | queryset = queryset.prefetch_related("tags") 220 | return queryset.annotate(total_favorites=Count("favorites")) 221 | -------------------------------------------------------------------------------- /demo/demo_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | try: 4 | from django.utils.translation import gettext_lazy as _ # Django >= 4 5 | except ImportError: 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | class DemoAppConfig(AppConfig): 10 | """Demo App Config.""" 11 | 12 | name = "demo_app" 13 | verbose_name = _("demo") 14 | -------------------------------------------------------------------------------- /demo/demo_app/factory.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | 3 | from factory import LazyAttribute 4 | from factory.django import DjangoModelFactory 5 | 6 | from .models import Person 7 | 8 | fake = Faker() 9 | 10 | 11 | class PersonFactory(DjangoModelFactory): 12 | name = LazyAttribute(lambda x: fake.first_name()) 13 | url = LazyAttribute(lambda x: fake.safe_domain_name()) 14 | email = LazyAttribute(lambda x: fake.email()) 15 | birthday = LazyAttribute(lambda x: fake.past_date()) 16 | 17 | class Meta: 18 | model = Person 19 | -------------------------------------------------------------------------------- /demo/demo_app/filters.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from django.db.models import QuerySet 4 | from semantic_forms.filters import SemanticFilterSet, SemanticModelMultipleChoiceFilter 5 | 6 | from .models import Person, Picture 7 | 8 | try: 9 | from django.utils.translation import gettext_lazy as _ # Django >= 4 10 | except ImportError: 11 | from django.utils.translation import ugettext_lazy as _ 12 | 13 | 14 | class PersonFilter(SemanticFilterSet): 15 | """Person filter.""" 16 | 17 | favorite_pictures = SemanticModelMultipleChoiceFilter( 18 | label=_("favorite pictures"), 19 | queryset=Picture.objects.exclude(favorites=None), 20 | method="filter_favorite_pictures", 21 | ) 22 | 23 | def filter_favorite_pictures( 24 | self, queryset: QuerySet, name: str, value: Iterable[int] 25 | ) -> QuerySet: 26 | """Filter by favorite pictures.""" 27 | if not value: 28 | return queryset 29 | else: 30 | queryset = queryset.filter(favorites__picture__in=value) 31 | return queryset.distinct() 32 | 33 | class Meta: 34 | model = Person 35 | fields = ("friends", "favorite_pictures") 36 | -------------------------------------------------------------------------------- /demo/demo_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-01-01 08:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import taggit.managers 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Person', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(help_text='Helpful text', max_length=256, verbose_name='name')), 22 | ('slug', models.SlugField(help_text='Helpful text', verbose_name='slug')), 23 | ('url', models.URLField(help_text='Helpful text', verbose_name='url')), 24 | ('email', models.EmailField(help_text='Helpful text', max_length=254, verbose_name='email')), 25 | ('birthday', models.DateField(help_text='Helpful text', verbose_name='birthday')), 26 | ('friends', models.ManyToManyField(blank=True, help_text='Helpful text', to='demo_app.person')), 27 | ], 28 | options={ 29 | 'verbose_name': 'person', 30 | 'verbose_name_plural': 'people', 31 | 'ordering': ('name',), 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='Picture', 36 | fields=[ 37 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('date_and_time', models.DateTimeField(help_text='Helpful text', verbose_name='date and time')), 39 | ('picture', models.ImageField(help_text='Helpful text', upload_to='pictures', verbose_name='picture')), 40 | ('is_color', models.BooleanField(default=True, help_text='Helpful text', verbose_name='color')), 41 | ('person', models.ForeignKey(help_text='Helpful text', on_delete=django.db.models.deletion.CASCADE, related_name='pictures', to='demo_app.person')), 42 | ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), 43 | ], 44 | options={ 45 | 'verbose_name': 'picture', 46 | 'verbose_name_plural': 'pictures', 47 | 'ordering': ('-date_and_time',), 48 | }, 49 | ), 50 | migrations.CreateModel( 51 | name='Favorite', 52 | fields=[ 53 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('person', models.ForeignKey(help_text='Helpful text', on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='demo_app.person')), 55 | ('picture', models.ForeignKey(help_text='Helpful text', on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='demo_app.picture')), 56 | ], 57 | options={ 58 | 'verbose_name': 'favorite', 59 | 'verbose_name_plural': 'favorites', 60 | }, 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /demo/demo_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/demo_app/migrations/__init__.py -------------------------------------------------------------------------------- /demo/demo_app/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.urls import reverse 6 | from django.utils.html import format_html 7 | from taggit.managers import TaggableManager 8 | 9 | try: 10 | from django.utils.translation import gettext_lazy as _ # Django >= 4 11 | except ImportError: 12 | from django.utils.translation import ugettext_lazy as _ 13 | 14 | 15 | class Person(models.Model): 16 | friends = models.ManyToManyField("self", help_text="Helpful text", blank=True) 17 | name = models.CharField(_("name"), max_length=256, help_text="Helpful text") 18 | slug = models.SlugField(_("slug"), help_text="Helpful text") 19 | url = models.URLField(_("url"), help_text="Helpful text") 20 | email = models.EmailField(_("email"), help_text="Helpful text") 21 | birthday = models.DateField(_("birthday"), help_text="Helpful text") 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | class Meta: 27 | ordering = ("name",) 28 | verbose_name = _("person") 29 | verbose_name_plural = _("people") 30 | 31 | 32 | class Picture(models.Model): 33 | person = models.ForeignKey( 34 | "Person", 35 | related_name="pictures", 36 | on_delete=models.CASCADE, 37 | help_text="Helpful text", 38 | ) 39 | date_and_time = models.DateTimeField( 40 | _("date and time"), 41 | help_text="Helpful text", 42 | ) 43 | picture = models.ImageField( 44 | _("picture"), help_text="Helpful text", upload_to="pictures" 45 | ) 46 | is_color = models.BooleanField(_("color"), help_text="Helpful text", default=True) 47 | tags = TaggableManager() 48 | 49 | @property 50 | def admin_url(self): 51 | return reverse("admin:demo_app_picture_change", args=(self.pk,)) 52 | 53 | @property 54 | def url(self): 55 | if self.picture and os.path.exists(self.picture.path): 56 | filename = self.picture.name 57 | self.picture.close() 58 | return settings.MEDIA_URL + filename 59 | 60 | def get_img(self, css="", style=""): 61 | return f'' 62 | 63 | @property 64 | def semantic_autocomplete(self): 65 | name = str(self) 66 | img = self.get_img(css="small rounded right spaced") 67 | html = f"

{img}{name}

" 68 | return format_html(html) 69 | 70 | def __str__(self): 71 | return ", ".join((tag.name for tag in self.tags.all())) if self.pk else "" 72 | 73 | class Meta: 74 | ordering = ("-date_and_time",) 75 | verbose_name = _("picture") 76 | verbose_name_plural = _("pictures") 77 | 78 | 79 | class Favorite(models.Model): 80 | person = models.ForeignKey( 81 | "Person", 82 | related_name="favorites", 83 | on_delete=models.CASCADE, 84 | help_text="Helpful text", 85 | ) 86 | picture = models.ForeignKey( 87 | "Picture", 88 | related_name="favorites", 89 | on_delete=models.CASCADE, 90 | help_text="Helpful text", 91 | ) 92 | 93 | def __str__(self): 94 | return format_html('') 95 | 96 | class Meta: 97 | verbose_name = _("favorite") 98 | verbose_name_plural = _("favorites") 99 | -------------------------------------------------------------------------------- /demo/demo_app/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/demo/demo_app/tests.py -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings.development") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /demo/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | from decouple import config 7 | from invoke import task 8 | 9 | 10 | @task 11 | def django_settings(ctx: Any) -> Any: 12 | """Get django settings.""" 13 | os.environ["DJANGO_SETTINGS_MODULE"] = "demo.settings.development" 14 | import django 15 | 16 | django.setup() 17 | from django.conf import settings 18 | 19 | return settings 20 | 21 | 22 | @task 23 | def build(ctx: Any) -> None: 24 | """Build the project.""" 25 | delete_database(ctx) 26 | delete_media(ctx) 27 | delete_migrations(ctx) 28 | create_database(ctx) 29 | create_user(ctx) 30 | populate_database(ctx) 31 | 32 | 33 | @task 34 | def create_database(ctx: Any) -> None: 35 | """Create the database.""" 36 | ctx.run("python manage.py makemigrations") 37 | ctx.run("python manage.py migrate") 38 | 39 | 40 | @task 41 | def create_user(ctx: Any) -> None: 42 | """Create a superuser.""" 43 | from django.contrib.auth import get_user_model 44 | 45 | User = get_user_model() 46 | user = User.objects.create(username="admin", is_superuser=True, is_staff=True) 47 | user.set_password("semantic") 48 | user.save() 49 | 50 | 51 | @task 52 | def populate_database(ctx: Any) -> None: 53 | """Populate the database.""" 54 | django_settings(ctx) 55 | 56 | import datetime 57 | import os 58 | import random 59 | 60 | from demo_app.factory import PersonFactory 61 | from demo_app.models import Favorite, Picture 62 | from django.conf import settings 63 | from django.core.files import File 64 | from django.utils.text import slugify 65 | from faker import Faker 66 | 67 | fake = Faker() 68 | 69 | COFFEE_DIR = settings.BASE_DIR / "coffee" 70 | 71 | coffees = [c for c in os.listdir(COFFEE_DIR) if Path(c).suffix == ".jpeg"] 72 | 73 | people = [] 74 | for index in range(int(len(coffees) / 2)): 75 | first_name = fake.first_name() 76 | last_name = fake.first_name() 77 | name = f"{first_name} {last_name}" 78 | slug = slugify(name) 79 | domain = fake.safe_domain_name() 80 | dotted_name = slug.replace("-", ".") 81 | email = f"{dotted_name}@{domain}" 82 | person = PersonFactory(name=name, slug=slug, url=domain, email=email) 83 | people.append(person) 84 | 85 | for person in people: 86 | total_friends = int(random.random() * 3) 87 | can_be_friends = [p for p in people if person != p] 88 | friends = random.sample(can_be_friends, total_friends) 89 | person.friends.add(*friends) 90 | 91 | pictures = [] 92 | random.shuffle(coffees) 93 | coffee_people = people + random.choices(people, k=len(coffees) - len(people)) 94 | for coffee, coffee_person in zip(coffees, coffee_people, strict=True): 95 | date_and_time = fake.past_datetime().replace(tzinfo=datetime.timezone.utc) 96 | picture = Picture(person=coffee_person, date_and_time=date_and_time) 97 | path = COFFEE_DIR / coffee 98 | with open(path, "rb") as f: 99 | picture.picture.save(coffee, File(f)) 100 | tags = fake.bs().split(" ") 101 | picture.tags.add(*tags) 102 | pictures.append(picture) 103 | 104 | for person in people: 105 | total_favorites = 1 + int(random.random() * 5) 106 | for index in range(total_favorites): 107 | picture = random.choice(pictures) 108 | Favorite.objects.get_or_create(person=person, picture=picture) 109 | 110 | 111 | @task 112 | def delete_database(ctx: any) -> None: 113 | """Delete the database.""" 114 | django_settings(ctx) 115 | 116 | from django.conf import settings 117 | 118 | db = settings.BASE_DIR / "db.sqlite3" 119 | if db.exists(): 120 | ctx.run(f"rm {db}") 121 | 122 | 123 | @task 124 | def delete_media(ctx: Any) -> None: 125 | """Delete media.""" 126 | django_settings(ctx) 127 | 128 | from django.conf import settings 129 | 130 | if settings.MEDIA_ROOT.exists(): 131 | ctx.run(f"rm -r {settings.MEDIA_ROOT}") 132 | 133 | 134 | @task 135 | def delete_migrations(ctx: Any) -> None: 136 | """Delete migrations.""" 137 | import os 138 | 139 | from django.conf import settings 140 | 141 | MIGRATIONS_DIR = settings.BASE_DIR / "demo_app/migrations/" 142 | 143 | migrations = [ 144 | MIGRATIONS_DIR / migration 145 | for migration in os.listdir(MIGRATIONS_DIR) 146 | if Path(migration).stem != "__init__" and Path(migration).suffix == ".py" 147 | ] 148 | 149 | for migration in migrations: 150 | ctx.run(f"rm {migration}") 151 | 152 | 153 | @task 154 | def get_container_name(ctx: Any, region: str = "asia-northeast1") -> str: 155 | """Get container name.""" 156 | project_id = ctx.run("gcloud config get-value project").stdout.strip() 157 | name = "django-semantic-admin" 158 | return f"{region}-docker.pkg.dev/{project_id}/{name}/{name}" 159 | 160 | 161 | def docker_secrets() -> str: 162 | """Get docker secrets.""" 163 | build_args = [ 164 | f'{secret}="{config(secret)}"' for secret in ("SECRET_KEY", "SENTRY_DSN") 165 | ] 166 | return " ".join([f"--build-arg {build_arg}" for build_arg in build_args]) 167 | 168 | 169 | def build_semantic_admin(ctx: Any) -> str: 170 | """Build semantic admin.""" 171 | result = ctx.run("poetry build").stdout 172 | return re.search(r"django_semantic_admin-.*\.whl", result).group() 173 | 174 | 175 | @task 176 | def build_container(ctx: Any, region: str = "asia-northeast1") -> None: 177 | """Build container.""" 178 | wheel = build_semantic_admin(ctx) 179 | ctx.run("echo yes | python manage.py collectstatic") 180 | name = get_container_name(ctx, region=region) 181 | # Requirements 182 | requirements = [ 183 | "django-filter", 184 | "django-taggit", 185 | "gunicorn", 186 | "pillow", 187 | "python-decouple", 188 | "whitenoise", 189 | ] 190 | # Versions 191 | reqs = " ".join( 192 | [ 193 | req.split(";")[0] 194 | for req in ctx.run("poetry export --dev --without-hashes").stdout.split( 195 | "\n" 196 | ) 197 | if req.split("==")[0] in requirements 198 | ] 199 | ) 200 | # Build 201 | build_args = {"WHEEL": wheel, "POETRY_EXPORT": reqs} 202 | build_args = " ".join( 203 | [f'--build-arg {key}="{value}"' for key, value in build_args.items()] 204 | ) 205 | with ctx.cd(".."): 206 | cmd = " ".join( 207 | [ 208 | "docker build", 209 | build_args, 210 | docker_secrets(), 211 | f"--no-cache --file=Dockerfile --tag={name} .", 212 | ] 213 | ) 214 | ctx.run(cmd) 215 | 216 | 217 | @task 218 | def push_container(ctx: Any, region: str = "asia-northeast1") -> None: 219 | """Push container.""" 220 | name = get_container_name(ctx, region=region) 221 | # Push 222 | cmd = f"docker push {name}" 223 | ctx.run(cmd) 224 | 225 | 226 | @task 227 | def deploy_container( 228 | ctx: Any, service: str = "django-semantic-admin", region: str = "asia-northeast1" 229 | ) -> None: 230 | """Deploy container.""" 231 | build_container(ctx, region=region) 232 | push_container(ctx, region=region) 233 | image = get_container_name(ctx, region=region) 234 | ctx.run(f"gcloud run deploy {service} --image={image} --region={region}") 235 | -------------------------------------------------------------------------------- /demo/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/index.html" %} 2 | 3 | {% block breadcrumbsbox %} 4 |
5 | 10 | 14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /demo/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | {% load i18n static %} 3 | 4 | {% block passwordreset %} 5 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /docs/awesome.md: -------------------------------------------------------------------------------- 1 | Awesome optional features 2 | ------------------------- 3 | 4 |
  1. Optional integration with django_filter:
5 | 6 | django-filter 7 | 8 | To enable this awesome feature, add `filterset_class` to your Django admin: 9 | 10 | ```python 11 | from semantic_forms.filters import SemanticFilterSet 12 | 13 | class DemoFilter(SemanticFilterSet): 14 | class Meta: 15 | model = Demo 16 | fields = ("demo_field",) 17 | 18 | class DemoAdmin(SemanticModelAdmin): 19 | filterset_class = DemoFilter 20 | ``` 21 | 22 |
  1. HTML preview in Django `autocomplete_fields`:
23 | 24 | html5-autocomplete 25 | 26 | To enable this awesome feature, add the `semantic_autocomplete` property to your Django model: 27 | 28 | ```python 29 | class DemoModel(models.Model): 30 | @property 31 | def semantic_autocomplete(self): 32 | html = self.get_img() 33 | return format_html(html) 34 | ``` 35 | 36 |
  1. Optional integration with django-import-export:
37 | 38 | django-import-export 39 | 40 | To enable this awesome feature, instead of `ImportExportModelAdmin`, etc: 41 | 42 | ```python 43 | from import_export.admin import ImportExportModelAdmin 44 | 45 | class ExampleImportExportAdmin(ImportExportModelAdmin): 46 | pass 47 | ``` 48 | 49 | Inherit from their `Semantic` equivalents: 50 | 51 | ```python 52 | from semantic_admin.contrib.import_export.admin import SemanticImportExportModelAdmin 53 | 54 | class ExampleImportExportAdmin(SemanticImportExportModelAdmin): 55 | pass 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/changes.md: -------------------------------------------------------------------------------- 1 | v0.6.0 2 | ------ 3 | * Require Python >= 3.10 4 | * Replace deprecated length_is filter with length for Django 5.1 5 | 6 | v0.5.0 7 | ------ 8 | * Forms were moved to [django-semantic-forms](https://github.com/globophobe/django-semantic-forms) so that they may be used outside of the admin. 9 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | Install dependencies with `poetry install`. The demo is built with [invoke tasks](https://github.com/globophobe/django-semantic-admin/blob/master/demo/tasks.py). For example, `invoke build`. 5 | 6 | -------------------------------------------------------------------------------- /docs/customization.md: -------------------------------------------------------------------------------- 1 | 2 | App list ordering 3 | ----- 4 | 5 | Ordering of sidebar, and index page, apps and models can be customized with the optional `SEMANTIC_APP_LIST` setting. 6 | 7 | With the following in settings.py, `app_2` will be displayed before `app_1` 8 | 9 | ```python 10 | SEMANTIC_APP_LIST = [{ "app_label": "app_2" }, { "app_label": "app_1" }] 11 | ``` 12 | 13 | 14 | In this example, `ModelB` will be displayed before `ModelA` 15 | 16 | ```python 17 | SEMANTIC_APP_LIST = [ 18 | { 19 | "app_label": "app_1", 20 | "models": [{"object_name": "ModelB"}, {"object_name": "ModelA"}], 21 | }, 22 | ] 23 | ``` 24 | 25 | Changing the logo 26 | ----- 27 | 28 | The logo may be changed by overriding `menu.html` 29 | 30 | pony-powered 31 | 32 |
  1. Add a dir to the TEMPLATES setting
33 | 34 | ```python 35 | "DIRS": [BASE_DIR / "templates"], 36 | ``` 37 | 38 |
  1. Create a file templates/admin/menu.html with the following
39 | 40 | ```html 41 | {% extends 'admin/menu.html' %} 42 | {% block branding %} 43 | 44 | {{ site_header|default:_('Django administration') }} 48 | 49 | {% endblock %} 50 | ``` 51 | 52 | Customizing the CSS 53 | ----- 54 | 55 | CSS may be customized by overriding `base.html`. 56 | 57 |
  1. Add a dir to the TEMPLATES setting
58 | 59 | ```python 60 | "DIRS": [BASE_DIR / "templates"], 61 | ``` 62 | 63 |
  1. Create a file templates/admin/base.html with the following
64 | 65 | ```html 66 | {% extends 'admin/base.html' %} 67 | {% load static %} 68 | {% block extrastyle %} 69 | 70 | {% endblock %} 71 | ``` 72 | 73 | Translating the calendar 74 | ----- 75 | 76 | `SemanticModelAdmin`, `SemanticStackedInline`, and `SemanticTabularInline` admin classes for models with `DateTimeField`, `DateField`, or `TimeField` will automatically use Semantic UI's calendar component. 77 | 78 | To translate the calendar add Django's `JavaScriptCatalog` to `urlpatterns`, as described in Django's [Translation documentation](https://docs.djangoproject.com/en/4.0/topics/i18n/translation/#module-django.views.i18n). 79 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Django Semantic UI admin theme 2 | ------------------------------ 3 | A completely free (MIT) [Semantic UI](https://semantic-ui.com/) admin theme for Django. Actually, this is my 3rd admin theme for Django. The first was forgettable, and the second was with [Pure CSS](https://purecss.io/). Pure CSS was great, but lacked JavaScript components. 4 | 5 | Semantic UI looks professional, and has great JavaScript components. 6 | 7 | Log in to the demo with username `admin` and password `semantic`: [https://semantic-admin.com](https://semantic-admin.com) 8 | 9 | Why? 10 | ---- 11 | * Looks professional, with a nice sidebar. 12 | * Responsive design, even [tables can stack](https://semantic-ui.com/collections/table.html#stacking) responsively on mobile. 13 | * JavaScript datepicker and timepicker components. 14 | * JavaScript selects, including multiple selections, which integrate well with Django autocomplete fields. 15 | * Semantic UI has libraries for [React](https://react.semantic-ui.com/) and [Vue](https://semantic-ui-vue.github.io/#/), in addition to jQuery. This means this package can be used to style the admin, and custom views can be added with React or Vue components with the same style. 16 | 17 | django-semantic-admin 18 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | Install 2 | ------- 3 | 4 | Install from PyPI: 5 | 6 | ``` 7 | pip install django-semantic-admin 8 | ``` 9 | 10 | Add to `settings.py` before `django.contrib.admin`: 11 | 12 | ```python 13 | INSTALLED_APPS = [ 14 | "semantic_admin", 15 | "django.contrib.admin", 16 | ... 17 | ] 18 | ``` 19 | 20 | Please remember to run `python manage.py collectstatic` for production deployments. 21 | 22 | Usage 23 | ----- 24 | 25 | Instead of `admin.ModelAdmin`, `admin.StackedInline`, or `admin.TabularInline`: 26 | 27 | ```python 28 | class ExampleStackedInline(admin.StackedInline): 29 | pass 30 | 31 | class ExampleTabularInline(admin.TabularInline): 32 | pass 33 | 34 | class ExampleAdmin(admin.ModelAdmin): 35 | inlines = (ExampleStackedInline, ExampleTabularInline) 36 | ``` 37 | 38 | Inherit from their `Semantic` equivalents: 39 | 40 | ```python 41 | from semantic_admin import SemanticModelAdmin, SemanticStackedInline, SemanticTabularInline 42 | 43 | class ExampleStackedInline(SemanticStackedInline): 44 | pass 45 | 46 | class ExampleTabularInline(SemanticTabularInline): 47 | pass 48 | 49 | class ExampleAdmin(SemanticModelAdmin): 50 | inlines = (ExampleStackedInline, ExampleTabularInline) 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /docs/notes.md: -------------------------------------------------------------------------------- 1 | Notes 2 | ----- 3 | Please note, this package uses [Fomantic UI](https://fomantic-ui.com/) the official community fork of Semantic UI. 4 | -------------------------------------------------------------------------------- /docs/screenshots.md: -------------------------------------------------------------------------------- 1 | Change list 2 | ----------- 3 | 4 |
  1. Desktop
5 | 6 | change-list-desktop 7 | 8 |
  1. Tablet
9 | 10 | change-list-tablet 11 | 12 |
  1. Mobile
13 | 14 | change-list-mobile 15 | 16 | 17 | Change form 18 | ----------- 19 | 20 |
  1. Desktop
21 | 22 | change-form-desktop 23 | 24 |
  1. Tablet
25 | 26 | change-form-tablet 27 | 28 |
  1. Mobile
29 | 30 | change-form-mobile 31 | -------------------------------------------------------------------------------- /docs/screenshots/change-form-collapse-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/change-form-collapse-menu.png -------------------------------------------------------------------------------- /docs/screenshots/change-form-stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/change-form-stacked.png -------------------------------------------------------------------------------- /docs/screenshots/change-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/change-form.png -------------------------------------------------------------------------------- /docs/screenshots/change-list-collapse-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/change-list-collapse-menu.png -------------------------------------------------------------------------------- /docs/screenshots/change-list-stacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/change-list-stacked.png -------------------------------------------------------------------------------- /docs/screenshots/change-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/change-list.png -------------------------------------------------------------------------------- /docs/screenshots/django-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/django-filter.png -------------------------------------------------------------------------------- /docs/screenshots/django-import-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/django-import-export.png -------------------------------------------------------------------------------- /docs/screenshots/html5-autocomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/html5-autocomplete.png -------------------------------------------------------------------------------- /docs/screenshots/pony-powered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/docs/screenshots/pony-powered.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | theme: readthedocs 2 | site_name: Django Semantic UI admin theme 3 | nav: 4 | - Home: index.md 5 | - Install: install.md 6 | - Customization: customization.md 7 | - Awesome optional features: awesome.md 8 | - Screenshots: screenshots.md 9 | - Contributing: contributing.md 10 | - Notes: notes.md 11 | - Changes: changes.md 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-semantic-admin" 3 | version = "0.6.7" 4 | description = "Django Semantic UI Admin theme" 5 | authors = ["Alex "] 6 | readme = "README.md" 7 | license = "MIT" 8 | homepage = "https://github.com/globophobe/django-semantic-admin" 9 | repository = "https://github.com/globophobe/django-semantic-admin" 10 | keywords = ["django", "admin", "theme"] 11 | classifiers = ["Framework :: Django", "Development Status :: 4 - Beta", "Operating System :: OS Independent"] 12 | packages = [{ include = "semantic_admin" }] 13 | 14 | [tool.poetry.dependencies] 15 | python = ">=3.10" 16 | django = ">=3.2" 17 | django-semantic-forms = "*" 18 | django-import-export = { version = "<=3.3.9", optional = true } 19 | 20 | [tool.poetry.extras] 21 | import-export = ["django-import-export"] 22 | 23 | [tool.poetry.dev-dependencies] 24 | black = "*" 25 | django-debug-toolbar = "*" 26 | django-import-export = "<=3.3.9" 27 | django-filter = "*" 28 | django-taggit = "*" 29 | factory-boy = "*" 30 | Faker = "*" 31 | gunicorn = "*" 32 | invoke = "*" 33 | mkdocs = "*" 34 | Pillow = "*" 35 | python-decouple = "*" 36 | ruff = "*" 37 | whitenoise = "*" 38 | 39 | [tool.ruff] 40 | exclude = ["migrations"] 41 | line-length = 88 42 | target-version = "py310" 43 | lint.ignore = [ 44 | "ANN002", # Missing type annotation for *{name} 45 | "ANN003", # Missing type annotation for **{name} 46 | "ANN101", # Missing type annotation for {name} in method 47 | "ANN102", # Missing type annotation for {name} in classmethod 48 | "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} 49 | "B007", # Loop control variable not used within loop body 50 | "B904", # Within an except clause, raise exceptions with... 51 | "D100", # Missing docstring in public module 52 | "D104", # Missing docstring in public package 53 | "D106", # Missing docstring in public nested class, i.e. Meta 54 | "D203", # 1 blank line required before class docstring 55 | "D205", # 1 blank line required between summary line and description 56 | "D212", # Multi-line docstring summary should start at the first line 57 | "D213", # Multi-line docstring summary should start at the second line 58 | "D400", # First line should end with a period 59 | "D401", # First line should be in imperative mood 60 | "D403", # First word of the first line should be properly capitalized 61 | "D404", # First word of the docstring should not be This 62 | "D406", # Section name should end with a newline 63 | "D407", # Missing dashed underline after section 64 | "D411", # Missing blank line before section 65 | "D413", # Missing blank line after last section 66 | "D415", # First line should end with a period, question mark, or exclamation point 67 | "D417", # Missing argument description in the docstring: {name} 68 | "E501", # Line too long 69 | "E721", # Do not compare types, use `isinstance()` 70 | "N803", # Argument name should be lowercase 71 | "N806", # Variable in function should be lowercase 72 | "PTH123", # open() should be replaced by Path.open() 73 | "UP032", # [*] Use f-string instead of `format` call 74 | ] 75 | lint.select = [ 76 | "ANN", # flake8-annotations 77 | "B", # flake8-bugbear 78 | "D", # pydocstyle 79 | "E", # pycodestyle 80 | "F", # Pyflakes 81 | "I", # isort 82 | "INT", # flake8-gettext 83 | "N", # pep8-naming 84 | "PTH", # flake8-use-pathlib 85 | "UP", # pyupgrade 86 | "RUF100", # unused-noqa 87 | "RUF200", # invalid-pyproject-toml 88 | "W", # pycodestyle 89 | ] 90 | lint.unfixable = ["F401", "F841"] 91 | 92 | [tool.ruff.lint.per-file-ignores] 93 | "**/tests/*" = ["ANN", "D"] 94 | "**/migrations/*" = ["ANN", "D"] 95 | 96 | [tool.ruff.lint.isort] 97 | known-first-party = ["semantic_admin"] 98 | 99 | [build-system] 100 | requires = ["poetry-core>=1.0.0"] 101 | build-backend = "poetry.core.masonry.api" 102 | -------------------------------------------------------------------------------- /semantic_admin/__init__.py: -------------------------------------------------------------------------------- 1 | from .admin import ( 2 | SemanticModelAdmin, 3 | SemanticStackedInline, 4 | SemanticTabularInline, 5 | ) 6 | 7 | __all__ = [ 8 | 'SemanticModelAdmin', 9 | 'SemanticStackedInline', 10 | 'SemanticTabularInline', 11 | ] 12 | -------------------------------------------------------------------------------- /semantic_admin/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/semantic_admin/contrib/__init__.py -------------------------------------------------------------------------------- /semantic_admin/contrib/import_export/__init__.py: -------------------------------------------------------------------------------- 1 | from .admin import ( 2 | SemanticExportActionModelAdmin, 3 | SemanticExportMixin, 4 | SemanticImportExportActionModelAdmin, 5 | SemanticImportExportMixin, 6 | SemanticImportExportModelAdmin, 7 | SemanticImportMixin, 8 | ) 9 | 10 | __all__ = [ 11 | "SemanticExportActionModelAdmin", 12 | "SemanticExportMixin", 13 | "SemanticImportExportActionModelAdmin", 14 | "SemanticImportExportMixin", 15 | "SemanticImportExportModelAdmin", 16 | "SemanticImportMixin", 17 | ] 18 | -------------------------------------------------------------------------------- /semantic_admin/contrib/import_export/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from import_export.admin import ExportActionMixin, ExportMixin, ImportMixin 3 | 4 | from semantic_admin import SemanticModelAdmin 5 | 6 | from .forms import ( 7 | SemanticExportForm, 8 | SemanticImportForm, 9 | semantic_export_action_form_factory, 10 | ) 11 | 12 | 13 | class SemanticImportMixin(ImportMixin): 14 | """Semantic import mixin.""" 15 | 16 | import_form_class = SemanticImportForm 17 | 18 | 19 | class SemanticExportMixin(ExportMixin): 20 | """Semantic export mixin.""" 21 | 22 | export_form_class = SemanticExportForm 23 | 24 | 25 | class SemanticExportActionMixin(ExportActionMixin): 26 | """Semantic export action mixin.""" 27 | 28 | export_form_class = SemanticExportForm 29 | 30 | def __init__(self, *args, **kwargs) -> None: 31 | """ 32 | Adds a custom action form initialized with the available export 33 | formats. 34 | """ 35 | super().__init__(*args, **kwargs) 36 | choices = [] 37 | formats = self.get_export_formats() 38 | if formats: 39 | for i, f in enumerate(formats): 40 | choices.append((str(i), f().get_title())) 41 | 42 | if len(formats) > 1: 43 | choices.insert(0, ("", "---")) 44 | # region CUSTOM 45 | self.action_form = semantic_export_action_form_factory(choices) 46 | # endregion 47 | 48 | @property 49 | def media(self) -> forms.Media: 50 | """Form media.""" 51 | return super().media 52 | 53 | 54 | class SemanticExportActionMixin(SemanticExportActionMixin): 55 | """Semantic export action mixin.""" 56 | 57 | 58 | class SemanticImportExportMixin(SemanticExportMixin, SemanticImportMixin): 59 | """Semantic import export mixin.""" 60 | 61 | #: template for change_list view 62 | import_export_change_list_template = ( 63 | "admin/import_export/change_list_import_export.html" 64 | ) 65 | 66 | 67 | class SemanticExportActionMixin(SemanticExportActionMixin, SemanticModelAdmin): 68 | """Semantic export action mixin.""" 69 | 70 | 71 | class SemanticImportExportModelAdmin(SemanticImportExportMixin, SemanticModelAdmin): 72 | """Semantic import export model admin.""" 73 | 74 | 75 | class SemanticExportActionModelAdmin(SemanticExportActionMixin, SemanticModelAdmin): 76 | """Semantic export action model admin.""" 77 | 78 | 79 | class SemanticImportExportActionModelAdmin( 80 | SemanticImportMixin, SemanticExportActionModelAdmin 81 | ): 82 | """Semantic import export action model admin.""" 83 | -------------------------------------------------------------------------------- /semantic_admin/contrib/import_export/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from import_export.forms import ExportForm, ImportForm 3 | from semantic_forms import SemanticChoiceField 4 | 5 | from semantic_admin.helpers import SemanticActionForm 6 | 7 | from .widgets import SemanticExportActionSelect 8 | 9 | try: 10 | from django.utils.translation import ugettext_lazy as _ 11 | except ImportError: 12 | from django.utils.translation import gettext_lazy as _ 13 | 14 | 15 | def semantic_export_action_form_factory(formats: list) -> SemanticActionForm: 16 | """ 17 | Returns an ActionForm subclass containing a ChoiceField populated with 18 | the given formats. 19 | """ 20 | 21 | class _ExportActionForm(SemanticActionForm): 22 | """Action form with export format ChoiceField.""" 23 | 24 | file_format = SemanticChoiceField( 25 | label=_("Format"), 26 | widget=SemanticExportActionSelect(), 27 | choices=formats, 28 | required=False, 29 | ) 30 | 31 | _ExportActionForm.__name__ = "ExportActionForm" 32 | 33 | return _ExportActionForm 34 | 35 | 36 | class SemanticImportForm(ImportForm): 37 | """Semantic import form.""" 38 | 39 | input_format = SemanticChoiceField(label=_("Format"), choices=()) 40 | 41 | @property 42 | def media(self) -> forms.Media: 43 | """Media.""" 44 | media = self.fields["input_format"].widget.media 45 | return media + forms.Media(js=["import_export/guess_format.js"]) 46 | 47 | 48 | class SemanticExportForm(ExportForm): 49 | """Semantic export form.""" 50 | 51 | file_format = SemanticChoiceField(label=_("Format"), choices=()) 52 | -------------------------------------------------------------------------------- /semantic_admin/contrib/import_export/widgets.py: -------------------------------------------------------------------------------- 1 | from semantic_forms.widgets import SemanticSelect 2 | 3 | 4 | class SemanticExportActionSelect(SemanticSelect): 5 | """Semantic export action select.""" 6 | 7 | template_name = "admin/import_export/export_action_select.html" 8 | -------------------------------------------------------------------------------- /semantic_admin/helpers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.helpers import ActionForm 2 | from semantic_forms.fields import SemanticChoiceField 3 | 4 | try: 5 | from django.utils.translation import gettext_lazy as _ # Django >= 4 6 | except ImportError: 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | 10 | class SemanticActionForm(ActionForm): 11 | """Semantic action form.""" 12 | 13 | action = SemanticChoiceField(label=_("Action:")) 14 | -------------------------------------------------------------------------------- /semantic_admin/static/admin/js/actions.js: -------------------------------------------------------------------------------- 1 | // Don't use Django jQuery, because we need Semantic UI customizations. 2 | 3 | /*global gettext, interpolate, ngettext*/ 4 | 'use strict'; 5 | { 6 | function show(selector) { 7 | document.querySelectorAll(selector).forEach(function(el) { 8 | el.classList.remove('hidden'); 9 | }); 10 | } 11 | 12 | function hide(selector) { 13 | document.querySelectorAll(selector).forEach(function(el) { 14 | el.classList.add('hidden'); 15 | }); 16 | } 17 | 18 | function showQuestion(options) { 19 | hide(options.acrossClears); 20 | show(options.acrossQuestions); 21 | hide(options.allContainer); 22 | } 23 | 24 | function showClear(options) { 25 | show(options.acrossClears); 26 | hide(options.acrossQuestions); 27 | document.querySelector(options.actionContainer).classList.remove(options.selectedClass); 28 | show(options.allContainer); 29 | hide(options.counterContainer); 30 | } 31 | 32 | function reset(options) { 33 | hide(options.acrossClears); 34 | hide(options.acrossQuestions); 35 | hide(options.allContainer); 36 | show(options.counterContainer); 37 | } 38 | 39 | function clearAcross(options) { 40 | reset(options); 41 | const acrossInputs = document.querySelectorAll(options.acrossInput); 42 | acrossInputs.forEach(function(acrossInput) { 43 | acrossInput.value = 0; 44 | }); 45 | document.querySelector(options.actionContainer).classList.remove(options.selectedClass); 46 | } 47 | 48 | function checker(actionCheckboxes, options, checked) { 49 | if (checked) { 50 | showQuestion(options); 51 | } else { 52 | reset(options); 53 | } 54 | // BEGIN CUSTOMIZATION // 55 | // actionCheckboxes.forEach(function(el) { 56 | // el.checked = checked; 57 | // el.closest('tr').classList.toggle(options.selectedClass, checked); 58 | // }); 59 | const update = checked ? "check" : "uncheck"; 60 | actionCheckboxes.forEach(function(actionCheckbox) { 61 | $(actionCheckbox).checkbox(update); 62 | }) 63 | // END CUSTOMIZATION // 64 | } 65 | 66 | function updateCounter(actionCheckboxes, options) { 67 | // BEGIN CUSTOMIZATION // 68 | // const sel = Array.from(actionCheckboxes).filter(function(el) { 69 | // return el.checked; 70 | // }).length; 71 | const sel = Array.from(actionCheckboxes).filter(function(el) { 72 | return $(el).checkbox('is checked'); 73 | }).length; 74 | // END CUSTOMIZATION // 75 | 76 | const counter = document.querySelector(options.counterContainer); 77 | // data-actions-icnt is defined in the generated HTML 78 | // and contains the total amount of objects in the queryset 79 | const actions_icnt = Number(counter.dataset.actionsIcnt); 80 | counter.textContent = interpolate( 81 | ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { 82 | sel: sel, 83 | cnt: actions_icnt 84 | }, true); 85 | const allToggle = document.getElementById(options.allToggleId); 86 | 87 | // BEGIN CUSTOMIZATION // 88 | // allToggle.checked = sel === actionCheckboxes.length; 89 | const allChecked = sel === actionCheckboxes.length; 90 | const noneChecked = sel === 0; 91 | 92 | if (allChecked) { 93 | $(allToggle).checkbox("check"); 94 | } else if (noneChecked) { 95 | $(allToggle).checkbox("uncheck"); 96 | } 97 | // END CUSTOMIZATION // 98 | 99 | if (allChecked) { 100 | showQuestion(options); 101 | } else { 102 | clearAcross(options); 103 | } 104 | } 105 | 106 | const defaults = { 107 | actionContainer: "div.actions", 108 | counterContainer: "span.action-counter", 109 | allContainer: "div.actions span.all", 110 | acrossInput: "div.actions input.select-across", 111 | acrossQuestions: "div.actions span.question", 112 | acrossClears: "div.actions span.clear", 113 | allToggleId: "action-toggle", 114 | // BEGIN CUSTOMIZATION // 115 | allToggleInputId: "action-toggle-input", 116 | // END CUSTOMIZATION // 117 | selectedClass: "selected" 118 | }; 119 | 120 | window.Actions = function(actionCheckboxes, options) { 121 | options = Object.assign({}, defaults, options); 122 | let list_editable_changed = false; 123 | let lastChecked = null; 124 | let shiftPressed = false; 125 | 126 | document.addEventListener('keydown', (event) => { 127 | shiftPressed = event.shiftKey; 128 | }); 129 | 130 | document.addEventListener('keyup', (event) => { 131 | shiftPressed = event.shiftKey; 132 | }); 133 | 134 | // BEGIN CUSTOMIZATION // 135 | // document.getElementById(options.allToggleId).addEventListener('click', function(event) { 136 | // checker(actionCheckboxes, options, this.checked); 137 | // updateCounter(actionCheckboxes, options); 138 | // }); 139 | document.getElementById(options.allToggleInputId).addEventListener('change', 140 | (event) => { 141 | checker(actionCheckboxes, options, event.target.checked); 142 | updateCounter(actionCheckboxes, options); 143 | }); 144 | // END CUSTOMIZATION // 145 | 146 | document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) { 147 | el.addEventListener('click', function(event) { 148 | event.preventDefault(); 149 | const acrossInputs = document.querySelectorAll(options.acrossInput); 150 | acrossInputs.forEach(function(acrossInput) { 151 | acrossInput.value = 1; 152 | }); 153 | showClear(options); 154 | }); 155 | }); 156 | 157 | document.querySelectorAll(options.acrossClears + " a").forEach(function(el) { 158 | el.addEventListener('click', function(event) { 159 | event.preventDefault(); 160 | 161 | // BEGIN CUSTOMIZATION // 162 | // document.getElementById(options.allToggleId).checked = false; 163 | $(document.getElementById(options.allToggleId)).checkbox('uncheck') 164 | // END CUSTOMIZATION // 165 | 166 | clearAcross(options); 167 | checker(actionCheckboxes, options, false); 168 | updateCounter(actionCheckboxes, options); 169 | }); 170 | }); 171 | 172 | function affectedCheckboxes(target, withModifier) { 173 | const multiSelect = (lastChecked && withModifier && lastChecked !== target); 174 | if (!multiSelect) { 175 | return [target]; 176 | } 177 | const checkboxes = Array.from(actionCheckboxes); 178 | const targetIndex = checkboxes.findIndex(el => el === target); 179 | const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked); 180 | const startIndex = Math.min(targetIndex, lastCheckedIndex); 181 | const endIndex = Math.max(targetIndex, lastCheckedIndex); 182 | const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex)); 183 | return filtered; 184 | }; 185 | 186 | Array.from(document.getElementById('result_list').tBodies).forEach(function(el) { 187 | el.addEventListener('change', function(event) { 188 | const target = event.target; 189 | if (target.classList.contains('action-select')) { 190 | const checkboxes = affectedCheckboxes(target, shiftPressed); 191 | checker(checkboxes, options, target.checked); 192 | updateCounter(actionCheckboxes, options); 193 | lastChecked = target; 194 | } else { 195 | list_editable_changed = true; 196 | } 197 | }); 198 | }); 199 | 200 | document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) { 201 | if (list_editable_changed) { 202 | const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); 203 | if (!confirmed) { 204 | event.preventDefault(); 205 | } 206 | } 207 | }); 208 | 209 | const el = document.querySelector('#changelist-form input[name=_save]'); 210 | // The button does not exist if no fields are editable. 211 | if (el) { 212 | el.addEventListener('click', function(event) { 213 | if (document.querySelector('[name=action]').value) { 214 | const text = list_editable_changed 215 | ? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.") 216 | : gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button."); 217 | if (!confirm(text)) { 218 | event.preventDefault(); 219 | } 220 | } 221 | }); 222 | } 223 | }; 224 | 225 | // Call function fn when the DOM is loaded and ready. If it is already 226 | // loaded, call the function now. 227 | // http://youmightnotneedjquery.com/#ready 228 | function ready(fn) { 229 | if (document.readyState !== 'loading') { 230 | fn(); 231 | } else { 232 | document.addEventListener('DOMContentLoaded', fn); 233 | } 234 | } 235 | 236 | ready(function() { 237 | 238 | // BEGIN CUSTOMIZATION // 239 | // const actionsEls = document.querySelectorAll('tr input.action-select'); 240 | const actionsEls = document.querySelectorAll('.ui.checkbox.action-select'); 241 | // END CUSTOMIZATION // 242 | 243 | if (actionsEls.length > 0) { 244 | Actions(actionsEls); 245 | } 246 | }); 247 | } 248 | -------------------------------------------------------------------------------- /semantic_admin/static/admin/js/admin/RelatedObjectLookups.js: -------------------------------------------------------------------------------- 1 | /*global SelectBox, interpolate*/ 2 | // Handles related-objects functionality: lookup link for raw_id_fields 3 | // and Add Another links. 4 | 5 | (function($) { 6 | "use strict"; 7 | 8 | // IE doesn't accept periods or dashes in the window name, but the element IDs 9 | // we use to generate popup window names may contain them, therefore we map them 10 | // to allowed characters in a reversible way so that we can locate the correct 11 | // element when the popup window is dismissed. 12 | function id_to_windowname(text) { 13 | text = text.replace(/\./g, "__dot__"); 14 | text = text.replace(/\-/g, "__dash__"); 15 | return text; 16 | } 17 | 18 | function windowname_to_id(text) { 19 | text = text.replace(/__dot__/g, "."); 20 | text = text.replace(/__dash__/g, "-"); 21 | return text; 22 | } 23 | 24 | function showAdminPopup(triggeringLink, name_regexp, add_popup) { 25 | var name = triggeringLink.id.replace(name_regexp, ""); 26 | name = id_to_windowname(name); 27 | var href = triggeringLink.href; 28 | if (add_popup) { 29 | if (href.indexOf("?") === -1) { 30 | href += "?_popup=1"; 31 | } else { 32 | href += "&_popup=1"; 33 | } 34 | } 35 | var win = window.open( 36 | href, 37 | name, 38 | "height=500,width=800,resizable=yes,scrollbars=yes" 39 | ); 40 | win.focus(); 41 | return false; 42 | } 43 | 44 | function showRelatedObjectLookupPopup(triggeringLink) { 45 | return showAdminPopup(triggeringLink, /^lookup_/, true); 46 | } 47 | 48 | function dismissRelatedLookupPopup(win, chosenId) { 49 | var name = windowname_to_id(win.name); 50 | var elem = document.getElementById(name); 51 | if ( 52 | elem.className.indexOf("vManyToManyRawIdAdminField") !== -1 && 53 | elem.value 54 | ) { 55 | elem.value += "," + chosenId; 56 | } else { 57 | document.getElementById(name).value = chosenId; 58 | } 59 | win.close(); 60 | } 61 | 62 | function showRelatedObjectPopup(triggeringLink) { 63 | return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false); 64 | } 65 | 66 | function updateRelatedObjectLinks(triggeringLink) { 67 | var $this = $(triggeringLink); 68 | // BEGIN CUSTOMIZATION 69 | var relatedWidget = $this.closest("div.related-widget").next(); 70 | var siblings = relatedWidget.children(".change-related, .delete-related"); 71 | // END CUSTOMIZATION 72 | if (!siblings.length) { 73 | return; 74 | } 75 | var value = $this.val(); 76 | if (value) { 77 | siblings.each(function() { 78 | var elm = $(this); 79 | elm.attr( 80 | "href", 81 | elm.attr("data-href-template").replace("__fk__", value) 82 | ); 83 | }); 84 | } else { 85 | siblings.removeAttr("href"); 86 | } 87 | } 88 | 89 | function dismissAddRelatedObjectPopup(win, newId, newRepr) { 90 | var name = windowname_to_id(win.name); 91 | var elem = document.getElementById(name); 92 | if (elem) { 93 | var elemName = elem.nodeName.toUpperCase(); 94 | if (elemName === "SELECT") { 95 | elem.options[elem.options.length] = new Option( 96 | newRepr, 97 | newId, 98 | true, 99 | true 100 | ); 101 | } else if (elemName === "INPUT") { 102 | if ( 103 | elem.className.indexOf("vManyToManyRawIdAdminField") !== -1 && 104 | elem.value 105 | ) { 106 | elem.value += "," + newId; 107 | } else { 108 | elem.value = newId; 109 | } 110 | } 111 | // Trigger a change event to update related links if required. 112 | $(elem).trigger("change"); 113 | } else { 114 | var toId = name + "_to"; 115 | var o = new Option(newRepr, newId); 116 | SelectBox.add_to_cache(toId, o); 117 | SelectBox.redisplay(toId); 118 | } 119 | win.close(); 120 | } 121 | 122 | function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { 123 | var id = windowname_to_id(win.name).replace(/^edit_/, ""); 124 | var selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]); 125 | var selects = $(selectsSelector); 126 | selects.find("option").each(function() { 127 | if (this.value === objId) { 128 | this.textContent = newRepr; 129 | this.value = newId; 130 | } 131 | }); 132 | win.close(); 133 | } 134 | 135 | function dismissDeleteRelatedObjectPopup(win, objId) { 136 | var id = windowname_to_id(win.name).replace(/^delete_/, ""); 137 | var selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]); 138 | var selects = $(selectsSelector); 139 | selects 140 | .find("option") 141 | .each(function() { 142 | if (this.value === objId) { 143 | $(this).remove(); 144 | } 145 | }) 146 | .trigger("change"); 147 | win.close(); 148 | } 149 | 150 | // Global for testing purposes 151 | window.id_to_windowname = id_to_windowname; 152 | window.windowname_to_id = windowname_to_id; 153 | 154 | window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup; 155 | window.dismissRelatedLookupPopup = dismissRelatedLookupPopup; 156 | window.showRelatedObjectPopup = showRelatedObjectPopup; 157 | window.updateRelatedObjectLinks = updateRelatedObjectLinks; 158 | window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; 159 | window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; 160 | window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; 161 | 162 | // Kept for backward compatibility 163 | window.showAddAnotherPopup = showRelatedObjectPopup; 164 | window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; 165 | 166 | $(document).ready(function() { 167 | $("a[data-popup-opener]").click(function(event) { 168 | event.preventDefault(); 169 | opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener")); 170 | }); 171 | $("body").on("click", ".related-widget-wrapper-link", function(e) { 172 | e.preventDefault(); 173 | if (this.href) { 174 | var event = $.Event("django:show-related", { href: this.href }); 175 | $(this).trigger(event); 176 | if (!event.isDefaultPrevented()) { 177 | showRelatedObjectPopup(this); 178 | } 179 | } 180 | }); 181 | // Trigger on 'related-widget', instead of 'related-widget-wrapper'. 182 | $("body").on("change", ".related-widget select", function(e) { 183 | var event = $.Event("django:update-related"); 184 | $(this).trigger(event); 185 | if (!event.isDefaultPrevented()) { 186 | updateRelatedObjectLinks(this); 187 | } 188 | }); 189 | // Trigger on 'related-widget', instead of 'related-widget-wrapper'. 190 | $(".related-widget select").trigger("change"); 191 | $("body").on("click", ".related-lookup", function(e) { 192 | e.preventDefault(); 193 | var event = $.Event("django:lookup-related"); 194 | $(this).trigger(event); 195 | if (!event.isDefaultPrevented()) { 196 | showRelatedObjectLookupPopup(this); 197 | } 198 | }); 199 | }); 200 | })(django.jQuery); 201 | -------------------------------------------------------------------------------- /semantic_admin/static/semantic_admin/django-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 47 | 49 | 53 | 57 | 61 | 65 | 69 | 73 | 77 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /semantic_admin/static/semantic_admin/semantic-admin.css: -------------------------------------------------------------------------------- 1 | @media only screen { 2 | /* Sidebar Layout */ 3 | #app-main > .pusher > .full.height { 4 | display: -webkit-box; 5 | display: -webkit-flex; 6 | display: -ms-flexbox; 7 | display: flex; 8 | -webkit-flex-direction: row; 9 | -ms-flex-direction: row; 10 | flex-direction: row; 11 | min-height: 100vh; 12 | } 13 | } 14 | 15 | /* Container Layout */ 16 | #app-main .container { 17 | -webkit-box-flex: 1; 18 | -webkit-flex: 1 1 auto; 19 | -ms-flex: 1 1 auto; 20 | flex: 1 1 auto; 21 | min-width: 0px; 22 | } 23 | 24 | /* Fix full-height bottom off by 2px */ 25 | #app-main .full.height > .computer-menu > .ui.inverted.menu { 26 | border-top: 1px solid transparent !important; 27 | border-bottom: 1px solid transparent !important; 28 | border-radius: 0 !important; 29 | } 30 | 31 | /* Hide Top Bar */ 32 | @media only screen and (min-width: 1145px) { 33 | #app-main .full.height { 34 | min-height: 100%; 35 | } 36 | #app-main .fixed.main.menu { 37 | display: none; 38 | } 39 | } 40 | 41 | /* Remove Fixed Sidebar / Show Top Bar */ 42 | @media only screen and (max-width: 1144px) { 43 | /* Hide Fixed Sidebar */ 44 | #app-main .full.height > .computer-menu { 45 | display: none; 46 | } 47 | #app-main #app-content { 48 | margin-top: 3em; 49 | } 50 | } 51 | 52 | .pagination-buttons { 53 | display: inline-flex; 54 | vertical-align: middle; 55 | } 56 | 57 | .logout-button { 58 | width: 100%; 59 | text-align: left; 60 | border: none; 61 | cursor: pointer; 62 | } 63 | 64 | .logout-button:hover { 65 | color: #fff !important; 66 | } 67 | -------------------------------------------------------------------------------- /semantic_admin/static/semantic_admin/unsemantic.css: -------------------------------------------------------------------------------- 1 | .empty-form { 2 | display: none !important; 3 | } 4 | 5 | @media only screen and (max-width: 767.98px) { 6 | .ui.table:not(.unstackable) > tbody > tr.empty-form { 7 | display: none !important; 8 | } 9 | } 10 | 11 | /* ACTIONS */ 12 | 13 | @media (max-width: 767px) { 14 | div.actions { 15 | margin-bottom: 0 !important; 16 | } 17 | 18 | div.actions > label { 19 | margin-bottom: 0.28571429rem !important; /* Same as non-inline form */ 20 | } 21 | 22 | div.actions > div.field { 23 | margin-bottom: 0 !important; 24 | } 25 | 26 | div.actions > div.field > button { 27 | margin-top: 1rem !important; 28 | } 29 | 30 | span.action-counter { 31 | margin-top: 1rem !important; 32 | } 33 | } 34 | 35 | @media (min-width: 768px) { 36 | div.actions > div.field { 37 | margin-left: 1rem !important; 38 | } 39 | } 40 | 41 | /* END ACTIONS */ 42 | 43 | /* ACTION CHECKBOX */ 44 | 45 | th.action-checkbox-column, 46 | td.action-checkbox { 47 | width: 3.875rem; 48 | } 49 | 50 | @media (min-width: 768px) { 51 | th.action-checkbox-column, 52 | td.action-checkbox { 53 | text-align: center !important; 54 | } 55 | } 56 | 57 | @media (max-width: 767px) { 58 | th.action-checkbox-column, 59 | td.action-checkbox { 60 | text-align: auto !important; 61 | } 62 | } 63 | 64 | div#action-toggle, 65 | div.action-select { 66 | max-width: 1.25rem; 67 | margin-bottom: 0 !important; 68 | } 69 | 70 | /* END ACTION CHECKBOX */ 71 | 72 | /* IMPORT EXPORT ACTION */ 73 | 74 | @media (min-width: 768px) { 75 | div.ui.export-action.dropdown { 76 | width: 35% !important; 77 | margin-left: 1rem !important; 78 | } 79 | 80 | label.action-label { 81 | margin: auto; 82 | padding-right: 1rem; 83 | } 84 | } 85 | 86 | @media (max-width: 767px) { 87 | div.ui.export-action.dropdown { 88 | margin-top: 1rem !important; 89 | } 90 | } 91 | 92 | .hidden { 93 | display: none !important; 94 | } 95 | 96 | /* END IMPORT EXPORT ACTION */ 97 | 98 | @media print { 99 | .noprint { 100 | display: none !important; 101 | height: 0 !important; 102 | border: 0 none !important; 103 | opacity: 0 !important; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/actions.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | {% comment %} Lift actions class to fields {% endcomment %} 4 |
5 |
6 |
7 | {% for field in action_form %} 8 | {% if field.name == 'action' %} 9 | {% if forloop.first %} 10 | 11 | {% endif %} 12 | {{ field }} 13 | {% else %} 14 | {{ field }} 15 | {% endif %} 16 | {% endfor %} 17 |
18 |
19 |
20 | 21 | {% if actions_selection_counter %} 22 | {{ selection_note }} 23 | {% if cl.result_count != cl.result_list|length %} 24 | 25 | 30 | 31 | {% endif %} 32 | {% endif %} 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/auth/user/add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block form_top %} 5 | {% if not is_popup %} 6 |

{% trans "First, enter a username and password. Then, you'll be able to edit more user options." %}

7 | {% else %} 8 |

{% trans "Enter a username and password." %}

9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block after_field_sets %} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static semantic_utils %} 2 | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} 3 | 4 | 5 | 6 | {% block title %}{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block extrastyle %}{% endblock %} 14 | {% block responsive %} 15 | 16 | {% if LANGUAGE_BIDI %} 17 | 18 | {% endif %} 19 | {% endblock %} 20 | 21 | 22 | 23 | {% block extrahead %} 24 | 25 | 26 | {% endblock %} 27 | 28 | 29 | {% block blockbots %}{% endblock %} 30 | 31 | 32 | 33 | 34 | {% if not is_popup %} 35 | {% comment %} 36 | There are two menus on the page. The first is the offscreen menu, which is only 37 | visible if toggled. The menu button to toggle it is only visible when the screen 38 | is less than 1144px. 39 | 40 | Otherwise, a second computer menu is visible when the screen is more than 1145px. 41 | {% endcomment %} 42 | 43 | 44 | 47 | 48 | 49 | {% comment %} 50 | When, less than 1144px, the top menu bar appears. Clicking the menu button 51 | launches the offscreen menu. 52 | {% endcomment %} 53 | 54 | 55 | 62 | 63 | {% endif %} 64 | 65 | {% comment %}{% endcomment %} 66 | 67 | {% if not is_popup %} 68 | 69 |
70 |
71 |
72 | 75 |
76 | {% endif %} 77 | 78 | 79 |
80 | {% if not is_popup %} 81 | 82 | {% block breadcrumbsbox %} 83 |
84 | {% block breadcrumbs %}{% endblock %} 85 |
86 |
87 | {% endblock %} 88 | 89 | 90 |
91 | 92 | {% block messages %} 93 | {% if messages %} 94 | {% for message in messages %} 95 |
{{ message|capfirst }} 107 |
108 | {% endfor %} 109 | {% endif %} 110 | {% endblock messages %} 111 | {% endif %} 112 | 113 | 114 |
115 | 116 | {% block contenttop %} 117 |
118 |
119 |
120 |
121 | {% block pretitle %}{% endblock %} 122 | {% block content_title %} 123 | {% if title %}

{{ title }}

{% endif %} 124 | {% endblock %} 125 |
126 | 127 | {% block object-tools %}{% endblock %} 128 |
129 |
130 |
131 | {% endblock %} 132 | 133 | {% block content %} 134 | {{ content }} 135 | {% endblock %} 136 | {% block sidebar %}{% endblock %} 137 |
138 |
139 | 140 | 141 |
142 | 143 | {% block footer %} 144 | 145 | {% endblock %} 146 | 147 | 148 |
149 | 150 | {% if not is_popup %} 151 |
152 | 153 | 154 | 155 | 163 | {% endif %} 164 | 165 | 198 | 199 | {% block extrascript %}{% endblock %} 200 | 201 | 202 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 4 | 5 | {% block nav-global %}{% endblock %} 6 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/change_form.html: -------------------------------------------------------------------------------- 1 | {# comment Extend base_site.html instead of change_form.html b/c override extrastyle without block.super #} 2 | {% extends "admin/base_site.html" %} 3 | {% load i18n admin_urls static admin_modify semantic_utils %} 4 | 5 | {% block extrahead %}{{ block.super }} 6 | 7 | {{ media }} 8 | {% endblock %} 9 | 10 |
11 | {% if not is_popup %} 12 | {% block breadcrumbs %} 13 |
14 | 25 |
26 | {% endblock %} 27 | {% endif %} 28 | 29 | {% block object-tools %} 30 | {% if change %} 31 | {% if not is_popup %} 32 | {% block object-tools-items %} 33 | {% if not has_absolute_url %} 34 |
35 | {% endif %} 36 |
37 | {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} 38 | {% trans "History" %} 39 |
40 | {% if has_absolute_url %} 41 | 44 | {% endif %} 45 | {% endblock %} 46 | {% endif %} 47 | {% endif %} 48 | {% endblock %} 49 | 50 | {% block content %} 51 |
52 |
{% csrf_token %}{% block form_top %}{% endblock %} 53 |
54 | {% if is_popup %}{% endif %} 55 | {% if to_field %}{% endif %} 56 | {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} 57 | {% if errors %} 58 |
59 | {% blocktranslate count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %} 60 |
61 | {{ adminform.form.non_field_errors }} 62 | {% endif %} 63 | 64 |
65 | {% block field_sets %} 66 | {% for fieldset in adminform %} 67 | {% include "admin/includes/fieldset.html" %} 68 | {% endfor %} 69 | {% endblock %} 70 | 71 | {% block after_field_sets %}{% endblock %} 72 |
73 | 74 | {% block inline_field_sets %} 75 | {% for inline_admin_formset in inline_admin_formsets %} 76 | {% include inline_admin_formset.opts.template %} 77 | {% endfor %} 78 | {% endblock %} 79 | 80 | {% block after_related_objects %}{% endblock %} 81 | 82 | {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} 83 | 84 | {% block admin_change_form_document_ready %} 85 | {{ block.super }} 86 | 154 | {% endblock %} 155 | 156 | {# JavaScript for prepopulated fields #} 157 | {% prepopulated_fields_js %} 158 | 159 |
160 |
161 |
162 | 163 |
164 | {% endblock %} 165 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {% load i18n admin_urls static admin_list semantic_utils %} 3 | 4 | {% block extrastyle %} 5 | {{ media.css }} 6 | {{ cl.model_admin.filterset.form.media.css }} 7 | {% endblock %} 8 | 9 | {% block extrahead %} 10 | 11 | {{ block.super }} 12 | {{ cl.model_admin.filterset.form.media.js }} 13 | {% endblock %} 14 | 15 | {% if not is_popup %} 16 | {% block breadcrumbs %} 17 | 23 | {% endblock %} 24 | {% endif %} 25 | 26 | {% block object-tools %} 27 | {#
#} 28 | {% block object-tools-items %} 29 | {% if has_add_permission %} 30 | {% url cl.opts|admin_urlname:'add' as add_url %} 31 | 36 | {% endif %} 37 | {% endblock %} 38 | {#
#} 39 | {% endblock %} 40 | 41 | {% block content %} 42 |
43 | {% if cl.formset.errors %} 44 |

45 | {% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 46 |

47 | {{ cl.formset.non_form_errors }} 48 | {% endif %} 49 |
50 | {% block search %}{% search_form cl %}{% endblock %} 51 | {% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %} 52 | 53 | {% block filters %} 54 | {% if cl.has_filters %} 55 |

{% trans 'Filter' %}

56 |
57 |
58 | {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} 59 |
60 |
61 | {% endif %} 62 | {% endblock %} 63 | 64 | {% block before_changelist %}{{ semantic_header }}{% endblock %} 65 | 66 |
{% csrf_token %} 67 | {% if cl.formset %} 68 |
{{ cl.formset.management_form }}
69 | {% endif %} 70 | 71 | {% block result_list %} 72 | 73 |
74 | 75 | {% if cl.result_count %} 76 | 77 | 78 | {% if action_form and actions_on_top and cl.show_admin_actions %} 79 | 80 | 83 | 84 | {% endif %} 85 | {% semantic_result_list cl %} 86 |
81 | {% admin_actions %} 82 |
87 | 88 | {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} 89 | {% endif %} 90 |
91 | {% endblock %} 92 | 93 | {% block pagination %}{% endblock %} 94 |
95 |
96 |
97 | {% endblock %} 98 | 99 | {% block extrascript %}{% endblock %} 100 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/change_list_results.html: -------------------------------------------------------------------------------- 1 | {% load i18n static admin_list %} 2 | {% if result_hidden_fields %} 3 |
{# DIV for HTML validation #} 4 | {% for item in result_hidden_fields %}{{ item }}{% endfor %} 5 |
6 | {% endif %} 7 | {% if results %} 8 | 9 | {% for header in result_headers %} 10 | 11 | {% if header.sortable %} 12 | {% if header.sort_priority > 0 %} 13 |
14 | 15 | {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} 16 | 17 |
18 | {% endif %} 19 | {% endif %} 20 | {% if header.sortable %} 21 | {{ header.text|capfirst }} 22 | {% else %} 23 | {{ header.text|capfirst }} 24 | {% endif %} 25 | {% endfor %} 26 | 27 | 28 | 29 | {% for result in results %} 30 | {% if result.form.non_field_errors %} 31 | {{ result.form.non_field_errors }} 32 | {% endif %} 33 | {% for item in result %}{{ item }}{% endfor %} 34 | {% endfor %} 35 | 36 | 37 | 38 | {% block pagination %} 39 | {% if cl.result_count %}{% pagination cl %}{% endif %} 40 | {% endblock %} 41 | 42 | 43 | 44 | {% endif %} 45 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/date_hierarchy.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if show %} 3 |

{% trans 'Date filter' %}

4 |
5 |
6 | {% block date-hierarchy-toplinks %} 7 | {% block date-hierarchy-back %} 8 | {% if back %}{% endif %} 9 | {% endblock %} 10 | {% block date-hierarchy-choices %} 11 | {% for choice in choices %} 12 | 13 | {% endfor %} 14 | {% endblock %} 15 | {% endblock %} 16 |
17 |
18 | {% endif %} 19 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/delete_button.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls %} 2 | {% if show_delete_link %} 3 | {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} 4 | {% trans "Delete" %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/delete_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls semantic_filters static %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | {{ media }} 7 | 8 | {% endblock %} 9 | 10 | {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %} 11 | 12 | {% block breadcrumbs %} 13 | 20 | {% endblock %} 21 | 22 | {% block content %} 23 |
24 | {% if perms_lacking %} 25 |

{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}

26 |
27 | {% for obj in perms_lacking %} 28 |
{{ obj }}
29 | {% endfor %} 30 |
31 | {% elif protected %} 32 |

{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would require deleting the following protected related objects:{% endblocktrans %}

33 |
34 | {% for obj in protected %} 35 |
{{ obj }}
36 | {% endfor %} 37 |
38 | {% else %} 39 |

{% blocktrans with escaped_object=object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}

40 | {% include "admin/includes/object_delete_summary.html" %} 41 |

{% trans "Objects" %}

42 |
{{ deleted_objects|semantic_unordered_list }}
43 |
44 | 45 |
{% csrf_token %} 46 |
47 | 48 | {% if is_popup %}{% endif %} 49 | {% if to_field %}{% endif %} 50 | 51 |
52 |
53 |
54 | 55 | 56 |
57 | 58 |
59 | 62 | 63 |
64 |
65 | 66 |
67 |
68 |
69 | 70 |
71 |
72 | {% endif %} 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/delete_selected_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n l10n admin_urls semantic_filters static %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | {{ media }} 7 | 8 | {% endblock %} 9 | 10 | {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %} 11 | 12 | {% block breadcrumbs %} 13 | 19 | {% endblock %} 20 | 21 | {% block content %} 22 |
23 | {% if perms_lacking %} 24 |

{% blocktrans %}Deleting the selected {{ objects_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}

25 |
26 | {% for obj in perms_lacking %} 27 |
{{ obj }}
28 | {% endfor %} 29 |
30 | {% elif protected %} 31 |

{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}

32 |
33 | {% for obj in protected %} 34 |
{{ obj }}
35 | {% endfor %} 36 |
37 | {% else %} 38 |

{% blocktrans %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktrans %}

39 | {% include "admin/includes/object_delete_summary.html" %} 40 |

{% trans "Objects" %}

41 | {% for deletable_object in deletable_objects %} 42 |
{{ deletable_object|semantic_unordered_list }}
43 | {% endfor %} 44 |
45 | 46 |
{% csrf_token %} 47 |
48 | {% for obj in queryset %} 49 | 50 | {% endfor %} 51 | 52 | 53 | 54 |
55 |
56 |
57 | 58 |
59 | 60 |
61 | 64 |
65 |
66 | 67 |
68 |
69 |
70 | 71 |
72 |
73 | {% endif %} 74 | {% endblock %} 75 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/edit_inline/stacked.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls static %} 2 |
7 |
8 | 9 |

10 | {% if inline_admin_formset.has_add_permission or inline_admin_formset.has_change_permission or inline_admin_formset.has_view_permission or inline_admin_formset.has_delete_permission %} 11 | {{ inline_admin_formset.opts.verbose_name_plural|capfirst }} 12 | {% endif %} 13 |

14 | 15 | {{ inline_admin_formset.formset.management_form }} 16 | {{ inline_admin_formset.formset.non_form_errors }} 17 | 18 | {% for inline_admin_form in inline_admin_formset %}
19 |

{{ inline_admin_formset.opts.verbose_name|capfirst }}: {% if inline_admin_form.original %}{{ inline_admin_form.original }} 20 | {% if inline_admin_form.show_url %}{% trans "View on site" %}{% endif %} 21 | {% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} {% trans "Change" %}{% endif %} 22 | {% else %}#{{ forloop.counter }}{% endif %} 23 | {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}
{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}
{% endif %} 24 |

25 | {% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %} 26 | {% for fieldset in inline_admin_form %} 27 | {% include "admin/includes/fieldset.html" %} 28 | {% endfor %} 29 | {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} 30 | {{ inline_admin_form.fk_field.field }} 31 |
{% endfor %} 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/edit_inline/tabular.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls static admin_modify %} 2 |
5 | 85 |
86 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/filter.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | 4 | 21 |
22 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_modify admin_urls static %} 3 | 4 | {% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} 5 | 6 | {% if not is_popup %} 7 | {% block breadcrumbs %} 8 | 15 | {% endblock %} 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends base_change_list_template %} 2 | 3 | {# Original template renders object-tools only when has_add_permission is True. #} 4 | {# This hack allows sub templates to add to object-tools #} 5 | {% block object-tools %} 6 | {% block object-tools-items %} 7 | {% if not has_import_permission %} 8 |
9 | {% endif %} 10 | {% if not has_export_permission %} 11 |
12 | {% endif %} 13 | 14 | {% if has_add_permission %} 15 | {{ block.super }} 16 | {% endif %} 17 | {% endblock %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/change_list_export_item.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load admin_urls %} 3 | 4 | {% if has_export_permission %} 5 | 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/change_list_import_item.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load admin_urls %} 3 | 4 | {% if has_import_permission %} 5 | 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/export.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/import_export/base.html" %} 2 | {% load i18n admin_urls import_export_tags %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | {{ form.media }} 7 | {% endblock %} 8 | 9 | {% block breadcrumbs_last %} 10 | {% trans "Export" %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 | {% include "admin/import_export/resource_fields_list.html" with import_or_export="export" %} 15 |
16 | {% csrf_token %} 17 | 18 |
19 |
20 | {% for field in form %} 21 |
22 | {{ field.label_tag }} 23 | 24 | {{ field }} 25 | 26 | {% if field.field.help_text %} 27 |
{{ field.field.help_text|safe }}
28 | {% endif %} 29 | 30 | {{ field.errors }} 31 |
32 | {% endfor %} 33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/export_action_select.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/import.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/import_export/base.html" %} 2 | {% load i18n admin_urls import_export_tags %} 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | {% if confirm_form %} 7 | {{ confirm_form.media }} 8 | {% else %} 9 | {{ form.media }} 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block breadcrumbs_last %} 14 | {% trans "Import" %} 15 | {% endblock %} 16 | 17 | {% block content %} 18 | 19 | {% if confirm_form %} 20 | {% block confirm_import_form %} 21 |
22 | {% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %} 23 |
24 | 25 |
26 | {% csrf_token %} 27 | {{ confirm_form.as_p }} 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | {% endblock %} 42 | {% else %} 43 | {% block import_form %} 44 |
45 | {% include "admin/import_export/resource_fields_list.html" with import_or_export="import" %} 46 |
47 | 48 |
49 | {% csrf_token %} 50 | 51 | {% block form_detail %} 52 | {#
#} 53 | 54 |
55 | {% for field in form %} 56 |
57 |
58 | {{ field.label_tag }} 59 | 60 | {{ field }} 61 | 62 | {% if field.field.help_text %} 63 |
{{ field.field.help_text|safe }}
64 | {% endif %} 65 | 66 | {{ field.errors }} 67 |
68 |
69 | {% endfor %} 70 | {#
#} 71 | 72 | 73 | {% endblock %} 74 | 75 | {% block form_submit_button %} 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 |
84 |
85 |
86 | {% endblock %} 87 |
88 | {% endblock %} 89 | {% endif %} 90 | 91 | {% if result %} 92 | 93 | {% if result.has_errors %} 94 | {% block errors %} 95 |
96 |

{% trans "Errors" %}

97 |
98 | {% for error in result.base_errors %} 99 |
100 | {{ error.error }} 101 |
{{ error.traceback|linebreaks }}
102 |
103 | {% endfor %} 104 | {% for line, errors in result.row_errors %} 105 |
106 | {% for error in errors %} 107 |
108 | {% trans "Line number" %}: {{ line }} - {{ error.error }} 109 |
{{ error.row.values|join:", " }}
110 |
{{ error.traceback|linebreaks }}
111 |
112 | {% endfor %} 113 |
114 | {% endfor %} 115 | 116 |
117 | {% endblock %} 118 | 119 | {% elif result.has_validation_errors %} 120 | 121 | {% block validation_errors %} 122 |

{% trans "Some rows failed to validate" %}

123 | 124 |
{% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}
125 | 126 | 127 | 128 | 129 | 130 | 131 | {% for field in result.diff_headers %} 132 | 133 | {% endfor %} 134 | 135 | 136 | 137 | {% for row in result.invalid_rows %} 138 | 139 | 140 | 170 | {% for field in row.values %} 171 | 172 | {% endfor %} 173 | 174 | {% endfor %} 175 | 176 |
{% trans "Row" %}{% trans "Errors" %}{{ field }}
{{ row.number }} 141 | {% if row.error_count > 0 %} 142 |
{{ row.error_count }}
143 | 167 | 168 | {% endif %} 169 |
{{ field }}
177 | {% endblock %} 178 | 179 | {% else %} 180 | 181 | {% block preview %} 182 |

{% trans "Preview" %}

183 | 184 | 185 | 186 | 187 | 188 | {% for field in result.diff_headers %} 189 | 190 | {% endfor %} 191 | 192 | 193 | {% for row in result.valid_rows %} 194 | 195 | 206 | {% for field in row.diff %} 207 | 208 | {% endfor %} 209 | 210 | {% endfor %} 211 |
{{ field }}
196 | {% if row.import_type == 'new' %} 197 | {% trans "New" %} 198 | {% elif row.import_type == 'skip' %} 199 | {% trans "Skipped" %} 200 | {% elif row.import_type == 'delete' %} 201 | {% trans "Delete" %} 202 | {% elif row.import_type == 'update' %} 203 | {% trans "Update" %} 204 | {% endif %} 205 | {{ field }}
212 | {% endblock %} 213 | 214 | {% endif %} 215 | 216 | {% endif %} 217 | {% endblock %} 218 | 219 | {% block extrascript %} 220 | 230 | {% endblock %} 231 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/import_export/resource_fields_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% block fields_help %} 3 |
4 | {% if import_or_export == "export" %} 5 | {% trans "This exporter will export the following fields: " %} 6 | {% elif import_or_export == "import" %} 7 | {% trans "This importer will import the following fields: " %} 8 | {% endif %} 9 | 10 | {% if fields_list|length <= 1 %} 11 | {{ fields_list.0.1|join:", " }} 12 | {% else %} 13 |
14 | {% for resource, fields in fields_list %} 15 |
{{ resource }}
16 |
{{ fields|join:", " }}
17 | {% endfor %} 18 |
19 | {% endif %} 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/includes/fieldset.html: -------------------------------------------------------------------------------- 1 |
2 | {% include "admin/includes/fieldset_content.html" %} 3 |
4 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/includes/fieldset_content.html: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Semantic opinions about fieldset and legend tags https://github.com/Semantic-Org/Semantic-UI/issues/596 However, class="ui segment" seems to do the same thing. 3 | {% endcomment %} 4 | {% if fieldset.name %}

{{ fieldset.name }}

{% endif %} 5 | {% if fieldset.description %} 6 |
{{ fieldset.description|safe }}
7 | {% endif %} 8 | 9 | {% for line in fieldset %} 10 | {% if line.has_visible_field %} 11 |
12 | {% endif %} 13 | 14 | {% for field in line %} 15 |
16 | {% if field.is_checkbox %} 17 |
18 | {% endif %} 19 | 20 | {% if not field.is_checkbox and not field.is_hidden %} 21 | {{ field.label_tag }} 22 | {% endif %} 23 | 24 | {% if field.is_readonly %} 25 |
26 | {% if field.contents %}{{ field.contents }}{% else %} {% endif %} 27 |
28 | {% else %} 29 | {{ field.field }} 30 | {% endif %} 31 | 32 | {% if field.is_checkbox %} 33 | {{ field.label_tag }} 34 | {% endif %} 35 | 36 | {% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %} 37 | {% if field.is_checkbox %} 38 |
39 | {% endif %} 40 | 41 | {% if field.field.help_text %} 42 |
{{ field.field.help_text|safe }}
43 | {% endif %} 44 | {% if line.fields|length == 1 %}{{ line.errors }}{% endif %} 45 |
46 | {% endfor %} 47 | 48 | {% if line.has_visible_field %} 49 |
50 | {% endif%} 51 | {% endfor %} 52 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/includes/object_delete_summary.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

{% trans "Summary" %}

3 |
4 | {% for model_name, object_count in model_count %} 5 |
{{ model_name|capfirst }}: {{ object_count }}
6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static semantic_app_list %} 3 | 4 | {% block content_title %}

{% endblock %} 5 | 6 | {% block breadcrumbsbox %} 7 |
8 | 11 |
12 |
13 | {% endblock %} 14 | 15 | {% block content %} 16 |
17 |
18 | 19 |
20 | {% admin_apps as app_list %} 21 | {% if app_list %} 22 | {% for app in app_list %} 23 |
24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | {% for model in app.models %} 34 | 35 | 42 | 43 | {% if model.add_url %} 44 | 45 | {% else %} 46 | 47 | {% endif %} 48 | 49 | {% if model.admin_url %} 50 | 51 | {% else %} 52 | 53 | {% endif %} 54 | 55 | {% endfor %} 56 | 57 |
28 | {{ app.name|capfirst }} 29 |
36 | {% if model.admin_url %} 37 | {{ model.name }} 38 | {% else %} 39 | {{ model.name }} 40 | {% endif %} 41 | {% trans 'Add' %} {% trans 'Change' %} 
58 |
59 |
60 | {% endfor %} 61 | {% else %} 62 |

{% trans "You don't have permission to edit anything." %}

63 | {% endif %} 64 |
65 | 66 | {% endblock %} 67 | 68 | {% block sidebar %} 69 |
70 |

{% trans 'Recent Actions' %}

71 | 100 | 101 |
102 |
103 |
104 | {% endblock %} 105 |
106 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "semantic_forms/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Login" %} | {{ site_title|default:_('Django site admin') }}{% endblock %} 5 | 6 | {% block extrastyle %} 7 | {{ form.media }} 8 | 29 | {% endblock %} 30 | 31 | {% block blockbots %}{% endblock %} 32 | 33 | {% block content %} 34 |
35 |
36 |
37 | 38 |
39 |
{% trans 'Log in' %}
40 |
41 | 42 |
43 | {% if form.errors and not form.non_field_errors %} 44 |

45 | {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 46 |

47 | {% endif %} 48 | 49 | {% if form.non_field_errors %} 50 | {% for error in form.non_field_errors %} 51 |

{{ error }}

52 | {% endfor %} 53 | {% endif %} 54 |
55 | 56 | {% if user.is_authenticated %} 57 |

58 | {% blocktrans trimmed %} 59 | You are authenticated as {{ user.get_username }}, but are not authorized to 60 | access this page. Would you like to login to a different account? 61 | {% endblocktrans %} 62 |

63 | {% endif %} 64 | 65 |
{% csrf_token %} 66 |
67 | {{ form.username.errors }} 68 | {{ form.username.label_tag }} {{ form.username }} 69 |
70 | 71 |
72 | {{ form.password.errors }} 73 | {{ form.password.label_tag }} {{ form.password }} 74 | 75 |
76 | 77 | {% url 'admin_password_reset' as password_reset_url %} 78 | 79 | {% block passwordreset %} 80 | {% if password_reset_url %} 81 | 84 | {% endif %} 85 | {% endblock %} 86 | 87 |
88 | 89 |
90 | 91 |
92 |
93 | 94 |
95 |
96 |
97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/main.html: -------------------------------------------------------------------------------- 1 | 2 | {% block breadbox %} 3 |
4 | {% block breadcrumbs %}{% endblock %} 5 |
6 | {% endblock %} 7 | 8 | {% if not is_popup %} 9 | {% block messages %} 10 | {% if messages %} 11 | {% for message in messages %} 12 | 21 | {{ message|capfirst }} 22 |
23 | {% endif %} 24 | {% endfor %} 25 | {% endif %} 26 | {% endblock messages %} 27 | {% endif %} 28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 | {% block pretitle %}{% endblock %} 36 | {% block content_title %} 37 | {% if title %} 38 |
39 |

{{ title }}

40 |
41 | {% endif %} 42 | {% endblock %} 43 |
44 | 45 |
46 | {% block object-tools %}{% endblock %} 47 |
48 | 49 |
50 | {% block content %} 51 | {{ content }} 52 | {% endblock %} 53 | {% block sidebar %}{% endblock %} 54 |
55 |
56 | 57 | 58 | {% block footer %} 59 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/menu.html: -------------------------------------------------------------------------------- 1 | {% load i18n static semantic_app_list semantic_utils %} 2 | 3 | {% block brandingbox %} 4 |
5 |
6 | {% block branding %} 7 | 8 | {{ site_header|default:_('Django administration') }} 9 | 10 | {% endblock %} 11 |
12 |
13 | {% endblock %} 14 | 15 | {% admin_apps as app_list %} 16 | {% if app_list %} 17 |
18 | {% for app in app_list %} 19 |
20 |
{{ app.name|title }}
21 |
22 | {% for model in app.models %} 23 | {% if model.admin_url %} 24 | {{ model.name }} 25 | {% else %} 26 | {{ model.name }} 27 | {% endif %} 28 | {% endfor %} 29 |
30 |
31 | {% endfor %} 32 |
33 | 34 | {% else %} 35 | 38 | {% endif %} 39 | 40 | {% block usertools %} 41 | {% if has_permission %} 42 |
43 | 44 |
45 | {% block welcome-msg %} 46 | {% trans 'Welcome,' %} {% firstof user.get_short_name user.get_username %} 47 | {% endblock %} 48 |
49 | 50 | {% block userlinks %} 51 | 70 | {% endblock %} 71 | 72 |
73 | {% endif %} 74 | 75 | {% endblock %} 76 | {% block nav-global %}{% endblock %} 77 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/object_history.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls %} 3 | 4 | {% block breadcrumbs %} 5 | 17 | {% endblock %} 18 | 19 | {% block content %} 20 |
21 |
22 | 23 | {% if action_list %} 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for action in action_list %} 35 | 36 | 37 | 38 | 39 | 40 | {% endfor %} 41 | 42 |
{% trans 'Date/time' %}{% trans 'User' %}{% trans 'Action' %}
{{ action.action_time|date:"DATETIME_FORMAT" }}{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}{{ action.get_change_message }}
43 | {% else %} 44 |

{% trans 'This object doesn’t have a change history. It probably wasn’t added via this admin site.' %}

45 | {% endif %} 46 |
47 |
48 |
49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/pagination.html: -------------------------------------------------------------------------------- 1 | {% load admin_list semantic_utils %} 2 | {% load i18n %} 3 | {% if pagination_required %} 4 | 9 | {% endif %} 10 | {{ cl.result_count }} {% if cl.result_count == 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endif %} 11 |
12 | {% if show_all_url %}{% trans 'Show all' %}{% endif %} 13 | {% if cl.formset and cl.result_count %}{% endif %} 14 |
15 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/save_buttons.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls %} 2 | 3 | {% if show_save_as_new %} 4 |
5 | 6 |
7 | {% endif %} 8 | 9 | {% if show_save_and_add_another and not hide_save_and_add_another %} 10 |
11 | 12 |
13 | {% endif %} 14 | 15 | {% if show_save_and_continue %} 16 |
17 | 18 |
19 | {% endif %} 20 | 21 | {% if show_save %} 22 |
23 | 24 |
25 | {% endif %} 26 | 27 | {% comment %} 28 | {% if show_close %} 29 | {% trans 'Close' %} 30 | {% endif %} 31 | {% endcomment %} 32 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/search_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls static awesomesearch semantic_utils %} 2 | 3 | {% if cl.search_fields or cl.model_admin.filterset.form.fields %} 4 |
5 |
6 |
7 | {% if cl.search_fields %} 8 | {% for pair in cl.params.items %} 9 | {% if pair.0 != search_var %}{% endif %} 10 | {% endfor %} 11 | {% endif %} 12 | 13 | {% search_fields cl %} 14 | 15 | {% if show_result_count %} 16 | {% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} ({% if cl.show_full_result_count %}{% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}{% else %}{% trans "Show all" %}{% endif %}) 17 | {% endif %} 18 | 19 |
20 |
21 |
22 | 23 | 28 | 29 | {% endif %} 30 | -------------------------------------------------------------------------------- /semantic_admin/templates/admin/submit_line.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
{% include "admin/delete_button.html" %}
6 | 7 | 8 | 9 | {% include "admin/save_buttons.html" %} 10 | 11 |
12 |
13 |
-------------------------------------------------------------------------------- /semantic_admin/templates/admin/widgets/related_widget_wrapper.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 5 | 32 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 8 | {% endblock %} 9 | 10 | {% block nav-sidebar %}{% endblock %} 11 | 12 | {% block content %} 13 | 14 |

{% trans "Thanks for spending some quality time with the Web site today." %}

15 | 16 |
17 |
18 |
19 | 20 | 23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | {% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} 4 | {% block breadcrumbs %} 5 | 9 | {% endblock %} 10 | 11 | {% block title %}{{ title }}{% endblock %} 12 | {% block content_title %}

{{ title }}

{% endblock %} 13 | {% block content %} 14 |

{% trans 'Your password was changed.' %}

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | {% block extrastyle %}{{ block.super }}{% endblock %} 4 | {% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | 12 | {% block content %}
13 | 14 |
{% csrf_token %} 15 |
16 | {% if form.errors %} 17 |

18 | {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 19 |

20 | {% endif %} 21 | 22 | 23 |

{% trans 'Please enter your old password, for security’s sake, and then enter your new password twice so we can verify you typed it in correctly.' %}

24 | 25 | {#
#} 26 |
27 | 28 |
29 | {{ form.old_password.errors }} 30 | {{ form.old_password.label_tag }} {{ form.old_password }} 31 |
32 | 33 |
34 | {{ form.new_password1.errors }} 35 | {{ form.new_password1.label_tag }} {{ form.new_password1 }} 36 | {% if form.new_password1.help_text %} 37 |
{{ form.new_password1.help_text|safe }}
38 | {% endif %} 39 |
40 | 41 |
42 | {{ form.new_password2.errors }} 43 | {{ form.new_password2.label_tag }} {{ form.new_password2 }} 44 | {% if form.new_password2.help_text %} 45 |
{{ form.new_password2.help_text|safe }}
46 | {% endif %} 47 |
48 | 49 |
50 | {#
#} 51 | 52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 9 | {% endblock %} 10 | 11 | {% block title %}{{ title }}{% endblock %} 12 | {% block content_title %}

{{ title }}

{% endblock %} 13 | 14 | {% block content %} 15 | 16 |

{% trans "Your password has been set. You may go ahead and log in now." %}

17 | 18 |

{% trans 'Log in' %}

19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }}{% endblock %} 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | 12 | {% block title %}{{ title }}{% endblock %} 13 | {% block content_title %}

{{ title }}

{% endblock %} 14 | {% block content %} 15 | 16 | {% if validlink %} 17 | 18 |

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

19 | 20 |
{% csrf_token %} 21 |
22 |
23 | {{ form.new_password1.errors }} 24 | 25 | {{ form.new_password1 }} 26 |
27 |
28 | {{ form.new_password2.errors }} 29 | 30 | {{ form.new_password2 }} 31 |
32 | 33 |
34 |
35 | 36 | {% else %} 37 | 38 |

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

39 | 40 | {% endif %} 41 | 42 | {% endblock %} 43 | 44 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %} 5 | 9 | {% endblock %} 10 | 11 | {% block title %}{{ title }}{% endblock %} 12 | {% block content_title %}

{{ title }}

{% endblock %} 13 | {% block content %} 14 | 15 |

{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}

16 | 17 |

{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}

18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /semantic_admin/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }}{% endblock %} 5 | {% block breadcrumbs %} 6 | 10 | {% endblock %} 11 | 12 | {% block title %}{{ title }}{% endblock %} 13 | {% block content_title %}

{{ title }}

{% endblock %} 14 | {% block content %} 15 | 16 |

{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

17 | 18 |
{% csrf_token %} 19 |
20 |
21 | {{ form.email.errors }} 22 | 23 | {{ form.email }} 24 |
25 | 26 |
27 |
28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /semantic_admin/templates/semantic_admin/action_checkbox.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /semantic_admin/templates/semantic_admin/changelist_checkbox.html: -------------------------------------------------------------------------------- 1 |
2 | {% include "django/forms/widgets/input.html" %} 3 |
4 | -------------------------------------------------------------------------------- /semantic_admin/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/semantic_admin/templatetags/__init__.py -------------------------------------------------------------------------------- /semantic_admin/templatetags/awesomesearch.py: -------------------------------------------------------------------------------- 1 | from django import forms, template 2 | from django.utils.html import format_html 3 | from django.utils.safestring import mark_safe 4 | 5 | BLANK_LABEL = "" 6 | FIELD = '
{}
' 7 | COMPUTER_FIELD = '
' 8 | 9 | register = template.Library() 10 | 11 | try: 12 | from django.utils.translation import gettext_lazy as _ # Django >= 4 13 | except ImportError: 14 | from django.utils.translation import ugettext_lazy as _ 15 | 16 | 17 | def format_fields(cl, fields): 18 | html = "" 19 | row = '
{}
' 20 | r = [] 21 | for index, field in enumerate(fields): 22 | r.append(field) 23 | i = index + 1 24 | is_divisible_by_4 = not i % 4 25 | if is_divisible_by_4: 26 | html += row.format("".join(r)) 27 | r = [] 28 | # Remaining fields. 29 | while len(r) < 3: 30 | r.append(COMPUTER_FIELD) 31 | search_button = format_search_button(cl) 32 | r.append(search_button) 33 | html += row.format("".join(r)) 34 | return html 35 | 36 | 37 | def format_search_field(context, cl): 38 | field = "" 39 | if len(cl.search_fields): 40 | label = _("Search") 41 | search_var = context["search_var"] 42 | search_label = f'' 43 | search_input = f""" 44 | 50 | """ 51 | if hasattr(cl.model_admin, "filterset"): 52 | field = f"{search_label}{search_input}" 53 | else: 54 | field = f""" 55 |
56 | {search_input} 57 | 60 |
61 | """ 62 | return FIELD.format(field) 63 | else: 64 | return "" 65 | 66 | 67 | def format_search_button(cl): 68 | html = "" 69 | search_label = _("Search") 70 | search_button = f""" 71 | 74 | """ 75 | if hasattr(cl.model_admin, "filterset"): 76 | html = f""" 77 |
78 | {BLANK_LABEL}{search_button} 79 |
80 | """ 81 | else: 82 | html = FIELD.format(search_button) 83 | return html 84 | 85 | 86 | @register.simple_tag(takes_context=True) 87 | def search_fields(context, cl): 88 | html = "" 89 | search_field = format_search_field(context, cl) 90 | if hasattr(cl.model_admin, "filterset"): 91 | fields = [search_field] 92 | filter_field = """ 93 | 94 | {field}{errors} 95 | """ 96 | filterset = cl.model_admin.filterset 97 | form = filterset.form 98 | for field in filterset.form: 99 | # WTF 100 | label = _(field.label.lower()).capitalize() 101 | if isinstance(form.fields[field.name].widget, forms.HiddenInput): 102 | f = f""" 103 | 104 | {filterset.email} 105 | {field}{field.errors} 106 | """ 107 | else: 108 | format_dict = dict( 109 | field_id=field.id_for_label, 110 | label=label, 111 | field=field, 112 | errors=field.errors, 113 | ) 114 | f = filter_field.format(**format_dict) 115 | f = FIELD.format(f) 116 | fields.append(f) 117 | try: 118 | from semantic_admin.filters import SemanticExcludeAllFilterSet 119 | except ImportError: 120 | pass 121 | else: 122 | if isinstance(cl.model_admin.filterset, SemanticExcludeAllFilterSet): 123 | exclude_label = _("Exclude") 124 | checked = cl.model_admin.filterset_exclude 125 | exclude_checkbox = f""" 126 | {BLANK_LABEL} 127 |
128 | 137 | 138 |
139 | """ 140 | f = FIELD.format(exclude_checkbox) 141 | fields.append(f) 142 | html += format_fields(cl, fields) 143 | else: 144 | html = search_field 145 | return format_html(mark_safe(html)) 146 | -------------------------------------------------------------------------------- /semantic_admin/templatetags/semantic_admin_list.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Generator 3 | 4 | from django import template 5 | from django.contrib.admin.templatetags.admin_list import ResultList, _coerce_field_name 6 | from django.contrib.admin.templatetags.admin_urls import add_preserved_filters 7 | from django.contrib.admin.utils import lookup_field 8 | from django.contrib.admin.views.main import ChangeList 9 | from django.contrib.admin.widgets import RelatedFieldWidgetWrapper 10 | from django.core.exceptions import ObjectDoesNotExist 11 | from django.db import models 12 | from django.urls import NoReverseMatch 13 | from django.utils.html import format_html 14 | from django.utils.safestring import mark_safe 15 | 16 | from semantic_admin.utils import semantic_display_for_field, semantic_display_for_value 17 | 18 | register = template.Library() 19 | 20 | 21 | def _semantic_boolean_icon(field_val: str) -> str: 22 | """Semantic boolean icon.""" 23 | return format_html( 24 | '' 25 | % {True: "green check", False: "red times", None: "gray question"}[field_val] 26 | ) 27 | 28 | 29 | def semantic_items_for_result(cl: ChangeList, result, form): 30 | """Generate the actual list of data.""" 31 | 32 | def link_in_col(is_first: bool, field_name: str, cl) -> bool: 33 | if cl.list_display_links is None: 34 | return False 35 | if is_first and not cl.list_display_links: 36 | return True 37 | return field_name in cl.list_display_links 38 | 39 | first = True 40 | pk = cl.lookup_opts.pk.attname 41 | for field_index, field_name in enumerate(cl.list_display): 42 | empty_value_display = cl.model_admin.get_empty_value_display() 43 | row_classes = ["field-%s" % _coerce_field_name(field_name, field_index)] 44 | try: 45 | f, attr, value = lookup_field(field_name, result, cl.model_admin) 46 | except ObjectDoesNotExist: 47 | result_repr = empty_value_display 48 | else: 49 | empty_value_display = getattr( 50 | attr, "empty_value_display", empty_value_display 51 | ) 52 | if f is None or f.auto_created: 53 | if field_name == "action_checkbox": 54 | row_classes = ["action-checkbox"] 55 | boolean = getattr(attr, "boolean", False) 56 | # BEGIN CUSTOMIZATION 57 | result_repr = semantic_display_for_value( 58 | value, empty_value_display, boolean 59 | ) 60 | # END CUSTOMIZATION 61 | if isinstance(value, (datetime.date, datetime.time)): 62 | row_classes.append("nowrap") 63 | else: 64 | if isinstance(f.remote_field, models.ManyToOneRel): 65 | field_val = getattr(result, f.name) 66 | if field_val is None: 67 | result_repr = empty_value_display 68 | else: 69 | result_repr = field_val 70 | else: 71 | # BEGIN CUSTOMIZATION 72 | result_repr = semantic_display_for_field( 73 | value, f, empty_value_display 74 | ) 75 | # END CUSTOMIZATION 76 | if isinstance( 77 | f, (models.DateField, models.TimeField, models.ForeignKey) 78 | ): 79 | row_classes.append("nowrap") 80 | row_class = mark_safe(' class="%s"' % " ".join(row_classes)) 81 | # If list_display_links not defined, add the link tag to the first field 82 | if link_in_col(first, field_name, cl): 83 | 84 | # BEGIN CUSTOMIZATION 85 | table_tag = "td" 86 | # END CUSTOMIZATION 87 | 88 | first = False 89 | 90 | # Display link to the result's change_view if the url exists, else 91 | # display just the result's representation. 92 | try: 93 | url = cl.url_for_result(result) 94 | except NoReverseMatch: 95 | link_or_text = result_repr 96 | else: 97 | url = add_preserved_filters( 98 | {"preserved_filters": cl.preserved_filters, "opts": cl.opts}, url 99 | ) 100 | # Convert the pk to something that can be used in Javascript. 101 | # Problem cases are non-ASCII strings. 102 | if cl.to_field: 103 | attr = str(cl.to_field) 104 | else: 105 | attr = pk 106 | value = result.serializable_value(attr) 107 | link_or_text = format_html( 108 | '{}', 109 | url, 110 | format_html(' data-popup-opener="{}"', value) 111 | if cl.is_popup 112 | else "", 113 | result_repr, 114 | ) 115 | 116 | yield format_html( 117 | "<{}{}>{}", table_tag, row_class, link_or_text, table_tag 118 | ) 119 | else: 120 | # By default the fields come from ModelAdmin.list_editable, but if we pull 121 | # the fields out of the form instead of list_editable custom admins 122 | # can provide fields on a per request basis 123 | if ( 124 | form 125 | and field_name in form.fields 126 | and not ( 127 | field_name == cl.model._meta.pk.name 128 | and form[cl.model._meta.pk.name].is_hidden 129 | ) 130 | ): 131 | 132 | # BEGIN CUSTOMIZATION 133 | field = form.fields[field_name] 134 | bf = form[field_name] 135 | if isinstance(field.widget, RelatedFieldWidgetWrapper): 136 | bf_repr = str(bf.errors) + " " + str(bf) 137 | else: 138 | bf_repr = str(bf.errors) + str(bf) 139 | result_repr = mark_safe(bf_repr) 140 | # END CUSTOMIZATION 141 | 142 | yield format_html("{}", row_class, result_repr) 143 | if form and not form[cl.model._meta.pk.name].is_hidden: 144 | yield format_html("{}", form[cl.model._meta.pk.name]) 145 | 146 | 147 | def semantic_results(cl: ChangeList) -> Generator[ResultList, None, None]: 148 | """Semantic results.""" 149 | if cl.formset: 150 | for res, form in zip(cl.result_list, cl.formset.forms): 151 | yield ResultList(form, semantic_items_for_result(cl, res, form)) 152 | else: 153 | for res in cl.result_list: 154 | yield ResultList(None, semantic_items_for_result(cl, res, None)) 155 | -------------------------------------------------------------------------------- /semantic_admin/templatetags/semantic_app_list.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.admin import AdminSite 5 | from django.urls import resolve, reverse 6 | 7 | register = template.Library() 8 | 9 | 10 | def get_semantic_app_list(app_list, current_app): 11 | semantic_app_list = getattr(settings, "SEMANTIC_APP_LIST", None) 12 | if semantic_app_list: 13 | apps = [] 14 | for semantic_app in semantic_app_list: 15 | for app in app_list: 16 | semantic_app_label = semantic_app.get("app_label", None) 17 | is_semantic_app = ( 18 | isinstance(semantic_app_label, str) 19 | and semantic_app_label == app["app_label"] 20 | ) 21 | if is_semantic_app: 22 | app["is_active"] = app["app_label"] == current_app 23 | semantic_models = semantic_app.get("models", None) 24 | if isinstance(semantic_models, list): 25 | models = [] 26 | for semantic_model in semantic_models: 27 | semantic_object_name = semantic_model.get( 28 | "object_name", None 29 | ) 30 | if isinstance(semantic_object_name, str): 31 | for model in app["models"]: 32 | if semantic_object_name == model["object_name"]: 33 | models.append(model) 34 | app["models"] = models 35 | apps.append(app) 36 | has_active = any([app["is_active"] for app in apps]) 37 | if len(apps) and not has_active: 38 | apps[0]["is_active"] = True 39 | return apps 40 | else: 41 | return app_list 42 | 43 | 44 | def get_app_label(resolver_match): 45 | if "app_label" in resolver_match.kwargs: 46 | return resolver_match.kwargs.get("app_label") 47 | else: 48 | # Reconstruct from url_name. 49 | url_name = resolver_match.url_name 50 | # Exclude model and action. 51 | parts = url_name.split("_")[:-2] 52 | # Return parts. 53 | return "_".join(parts) 54 | 55 | 56 | @register.simple_tag(takes_context=True) 57 | def get_app_list(context): 58 | request = context["request"] 59 | resolver_match = resolve(request.path_info) 60 | admin_name = resolver_match.namespace 61 | current_app = get_app_label(resolver_match) 62 | admin_site = get_admin_site(admin_name) 63 | app_list = admin_site.get_app_list(request) 64 | return get_semantic_app_list(app_list, current_app) 65 | 66 | 67 | def get_admin_site(current_app): 68 | try: 69 | resolver_match = resolve(reverse("%s:index" % current_app)) 70 | for func_closure in resolver_match.func.func_closure: 71 | if isinstance(func_closure.cell_contents, AdminSite): 72 | return func_closure.cell_contents 73 | except Exception: 74 | pass 75 | return admin.site 76 | 77 | 78 | def get_admin_url(request, admin_site): 79 | try: 80 | url = "{}:index".format(admin_site) 81 | url = reverse(url) 82 | except Exception: 83 | pass 84 | else: 85 | return url 86 | 87 | 88 | @register.simple_tag(takes_context=True) 89 | def admin_apps(context): 90 | return get_app_list(context) 91 | -------------------------------------------------------------------------------- /semantic_admin/templatetags/semantic_filters.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | from django.template.library import Library 4 | from django.utils.html import conditional_escape 5 | from django.utils.safestring import mark_safe 6 | 7 | register = Library() 8 | 9 | 10 | @register.filter(is_safe=True, needs_autoescape=True) 11 | def semantic_unordered_list(value, autoescape=True): 12 | """ 13 | Recursively take a self-nested list and return an HTML unordered list -- 14 | WITHOUT opening and closing
    tags. 15 | Assume the list is in the proper format. For example, if ``var`` contains: 16 | ``['States', ['Kansas', ['Lawrence', 'Topeka'], 'Illinois']]``, then 17 | ``{{ var|unordered_list }}`` returns:: 18 |
    States 19 |
    20 |
    Kansas
    21 |
    22 |
    Lawrence
    23 |
    Topeka
    24 |
    25 |
    26 |
    Illinois
    27 |
    28 |
29 | """ 30 | if autoescape: 31 | escaper = conditional_escape 32 | else: 33 | 34 | def escaper(x): 35 | return x 36 | 37 | def walk_items(item_list): 38 | item_iterator = iter(item_list) 39 | try: 40 | item = next(item_iterator) 41 | while True: 42 | try: 43 | next_item = next(item_iterator) 44 | except StopIteration: 45 | yield item, None 46 | break 47 | if isinstance(next_item, (list, tuple, types.GeneratorType)): 48 | try: 49 | iter(next_item) 50 | except TypeError: 51 | pass 52 | else: 53 | yield item, next_item 54 | item = next(item_iterator) 55 | continue 56 | yield item, None 57 | item = next_item 58 | except StopIteration: 59 | pass 60 | 61 | def list_formatter(item_list, tabs=1): 62 | indent = "\t" * tabs 63 | output = [] 64 | for item, children in walk_items(item_list): 65 | sublist = "" 66 | if children: 67 | sublist = '\n%s
\n%s\n%s
\n%s' % ( 68 | indent, 69 | list_formatter(children, tabs + 1), 70 | indent, 71 | indent, 72 | ) 73 | output.append( 74 | '%s
%s%s
' % (indent, escaper(item), sublist) 75 | ) 76 | return "\n".join(output) 77 | 78 | return mark_safe(list_formatter(value)) 79 | -------------------------------------------------------------------------------- /semantic_admin/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import decimal 3 | import json 4 | from typing import Any 5 | 6 | from django.contrib.admin.utils import display_for_value 7 | from django.db import models 8 | from django.forms import Field 9 | from django.utils import formats, timezone 10 | from django.utils.html import format_html 11 | 12 | try: 13 | from django.contrib.postgres.fields import JSONField 14 | except ImportError: 15 | from django.db.models import JSONField # type: ignore 16 | 17 | 18 | def semantic_display_for_field( 19 | value: Any, field: Field, empty_value_display: str 20 | ) -> str: 21 | """Semantic display for field""" 22 | from .templatetags.semantic_admin_list import _semantic_boolean_icon 23 | 24 | if getattr(field, "flatchoices", None): 25 | return dict(field.flatchoices).get(value, empty_value_display) 26 | # BooleanField needs special-case null-handling, so it comes before the 27 | # general null test. 28 | 29 | # BEGIN CUSTOMIZATION 30 | elif isinstance(field, models.BooleanField): 31 | return _semantic_boolean_icon(value) 32 | # END CUSTOMIZATION 33 | 34 | elif value is None: 35 | return empty_value_display 36 | elif isinstance(field, models.DateTimeField): 37 | return formats.localize(timezone.template_localtime(value)) 38 | elif isinstance(field, (models.DateField, models.TimeField)): 39 | return formats.localize(value) 40 | elif isinstance(field, models.DecimalField): 41 | return formats.number_format(value, field.decimal_places) 42 | elif isinstance(field, (models.IntegerField, models.FloatField)): 43 | return formats.number_format(value) 44 | elif isinstance(field, models.FileField) and value: 45 | return format_html('{}', value.url, value) 46 | elif isinstance(field, JSONField) and value: 47 | try: 48 | return json.dumps(value, ensure_ascii=False, cls=field.encoder) 49 | except TypeError: 50 | return display_for_value(value, empty_value_display) 51 | else: 52 | return display_for_value(value, empty_value_display) 53 | 54 | 55 | def semantic_display_for_value( 56 | value: Any, empty_value_display: str, boolean: bool = False 57 | ) -> str: 58 | """Semantic display for value""" 59 | from .templatetags.semantic_admin_list import _semantic_boolean_icon 60 | 61 | # BEGIN CUSTOMIZATION 62 | if boolean: 63 | return _semantic_boolean_icon(value) 64 | # END CUSTOMIZATION 65 | 66 | elif value is None: 67 | return empty_value_display 68 | elif isinstance(value, bool): 69 | return str(value) 70 | elif isinstance(value, datetime.datetime): 71 | return formats.localize(timezone.template_localtime(value)) 72 | elif isinstance(value, (datetime.date, datetime.time)): 73 | return formats.localize(value) 74 | elif isinstance(value, (int, decimal.Decimal, float)): 75 | return formats.number_format(value) 76 | elif isinstance(value, (list, tuple)): 77 | return ", ".join(str(v) for v in value) 78 | else: 79 | return str(value) 80 | -------------------------------------------------------------------------------- /semantic_admin/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globophobe/django-semantic-admin/eed8c5699042343bb2ec2e2600bdeb4669a442db/semantic_admin/views/__init__.py -------------------------------------------------------------------------------- /semantic_admin/views/autocomplete.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.views.autocomplete import AutocompleteJsonView 2 | from django.core.exceptions import PermissionDenied 3 | from django.db import models 4 | from django.http import HttpRequest, JsonResponse 5 | 6 | 7 | class SemanticAutocompleteJsonView(AutocompleteJsonView): 8 | """Semantic autocomplete JSON view""" 9 | 10 | # TODO: Delete overridden get method once serialize_result is released 11 | # in a future Django version 12 | def get(self, request: HttpRequest, *args, **kwargs) -> JsonResponse: 13 | """ 14 | Return a JsonResponse with search results of the form: 15 | { 16 | results: [{id: "123" text: "foo"}], 17 | pagination: {more: true} 18 | } 19 | """ 20 | ( 21 | self.term, 22 | self.model_admin, 23 | self.source_field, 24 | to_field_name, 25 | ) = self.process_request(request) 26 | 27 | if not self.has_perm(request): 28 | raise PermissionDenied 29 | 30 | self.object_list = self.get_queryset() 31 | context = self.get_context_data() 32 | return JsonResponse( 33 | { 34 | # BEGIN CUSTOMIZATION # 35 | "results": [ 36 | self.serialize_result(obj, to_field_name) 37 | for obj in context["object_list"] 38 | ], 39 | # END CUSTOMIZATION # 40 | "pagination": {"more": context["page_obj"].has_next()}, 41 | } 42 | ) 43 | 44 | def serialize_result(self, obj: models.Model, to_field_name: str) -> dict: 45 | """ 46 | Convert the provided model object to a dictionary that is added to the 47 | results list. 48 | """ 49 | return { 50 | "id": str(getattr(obj, to_field_name)), 51 | "name": getattr(obj, "semantic_autocomplete", str(obj)), 52 | "text": str(obj), 53 | } 54 | -------------------------------------------------------------------------------- /semantic_admin/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .admin import SemanticActionCheckboxInput, SemanticChangelistCheckboxInput 2 | from .autocomplete import SemanticAutocompleteSelect, SemanticAutocompleteSelectMultiple 3 | 4 | __all__ = [ 5 | "SemanticActionCheckboxInput", 6 | "SemanticAutocompleteSelect", 7 | "SemanticAutocompleteSelectMultiple", 8 | "SemanticChangelistCheckboxInput", 9 | ] 10 | -------------------------------------------------------------------------------- /semantic_admin/widgets/admin.py: -------------------------------------------------------------------------------- 1 | from semantic_forms import SemanticCheckboxInput 2 | 3 | 4 | class SemanticChangelistCheckboxInput(SemanticCheckboxInput): 5 | """Semantic changelist checkbox input.""" 6 | 7 | template_name = "semantic_admin/changelist_checkbox.html" 8 | 9 | 10 | class SemanticActionCheckboxInput(SemanticCheckboxInput): 11 | """Semantic action checkbox input.""" 12 | 13 | template_name = "semantic_admin/action_checkbox.html" 14 | -------------------------------------------------------------------------------- /semantic_admin/widgets/autocomplete.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.admin.widgets import AutocompleteMixin 3 | 4 | 5 | class SemanticAutocompleteMixin(AutocompleteMixin): 6 | """ 7 | Select widget mixin that loads options from SemanticAutocompleteJsonView 8 | via AJAX. Renders the necessary data attributes for Semantic UI Dropdowns 9 | and adds the static form media. 10 | """ 11 | 12 | template_name = "semantic_forms/forms/widgets/chooser.html" 13 | 14 | @property 15 | def media(self) -> None: 16 | """Media.""" 17 | # Does not use django autocomplete media. 18 | 19 | 20 | class SemanticAutocompleteSelect( # type: ignore 21 | SemanticAutocompleteMixin, forms.Select 22 | ): 23 | """Semantic autocomplete select.""" 24 | 25 | template_name = "semantic_forms/forms/widgets/select.html" 26 | 27 | 28 | class SemanticAutocompleteSelectMultiple( # type: ignore 29 | SemanticAutocompleteMixin, forms.SelectMultiple 30 | ): 31 | """Semantic autocomplete select multiple.""" 32 | 33 | template_name = "semantic_forms/forms/widgets/select.html" 34 | --------------------------------------------------------------------------------