Compare commits

...

9 commits

Author SHA1 Message Date
sinavir 8048c2593d style(pre-commit): Add hook
Python:
- black
- isort (black profile)
- ruff

Nix:
- statix
- nixfmt-rfc-style
- deadnix
2024-07-04 20:26:24 +02:00
sinavir 4d99ba9026 fest(inventory): Improve game image names generation 2024-07-04 18:24:57 +02:00
sinavir 14509cf9b5 feat(FORK): Rework settings to be nicer for djangonix 2024-07-04 18:15:57 +02:00
sinavir b95b0ccd3f feat(qrcode): Automatic generation of qrcodes 2024-07-04 18:15:57 +02:00
sinavir 541d840727 feat(FORK): Remove unnecessary fields 2024-07-04 18:15:57 +02:00
sinavir 448bcad382 feat(FORK): jeu -> outil/hackens 2024-07-04 18:15:57 +02:00
sinavir 1f8a03bdab feat(FORK): Disable suggestions 2024-07-04 18:15:57 +02:00
sinavir 89f2808087 fix(pk field): Make manage.py makemigrations happy 2024-07-04 18:15:57 +02:00
sinavir fee31ab07b feat: switch to authens
Be careful there is no User migration
2024-07-04 18:15:57 +02:00
87 changed files with 2095 additions and 722 deletions

2
.gitignore vendored
View file

@ -68,3 +68,5 @@ public/
# Vim recover files # Vim recover files
*~ *~
.pre-commit-config.yaml

View file

@ -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)

View file

@ -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"

View file

@ -1,5 +1,5 @@
from django.contrib.auth.models import User
from django.forms import ModelForm, ValidationError from django.forms import ModelForm, ValidationError
from .models import User
class AccountSettingsForm(ModelForm): class AccountSettingsForm(ModelForm):
@ -7,13 +7,13 @@ class AccountSettingsForm(ModelForm):
class Meta: class Meta:
model = User model = User
fields = ["public_name"] fields = ["username"]
def clean_public_name(self): def clean_public_name(self):
public_name = self.cleaned_data["public_name"] public_name = self.cleaned_data["username"]
public_name = public_name.strip() public_name = public_name.strip()
if ( if (
User.objects.filter(public_name=public_name) User.objects.filter(username=public_name)
.exclude(pk=self.instance.pk) .exclude(pk=self.instance.pk)
.exists() .exists()
): ):

View file

@ -8,27 +8,94 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ("auth", "0012_alter_user_first_name_max_length"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('password', models.CharField(max_length=128, verbose_name='password')), "id",
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), models.AutoField(
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), auto_created=True,
('email', models.EmailField(max_length=254, unique=True, verbose_name='adresse email')), primary_key=True,
('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')), serialize=False,
('is_staff', models.BooleanField(default=False, help_text="Précise si lutilisateur peut se connecter à ce site d'administration.", verbose_name='statut équipe')), verbose_name="ID",
('is_active', models.BooleanField(default=True, help_text='Précise si lutilisateur 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')), ("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 lutilisateur peut se connecter à ce site d'administration.",
verbose_name="statut équipe",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Précise si lutilisateur 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={ options={
'verbose_name': 'utilisateur·ice', "verbose_name": "utilisateur·ice",
'verbose_name_plural': 'utilisateur·ice·s', "verbose_name_plural": "utilisateur·ice·s",
}, },
), ),
] ]

View 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",
),
]

View file

@ -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 lutilisateur peut se connecter à ce site d'administration.",
)
is_active = models.BooleanField(
"actif",
default=True,
help_text="Précise si lutilisateur 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

View file

@ -3,7 +3,7 @@
{% block "content" %} {% block "content" %}
<h1>Paramètres du compte</h1> <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"> <form method="post">
{% csrf_token %} {% csrf_token %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -1,25 +1,10 @@
from django.urls import include, path from django.urls import include, path
import django.contrib.auth.views as dj_auth_views
from .views import LoginView, LogoutView, PasswordChangeView, AccountSettingsView from .views import AccountSettingsView, PasswordChangeView
import django_cas_ng.views
app_name = "accounts" 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 = [ 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("change_password/", PasswordChangeView.as_view(), name="change_password"),
path("settings/", AccountSettingsView.as_view(), name="account_settings"), path("settings/", AccountSettingsView.as_view(), name="account_settings"),
] ]

View file

@ -1,72 +1,14 @@
from django.views.generic import TemplateView, RedirectView from django.contrib import messages
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.auth import user_logged_in, user_logged_out, user_login_failed 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.mixins import LoginRequiredMixin
from django.contrib.auth.views import PasswordChangeView from django.contrib.auth.views import PasswordChangeView
from django.contrib import messages from django.dispatch import receiver
from django.urls import reverse
from urllib.parse import quote as urlquote from django.views.generic.edit import UpdateView
from .forms import AccountSettingsForm 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) @receiver(user_logged_in)
def on_login(request, user, **kwargs): def on_login(request, user, **kwargs):
messages.success(request, "Connexion réussie. Bienvenue, {}.".format(user)) messages.success(request, "Connexion réussie. Bienvenue, {}.".format(user))

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class CommentsConfig(AppConfig): class CommentsConfig(AppConfig):
name = 'comments' name = "comments"

View file

@ -1,5 +1,5 @@
from django.contrib.auth.models import User
from django.db import models from django.db import models
from accounts.models import User
class AbstractComment(models.Model): class AbstractComment(models.Model):

View file

@ -24,7 +24,7 @@
</div> </div>
{% endif %} {% endif %}
{% empty %} {% empty %}
<p>(Aucun commentaire sur ce jeu)</p> <p>(Aucun commentaire sur cet outil)</p>
{% endfor %} {% endfor %}
{% if not edited_comment %} {% if not edited_comment %}
{% if request.user.is_authenticated %} {% 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> <button type="submit"><i class="fa fa-paper-plane" aria-hidden="true"></i> Envoyer le commentaire</button>
</form> </form>
{% else %} {% 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 %}
{% endif %} {% endif %}

View file

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View file

@ -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.contrib import messages
from django.http import Http404 from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied 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): class AddCommentView(LoginRequiredMixin, SingleObjectMixin, RedirectView):

View file

@ -6,14 +6,40 @@
let let
nix-pkgs = import sources.nix-pkgs { inherit pkgs; }; 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 { python3 = pkgs.python3.override {
packageOverrides = _: _: { packageOverrides = final: _: {
inherit (nix-pkgs) inherit (nix-pkgs)
django-autoslug django-autoslug
django-cas-ng
loadcredential loadcredential
markdown-icons markdown-icons
python-cas
; ;
authens = final.callPackage ./nix/authens { };
}; };
}; };
in in
@ -22,12 +48,12 @@ in
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
name = "gestiojeux.dev"; name = "gestiojeux.dev";
packages = [ packages = check.enabledPackages ++ [
(python3.withPackages (ps: [ (python3.withPackages (ps: [
ps.django ps.django
ps.django-types
ps.django-autoslug ps.django-autoslug
ps.loadcredential ps.loadcredential
ps.django-cas-ng
ps.django-cleanup ps.django-cleanup
ps.django-haystack ps.django-haystack
ps.django-markdownx ps.django-markdownx
@ -35,6 +61,10 @@ in
ps.pillow ps.pillow
ps.whoosh ps.whoosh
ps.markdown-icons ps.markdown-icons
ps.authens
ps.qrcode
ps.pillow
# Django haystack is drunk # Django haystack is drunk
ps.setuptools ps.setuptools
@ -48,5 +78,8 @@ in
GESTIOJEUX_DEBUG = builtins.toJSON true; GESTIOJEUX_DEBUG = builtins.toJSON true;
}; };
shellHook = ''
${check.shellHook}
'';
}; };
} }

View file

@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application 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() application = get_asgi_application()

View file

@ -12,12 +12,13 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
import os import os
from django.urls import reverse_lazy
from loadcredential import Credentials from loadcredential import Credentials
# Secrets # Secrets
credentials = Credentials(env_prefix="GESTIOJEUX_") credentials = Credentials(env_prefix="GESTIOJEUX_")
SECRET_KEY = credentials["SECRET_KEY"] SECRET_KEY = credentials.get("SECRET_KEY", "insecure")
DEBUG = credentials.get_json( DEBUG = credentials.get_json(
"DEBUG", False "DEBUG", False
@ -31,36 +32,15 @@ ADMINS = credentials.get_json("ADMINS", [])
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PUBLIC_DIR = os.path.join(BASE_DIR, "public") PUBLIC_DIR = os.path.join(BASE_DIR, "public")
# Conditional settings # Database
if DEBUG: # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.sqlite3", "ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.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"],
}
} }
}
# Search engine # Search engine
@ -80,7 +60,6 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django_cas_ng",
"django_tables2", "django_tables2",
"markdownx", "markdownx",
"haystack", "haystack",
@ -88,9 +67,10 @@ INSTALLED_APPS = [
"accounts", "accounts",
"comments", "comments",
"inventory", "inventory",
"suggestions", # "suggestions",
"loans", "loans",
"django_cleanup", # Keep last "django_cleanup", # Keep last
"authens",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -101,7 +81,6 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_cas_ng.middleware.CASMiddleware",
] ]
ROOT_URLCONF = "gestiojeux.urls" ROOT_URLCONF = "gestiojeux.urls"
@ -124,11 +103,12 @@ TEMPLATES = [
WSGI_APPLICATION = "gestiojeux.wsgi.application" WSGI_APPLICATION = "gestiojeux.wsgi.application"
AUTH_USER_MODEL = "accounts.User" # Authentication backends
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
"accounts.backends.CasBackend", "authens.backends.ENSCASBackend",
"authens.backends.OldCASBackend",
) )
# Password validation # Password validation
@ -143,7 +123,11 @@ AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, {"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 # Use markdown extensions
MARKDOWNX_MARKDOWN_EXTENSIONS = [ MARKDOWNX_MARKDOWN_EXTENSIONS = [
@ -185,12 +169,7 @@ MEDIA_URL = "/media/"
STATIC_ROOT = os.path.join(PUBLIC_DIR, "static") STATIC_ROOT = os.path.join(PUBLIC_DIR, "static")
MEDIA_ROOT = os.path.join(PUBLIC_DIR, "media") 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/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
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"

View file

@ -13,17 +13,19 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 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 import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("markdownx/", include("markdownx.urls")), path("markdownx/", include("markdownx.urls")),
path("inventory/", include("inventory.urls")), path("inventory/", include("inventory.urls")),
path("suggestions/", include("suggestions.urls")), # path("suggestions/", include("suggestions.urls")),
path("account/", include("accounts.urls")), path("account/", include("accounts.urls")),
path("authens/", include("authens.urls")),
path("", include("website.urls")), path("", include("website.urls")),
] ]

View file

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application 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() application = get_wsgi_application()

View file

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import Category, Tag, Game, GameComment
from comments.admin import CommentAdmin from comments.admin import CommentAdmin
from .models import Category, Game, GameComment, Tag
admin.site.register(Category) admin.site.register(Category)
admin.site.register(Tag) admin.site.register(Tag)
admin.site.register(Game) admin.site.register(Game)

View file

@ -1,5 +1,6 @@
from loans.forms import BorrowForm from loans.forms import BorrowForm
class BorrowGameForm(BorrowForm): class BorrowGameForm(BorrowForm):
error_css_class = "errorfield" error_css_class = "errorfield"
required_css_class = "requiredfield" required_css_class = "requiredfield"

View file

@ -1,6 +1,7 @@
import markdown import markdown
from django.template.loader import get_template from django.template.loader import get_template
from .models import Category, Tag, Game
from .models import Category, Game, Tag
class InventoryLinkProcessor(markdown.inlinepatterns.InlineProcessor): class InventoryLinkProcessor(markdown.inlinepatterns.InlineProcessor):

View file

@ -1,9 +1,10 @@
# Generated by Django 3.1.2 on 2020-12-29 23:21 # Generated by Django 3.1.2 on 2020-12-29 23:21
import autoslug.fields import autoslug.fields
import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
import website.validators import website.validators
@ -17,71 +18,216 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ 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')), "id",
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True)), 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={ options={
'verbose_name': 'catégorie', "verbose_name": "catégorie",
'ordering': ['name'], "ordering": ["name"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Game', name="Game",
fields=[ 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')), "id",
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)), models.AutoField(
('nb_player_min', models.PositiveSmallIntegerField(verbose_name='nombre de joueur·se·s minimum')), auto_created=True,
('nb_player_max', models.PositiveSmallIntegerField(verbose_name='nombre de joueur·se·s maximum')), primary_key=True,
('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')), serialize=False,
('duration', models.CharField(blank=True, max_length=256, verbose_name='durée de partie')), verbose_name="ID",
('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')), "title",
('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')), models.CharField(
('missing_elements', models.TextField(blank=True, verbose_name='pièces manquantes')), max_length=256, unique=True, verbose_name="titre du jeu"
('category', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='inventory.category', verbose_name='catégorie')), ),
),
(
"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={ options={
'verbose_name': 'jeu', "verbose_name": "jeu",
'verbose_name_plural': 'jeux', "verbose_name_plural": "jeux",
'ordering': ['title'], "ordering": ["title"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Tag', name="Tag",
fields=[ 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')), "id",
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True)), 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={ options={
'ordering': ['name'], "ordering": ["name"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='GameComment', name="GameComment",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('text', models.TextField(verbose_name='texte')), "id",
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='date de publication')), models.AutoField(
('modified_on', models.DateTimeField(auto_now=True, verbose_name='date de modification')), auto_created=True,
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='auteur·ice')), primary_key=True,
('commented_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='inventory.game', verbose_name='jeu')), 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={ options={
'verbose_name': 'commentaire sur un jeu', "verbose_name": "commentaire sur un jeu",
'verbose_name_plural': 'commentaires sur des jeux', "verbose_name_plural": "commentaires sur des jeux",
'ordering': ['created_on'], "ordering": ["created_on"],
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='game', model_name="game",
name='tags', name="tags",
field=models.ManyToManyField(blank=True, to='inventory.Tag', verbose_name='tags'), field=models.ManyToManyField(
blank=True, to="inventory.Tag", verbose_name="tags"
),
), ),
] ]

View file

@ -1,32 +1,52 @@
# Generated by Django 4.2.8 on 2024-05-02 09:30 # Generated by Django 4.2.8 on 2024-05-02 09:30
import autoslug.fields import autoslug.fields
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('inventory', '0002_duration_range'), ("inventory", "0002_duration_range"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='GameLoan', name="GameLoan",
fields=[ 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)), "id",
('borrow_date', models.DateTimeField(auto_now_add=True)), models.AutoField(
('return_date', models.DateTimeField(null=True)), auto_created=True,
('mail', models.EmailField(max_length=254)), primary_key=True,
('lent_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.game', verbose_name='jeu emprunté')), 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={ options={
'verbose_name': 'emprunt', "verbose_name": "emprunt",
'verbose_name_plural': 'emprunts', "verbose_name_plural": "emprunts",
'ordering': ['borrow_date'], "ordering": ["borrow_date"],
'abstract': False, "abstract": False,
}, },
), ),
] ]

View file

@ -6,26 +6,33 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('inventory', '0003_gameloan'), ("inventory", "0003_gameloan"),
] ]
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='category', name="category",
options={'ordering': ['name'], 'verbose_name': 'étagère'}, options={"ordering": ["name"], "verbose_name": "étagère"},
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='gameloan', name="gameloan",
options={'ordering': ['borrow_date'], 'permissions': [('can_see_loan_details', 'Can see loan details')], 'verbose_name': 'emprunt', 'verbose_name_plural': 'emprunts'}, options={
"ordering": ["borrow_date"],
"permissions": [("can_see_loan_details", "Can see loan details")],
"verbose_name": "emprunt",
"verbose_name_plural": "emprunts",
},
), ),
migrations.AlterField( migrations.AlterField(
model_name='gameloan', model_name="gameloan",
name='borrow_date', name="borrow_date",
field=models.DateTimeField(auto_now_add=True, verbose_name='Date demprunt'), field=models.DateTimeField(
auto_now_add=True, verbose_name="Date demprunt"
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='gameloan', model_name="gameloan",
name='return_date', name="return_date",
field=models.DateTimeField(null=True, verbose_name='Date de retour'), field=models.DateTimeField(null=True, verbose_name="Date de retour"),
), ),
] ]

View file

@ -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"
),
),
]

View file

@ -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é",
),
),
]

View file

@ -1,10 +1,13 @@
import os
import uuid
from autoslug import AutoSlugField
from django.db import models from django.db import models
from django.urls import reverse 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 comments.models import AbstractComment
from loans.models import AbstractLoan from loans.models import AbstractLoan
from website.validators import MaxFileSizeValidator
class Category(models.Model): class Category(models.Model):
@ -36,47 +39,21 @@ class Tag(models.Model):
return reverse("inventory:tag", args=(self.slug,)) 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): 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) 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 = models.ForeignKey(
Category, on_delete=models.RESTRICT, verbose_name="catégorie" 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") description = models.TextField(blank=True, verbose_name="description")
image = models.ImageField( image = models.ImageField(
upload_to="game_img/", upload_to=image_uuid_path,
blank=True, blank=True,
verbose_name="image", verbose_name="image",
help_text="L'image doit peser 512 Kio au maximum", help_text="L'image doit peser 512 Kio au maximum",
@ -95,45 +72,17 @@ class Game(models.Model):
class Meta: class Meta:
ordering = ["title"] ordering = ["title"]
verbose_name = "jeu" verbose_name = "outil"
verbose_name_plural = "jeux" verbose_name_plural = "outils"
def __str__(self): def __str__(self):
return self.title 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): def get_player_range(self):
if self.player_range: return ""
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)
def get_duration_range(self): def get_duration_range(self):
if self.duration: return ""
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)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("inventory:game", args=(self.slug,)) return reverse("inventory:game", args=(self.slug,))
@ -141,26 +90,25 @@ class Game(models.Model):
class GameComment(AbstractComment): class GameComment(AbstractComment):
commented_object = models.ForeignKey( 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: class Meta:
ordering = ["created_on"] ordering = ["created_on"]
verbose_name = "commentaire sur un jeu" verbose_name = "commentaire sur un outil"
verbose_name_plural = "commentaires sur des jeux" verbose_name_plural = "commentaires sur des outils"
def get_modification_url(self): def get_modification_url(self):
return reverse( return reverse(
"inventory:modify_game_comment", args=(self.commented_object.slug, self.id) "inventory:modify_game_comment", args=(self.commented_object.slug, self.id)
) )
class GameLoan(AbstractLoan): class GameLoan(AbstractLoan):
lent_object = models.ForeignKey( lent_object = models.ForeignKey(
Game, on_delete=models.CASCADE, Game, on_delete=models.CASCADE, verbose_name="outil emprunté"
verbose_name="jeu emprunté"
) )
class Meta(AbstractLoan.Meta): class Meta(AbstractLoan.Meta):
abstract = False abstract = False
permissions = [("can_see_loan_details", "Can see loan details")] permissions = [("can_see_loan_details", "Can see loan details")]

View file

@ -1,5 +1,6 @@
from haystack import indexes from haystack import indexes
from .models import Category, Tag, Game
from .models import Category, Game, Tag
class CategoryIndex(indexes.SearchIndex, indexes.Indexable): class CategoryIndex(indexes.SearchIndex, indexes.Indexable):

View file

@ -1,8 +1,10 @@
import django_tables2 as tables import django_tables2 as tables
from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.html import format_html
from .models import GameLoan from .models import GameLoan
class LoanTable(tables.Table): class LoanTable(tables.Table):
next_pattern = "inventory:all_loans" next_pattern = "inventory:all_loans"
@ -10,24 +12,29 @@ class LoanTable(tables.Table):
model = GameLoan model = GameLoan
sequence = ("lent_object", "borrow_date", "return_date", "mail") sequence = ("lent_object", "borrow_date", "return_date", "mail")
exclude = ("id",) exclude = ("id",)
slug = tables.Column(verbose_name="Actions", orderable=False) slug = tables.Column(verbose_name="Actions", orderable=False)
def render_lent_object(self, value, record): def render_lent_object(self, value, record):
return format_html("<a href='{}'>{}</a>", return format_html(
reverse("inventory:game", args=[record.lent_object.slug]), value) "<a href='{}'>{}</a>",
reverse("inventory:game", args=[record.lent_object.slug]),
value,
)
def render_slug(self, value, record): def render_slug(self, value, record):
res = "" res = ""
if record.return_date == None: if record.return_date is None:
res = format_html("<a class='button' href='{}?next={}'>Rendre le jeu</a>", res = format_html(
"<a class='button' href='{}?next={}'>Rendre l'outil</a>",
reverse("inventory:return_game", args=[value]), reverse("inventory:return_game", args=[value]),
reverse(self.next_pattern)) reverse(self.next_pattern),
)
return res return res
class OngoingLoansTable(LoanTable): class OngoingLoansTable(LoanTable):
next_pattern = "inventory:ongoing_loans" next_pattern = "inventory:ongoing_loans"
class Meta(LoanTable.Meta): class Meta(LoanTable.Meta):
exclude = ("return_date", "mail", "id") exclude = ("return_date", "mail", "id")

View file

@ -4,7 +4,7 @@
<h1><i class="fa fa-bookmark" aria-hidden="true"></i> {{ category.name }}</h1> <h1><i class="fa fa-bookmark" aria-hidden="true"></i> {{ category.name }}</h1>
{% with game_list=category.game_set.all %} {% 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&nbsp;:</p> étagère&nbsp;:</p>
{% for game in game_list %} {% for game in game_list %}
{% include "./partials/game_item.html" %} {% include "./partials/game_item.html" %}

View file

@ -3,7 +3,7 @@
{% block "content" %} {% block "content" %}
<h1>Liste des étagères</h1> <h1>Liste des étagères</h1>
<p>Il y a {{ paginator.count }} étagère{{ paginator.count|pluralize }} de jeux&nbsp;:</p> <p>Il y a {{ paginator.count }} étagère{{ paginator.count|pluralize }} d'outils&nbsp;:</p>
{% include "partials/pagination.html" %} {% include "partials/pagination.html" %}
{% for category in category_list %} {% for category in category_list %}

View file

@ -12,9 +12,6 @@
<div id="details"> <div id="details">
<p><i class="fa fa-fw fa-bookmark"></i> <a href="{{ game.category.get_absolute_url }}">{{ game.category }}</a></p> <p><i class="fa fa-fw fa-bookmark"></i> <a href="{{ game.category.get_absolute_url }}">{{ game.category }}</a></p>
<hr/> <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> <p><i class="fa fa-fw fa-tags" aria-hidden="true"></i>
{% for tag in game.tags.all %} {% for tag in game.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if not forloop.last %},{% endif %} <a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if not forloop.last %},{% endif %}
@ -23,19 +20,19 @@
{% endfor %} {% endfor %}
</p> </p>
<hr/> <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>
</div> </div>
{% if is_borrowed %} {% 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 %} {% endif %}
<a class="button" href="{% url "inventory:game_loan" game.slug %}"> <a class="button" href="{% url "inventory:game_loan" game.slug %}">
Emprunter ou rendre « {{ game.title }} » Emprunter ou rendre « {{ game.title }} »
</a> </a>
<a class="button" href="{% url "inventory:qrcode_borrow" game.slug %}">
Générer un QR-code
</a>
<h2 id="description">Description</h2> <h2 id="description">Description</h2>
{{ object.description|linebreaks }} {{ object.description|linebreaks }}
@ -45,7 +42,7 @@
<p class="warning">{{ game.missing_elements|linebreaksbr }}</p> <p class="warning">{{ game.missing_elements|linebreaksbr }}</p>
{% endif %} {% endif %}
<h2>Commentaires et propositions de variantes</h2> <h2>Commentaires</h2>
{% url "inventory:add_game_comment" game.slug as add_comment_url %} {% url "inventory:add_game_comment" game.slug as add_comment_url %}
{% include "comments.html" with comments=game.comments.all %} {% include "comments.html" with comments=game.comments.all %}
{% endblock %} {% endblock %}

View file

@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block "content" %} {% 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&nbsp;:</p> <p>Il y a {{ paginator.count }} outil{{ paginator.count|pluralize:"s" }} en Hackens&nbsp;:</p>
{% include "partials/pagination.html" %} {% include "partials/pagination.html" %}
{% for game in game_list %} {% for game in game_list %}

View file

@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block "content" %} {% 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" %}"> <form class="search" method="get" action="{% url "inventory:search" %}">
<input type="search" name="q" /> <input type="search" name="q" />
<button type="submit"><i class="fa fa-fw fa-search" aria-hidden="true"></i></button> <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" %}"> <a class="button" href="{% url "inventory:category_list" %}">
Liste des étagères Liste des étagères
<p class="helptext"> <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> </p>
</a> </a>
<a class="button" href="{% url "inventory:tag_list" %}"> <a class="button" href="{% url "inventory:tag_list" %}">
Liste des tags Liste des tags
<p class="helptext"> <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> </p>
</a> </a>
<a class="button" href="{% url "inventory:game_list" %}"> <a class="button" href="{% url "inventory:game_list" %}">
Liste alphabétique Liste alphabétique
<p class="helptext"> <p class="helptext">
La liste complète des jeux de la ludothèque La liste complète des outils
</p> </p>
</a> </a>
{% endblock %} {% endblock %}

View file

@ -7,7 +7,7 @@
{{ form.as_p }} {{ form.as_p }}
<button type="submit">Emprunter</button> <button type="submit">Emprunter</button>
<br/> <br/>
Vos données seront traitées pour les besoins de la gestion de la salle Jeux. 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@xuejopser</tt> Pour toute demande, contactez: <tt class="antispam">rf.sne@snekcah</tt>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -30,7 +30,7 @@
<a class="button" href="{% url "inventory:borrow_game" game.slug %}"> <a class="button" href="{% url "inventory:borrow_game" game.slug %}">
Emprunter « {{ game.title }} » Emprunter « {{ game.title }} »
<p class="helptext"> <p class="helptext">
Si le jeu est emprunté par quelquun dautre, il sera rendu Si l'outil est emprunté par quelquun dautre, il sera rendu
automatiquement. automatiquement.
</p> </p>
</a> </a>
@ -39,12 +39,12 @@ automatiquement.
<a class="button" href="{% url "inventory:return_game" loan.slug %}"> <a class="button" href="{% url "inventory:return_game" loan.slug %}">
Rendre « {{ game.title }} » Rendre « {{ game.title }} »
<p class="helptext"> <p class="helptext">
Ce jeu est emprunté depuis le {{ loan.borrow_date }}. Ceit outil est emprunté depuis le {{ loan.borrow_date }}.
</p> </p>
</a> </a>
{% endif %} {% endif %}
<a class="button" href="/inventory/game/{{ game.slug }}/"> <a class="button" href="/inventory/game/{{ game.slug }}/">
Détails du jeu Détails de l'outil
</a> </a>
{% endblock %} {% endblock %}

View file

@ -1,20 +1,9 @@
<a class="inventory_item game" href="{{ game.get_absolute_url }}"> <a class="inventory_item game" href="{{ game.get_absolute_url }}">
<span class="title">{{ game.title }}</span> <span class="title">{{ game.title }}</span>
<span class="details"> <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> <span><i class="fa fa-bookmark"></i> {{ game.category }}</span>
{% for tag in game.tags.all %} {% for tag in game.tags.all %}
<span><i class="fa fa-tag" aria-hidden="true"></i> {{ tag }}</span> <span><i class="fa fa-tag" aria-hidden="true"></i> {{ tag }}</span>
{% endfor %} {% 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> </span>
</a> </a>

View file

@ -4,7 +4,7 @@
<h1><i class="fa fa-tag" aria-hidden="true"></i> {{ tag.name }}</h1> <h1><i class="fa fa-tag" aria-hidden="true"></i> {{ tag.name }}</h1>
{% with game_list=tag.game_set.all %} {% 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&nbsp;:</p> <p>Il y a {{ game_list|length }} outil{{ game_list|pluralize:"s" }} marqué{{ game_list|pluralize }} avec ce tag&nbsp;:</p>
{% for game in game_list %} {% for game in game_list %}
{% include "./partials/game_item.html" %} {% include "./partials/game_item.html" %}
{% endfor %} {% endfor %}

View file

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View file

@ -1,20 +1,22 @@
from django.urls import path from django.urls import path
from .views import ( from .views import (
InventoryView, AddGameCommentView,
BorrowGameView,
CategoryListView, CategoryListView,
CategoryView, CategoryView,
DetailLoanView,
GameListView,
GameLoanView,
GameView,
InventorySearchView,
InventoryView,
ModifyGameCommentView,
OngoingLoansView,
QrCodeView,
ReturnGameView,
TagListView, TagListView,
TagView, TagView,
GameListView,
GameView,
AddGameCommentView,
ModifyGameCommentView,
InventorySearchView,
GameLoanView,
BorrowGameView,
ReturnGameView,
OngoingLoansView,
DetailLoanView,
) )
app_name = "inventory" app_name = "inventory"
@ -39,6 +41,11 @@ urlpatterns = [
path("loans/game/<slug>/", GameLoanView.as_view(), name="game_loan"), path("loans/game/<slug>/", GameLoanView.as_view(), name="game_loan"),
path("loans/return/<slug>/", ReturnGameView.as_view(), name="return_game"), path("loans/return/<slug>/", ReturnGameView.as_view(), name="return_game"),
path("loans/borrow/<slug>/", BorrowGameView.as_view(), name="borrow_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/ongoing/", OngoingLoansView.as_view(), name="ongoing_loans"),
path("loans/all/", DetailLoanView.as_view(), name="all_loans"), path("loans/all/", DetailLoanView.as_view(), name="all_loans"),
] ]

View file

@ -1,13 +1,18 @@
from django.views.generic import TemplateView, ListView, DetailView import qrcode
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from haystack.generic_views import SearchView from django.http import HttpResponse
from haystack.forms import SearchForm from django.urls import reverse
from haystack.query import SearchQuerySet from django.views.generic import DetailView, ListView, TemplateView
from django_tables2.views import SingleTableView 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 comments.views import AddCommentView, ModifyCommentView
from loans.views import BorrowView, ReturnView, DetailLoanView from loans.views import BorrowView, DetailLoanView, ReturnView
from .models import Category, Tag, Game, GameComment, GameLoan
from .forms import BorrowGameForm from .forms import BorrowGameForm
from .models import Category, Game, GameComment, GameLoan, Tag
from .tables import LoanTable, OngoingLoansTable from .tables import LoanTable, OngoingLoansTable
@ -102,3 +107,13 @@ class DetailLoanView(PermissionRequiredMixin, SingleTableView):
table_class = LoanTable table_class = LoanTable
template_name = "inventory/loans/loans_table.html" 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

View file

@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
class LoanAdmin(admin.ModelAdmin): class LoanAdmin(admin.ModelAdmin):
list_display = ("lent_object", "borrow_date", "return_date") list_display = ("lent_object", "borrow_date", "return_date")
ordering = ("-borrow_date",) ordering = ("-borrow_date",)

View file

@ -2,5 +2,5 @@ from django.apps import AppConfig
class LoansConfig(AppConfig): class LoansConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'loans' name = "loans"

View file

@ -1,4 +1,5 @@
from django import forms from django import forms
class BorrowForm(forms.Form): class BorrowForm(forms.Form):
mail = forms.EmailField(label="Mail") mail = forms.EmailField(label="Mail")

View file

@ -1,8 +1,8 @@
# Generated by Django 4.2.8 on 2024-04-23 16:45 # Generated by Django 4.2.8 on 2024-04-23 16:45
import autoslug.fields import autoslug.fields
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -10,24 +10,45 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('inventory', '0002_duration_range'), ("inventory", "0002_duration_range"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Loan', name="Loan",
fields=[ 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)), "id",
('borrow_date', models.DateTimeField(auto_now_add=True)), models.BigAutoField(
('return_date', models.DateTimeField(null=True)), auto_created=True,
('mail', models.EmailField(max_length=254)), primary_key=True,
('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loans', to='inventory.game', verbose_name='jeu emprunté')), 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={ options={
'verbose_name': 'emprunt', "verbose_name": "emprunt",
'verbose_name_plural': 'emprunts', "verbose_name_plural": "emprunts",
'ordering': ['borrow_date'], "ordering": ["borrow_date"],
}, },
), ),
] ]

View file

@ -6,11 +6,11 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('loans', '0001_initial'), ("loans", "0001_initial"),
] ]
operations = [ operations = [
migrations.DeleteModel( migrations.DeleteModel(
name='Loan', name="Loan",
), ),
] ]

View file

@ -1,12 +1,12 @@
from django.db import models
from autoslug import AutoSlugField from autoslug import AutoSlugField
from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
class AbstractLoan(models.Model): 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") slug = AutoSlugField(unique=True, populate_from="lent_object")
borrow_date = models.DateTimeField( borrow_date = models.DateTimeField(auto_now_add=True, verbose_name="Date demprunt")
auto_now_add=True, verbose_name="Date demprunt")
return_date = models.DateTimeField(null=True, verbose_name="Date de retour") return_date = models.DateTimeField(null=True, verbose_name="Date de retour")
mail = models.EmailField() mail = models.EmailField()
@ -14,7 +14,7 @@ class AbstractLoan(models.Model):
class Meta: class Meta:
abstract = True abstract = True
ordering=["borrow_date"] ordering = ["borrow_date"]
verbose_name = "emprunt" verbose_name = "emprunt"
verbose_name_plural = "emprunts" verbose_name_plural = "emprunts"
@ -26,9 +26,9 @@ class AbstractLoan(models.Model):
self.save() self.save()
@classmethod @classmethod
def ongoing_loans(cls, obj = None): def ongoing_loans(cls, obj=None):
ongoing = cls.objects.filter(return_date=None) ongoing = cls.objects.filter(return_date=None)
if obj != None: if obj is not None:
return ongoing.filter(lent_object=obj) return ongoing.filter(lent_object=obj)
else: else:
return ongoing return ongoing

View file

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View file

@ -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.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from inventory.models import Game from django.views.generic import DetailView, FormView, RedirectView
from .models import AbstractLoan from django.views.generic.detail import SingleObjectMixin
from .forms import BorrowForm from .forms import BorrowForm
class ReturnView(SingleObjectMixin, RedirectView): class ReturnView(SingleObjectMixin, RedirectView):
# Inherited classes should contain: # Inherited classes should contain:
# model = LoanModel # model = LoanModel
# pattern_name = # pattern_name =
redirect_slug_field = "slug" redirect_slug_field = "slug"
permanent = False permanent = False
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
loan = self.get_object() loan = self.get_object()
loan.return_object() loan.return_object()
kwargs[self.redirect_slug_field] = getattr(loan.lent_object, kwargs[self.redirect_slug_field] = getattr(
loan.lent_object_slug_field) loan.lent_object, loan.lent_object_slug_field
)
messages.success(self.request, "Rendu effectué.") messages.success(self.request, "Rendu effectué.")
if "next" in self.request.GET: if "next" in self.request.GET:
return self.request.GET["next"] return self.request.GET["next"]
@ -31,7 +31,7 @@ class BorrowView(SingleObjectMixin, FormView):
# model = LentObjectModel # model = LentObjectModel
# loan_model = LoanModel # loan_model = LoanModel
# template_name = "path/to/template.html" # 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): def get_initial(self):
initial = super().get_initial() initial = super().get_initial()
@ -52,21 +52,22 @@ class BorrowView(SingleObjectMixin, FormView):
loan.save() loan.save()
self.request.session["loan_mail"] = loan.mail self.request.session["loan_mail"] = loan.mail
messages.success(self.request, "Votre emprunt est enregistré.") messages.success(self.request, "Votre emprunt est enregistré.")
return redirect(self.success_pattern_name, return redirect(
getattr(obj, loan.lent_object_slug_field)) self.success_pattern_name, getattr(obj, loan.lent_object_slug_field)
)
class DetailLoanView(DetailView): class DetailLoanView(DetailView):
# Inherited classes should contain: # Inherited classes should contain:
# model = LentObjectModel # model = LentObjectModel
# loan_model = LoanModel # loan_model = LoanModel
# template_name = "path/to/template.html" # template_name = "path/to/template.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
loans = self.loan_model.ongoing_loans(self.get_object()) loans = self.loan_model.ongoing_loans(self.get_object())
is_borrowed = loans.exists() is_borrowed = loans.exists()
context["is_borrowed"] = is_borrowed context["is_borrowed"] = is_borrowed
if is_borrowed: if is_borrowed:
context["loan"] = loans.get() context["loan"] = loans.get()
return context return context

View file

@ -5,7 +5,7 @@ import sys
def main(): def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestiojeux.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestiojeux.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@ -17,5 +17,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View 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
View 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
];
}

View 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
];
}

View file

@ -1,20 +1,34 @@
# Generated by npins. Do not modify; will be overwritten regularly # Generated by npins. Do not modify; will be overwritten regularly
let let
data = builtins.fromJSON (builtins.readFile ./sources.json); data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version; inherit (data) version;
mkSource = spec: mkSource =
assert spec ? type; let spec:
assert spec ? type;
let
path = path =
if spec.type == "Git" then mkGitSource spec if spec.type == "Git" then
else if spec.type == "GitRelease" then mkGitSource spec mkGitSource spec
else if spec.type == "PyPi" then mkPyPiSource spec else if spec.type == "GitRelease" then
else if spec.type == "Channel" then mkChannelSource spec mkGitSource spec
else builtins.throw "Unknown source type ${spec.type}"; 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 in
spec // { outPath = path; }; spec // { outPath = path; };
mkGitSource = { repository, revision, url ? null, hash, ... }: mkGitSource =
{
repository,
revision,
url ? null,
hash,
...
}:
assert repository ? type; assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository # 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 # In the latter case, there we will always be an url to the tarball
@ -23,19 +37,23 @@ let
inherit url; inherit url;
sha256 = hash; # FIXME: check nix version & use SRI hashes sha256 = hash; # FIXME: check nix version & use SRI hashes
}) })
else assert repository.type == "Git"; builtins.fetchGit { else
url = repository.url; assert repository.type == "Git";
rev = revision; builtins.fetchGit {
# hash = hash; inherit (repository) url;
}; rev = revision;
# hash = hash;
};
mkPyPiSource = { url, hash, ... }: mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl { builtins.fetchurl {
inherit url; inherit url;
sha256 = hash; sha256 = hash;
}; };
mkChannelSource = { url, hash, ... }: mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball { builtins.fetchTarball {
inherit url; inherit url;
sha256 = hash; sha256 = hash;

View file

@ -1,5 +1,17 @@
{ {
"pins": { "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": { "nix-pkgs": {
"type": "Git", "type": "Git",
"repository": { "repository": {
@ -19,4 +31,4 @@
} }
}, },
"version": 3 "version": 3
} }

2
pyproject.toml Normal file
View file

@ -0,0 +1,2 @@
[tool.isort]
profile = "black"

View file

@ -7,5 +7,6 @@ django-markdownx==4.0.5
django-tables2==2.7.0 django-tables2==2.7.0
markdown-iconfonts==3.0.0 markdown-iconfonts==3.0.0
Pillow==10.1.0 Pillow==10.1.0
qrcode>=7.4.2
Whoosh==2.7.4 Whoosh==2.7.4
loadcredential==1.1 loadcredential==1.1

View file

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import Suggestion, SuggestionComment
from comments.admin import CommentAdmin from comments.admin import CommentAdmin
from .models import Suggestion, SuggestionComment
class SuggestionAdmin(admin.ModelAdmin): class SuggestionAdmin(admin.ModelAdmin):
exclude = ("upvoting_users",) exclude = ("upvoting_users",)

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class SuggestionsConfig(AppConfig): class SuggestionsConfig(AppConfig):
name = 'suggestions' name = "suggestions"

View file

@ -1,4 +1,5 @@
from django import forms from django import forms
from .models import Suggestion from .models import Suggestion

View file

@ -1,10 +1,11 @@
# Generated by Django 3.1.2 on 2020-12-29 23:43 # Generated by Django 3.1.2 on 2020-12-29 23:43
import autoslug.fields import autoslug.fields
from django.conf import settings
import django.core.validators import django.core.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import website.validators import website.validators
@ -13,52 +14,189 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('inventory', '0001_initial'), ("inventory", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Suggestion', name="Suggestion",
fields=[ 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')), "id",
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)), models.AutoField(
('price', models.DecimalField(decimal_places=2, max_digits=6, validators=[django.core.validators.MinValueValidator(0)], verbose_name='prix en euros')), auto_created=True,
('buy_link', models.URLField(verbose_name="lien vers un site d'achat")), primary_key=True,
('nb_player_min', models.PositiveSmallIntegerField(verbose_name='nombre de joueur·se·s minimum')), serialize=False,
('nb_player_max', models.PositiveSmallIntegerField(verbose_name='nombre de joueur·se·s maximum')), verbose_name="ID",
('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')), "title",
('editor', models.CharField(blank=True, max_length=256, verbose_name='éditeur')), models.CharField(
('description', models.TextField(blank=True, help_text="Peut correspondre à celle de l'éditeur et ne doit pas contenir d'avis personnel", verbose_name='description')), max_length=256, unique=True, verbose_name="titre du jeu"
('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')), "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={ options={
'verbose_name': 'suggestion de jeu', "verbose_name": "suggestion de jeu",
'verbose_name_plural': 'suggestions de jeux', "verbose_name_plural": "suggestions de jeux",
'ordering': ['title'], "ordering": ["title"],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='SuggestionComment', name="SuggestionComment",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('text', models.TextField(verbose_name='texte')), "id",
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='date de publication')), models.AutoField(
('modified_on', models.DateTimeField(auto_now=True, verbose_name='date de modification')), auto_created=True,
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='auteur·ice')), primary_key=True,
('commented_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='suggestions.suggestion', verbose_name='suggestion')), 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={ options={
'verbose_name': 'commentaire sur une suggestion', "verbose_name": "commentaire sur une suggestion",
'verbose_name_plural': 'commentaires sur des suggestions', "verbose_name_plural": "commentaires sur des suggestions",
'ordering': ['created_on'], "ordering": ["created_on"],
}, },
), ),
] ]

View file

@ -6,29 +6,43 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('suggestions', '0001_initial'), ("suggestions", "0001_initial"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='suggestion', model_name="suggestion",
name='duration', name="duration",
), ),
migrations.AddField( migrations.AddField(
model_name='suggestion', model_name="suggestion",
name='duration_max', 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'), 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, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='suggestion', model_name="suggestion",
name='duration_min', 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'), 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, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='suggestion', model_name="suggestion",
name='duration_precisions', 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'), 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",
),
), ),
] ]

View file

@ -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"
),
),
]

View file

@ -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.db import models
from django.urls import reverse 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 comments.models import AbstractComment
from inventory.models import Category, Tag
from website.validators import MaxFileSizeValidator
class Suggestion(models.Model): class Suggestion(models.Model):

View file

@ -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> <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 %} {% endif %}
{% else %} {% 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 %} {% endif %}
{% if suggestion.description %} {% if suggestion.description %}

View file

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View file

@ -1,12 +1,13 @@
from django.urls import path from django.urls import path
from .views import ( from .views import (
SuggestionListView, AddSuggestionCommentView,
AddSuggestionView, AddSuggestionView,
DownvoteSuggestionView,
ModifySuggestionCommentView,
SuggestionListView,
SuggestionView, SuggestionView,
UpvoteSuggestionView, UpvoteSuggestionView,
DownvoteSuggestionView,
AddSuggestionCommentView,
ModifySuggestionCommentView,
) )
app_name = "suggestions" app_name = "suggestions"

View file

@ -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.contrib import messages
from django.shortcuts import redirect from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count 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 comments.views import AddCommentView, ModifyCommentView
from .models import Suggestion, SuggestionComment
from .forms import AddSuggestionForm from .forms import AddSuggestionForm
from .models import Suggestion, SuggestionComment
class SuggestionListView(ListView): class SuggestionListView(ListView):

View file

@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import MarkdownPage
from markdownx.admin import MarkdownxModelAdmin from markdownx.admin import MarkdownxModelAdmin
from .models import MarkdownPage
admin.site.register(MarkdownPage, MarkdownxModelAdmin) admin.site.register(MarkdownPage, MarkdownxModelAdmin)

View file

@ -1,6 +1,7 @@
import markdown
import re import re
import markdown
class NbspPreprocessor(markdown.preprocessors.Preprocessor): class NbspPreprocessor(markdown.preprocessors.Preprocessor):
"""Replace regular spaces with non-breaking spaces within a text around relevant """Replace regular spaces with non-breaking spaces within a text around relevant

View file

@ -1,28 +1,43 @@
# Generated by Django 3.1.2 on 2020-12-27 11:34 # Generated by Django 3.1.2 on 2020-12-27 11:34
from django.db import migrations, models
import markdownx.models import markdownx.models
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='MarkdownPage', name="MarkdownPage",
fields=[ 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')), "id",
('content', markdownx.models.MarkdownxField(verbose_name='Contenu')), 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={ options={
'verbose_name': 'page Markdown', "verbose_name": "page Markdown",
'verbose_name_plural': 'pages Markdown', "verbose_name_plural": "pages Markdown",
'ordering': ['slug'], "ordering": ["slug"],
}, },
), ),
] ]

View 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"
),
),
]

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

View file

@ -5,7 +5,7 @@
<p>Vous n'avez pas la permission pour consulter cette page.</p> <p>Vous n'avez pas la permission pour consulter cette page.</p>
{% if not user.is_authenticated %} {% if not user.is_authenticated %}
<p>Cet accès vous est probablement refusé car vous n'êtes actuellement pas connecté·e. <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 %} {% endif %}
<p>Vous pouvez retourner sur la <a href="{% url "website:home" %}">page d'accueil</a>.</p> <p>Vous pouvez retourner sur la <a href="{% url "website:home" %}">page d'accueil</a>.</p>
{% endblock %} {% endblock %}

View file

@ -1,3 +1,3 @@
<footer> <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> </footer>

View file

@ -2,7 +2,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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="text/css" rel="stylesheet" href="{% static "css/style.css" %}" />
<link type="image/png" rel="shortcut icon" href="{% static "img/favicon.png" %}"/> <link type="image/png" rel="shortcut icon" href="{% static "img/favicon.png" %}"/>
<script src="{% static "js/jquery-3.4.1.min.js" %}"></script> <script src="{% static "js/jquery-3.4.1.min.js" %}"></script>

View file

@ -1,5 +1,5 @@
<header> <header>
<h1>GestioJeux</h1> <h1>GestioHackens</h1>
<div id="mobile_nav_trigger" onclick="toggle_mobile_nav()"> <div id="mobile_nav_trigger" onclick="toggle_mobile_nav()">
<i class="fa fa-fw fa-bars" aria-hidden="true"></i> Menu <i class="fa fa-fw fa-bars" aria-hidden="true"></i> Menu
@ -10,7 +10,9 @@
<div> <div>
<a {% if url_name == "home" %}class="current"{% endif %} href="{% url "website:home" %}"><i class="fa fa-home" aria-hidden="true"></i></a> <a {% if url_name == "home" %}class="current"{% endif %} href="{% url "website:home" %}"><i class="fa fa-home" aria-hidden="true"></i></a>
<a {% if url_name == "inventory" %}class="current"{% endif %} href="{% url "inventory:inventory" %}">Inventaire</a> <a {% if url_name == "inventory" %}class="current"{% endif %} href="{% url "inventory:inventory" %}">Inventaire</a>
{% comment %}
<a {% if url_name == "suggestions" %}class="current"{% endif %} href="{% url "suggestions:suggestions" %}">Suggestions</a> <a {% if url_name == "suggestions" %}class="current"{% endif %} href="{% url "suggestions:suggestions" %}">Suggestions</a>
{% endcomment %}
{% if perms.inventory.can_see_loan_details %} {% if perms.inventory.can_see_loan_details %}
<a href={% url "inventory:all_loans" %}>Gestion des emprunts</a> <a href={% url "inventory:all_loans" %}>Gestion des emprunts</a>
{% endif %} {% endif %}
@ -21,9 +23,9 @@
<div id="account"> <div id="account">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a class="username" href="{% url "accounts:account_settings" %}">{{ request.user }}</a> <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 %} {% 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 %} {% endif %}
</div> </div>
{% endwith %} {% endwith %}

View file

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View file

@ -1,4 +1,5 @@
from django.urls import path from django.urls import path
from .views import MarkdownPageView from .views import MarkdownPageView
app_name = "website" app_name = "website"

View file

@ -1,6 +1,7 @@
from django.views.generic import DetailView
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.views.generic import DetailView
from markdownx.utils import markdownify from markdownx.utils import markdownify
from .models import MarkdownPage from .models import MarkdownPage