sandwitches 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. sandwitches/__init__.py +6 -0
  2. sandwitches/admin.py +69 -0
  3. sandwitches/api.py +207 -0
  4. sandwitches/asgi.py +16 -0
  5. sandwitches/feeds.py +23 -0
  6. sandwitches/forms.py +196 -0
  7. sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  8. sandwitches/locale/nl/LC_MESSAGES/django.po +1010 -0
  9. sandwitches/migrations/0001_initial.py +328 -0
  10. sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +27 -0
  11. sandwitches/migrations/0003_setting.py +35 -0
  12. sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +37 -0
  13. sandwitches/migrations/0005_rating_comment.py +17 -0
  14. sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +22 -0
  15. sandwitches/migrations/__init__.py +0 -0
  16. sandwitches/models.py +218 -0
  17. sandwitches/settings.py +220 -0
  18. sandwitches/storage.py +114 -0
  19. sandwitches/tasks.py +115 -0
  20. sandwitches/templates/admin/admin_base.html +118 -0
  21. sandwitches/templates/admin/confirm_delete.html +23 -0
  22. sandwitches/templates/admin/dashboard.html +262 -0
  23. sandwitches/templates/admin/rating_list.html +38 -0
  24. sandwitches/templates/admin/recipe_form.html +184 -0
  25. sandwitches/templates/admin/recipe_list.html +64 -0
  26. sandwitches/templates/admin/tag_form.html +30 -0
  27. sandwitches/templates/admin/tag_list.html +37 -0
  28. sandwitches/templates/admin/task_detail.html +91 -0
  29. sandwitches/templates/admin/task_list.html +41 -0
  30. sandwitches/templates/admin/user_form.html +37 -0
  31. sandwitches/templates/admin/user_list.html +60 -0
  32. sandwitches/templates/base.html +94 -0
  33. sandwitches/templates/base_beer.html +57 -0
  34. sandwitches/templates/components/carousel_scripts.html +59 -0
  35. sandwitches/templates/components/favorites_search_form.html +85 -0
  36. sandwitches/templates/components/footer.html +14 -0
  37. sandwitches/templates/components/ingredients_scripts.html +50 -0
  38. sandwitches/templates/components/ingredients_section.html +11 -0
  39. sandwitches/templates/components/instructions_section.html +9 -0
  40. sandwitches/templates/components/language_dialog.html +26 -0
  41. sandwitches/templates/components/navbar.html +27 -0
  42. sandwitches/templates/components/rating_section.html +66 -0
  43. sandwitches/templates/components/recipe_header.html +32 -0
  44. sandwitches/templates/components/search_form.html +106 -0
  45. sandwitches/templates/components/search_scripts.html +98 -0
  46. sandwitches/templates/components/side_menu.html +35 -0
  47. sandwitches/templates/components/user_menu.html +10 -0
  48. sandwitches/templates/detail.html +178 -0
  49. sandwitches/templates/favorites.html +42 -0
  50. sandwitches/templates/index.html +76 -0
  51. sandwitches/templates/login.html +57 -0
  52. sandwitches/templates/partials/recipe_list.html +87 -0
  53. sandwitches/templates/recipe_form.html +119 -0
  54. sandwitches/templates/setup.html +105 -0
  55. sandwitches/templates/signup.html +133 -0
  56. sandwitches/templatetags/__init__.py +0 -0
  57. sandwitches/templatetags/custom_filters.py +15 -0
  58. sandwitches/templatetags/markdown_extras.py +17 -0
  59. sandwitches/urls.py +109 -0
  60. sandwitches/utils.py +222 -0
  61. sandwitches/views.py +647 -0
  62. sandwitches/wsgi.py +16 -0
  63. sandwitches-2.2.0.dist-info/METADATA +104 -0
  64. sandwitches-2.2.0.dist-info/RECORD +65 -0
  65. sandwitches-2.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,220 @@
1
+ """
2
+ Django settings for sandwitches project.
3
+
4
+ Generated by 'django-admin startproject' using Django 5.2.7.
5
+
6
+ For more information on this file, see
7
+ https://docs.djangoproject.com/en/5.2/topics/settings/
8
+
9
+ For the full list of settings and their values, see
10
+ https://docs.djangoproject.com/en/5.2/ref/settings/
11
+ """
12
+
13
+ from pathlib import Path
14
+ import os
15
+
16
+ from django.core.exceptions import ImproperlyConfigured
17
+ from . import storage
18
+
19
+ DEBUG = bool(os.environ.get("DEBUG", default=0))
20
+
21
+ SECRET_KEY = os.environ.get("SECRET_KEY")
22
+ if not SECRET_KEY:
23
+ raise ImproperlyConfigured(
24
+ "The SECRET_KEY environment variable must be set in production."
25
+ )
26
+
27
+ ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "127.0.0.1, localhost").split(",")
28
+ CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",")
29
+ DATABASE_FILE = Path(os.environ.get("DATABASE_FILE", default="/db/db.sqlite3"))
30
+
31
+ storage.is_database_readable(DATABASE_FILE)
32
+ storage.is_database_writable(DATABASE_FILE)
33
+
34
+
35
+ # Build paths inside the project like this: BASE_DIR / 'subdir'.
36
+ BASE_DIR = Path(__file__).resolve().parent.parent
37
+
38
+
39
+ TASKS = {
40
+ "default": {
41
+ "BACKEND": "django_tasks.backends.database.DatabaseBackend",
42
+ "QUEUES": ["default", "emails"],
43
+ "OPTIONS": {
44
+ "queues": {
45
+ "low_priority": {
46
+ "max_attempts": 5,
47
+ }
48
+ },
49
+ "max_attempts": 10,
50
+ "backoff_factor": 3,
51
+ "purge": {"finished": "10 days", "unfinished": "20 days"},
52
+ },
53
+ }
54
+ }
55
+
56
+ # Application definition
57
+ INSTALLED_APPS = [
58
+ "django.contrib.admin",
59
+ "django.contrib.auth",
60
+ "django.contrib.contenttypes",
61
+ "django.contrib.sessions",
62
+ "django.contrib.messages",
63
+ "django.contrib.staticfiles",
64
+ "sandwitches",
65
+ "django_tasks",
66
+ "django_tasks.backends.database",
67
+ "debug_toolbar",
68
+ "imagekit",
69
+ "import_export",
70
+ "simple_history",
71
+ "solo",
72
+ ]
73
+
74
+ MIDDLEWARE = [
75
+ "django.middleware.security.SecurityMiddleware",
76
+ "whitenoise.middleware.WhiteNoiseMiddleware",
77
+ "django.contrib.sessions.middleware.SessionMiddleware",
78
+ "django.middleware.locale.LocaleMiddleware",
79
+ "django.middleware.common.CommonMiddleware",
80
+ "django.middleware.csrf.CsrfViewMiddleware",
81
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
82
+ "django.contrib.messages.middleware.MessageMiddleware",
83
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
84
+ "debug_toolbar.middleware.DebugToolbarMiddleware",
85
+ "simple_history.middleware.HistoryRequestMiddleware",
86
+ ]
87
+
88
+ ROOT_URLCONF = "sandwitches.urls"
89
+
90
+ TEMPLATES = [
91
+ {
92
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
93
+ "DIRS": [],
94
+ "APP_DIRS": True,
95
+ "OPTIONS": {
96
+ "context_processors": [
97
+ "django.template.context_processors.request",
98
+ "django.contrib.auth.context_processors.auth",
99
+ "django.contrib.messages.context_processors.messages",
100
+ "django.template.context_processors.csrf",
101
+ "django.template.context_processors.i18n",
102
+ ],
103
+ },
104
+ },
105
+ ]
106
+
107
+ WSGI_APPLICATION = "sandwitches.wsgi.application"
108
+
109
+ DATABASES = {
110
+ "default": {
111
+ "ENGINE": "django.db.backends.sqlite3",
112
+ "NAME": DATABASE_FILE,
113
+ }
114
+ }
115
+
116
+ LOGGING = {
117
+ "version": 1,
118
+ "disable_existing_loggers": False,
119
+ "handlers": {
120
+ "console": {
121
+ "class": "logging.StreamHandler",
122
+ },
123
+ },
124
+ "loggers": {
125
+ "django": {
126
+ "handlers": ["console"],
127
+ "level": os.getenv("LOG_LEVEL", "INFO"),
128
+ "propagate": False,
129
+ },
130
+ },
131
+ }
132
+
133
+ LOGIN_REDIRECT_URL = "index"
134
+ LOGOUT_REDIRECT_URL = "index"
135
+
136
+
137
+ # Password validation
138
+ # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
139
+
140
+ AUTH_USER_MODEL = "sandwitches.User"
141
+
142
+ AUTH_PASSWORD_VALIDATORS = [
143
+ {
144
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
145
+ },
146
+ {
147
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
148
+ },
149
+ {
150
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
151
+ },
152
+ {
153
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
154
+ },
155
+ ]
156
+
157
+
158
+ # Media files (for uploaded images)
159
+ MEDIA_URL = "/media/"
160
+ MEDIA_ROOT = Path(os.environ.get("MEDIA_ROOT", default=BASE_DIR / "media"))
161
+
162
+ # Static (for CSS etc)
163
+ STATIC_URL = "/static/"
164
+ STATIC_ROOT = Path("/tmp/staticfiles")
165
+ STATICFILES_DIRS = [BASE_DIR / "static", MEDIA_ROOT]
166
+
167
+ LANGUAGE_CODE = "en"
168
+ TIME_ZONE = "UTC"
169
+ USE_I18N = True
170
+ LANGUAGES = [
171
+ ("en", "English"),
172
+ ("nl", "Nederlands"),
173
+ ]
174
+
175
+ LOCALE_PATHS = [BASE_DIR / "locale"]
176
+
177
+ USE_TZ = True
178
+
179
+ # EU Date formats
180
+ DATE_FORMAT = "d/m/Y"
181
+ DATETIME_FORMAT = "d/m/Y H:i:s"
182
+ SHORT_DATE_FORMAT = "d/m/Y"
183
+ SHORT_DATETIME_FORMAT = "d/m/Y H:i"
184
+
185
+ INTERNAL_IPS = [
186
+ "127.0.0.1",
187
+ ]
188
+
189
+ STORAGES = {
190
+ "default": {
191
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
192
+ },
193
+ "staticfiles": {
194
+ "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
195
+ },
196
+ }
197
+
198
+
199
+ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
200
+ EMAIL_USE_TLS = os.environ.get("SMTP_USE_TLS")
201
+ EMAIL_HOST = os.environ.get("SMTP_HOST")
202
+ EMAIL_HOST_USER = os.environ.get("SMTP_USER")
203
+ EMAIL_HOST_PASSWORD = os.environ.get("SMTP_PASSWORD")
204
+ EMAIL_PORT = os.environ.get("SMTP_PORT")
205
+ EMAIL_FROM_ADDRESS = os.environ.get("SMTP_FROM_EMAIL")
206
+ SEND_EMAIL = all(
207
+ v is not None
208
+ for v in [
209
+ EMAIL_HOST,
210
+ EMAIL_HOST_USER,
211
+ EMAIL_HOST_PASSWORD,
212
+ EMAIL_PORT,
213
+ EMAIL_FROM_ADDRESS,
214
+ ]
215
+ )
216
+
217
+ # Default primary key field type
218
+ # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
219
+
220
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
sandwitches/storage.py ADDED
@@ -0,0 +1,114 @@
1
+ import hashlib
2
+ import os
3
+ from django.core.files.base import ContentFile
4
+ from django.core.files.storage import FileSystemStorage
5
+ from pathlib import Path
6
+ from django.conf import settings
7
+ import logging
8
+
9
+
10
+ class HashedFilenameStorage(FileSystemStorage):
11
+ """
12
+ Save uploaded files under a hash of their contents + original extension.
13
+ Example output: media/recipes/3f8a9d...png
14
+ """
15
+
16
+ def _save(self, name, content):
17
+ try:
18
+ content.seek(0)
19
+ except Exception:
20
+ pass
21
+
22
+ data = content.read()
23
+ h = hashlib.sha256(data).hexdigest()[:32]
24
+ ext = os.path.splitext(name)[1].lower() or ""
25
+ name = f"recipes/{h}{ext}"
26
+
27
+ content = ContentFile(data)
28
+ return super()._save(name, content)
29
+
30
+
31
+ def is_database_readable(path=None) -> bool:
32
+ """
33
+ Check whether the database file exists and is readable.
34
+
35
+ If `path` is None, uses `django.conf.settings.DATABASE_FILE` by default.
36
+
37
+ Returns:
38
+ bool: True if the file exists and is readable by the current process, False otherwise.
39
+ """
40
+
41
+ if path is None:
42
+ path = getattr(settings, "DATABASE_FILE", None)
43
+
44
+ if not path:
45
+ return False
46
+
47
+ p = Path(path)
48
+ logging.debug(f"Checking database file readability at: {p}")
49
+ try:
50
+ if p.is_file():
51
+ with open(p, "r"): # Removed 'as f'
52
+ # If we can open it, it's readable. No need to read content.
53
+ pass
54
+ logging.debug(f"Database file at {p} is readable.")
55
+ return True
56
+ else:
57
+ logging.error(f"Database file at {p} does not exist.")
58
+ return False
59
+ except IOError:
60
+ logging.error(
61
+ f"Database file at {p} is not readable due to permission or other IO error."
62
+ )
63
+ return False
64
+
65
+
66
+ def is_database_writable(path=None) -> bool:
67
+ """
68
+ Check whether the database file exists and is writable.
69
+
70
+ If `path` is None, uses `django.conf.settings.DATABASE_FILE` by default.
71
+
72
+ Returns:
73
+ bool: True if the file exists and is writable by the current process, False otherwise.
74
+ """
75
+
76
+ if path is None:
77
+ path = getattr(settings, "DATABASE_FILE", None)
78
+
79
+ if not path:
80
+ return False
81
+
82
+ p = Path(path)
83
+ logging.debug(f"Checking database file writability at: {p}")
84
+ try:
85
+ # If the file exists, try to open it for appending
86
+ if p.is_file():
87
+ with open(p, "a"): # Removed 'as f'
88
+ pass
89
+ logging.debug(f"Database file at {p} is writable.")
90
+ return True
91
+ else:
92
+ # If file does not exist, check if its parent directory is writable
93
+ if p.parent.is_dir() and os.access(p.parent, os.W_OK):
94
+ # Try creating a dummy file to confirm writability
95
+ dummy_file = p.parent / f".tmp_writable_test_{os.getpid()}"
96
+ try:
97
+ dummy_file.touch()
98
+ dummy_file.unlink()
99
+ logging.debug(f"Database path at {p.parent} is writable.")
100
+ return True
101
+ except IOError:
102
+ logging.error(f"Cannot create dummy file in {p.parent}.")
103
+ return False
104
+ else:
105
+ logging.error(
106
+ f"Parent directory {p.parent} is not writable or does not exist."
107
+ )
108
+ return False
109
+
110
+ except IOError:
111
+ logging.error(
112
+ f"Database file at {p} is not writable due to permission or other IO error."
113
+ )
114
+ return False
sandwitches/tasks.py ADDED
@@ -0,0 +1,115 @@
1
+ # from gunicorn.http.wsgi import log
2
+ import logging
3
+
4
+ # from django.core.mail import send_mail
5
+ from django_tasks import task
6
+ from django.contrib.auth import get_user_model
7
+
8
+ from django.core.mail import EmailMultiAlternatives
9
+ from django.conf import settings
10
+ from django.utils.translation import gettext as _
11
+
12
+
13
+ import textwrap
14
+
15
+
16
+ @task(takes_context=True, priority=2, queue_name="emails")
17
+ def email_users(context, recipe_id):
18
+ logging.debug(
19
+ f"Attempt {context.attempt} to send users an email. Task result id: {context.task_result.id}."
20
+ )
21
+
22
+ User = get_user_model()
23
+ emails = list(
24
+ User.objects.exclude(email__isnull=True)
25
+ .exclude(email="")
26
+ .values_list("email", flat=True)
27
+ )
28
+
29
+ if not emails:
30
+ logging.warning("No users with valid emails found.")
31
+ return 0
32
+
33
+ send_emails(recipe_id, emails)
34
+
35
+ return True
36
+
37
+
38
+ def send_emails(recipe_id, emails):
39
+ from .models import Recipe
40
+
41
+ logging.debug(f"Preparing to send email to: {emails}")
42
+ recipe = Recipe.objects.get(pk=recipe_id) # ty:ignore[unresolved-attribute]
43
+ from_email = getattr(settings, "EMAIL_FROM_ADDRESS")
44
+
45
+ recipe_slug = recipe.get_absolute_url()
46
+ base_url = (
47
+ settings.CSRF_TRUSTED_ORIGINS[0]
48
+ if settings.CSRF_TRUSTED_ORIGINS
49
+ else "http://localhost"
50
+ ).rstrip("/")
51
+
52
+ raw_message_fmt = _("""
53
+ Hungry? We just added <strong>%(title)s</strong> to our collection.
54
+
55
+ It's a delicious recipe that you won't want to miss!
56
+ %(description)s
57
+
58
+ Check out the full recipe, ingredients, and steps here:
59
+ %(url)s
60
+
61
+ Happy Cooking!
62
+
63
+ The Sandwitches Team
64
+ """)
65
+
66
+ context_data = {
67
+ "title": recipe.title,
68
+ "uploaded_by": recipe.uploaded_by,
69
+ "description": recipe.description,
70
+ "url": f"{base_url}{recipe_slug}",
71
+ "image_url": f"{base_url}{recipe.image.url}" if recipe.image else "",
72
+ }
73
+
74
+ wrapped_message = textwrap.fill(
75
+ textwrap.dedent(raw_message_fmt) % context_data, width=70
76
+ )
77
+
78
+ html_content_fmt = _("""
79
+ <div style="font-family: 'Helvetica', sans-serif; max-width: 600px; margin: auto; border: 1px solid #eee; padding: 20px;">
80
+ <h2 style="color: #d35400; text-align: center;">New Recipe: %(title)s by %(uploaded_by)s</h2>
81
+ <div style="text-align: center; margin: 20px 0;">
82
+ <img src="%(image_url)s" alt="%(title)s" style="width: 100%%; border-radius: 8px;">
83
+ </div>
84
+ <p style="font-size: 16px; line-height: 1.5; color: #333;">
85
+ Hungry? We just added <strong>%(title)s</strong> to our collection.
86
+ <br>
87
+ It's a delicious recipe that you won't want to miss!
88
+ <br>
89
+ %(description)s
90
+ <br>
91
+ Check out the full recipe, ingredients, and steps here:
92
+ Click the button below to see how to make it!
93
+ <br>
94
+ Happy Cooking!
95
+ <br>
96
+ The Sandwitches Team
97
+ </p>
98
+ <div style="text-align: center; margin-top: 30px;">
99
+ <a href="%(url)s" style="background-color: #e67e22; color: white; padding: 12px 25px; text-decoration: none; border-radius: 5px; font-weight: bold;">VIEW RECIPE</a>
100
+ </div>
101
+ </div>
102
+ """)
103
+
104
+ html_content = html_content_fmt % context_data
105
+
106
+ subject = _("Sandwitches - New Recipe: %(title)s by %(uploaded_by)s") % context_data
107
+ for email in emails:
108
+ msg = EmailMultiAlternatives(
109
+ subject=subject,
110
+ body=wrapped_message,
111
+ from_email=from_email,
112
+ to=[email],
113
+ )
114
+ msg.attach_alternative(html_content, "text/html")
115
+ msg.send()
@@ -0,0 +1,118 @@
1
+ {% extends "base.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block extra_head %}
5
+ <link href="https://cdn.jsdelivr.net/npm/beercss@3.7.12/dist/cdn/beer.min.css" rel="stylesheet">
6
+ <script type="module" src="https://cdn.jsdelivr.net/npm/beercss@3.7.12/dist/cdn/beer.min.js"></script>
7
+ <script type="module" src="https://cdn.jsdelivr.net/npm/material-dynamic-colors@1.1.2/dist/cdn/material-dynamic-colors.min.js"></script>
8
+ <script src="https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"></script>
9
+ <link rel="icon" type="image/svg+xml" href="{% static "icons/favicon.svg" %}">
10
+ <style>
11
+ main.container {
12
+ padding-top: 2rem;
13
+ padding-bottom: 2rem;
14
+ }
15
+ .admin-thumb {
16
+ width: 80px;
17
+ height: 80px;
18
+ object-fit: cover;
19
+ }
20
+ </style>
21
+ {% endblock %}
22
+
23
+ {% block navbar %}
24
+ <nav class="top primary-container">
25
+ <button class="circle transparent" data-ui="#admin-menu">
26
+ <i>menu</i>
27
+ </button>
28
+ <a href="{% url 'admin_dashboard' %}" class="row align-center">
29
+ <img src="{% static 'icons/icon.svg' %}" class="circle small">
30
+ <h6 class="max">{% block admin_title %}{% trans "Sandwitches Admin" %}{% endblock %}</h6>
31
+ </a>
32
+ <div class="max"></div>
33
+ <button class="circle transparent" onclick="toggleMode()">
34
+ <i>dark_mode</i>
35
+ </button>
36
+
37
+ {% if user.avatar %}
38
+ <img src="{{ user.avatar.url }}" class="circle" data-ui="#user-menu">
39
+ {% else %}
40
+ <img src="https://www.w3schools.com/howto/img_avatar.png" class="circle" data-ui="#user-menu">
41
+ {% endif %}
42
+ </nav>
43
+
44
+ <dialog id="admin-menu" class="left">
45
+ <nav class="drawer vertical">
46
+ <header>
47
+ <img src="{% static 'icons/icon.svg' %}" class="circle large">
48
+ <h5 class="max">{% trans "Admin" %}</h5>
49
+ </header>
50
+ <div class="space"></div>
51
+ <a href="{% url 'admin_dashboard' %}" class="{% if request.resolver_match.url_name == 'admin_dashboard' %}active{% endif %}">
52
+ <i>dashboard</i>
53
+ <span>{% trans "Dashboard" %}</span>
54
+ </a>
55
+ <a href="{% url 'admin_recipe_list' %}" class="{% if request.resolver_match.url_name == 'admin_recipe_list' %}active{% endif %}">
56
+ <i>restaurant</i>
57
+ <span>{% trans "Recipes" %}</span>
58
+ </a>
59
+ <a href="{% url 'admin_user_list' %}" class="{% if request.resolver_match.url_name == 'admin_user_list' %}active{% endif %}">
60
+ <i>people</i>
61
+ <span>{% trans "Users" %}</span>
62
+ </a>
63
+ <a href="{% url 'admin_tag_list' %}" class="{% if request.resolver_match.url_name == 'admin_tag_list' %}active{% endif %}">
64
+ <i>label</i>
65
+ <span>{% trans "Tags" %}</span>
66
+ </a>
67
+ <a href="{% url 'admin_rating_list' %}" class="{% if request.resolver_match.url_name == 'admin_rating_list' %}active{% endif %}">
68
+ <i>star</i>
69
+ <span>{% trans "Ratings" %}</span>
70
+ </a>
71
+ <a href="{% url 'admin_task_list' %}" class="{% if request.resolver_match.url_name == 'admin_task_list' %}active{% endif %}">
72
+ <i>assignment</i>
73
+ <span>{% trans "Tasks" %}</span>
74
+ </a>
75
+ <div class="divider"></div>
76
+ <a href="{% url 'index' %}">
77
+ <i>home</i>
78
+ <span>{% trans "Public Site" %}</span>
79
+ </a>
80
+ </nav>
81
+ </dialog>
82
+
83
+ {% if user.is_authenticated %}
84
+ <menu id="user-menu" class="no-wrap left">
85
+ <a href="{% url 'admin:logout' %}" class="row"><i>logout</i>{% trans "Logout" %}</a>
86
+ </menu>
87
+ {% endif %}
88
+
89
+ {% if messages %}
90
+ <div class="padding">
91
+ {% for message in messages %}
92
+ <div class="snackbar active {% if message.tags == 'error' %}error{% else %}primary{% endif %}">
93
+ <i>{% if message.tags == 'error' %}error{% else %}info{% endif %}</i>
94
+ <span>{{ message }}</span>
95
+ </div>
96
+ {% endfor %}
97
+ </div>
98
+ {% endif %}
99
+ {% endblock %}
100
+
101
+ {% block footer %}
102
+ <footer class="padding">
103
+ <div class="row align-center">
104
+ <div class="max">
105
+ <h6 class="small">Sandwitches Admin</h6>
106
+ <div class="small-text">© {% now "Y" %} Sandwitches Inc.</div>
107
+ </div>
108
+ <nav>
109
+ version {{ version }}
110
+ </nav>
111
+ </div>
112
+ </footer>
113
+ {% endblock %}
114
+
115
+ {% block extra_scripts %}
116
+ {{ block.super }}
117
+ {% block admin_scripts %}{% endblock %}
118
+ {% endblock %}
@@ -0,0 +1,23 @@
1
+ {% extends "admin/admin_base.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block admin_title %}{% trans "Confirm Delete" %}{% endblock %}
5
+
6
+ {% block content %}
7
+ <article class="round padding center-align">
8
+ <i class="extra error-text">warning</i>
9
+ <h5>{% trans "Are you sure?" %}</h5>
10
+ <p>{% trans "You are about to delete the following" %} {{ type }}: <b>{{ object }}</b></p>
11
+ <p>{% trans "This action cannot be undone." %}</p>
12
+
13
+ <div class="space"></div>
14
+
15
+ <form method="post">
16
+ {% csrf_token %}
17
+ <nav class="center-align">
18
+ <button type="button" class="button transparent" onclick="history.back()">{% trans "Cancel" %}</button>
19
+ <button type="submit" class="button error round">{% trans "Yes, Delete" %}</button>
20
+ </nav>
21
+ </form>
22
+ </article>
23
+ {% endblock %}