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",
|
"comments",
|
||||||
"inventory",
|
"inventory",
|
||||||
"suggestions",
|
"suggestions",
|
||||||
|
"loans",
|
||||||
"django_cleanup", # Keep last
|
"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 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é"
|
||||||
|
)
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
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,
|
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"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
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