Add minimal loans system

Functional loans system for users.
No manager interface yet.

See:
- app "loans"
- a few new views/templates in app "inventory"
This commit is contained in:
Sylvain Gay 2024-05-02 11:13:34 +02:00
parent a552e6d63b
commit 474083dd38
19 changed files with 318 additions and 2 deletions

View file

@ -33,6 +33,7 @@ INSTALLED_APPS = [
"comments", "comments",
"inventory", "inventory",
"suggestions", "suggestions",
"loans",
"django_cleanup", # Keep last "django_cleanup", # Keep last
] ]

5
inventory/forms.py Normal file
View file

@ -0,0 +1,5 @@
from loans.forms import BorrowForm
class BorrowGameForm(BorrowForm):
error_css_class = "errorfield"
required_css_class = "requiredfield"

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.8 on 2024-04-30 15:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('loans', '0002_abstractloan_delete_loan'),
('inventory', '0002_duration_range'),
]
operations = [
migrations.CreateModel(
name='GameLoan',
fields=[
('abstractloan_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='loans.abstractloan')),
('lent_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.game', verbose_name='jeu emprunté')),
],
bases=('loans.abstractloan',),
),
]

View file

@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from autoslug import AutoSlugField from autoslug import AutoSlugField
from website.validators import MaxFileSizeValidator from website.validators import MaxFileSizeValidator
from comments.models import AbstractComment from comments.models import AbstractComment
from loans.models import AbstractLoan
class Category(models.Model): class Category(models.Model):
@ -152,3 +153,9 @@ class GameComment(AbstractComment):
return reverse( return reverse(
"inventory:modify_game_comment", args=(self.commented_object.slug, self.id) "inventory:modify_game_comment", args=(self.commented_object.slug, self.id)
) )
class GameLoan(AbstractLoan):
lent_object = models.ForeignKey(
Game, on_delete=models.CASCADE,
verbose_name="jeu emprunté"
)

View file

@ -29,6 +29,14 @@
</div> </div>
</div> </div>
{% if is_borrowed %}
<p class="warning">Ce jeu est emprunté depuis le {{ loan.borrow_date }}.</p>
{% endif %}
<a class="button" href="{% url "inventory:game_loan" game.slug %}">
Emprunter ou rendre « {{ game.title }} »
</a>
<h2 id="description">Description</h2> <h2 id="description">Description</h2>
{{ object.description|linebreaks }} {{ object.description|linebreaks }}

View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block "content" %}
<h1>Emprunter « {{ game.title }} »</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Emprunter</button>
</form>
{% endblock %}

View file

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% load static %}
{% block "content" %}
<h1>{{ game.title }}</h1>
<div id="game_infos">
{% if game.image %}
<img src="{{ game.image.url }}" alt="{{ game.title }}"/>
{% endif %}
<div id="details">
<p><i class="fa fa-fw fa-bookmark"></i> <a href="{{ game.category.get_absolute_url }}">{{ game.category }}</a></p>
<hr/>
<p><i class="fa fa-fw fa-users" aria-hidden="true"></i> {{ game.get_player_range }}</p>
<p><i class="fa fa-fw fa-clock-o" aria-hidden="true"></i> {{ game.get_duration_range }}</p>
<hr/>
<p><i class="fa fa-fw fa-tags" aria-hidden="true"></i>
{% for tag in game.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if not forloop.last %},{% endif %}
{% empty %}
(Aucun tag)
{% endfor %}
</p>
</div>
</div>
<hr/>
<a class="button" href="{% url "inventory:borrow_game" game.slug %}">
Emprunter « {{ game.title }} »
<p class="helptext">
Si le jeu est emprunté par quelquun dautre, il sera rendu
automatiquement.
</p>
</a>
{% if is_borrowed %}
<a class="button" href="{% url "inventory:return_game" loan.slug %}">
Rendre « {{ game.title }} »
<p class="helptext">
Ce jeu est emprunté depuis le {{ loan.borrow_date }}.
</p>
</a>
{% endif %}
<a class="button" href="/inventory/game/{{ game.slug }}/">
Détails du jeu
</a>
{% endblock %}

View file

@ -10,6 +10,9 @@ from .views import (
AddGameCommentView, AddGameCommentView,
ModifyGameCommentView, ModifyGameCommentView,
InventorySearchView, InventorySearchView,
GameLoanView,
BorrowGameView,
ReturnGameView,
) )
app_name = "inventory" app_name = "inventory"
@ -31,4 +34,7 @@ urlpatterns = [
name="modify_game_comment", name="modify_game_comment",
), ),
path("search/", InventorySearchView.as_view(), name="search"), path("search/", InventorySearchView.as_view(), name="search"),
path("loans/<slug>/", GameLoanView.as_view(), name="game_loan"),
path("loans/return/<slug>/", ReturnGameView.as_view(), name="return_game"),
path("loans/borrow/<slug>/", BorrowGameView.as_view(), name="borrow_game"),
] ]

View file

@ -3,7 +3,9 @@ from haystack.generic_views import SearchView
from haystack.forms import SearchForm from haystack.forms import SearchForm
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from comments.views import AddCommentView, ModifyCommentView from comments.views import AddCommentView, ModifyCommentView
from .models import Category, Tag, Game, GameComment from loans.views import BorrowView, ReturnView, DetailLoanView
from .models import Category, Tag, Game, GameComment, GameLoan
from .forms import BorrowGameForm
class InventoryView(TemplateView): class InventoryView(TemplateView):
@ -38,8 +40,9 @@ class GameListView(ListView):
paginate_by = 20 paginate_by = 20
class GameView(DetailView): class GameView(DetailLoanView):
model = Game model = Game
loan_model = GameLoan
template_name = "inventory/game.html" template_name = "inventory/game.html"
@ -63,3 +66,20 @@ class ModifyGameCommentView(ModifyCommentView):
comment_model = GameComment comment_model = GameComment
template_name = "inventory/game.html" template_name = "inventory/game.html"
success_pattern_name = "inventory:game" success_pattern_name = "inventory:game"
class BorrowGameView(BorrowView):
model = Game
loan_model = GameLoan
template_name = "inventory/loans/borrow.html"
form_class = BorrowGameForm
success_pattern_name = "inventory:game_loan"
class ReturnGameView(ReturnView):
model = GameLoan
pattern_name = "inventory:game_loan"
class GameLoanView(DetailLoanView):
model = Game
loan_model = GameLoan
template_name = "inventory/loans/game_loan.html"

0
loans/__init__.py Normal file
View file

6
loans/admin.py Normal file
View file

@ -0,0 +1,6 @@
from django.contrib import admin
class LoanAdmin(admin.ModelAdmin):
list_display = ("lent_object", "borrow_date", "return_date")
ordering = ("-borrow_date",)
list_filter = ("return_date", "borrow_date")

6
loans/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class LoansConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'loans'

4
loans/forms.py Normal file
View file

@ -0,0 +1,4 @@
from django import forms
class BorrowForm(forms.Form):
mail = forms.EmailField(label="Mail")

View file

@ -0,0 +1,33 @@
# Generated by Django 4.2.8 on 2024-04-23 16:45
import autoslug.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('inventory', '0002_duration_range'),
]
operations = [
migrations.CreateModel(
name='Loan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='game', unique=True)),
('borrow_date', models.DateTimeField(auto_now_add=True)),
('return_date', models.DateTimeField(null=True)),
('mail', models.EmailField(max_length=254)),
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='inventory.game', verbose_name='jeu emprunté')),
],
options={
'verbose_name': 'emprunt',
'verbose_name_plural': 'emprunts',
'ordering': ['borrow_date'],
},
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 4.2.8 on 2024-04-30 15:24
import autoslug.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('loans', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AbstractLoan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='game', unique=True)),
('borrow_date', models.DateTimeField(auto_now_add=True)),
('return_date', models.DateTimeField(null=True)),
('mail', models.EmailField(max_length=254)),
],
options={
'verbose_name': 'emprunt',
'verbose_name_plural': 'emprunts',
'ordering': ['borrow_date'],
},
),
migrations.DeleteModel(
name='Loan',
),
]

View file

32
loans/models.py Normal file
View file

@ -0,0 +1,32 @@
from django.db import models
from autoslug import AutoSlugField
from django.utils.timezone import now
class AbstractLoan(models.Model):
lent_object = None # Fill this with a foreign key in subclasses
slug = AutoSlugField(unique=True, populate_from="lent_object")
borrow_date = models.DateTimeField(auto_now_add=True)
return_date = models.DateTimeField(null=True)
mail = models.EmailField()
lent_object_slug_field = "slug"
class Meta:
ordering=["borrow_date"]
verbose_name = "emprunt"
verbose_name_plural = "emprunts"
def __str__(self):
return self.slug
def return_object(self):
self.return_date = now()
self.save()
@classmethod
def ongoing_loans(cls, obj = None):
ongoing = cls.objects.filter(return_date=None)
if obj != None:
return ongoing.filter(lent_object=obj)
else:
return ongoing

3
loans/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

70
loans/views.py Normal file
View file

@ -0,0 +1,70 @@
from django.views.generic import DetailView, FormView, RedirectView
from django.views.generic.detail import SingleObjectMixin
from django.contrib import messages
from django.shortcuts import redirect
from inventory.models import Game
from .models import AbstractLoan
from .forms import BorrowForm
class ReturnView(SingleObjectMixin, RedirectView):
# Inherited classes should contain:
# model = LoanModel
# pattern_name =
redirect_slug_field = "slug"
permanent = False
def get_redirect_url(self, *args, **kwargs):
loan = self.get_object()
loan.return_object()
kwargs[self.redirect_slug_field] = getattr(loan.lent_object,
loan.lent_object_slug_field)
messages.success(self.request, "Rendu effectué.")
return super().get_redirect_url(*args, **kwargs)
class BorrowView(SingleObjectMixin, FormView):
# Inherited classes should contain:
# model = LentObjectModel
# loan_model = LoanModel
# template_name = "path/to/template.html"
form_class = BorrowForm # Update this for a more complex form
def get_initial(self):
initial = super().get_initial()
if "loan_mail" in self.request.session:
initial["mail"] = self.request.session["loan_mail"]
return initial
def get_context_data(self, **kwargs):
self.object = self.get_object()
return super().get_context_data(**kwargs)
def form_valid(self, form):
obj = self.get_object()
ongoing = self.loan_model.ongoing_loans(obj)
if ongoing.exists():
ongoing.get().return_object()
loan = self.loan_model(lent_object=obj, mail=form.cleaned_data["mail"])
loan.save()
self.request.session["loan_mail"] = loan.mail
messages.success(self.request, "Votre emprunt est enregistré.")
return redirect(self.success_pattern_name,
getattr(obj, loan.lent_object_slug_field))
class DetailLoanView(DetailView):
# Inherited classes should contain:
# model = LentObjectModel
# loan_model = LoanModel
# template_name = "path/to/template.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
loans = self.loan_model.ongoing_loans(self.get_object())
is_borrowed = loans.exists()
context["is_borrowed"] = is_borrowed
if is_borrowed:
context["loan"] = loans.get()
return context