Add game app with group and music video models, views, and templates

This commit is contained in:
Edgar P. Burkhart 2025-06-13 17:10:56 +02:00
parent d7545ab43c
commit ba746c9cae
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
20 changed files with 315 additions and 48 deletions

1
.gitignore vendored
View file

@ -166,4 +166,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View file

@ -7,3 +7,11 @@ header .container {
align-items: center;
justify-content: space-between;
}
form a[role="button"] {
width: 100%;
}
i.ri-vip-crown-fill {
color: var(--pico-color-amber-200);
}

View file

@ -1,36 +1,52 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Musik{% endblock %}</title>
<link rel="stylesheet" href="{% static "css/main.css" %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.purple.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.min.css">
</head>
<body>
<header>
<div class="container">
<a href="{% url 'index' %}">
<img class="logo" src="{% static "logo.svg" %}" height="64" width="64" alt="Musik">
</a>
<nav>
<ul>
{% if user.is_authenticated %}
<li>{{ user.username }}</li>
<li><form action="{% url 'logout' %}" method="post">{% csrf_token %}<input type="submit" value="Se déconnecter" class="logout"></form></li>
{% else %}
<li><a href="{% url 'login' %}" role="button">Se connecter <i class="ri-login-box-fill"></i></a></li>
{% endif %}
</ul>
</nav>
</div>
</header>
<main class="container">
{% block content %}
{% endblock content %}
</main>
</body>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% block title %}Musik{% endblock %}
</title>
<link rel="stylesheet" href="{% static "css/main.css" %}">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.purple.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.colors.min.css">
</head>
<body>
<header>
<div class="container">
<a href="{% url 'index' %}">
<img class="logo"
src="{% static "logo.svg" %}"
height="64"
width="64"
alt="Musik">
</a>
<nav>
<ul>
{% if user.is_authenticated %}
<li>{{ user.username }}</li>
<li>
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<input type="submit" value="Se déconnecter" class="logout">
</form>
</li>
{% else %}
<li>
<a href="{% url 'login' %}" role="button">Se connecter <i class="ri-login-box-fill"></i></a>
</li>
{% endif %}
</ul>
</nav>
</div>
</header>
<main class="container">
{% block content %}
{% endblock content %}
</main>
</body>
</html>

View file

@ -1,12 +1,9 @@
{% if form.non_field_errors %}
<ul class="form-errors">
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
<ul class="form-errors">
{% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% endif %}
<form method="post" action="{% url "login" %}">
<form method="post" {% if action %}action="{% url action %}"{% endif %}>
{% csrf_token %}
<fieldset>
{% for field in form %}
@ -16,9 +13,7 @@
</label>
{% if field.errors %}
<ul class="form-errors">
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
{% for error in field.errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% endif %}
{% endfor %}

View file

@ -1,5 +1,7 @@
{% extends "base.html" %}
{% block content %}
<h1>Musik</h1>
<h1>Musik</h1>
{% if user.is_authenticated %}
{% include "game/home.html" %}
{% endif %}
{% endblock content %}

View file

@ -1,7 +1,6 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
<h1>Connexion</h1>
{% form form submit="Se connecter" %}
<h1>Connexion</h1>
{% form form submit="Se connecter" %}
{% endblock content %}

0
game/__init__.py Normal file
View file

6
game/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GameConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "game"

View file

@ -0,0 +1,72 @@
# Generated by Django 5.2.3 on 2025-06-13 14:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Group",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField()),
(
"members",
models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="owned_group_set",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="MusicVideo",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("yt_id", models.CharField(max_length=16)),
("blacklisted", models.BooleanField(default=False)),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="game.group"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.3 on 2025-06-13 15:04
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("game", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="group",
name="name",
field=models.CharField(verbose_name="Nom du groupe"),
),
migrations.AlterUniqueTogether(
name="group",
unique_together={("name", "owner")},
),
]

View file

24
game/models.py Normal file
View file

@ -0,0 +1,24 @@
from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
class Group(models.Model):
name = models.CharField(verbose_name="Nom du groupe")
owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="owned_group_set"
)
members = models.ManyToManyField(User, blank=True)
def get_absolute_url(self):
return reverse("group_detail", kwargs={"pk": self.pk})
class Meta:
unique_together = ["name", "owner"]
class MusicVideo(models.Model):
yt_id = models.CharField(max_length=16)
user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
blacklisted = models.BooleanField(default=False)

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
<h1></h1>
<dialog open>
<article>
<header>
<p>
<i class="ri-group-2-fill"></i> {{ group.name }}
</p>
</header>
<p>
Confirmer la supression du groupe <strong>{{ group.name }}</strong> ?
</p>
<form method="post">
{% csrf_token %}
<button type="submit">
<i class="ri-delete-bin-fill"></i> Confirmer
</button>
<a href="{{ group.get_absolute_url }}" role="button" class="secondary"><i class="ri-close-line"></i> Annuler</a>
</form>
</article>
</dialog>
{% endblock content %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<h1>
<i class="ri-group-2-fill"></i> {{ group.name }}
</h1>
<p>
<a href="{% url "group_update" pk=group.pk %}"><i class="ri-edit-line"></i> Modifier le groupe</a>
</p>
<h2>Membres</h2>
<ul>
<li>
{{ group.owner }} <i class="ri-vip-crown-fill"></i>
</li>
{% for member in group.members.all %}<li>{{ member }}</li>{% endfor %}
</ul>
{% endblock content %}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load form %}
{% block content %}
{% if group %}
<h1>
<i class="ri-group-2-fill"></i> {{ group.name }}
</h1>
<p>
<a href="{% url "group_delete" group.pk %}">Supprimer le groupe</a>
</p>
{% else %}
<h1>Créer un groupe</h1>
{% endif %}
{% form form %}
{% endblock content %}

View file

@ -0,0 +1,19 @@
<p>Bienvenue {{ user.username }} !</p>
<h2>Mes groupes</h2>
<p>
<a href="{% url "group_create" %}" role="button"><i class="ri-add-box-fill"></i> Créer un groupe</a>
</p>
{% if user.owned_group_set.exists or user.group_set.exists %}
<ul>
{% for group in user.owned_group_set.all %}
<li>
<a href="{{ group.get_absolute_url }}">{{ group.name }}</a> <i class="ri-vip-crown-fill"></i>
</li>
{% endfor %}
{% for group in user.group_set.all %}
<li>
<a href="{{ group.get_absolute_url }}">{{ group.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}

14
game/urls.py Normal file
View file

@ -0,0 +1,14 @@
from django.urls import path
from . import views
urlpatterns = [
path("group/create/", views.GroupCreateView.as_view(), name="group_create"),
path(
"group/<int:pk>/update/", views.GroupUpdateView.as_view(), name="group_update"
),
path(
"group/<int:pk>/delete/", views.GroupDeleteView.as_view(), name="group_delete"
),
path("group/<int:pk>/", views.GroupDetailView.as_view(), name="group_detail"),
]

33
game/views.py Normal file
View file

@ -0,0 +1,33 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from . import models
class OwnerFilterMixin(LoginRequiredMixin):
def get_queryset(self):
return super().get_queryset().filter(owner=self.request.user)
class GroupMixin:
model = models.Group
fields = ["name"]
class GroupCreateView(LoginRequiredMixin, GroupMixin, CreateView):
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
class GroupUpdateView(OwnerFilterMixin, GroupMixin, UpdateView):
pass
class GroupDeleteView(OwnerFilterMixin, GroupMixin, DeleteView):
success_url = "/"
class GroupDetailView(OwnerFilterMixin, GroupMixin, DetailView):
pass

View file

@ -32,6 +32,7 @@ ALLOWED_HOSTS = []
INSTALLED_APPS = [
"base",
"game",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",

View file

@ -20,5 +20,6 @@ from django.urls import include, path
urlpatterns = [
path("", include("base.urls")),
path("game/", include("game.urls")),
path("admin/", admin.site.urls),
]