{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 | 15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /demo/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | {% load i18n static %} 3 | 4 | {% block passwordreset %} 5 | 10 |TEMPLATES
settingtemplates/admin/menu.html
with the followingTEMPLATES
settingtemplates/admin/base.html
with the following{% 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 |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 |{% 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 |{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would require deleting the following protected related objects:{% endblocktrans %}
33 |{% 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 |{% 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 |{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}
32 |{% 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 |{{ error.row.values|join:", " }}
{% trans "Row" %} | 130 |{% trans "Errors" %} | 131 | {% for field in result.diff_headers %} 132 |{{ field }} | 133 | {% endfor %} 134 |
---|---|---|
{{ row.number }} | 140 |
141 | {% if row.error_count > 0 %}
142 |
144 |
167 |
168 | {% endif %}
169 |
145 | {% for field_name, error_list in row.field_specific_errors.items %}
146 |
166 |
147 |
154 | {% endfor %}
155 | {% if row.non_field_specific_errors %}
156 | {{ field_name }}
148 |
149 | {% for error in error_list %}
150 |
153 | {{ error }}
151 | {% endfor %}
152 |
157 |
164 | {% endif %}
165 | {% trans "Non field specific" %}
158 |
159 | {% for error in row.non_field_specific_errors %}
160 |
163 | {{ error }}
161 | {% endfor %}
162 | |
170 | {% for field in row.values %}
171 |
143 | {{ field }} | 172 | {% endfor %} 173 |
188 | {% for field in result.diff_headers %} 189 | | {{ field }} | 190 | {% endfor %} 191 |
---|---|
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 | | 206 | {% for field in row.diff %} 207 |{{ field }} | 208 | {% endfor %} 209 |
28 | {{ app.name|capfirst }} 29 | | 30 |||||
---|---|---|---|---|
36 | {% if model.admin_url %} 37 | {{ model.name }} 38 | {% else %} 39 | {{ model.name }} 40 | {% endif %} 41 | | 42 | 43 | {% if model.add_url %} 44 |{% trans 'Add' %} | 45 | {% else %} 46 |47 | {% endif %} 48 | 49 | {% if model.admin_url %} 50 | | {% trans 'Change' %} | 51 | {% else %} 52 |53 | {% endif %} 54 | |
{% trans "You don't have permission to edit anything." %}
63 | {% endif %} 64 |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 |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 | 93 | 94 |{% trans 'Date/time' %} | 29 |{% trans 'User' %} | 30 |{% trans 'Action' %} | 31 |
---|---|---|
{{ action.action_time|date:"DATETIME_FORMAT" }} | 37 |{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %} | 38 |{{ action.get_change_message }} | 39 |
{% trans 'This object doesn’t have a change history. It probably wasn’t added via this admin site.' %}
45 | {% endif %} 46 |{% trans "Thanks for spending some quality time with the Web site today." %}
15 | 16 |{% 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 %}{% trans "Your password has been set. You may go ahead and log in now." %}
17 | 18 | 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 %}{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}
19 | 20 | 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 %}{% 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 %}{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}
17 | 18 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /semantic_admin/templates/semantic_admin/action_checkbox.html: -------------------------------------------------------------------------------- 1 |