sandwitches 2.3.3__tar.gz → 2.4.0__tar.gz

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 (82) hide show
  1. {sandwitches-2.3.3 → sandwitches-2.4.0}/PKG-INFO +1 -1
  2. {sandwitches-2.3.3 → sandwitches-2.4.0}/pyproject.toml +1 -1
  3. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/forms.py +1 -1
  4. sandwitches-2.4.0/src/sandwitches/migrations/0012_rename_is_community_made_historicalrecipe_is_approved_and_more.py +22 -0
  5. sandwitches-2.4.0/src/sandwitches/migrations/0013_cartitem.py +55 -0
  6. sandwitches-2.4.0/src/sandwitches/migrations/0014_ensure_groups_exist.py +22 -0
  7. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/models.py +27 -1
  8. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/admin_base.html +4 -0
  9. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/dashboard.html +1 -0
  10. sandwitches-2.4.0/src/sandwitches/templates/admin/recipe_approval_list.html +56 -0
  11. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/recipe_form.html +2 -2
  12. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/recipe_list.html +1 -1
  13. sandwitches-2.4.0/src/sandwitches/templates/cart.html +102 -0
  14. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/navbar.html +6 -0
  15. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/recipe_header.html +3 -3
  16. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/partials/recipe_list.html +11 -3
  17. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/urls.py +12 -0
  18. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/views.py +158 -16
  19. {sandwitches-2.3.3 → sandwitches-2.4.0}/README.md +0 -0
  20. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/__init__.py +0 -0
  21. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/admin.py +0 -0
  22. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/api.py +0 -0
  23. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/asgi.py +0 -0
  24. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/feeds.py +0 -0
  25. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/locale/nl/LC_MESSAGES/django.mo +0 -0
  26. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/locale/nl/LC_MESSAGES/django.po +0 -0
  27. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/management/__init__.py +0 -0
  28. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/management/commands/__init__.py +0 -0
  29. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/management/commands/reset_daily_orders.py +0 -0
  30. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0001_initial.py +0 -0
  31. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0002_historicalrecipe_servings_recipe_servings.py +0 -0
  32. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0003_setting.py +0 -0
  33. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0004_alter_setting_ai_api_key_and_more.py +0 -0
  34. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0005_rating_comment.py +0 -0
  35. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0006_historicalrecipe_is_highlighted_and_more.py +0 -0
  36. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0007_historicalrecipe_price_recipe_price_order.py +0 -0
  37. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py +0 -0
  38. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py +0 -0
  39. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0010_rename_is_approved_historicalrecipe_is_community_made_and_more.py +0 -0
  40. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py +0 -0
  41. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/migrations/__init__.py +0 -0
  42. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/settings.py +0 -0
  43. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/storage.py +0 -0
  44. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/tasks.py +0 -0
  45. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/confirm_delete.html +0 -0
  46. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/order_list.html +0 -0
  47. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/partials/dashboard_charts.html +0 -0
  48. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/partials/order_rows.html +0 -0
  49. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/rating_list.html +0 -0
  50. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/tag_form.html +0 -0
  51. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/tag_list.html +0 -0
  52. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/task_detail.html +0 -0
  53. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/task_list.html +0 -0
  54. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/user_form.html +0 -0
  55. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/admin/user_list.html +0 -0
  56. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/base.html +0 -0
  57. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/base_beer.html +0 -0
  58. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/community.html +0 -0
  59. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/carousel_scripts.html +0 -0
  60. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/favorites_search_form.html +0 -0
  61. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/footer.html +0 -0
  62. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/ingredients_scripts.html +0 -0
  63. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/ingredients_section.html +0 -0
  64. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/instructions_section.html +0 -0
  65. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/language_dialog.html +0 -0
  66. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/rating_section.html +0 -0
  67. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/search_form.html +0 -0
  68. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/search_scripts.html +0 -0
  69. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/side_menu.html +0 -0
  70. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/components/user_menu.html +0 -0
  71. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/detail.html +0 -0
  72. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/favorites.html +0 -0
  73. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/index.html +0 -0
  74. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/login.html +0 -0
  75. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/profile.html +0 -0
  76. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/setup.html +0 -0
  77. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templates/signup.html +0 -0
  78. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templatetags/__init__.py +0 -0
  79. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templatetags/custom_filters.py +0 -0
  80. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/templatetags/markdown_extras.py +0 -0
  81. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/utils.py +0 -0
  82. {sandwitches-2.3.3 → sandwitches-2.4.0}/src/sandwitches/wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sandwitches
3
- Version: 2.3.3
3
+ Version: 2.4.0
4
4
  Summary: Add your description here
5
5
  Author: Martyn van Dijke
6
6
  Author-email: Martyn van Dijke <martijnvdijke600@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sandwitches"
3
- version = "2.3.3"
3
+ version = "2.4.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -138,7 +138,7 @@ class RecipeForm(forms.ModelForm):
138
138
  "instructions",
139
139
  "price",
140
140
  "is_highlighted",
141
- "is_community_made",
141
+ "is_approved",
142
142
  "max_daily_orders",
143
143
  ]
144
144
  widgets = {
@@ -0,0 +1,22 @@
1
+ # Generated by Django 6.0.1 on 2026-01-25 10:53
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("sandwitches", "0011_alter_historicalrecipe_is_community_made_and_more"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.RenameField(
13
+ model_name="historicalrecipe",
14
+ old_name="is_community_made",
15
+ new_name="is_approved",
16
+ ),
17
+ migrations.RenameField(
18
+ model_name="recipe",
19
+ old_name="is_community_made",
20
+ new_name="is_approved",
21
+ ),
22
+ ]
@@ -0,0 +1,55 @@
1
+ # Generated by Django 6.0.1 on 2026-01-25 11:12
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ (
11
+ "sandwitches",
12
+ "0012_rename_is_community_made_historicalrecipe_is_approved_and_more",
13
+ ),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="CartItem",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ ("quantity", models.PositiveIntegerField(default=1)),
30
+ ("created_at", models.DateTimeField(auto_now_add=True)),
31
+ ("updated_at", models.DateTimeField(auto_now=True)),
32
+ (
33
+ "recipe",
34
+ models.ForeignKey(
35
+ on_delete=django.db.models.deletion.CASCADE,
36
+ related_name="cart_items",
37
+ to="sandwitches.recipe",
38
+ ),
39
+ ),
40
+ (
41
+ "user",
42
+ models.ForeignKey(
43
+ on_delete=django.db.models.deletion.CASCADE,
44
+ related_name="cart_items",
45
+ to=settings.AUTH_USER_MODEL,
46
+ ),
47
+ ),
48
+ ],
49
+ options={
50
+ "verbose_name": "Cart Item",
51
+ "verbose_name_plural": "Cart Items",
52
+ "unique_together": {("user", "recipe")},
53
+ },
54
+ ),
55
+ ]
@@ -0,0 +1,22 @@
1
+ from django.db import migrations
2
+
3
+
4
+ def create_groups(apps, schema_editor):
5
+ Group = apps.get_model("auth", "Group")
6
+ Group.objects.get_or_create(name="admin")
7
+ Group.objects.get_or_create(name="community")
8
+
9
+
10
+ def remove_groups(apps, schema_editor):
11
+ Group = apps.get_model("auth", "Group")
12
+ Group.objects.filter(name__in=["admin", "community"]).delete()
13
+
14
+
15
+ class Migration(migrations.Migration):
16
+ dependencies = [
17
+ ("sandwitches", "0013_cartitem"),
18
+ ]
19
+
20
+ operations = [
21
+ migrations.RunPython(create_groups, remove_groups),
22
+ ]
@@ -132,7 +132,7 @@ class Recipe(models.Model):
132
132
  )
133
133
  tags = models.ManyToManyField(Tag, blank=True, related_name="recipes")
134
134
  is_highlighted = models.BooleanField(default=False)
135
- is_community_made = models.BooleanField(default=False)
135
+ is_approved = models.BooleanField(default=False)
136
136
  max_daily_orders = models.PositiveIntegerField(
137
137
  null=True, blank=True, verbose_name="Max daily orders"
138
138
  )
@@ -278,3 +278,29 @@ class Order(models.Model):
278
278
 
279
279
  def __str__(self):
280
280
  return f"Order #{self.pk} - {self.user} - {self.recipe}"
281
+
282
+
283
+ class CartItem(models.Model):
284
+ user = models.ForeignKey(
285
+ settings.AUTH_USER_MODEL, related_name="cart_items", on_delete=models.CASCADE
286
+ )
287
+ recipe = models.ForeignKey(
288
+ Recipe, related_name="cart_items", on_delete=models.CASCADE
289
+ )
290
+ quantity = models.PositiveIntegerField(default=1)
291
+ created_at = models.DateTimeField(auto_now_add=True)
292
+ updated_at = models.DateTimeField(auto_now=True)
293
+
294
+ class Meta:
295
+ unique_together = ("user", "recipe")
296
+ verbose_name = "Cart Item"
297
+ verbose_name_plural = "Cart Items"
298
+
299
+ def __str__(self):
300
+ return f"{self.user.username}'s cart: {self.recipe.title} (x{self.quantity})"
301
+
302
+ @property
303
+ def total_price(self):
304
+ if self.recipe.price:
305
+ return self.recipe.price * self.quantity
306
+ return 0
@@ -56,6 +56,10 @@
56
56
  <i>restaurant</i>
57
57
  <span>{% trans "Recipes" %}</span>
58
58
  </a>
59
+ <a href="{% url 'admin_recipe_approval_list' %}" class="{% if request.resolver_match.url_name == 'admin_recipe_approval_list' %}active{% endif %}">
60
+ <i>how_to_reg</i>
61
+ <span>{% trans "Approvals" %}</span>
62
+ </a>
59
63
  <a href="{% url 'admin_user_list' %}" class="{% if request.resolver_match.url_name == 'admin_user_list' %}active{% endif %}">
60
64
  <i>people</i>
61
65
  <span>{% trans "Users" %}</span>
@@ -7,6 +7,7 @@
7
7
  {{ block.super }}
8
8
  <link href="{% static "dist/main.css" %}" rel="stylesheet">
9
9
  <script src="{% static "dist/main.js" %}" defer></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><!-- Chart.js TODO -->
10
11
  {% endblock %}
11
12
 
12
13
  {% block content %}
@@ -0,0 +1,56 @@
1
+ {% extends "admin/admin_base.html" %}
2
+ {% load i18n %}
3
+
4
+ {% block admin_title %}{% trans "Pending Approvals" %}{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="row align-center mb-2">
8
+ <h5 class="max bold">{% trans "Recipes Awaiting Approval" %}</h5>
9
+ </div>
10
+
11
+ <table class="border striped no-space">
12
+ <thead>
13
+ <tr>
14
+ <th class="min">{% trans "Image" %}</th>
15
+ <th class="max">{% trans "Title" %}</th>
16
+ <th>{% trans "Uploader" %}</th>
17
+ <th>{% trans "Created" %}</th>
18
+ <th class="right-align">{% trans "Actions" %}</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ {% for recipe in recipes %}
23
+ <tr class="pointer" onclick="location.href='{% url 'admin_recipe_edit' recipe.pk %}'">
24
+ <td class="min">
25
+ {% if recipe.image %}
26
+ <img src="{{ recipe.image_thumbnail.url }}" class="admin-thumb round">
27
+ {% else %}
28
+ <div class="admin-thumb round gray1 middle-align center-align">
29
+ <i class="extra">restaurant</i>
30
+ </div>
31
+ {% endif %}
32
+ </td>
33
+ <td class="max">
34
+ <b>{{ recipe.title }}</b>
35
+ </td>
36
+ <td>{{ recipe.uploaded_by.username|default:"-" }}</td>
37
+ <td>{{ recipe.created_at|date:"SHORT_DATETIME_FORMAT" }}</td>
38
+ <td class="right-align">
39
+ <a href="{% url 'admin_recipe_approve' recipe.pk %}" class="button tiny primary round" onclick="event.stopPropagation();">
40
+ <i>check</i>
41
+ <span>{% trans "Approve" %}</span>
42
+ </a>
43
+ <a href="{% url 'admin_recipe_edit' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();" title="{% trans 'Edit' %}"><i>edit</i></a>
44
+ <a href="{% url 'admin_recipe_delete' recipe.pk %}" class="button circle transparent" onclick="event.stopPropagation();" title="{% trans 'Delete' %}"><i>delete</i></a>
45
+ </td>
46
+ </tr>
47
+ {% empty %}
48
+ <tr>
49
+ <td colspan="5" class="center-align padding">
50
+ {% trans "No recipes pending approval." %}
51
+ </td>
52
+ </tr>
53
+ {% endfor %}
54
+ </tbody>
55
+ </table>
56
+ {% endblock %}
@@ -82,8 +82,8 @@
82
82
 
83
83
  <div class="field middle-align mt-1">
84
84
  <label class="checkbox">
85
- {{ form.is_community_made }}
86
- <span>{{ form.is_community_made.label }}</span>
85
+ {{ form.is_approved }}
86
+ <span>{{ form.is_approved.label }}</span>
87
87
  </label>
88
88
  </div>
89
89
  </article>
@@ -88,7 +88,7 @@
88
88
  </div>
89
89
  </td>
90
90
  <td class="min center-align">
91
- {% if recipe.is_community_made %}
91
+ {% if recipe.is_approved %}
92
92
  <i class="primary-text">check_circle</i>
93
93
  {% else %}
94
94
  <a href="{% url 'admin_recipe_approve' recipe.pk %}" class="button tiny primary round" onclick="event.stopPropagation();">
@@ -0,0 +1,102 @@
1
+ {% extends "base_beer.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block title %}{% trans "Your Cart" %}{% endblock %}
5
+
6
+ {% block content %}
7
+ <main class="responsive">
8
+ <div class="large-space"></div>
9
+ <div class="row align-center">
10
+ <i class="extra">shopping_cart</i>
11
+ <h4 class="max">{% trans "Your Shopping Cart" %}</h4>
12
+ </div>
13
+ <div class="space"></div>
14
+
15
+ {% if cart_items %}
16
+ <div class="grid">
17
+ <div class="s12 m8">
18
+ <div class="padding border round surface">
19
+ <table class="border striped no-space">
20
+ <thead>
21
+ <tr>
22
+ <th class="min">{% trans "Item" %}</th>
23
+ <th class="max">{% trans "Description" %}</th>
24
+ <th class="min">{% trans "Quantity" %}</th>
25
+ <th class="min">{% trans "Price" %}</th>
26
+ <th class="min"></th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {% for item in cart_items %}
31
+ <tr>
32
+ <td class="min">
33
+ {% if item.recipe.image %}
34
+ <img src="{{ item.recipe.image_thumbnail.url }}" class="circle small">
35
+ {% else %}
36
+ <i class="circle small gray1">restaurant</i>
37
+ {% endif %}
38
+ </td>
39
+ <td class="max">
40
+ <a href="{% url 'recipe_detail' item.recipe.slug %}" class="bold">{{ item.recipe.title }}</a>
41
+ </td>
42
+ <td class="min">
43
+ <form action="{% url 'update_cart_quantity' item.pk %}" method="post" class="row no-space align-center">
44
+ {% csrf_token %}
45
+ <div class="field border round small no-margin" style="width: 80px;">
46
+ <input type="number" name="quantity" value="{{ item.quantity }}" min="1" onchange="this.form.submit()">
47
+ </div>
48
+ </form>
49
+ </td>
50
+ <td class="min no-wrap">{{ item.total_price }} €</td>
51
+ <td class="min">
52
+ <a href="{% url 'remove_from_cart' item.pk %}" class="button circle transparent">
53
+ <i>delete</i>
54
+ </a>
55
+ </td>
56
+ </tr>
57
+ {% endfor %}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ </div>
62
+ <div class="s12 m4">
63
+ <article class="round padding border primary-container">
64
+ <h6 class="bold">{% trans "Summary" %}</h6>
65
+ <div class="row mt-1">
66
+ <div class="max">{% trans "Subtotal" %}</div>
67
+ <div class="bold">{{ total }} €</div>
68
+ </div>
69
+ <div class="divider mt-1 mb-1"></div>
70
+ <div class="row">
71
+ <div class="max bold">{% trans "Total" %}</div>
72
+ <div class="bold large-text">{{ total }} €</div>
73
+ </div>
74
+ <div class="space"></div>
75
+ <form action="{% url 'checkout_cart' %}" method="post">
76
+ {% csrf_token %}
77
+ <button type="submit" class="button primary round extend">
78
+ <i>check</i>
79
+ <span>{% trans "Checkout" %}</span>
80
+ </button>
81
+ </form>
82
+ </article>
83
+ <div class="space"></div>
84
+ <a href="{% url 'index' %}" class="button transparent extend">
85
+ <i>arrow_back</i>
86
+ <span>{% trans "Continue Shopping" %}</span>
87
+ </a>
88
+ </div>
89
+ </div>
90
+ {% else %}
91
+ <div class="center-align padding">
92
+ <i class="extra gray-text">shopping_cart_off</i>
93
+ <h5 class="gray-text">{% trans "Your cart is empty." %}</h5>
94
+ <div class="space"></div>
95
+ <a href="{% url 'index' %}" class="button primary round">
96
+ <span>{% trans "Start Shopping" %}</span>
97
+ </a>
98
+ </div>
99
+ {% endif %}
100
+ <div class="large-space"></div>
101
+ </main>
102
+ {% endblock %}
@@ -15,6 +15,12 @@
15
15
  </button>
16
16
 
17
17
  {% if user.is_authenticated %}
18
+ <a href="{% url 'view_cart' %}" class="button circle transparent">
19
+ <i>shopping_cart</i>
20
+ {% if user.cart_items.exists %}
21
+ <badge class="red">{{ user.cart_items.count }}</badge>
22
+ {% endif %}
23
+ </a>
18
24
  <a href="{% url 'user_profile' %}">
19
25
  {% if user.avatar %}
20
26
  <img src="{{ user.avatar.url }}" class="circle">
@@ -37,11 +37,11 @@
37
37
  <i class="primary-text">euro_symbol</i>
38
38
  <h5 class="bold ml-1">{{ recipe.price }}</h5>
39
39
  {% if user.is_authenticated %}
40
- <form action="{% url 'order_recipe' recipe.pk %}" method="post" class="ml-2">
40
+ <form action="{% url 'add_to_cart' recipe.pk %}" method="post" class="ml-2">
41
41
  {% csrf_token %}
42
42
  <button type="submit" class="button primary round">
43
- <i>shopping_cart</i>
44
- <span>{% trans "Order Now" %}</span>
43
+ <i>add_shopping_cart</i>
44
+ <span>{% trans "Add to Cart" %}</span>
45
45
  </button>
46
46
  </form>
47
47
  {% endif %}
@@ -72,11 +72,19 @@
72
72
  </div>
73
73
  </div>
74
74
 
75
- <div class="padding pt-0">
76
- <a href="{% url 'recipe_detail' recipe.slug %}" class="button fill round responsive">
77
- <span>{% trans 'View Recipe' %}</span>
75
+ <div class="padding pt-0 row no-space">
76
+ <a href="{% url 'recipe_detail' recipe.slug %}" class="button fill round max">
77
+ <span>{% trans 'View' %}</span>
78
78
  <i class="suffix">arrow_forward</i>
79
79
  </a>
80
+ {% if user.is_authenticated and recipe.price %}
81
+ <form action="{% url 'add_to_cart' recipe.pk %}" method="post" class="ml-1">
82
+ {% csrf_token %}
83
+ <button type="submit" class="button circle primary" title="{% trans 'Add to Cart' %}">
84
+ <i>add_shopping_cart</i>
85
+ </button>
86
+ </form>
87
+ {% endif %}
80
88
  </div>
81
89
  </article>
82
90
  </div>
@@ -39,6 +39,13 @@ urlpatterns = [
39
39
  path("api/", api.urls),
40
40
  path("media/<path:file_path>", views.media, name="media"),
41
41
  path("favorites/", views.favorites, name="favorites"),
42
+ path("cart/", views.view_cart, name="view_cart"),
43
+ path("cart/add/<int:pk>/", views.add_to_cart, name="add_to_cart"),
44
+ path("cart/remove/<int:pk>/", views.remove_from_cart, name="remove_from_cart"),
45
+ path(
46
+ "cart/update/<int:pk>/", views.update_cart_quantity, name="update_cart_quantity"
47
+ ),
48
+ path("cart/checkout/", views.checkout_cart, name="checkout_cart"),
42
49
  path("", views.index, name="index"),
43
50
  path("feeds/latest/", LatestRecipesFeed(), name="latest_recipes_feed"),
44
51
  path(
@@ -54,6 +61,11 @@ urlpatterns += i18n_patterns(
54
61
  path("recipes/<int:pk>/favorite/", views.toggle_favorite, name="toggle_favorite"),
55
62
  path("dashboard/", views.admin_dashboard, name="admin_dashboard"),
56
63
  path("dashboard/recipes/", views.admin_recipe_list, name="admin_recipe_list"),
64
+ path(
65
+ "dashboard/approvals/",
66
+ views.admin_recipe_approval_list,
67
+ name="admin_recipe_approval_list",
68
+ ),
57
69
  path("dashboard/recipes/add/", views.admin_recipe_add, name="admin_recipe_add"),
58
70
  path(
59
71
  "dashboard/recipes/<int:pk>/edit/",
@@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model
8
8
  from django.contrib.auth.decorators import login_required
9
9
  from django.contrib.admin.views.decorators import staff_member_required
10
10
  from django.utils.translation import gettext as _
11
- from .models import Recipe, Rating, Tag, Order
11
+ from .models import Recipe, Rating, Tag, Order, CartItem
12
12
  from .forms import (
13
13
  RecipeForm,
14
14
  AdminSetupForm,
@@ -42,7 +42,7 @@ def community(request):
42
42
  if form.is_valid():
43
43
  recipe = form.save(commit=False)
44
44
  recipe.uploaded_by = request.user
45
- recipe.is_community_made = True
45
+ recipe.is_approved = False
46
46
  recipe.save()
47
47
  form.save_m2m()
48
48
  messages.success(
@@ -53,16 +53,17 @@ def community(request):
53
53
  else:
54
54
  form = UserRecipeSubmissionForm()
55
55
 
56
- # Community recipes = non-staff uploaded
57
- recipes = Recipe.objects.filter(is_community_made=True).prefetch_related( # ty:ignore[unresolved-attribute]
58
- "favorited_by"
59
- )
56
+ # Community recipes = uploaded by users in 'community' group
57
+ recipes = Recipe.objects.filter( # ty:ignore[unresolved-attribute]
58
+ uploaded_by__groups__name="community"
59
+ ).prefetch_related("favorited_by")
60
60
 
61
- if not request.user.is_staff:
61
+ if not (request.user.is_staff or request.user.groups.filter(name="admin").exists()):
62
62
  # Regular users only see approved community recipes or their own
63
- recipes = recipes.filter(
64
- Q(is_community_made=True) | Q(uploaded_by=request.user)
65
- )
63
+ recipes = recipes.filter(Q(is_approved=True) | Q(uploaded_by=request.user))
64
+ else:
65
+ # Admins see all community recipes
66
+ pass
66
67
 
67
68
  recipes = recipes.order_by("-created_at")
68
69
 
@@ -155,7 +156,7 @@ def admin_dashboard(request):
155
156
  order_counts = [d["count"] for d in order_data]
156
157
 
157
158
  pending_recipes = Recipe.objects.filter( # ty:ignore[unresolved-attribute]
158
- uploaded_by__is_staff=False, uploaded_by__is_superuser=False
159
+ is_approved=False, uploaded_by__groups__name="community"
159
160
  ).order_by("-created_at")
160
161
  context = {
161
162
  "recipe_count": recipe_count,
@@ -220,6 +221,21 @@ def admin_recipe_list(request):
220
221
  )
221
222
 
222
223
 
224
+ @staff_member_required
225
+ def admin_recipe_approval_list(request):
226
+ recipes = Recipe.objects.filter( # ty:ignore[unresolved-attribute]
227
+ is_approved=False, uploaded_by__groups__name="community"
228
+ ).order_by("-created_at")
229
+ return render(
230
+ request,
231
+ "admin/recipe_approval_list.html",
232
+ {
233
+ "recipes": recipes,
234
+ "version": sandwitches_version,
235
+ },
236
+ )
237
+
238
+
223
239
  @staff_member_required
224
240
  def admin_recipe_add(request):
225
241
  if request.method == "POST":
@@ -266,11 +282,14 @@ def admin_recipe_edit(request, pk):
266
282
  @staff_member_required
267
283
  def admin_recipe_approve(request, pk):
268
284
  recipe = get_object_or_404(Recipe, pk=pk)
269
- recipe.is_community_made = True
285
+ recipe.is_approved = True
270
286
  recipe.save()
271
287
  messages.success(
272
288
  request, _("Recipe '%(title)s' approved.") % {"title": recipe.title}
273
289
  )
290
+ referer = request.META.get("HTTP_REFERER")
291
+ if referer and "dashboard/approvals" in referer:
292
+ return redirect("admin_recipe_approval_list")
274
293
  return redirect("admin_recipe_list")
275
294
 
276
295
 
@@ -494,10 +513,19 @@ def admin_order_list(request):
494
513
  def recipe_detail(request, slug):
495
514
  recipe = get_object_or_404(Recipe, slug=slug)
496
515
 
497
- if not recipe.is_community_made:
516
+ # If it's a community recipe, it must be approved or viewed by staff/owner
517
+ is_community = (
518
+ recipe.uploaded_by
519
+ and recipe.uploaded_by.groups.filter(name="community").exists()
520
+ )
521
+ if is_community and not recipe.is_approved:
498
522
  if not (
499
523
  request.user.is_authenticated
500
- and (request.user.is_staff or recipe.uploaded_by == request.user)
524
+ and (
525
+ request.user.is_staff
526
+ or recipe.uploaded_by == request.user
527
+ or request.user.groups.filter(name="admin").exists()
528
+ )
501
529
  ):
502
530
  raise Http404("Recipe not found or pending approval.")
503
531
 
@@ -668,8 +696,8 @@ def index(request):
668
696
 
669
697
  recipes = Recipe.objects.all().prefetch_related("favorited_by") # ty:ignore[unresolved-attribute]
670
698
 
671
- # Only show "normal" recipes (uploaded by staff or no uploader)
672
- recipes = recipes.filter(Q(is_community_made=False))
699
+ # Only show recipes from people in the admin group
700
+ recipes = recipes.filter(uploaded_by__groups__name="admin")
673
701
 
674
702
  # Filtering
675
703
  q = request.GET.get("q")
@@ -743,6 +771,8 @@ def setup(request):
743
771
  First-time setup page: create initial superuser if none exists.
744
772
  Visible only while there are no superusers in the DB.
745
773
  """
774
+ from django.contrib.auth.models import Group
775
+
746
776
  # do not allow access if a superuser already exists
747
777
  if User.objects.filter(is_superuser=True).exists():
748
778
  return redirect("index")
@@ -751,6 +781,12 @@ def setup(request):
751
781
  form = AdminSetupForm(request.POST)
752
782
  if form.is_valid():
753
783
  user = form.save()
784
+
785
+ # Ensure groups exist and add user to admin group
786
+ admin_group, created = Group.objects.get_or_create(name="admin")
787
+ Group.objects.get_or_create(name="community")
788
+ user.groups.add(admin_group)
789
+
754
790
  user.backend = "django.contrib.auth.backends.ModelBackend"
755
791
  login(request, user)
756
792
  messages.success(request, _("Admin account created and signed in."))
@@ -765,10 +801,17 @@ def signup(request):
765
801
  """
766
802
  User signup page: create new regular user accounts.
767
803
  """
804
+ from django.contrib.auth.models import Group
805
+
768
806
  if request.method == "POST":
769
807
  form = UserSignupForm(request.POST, request.FILES)
770
808
  if form.is_valid():
771
809
  user = form.save()
810
+
811
+ # Add user to community group
812
+ community_group, created = Group.objects.get_or_create(name="community")
813
+ user.groups.add(community_group)
814
+
772
815
  # log in the newly created user
773
816
  user.backend = "django.contrib.auth.backends.ModelBackend"
774
817
  login(request, user)
@@ -818,3 +861,102 @@ def user_profile(request):
818
861
  return render(
819
862
  request, "profile.html", {"form": form, "version": sandwitches_version}
820
863
  )
864
+
865
+
866
+ @login_required
867
+ def view_cart(request):
868
+ cart_items = CartItem.objects.filter(user=request.user).select_related("recipe") # ty:ignore[unresolved-attribute]
869
+ total = sum(item.total_price for item in cart_items)
870
+ return render(
871
+ request,
872
+ "cart.html",
873
+ {
874
+ "cart_items": cart_items,
875
+ "total": total,
876
+ "version": sandwitches_version,
877
+ },
878
+ )
879
+
880
+
881
+ @login_required
882
+ def add_to_cart(request, pk):
883
+ recipe = get_object_or_404(Recipe, pk=pk)
884
+ if not recipe.price:
885
+ messages.error(request, _("This recipe cannot be ordered (no price set)."))
886
+ return redirect("recipe_detail", slug=recipe.slug)
887
+
888
+ cart_item, created = CartItem.objects.get_or_create( # ty:ignore[unresolved-attribute]
889
+ user=request.user, recipe=recipe
890
+ )
891
+ if not created:
892
+ cart_item.quantity += 1
893
+ cart_item.save()
894
+
895
+ messages.success(
896
+ request, _("Added %(title)s to your cart.") % {"title": recipe.title}
897
+ )
898
+ return redirect("view_cart")
899
+
900
+
901
+ @login_required
902
+ def remove_from_cart(request, pk):
903
+ cart_item = get_object_or_404(CartItem, pk=pk, user=request.user)
904
+ cart_item.delete()
905
+ messages.success(request, _("Removed from cart."))
906
+ return redirect("view_cart")
907
+
908
+
909
+ @login_required
910
+ def update_cart_quantity(request, pk):
911
+ if request.method == "POST":
912
+ cart_item = get_object_or_404(CartItem, pk=pk, user=request.user)
913
+ try:
914
+ quantity = int(request.POST.get("quantity", 1))
915
+ if quantity > 0:
916
+ cart_item.quantity = quantity
917
+ cart_item.save()
918
+ else:
919
+ cart_item.delete()
920
+ except ValueError:
921
+ pass
922
+ return redirect("view_cart")
923
+
924
+
925
+ @login_required
926
+ def checkout_cart(request):
927
+ cart_items = CartItem.objects.filter(user=request.user) # ty:ignore[unresolved-attribute]
928
+ if not cart_items.exists():
929
+ messages.error(request, _("Your cart is empty."))
930
+ return redirect("view_cart")
931
+
932
+ created_orders = [] # noqa: F841
933
+ errors = []
934
+
935
+ # We use a transaction to ensure either all orders are created or none if something goes wrong
936
+ from django.db import transaction
937
+
938
+ try:
939
+ with transaction.atomic():
940
+ for item in cart_items:
941
+ # Create Order for each recipe in cart (quantity times?)
942
+ # Current Order model doesn't have quantity, so we create multiple orders or update Order model.
943
+ # For now, let's create 'quantity' number of orders as per current schema
944
+ # OR we could update Order model to support quantity.
945
+ # Let's see if Order has quantity. (Checked: it does not).
946
+ for i in range(item.quantity):
947
+ try:
948
+ Order.objects.create(user=request.user, recipe=item.recipe) # ty:ignore[unresolved-attribute]
949
+ except (ValidationError, ValueError) as e:
950
+ errors.append(f"{item.recipe.title}: {str(e)}")
951
+ raise e # Trigger rollback
952
+
953
+ cart_items.delete()
954
+ messages.success(request, _("Orders submitted successfully!"))
955
+ return redirect("user_profile")
956
+ except Exception:
957
+ if errors:
958
+ for error in errors:
959
+ messages.error(request, error)
960
+ else:
961
+ messages.error(request, _("An error occurred during checkout."))
962
+ return redirect("view_cart")
File without changes