forked from DGNum/gestiojeux
Compare commits
9 commits
356ad708c6
...
8048c2593d
Author | SHA1 | Date | |
---|---|---|---|
|
8048c2593d | ||
|
4d99ba9026 | ||
|
14509cf9b5 | ||
|
b95b0ccd3f | ||
|
541d840727 | ||
|
448bcad382 | ||
|
1f8a03bdab | ||
|
89f2808087 | ||
|
fee31ab07b |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -68,3 +68,5 @@ public/
|
|||
|
||||
# Vim recover files
|
||||
*~
|
||||
|
||||
.pre-commit-config.yaml
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserAdmin(DjangoUserAdmin):
|
||||
fieldsets = (
|
||||
(None, {"fields": ("email", "password")}),
|
||||
(_("Personal info"), {"fields": ("public_name",)}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
"fields": (
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"user_permissions",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"classes": ("wide",),
|
||||
"fields": ("email", "public_name", "password1", "password2"),
|
||||
},
|
||||
),
|
||||
)
|
||||
list_display = ("public_name", "email", "is_staff")
|
||||
search_fields = ("public_name", "email")
|
||||
ordering = ("email",)
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
|
@ -1,7 +0,0 @@
|
|||
from django_cas_ng.backends import CASBackend as DjangoCasNgBackend
|
||||
|
||||
|
||||
class CasBackend(DjangoCasNgBackend):
|
||||
def clean_username(self, username):
|
||||
# We need to build an email out of the CAS username
|
||||
return username.lower() + "@clipper.ens.fr"
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.forms import ModelForm, ValidationError
|
||||
from .models import User
|
||||
|
||||
|
||||
class AccountSettingsForm(ModelForm):
|
||||
|
@ -7,13 +7,13 @@ class AccountSettingsForm(ModelForm):
|
|||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["public_name"]
|
||||
fields = ["username"]
|
||||
|
||||
def clean_public_name(self):
|
||||
public_name = self.cleaned_data["public_name"]
|
||||
public_name = self.cleaned_data["username"]
|
||||
public_name = public_name.strip()
|
||||
if (
|
||||
User.objects.filter(public_name=public_name)
|
||||
User.objects.filter(username=public_name)
|
||||
.exclude(pk=self.instance.pk)
|
||||
.exists()
|
||||
):
|
||||
|
|
|
@ -8,27 +8,94 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name="User",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('email', models.EmailField(max_length=254, unique=True, verbose_name='adresse email')),
|
||||
('public_name', models.CharField(help_text='Ce nom est utilisé pour toutes les interactions publiques sur GestioJeux. Il doit être unique.', max_length=150, unique=True, verbose_name='nom ou pseudo')),
|
||||
('is_staff', models.BooleanField(default=False, help_text="Précise si l’utilisateur peut se connecter à ce site d'administration.", verbose_name='statut équipe')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Précise si l’utilisateur doit être considéré comme actif. Décochez ceci plutôt que de supprimer le compte.', verbose_name='actif')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
max_length=254, unique=True, verbose_name="adresse email"
|
||||
),
|
||||
),
|
||||
(
|
||||
"public_name",
|
||||
models.CharField(
|
||||
help_text="Ce nom est utilisé pour toutes les interactions publiques sur GestioJeux. Il doit être unique.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
verbose_name="nom ou pseudo",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Précise si l’utilisateur peut se connecter à ce site d'administration.",
|
||||
verbose_name="statut équipe",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Précise si l’utilisateur doit être considéré comme actif. Décochez ceci plutôt que de supprimer le compte.",
|
||||
verbose_name="actif",
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'utilisateur·ice',
|
||||
'verbose_name_plural': 'utilisateur·ice·s',
|
||||
"verbose_name": "utilisateur·ice",
|
||||
"verbose_name_plural": "utilisateur·ice·s",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
16
accounts/migrations/0002_delete_user.py
Normal file
16
accounts/migrations/0002_delete_user.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-02 20:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("accounts", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="User",
|
||||
),
|
||||
]
|
|
@ -1,76 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
||||
from django.contrib.auth.models import PermissionsMixin
|
||||
from django.core.mail import send_mail
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
USERNAME_FIELD = "email"
|
||||
EMAIL_FIELD = "email"
|
||||
REQUIRED_FIELDS = ["public_name"]
|
||||
MAX_NAME_LENGTH = 150
|
||||
|
||||
email = models.EmailField(verbose_name="adresse email", unique=True)
|
||||
public_name = models.CharField(
|
||||
verbose_name="nom ou pseudo",
|
||||
max_length=MAX_NAME_LENGTH,
|
||||
unique=True,
|
||||
help_text="Ce nom est utilisé pour toutes les interactions publiques sur GestioJeux. Il doit être unique.",
|
||||
)
|
||||
is_staff = models.BooleanField(
|
||||
"statut équipe",
|
||||
default=False,
|
||||
help_text="Précise si l’utilisateur peut se connecter à ce site d'administration.",
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
"actif",
|
||||
default=True,
|
||||
help_text="Précise si l’utilisateur doit être considéré comme actif. Décochez ceci plutôt que de supprimer le compte.",
|
||||
)
|
||||
|
||||
objects = BaseUserManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "utilisateur·ice"
|
||||
verbose_name_plural = "utilisateur·ice·s"
|
||||
|
||||
@classmethod
|
||||
def generate_unique_public_name(cls, base_name):
|
||||
index = 0
|
||||
public_name = base_name
|
||||
while cls.objects.filter(public_name=public_name).exists():
|
||||
index += 1
|
||||
|
||||
# ensure the resulting string is not too long
|
||||
tail_length = len(str(index))
|
||||
combined_length = len(base_name) + tail_length
|
||||
if cls.MAX_NAME_LENGTH < combined_length:
|
||||
base_name = base_name[: cls.MAX_NAME_LENGTH - tail_length]
|
||||
|
||||
public_name = base_name + str(index)
|
||||
|
||||
return public_name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.public_name:
|
||||
# Fill the public name with a generated one from email address
|
||||
base_name = self.email.split("@")[0]
|
||||
self.public_name = User.generate_unique_public_name(base_name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_full_name(self):
|
||||
return self.public_name
|
||||
|
||||
def get_short_name(self):
|
||||
return self.public_name
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""Send an email to this user."""
|
||||
send_mail(subject, message, from_email, [self.email], **kwargs)
|
||||
|
||||
@classmethod
|
||||
def normalize_username(cls, username):
|
||||
return super().normalize_username(username.lower())
|
||||
|
||||
def __str__(self):
|
||||
return self.public_name
|
|
@ -3,7 +3,7 @@
|
|||
{% block "content" %}
|
||||
<h1>Paramètres du compte</h1>
|
||||
|
||||
<p>Vous êtes connecté en tant que <tt>{{ request.user.email }}</tt></p>
|
||||
<p>Vous êtes connecté en tant que <tt>{{ request.user }}</tt></p>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{% extends "small_page.html" %}
|
||||
|
||||
{% block "content" %}
|
||||
<h1>Changement de mot de passe</h1>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Changer de mot de passe</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{% extends "small_page.html" %}
|
||||
|
||||
{% block "content" %}
|
||||
<h1>Connexion par mot de passe</h1>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Connexion</button>
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "small_page.html" %}
|
||||
|
||||
{% block "content" %}
|
||||
<h1>Mode de connexion</h1>
|
||||
|
||||
<a class="button" href="{{ cas_url }}">
|
||||
Compte clipper
|
||||
</a>
|
||||
<a class="button" href="{{ pass_url }}">
|
||||
Mot de passe
|
||||
</a>
|
||||
|
||||
<p>Si vous êtes exté·e et que vous n'avez pas encore de compte, demandez en un.</p>
|
||||
{% endblock %}
|
||||
|
|
@ -1,25 +1,10 @@
|
|||
from django.urls import include, path
|
||||
import django.contrib.auth.views as dj_auth_views
|
||||
from .views import LoginView, LogoutView, PasswordChangeView, AccountSettingsView
|
||||
import django_cas_ng.views
|
||||
|
||||
from .views import AccountSettingsView, PasswordChangeView
|
||||
|
||||
app_name = "accounts"
|
||||
|
||||
cas_patterns = [
|
||||
path("login/", django_cas_ng.views.LoginView.as_view(), name="cas_ng_login"),
|
||||
path("logout/", django_cas_ng.views.LogoutView.as_view(), name="cas_ng_logout"),
|
||||
path(
|
||||
"callback/",
|
||||
django_cas_ng.views.CallbackView.as_view(),
|
||||
name="cas_ng_proxy_callback",
|
||||
),
|
||||
]
|
||||
|
||||
accounts_patterns = [
|
||||
path("cas/", include(cas_patterns)),
|
||||
path("login/", LoginView.as_view(), name="login"),
|
||||
path("logout/", LogoutView.as_view(), name="logout"),
|
||||
path("password_login/", dj_auth_views.LoginView.as_view(), name="password_login"),
|
||||
path("change_password/", PasswordChangeView.as_view(), name="change_password"),
|
||||
path("settings/", AccountSettingsView.as_view(), name="account_settings"),
|
||||
]
|
||||
|
|
|
@ -1,72 +1,14 @@
|
|||
from django.views.generic import TemplateView, RedirectView
|
||||
from django.views.generic.edit import UpdateView
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import user_logged_in, user_logged_out, user_login_failed
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import PasswordChangeView
|
||||
from django.contrib import messages
|
||||
|
||||
from urllib.parse import quote as urlquote
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.views.generic.edit import UpdateView
|
||||
|
||||
from .forms import AccountSettingsForm
|
||||
|
||||
|
||||
class LoginView(TemplateView):
|
||||
template_name = "registration/login_switch.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return redirect(self.get_next_url() or "/")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_next_url(self):
|
||||
if self.request.method == "GET":
|
||||
req_dict = self.request.GET
|
||||
elif self.request.method == "POST":
|
||||
req_dict = self.request.POST
|
||||
return req_dict.get("next")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
next_url = self.get_next_url()
|
||||
if next_url:
|
||||
context["pass_url"] = "{}?next={}".format(
|
||||
reverse("accounts:password_login"), urlquote(next_url, safe="")
|
||||
)
|
||||
context["cas_url"] = "{}?next={}".format(
|
||||
reverse("accounts:cas_ng_login"), urlquote(next_url, safe="")
|
||||
)
|
||||
else:
|
||||
context["pass_url"] = reverse("accounts:password_login")
|
||||
context["cas_url"] = reverse("accounts:cas_ng_login")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class LogoutView(RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
CAS_BACKEND_NAME = "accounts.backends.CasBackend"
|
||||
if self.request.session["_auth_user_backend"] != CAS_BACKEND_NAME:
|
||||
auth_logout(self.request)
|
||||
if "next" in self.request.GET:
|
||||
return self.request.GET["next"]
|
||||
return reverse("website:home")
|
||||
|
||||
if "next" in self.request.GET:
|
||||
return "{}?next={}".format(
|
||||
reverse("accounts:cas_ng_logout"),
|
||||
urlquote(self.request.GET["next"], safe=""),
|
||||
)
|
||||
return reverse("accounts:cas_ng_logout")
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
def on_login(request, user, **kwargs):
|
||||
messages.success(request, "Connexion réussie. Bienvenue, {}.".format(user))
|
||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class CommentsConfig(AppConfig):
|
||||
name = 'comments'
|
||||
name = "comments"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class AbstractComment(models.Model):
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<p>(Aucun commentaire sur ce jeu)</p>
|
||||
<p>(Aucun commentaire sur cet outil)</p>
|
||||
{% endfor %}
|
||||
{% if not edited_comment %}
|
||||
{% if request.user.is_authenticated %}
|
||||
|
@ -34,6 +34,6 @@
|
|||
<button type="submit"><i class="fa fa-paper-plane" aria-hidden="true"></i> Envoyer le commentaire</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p><a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour ajouter un commentaire.</p>
|
||||
<p><a href="{% url "authens:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour ajouter un commentaire.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from django.views.generic import TemplateView, RedirectView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.http import Http404
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic import RedirectView, TemplateView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
|
||||
class AddCommentView(LoginRequiredMixin, SingleObjectMixin, RedirectView):
|
||||
|
|
41
default.nix
41
default.nix
|
@ -6,14 +6,40 @@
|
|||
let
|
||||
nix-pkgs = import sources.nix-pkgs { inherit pkgs; };
|
||||
|
||||
check = (import sources.git-hooks).run {
|
||||
src = ./.;
|
||||
|
||||
hooks = {
|
||||
# Python hooks
|
||||
ruff.enable = true;
|
||||
black.enable = true;
|
||||
isort.enable = true;
|
||||
|
||||
# Nix Hooks
|
||||
statix.enable = true;
|
||||
deadnix.enable = true;
|
||||
rfc101 = {
|
||||
enable = true;
|
||||
|
||||
name = "RFC-101 formatting";
|
||||
entry = "${pkgs.lib.getExe pkgs.nixfmt-rfc-style}";
|
||||
files = "\\.nix$";
|
||||
};
|
||||
|
||||
# Misc Hooks
|
||||
commitizen.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
python3 = pkgs.python3.override {
|
||||
packageOverrides = _: _: {
|
||||
packageOverrides = final: _: {
|
||||
inherit (nix-pkgs)
|
||||
django-autoslug
|
||||
django-cas-ng
|
||||
loadcredential
|
||||
markdown-icons
|
||||
python-cas
|
||||
;
|
||||
authens = final.callPackage ./nix/authens { };
|
||||
};
|
||||
};
|
||||
in
|
||||
|
@ -22,12 +48,12 @@ in
|
|||
devShell = pkgs.mkShell {
|
||||
name = "gestiojeux.dev";
|
||||
|
||||
packages = [
|
||||
packages = check.enabledPackages ++ [
|
||||
(python3.withPackages (ps: [
|
||||
ps.django
|
||||
ps.django-types
|
||||
ps.django-autoslug
|
||||
ps.loadcredential
|
||||
ps.django-cas-ng
|
||||
ps.django-cleanup
|
||||
ps.django-haystack
|
||||
ps.django-markdownx
|
||||
|
@ -35,6 +61,10 @@ in
|
|||
ps.pillow
|
||||
ps.whoosh
|
||||
ps.markdown-icons
|
||||
ps.authens
|
||||
|
||||
ps.qrcode
|
||||
ps.pillow
|
||||
|
||||
# Django haystack is drunk
|
||||
ps.setuptools
|
||||
|
@ -48,5 +78,8 @@ in
|
|||
|
||||
GESTIOJEUX_DEBUG = builtins.toJSON true;
|
||||
};
|
||||
shellHook = ''
|
||||
${check.shellHook}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@ import os
|
|||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestiojeux.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestiojeux.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
|
|
|
@ -12,12 +12,13 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
|
|||
|
||||
import os
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from loadcredential import Credentials
|
||||
|
||||
# Secrets
|
||||
credentials = Credentials(env_prefix="GESTIOJEUX_")
|
||||
|
||||
SECRET_KEY = credentials["SECRET_KEY"]
|
||||
SECRET_KEY = credentials.get("SECRET_KEY", "insecure")
|
||||
|
||||
DEBUG = credentials.get_json(
|
||||
"DEBUG", False
|
||||
|
@ -31,36 +32,15 @@ ADMINS = credentials.get_json("ADMINS", [])
|
|||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PUBLIC_DIR = os.path.join(BASE_DIR, "public")
|
||||
|
||||
# Conditional settings
|
||||
if DEBUG:
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
else:
|
||||
EMAIL_HOST = "clipper.ens.fr"
|
||||
SERVER_EMAIL = credentials["SERVER_EMAIL"]
|
||||
DEFAULT_FROM_EMAIL = credentials["DEFAULT_FROM_EMAIL"]
|
||||
|
||||
# HTTPS only
|
||||
CSRF_COOKIE_SECURE = True
|
||||
SESSION_COOKIE_SECURE = True
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql_psycopg2",
|
||||
"NAME": credentials["DB_NAME"],
|
||||
"USER": credentials["DB_USER"],
|
||||
}
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Search engine
|
||||
|
@ -80,7 +60,6 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_cas_ng",
|
||||
"django_tables2",
|
||||
"markdownx",
|
||||
"haystack",
|
||||
|
@ -88,9 +67,10 @@ INSTALLED_APPS = [
|
|||
"accounts",
|
||||
"comments",
|
||||
"inventory",
|
||||
"suggestions",
|
||||
# "suggestions",
|
||||
"loans",
|
||||
"django_cleanup", # Keep last
|
||||
"authens",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -101,7 +81,6 @@ MIDDLEWARE = [
|
|||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_cas_ng.middleware.CASMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "gestiojeux.urls"
|
||||
|
@ -124,11 +103,12 @@ TEMPLATES = [
|
|||
|
||||
WSGI_APPLICATION = "gestiojeux.wsgi.application"
|
||||
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
# Authentication backends
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"accounts.backends.CasBackend",
|
||||
"authens.backends.ENSCASBackend",
|
||||
"authens.backends.OldCASBackend",
|
||||
)
|
||||
|
||||
# Password validation
|
||||
|
@ -143,7 +123,11 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||
]
|
||||
|
||||
LOGIN_URL = "accounts:login"
|
||||
LOGIN_URL = reverse_lazy("authens:login")
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy("website:home")
|
||||
|
||||
LOGOUT_REDIRECT_URL = reverse_lazy("website:home")
|
||||
|
||||
# Use markdown extensions
|
||||
MARKDOWNX_MARKDOWN_EXTENSIONS = [
|
||||
|
@ -185,12 +169,7 @@ MEDIA_URL = "/media/"
|
|||
STATIC_ROOT = os.path.join(PUBLIC_DIR, "static")
|
||||
MEDIA_ROOT = os.path.join(PUBLIC_DIR, "media")
|
||||
|
||||
# CAS settings
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
CAS_SERVER_URL = "https://cas.eleves.ens.fr/"
|
||||
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
|
||||
CAS_VERSION = "CAS_2_SAML_1_0"
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_LOGIN_MSG = None
|
||||
CAS_LOGIN_URL_NAME = "accounts:cas_ng_login"
|
||||
CAS_LOGOUT_URL_NAME = "accounts:cas_ng_logout"
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
|
|
@ -13,17 +13,19 @@ Including another URLconf
|
|||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("markdownx/", include("markdownx.urls")),
|
||||
path("inventory/", include("inventory.urls")),
|
||||
path("suggestions/", include("suggestions.urls")),
|
||||
# path("suggestions/", include("suggestions.urls")),
|
||||
path("account/", include("accounts.urls")),
|
||||
path("authens/", include("authens.urls")),
|
||||
path("", include("website.urls")),
|
||||
]
|
||||
|
||||
|
|
|
@ -11,6 +11,6 @@ import os
|
|||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestiojeux.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestiojeux.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib import admin
|
||||
from .models import Category, Tag, Game, GameComment
|
||||
|
||||
from comments.admin import CommentAdmin
|
||||
|
||||
from .models import Category, Game, GameComment, Tag
|
||||
|
||||
admin.site.register(Category)
|
||||
admin.site.register(Tag)
|
||||
admin.site.register(Game)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from loans.forms import BorrowForm
|
||||
|
||||
|
||||
class BorrowGameForm(BorrowForm):
|
||||
error_css_class = "errorfield"
|
||||
required_css_class = "requiredfield"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import markdown
|
||||
from django.template.loader import get_template
|
||||
from .models import Category, Tag, Game
|
||||
|
||||
from .models import Category, Game, Tag
|
||||
|
||||
|
||||
class InventoryLinkProcessor(markdown.inlinepatterns.InlineProcessor):
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# Generated by Django 3.1.2 on 2020-12-29 23:21
|
||||
|
||||
import autoslug.fields
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
import website.validators
|
||||
|
||||
|
||||
|
@ -17,71 +18,216 @@ class Migration(migrations.Migration):
|
|||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
name="Category",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=256, unique=True, verbose_name='nom')),
|
||||
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(max_length=256, unique=True, verbose_name="nom"),
|
||||
),
|
||||
(
|
||||
"slug",
|
||||
autoslug.fields.AutoSlugField(
|
||||
editable=False, populate_from="name", unique=True
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'catégorie',
|
||||
'ordering': ['name'],
|
||||
"verbose_name": "catégorie",
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Game',
|
||||
name="Game",
|
||||
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)),
|
||||
('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', models.CharField(blank=True, help_text='Affichage personnalisé pour le nombre de joueur·se·s', max_length=256, verbose_name='nombre de joueur·se·s')),
|
||||
('duration', models.CharField(blank=True, 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, verbose_name='description')),
|
||||
('image', models.ImageField(blank=True, help_text="L'image doit peser 512 Kio au maximum", upload_to='game_img/', validators=[website.validators.MaxFileSizeValidator(512)], verbose_name='image')),
|
||||
('missing_elements', models.TextField(blank=True, verbose_name='pièces manquantes')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='inventory.category', verbose_name='catégorie')),
|
||||
(
|
||||
"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
|
||||
),
|
||||
),
|
||||
(
|
||||
"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",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Affichage personnalisé pour le nombre de joueur·se·s",
|
||||
max_length=256,
|
||||
verbose_name="nombre de joueur·se·s",
|
||||
),
|
||||
),
|
||||
(
|
||||
"duration",
|
||||
models.CharField(
|
||||
blank=True, 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, verbose_name="description"),
|
||||
),
|
||||
(
|
||||
"image",
|
||||
models.ImageField(
|
||||
blank=True,
|
||||
help_text="L'image doit peser 512 Kio au maximum",
|
||||
upload_to="game_img/",
|
||||
validators=[website.validators.MaxFileSizeValidator(512)],
|
||||
verbose_name="image",
|
||||
),
|
||||
),
|
||||
(
|
||||
"missing_elements",
|
||||
models.TextField(blank=True, verbose_name="pièces manquantes"),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.RESTRICT,
|
||||
to="inventory.category",
|
||||
verbose_name="catégorie",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'jeu',
|
||||
'verbose_name_plural': 'jeux',
|
||||
'ordering': ['title'],
|
||||
"verbose_name": "jeu",
|
||||
"verbose_name_plural": "jeux",
|
||||
"ordering": ["title"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
name="Tag",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=256, unique=True, verbose_name='nom')),
|
||||
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(max_length=256, unique=True, verbose_name="nom"),
|
||||
),
|
||||
(
|
||||
"slug",
|
||||
autoslug.fields.AutoSlugField(
|
||||
editable=False, populate_from="name", unique=True
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
"ordering": ["name"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GameComment',
|
||||
name="GameComment",
|
||||
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='inventory.game', verbose_name='jeu')),
|
||||
(
|
||||
"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="inventory.game",
|
||||
verbose_name="jeu",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'commentaire sur un jeu',
|
||||
'verbose_name_plural': 'commentaires sur des jeux',
|
||||
'ordering': ['created_on'],
|
||||
"verbose_name": "commentaire sur un jeu",
|
||||
"verbose_name_plural": "commentaires sur des jeux",
|
||||
"ordering": ["created_on"],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='game',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, to='inventory.Tag', verbose_name='tags'),
|
||||
model_name="game",
|
||||
name="tags",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, to="inventory.Tag", verbose_name="tags"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,32 +1,52 @@
|
|||
# Generated by Django 4.2.8 on 2024-05-02 09:30
|
||||
|
||||
import autoslug.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0002_duration_range'),
|
||||
("inventory", "0002_duration_range"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GameLoan',
|
||||
name="GameLoan",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='lent_object', unique=True)),
|
||||
('borrow_date', models.DateTimeField(auto_now_add=True)),
|
||||
('return_date', models.DateTimeField(null=True)),
|
||||
('mail', models.EmailField(max_length=254)),
|
||||
('lent_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.game', verbose_name='jeu emprunté')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"slug",
|
||||
autoslug.fields.AutoSlugField(
|
||||
editable=False, populate_from="lent_object", unique=True
|
||||
),
|
||||
),
|
||||
("borrow_date", models.DateTimeField(auto_now_add=True)),
|
||||
("return_date", models.DateTimeField(null=True)),
|
||||
("mail", models.EmailField(max_length=254)),
|
||||
(
|
||||
"lent_object",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.game",
|
||||
verbose_name="jeu emprunté",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'emprunt',
|
||||
'verbose_name_plural': 'emprunts',
|
||||
'ordering': ['borrow_date'],
|
||||
'abstract': False,
|
||||
"verbose_name": "emprunt",
|
||||
"verbose_name_plural": "emprunts",
|
||||
"ordering": ["borrow_date"],
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,26 +6,33 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0003_gameloan'),
|
||||
("inventory", "0003_gameloan"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='category',
|
||||
options={'ordering': ['name'], 'verbose_name': 'étagère'},
|
||||
name="category",
|
||||
options={"ordering": ["name"], "verbose_name": "étagère"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='gameloan',
|
||||
options={'ordering': ['borrow_date'], 'permissions': [('can_see_loan_details', 'Can see loan details')], 'verbose_name': 'emprunt', 'verbose_name_plural': 'emprunts'},
|
||||
name="gameloan",
|
||||
options={
|
||||
"ordering": ["borrow_date"],
|
||||
"permissions": [("can_see_loan_details", "Can see loan details")],
|
||||
"verbose_name": "emprunt",
|
||||
"verbose_name_plural": "emprunts",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gameloan',
|
||||
name='borrow_date',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Date d’emprunt'),
|
||||
model_name="gameloan",
|
||||
name="borrow_date",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Date d’emprunt"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gameloan',
|
||||
name='return_date',
|
||||
field=models.DateTimeField(null=True, verbose_name='Date de retour'),
|
||||
model_name="gameloan",
|
||||
name="return_date",
|
||||
field=models.DateTimeField(null=True, verbose_name="Date de retour"),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-02 20:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("inventory", "0004_alter_category_options_alter_gameloan_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="category",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="gamecomment",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="gameloan",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="tag",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,95 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-02 22:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"inventory",
|
||||
"0005_alter_category_id_alter_game_id_alter_gamecomment_id_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="game",
|
||||
options={
|
||||
"ordering": ["title"],
|
||||
"verbose_name": "outil",
|
||||
"verbose_name_plural": "outils",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="gamecomment",
|
||||
options={
|
||||
"ordering": ["created_on"],
|
||||
"verbose_name": "commentaire sur un outil",
|
||||
"verbose_name_plural": "commentaires sur des outils",
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="duration",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="duration_max",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="duration_min",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="editor",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="game_designer",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="illustrator",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="nb_player_max",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="nb_player_min",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="game",
|
||||
name="player_range",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="title",
|
||||
field=models.CharField(
|
||||
max_length=256, unique=True, verbose_name="Nom de l'outil"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="gamecomment",
|
||||
name="commented_object",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="comments",
|
||||
to="inventory.game",
|
||||
verbose_name="outil",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="gameloan",
|
||||
name="lent_object",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="inventory.game",
|
||||
verbose_name="outil emprunté",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,10 +1,13 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
from autoslug import AutoSlugField
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
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
|
||||
from website.validators import MaxFileSizeValidator
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
|
@ -36,47 +39,21 @@ class Tag(models.Model):
|
|||
return reverse("inventory:tag", args=(self.slug,))
|
||||
|
||||
|
||||
def image_uuid_path(instance, filename):
|
||||
"""
|
||||
Compute filename for game images as follow:
|
||||
|
||||
game_img/{random_uuid}.{extension}
|
||||
"""
|
||||
ext = filename.split(".")[-1]
|
||||
filename = "%s.%s" % (uuid.uuid4(), ext)
|
||||
return os.path.join("uploads/logos", filename)
|
||||
|
||||
|
||||
class Game(models.Model):
|
||||
title = models.CharField(verbose_name="titre du jeu", max_length=256, unique=True)
|
||||
title = models.CharField(verbose_name="Nom de l'outil", max_length=256, unique=True)
|
||||
slug = AutoSlugField(populate_from="title", unique=True)
|
||||
|
||||
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 = models.CharField(
|
||||
max_length=256,
|
||||
blank=True,
|
||||
help_text="Affichage personnalisé pour le nombre de joueur·se·s",
|
||||
verbose_name="nombre de joueur·se·s",
|
||||
)
|
||||
|
||||
duration_min = models.PositiveSmallIntegerField(
|
||||
verbose_name="durée de partie minimale",
|
||||
help_text="En minutes, telle qu'indiquée par l'éditeur",
|
||||
)
|
||||
duration_max = models.PositiveSmallIntegerField(
|
||||
verbose_name="durée de partie maximale",
|
||||
help_text="En minutes, telle qu'indiquée par l'éditeur, identique à la durée minimale si laissée vide",
|
||||
blank=True,
|
||||
)
|
||||
duration = models.CharField(
|
||||
max_length=256,
|
||||
blank=True,
|
||||
help_text="Affichage personnalisé pour la durée de la partie",
|
||||
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.RESTRICT, verbose_name="catégorie"
|
||||
)
|
||||
|
@ -84,7 +61,7 @@ class Game(models.Model):
|
|||
|
||||
description = models.TextField(blank=True, verbose_name="description")
|
||||
image = models.ImageField(
|
||||
upload_to="game_img/",
|
||||
upload_to=image_uuid_path,
|
||||
blank=True,
|
||||
verbose_name="image",
|
||||
help_text="L'image doit peser 512 Kio au maximum",
|
||||
|
@ -95,45 +72,17 @@ class Game(models.Model):
|
|||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
verbose_name = "jeu"
|
||||
verbose_name_plural = "jeux"
|
||||
verbose_name = "outil"
|
||||
verbose_name_plural = "outils"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def clean(self):
|
||||
if not self.nb_player_min or not self.nb_player_max or not self.duration_min:
|
||||
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"
|
||||
}
|
||||
)
|
||||
if self.duration_max is None:
|
||||
self.duration_max = self.duration_min
|
||||
if self.duration_min > self.duration_max:
|
||||
raise ValidationError(
|
||||
{
|
||||
"duration_max": "La durée maximale doit être supérieure à la durée minimale"
|
||||
}
|
||||
)
|
||||
|
||||
def get_player_range(self):
|
||||
if self.player_range:
|
||||
return self.player_range
|
||||
elif self.nb_player_min != self.nb_player_max:
|
||||
return "{} à {} joueur·se·s".format(self.nb_player_min, self.nb_player_max)
|
||||
else:
|
||||
return "{} joueur·se·s".format(self.nb_player_min)
|
||||
return ""
|
||||
|
||||
def get_duration_range(self):
|
||||
if self.duration:
|
||||
return self.duration
|
||||
elif self.duration_min != self.duration_max:
|
||||
return "{} à {} min".format(self.duration_min, self.duration_max)
|
||||
else:
|
||||
return "{} min".format(self.duration_min)
|
||||
return ""
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("inventory:game", args=(self.slug,))
|
||||
|
@ -141,26 +90,25 @@ class Game(models.Model):
|
|||
|
||||
class GameComment(AbstractComment):
|
||||
commented_object = models.ForeignKey(
|
||||
Game, on_delete=models.CASCADE, related_name="comments", verbose_name="jeu"
|
||||
Game, on_delete=models.CASCADE, related_name="comments", verbose_name="outil"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["created_on"]
|
||||
verbose_name = "commentaire sur un jeu"
|
||||
verbose_name_plural = "commentaires sur des jeux"
|
||||
verbose_name = "commentaire sur un outil"
|
||||
verbose_name_plural = "commentaires sur des outils"
|
||||
|
||||
def get_modification_url(self):
|
||||
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é"
|
||||
Game, on_delete=models.CASCADE, verbose_name="outil emprunté"
|
||||
)
|
||||
|
||||
class Meta(AbstractLoan.Meta):
|
||||
abstract = False
|
||||
permissions = [("can_see_loan_details", "Can see loan details")]
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from haystack import indexes
|
||||
from .models import Category, Tag, Game
|
||||
|
||||
from .models import Category, Game, Tag
|
||||
|
||||
|
||||
class CategoryIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import django_tables2 as tables
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .models import GameLoan
|
||||
|
||||
|
||||
class LoanTable(tables.Table):
|
||||
next_pattern = "inventory:all_loans"
|
||||
|
||||
|
@ -10,24 +12,29 @@ class LoanTable(tables.Table):
|
|||
model = GameLoan
|
||||
sequence = ("lent_object", "borrow_date", "return_date", "mail")
|
||||
exclude = ("id",)
|
||||
|
||||
slug = tables.Column(verbose_name="Actions", orderable=False)
|
||||
|
||||
def render_lent_object(self, value, record):
|
||||
return format_html("<a href='{}'>{}</a>",
|
||||
reverse("inventory:game", args=[record.lent_object.slug]), value)
|
||||
return format_html(
|
||||
"<a href='{}'>{}</a>",
|
||||
reverse("inventory:game", args=[record.lent_object.slug]),
|
||||
value,
|
||||
)
|
||||
|
||||
def render_slug(self, value, record):
|
||||
res = ""
|
||||
if record.return_date == None:
|
||||
res = format_html("<a class='button' href='{}?next={}'>Rendre le jeu</a>",
|
||||
if record.return_date is None:
|
||||
res = format_html(
|
||||
"<a class='button' href='{}?next={}'>Rendre l'outil</a>",
|
||||
reverse("inventory:return_game", args=[value]),
|
||||
reverse(self.next_pattern))
|
||||
reverse(self.next_pattern),
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
|
||||
class OngoingLoansTable(LoanTable):
|
||||
next_pattern = "inventory:ongoing_loans"
|
||||
|
||||
class Meta(LoanTable.Meta):
|
||||
exclude = ("return_date", "mail", "id")
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h1><i class="fa fa-bookmark" aria-hidden="true"></i> {{ category.name }}</h1>
|
||||
|
||||
{% with game_list=category.game_set.all %}
|
||||
<p>Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} dans cette
|
||||
<p>Il y a {{ game_list|length }} outil{{ game_list|pluralize:"s" }} dans cette
|
||||
étagère :</p>
|
||||
{% for game in game_list %}
|
||||
{% include "./partials/game_item.html" %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% block "content" %}
|
||||
<h1>Liste des étagères</h1>
|
||||
|
||||
<p>Il y a {{ paginator.count }} étagère{{ paginator.count|pluralize }} de jeux :</p>
|
||||
<p>Il y a {{ paginator.count }} étagère{{ paginator.count|pluralize }} d'outils :</p>
|
||||
{% include "partials/pagination.html" %}
|
||||
|
||||
{% for category in category_list %}
|
||||
|
|
|
@ -12,9 +12,6 @@
|
|||
<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 %}
|
||||
|
@ -23,19 +20,19 @@
|
|||
{% endfor %}
|
||||
</p>
|
||||
<hr/>
|
||||
<p><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> {{ game.game_designer|default:"(Game designer inconnu·e)" }}</p>
|
||||
<p><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> {{ game.illustrator|default:"(Illustrateur·trice inconnu·e)" }}</p>
|
||||
<p><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> {{ game.editor|default:"(Éditeur inconnu)" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_borrowed %}
|
||||
<p class="warning">Ce jeu est emprunté depuis le {{ loan.borrow_date }}.</p>
|
||||
<p class="warning">Cet outil 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>
|
||||
<a class="button" href="{% url "inventory:qrcode_borrow" game.slug %}">
|
||||
Générer un QR-code
|
||||
</a>
|
||||
|
||||
<h2 id="description">Description</h2>
|
||||
{{ object.description|linebreaks }}
|
||||
|
@ -45,7 +42,7 @@
|
|||
<p class="warning">{{ game.missing_elements|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Commentaires et propositions de variantes</h2>
|
||||
<h2>Commentaires</h2>
|
||||
{% url "inventory:add_game_comment" game.slug as add_comment_url %}
|
||||
{% include "comments.html" with comments=game.comments.all %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block "content" %}
|
||||
<h1>Liste des jeux</h1>
|
||||
<h1>Liste des outils</h1>
|
||||
|
||||
<p>Il y a {{ paginator.count }} jeu{{ paginator.count|pluralize:"x" }} en salle jeux :</p>
|
||||
<p>Il y a {{ paginator.count }} outil{{ paginator.count|pluralize:"s" }} en Hackens :</p>
|
||||
{% include "partials/pagination.html" %}
|
||||
|
||||
{% for game in game_list %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block "content" %}
|
||||
<h1>Inventaire du club Jeux</h1>
|
||||
<h1>Inventaire d'HackENS</h1>
|
||||
|
||||
Rechercher dans la ludothèque:
|
||||
Rechercher dans les outils:
|
||||
<form class="search" method="get" action="{% url "inventory:search" %}">
|
||||
<input type="search" name="q" />
|
||||
<button type="submit"><i class="fa fa-fw fa-search" aria-hidden="true"></i></button>
|
||||
|
@ -17,19 +17,19 @@
|
|||
<a class="button" href="{% url "inventory:category_list" %}">
|
||||
Liste des étagères
|
||||
<p class="helptext">
|
||||
Chaque jeu est rangé dans une unique étagère comme en salle Jeux
|
||||
Chaque outils est rangé dans une étagère
|
||||
</p>
|
||||
</a>
|
||||
<a class="button" href="{% url "inventory:tag_list" %}">
|
||||
Liste des tags
|
||||
<p class="helptext">
|
||||
Chaque jeu est marqué par tous les tags qui lui correspondent
|
||||
Chaque outils est marqué par tous les tags qui lui correspondent
|
||||
</p>
|
||||
</a>
|
||||
<a class="button" href="{% url "inventory:game_list" %}">
|
||||
Liste alphabétique
|
||||
<p class="helptext">
|
||||
La liste complète des jeux de la ludothèque
|
||||
La liste complète des outils
|
||||
</p>
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{ form.as_p }}
|
||||
<button type="submit">Emprunter</button>
|
||||
<br/>
|
||||
Vos données seront traitées pour les besoins de la gestion de la salle Jeux.
|
||||
Pour toute demande, contactez : <tt class="antispam">rf.sne@xuejopser</tt>
|
||||
Vos données seront traitées pour les besoins de la gestion du parc d'outils d'hackens.
|
||||
Pour toute demande, contactez : <tt class="antispam">rf.sne@snekcah</tt>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<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
|
||||
Si l'outil est emprunté par quelqu’un d’autre, il sera rendu
|
||||
automatiquement.
|
||||
</p>
|
||||
</a>
|
||||
|
@ -39,12 +39,12 @@ automatiquement.
|
|||
<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 }}.
|
||||
Ceit outil est emprunté depuis le {{ loan.borrow_date }}.
|
||||
</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="button" href="/inventory/game/{{ game.slug }}/">
|
||||
Détails du jeu
|
||||
Détails de l'outil
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
<a class="inventory_item game" href="{{ game.get_absolute_url }}">
|
||||
<span class="title">{{ game.title }}</span>
|
||||
<span class="details">
|
||||
<span><i class="fa fa-users" aria-hidden="true"></i> {{ game.get_player_range }}</span>
|
||||
<span><i class="fa fa-clock-o" aria-hidden="true"></i> {{ game.get_duration_range }}</span>
|
||||
<span><i class="fa fa-bookmark"></i> {{ game.category }}</span>
|
||||
{% for tag in game.tags.all %}
|
||||
<span><i class="fa fa-tag" aria-hidden="true"></i> {{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% if game.game_designer %}
|
||||
<span><i class="fa fa-wrench" aria-hidden="true"></i> {{ game.game_designer }}</span>
|
||||
{% endif %}
|
||||
{% if game.illustrator %}
|
||||
<span><i class="fa fa-paint-brush" aria-hidden="true"></i> {{ game.illustrator }}</span>
|
||||
{% endif %}
|
||||
{% if game.editor %}
|
||||
<span><i class="fa fa-cogs" aria-hidden="true"></i> {{ game.editor }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<h1><i class="fa fa-tag" aria-hidden="true"></i> {{ tag.name }}</h1>
|
||||
|
||||
{% with game_list=tag.game_set.all %}
|
||||
<p>Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} marqué{{ game_list|pluralize }} avec ce tag :</p>
|
||||
<p>Il y a {{ game_list|length }} outil{{ game_list|pluralize:"s" }} marqué{{ game_list|pluralize }} avec ce tag :</p>
|
||||
{% for game in game_list %}
|
||||
{% include "./partials/game_item.html" %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import (
|
||||
InventoryView,
|
||||
AddGameCommentView,
|
||||
BorrowGameView,
|
||||
CategoryListView,
|
||||
CategoryView,
|
||||
DetailLoanView,
|
||||
GameListView,
|
||||
GameLoanView,
|
||||
GameView,
|
||||
InventorySearchView,
|
||||
InventoryView,
|
||||
ModifyGameCommentView,
|
||||
OngoingLoansView,
|
||||
QrCodeView,
|
||||
ReturnGameView,
|
||||
TagListView,
|
||||
TagView,
|
||||
GameListView,
|
||||
GameView,
|
||||
AddGameCommentView,
|
||||
ModifyGameCommentView,
|
||||
InventorySearchView,
|
||||
GameLoanView,
|
||||
BorrowGameView,
|
||||
ReturnGameView,
|
||||
OngoingLoansView,
|
||||
DetailLoanView,
|
||||
)
|
||||
|
||||
app_name = "inventory"
|
||||
|
@ -39,6 +41,11 @@ urlpatterns = [
|
|||
path("loans/game/<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"),
|
||||
path(
|
||||
"qrcode/borrow/<slug>/",
|
||||
QrCodeView.as_view(url="inventory:borrow_game"),
|
||||
name="qrcode_borrow",
|
||||
),
|
||||
path("loans/ongoing/", OngoingLoansView.as_view(), name="ongoing_loans"),
|
||||
path("loans/all/", DetailLoanView.as_view(), name="all_loans"),
|
||||
]
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
from django.views.generic import TemplateView, ListView, DetailView
|
||||
import qrcode
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from haystack.generic_views import SearchView
|
||||
from haystack.forms import SearchForm
|
||||
from haystack.query import SearchQuerySet
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
from django_tables2.views import SingleTableView
|
||||
from haystack.forms import SearchForm
|
||||
from haystack.generic_views import SearchView
|
||||
from haystack.query import SearchQuerySet
|
||||
|
||||
from comments.views import AddCommentView, ModifyCommentView
|
||||
from loans.views import BorrowView, ReturnView, DetailLoanView
|
||||
from .models import Category, Tag, Game, GameComment, GameLoan
|
||||
from loans.views import BorrowView, DetailLoanView, ReturnView
|
||||
|
||||
from .forms import BorrowGameForm
|
||||
from .models import Category, Game, GameComment, GameLoan, Tag
|
||||
from .tables import LoanTable, OngoingLoansTable
|
||||
|
||||
|
||||
|
@ -102,3 +107,13 @@ class DetailLoanView(PermissionRequiredMixin, SingleTableView):
|
|||
table_class = LoanTable
|
||||
template_name = "inventory/loans/loans_table.html"
|
||||
|
||||
|
||||
class QrCodeView(DetailView):
|
||||
model = Game
|
||||
url = "inventory:game" # Sensible default
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = HttpResponse(content_type="image/png")
|
||||
img = qrcode.make(reverse(self.url, kwargs={"slug": self.get_object().slug}))
|
||||
img.save(response, "PNG")
|
||||
return response
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
|
||||
class LoanAdmin(admin.ModelAdmin):
|
||||
list_display = ("lent_object", "borrow_date", "return_date")
|
||||
ordering = ("-borrow_date",)
|
||||
|
|
|
@ -2,5 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class LoansConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'loans'
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "loans"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class BorrowForm(forms.Form):
|
||||
mail = forms.EmailField(label="Mail")
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# 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
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -10,24 +10,45 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0002_duration_range'),
|
||||
("inventory", "0002_duration_range"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Loan',
|
||||
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é')),
|
||||
(
|
||||
"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'],
|
||||
"verbose_name": "emprunt",
|
||||
"verbose_name_plural": "emprunts",
|
||||
"ordering": ["borrow_date"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,11 +6,11 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('loans', '0001_initial'),
|
||||
("loans", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Loan',
|
||||
name="Loan",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from django.db import models
|
||||
from autoslug import AutoSlugField
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
|
||||
|
||||
class AbstractLoan(models.Model):
|
||||
lent_object = None # Fill this with a foreign key in subclasses
|
||||
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, verbose_name="Date d’emprunt")
|
||||
borrow_date = models.DateTimeField(auto_now_add=True, verbose_name="Date d’emprunt")
|
||||
return_date = models.DateTimeField(null=True, verbose_name="Date de retour")
|
||||
mail = models.EmailField()
|
||||
|
||||
|
@ -14,7 +14,7 @@ class AbstractLoan(models.Model):
|
|||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering=["borrow_date"]
|
||||
ordering = ["borrow_date"]
|
||||
verbose_name = "emprunt"
|
||||
verbose_name_plural = "emprunts"
|
||||
|
||||
|
@ -26,9 +26,9 @@ class AbstractLoan(models.Model):
|
|||
self.save()
|
||||
|
||||
@classmethod
|
||||
def ongoing_loans(cls, obj = None):
|
||||
def ongoing_loans(cls, obj=None):
|
||||
ongoing = cls.objects.filter(return_date=None)
|
||||
if obj != None:
|
||||
if obj is not None:
|
||||
return ongoing.filter(lent_object=obj)
|
||||
else:
|
||||
return ongoing
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
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 django.views.generic import DetailView, FormView, RedirectView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from .forms import BorrowForm
|
||||
|
||||
|
||||
class ReturnView(SingleObjectMixin, RedirectView):
|
||||
# Inherited classes should contain:
|
||||
# model = LoanModel
|
||||
# pattern_name =
|
||||
# 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)
|
||||
kwargs[self.redirect_slug_field] = getattr(
|
||||
loan.lent_object, loan.lent_object_slug_field
|
||||
)
|
||||
messages.success(self.request, "Rendu effectué.")
|
||||
if "next" in self.request.GET:
|
||||
return self.request.GET["next"]
|
||||
|
@ -31,7 +31,7 @@ class BorrowView(SingleObjectMixin, FormView):
|
|||
# model = LentObjectModel
|
||||
# loan_model = LoanModel
|
||||
# template_name = "path/to/template.html"
|
||||
form_class = BorrowForm # Update this for a more complex form
|
||||
form_class = BorrowForm # Update this for a more complex form
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
|
@ -52,21 +52,22 @@ class BorrowView(SingleObjectMixin, FormView):
|
|||
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))
|
||||
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
|
||||
context["is_borrowed"] = is_borrowed
|
||||
if is_borrowed:
|
||||
context["loan"] = loans.get()
|
||||
return context
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import sys
|
|||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestiojeux.settings')
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestiojeux.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
@ -17,5 +17,5 @@ def main():
|
|||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
15
nix/authens/01-get-success_url.patch
Normal file
15
nix/authens/01-get-success_url.patch
Normal file
|
@ -0,0 +1,15 @@
|
|||
diff --git a/authens/views.py b/authens/views.py
|
||||
index 0478861..b1c93e9 100644
|
||||
--- a/authens/views.py
|
||||
+++ b/authens/views.py
|
||||
@@ -138,8 +138,8 @@ class LogoutView(auth_views.LogoutView):
|
||||
else:
|
||||
self.cas_connected = False
|
||||
|
||||
- def get_next_page(self):
|
||||
- next_page = super().get_next_page()
|
||||
+ def get_success_url(self):
|
||||
+ next_page = super().get_success_url()
|
||||
if self.cas_connected:
|
||||
cas_client = get_cas_client(self.request)
|
||||
|
22
nix/authens/default.nix
Normal file
22
nix/authens/default.nix
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
python-cas,
|
||||
django,
|
||||
ldap,
|
||||
buildPythonPackage,
|
||||
}:
|
||||
buildPythonPackage rec {
|
||||
pname = "authens";
|
||||
version = "v0.1b5";
|
||||
doCheck = false;
|
||||
patches = [ ./01-get-success_url.patch ];
|
||||
src = builtins.fetchGit {
|
||||
url = "https://git.eleves.ens.fr/klub-dev-ens/authens.git";
|
||||
#rev = "master";
|
||||
#sha256 = "sha256-R0Nw212/BOPHfpspT5wzxtji1vxZ/JOuwr00naklWE8=";
|
||||
};
|
||||
propagatedBuildInputs = [
|
||||
django
|
||||
ldap
|
||||
python-cas
|
||||
];
|
||||
}
|
23
nix/python-cas/default.nix
Normal file
23
nix/python-cas/default.nix
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
requests,
|
||||
lxml,
|
||||
six,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
}:
|
||||
buildPythonPackage rec {
|
||||
pname = "python-cas";
|
||||
version = "1.6.0";
|
||||
doCheck = false;
|
||||
src = fetchFromGitHub {
|
||||
owner = "python-cas";
|
||||
repo = "python-cas";
|
||||
rev = "v1.6.0";
|
||||
sha512 = "sha512-qnYzgwELUij2EdqA6H17q8vnNUsfI7DkbZSI8CCIGfXOM+cZ7vsWe7CJxzsDUw73sBPB4+zzpLxvb7tpm/IDeg==";
|
||||
};
|
||||
propagatedBuildInputs = [
|
||||
requests
|
||||
lxml
|
||||
six
|
||||
];
|
||||
}
|
|
@ -1,20 +1,34 @@
|
|||
# Generated by npins. Do not modify; will be overwritten regularly
|
||||
let
|
||||
data = builtins.fromJSON (builtins.readFile ./sources.json);
|
||||
version = data.version;
|
||||
inherit (data) version;
|
||||
|
||||
mkSource = spec:
|
||||
assert spec ? type; let
|
||||
mkSource =
|
||||
spec:
|
||||
assert spec ? type;
|
||||
let
|
||||
path =
|
||||
if spec.type == "Git" then mkGitSource spec
|
||||
else if spec.type == "GitRelease" then mkGitSource spec
|
||||
else if spec.type == "PyPi" then mkPyPiSource spec
|
||||
else if spec.type == "Channel" then mkChannelSource spec
|
||||
else builtins.throw "Unknown source type ${spec.type}";
|
||||
if spec.type == "Git" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "GitRelease" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "PyPi" then
|
||||
mkPyPiSource spec
|
||||
else if spec.type == "Channel" then
|
||||
mkChannelSource spec
|
||||
else
|
||||
builtins.throw "Unknown source type ${spec.type}";
|
||||
in
|
||||
spec // { outPath = path; };
|
||||
|
||||
mkGitSource = { repository, revision, url ? null, hash, ... }:
|
||||
mkGitSource =
|
||||
{
|
||||
repository,
|
||||
revision,
|
||||
url ? null,
|
||||
hash,
|
||||
...
|
||||
}:
|
||||
assert repository ? type;
|
||||
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
|
||||
# In the latter case, there we will always be an url to the tarball
|
||||
|
@ -23,19 +37,23 @@ let
|
|||
inherit url;
|
||||
sha256 = hash; # FIXME: check nix version & use SRI hashes
|
||||
})
|
||||
else assert repository.type == "Git"; builtins.fetchGit {
|
||||
url = repository.url;
|
||||
rev = revision;
|
||||
# hash = hash;
|
||||
};
|
||||
else
|
||||
assert repository.type == "Git";
|
||||
builtins.fetchGit {
|
||||
inherit (repository) url;
|
||||
rev = revision;
|
||||
# hash = hash;
|
||||
};
|
||||
|
||||
mkPyPiSource = { url, hash, ... }:
|
||||
mkPyPiSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchurl {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkChannelSource = { url, hash, ... }:
|
||||
mkChannelSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
{
|
||||
"pins": {
|
||||
"git-hooks": {
|
||||
"type": "Git",
|
||||
"repository": {
|
||||
"type": "GitHub",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix"
|
||||
},
|
||||
"branch": "master",
|
||||
"revision": "0ff4381bbb8f7a52ca4a851660fc7a437a4c6e07",
|
||||
"url": "https://github.com/cachix/git-hooks.nix/archive/0ff4381bbb8f7a52ca4a851660fc7a437a4c6e07.tar.gz",
|
||||
"hash": "0bmgc731c5rvky6qxc4f6gvgyiic8dna5dv3j19kya86idf7wn0p"
|
||||
},
|
||||
"nix-pkgs": {
|
||||
"type": "Git",
|
||||
"repository": {
|
||||
|
@ -19,4 +31,4 @@
|
|||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
}
|
||||
|
|
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[tool.isort]
|
||||
profile = "black"
|
|
@ -7,5 +7,6 @@ django-markdownx==4.0.5
|
|||
django-tables2==2.7.0
|
||||
markdown-iconfonts==3.0.0
|
||||
Pillow==10.1.0
|
||||
qrcode>=7.4.2
|
||||
Whoosh==2.7.4
|
||||
loadcredential==1.1
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib import admin
|
||||
from .models import Suggestion, SuggestionComment
|
||||
|
||||
from comments.admin import CommentAdmin
|
||||
|
||||
from .models import Suggestion, SuggestionComment
|
||||
|
||||
|
||||
class SuggestionAdmin(admin.ModelAdmin):
|
||||
exclude = ("upvoting_users",)
|
||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class SuggestionsConfig(AppConfig):
|
||||
name = 'suggestions'
|
||||
name = "suggestions"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django import forms
|
||||
|
||||
from .models import Suggestion
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# 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
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import website.validators
|
||||
|
||||
|
||||
|
@ -13,52 +14,189 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0001_initial'),
|
||||
("inventory", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Suggestion',
|
||||
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')),
|
||||
(
|
||||
"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'],
|
||||
"verbose_name": "suggestion de jeu",
|
||||
"verbose_name_plural": "suggestions de jeux",
|
||||
"ordering": ["title"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SuggestionComment',
|
||||
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')),
|
||||
(
|
||||
"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'],
|
||||
"verbose_name": "commentaire sur une suggestion",
|
||||
"verbose_name_plural": "commentaires sur des suggestions",
|
||||
"ordering": ["created_on"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -6,29 +6,43 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('suggestions', '0001_initial'),
|
||||
("suggestions", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='suggestion',
|
||||
name='duration',
|
||||
model_name="suggestion",
|
||||
name="duration",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='suggestion',
|
||||
name='duration_max',
|
||||
field=models.PositiveSmallIntegerField(blank=True, default=60, help_text="En minutes, telle qu'indiquée par l'éditeur, identique à la durée minimale si laissée vide", verbose_name='durée de partie maximale'),
|
||||
model_name="suggestion",
|
||||
name="duration_max",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
blank=True,
|
||||
default=60,
|
||||
help_text="En minutes, telle qu'indiquée par l'éditeur, identique à la durée minimale si laissée vide",
|
||||
verbose_name="durée de partie maximale",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='suggestion',
|
||||
name='duration_min',
|
||||
field=models.PositiveSmallIntegerField(default=60, help_text="En minutes, telle qu'indiquée par l'éditeur", verbose_name='durée de partie minimale'),
|
||||
model_name="suggestion",
|
||||
name="duration_min",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
default=60,
|
||||
help_text="En minutes, telle qu'indiquée par l'éditeur",
|
||||
verbose_name="durée de partie minimale",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='suggestion',
|
||||
name='duration_precisions',
|
||||
field=models.CharField(blank=True, help_text='Pour indiquer des informations complémentaires sur la durée de la partie (ex. évolution en fonction du nombre de joueur·se·s)', max_length=256, verbose_name='précisions sur la durée de partie'),
|
||||
model_name="suggestion",
|
||||
name="duration_precisions",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Pour indiquer des informations complémentaires sur la durée de la partie (ex. évolution en fonction du nombre de joueur·se·s)",
|
||||
max_length=256,
|
||||
verbose_name="précisions sur la durée de partie",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-02 20:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("suggestions", "0002_duration_range"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="suggestion",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="suggestioncomment",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,12 +1,13 @@
|
|||
from autoslug import AutoSlugField
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator
|
||||
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
|
||||
from inventory.models import Category, Tag
|
||||
from website.validators import MaxFileSizeValidator
|
||||
|
||||
|
||||
class Suggestion(models.Model):
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<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>
|
||||
<p><a href="{% url "authens:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour voter pour une suggestion.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if suggestion.description %}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import (
|
||||
SuggestionListView,
|
||||
AddSuggestionCommentView,
|
||||
AddSuggestionView,
|
||||
DownvoteSuggestionView,
|
||||
ModifySuggestionCommentView,
|
||||
SuggestionListView,
|
||||
SuggestionView,
|
||||
UpvoteSuggestionView,
|
||||
DownvoteSuggestionView,
|
||||
AddSuggestionCommentView,
|
||||
ModifySuggestionCommentView,
|
||||
)
|
||||
|
||||
app_name = "suggestions"
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import redirect
|
||||
from django.views.generic import DetailView, FormView, ListView, RedirectView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from comments.views import AddCommentView, ModifyCommentView
|
||||
from .models import Suggestion, SuggestionComment
|
||||
|
||||
from .forms import AddSuggestionForm
|
||||
from .models import Suggestion, SuggestionComment
|
||||
|
||||
|
||||
class SuggestionListView(ListView):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from .models import MarkdownPage
|
||||
from markdownx.admin import MarkdownxModelAdmin
|
||||
|
||||
from .models import MarkdownPage
|
||||
|
||||
admin.site.register(MarkdownPage, MarkdownxModelAdmin)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import markdown
|
||||
import re
|
||||
|
||||
import markdown
|
||||
|
||||
|
||||
class NbspPreprocessor(markdown.preprocessors.Preprocessor):
|
||||
"""Replace regular spaces with non-breaking spaces within a text around relevant
|
||||
|
|
|
@ -1,28 +1,43 @@
|
|||
# Generated by Django 3.1.2 on 2020-12-27 11:34
|
||||
|
||||
from django.db import migrations, models
|
||||
import markdownx.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MarkdownPage',
|
||||
name="MarkdownPage",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(blank=True, help_text="Identifiant de la page qui se voit dans l'URL. Ne doit pas collisionner avec une page existante. Laisser vide pour la page d'accueil, requis sinon.", unique=True, verbose_name='Adresse de la page')),
|
||||
('content', markdownx.models.MarkdownxField(verbose_name='Contenu')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"slug",
|
||||
models.SlugField(
|
||||
blank=True,
|
||||
help_text="Identifiant de la page qui se voit dans l'URL. Ne doit pas collisionner avec une page existante. Laisser vide pour la page d'accueil, requis sinon.",
|
||||
unique=True,
|
||||
verbose_name="Adresse de la page",
|
||||
),
|
||||
),
|
||||
("content", markdownx.models.MarkdownxField(verbose_name="Contenu")),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'page Markdown',
|
||||
'verbose_name_plural': 'pages Markdown',
|
||||
'ordering': ['slug'],
|
||||
"verbose_name": "page Markdown",
|
||||
"verbose_name_plural": "pages Markdown",
|
||||
"ordering": ["slug"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
20
website/migrations/0002_alter_markdownpage_id.py
Normal file
20
website/migrations/0002_alter_markdownpage_id.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.11 on 2024-07-02 20:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("website", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="markdownpage",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
]
|
BIN
website/static/img/favicon.ico
Normal file
BIN
website/static/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 6.9 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 59 KiB |
|
@ -5,7 +5,7 @@
|
|||
<p>Vous n'avez pas la permission pour consulter cette page.</p>
|
||||
{% if not user.is_authenticated %}
|
||||
<p>Cet accès vous est probablement refusé car vous n'êtes actuellement pas connecté·e.
|
||||
Vous pouvez vous rendre à la page de <a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">connexion</a>.</p>
|
||||
Vous pouvez vous rendre à la page de <a href="{% url "authens:login" %}?next={{ request.get_full_path }}">connexion</a>.</p>
|
||||
{% endif %}
|
||||
<p>Vous pouvez retourner sur la <a href="{% url "website:home" %}">page d'accueil</a>.</p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<footer>
|
||||
Pour tout problème, contactez <tt class="antispam">rf.sne@xuejopser</tt>.
|
||||
Pour tout problème, contactez <tt class="antispam">rf.sne@snekcah</tt>.
|
||||
</footer>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GestioJeux</title>
|
||||
<title>GestioHackens</title>
|
||||
<link type="text/css" rel="stylesheet" href="{% static "css/style.css" %}" />
|
||||
<link type="image/png" rel="shortcut icon" href="{% static "img/favicon.png" %}"/>
|
||||
<script src="{% static "js/jquery-3.4.1.min.js" %}"></script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<header>
|
||||
<h1>GestioJeux</h1>
|
||||
<h1>GestioHackens</h1>
|
||||
|
||||
<div id="mobile_nav_trigger" onclick="toggle_mobile_nav()">
|
||||
<i class="fa fa-fw fa-bars" aria-hidden="true"></i> Menu
|
||||
|
@ -10,7 +10,9 @@
|
|||
<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 == "inventory" %}class="current"{% endif %} href="{% url "inventory:inventory" %}">Inventaire</a>
|
||||
{% comment %}
|
||||
<a {% if url_name == "suggestions" %}class="current"{% endif %} href="{% url "suggestions:suggestions" %}">Suggestions</a>
|
||||
{% endcomment %}
|
||||
{% if perms.inventory.can_see_loan_details %}
|
||||
<a href={% url "inventory:all_loans" %}>Gestion des emprunts</a>
|
||||
{% endif %}
|
||||
|
@ -21,9 +23,9 @@
|
|||
<div id="account">
|
||||
{% if request.user.is_authenticated %}
|
||||
<a class="username" href="{% url "accounts:account_settings" %}">{{ request.user }}</a>
|
||||
<a class="logout" href="{% url "accounts:logout" %}?next={{ request.get_full_path }}"><i class="fa fa-sign-out" aria-hidden="true"></i></a>
|
||||
<a class="logout" href="{% url "authens:logout" %}?next={{ request.get_full_path }}"><i class="fa fa-sign-out" aria-hidden="true"></i></a>
|
||||
{% else %}
|
||||
<a class="login{% if url_name == "login" %} current{% endif %}" href="{% url "accounts:login" %}?next={{ request.get_full_path }}">Connexion</a>
|
||||
<a class="login{% if url_name == "login" %} current{% endif %}" href="{% url "authens:login" %}?next={{ request.get_full_path }}">Connexion</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import MarkdownPageView
|
||||
|
||||
app_name = "website"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.views.generic import DetailView
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import DetailView
|
||||
from markdownx.utils import markdownify
|
||||
|
||||
from .models import MarkdownPage
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue