Compare commits

...

7 commits

Author SHA1 Message Date
sinavir 684e277e39 feat(qrcode): Automatic generation of qrcodes 2024-07-03 01:50:01 +02:00
sinavir 6952cbbc2d feat(FORK): Remove unnecessary fields 2024-07-03 01:47:59 +02:00
sinavir fa07727a35 feat(FORK): jeu -> outil/hackens 2024-07-03 01:47:59 +02:00
sinavir 1aa5fb6dfe feat(FORK): Disable suggestions 2024-07-03 01:17:29 +02:00
sinavir bdb8da43c5 fix(pk field): Make manage.py makemigrations happy 2024-07-03 01:17:29 +02:00
sinavir 8578af1d41 feat: switch to authens
Be careful there is no User migration
2024-07-03 01:17:29 +02:00
sinavir 253acde240 fix(shell): Add dependency 2024-07-03 01:17:10 +02:00
45 changed files with 1318 additions and 447 deletions

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

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

@ -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,9 @@
from django.urls import include, path
import django.contrib.auth.views as dj_auth_views
from .views import LoginView, LogoutView, PasswordChangeView, AccountSettingsView
import django_cas_ng.views
from .views import PasswordChangeView, AccountSettingsView
app_name = "accounts"
cas_patterns = [
path("login/", django_cas_ng.views.LoginView.as_view(), name="cas_ng_login"),
path("logout/", django_cas_ng.views.LogoutView.as_view(), name="cas_ng_logout"),
path(
"callback/",
django_cas_ng.views.CallbackView.as_view(),
name="cas_ng_proxy_callback",
),
]
accounts_patterns = [
path("cas/", include(cas_patterns)),
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
path("password_login/", dj_auth_views.LoginView.as_view(), name="password_login"),
path("change_password/", PasswordChangeView.as_view(), name="change_password"),
path("settings/", AccountSettingsView.as_view(), name="account_settings"),
]

View file

@ -14,59 +14,6 @@ from urllib.parse import quote as urlquote
from .forms import AccountSettingsForm
class LoginView(TemplateView):
template_name = "registration/login_switch.html"
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect(self.get_next_url() or "/")
return super().dispatch(request, *args, **kwargs)
def get_next_url(self):
if self.request.method == "GET":
req_dict = self.request.GET
elif self.request.method == "POST":
req_dict = self.request.POST
return req_dict.get("next")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
next_url = self.get_next_url()
if next_url:
context["pass_url"] = "{}?next={}".format(
reverse("accounts:password_login"), urlquote(next_url, safe="")
)
context["cas_url"] = "{}?next={}".format(
reverse("accounts:cas_ng_login"), urlquote(next_url, safe="")
)
else:
context["pass_url"] = reverse("accounts:password_login")
context["cas_url"] = reverse("accounts:cas_ng_login")
return context
class LogoutView(RedirectView):
permanent = False
def get_redirect_url(self, *args, **kwargs):
CAS_BACKEND_NAME = "accounts.backends.CasBackend"
if self.request.session["_auth_user_backend"] != CAS_BACKEND_NAME:
auth_logout(self.request)
if "next" in self.request.GET:
return self.request.GET["next"]
return reverse("website:home")
if "next" in self.request.GET:
return "{}?next={}".format(
reverse("accounts:cas_ng_logout"),
urlquote(self.request.GET["next"], safe=""),
)
return reverse("accounts:cas_ng_logout")
@receiver(user_logged_in)
def on_login(request, user, **kwargs):
messages.success(request, "Connexion réussie. Bienvenue, {}.".format(user))

View file

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

View file

@ -24,7 +24,7 @@
</div>
{% endif %}
{% empty %}
<p>(Aucun commentaire sur ce jeu)</p>
<p>(Aucun commentaire sur cet outil)</p>
{% endfor %}
{% if not edited_comment %}
{% if request.user.is_authenticated %}
@ -34,6 +34,6 @@
<button type="submit"><i class="fa fa-paper-plane" aria-hidden="true"></i> Envoyer le commentaire</button>
</form>
{% else %}
<p><a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour ajouter un commentaire.</p>
<p><a href="{% url "authens:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour ajouter un commentaire.</p>
{% endif %}
{% endif %}

View file

@ -7,13 +7,14 @@ let
nix-pkgs = import sources.nix-pkgs { inherit pkgs; };
python3 = pkgs.python3.override {
packageOverrides = _: _: {
packageOverrides = final: _: {
inherit (nix-pkgs)
django-autoslug
django-cas-ng
loadcredential
markdown-icons
python-cas
;
authens = final.callPackage ./nix/authens {};
};
};
in
@ -25,15 +26,20 @@ in
packages = [
(python3.withPackages (ps: [
ps.django
ps.django-types
ps.django-autoslug
ps.loadcredential
ps.django-cas-ng
ps.django-cleanup
ps.django-haystack
ps.django-markdownx
ps.django-tables2
ps.pillow
ps.whoosh
ps.markdown-icons
ps.authens
ps.qrcode
ps.pillow
# Django haystack is drunk
ps.setuptools

View file

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
import os
from django.urls import reverse_lazy
from loadcredential import Credentials
# Secrets
@ -80,7 +81,6 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_cas_ng",
"django_tables2",
"markdownx",
"haystack",
@ -88,9 +88,10 @@ INSTALLED_APPS = [
"accounts",
"comments",
"inventory",
"suggestions",
# "suggestions",
"loans",
"django_cleanup", # Keep last
"authens",
]
MIDDLEWARE = [
@ -101,7 +102,6 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_cas_ng.middleware.CASMiddleware",
]
ROOT_URLCONF = "gestiojeux.urls"
@ -124,11 +124,12 @@ TEMPLATES = [
WSGI_APPLICATION = "gestiojeux.wsgi.application"
AUTH_USER_MODEL = "accounts.User"
# Authentication backends
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"accounts.backends.CasBackend",
"authens.backends.ENSCASBackend",
"authens.backends.OldCASBackend",
)
# Password validation
@ -143,7 +144,11 @@ AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LOGIN_URL = "accounts:login"
LOGIN_URL = reverse_lazy("authens:login")
LOGIN_REDIRECT_URL = reverse_lazy("home")
LOGOUT_REDIRECT_URL = reverse_lazy("home")
# Use markdown extensions
MARKDOWNX_MARKDOWN_EXTENSIONS = [
@ -185,12 +190,7 @@ MEDIA_URL = "/media/"
STATIC_ROOT = os.path.join(PUBLIC_DIR, "static")
MEDIA_ROOT = os.path.join(PUBLIC_DIR, "media")
# CAS settings
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
CAS_SERVER_URL = "https://cas.eleves.ens.fr/"
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
CAS_VERSION = "CAS_2_SAML_1_0"
CAS_IGNORE_REFERER = True
CAS_LOGIN_MSG = None
CAS_LOGIN_URL_NAME = "accounts:cas_ng_login"
CAS_LOGOUT_URL_NAME = "accounts:cas_ng_logout"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View file

@ -22,8 +22,9 @@ urlpatterns = [
path("admin/", admin.site.urls),
path("markdownx/", include("markdownx.urls")),
path("inventory/", include("inventory.urls")),
path("suggestions/", include("suggestions.urls")),
# path("suggestions/", include("suggestions.urls")),
path("account/", include("accounts.urls")),
path("authens/", include("authens.urls")),
path("", include("website.urls")),
]

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
from django.db import migrations, models
import django.db.models.deletion
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

@ -37,46 +37,9 @@ class Tag(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)
nb_player_min = models.PositiveSmallIntegerField(
verbose_name="nombre de joueur·se·s minimum"
)
nb_player_max = models.PositiveSmallIntegerField(
verbose_name="nombre de joueur·se·s maximum"
)
player_range = models.CharField(
max_length=256,
blank=True,
help_text="Affichage personnalisé pour le nombre de joueur·se·s",
verbose_name="nombre de joueur·se·s",
)
duration_min = models.PositiveSmallIntegerField(
verbose_name="durée de partie minimale",
help_text="En minutes, telle qu'indiquée par l'éditeur",
)
duration_max = models.PositiveSmallIntegerField(
verbose_name="durée de partie maximale",
help_text="En minutes, telle qu'indiquée par l'éditeur, identique à la durée minimale si laissée vide",
blank=True,
)
duration = models.CharField(
max_length=256,
blank=True,
help_text="Affichage personnalisé pour la durée de la partie",
verbose_name="durée de partie",
)
game_designer = models.CharField(
max_length=256, blank=True, verbose_name="game designer"
)
illustrator = models.CharField(
max_length=256, blank=True, verbose_name="illustrateur·trice"
)
editor = models.CharField(max_length=256, blank=True, verbose_name="éditeur")
category = models.ForeignKey(
Category, on_delete=models.RESTRICT, verbose_name="catégorie"
)
@ -95,45 +58,18 @@ class Game(models.Model):
class Meta:
ordering = ["title"]
verbose_name = "jeu"
verbose_name_plural = "jeux"
verbose_name = "outil"
verbose_name_plural = "outils"
def __str__(self):
return self.title
def clean(self):
if not self.nb_player_min or not self.nb_player_max or not self.duration_min:
return
if self.nb_player_min > self.nb_player_max:
raise ValidationError(
{
"nb_player_max": "Le nombre de joueur·se·s maximum doit être supérieur au nombre de joueur·se·s minimum"
}
)
if self.duration_max is None:
self.duration_max = self.duration_min
if self.duration_min > self.duration_max:
raise ValidationError(
{
"duration_max": "La durée maximale doit être supérieure à la durée minimale"
}
)
def get_player_range(self):
if self.player_range:
return self.player_range
elif self.nb_player_min != self.nb_player_max:
return "{} à {} joueur·se·s".format(self.nb_player_min, self.nb_player_max)
else:
return "{} joueur·se·s".format(self.nb_player_min)
return ""
def get_duration_range(self):
if self.duration:
return self.duration
elif self.duration_min != self.duration_max:
return "{} à {} min".format(self.duration_min, self.duration_max)
else:
return "{} min".format(self.duration_min)
return ""
def get_absolute_url(self):
return reverse("inventory:game", args=(self.slug,))
@ -141,13 +77,13 @@ class Game(models.Model):
class GameComment(AbstractComment):
commented_object = models.ForeignKey(
Game, on_delete=models.CASCADE, related_name="comments", verbose_name="jeu"
Game, on_delete=models.CASCADE, related_name="comments", verbose_name="outil"
)
class Meta:
ordering = ["created_on"]
verbose_name = "commentaire sur un jeu"
verbose_name_plural = "commentaires sur des jeux"
verbose_name = "commentaire sur un outil"
verbose_name_plural = "commentaires sur des outils"
def get_modification_url(self):
return reverse(
@ -157,7 +93,7 @@ class GameComment(AbstractComment):
class GameLoan(AbstractLoan):
lent_object = models.ForeignKey(
Game, on_delete=models.CASCADE,
verbose_name="jeu emprunté"
verbose_name="outil emprunté"
)
class Meta(AbstractLoan.Meta):

View file

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

View file

@ -4,7 +4,7 @@
<h1><i class="fa fa-bookmark" aria-hidden="true"></i> {{ category.name }}</h1>
{% with game_list=category.game_set.all %}
<p>Il y a {{ game_list|length }} jeu{{ game_list|pluralize:"x" }} dans cette
<p>Il y a {{ game_list|length }} outil{{ game_list|pluralize:"s" }} dans cette
étagère&nbsp;:</p>
{% for game in game_list %}
{% include "./partials/game_item.html" %}

View file

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

View file

@ -12,9 +12,6 @@
<div id="details">
<p><i class="fa fa-fw fa-bookmark"></i> <a href="{{ game.category.get_absolute_url }}">{{ game.category }}</a></p>
<hr/>
<p><i class="fa fa-fw fa-users" aria-hidden="true"></i> {{ game.get_player_range }}</p>
<p><i class="fa fa-fw fa-clock-o" aria-hidden="true"></i> {{ game.get_duration_range }}</p>
<hr/>
<p><i class="fa fa-fw fa-tags" aria-hidden="true"></i>
{% for tag in game.tags.all %}
<a href="{{ tag.get_absolute_url }}">{{ tag }}</a>{% if not forloop.last %},{% endif %}
@ -23,14 +20,11 @@
{% endfor %}
</p>
<hr/>
<p><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> {{ game.game_designer|default:"(Game designer inconnu·e)" }}</p>
<p><i class="fa fa-fw fa-paint-brush" aria-hidden="true"></i> {{ game.illustrator|default:"(Illustrateur·trice inconnu·e)" }}</p>
<p><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> {{ game.editor|default:"(Éditeur inconnu)" }}</p>
</div>
</div>
{% if is_borrowed %}
<p class="warning">Ce jeu est emprunté depuis le {{ loan.borrow_date }}.</p>
<p class="warning">Cet outil est emprunté depuis le {{ loan.borrow_date }}.</p>
{% endif %}
<a class="button" href="{% url "inventory:game_loan" game.slug %}">
@ -45,7 +39,7 @@
<p class="warning">{{ game.missing_elements|linebreaksbr }}</p>
{% endif %}
<h2>Commentaires et propositions de variantes</h2>
<h2>Commentaires</h2>
{% url "inventory:add_game_comment" game.slug as add_comment_url %}
{% include "comments.html" with comments=game.comments.all %}
{% endblock %}

View file

@ -1,9 +1,9 @@
{% extends "base.html" %}
{% block "content" %}
<h1>Liste des jeux</h1>
<h1>Liste des outils</h1>
<p>Il y a {{ paginator.count }} jeu{{ paginator.count|pluralize:"x" }} en salle jeux&nbsp;:</p>
<p>Il y a {{ paginator.count }} outil{{ paginator.count|pluralize:"s" }} en Hackens&nbsp;:</p>
{% include "partials/pagination.html" %}
{% for game in game_list %}

View file

@ -1,9 +1,9 @@
{% extends "base.html" %}
{% block "content" %}
<h1>Inventaire du club Jeux</h1>
<h1>Inventaire d'HackENS</h1>
Rechercher dans la ludothèque:
Rechercher dans les outils:
<form class="search" method="get" action="{% url "inventory:search" %}">
<input type="search" name="q" />
<button type="submit"><i class="fa fa-fw fa-search" aria-hidden="true"></i></button>
@ -17,19 +17,19 @@
<a class="button" href="{% url "inventory:category_list" %}">
Liste des étagères
<p class="helptext">
Chaque jeu est rangé dans une unique étagère comme en salle Jeux
Chaque outils est rangé dans une étagère
</p>
</a>
<a class="button" href="{% url "inventory:tag_list" %}">
Liste des tags
<p class="helptext">
Chaque jeu est marqué par tous les tags qui lui correspondent
Chaque outils est marqué par tous les tags qui lui correspondent
</p>
</a>
<a class="button" href="{% url "inventory:game_list" %}">
Liste alphabétique
<p class="helptext">
La liste complète des jeux de la ludothèque
La liste complète des outils
</p>
</a>
{% endblock %}

View file

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

View file

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

View file

@ -1,20 +1,9 @@
<a class="inventory_item game" href="{{ game.get_absolute_url }}">
<span class="title">{{ game.title }}</span>
<span class="details">
<span><i class="fa fa-users" aria-hidden="true"></i> {{ game.get_player_range }}</span>
<span><i class="fa fa-clock-o" aria-hidden="true"></i> {{ game.get_duration_range }}</span>
<span><i class="fa fa-bookmark"></i> {{ game.category }}</span>
{% for tag in game.tags.all %}
<span><i class="fa fa-tag" aria-hidden="true"></i> {{ tag }}</span>
{% endfor %}
{% if game.game_designer %}
<span><i class="fa fa-wrench" aria-hidden="true"></i> {{ game.game_designer }}</span>
{% endif %}
{% if game.illustrator %}
<span><i class="fa fa-paint-brush" aria-hidden="true"></i> {{ game.illustrator }}</span>
{% endif %}
{% if game.editor %}
<span><i class="fa fa-cogs" aria-hidden="true"></i> {{ game.editor }}</span>
{% endif %}
</span>
</a>

View file

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

View file

@ -1,21 +1,10 @@
from django.urls import path
from .views import (
InventoryView,
CategoryListView,
CategoryView,
TagListView,
TagView,
GameListView,
GameView,
AddGameCommentView,
ModifyGameCommentView,
InventorySearchView,
GameLoanView,
BorrowGameView,
ReturnGameView,
OngoingLoansView,
DetailLoanView,
)
from .views import (AddGameCommentView, BorrowGameView, CategoryListView,
CategoryView, DetailLoanView, GameListView, GameLoanView,
GameView, InventorySearchView, InventoryView,
ModifyGameCommentView, OngoingLoansView, QrCodeView,
ReturnGameView, TagListView, TagView)
app_name = "inventory"
@ -39,6 +28,12 @@ urlpatterns = [
path("loans/game/<slug>/", GameLoanView.as_view(), name="game_loan"),
path("loans/return/<slug>/", ReturnGameView.as_view(), name="return_game"),
path("loans/borrow/<slug>/", BorrowGameView.as_view(), name="borrow_game"),
path("qrcode/<slug>/", QrCodeView.as_view(), name="qrcode"),
path(
"qrcode/borrow/<slug>/",
QrCodeView.as_view(url="inventory:borrow_game"),
name="qrcode_borrow",
),
path("loans/ongoing/", OngoingLoansView.as_view(), name="ongoing_loans"),
path("loans/all/", DetailLoanView.as_view(), name="all_loans"),
]

View file

@ -1,13 +1,17 @@
from django.views.generic import TemplateView, ListView, DetailView
from django.contrib.auth.mixins import PermissionRequiredMixin
from haystack.generic_views import SearchView
from haystack.forms import SearchForm
from haystack.query import SearchQuerySet
from django_tables2.views import SingleTableView
import qrcode
from comments.views import AddCommentView, ModifyCommentView
from loans.views import BorrowView, ReturnView, DetailLoanView
from .models import Category, Tag, Game, GameComment, GameLoan
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.http import HttpResponse
from django.urls import reverse
from django.views.generic import DetailView, ListView, TemplateView
from django_tables2.views import SingleTableView
from haystack.forms import SearchForm
from haystack.generic_views import SearchView
from haystack.query import SearchQuerySet
from loans.views import BorrowView, DetailLoanView, ReturnView
from .forms import BorrowGameForm
from .models import Category, Game, GameComment, GameLoan, Tag
from .tables import LoanTable, OngoingLoansTable
@ -102,3 +106,13 @@ class DetailLoanView(PermissionRequiredMixin, SingleTableView):
table_class = LoanTable
template_name = "inventory/loans/loans_table.html"
class QrCodeView(DetailView):
model = Game
url = "inventory:game" # Sensible default
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="image/png")
img = qrcode.make(reverse(self.url, kwargs={"slug": self.get_object().slug}))
img.save(response, "PNG")
return response

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

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

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

@ -4,7 +4,7 @@ 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 django.contrib.auth.models import User
from inventory.models import Category, Tag
from comments.models import AbstractComment

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>
{% endif %}
{% else %}
<p><a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour voter pour une suggestion.</p>
<p><a href="{% url "authens:login" %}?next={{ request.get_full_path }}">Connectez-vous</a> pour voter pour une suggestion.</p>
{% endif %}
{% if suggestion.description %}

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>
{% if not user.is_authenticated %}
<p>Cet accès vous est probablement refusé car vous n'êtes actuellement pas connecté·e.
Vous pouvez vous rendre à la page de <a href="{% url "accounts:login" %}?next={{ request.get_full_path }}">connexion</a>.</p>
Vous pouvez vous rendre à la page de <a href="{% url "authens:login" %}?next={{ request.get_full_path }}">connexion</a>.</p>
{% endif %}
<p>Vous pouvez retourner sur la <a href="{% url "website:home" %}">page d'accueil</a>.</p>
{% endblock %}

View file

@ -1,3 +1,3 @@
<footer>
Pour tout problème, contactez <tt class="antispam">rf.sne@xuejopser</tt>.
Pour tout problème, contactez <tt class="antispam">rf.sne@snekcah</tt>.
</footer>

View file

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

View file

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