add endpoint for donor and page view

This commit is contained in:
Cayo Puigdefabregas 2025-03-10 18:01:33 +01:00
parent a6f0bcc35a
commit 64a95bf114
11 changed files with 379 additions and 85 deletions

View file

@ -28,7 +28,7 @@
<i class="bi bi-tag"></i>
{% trans 'Unsubscription' %}
</a>
{% if lot.donor %}
{% if donor %}
<a href="{% url 'lot:del_donor' object.id %}" type="button" class="btn btn-green-admin">
<i class="bi bi-tag"></i>
{% trans 'Donor' %}

View file

@ -9,7 +9,7 @@ from dashboard.mixins import InventaryMixin, DetailsMixin
from evidence.models import SystemProperty
from evidence.xapian import search
from device.models import Device
from lot.models import Lot
from lot.models import Lot, Donor
class UnassignedDevicesView(InventaryMixin):
@ -42,6 +42,13 @@ class LotDashboardView(InventaryMixin, DetailsMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
lot = context.get('object')
donor = Donor.objects.filter(
lot=lot,
).first()
if donor:
context["donor"] = donor
context.update({
'lot': lot,
})

View file

@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django import forms
from user.models import User
from lot.models import Lot, LotSubscription
from lot.models import Lot, LotSubscription, Donor
class LotsForm(forms.Form):
@ -132,6 +132,7 @@ class AddDonorForm(forms.Form):
def __init__(self, *args, **kwargs):
self.institution = kwargs.pop("institution")
self.lot = kwargs.pop("lot")
self.donor = kwargs.pop("donor", None)
super().__init__(*args, **kwargs)
def clean(self):
@ -142,14 +143,20 @@ class AddDonorForm(forms.Form):
if not commit:
return
self.lot.donor = self._user
self.lot.save()
if self.donor:
self.donor.email = self._user
self.donor.save()
else:
self.donor = Donor.objects.create(
lot=self.lot,
email=self._user
)
# TODO
# if self._user:
# self.send_email()
def remove(self):
self.lot.donor = ""
self.lot.save()
if self.donor:
self.donor.delete()
return

View file

@ -0,0 +1,85 @@
# Generated by Django 5.0.6 on 2025-03-10 16:29
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0008_rename_closed_lot_archived"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Donor",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"reconciliation",
models.BooleanField(default=False, verbose_name="Reconciliation"),
),
(
"email",
models.EmailField(max_length=255, verbose_name="Email address"),
),
(
"lot",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="lot.lot"
),
),
],
),
migrations.CreateModel(
name="LotSubscription",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"is_circuit_manager",
models.BooleanField(
default=False, verbose_name="is circuit manager"
),
),
("is_shop", models.BooleanField(default=False, verbose_name="is shop")),
(
"lot",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="lot.lot"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddConstraint(
model_name="lotsubscription",
constraint=models.UniqueConstraint(
fields=("lot", "user"), name="unique_lot_user"
),
),
]

View file

@ -1,28 +0,0 @@
# Generated by Django 5.0.6 on 2025-03-03 10:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('lot', '0008_rename_closed_lot_archived'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LotSubscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('lot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lot.lot')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddConstraint(
model_name='lotsubscription',
constraint=models.UniqueConstraint(fields=('lot', 'user'), name='unique_lot_user'),
),
]

View file

@ -1,28 +0,0 @@
# Generated by Django 5.0.6 on 2025-03-07 10:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("lot", "0009_lotsubscription_lotsubscription_unique_lot_user"),
]
operations = [
migrations.AddField(
model_name="lot",
name="donor",
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AddField(
model_name="lotsubscription",
name="is_circuit_manager",
field=models.BooleanField(default=False, verbose_name="is circuit manager"),
),
migrations.AddField(
model_name="lotsubscription",
name="is_shop",
field=models.BooleanField(default=False, verbose_name="is shop"),
),
]

View file

@ -1,3 +1,5 @@
import uuid
from django.db import models
from django.utils.translation import gettext_lazy as _
from utils.constants import (
@ -35,7 +37,6 @@ class Lot(models.Model):
owner = models.ForeignKey(Institution, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
type = models.ForeignKey(LotTag, on_delete=models.CASCADE)
donor = models.CharField(max_length=STR_SIZE, blank=True, null=True)
def add(self, v):
if DeviceLot.objects.filter(lot=self, device_id=v).exists():
@ -73,3 +74,13 @@ class LotSubscription(models.Model):
models.UniqueConstraint(
fields=["lot", "user"], name="unique_lot_user")
]
class Donor(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
reconciliation = models.BooleanField(_("Reconciliation"), default=False)
lot = models.ForeignKey(Lot, on_delete=models.CASCADE)
email = models.EmailField(
_('Email address'),
max_length=255,
)

40
lot/templates/donor.html Normal file
View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col">
<h3>{{ subtitle }}</h3>
</div>
</div>
{% load django_bootstrap5 %}
{% if donor %}
<div class="row mb-4">
<div class="col">
<a class="btn btn-green-admin" href="{% url 'lot:web_donor' lot.id donor.id %}">{% trans "Donor web" %}</a>
</div>
</div>
{% endif %}
<form role="form" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
<div class="message">
{% for field, error in form.errors.items %}
{{ error }}<br />
{% endfor %}
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
{% bootstrap_form form %}
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'dashboard:lot' lot.id %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{{ action }}" />
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,129 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ donor.email }}</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
<style>
body {
font-size: 0.875rem;
background-color: #f8f9fa;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.custom-container {
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 30px;
margin-top: 30px;
flex-grow: 1;
}
.section-title {
color: #7a9f4f;
border-bottom: 2px solid #9cc666;
padding-bottom: 10px;
margin-bottom: 20px;
font-size: 1.5em;
}
.info-row {
margin-bottom: 10px;
}
.info-label {
font-weight: bold;
color: #545f71;
}
.info-value {
color: #333;
}
.component-card {
background-color: #f8f9fa;
border-left: 4px solid #9cc666;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.component-card:hover {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.hash-value {
word-break: break-all;
background-color: #f3f3f3;
padding: 5px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
border: 1px solid #e0e0e0;
}
.card-title {
color: #9cc666;
}
.btn-primary {
background-color: #9cc666;
border-color: #9cc666;
padding: 0.1em 2em;
font-weight: 700;
}
.btn-primary:hover {
background-color: #8ab555;
border-color: #8ab555;
}
.btn-green-user {
background-color: #c7e3a3;
}
.btn-grey {
background-color: #f3f3f3;
}
footer {
background-color: #545f71;
color: #ffffff;
text-align: center;
padding: 10px 0;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container custom-container">
<h1 class="text-center mb-4" style="color: #545f71;">{{ donor.email }}</h1>
<div class="row mt-4">
<div class="col">
<table class="table table-hover table-bordered align-middel">
<thead class="table-light">
<tr>
<th scope="col">ID</th>
<th scope="col" width="15%" class="text-center">{% trans "Manufacturer" %}
<th scope="col" width="15%" class="text-center">{% trans "Model" %}
<th scope="col" width="15%" class="text-center">{% trans "Serial Number" %}
</th>
</tr>
</thead>
<tbody>
{% for dev in devices %}
<tr>
<td class="font-monospace">{{ dev.shortid }}</td>
<td class="font-monospace">{{ dev.manufacturer }}</td>
<td class="font-monospace">{{ dev.model }}</td>
<td class="font-monospace">{{ dev.serial_number }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<footer>
<p>
&copy;{% now 'Y' %}eReuse. All rights reserved.
</p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -18,4 +18,5 @@ urlpatterns = [
path("<int:pk>/unsubscription/", views.UnsubscriptLotView.as_view(), name="unsubscription"),
path("<int:pk>/donor/add", views.AddDonorView.as_view(), name="add_donor"),
path("<int:pk>/donor/del", views.DelDonorView.as_view(), name="del_donor"),
path("<int:pk>/donor/<uuid:id>", views.DonorView.as_view(), name="web_donor"),
]

View file

@ -11,7 +11,9 @@ from django.views.generic.edit import (
FormView,
)
from dashboard.mixins import DashboardView
from lot.models import Lot, LotTag, LotProperty, LotSubscription
from evidence.models import SystemProperty
from device.models import Device
from lot.models import Lot, LotTag, LotProperty, LotSubscription, Donor
from lot.forms import LotsForm, LotSubscriptionForm, AddDonorForm
class NewLotView(DashboardView, CreateView):
@ -333,39 +335,47 @@ class UnsubscriptLotView(SubscriptLotMixing):
return response
class AddDonorView(DashboardView, FormView):
template_name = "subscription.html"
title = _("Add Donor")
breadcrumb = "Lot / {}".format(title)
class DonorMixing(DashboardView, FormView):
template_name = "donor.html"
form_class = AddDonorForm
lot = None
donor = None
def get_context_data(self, **kwargs):
self.pk = self.kwargs.get('pk')
context = super().get_context_data(**kwargs)
if not self.lot:
self.get_lot()
if not self.donor:
self.get_donor()
context.update({
'lot': self.lot,
"action": _("Add")
'donor': self.donor,
})
return context
def form_valid(self, form):
form.save()
response = super().form_valid(form)
return response
def get_form_kwargs(self):
self.pk = self.kwargs.get('pk')
self.success_url = reverse_lazy('dashboard:lot', args=[self.pk])
self.get_lot()
self.get_donor()
cmanager = LotSubscription.objects.filter(
lot=self.lot,
is_circuit_manager=True,
user=self.request.user
).first()
if not self.request.user.is_admin and not cmanager:
raise Http404
kwargs = super().get_form_kwargs()
kwargs["institution"] = self.request.user.institution
kwargs["lot"] = self.lot
donor = self.lot.donor or ""
kwargs["initial"] = {"user": donor}
if self.donor:
kwargs["initial"] = {"user": self.donor.email}
kwargs["donor"] = self.donor
return kwargs
def get_lot(self):
@ -375,23 +385,83 @@ class AddDonorView(DashboardView, FormView):
id=self.pk
)
def get_donor(self):
if not self.lot:
self.get_lot()
class DelDonorView(AddDonorView):
self.donor = Donor.objects.filter(
lot=self.lot,
).first()
class AddDonorView(DonorMixing):
title = _("Add Donor")
breadcrumb = "Lot / {}".format(title)
def form_valid(self, form):
form.save()
response = super().form_valid(form)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["action"] = _("Add")
return context
class DelDonorView(DonorMixing):
title = _("Remove Donor")
breadcrumb = "Lot / {}".format(title)
def form_valid(self, form):
response = super().form_valid(form)
form.remove()
response = super().form_valid(form)
return response
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
donor = kwargs["lot"].donor
kwargs["initial"] = {"user": donor}
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["action"] = _("Remove")
return context
class DonorView(TemplateView, UpdateView):
template_name = "donor_web.html"
model = Donor
fields = ("reconciliation",)
def get_form_kwargs(self):
pk = self.kwargs.get('pk')
id = self.kwargs.get('id')
self.object = get_object_or_404(
Donor,
id=id,
lot_id=pk
)
self.success_url = reverse_lazy('lot:web_donor', args=[pk, id])
kwargs = super().get_form_kwargs()
kwargs['instance'] = self.object
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["donor"] = self.object
context["devices"] = self.get_devices()
return context
def get_devices(self):
chids = self.object.lot.devicelot_set.all().values_list(
"device_id", flat=True
).distinct()
props = SystemProperty.objects.filter(
owner=self.request.user.institution,
value__in=chids
).order_by("-created")
chids_ordered = []
for x in props:
if x.value not in chids_ordered:
chids_ordered.append(Device(id=x.value))
return chids_ordered