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 }} »

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% 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 %} + {{ game.title }} + {% 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 +