diff --git a/gestiojeux/settings_base.py b/gestiojeux/settings_base.py
index 1fa3f13..78c4d5d 100644
--- a/gestiojeux/settings_base.py
+++ b/gestiojeux/settings_base.py
@@ -33,6 +33,7 @@ INSTALLED_APPS = [
"comments",
"inventory",
"suggestions",
+ "loans",
"django_cleanup", # Keep last
]
diff --git a/inventory/forms.py b/inventory/forms.py
new file mode 100644
index 0000000..12be68a
--- /dev/null
+++ b/inventory/forms.py
@@ -0,0 +1,5 @@
+from loans.forms import BorrowForm
+
+class BorrowGameForm(BorrowForm):
+ error_css_class = "errorfield"
+ required_css_class = "requiredfield"
diff --git a/inventory/migrations/0003_gameloan.py b/inventory/migrations/0003_gameloan.py
new file mode 100644
index 0000000..50752df
--- /dev/null
+++ b/inventory/migrations/0003_gameloan.py
@@ -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',),
+ ),
+ ]
diff --git a/inventory/models.py b/inventory/models.py
index 853e9a1..7162775 100644
--- a/inventory/models.py
+++ b/inventory/models.py
@@ -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é"
+ )
diff --git a/inventory/templates/inventory/game.html b/inventory/templates/inventory/game.html
index 86f89f3..3ab6840 100644
--- a/inventory/templates/inventory/game.html
+++ b/inventory/templates/inventory/game.html
@@ -29,6 +29,14 @@
+ {% if is_borrowed %}
+
Ce jeu est emprunté depuis le {{ loan.borrow_date }}.
+ {% endif %}
+
+
+ Emprunter ou rendre « {{ game.title }} »
+
+
Description
{{ object.description|linebreaks }}
diff --git a/inventory/templates/inventory/loans/borrow.html b/inventory/templates/inventory/loans/borrow.html
new file mode 100644
index 0000000..4f7c434
--- /dev/null
+++ b/inventory/templates/inventory/loans/borrow.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block "content" %}
+ Emprunter « {{ game.title }} »
+
+{% endblock %}
diff --git a/inventory/templates/inventory/loans/game_loan.html b/inventory/templates/inventory/loans/game_loan.html
new file mode 100644
index 0000000..62b7041
--- /dev/null
+++ b/inventory/templates/inventory/loans/game_loan.html
@@ -0,0 +1,50 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block "content" %}
+ {{ game.title }}
+
+
+ {% if game.image %}
+
+ {% endif %}
+
+
+
{{ game.category }}
+
+
{{ game.get_player_range }}
+
{{ game.get_duration_range }}
+
+
+ {% for tag in game.tags.all %}
+ {{ tag }}{% if not forloop.last %},{% endif %}
+ {% empty %}
+ (Aucun tag)
+ {% endfor %}
+
+
+
+
+
+
+
+ Emprunter « {{ game.title }} »
+
+ Si le jeu est emprunté par quelqu’un d’autre, il sera rendu
+automatiquement.
+
+
+
+ {% if is_borrowed %}
+
+ Rendre « {{ game.title }} »
+
+ Ce jeu est emprunté depuis le {{ loan.borrow_date }}.
+
+
+ {% endif %}
+
+
+ Détails du jeu
+
+{% endblock %}
diff --git a/inventory/urls.py b/inventory/urls.py
index 1411cfa..65cca0c 100644
--- a/inventory/urls.py
+++ b/inventory/urls.py
@@ -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//", GameLoanView.as_view(), name="game_loan"),
+ path("loans/return//", ReturnGameView.as_view(), name="return_game"),
+ path("loans/borrow//", BorrowGameView.as_view(), name="borrow_game"),
]
diff --git a/inventory/views.py b/inventory/views.py
index a6c693a..cd9ccf2 100644
--- a/inventory/views.py
+++ b/inventory/views.py
@@ -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"
+
diff --git a/loans/__init__.py b/loans/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/loans/admin.py b/loans/admin.py
new file mode 100644
index 0000000..d5b22b0
--- /dev/null
+++ b/loans/admin.py
@@ -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")
diff --git a/loans/apps.py b/loans/apps.py
new file mode 100644
index 0000000..afe898f
--- /dev/null
+++ b/loans/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class LoansConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'loans'
diff --git a/loans/forms.py b/loans/forms.py
new file mode 100644
index 0000000..cbfec56
--- /dev/null
+++ b/loans/forms.py
@@ -0,0 +1,4 @@
+from django import forms
+
+class BorrowForm(forms.Form):
+ mail = forms.EmailField(label="Mail")
diff --git a/loans/migrations/0001_initial.py b/loans/migrations/0001_initial.py
new file mode 100644
index 0000000..29a4716
--- /dev/null
+++ b/loans/migrations/0001_initial.py
@@ -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'],
+ },
+ ),
+ ]
diff --git a/loans/migrations/0002_abstractloan_delete_loan.py b/loans/migrations/0002_abstractloan_delete_loan.py
new file mode 100644
index 0000000..6a91cb8
--- /dev/null
+++ b/loans/migrations/0002_abstractloan_delete_loan.py
@@ -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',
+ ),
+ ]
diff --git a/loans/migrations/__init__.py b/loans/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/loans/models.py b/loans/models.py
new file mode 100644
index 0000000..6c37694
--- /dev/null
+++ b/loans/models.py
@@ -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
diff --git a/loans/tests.py b/loans/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/loans/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/loans/views.py b/loans/views.py
new file mode 100644
index 0000000..9903472
--- /dev/null
+++ b/loans/views.py
@@ -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
+