add endpoint for donor and page view
This commit is contained in:
parent
a6f0bcc35a
commit
64a95bf114
|
@ -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' %}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
17
lot/forms.py
17
lot/forms.py
|
@ -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
|
||||
|
|
85
lot/migrations/0009_donor_lotsubscription_and_more.py
Normal file
85
lot/migrations/0009_donor_lotsubscription_and_more.py
Normal 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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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
40
lot/templates/donor.html
Normal 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 %}
|
129
lot/templates/donor_web.html
Normal file
129
lot/templates/donor_web.html
Normal 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>
|
||||
©{% 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>
|
|
@ -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"),
|
||||
]
|
||||
|
|
112
lot/views.py
112
lot/views.py
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue