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:
parent
a552e6d63b
commit
474083dd38
19 changed files with 318 additions and 2 deletions
|
@ -33,6 +33,7 @@ INSTALLED_APPS = [
|
|||
"comments",
|
||||
"inventory",
|
||||
"suggestions",
|
||||
"loans",
|
||||
"django_cleanup", # Keep last
|
||||
]
|
||||
|
||||
|
|
5
inventory/forms.py
Normal file
5
inventory/forms.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from loans.forms import BorrowForm
|
||||
|
||||
class BorrowGameForm(BorrowForm):
|
||||
error_css_class = "errorfield"
|
||||
required_css_class = "requiredfield"
|
23
inventory/migrations/0003_gameloan.py
Normal file
23
inventory/migrations/0003_gameloan.py
Normal 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',),
|
||||
),
|
||||
]
|
|
@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
|
|||
from autoslug import AutoSlugField
|
||||
from website.validators import MaxFileSizeValidator
|
||||
from comments.models import AbstractComment
|
||||
from loans.models import AbstractLoan
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
|
@ -152,3 +153,9 @@ class GameComment(AbstractComment):
|
|||
return reverse(
|
||||
"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é"
|
||||
)
|
||||
|
|
|
@ -29,6 +29,14 @@
|
|||
</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>
|
||||
{{ object.description|linebreaks }}
|
||||
|
||||
|
|
10
inventory/templates/inventory/loans/borrow.html
Normal file
10
inventory/templates/inventory/loans/borrow.html
Normal 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 %}
|
50
inventory/templates/inventory/loans/game_loan.html
Normal file
50
inventory/templates/inventory/loans/game_loan.html
Normal 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 quelqu’un d’autre, 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 %}
|
|
@ -10,6 +10,9 @@ from .views import (
|
|||
AddGameCommentView,
|
||||
ModifyGameCommentView,
|
||||
InventorySearchView,
|
||||
GameLoanView,
|
||||
BorrowGameView,
|
||||
ReturnGameView,
|
||||
)
|
||||
|
||||
app_name = "inventory"
|
||||
|
@ -31,4 +34,7 @@ urlpatterns = [
|
|||
name="modify_game_comment",
|
||||
),
|
||||
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"),
|
||||
]
|
||||
|
|
|
@ -3,7 +3,9 @@ from haystack.generic_views import SearchView
|
|||
from haystack.forms import SearchForm
|
||||
from haystack.query import SearchQuerySet
|
||||
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):
|
||||
|
@ -38,8 +40,9 @@ class GameListView(ListView):
|
|||
paginate_by = 20
|
||||
|
||||
|
||||
class GameView(DetailView):
|
||||
class GameView(DetailLoanView):
|
||||
model = Game
|
||||
loan_model = GameLoan
|
||||
template_name = "inventory/game.html"
|
||||
|
||||
|
||||
|
@ -63,3 +66,20 @@ class ModifyGameCommentView(ModifyCommentView):
|
|||
comment_model = GameComment
|
||||
template_name = "inventory/game.html"
|
||||
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
0
loans/__init__.py
Normal file
6
loans/admin.py
Normal file
6
loans/admin.py
Normal 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
6
loans/apps.py
Normal 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
4
loans/forms.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from django import forms
|
||||
|
||||
class BorrowForm(forms.Form):
|
||||
mail = forms.EmailField(label="Mail")
|
33
loans/migrations/0001_initial.py
Normal file
33
loans/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
32
loans/migrations/0002_abstractloan_delete_loan.py
Normal file
32
loans/migrations/0002_abstractloan_delete_loan.py
Normal 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',
|
||||
),
|
||||
]
|
0
loans/migrations/__init__.py
Normal file
0
loans/migrations/__init__.py
Normal file
32
loans/models.py
Normal file
32
loans/models.py
Normal 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
3
loans/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
70
loans/views.py
Normal file
70
loans/views.py
Normal 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
|
||||
|
Loading…
Reference in a new issue