sandwitches 2.3.3__py3-none-any.whl → 2.4.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.
sandwitches/forms.py CHANGED
@@ -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
+ ]
sandwitches/models.py CHANGED
@@ -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>
sandwitches/urls.py CHANGED
@@ -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/",
sandwitches/views.py CHANGED
@@ -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")
@@ -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>
@@ -3,7 +3,7 @@ sandwitches/admin.py,sha256=-02WqE8U3rxrVCoNB7sfvtyE4v_e3pt7mFwXfUlindo,2421
3
3
  sandwitches/api.py,sha256=ruD5QeOPY-l9PvkJQiaOYoI0sRARDpqpFrFDgBxo9cQ,6389
4
4
  sandwitches/asgi.py,sha256=cygnXdXSSVspM7ZXuj47Ef6oz7HSTw4D7BPzgE2PU5w,399
5
5
  sandwitches/feeds.py,sha256=iz1d11dV0utA0ZNsB7VIAp0h8Zr5mFNSKJWHbw_j6YM,683
6
- sandwitches/forms.py,sha256=rcXAL-Dn1gY3mWIPV9X9tZpFp2z6Xrkv6k9f2YUp8OU,7158
6
+ sandwitches/forms.py,sha256=YvkSTa9h_ag_b58ToOHCQIHBa3VeHMC9RKB9F7qI-gk,7152
7
7
  sandwitches/locale/nl/LC_MESSAGES/django.mo,sha256=EzQWzIhz_Na3w9AS7F-YjB-Xv63t4sMRSAkEQ1-g32M,5965
8
8
  sandwitches/locale/nl/LC_MESSAGES/django.po,sha256=znxspEoMwkmktusZtbVrt1KG1LDUwIEi4ZEIE3XGeoI,25904
9
9
  sandwitches/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -20,20 +20,24 @@ sandwitches/migrations/0008_historicalrecipe_daily_orders_count_and_more.py,sha2
20
20
  sandwitches/migrations/0009_historicalrecipe_is_approved_recipe_is_approved.py,sha256=XP76J2_HiMOFeIU17Yu8AYXtswE-dcsNZq3lJGFgGtg,588
21
21
  sandwitches/migrations/0010_rename_is_approved_historicalrecipe_is_community_made_and_more.py,sha256=9Xv-rBRUvx5UWbr7i4BeWbllCcVkcNuC8T3sxKSdWyU,575
22
22
  sandwitches/migrations/0011_alter_historicalrecipe_is_community_made_and_more.py,sha256=O2D57bAsSwBklYqfMTWrHE3Zxj3lrk-CO9yDP8sQS0M,659
23
+ sandwitches/migrations/0012_rename_is_community_made_historicalrecipe_is_approved_and_more.py,sha256=bCDPpHmZTIW70-YeL30WhuJ2mORktkrsntKqTw0vj94,577
24
+ sandwitches/migrations/0013_cartitem.py,sha256=KYMinpnZiLHwjo7p7EdJHQExuEGC9jtpcZcbm1r7JFo,1787
25
+ sandwitches/migrations/0014_ensure_groups_exist.py,sha256=5FSA742bEQtwHZl5CWZQYIdmS8FBxMgWS079dOaOltY,564
23
26
  sandwitches/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- sandwitches/models.py,sha256=MmSP1P7Kq2ObjYTJs9AKTEVeWcD5-Pqumj1BTnmBqTc,9986
27
+ sandwitches/models.py,sha256=ptEdQP4oOvyjdhYQlTtnZD-3eIUAY-8nU17G-hmN_2I,10796
25
28
  sandwitches/settings.py,sha256=5_eQAJCAV093hnhr3XOxHekT4IF-PEJcRiTecq71_SQ,5841
26
29
  sandwitches/storage.py,sha256=ibBG6tVtArqzgEKsRimZPwsqW7i9j4WiPLLHrOJchow,3578
27
30
  sandwitches/tasks.py,sha256=YiliAT2rj0fh7hrwKq5_qWtv9AGhd5iulj_iBwZBBKg,6024
28
- sandwitches/templates/admin/admin_base.html,sha256=Mzq0A6Pl-x61gXv15XegW26X7HKHS0aGcAD-WBSwSG4,4249
31
+ sandwitches/templates/admin/admin_base.html,sha256=aXba3MKFOKhaauFf0z0fFRjPpFHEbT_fREQx31TyxAM,4497
29
32
  sandwitches/templates/admin/confirm_delete.html,sha256=HfsZI_gV8JQTKz215TYgPWBrgrFhGv1UB3N-0Hln-14,804
30
- sandwitches/templates/admin/dashboard.html,sha256=1aZd5AQ2opWjzAsP1q_d15EWZIC04NWoKSH8RTgSyTg,5832
33
+ sandwitches/templates/admin/dashboard.html,sha256=Ial8zH2odIPpstSkQmzGrasl0QxvgGhFPAGy7V5xRzY,5916
31
34
  sandwitches/templates/admin/order_list.html,sha256=eHFUn2speXaaj5_SFUG0Z0HfWVUR9-VCDRBeb8ufFb0,819
32
35
  sandwitches/templates/admin/partials/dashboard_charts.html,sha256=NYrt-LDZO4__2KDWhAYL5K_f-2Zgj0iiuaZQiRZlBWg,3639
33
36
  sandwitches/templates/admin/partials/order_rows.html,sha256=Ye35liahKbQ3rqa6fIGSTwb7seoXoqyqSw0wyNq2C_o,893
34
37
  sandwitches/templates/admin/rating_list.html,sha256=8CHAsBfKfs4izhb-IyOiDjJXqAZxFcStoRSGh4pRlgM,1365
35
- sandwitches/templates/admin/recipe_form.html,sha256=OpDnsB6WsoC45POtil54qHGn93P4A3DiccKVisM5NGM,8746
36
- sandwitches/templates/admin/recipe_list.html,sha256=cVsAm1TRP4bsE7rkcxIVsThKs3FLr5MWvebSvJoRmAo,5365
38
+ sandwitches/templates/admin/recipe_approval_list.html,sha256=M6GFYI45lAkLkvqP44cu5tDYVOeeVNklEphof1euesM,2281
39
+ sandwitches/templates/admin/recipe_form.html,sha256=23wHT4hs128xnv2nkS6AtcKzY3sblia_dGVNnaeIp5Y,8734
40
+ sandwitches/templates/admin/recipe_list.html,sha256=5fGnRIQ7JfvM3yfG-sngEIEgiPnPDkjK1Tn3nO8EDh4,5359
37
41
  sandwitches/templates/admin/tag_form.html,sha256=JRWgAl4fz_Oy-Kuo1K6Mex_CXdsHMABzzyPazthr1Kg,989
38
42
  sandwitches/templates/admin/tag_list.html,sha256=ttxwXgfdxkEs4Cmrz5RHaGmaqLd7JDmWhjv80XIQqyw,1246
39
43
  sandwitches/templates/admin/task_detail.html,sha256=dO5zHOG-yTY1ly3VPA3o3jjie0wmxw0gdddtSKpN-W8,3825
@@ -42,6 +46,7 @@ sandwitches/templates/admin/user_form.html,sha256=7_6GShLROFeJJexL1XLFUXdW9_lYF8
42
46
  sandwitches/templates/admin/user_list.html,sha256=6O1YctULY-tqJnagybJof9ERA_NL1LX_a8cAu6_aWVQ,2193
43
47
  sandwitches/templates/base.html,sha256=mwCESNirfvvdyMg2e1Siy_LA8fLH29m0aS_Jv0Qom4U,3597
44
48
  sandwitches/templates/base_beer.html,sha256=4QgU4_gu_RRMtimmRAhATDJ3mj_WANxtilQJYNgAL60,2077
49
+ sandwitches/templates/cart.html,sha256=YqmrzOLLPAXSqeXeUTrt9AwTTWOitOLTaD_k3mYYVpM,4537
45
50
  sandwitches/templates/community.html,sha256=6x-Z8E0W3Ii-d0aG7DdCJoWQM9bVKNP_NSP8fTqpo6o,5324
46
51
  sandwitches/templates/components/carousel_scripts.html,sha256=9vEL5JJv8zUUjEtsnHW-BwwXUNWqQ6w_vf6UdxgEv_I,1934
47
52
  sandwitches/templates/components/favorites_search_form.html,sha256=tpD8SpS47TUDJBwxhMuvjhTN9pjWoRGFW50TBv48Ld4,5202
@@ -50,9 +55,9 @@ sandwitches/templates/components/ingredients_scripts.html,sha256=2zKTC65GYF589uW
50
55
  sandwitches/templates/components/ingredients_section.html,sha256=XsaVXTs9MIwjfJeLjlzah3GWWj8oFU-_HJd9i9l1HAo,665
51
56
  sandwitches/templates/components/instructions_section.html,sha256=RFlA4uPiI6vf1e2QgiD5KzGoy7Vg7y7nFY7TFabCYLA,277
52
57
  sandwitches/templates/components/language_dialog.html,sha256=iz-6QhFe4f_dsVhGDhVx6KKKLgQz4grX8tbIqSQjDsg,1184
53
- sandwitches/templates/components/navbar.html,sha256=X2qOPHhVrSl4TkTk4YY-60YjpwCG7IEe4L6a9ywMih4,1164
58
+ sandwitches/templates/components/navbar.html,sha256=t-ZWvd9Z3UQRR2RswcsRXRNbygiesOD0Bh1jhmm2vEY,1396
54
59
  sandwitches/templates/components/rating_section.html,sha256=8O5IsFfQwnElMQZLnDpJiuCvvQMLa3jCS67u_RhMi7o,2717
55
- sandwitches/templates/components/recipe_header.html,sha256=jH9bnT6tis3OuePBq-xzP87IIx2ipDTuw2LO0BQwLXg,1996
60
+ sandwitches/templates/components/recipe_header.html,sha256=U6CxuR275QD9TIqo3VQftqvV6tqmH1rJdDcQotnXxYM,2001
56
61
  sandwitches/templates/components/search_form.html,sha256=B8579Jo44gLrlmvkkc2-Vuv_QD93Ljt6F2J1WgTDV60,6617
57
62
  sandwitches/templates/components/search_scripts.html,sha256=HvsO5e50DoTZeoFiYeNP5S8S5h7Zfr9VULOWKKR1i_M,3423
58
63
  sandwitches/templates/components/side_menu.html,sha256=qXYyk8W-GnG-u_mJ65ADa4HWfUa2ubxnQAKwxwacF9M,1787
@@ -61,17 +66,17 @@ sandwitches/templates/detail.html,sha256=g-O_RsW9Ix9ivWC0nZ4FwHY2NhgYZ3bEGLpqGY0
61
66
  sandwitches/templates/favorites.html,sha256=0cPpW07N6Isrb8XpvA5Eh97L2-12QFZ43EzeJvbOlXo,917
62
67
  sandwitches/templates/index.html,sha256=7anU7k8s80JYk59Rwsm8EdlNYd7B5clCvV7pKq2IUy0,2518
63
68
  sandwitches/templates/login.html,sha256=LiQskhkOkfx0EE4ssA1ToqQ3oEll08OPYLDIkLjHfU8,2177
64
- sandwitches/templates/partials/recipe_list.html,sha256=oF5zlgAyX_uG63BwulyhFywrn8QLnWXatXVtZPVcPmE,4186
69
+ sandwitches/templates/partials/recipe_list.html,sha256=LUHKFKG90D72K9X2X3d1osvj2jX1QU_MbPe0lNwRSII,4555
65
70
  sandwitches/templates/profile.html,sha256=PQTL6_xn0pGUxqEOYuz5j0pmqAyG0Wr3KvyFBO8_k1s,4156
66
71
  sandwitches/templates/setup.html,sha256=iNveFgePATsCSO4XMbGPa8TnWHyvj8S_5WwcW6i7pbo,4661
67
72
  sandwitches/templates/signup.html,sha256=pNBSlRGZI_B5ccF3dWpUgWBcjODkdLlq7HhyJLYIHCI,6176
68
73
  sandwitches/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
74
  sandwitches/templatetags/custom_filters.py,sha256=0KDFlFz4b5LwlcURBAmzyYWKKea-LwydZytJGVkkuKA,243
70
75
  sandwitches/templatetags/markdown_extras.py,sha256=0ibmRzxE3r85x4k7kK71R-9UT0CgeegYF7MHzj3juTI,344
71
- sandwitches/urls.py,sha256=4VeccEtx2dz-Y8IjqD7Cj24nnd89PteoXo2NUUfWJiU,4339
76
+ sandwitches/urls.py,sha256=1GyqdrWsCIbKN8wsT4eeE98blx-bBHc6tJgvXzta2nc,4859
72
77
  sandwitches/utils.py,sha256=SJP-TkeRZ0OIfaMigYrOSbxRqYXswoqoWhwll3nFuAM,7245
73
- sandwitches/views.py,sha256=bFe1y8FJ00Tke6yUJteY0j3VysXUsZJkuQXmmbK_yso,25876
78
+ sandwitches/views.py,sha256=WF17_nRo6wDdC8oVjGBcPHGRtoi_Ji7x8W9HdcjAyQA,30890
74
79
  sandwitches/wsgi.py,sha256=Eyncpnahq_4s3Lr9ruB-R3Lu9j9zBXqgPbUj7qhIbwU,399
75
- sandwitches-2.3.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
76
- sandwitches-2.3.3.dist-info/METADATA,sha256=TltlxFRdT55B1vxKqYv9CjjUEwITAOgFXvw3UwWKA2k,3111
77
- sandwitches-2.3.3.dist-info/RECORD,,
80
+ sandwitches-2.4.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
81
+ sandwitches-2.4.0.dist-info/METADATA,sha256=E9TYl5ZmaC1n9_NM-va3fhaloK8HkrYiP_rei8B_DhE,3111
82
+ sandwitches-2.4.0.dist-info/RECORD,,