forked from DGNum/gestiojeux
Add the game suggestion system
This commit is contained in:
parent
0ad6c7543e
commit
88a6f06f0d
17 changed files with 505 additions and 1 deletions
|
@ -32,6 +32,7 @@ INSTALLED_APPS = [
|
||||||
"accounts",
|
"accounts",
|
||||||
"comments",
|
"comments",
|
||||||
"inventory",
|
"inventory",
|
||||||
|
"suggestions",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
@ -22,6 +22,7 @@ urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("markdownx/", include("markdownx.urls")),
|
path("markdownx/", include("markdownx.urls")),
|
||||||
path("inventory/", include("inventory.urls")),
|
path("inventory/", include("inventory.urls")),
|
||||||
|
path("suggestions/", include("suggestions.urls")),
|
||||||
path("account/", include("accounts.urls")),
|
path("account/", include("accounts.urls")),
|
||||||
path("", include("website.urls")),
|
path("", include("website.urls")),
|
||||||
]
|
]
|
||||||
|
|
1
suggestions/__init__.py
Normal file
1
suggestions/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = "suggestions.apps.SuggestionsConfig"
|
32
suggestions/admin.py
Normal file
32
suggestions/admin.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import Suggestion, SuggestionComment
|
||||||
|
from comments.admin import CommentAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionAdmin(admin.ModelAdmin):
|
||||||
|
exclude = ("upvoting_users",)
|
||||||
|
list_display = ("title", "num_upvotes", "price")
|
||||||
|
actions = ["reset_upvotes"]
|
||||||
|
|
||||||
|
def num_upvotes(self, obj):
|
||||||
|
return obj.upvoting_users.all().count()
|
||||||
|
|
||||||
|
num_upvotes.short_description = "Nombre de votes"
|
||||||
|
|
||||||
|
def reset_upvotes(self, request, queryset):
|
||||||
|
SuggestionVote = Suggestion.upvoting_users.through
|
||||||
|
SuggestionVote.objects.filter(suggestion__in=queryset).delete()
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Les votes pour {} suggestions ont été réinitialisés".format(
|
||||||
|
queryset.count()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
reset_upvotes.short_description = (
|
||||||
|
"Remettre à zero les votes pour les suggestions sélectionnées"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Suggestion, SuggestionAdmin)
|
||||||
|
admin.site.register(SuggestionComment, CommentAdmin)
|
5
suggestions/apps.py
Normal file
5
suggestions/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionsConfig(AppConfig):
|
||||||
|
name = 'suggestions'
|
30
suggestions/forms.py
Normal file
30
suggestions/forms.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django import forms
|
||||||
|
from .models import Suggestion
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionForm(forms.ModelForm):
|
||||||
|
error_css_class = "errorfield"
|
||||||
|
required_css_class = "requiredfield"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Suggestion
|
||||||
|
fields = [
|
||||||
|
"title",
|
||||||
|
"price",
|
||||||
|
"buy_link",
|
||||||
|
"nb_player_min",
|
||||||
|
"nb_player_max",
|
||||||
|
"player_range_precisions",
|
||||||
|
"duration",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"game_designer",
|
||||||
|
"illustrator",
|
||||||
|
"editor",
|
||||||
|
"image",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AddSuggestionForm(SuggestionForm):
|
||||||
|
comment = forms.CharField(widget=forms.Textarea, label="Commentaire personnel")
|
64
suggestions/migrations/0001_initial.py
Normal file
64
suggestions/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# Generated by Django 3.1.2 on 2020-12-29 23:43
|
||||||
|
|
||||||
|
import autoslug.fields
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import website.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('inventory', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Suggestion',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=256, unique=True, verbose_name='titre du jeu')),
|
||||||
|
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=6, validators=[django.core.validators.MinValueValidator(0)], verbose_name='prix en euros')),
|
||||||
|
('buy_link', models.URLField(verbose_name="lien vers un site d'achat")),
|
||||||
|
('nb_player_min', models.PositiveSmallIntegerField(verbose_name='nombre de joueur·se·s minimum')),
|
||||||
|
('nb_player_max', models.PositiveSmallIntegerField(verbose_name='nombre de joueur·se·s maximum')),
|
||||||
|
('player_range_precisions', models.CharField(blank=True, help_text='Pour indiquer une éventuelle contrainte (ex. parité) ou information sur le nombre de joueur·se·s', max_length=256, verbose_name='précisions sur le nombre de joueur·se·s')),
|
||||||
|
('duration', models.CharField(max_length=256, verbose_name='durée de partie')),
|
||||||
|
('game_designer', models.CharField(blank=True, max_length=256, verbose_name='game designer')),
|
||||||
|
('illustrator', models.CharField(blank=True, max_length=256, verbose_name='illustrateur·trice')),
|
||||||
|
('editor', models.CharField(blank=True, max_length=256, verbose_name='éditeur')),
|
||||||
|
('description', models.TextField(blank=True, help_text="Peut correspondre à celle de l'éditeur et ne doit pas contenir d'avis personnel", verbose_name='description')),
|
||||||
|
('image', models.ImageField(blank=True, help_text='Image du jeu de moins de 512 Kio à téléverser (par exemple une photo de sa boite)', upload_to='suggestion_img/', validators=[website.validators.MaxFileSizeValidator(512)], verbose_name='image')),
|
||||||
|
('category', models.ForeignKey(blank=True, help_text='Idée de catégorie dans laquelle ranger ce jeu', null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.category', verbose_name='catégorie')),
|
||||||
|
('tags', models.ManyToManyField(blank=True, help_text="Vous pouvez en sélectionner plusieurs ou aucun (sur ordinateur Ctrl+Clic change l'état de selection d'un tag)", to='inventory.Tag', verbose_name='tags qui correspondent à ce jeu')),
|
||||||
|
('upvoting_users', models.ManyToManyField(blank=True, related_name='upvoted_suggestions', to=settings.AUTH_USER_MODEL, verbose_name='personnes intéressées')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'suggestion de jeu',
|
||||||
|
'verbose_name_plural': 'suggestions de jeux',
|
||||||
|
'ordering': ['title'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SuggestionComment',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('text', models.TextField(verbose_name='texte')),
|
||||||
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='date de publication')),
|
||||||
|
('modified_on', models.DateTimeField(auto_now=True, verbose_name='date de modification')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='auteur·ice')),
|
||||||
|
('commented_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='suggestions.suggestion', verbose_name='suggestion')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'commentaire sur une suggestion',
|
||||||
|
'verbose_name_plural': 'commentaires sur des suggestions',
|
||||||
|
'ordering': ['created_on'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
suggestions/migrations/__init__.py
Normal file
0
suggestions/migrations/__init__.py
Normal file
133
suggestions/models.py
Normal file
133
suggestions/models.py
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from autoslug import AutoSlugField
|
||||||
|
from website.validators import MaxFileSizeValidator
|
||||||
|
from accounts.models import User
|
||||||
|
from inventory.models import Category, Tag
|
||||||
|
from comments.models import AbstractComment
|
||||||
|
|
||||||
|
|
||||||
|
class Suggestion(models.Model):
|
||||||
|
title = models.CharField(verbose_name="titre du jeu", max_length=256, unique=True)
|
||||||
|
slug = AutoSlugField(populate_from="title", unique=True)
|
||||||
|
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=6,
|
||||||
|
decimal_places=2,
|
||||||
|
verbose_name="prix en euros",
|
||||||
|
validators=[MinValueValidator(0)],
|
||||||
|
)
|
||||||
|
buy_link = models.URLField(verbose_name="lien vers un site d'achat")
|
||||||
|
|
||||||
|
nb_player_min = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name="nombre de joueur·se·s minimum"
|
||||||
|
)
|
||||||
|
nb_player_max = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name="nombre de joueur·se·s maximum"
|
||||||
|
)
|
||||||
|
player_range_precisions = models.CharField(
|
||||||
|
max_length=256,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="précisions sur le nombre de joueur·se·s",
|
||||||
|
help_text="Pour indiquer une éventuelle contrainte (ex. parité) ou information sur le nombre de joueur·se·s",
|
||||||
|
)
|
||||||
|
|
||||||
|
duration = models.CharField(max_length=256, verbose_name="durée de partie")
|
||||||
|
|
||||||
|
game_designer = models.CharField(
|
||||||
|
max_length=256, blank=True, verbose_name="game designer"
|
||||||
|
)
|
||||||
|
illustrator = models.CharField(
|
||||||
|
max_length=256, blank=True, verbose_name="illustrateur·trice"
|
||||||
|
)
|
||||||
|
editor = models.CharField(max_length=256, blank=True, verbose_name="éditeur")
|
||||||
|
|
||||||
|
category = models.ForeignKey(
|
||||||
|
Category,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="catégorie",
|
||||||
|
help_text="Idée de catégorie dans laquelle ranger ce jeu",
|
||||||
|
)
|
||||||
|
tags = models.ManyToManyField(
|
||||||
|
Tag,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="tags qui correspondent à ce jeu",
|
||||||
|
help_text="Vous pouvez en sélectionner plusieurs ou aucun (sur ordinateur Ctrl+Clic change l'état de selection d'un tag)",
|
||||||
|
)
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
verbose_name="description",
|
||||||
|
help_text="Peut correspondre à celle de l'éditeur et ne doit pas contenir d'avis personnel",
|
||||||
|
)
|
||||||
|
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to="suggestion_img/",
|
||||||
|
blank=True,
|
||||||
|
verbose_name="image",
|
||||||
|
help_text="Image du jeu de moins de 512 Kio à téléverser (par exemple une photo de sa boite)",
|
||||||
|
validators=[MaxFileSizeValidator(512)],
|
||||||
|
)
|
||||||
|
|
||||||
|
upvoting_users = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
blank=True,
|
||||||
|
related_name="upvoted_suggestions",
|
||||||
|
verbose_name="personnes intéressées",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["title"]
|
||||||
|
verbose_name = "suggestion de jeu"
|
||||||
|
verbose_name_plural = "suggestions de jeux"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not self.nb_player_min or not self.nb_player_max:
|
||||||
|
return
|
||||||
|
if self.nb_player_min > self.nb_player_max:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"nb_player_max": "Le nombre de joueur·se·s maximum doit être supérieur au nombre de joueur·se·s minimum"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_player_range(self):
|
||||||
|
precisions = ""
|
||||||
|
if self.player_range_precisions:
|
||||||
|
precisions = " ({})".format(self.player_range_precisions)
|
||||||
|
if self.nb_player_min != self.nb_player_max:
|
||||||
|
return "{} à {} joueur·se·s{}".format(
|
||||||
|
self.nb_player_min, self.nb_player_max, precisions
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "{} joueur·se·s{}".format(self.nb_player_min, precisions)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("suggestions:suggestion", args=(self.slug,))
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionComment(AbstractComment):
|
||||||
|
commented_object = models.ForeignKey(
|
||||||
|
Suggestion,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="comments",
|
||||||
|
verbose_name="suggestion",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["created_on"]
|
||||||
|
verbose_name = "commentaire sur une suggestion"
|
||||||
|
verbose_name_plural = "commentaires sur des suggestions"
|
||||||
|
|
||||||
|
def get_modification_url(self):
|
||||||
|
return reverse(
|
||||||
|
"suggestions:modify_suggestion_comment",
|
||||||
|
args=(self.commented_object.slug, self.id),
|
||||||
|
)
|
10
suggestions/templates/suggestions/add_suggestion.html
Normal file
10
suggestions/templates/suggestions/add_suggestion.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block "content" %}
|
||||||
|
<h1>Ajout d'une suggestion</h1>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Envoyer la suggestion</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<a class="inventory_item suggestion" href="{{ suggestion.get_absolute_url }}">
|
||||||
|
<span class="title">{{ suggestion.title }}</span>
|
||||||
|
<span class="details">
|
||||||
|
<span><i class="fa fa-fw fa-thumbs-up" aria-hidden="true"></i> {{ suggestion.upvoting_users.count }}</span>
|
||||||
|
<span><i class="fa fa-fw fa-money" aria-hidden="true"></i> {{ suggestion.price }} €</span>
|
||||||
|
<span><i class="fa fa-fw fa-users" aria-hidden="true"></i> {{ suggestion.get_player_range }}</span>
|
||||||
|
{% if suggestion.duration %}
|
||||||
|
<span><i class="fa fa-fw fa-clock-o" aria-hidden="true"></i> {{ suggestion.duration }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if suggestion.category %}
|
||||||
|
<span><i class="fa fa-fw fa-bookmark"></i> {{ suggestion.category }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% for tag in suggestion.tags.all %}
|
||||||
|
<span><i class="fa fa-fw fa-tag" aria-hidden="true"></i> {{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% if suggestion.game_designer %}
|
||||||
|
<span><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> {{ suggestion.game_designer }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if suggestion.illustrator %}
|
||||||
|
<span><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> {{ suggestion.illustrator }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if suggestion.editor %}
|
||||||
|
<span><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> {{ suggestion.editor }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</a>
|
58
suggestions/templates/suggestions/suggestion.html
Normal file
58
suggestions/templates/suggestions/suggestion.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block "content" %}
|
||||||
|
<h1>{{ suggestion.title }}</h1>
|
||||||
|
|
||||||
|
<div id="game_infos">
|
||||||
|
{% if suggestion.image %}
|
||||||
|
<img src="{{ suggestion.image.url }}" alt="{{ suggestion.title }}"/>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="details">
|
||||||
|
<p><i class="fa fa-fw fa-money" aria-hidden="true"></i> <a href="{{ suggestion.buy_link }}" rel="nofollow">{{ suggestion.price }} €</a></p>
|
||||||
|
<hr/>
|
||||||
|
<p><i class="fa fa-fw fa-users" aria-hidden="true"></i> {{ suggestion.get_player_range }}</p>
|
||||||
|
<p><i class="fa fa-fw fa-clock-o" aria-hidden="true"></i> {{ suggestion.duration|default:"(Durée de jeu inconnue)" }}
|
||||||
|
<hr/>
|
||||||
|
<p><i class="fa fa-fw fa-bookmark" aria-hidden="true"></i> {% if suggestion.category %}<a href="{{ suggestion.category.get_absolute_url }}">{{ suggestion.category }}</a>{% else %}(Pas de catégorie){% endif %}</p>
|
||||||
|
<p><i class="fa fa-fw fa-tags" aria-hidden="true"></i>
|
||||||
|
{% for tag in suggestion.tags.all %}
|
||||||
|
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if not forloop.last %},{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
(Aucun tag)
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<hr/>
|
||||||
|
<p><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> {{ suggestion.game_designer|default:"(Game designer inconnu·e)" }}</li>
|
||||||
|
<p><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> {{ suggestion.illustrator|default:"(Illustrateur·trice inconnu·e)" }}</li>
|
||||||
|
<p><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> {{ suggestion.editor|default:"(Éditeur inconnu)" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Votes pour cette suggestion</h2>
|
||||||
|
{% if suggestion.upvoting_users.count %}
|
||||||
|
<p>Actuellement, {{ suggestion.upvoting_users.count }} {{ suggestion.upvoting_users.count|pluralize:"personne aimerait,personnes aimeraient" }} avoir ce jeu dans la ludothèque.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Pour l'instant, personne n'est intéressé par ce jeu.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
{% if request.user in suggestion.upvoting_users.all %}
|
||||||
|
<p>Vous faites partie des personnes intéressées.</p>
|
||||||
|
<a class="button" href="{% url "suggestions:downvote_suggestion" suggestion.slug %}"><i class="fa fa-thumbs-down" aria-hidden="true"></i> Annuler mon vote pour l'achat de ce jeu</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="button" href="{% url "suggestions:upvote_suggestion" suggestion.slug %}"><i class="fa fa-thumbs-up" aria-hidden="true"></i> Voter pour acheter ce jeu</a>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p><a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour voter pour une suggestion.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if suggestion.description %}
|
||||||
|
<h2 id="description">Description</h2>
|
||||||
|
{{ suggestion.description|linebreaks }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>Avis et commentaires</h2>
|
||||||
|
{% url "suggestions:add_suggestion_comment" suggestion.slug as add_comment_url %}
|
||||||
|
{% include "comments.html" with comments=suggestion.comments.all %}
|
||||||
|
{% endblock %}
|
17
suggestions/templates/suggestions/suggestion_list.html
Normal file
17
suggestions/templates/suggestions/suggestion_list.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block "content" %}
|
||||||
|
<h1>Liste des suggestions</h1>
|
||||||
|
<p>Ceci est la liste des achats suggérés par les membres du club jeux pour compléter la ludothèque.</p>
|
||||||
|
{% for suggestion in suggestion_list %}
|
||||||
|
{% include "./partials/suggestion_item.html" %}
|
||||||
|
{% empty %}
|
||||||
|
<p>(Aucune suggestion pour le moment)</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<a class="button" href="{% url "suggestions:add_suggestion" %}"><i class="fa fa-plus" aria-hidden="true"></i> Ajouter une suggestion</a>
|
||||||
|
{% else %}
|
||||||
|
<p><a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour ajouter une suggestion.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
3
suggestions/tests.py
Normal file
3
suggestions/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
36
suggestions/urls.py
Normal file
36
suggestions/urls.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from django.urls import path
|
||||||
|
from .views import (
|
||||||
|
SuggestionListView,
|
||||||
|
AddSuggestionView,
|
||||||
|
SuggestionView,
|
||||||
|
UpvoteSuggestionView,
|
||||||
|
DownvoteSuggestionView,
|
||||||
|
AddSuggestionCommentView,
|
||||||
|
ModifySuggestionCommentView,
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name = "suggestions"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", SuggestionListView.as_view(), name="suggestions"),
|
||||||
|
path("add/", AddSuggestionView.as_view(), name="add_suggestion"),
|
||||||
|
path("item/<slug>/", SuggestionView.as_view(), name="suggestion"),
|
||||||
|
path(
|
||||||
|
"item/<slug>/upvote", UpvoteSuggestionView.as_view(), name="upvote_suggestion"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"item/<slug>/downvote",
|
||||||
|
DownvoteSuggestionView.as_view(),
|
||||||
|
name="downvote_suggestion",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"item/<slug>/add_comment",
|
||||||
|
AddSuggestionCommentView.as_view(),
|
||||||
|
name="add_suggestion_comment",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"item/<slug>/modify_comment/<int:comment_id>",
|
||||||
|
ModifySuggestionCommentView.as_view(),
|
||||||
|
name="modify_suggestion_comment",
|
||||||
|
),
|
||||||
|
]
|
87
suggestions/views.py
Normal file
87
suggestions/views.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
from django.views.generic import ListView, DetailView, FormView, RedirectView
|
||||||
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.db.models import Count
|
||||||
|
from comments.views import AddCommentView, ModifyCommentView
|
||||||
|
from .models import Suggestion, SuggestionComment
|
||||||
|
from .forms import AddSuggestionForm
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionListView(ListView):
|
||||||
|
model = Suggestion
|
||||||
|
template_name = "suggestions/suggestion_list.html"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Suggestion.objects.annotate(
|
||||||
|
num_upvotes=Count("upvoting_users")
|
||||||
|
).order_by("-num_upvotes")
|
||||||
|
|
||||||
|
|
||||||
|
class AddSuggestionView(LoginRequiredMixin, FormView):
|
||||||
|
template_name = "suggestions/add_suggestion.html"
|
||||||
|
form_class = AddSuggestionForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
suggestion = form.save()
|
||||||
|
suggestion.upvoting_users.add(self.request.user)
|
||||||
|
suggestion.comments.create(
|
||||||
|
author=self.request.user, text=form.cleaned_data["comment"]
|
||||||
|
)
|
||||||
|
messages.success(self.request, "Votre suggestion est enregistrée")
|
||||||
|
return redirect("suggestions:suggestion", suggestion.slug)
|
||||||
|
|
||||||
|
|
||||||
|
class SuggestionView(DetailView):
|
||||||
|
model = Suggestion
|
||||||
|
template_name = "suggestions/suggestion.html"
|
||||||
|
|
||||||
|
|
||||||
|
class UpvoteSuggestionView(LoginRequiredMixin, SingleObjectMixin, RedirectView):
|
||||||
|
model = Suggestion
|
||||||
|
pattern_name = "suggestions:suggestion"
|
||||||
|
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
suggestion = self.get_object()
|
||||||
|
if self.request.user not in suggestion.upvoting_users.all():
|
||||||
|
suggestion.upvoting_users.add(self.request.user)
|
||||||
|
messages.success(
|
||||||
|
self.request, "Votre vote pour cette suggestion est enregistré"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.error(self.request, "Vous avez déjà voté pour cette suggestion")
|
||||||
|
|
||||||
|
return super().get_redirect_url(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DownvoteSuggestionView(LoginRequiredMixin, SingleObjectMixin, RedirectView):
|
||||||
|
model = Suggestion
|
||||||
|
pattern_name = "suggestions:suggestion"
|
||||||
|
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
suggestion = self.get_object()
|
||||||
|
if self.request.user in suggestion.upvoting_users.all():
|
||||||
|
suggestion.upvoting_users.remove(self.request.user)
|
||||||
|
messages.success(
|
||||||
|
self.request, "Votre vote pour cette suggestion a été retiré"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
self.request, "Vous ne votiez déjà pas pour cette suggestion"
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().get_redirect_url(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AddSuggestionCommentView(AddCommentView):
|
||||||
|
model = Suggestion
|
||||||
|
comment_model = SuggestionComment
|
||||||
|
pattern_name = "suggestions:suggestion"
|
||||||
|
|
||||||
|
|
||||||
|
class ModifySuggestionCommentView(ModifyCommentView):
|
||||||
|
model = Suggestion
|
||||||
|
comment_model = SuggestionComment
|
||||||
|
template_name = "suggestions/suggestion.html"
|
||||||
|
success_pattern_name = "suggestions:suggestion"
|
|
@ -10,7 +10,7 @@
|
||||||
<div>
|
<div>
|
||||||
<a {% if url_name == "home" %}class="current"{% endif %} href="{% url "website:home" %}"><i class="fa fa-home" aria-hidden="true"></i></a>
|
<a {% if url_name == "home" %}class="current"{% endif %} href="{% url "website:home" %}"><i class="fa fa-home" aria-hidden="true"></i></a>
|
||||||
<a {% if url_name == "inventory" %}class="current"{% endif %} href="{% url "inventory:inventory" %}">Inventaire</a>
|
<a {% if url_name == "inventory" %}class="current"{% endif %} href="{% url "inventory:inventory" %}">Inventaire</a>
|
||||||
<a {% if url_name == "suggestions" %}class="current"{% endif %} href="">Suggestions</a>
|
<a {% if url_name == "suggestions" %}class="current"{% endif %} href="{% url "suggestions:suggestions" %}">Suggestions</a>
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<a href="{% url "admin:index" %}">Admin</a>
|
<a href="{% url "admin:index" %}">Admin</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in a new issue