Add game app with group and music video models, views, and templates
This commit is contained in:
parent
d7545ab43c
commit
ba746c9cae
20 changed files with 315 additions and 48 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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/
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
0
game/__init__.py
Normal file
6
game/apps.py
Normal file
6
game/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GameConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "game"
|
72
game/migrations/0001_initial.py
Normal file
72
game/migrations/0001_initial.py
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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")},
|
||||
),
|
||||
]
|
0
game/migrations/__init__.py
Normal file
0
game/migrations/__init__.py
Normal file
24
game/models.py
Normal file
24
game/models.py
Normal 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)
|
24
game/templates/game/group_confirm_delete.html
Normal file
24
game/templates/game/group_confirm_delete.html
Normal 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 %}
|
16
game/templates/game/group_detail.html
Normal file
16
game/templates/game/group_detail.html
Normal 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 %}
|
15
game/templates/game/group_form.html
Normal file
15
game/templates/game/group_form.html
Normal 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 %}
|
19
game/templates/game/home.html
Normal file
19
game/templates/game/home.html
Normal 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
14
game/urls.py
Normal 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
33
game/views.py
Normal 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
|
|
@ -32,6 +32,7 @@ ALLOWED_HOSTS = []
|
|||
|
||||
INSTALLED_APPS = [
|
||||
"base",
|
||||
"game",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue