Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 8 additions & 32 deletions tapir/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.utils.translation import gettext_lazy as _

from tapir import settings
from tapir.accounts.models import TapirUser
from tapir.accounts.models import TapirUser, CoPurchaser
from tapir.core.mail_option import MailOption
from tapir.core.services.mail_classes_service import MailClassesService
from tapir.core.services.optional_mail_choices_service import OptionalMailChoicesService
Expand Down Expand Up @@ -50,44 +50,13 @@ class Meta(TapirUserSelfUpdateForm.Meta):
"postcode",
"city",
"preferred_language",
"co_purchaser",
"co_purchaser_mail",
"co_purchaser_2",
"co_purchaser_2_mail",
] + TapirUserSelfUpdateForm.Meta.fields

widgets = TapirUserSelfUpdateForm.Meta.widgets | {
"birthdate": DateInputTapir(),
"username": TextInput(attrs={"readonly": True}),
}

def clean(self):
cleaned_data = super().clean()

if (
cleaned_data.get("co_purchaser_mail", "") != ""
and cleaned_data.get("co_purchaser", "") == ""
):
raise ValidationError(
{
"co_purchaser_mail": _(
"If there is not co-purchaser then the co-purchaser-mail field must also be empty"
)
}
)

if (
cleaned_data.get("co_purchaser_2_mail", "") != ""
and cleaned_data.get("co_purchaser_2", "") == ""
):
raise ValidationError(
{
"co_purchaser_2_mail": _(
"If there is not co-purchaser 2 then the co-purchaser-mail 2 field must also be empty"
)
}
)


class PasswordResetForm(auth_forms.PasswordResetForm):
def get_users(self, email):
Expand Down Expand Up @@ -176,3 +145,10 @@ def __init__(self, *args, **kwargs):
tapir_user
)
)


class CoPurchaserForm(forms.ModelForm):
class Meta:
model = CoPurchaser
fields = ["first_name", "last_name", "email"]
widgets = {}
49 changes: 49 additions & 0 deletions tapir/accounts/migrations/0024_copurchaser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 5.2.13 on 2026-04-26 17:01

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
(
"accounts",
"0023_updatetapiruserlogentry_accounts_up_old_val_25b95f_gin_and_more",
),
]

operations = [
migrations.CreateModel(
name="CoPurchaser",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("first_name", models.CharField(blank=True, max_length=255)),
("last_name", models.CharField(blank=True, max_length=255)),
("email", models.EmailField(blank=True, max_length=254)),
("order", models.PositiveIntegerField(default=0)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="copurchaser",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Co-Purchaser",
"verbose_name_plural": "Co-Purchasers",
"ordering": ["order"],
},
),
]
50 changes: 50 additions & 0 deletions tapir/accounts/migrations/0025_co_purchaser_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 5.2.13 on 2026-04-26 17:02

from django.db import migrations


def split_name(full_name):
if not full_name:
return "", ""
parts = full_name.strip().split()
if len(parts) == 1:
return parts[0], ""
else:
return " ".join(parts[:-1]), parts[-1]


def migrate_co_purchasers(apps, schema_editor):
TapirUser = apps.get_model("accounts", "TapirUser")
CoPurchaser = apps.get_model("accounts", "CoPurchaser")

for user in TapirUser.objects.all():

if user.co_purchaser:
first_name, last_name = split_name(user.co_purchaser)
CoPurchaser.objects.create(
user=user,
first_name=first_name,
last_name=last_name,
email=user.co_purchaser_mail,
order=0,
)
if user.co_purchaser_2:
first_name, last_name = split_name(user.co_purchaser_2)
CoPurchaser.objects.create(
user=user,
first_name=first_name,
last_name=last_name,
email=user.co_purchaser_2_mail,
order=1,
)


class Migration(migrations.Migration):

dependencies = [
("accounts", "0024_copurchaser"),
]

operations = [
migrations.RunPython(migrate_co_purchasers),
]
18 changes: 18 additions & 0 deletions tapir/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,21 @@ class Meta:
fields=["user", "mail_id"], name="user-mail-constraint"
)
]


class CoPurchaser(models.Model):
user = models.ForeignKey(
"TapirUser", on_delete=models.CASCADE, related_name="copurchaser"
)
first_name = models.CharField(blank=True, max_length=255)
last_name = models.CharField(blank=True, max_length=255)
email = models.EmailField(blank=True, max_length=254)
order = models.PositiveIntegerField(default=0)

class Meta:
verbose_name_plural = _("Co-Purchasers")
verbose_name = _("Co-Purchaser")
ordering = ["order"]

def get_full_name(self):
return f"{self.first_name} {self.last_name}"
44 changes: 43 additions & 1 deletion tapir/accounts/templates/accounts/user_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,49 @@ <h5 class="card-header d-flex justify-content-between align-items-center flex-wr
</div>
</div>
</div>
<div class="col-xl-6">{% share_owner_ownership_list object.share_owner %}</div>
<div class="col-xl-6">
<div class="card mb-2" id="tapir_user_co_purchaser_card">
<h5 class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<span>{% translate "Co-Purchasers" %}</span>
{% if perms.accounts.manage %}
<span class="d-flex justify-content-end flex-fill flex-wrap gap-2">
<a class="{% tapir_button_link_to_action %}"
href="{% url 'accounts:co_purchaser_create' object.pk %}">
<span class="material-icons">person_add</span>{% translate "Add Co-Purchaser" %}
</a>
</span>
{% endif %}
</h5>
<div class="card-body">
{% with co_purchasers=object.copurchaser.all %}
{% if co_purchasers %}
<div class="list-group list-group-flush">
{% for co_purchaser in co_purchasers %}
<div class="list-group-item d-flex justify-content-between align-items-start">
<div class="flex-grow-1">{{ co_purchaser.get_full_name }}</div>
{% if perms.accounts.manage %}
<div class="btn-group btn-group-sm ms-2" role="group">
<a href="{% url 'accounts:co_purchaser_update' co_purchaser.pk %}"
class="btn btn-outline-primary btn-sm"
title="{% translate 'Edit' %}">
<span class="material-icons" style="font-size: 16px;">edit</span>
</a>
<a href="{% url 'accounts:co_purchaser_delete' co_purchaser.pk %}"
class="btn btn-outline-danger btn-sm"
title="{% translate 'Delete' %}"><span class="material-icons" style="font-size: 16px;">delete</span> </a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted mb-0">{% translate "No co-purchasers registered." %}</p>
{% endif %}
{% endwith %}
</div>
</div>
{% share_owner_ownership_list object.share_owner %}
</div>
</div>
<div class="row">
<div class="col-xl-6">{% user_shifts_overview object %}</div>
Expand Down
15 changes: 15 additions & 0 deletions tapir/accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@
views.MailSettingsView.as_view(),
name="mail_settings",
),
path(
"user/<int:pk>/co-purchasers/create/",
views.CoPurchaserCreateView.as_view(),
name="co_purchaser_create",
),
path(
"user/<int:pk>/co-purchasers/update/",
views.CoPurchaserUpdateView.as_view(),
name="co_purchaser_update",
),
path(
"user/<int:pk>/co-purchasers/delete/",
views.CoPurchaserDeleteView.as_view(),
name="co_purchaser_delete",
),
path(
"open_door",
views.OpenDoorView.as_view(),
Expand Down
88 changes: 87 additions & 1 deletion tapir/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from django.db import transaction
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import generic
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_POST, require_GET
from django.views.generic import CreateView

from tapir import settings
from tapir.accounts import pdfs
Expand All @@ -24,11 +25,13 @@
TapirUserSelfUpdateForm,
EditUsernameForm,
OptionalMailsForm,
CoPurchaserForm,
)
from tapir.accounts.models import (
TapirUser,
UpdateTapirUserLogEntry,
OptionalMails,
CoPurchaser,
)
from tapir.coop.emails.co_purchaser_updated_mail import CoPurchaserUpdatedMail
from tapir.coop.emails.tapir_account_created_email import (
Expand Down Expand Up @@ -499,3 +502,86 @@ def get_context_data(self, **kwargs):
hasattr(user, "share_owner") and user.share_owner is not None
)
return context


class CoPurchaserCreateView(
PermissionRequiredMixin, LoginRequiredMixin, TapirFormMixin, CreateView
):
model = CoPurchaser
permission_required = PERMISSION_ACCOUNTS_MANAGE
form_class = CoPurchaserForm

def dispatch(self, request, *args, **kwargs):
self.tapir_user = get_object_or_404(TapirUser, pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["card_title"] = _("Add Co-Purchaser for %(name)s") % {
"name": UserUtils.build_html_link_for_viewer(
self.tapir_user, self.request.user
)
}
context["page_title"] = _("Add Co-Purchaser")
return context

def form_valid(self, form):
max_co_purchasers = getattr(settings, "CO_PURCHASER_MAX_CO_PURCHASERS", 2)
existing_count = CoPurchaser.objects.filter(user=self.tapir_user).count()

if existing_count >= max_co_purchasers:
messages.error(self.request, _("Maximum numbers of Co-Purchasers exceeded"))
return self.form_invalid(form)

form.instance.user = self.tapir_user
form.instance.order = existing_count

messages.success(self.request, _("Co-Purchaser successfully added"))
return super().form_valid(form)

def get_success_url(self):
return reverse_lazy("accounts:user_detail", kwargs={"pk": self.tapir_user.pk})


class CoPurchaserUpdateView(
LoginRequiredMixin, PermissionRequiredMixin, TapirFormMixin, generic.UpdateView
):
permission_required = PERMISSION_ACCOUNTS_MANAGE
model = CoPurchaser
form_class = CoPurchaserForm

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["card_title"] = _("Update Co-Purchaser for %(name)s") % {
"name": UserUtils.build_html_link_for_viewer(
self.object.user, self.request.user
)
}
context["page_title"] = _("Add Co-Purchaser")
return context

def get_success_url(self):
return reverse_lazy("accounts:user_detail", kwargs={"pk": self.object.user.pk})


class CoPurchaserDeleteView(
LoginRequiredMixin, PermissionRequiredMixin, TapirFormMixin, generic.DeleteView
):
permission_required = PERMISSION_ACCOUNTS_MANAGE
model = CoPurchaser

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["card_title"] = _(
"Delete Co-Purchaser %(copurchaser)s for %(name)s"
) % {
"copurchaser": self.object.get_full_name(),
"name": UserUtils.build_html_link_for_viewer(
self.object.user, self.request.user
),
}
context["page_title"] = _("Add Co-Purchaser")
return context

def get_success_url(self):
return reverse_lazy("accounts:user_detail", kwargs={"pk": self.object.user.pk})
2 changes: 2 additions & 0 deletions tapir/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,5 @@
SUBDIV_FOR_HOLIDAYS_AUTO_CANCEL = env.str(
"SUBDIV_FOR_HOLIDAYS_AUTO_CANCEL", default="BE"
)

CO_PURCHASER_MAX_CO_PURCHASERS = 2
Loading