Compare commits

..

3 commits

Author SHA1 Message Date
Maurice Debray
84af71a6c7 linting 2022-02-26 12:44:47 +01:00
Maurice Debray
f8b96586db Merge branch 'master' into mdebray/repondre_aux_sondages_a_la_place_des_fanfarons 2022-02-26 12:44:01 +01:00
Maurice Debray
b49accf96e Repondre à la place des fanfarons 2022-02-26 12:42:45 +01:00
50 changed files with 162 additions and 999 deletions

View file

@ -1 +0,0 @@
insecure-secret_key

View file

@ -1,28 +1,40 @@
from pathlib import Path
import os
from django.utils.translation import gettext_lazy as _
from loadcredential import Credentials
credentials = Credentials(env_prefix="ERNESTOPHONE_")
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = credentials.get_json("DEBUG", False)
ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", [])
SECRET_KEY = credentials["SECRET_KEY"]
ADMINS = credentials.get_json("ADMINS", [])
SERVER_EMAIL = credentials.get("SERVER_EMAIL", "ernesto@localhost")
try:
from . import secret
except ImportError:
raise ImportError(
"The secret.py file is missing.\n"
"For a development environment, simply copy secret_example.py"
)
ACCOUNT_CREATION_PASS = credentials.get("ACCOUNT_CREATION_PASS", "dummy")
def import_secret(name):
"""
Shorthand for importing a value from the secret module and raising an
informative exception if a secret is missing.
"""
try:
return getattr(secret, name)
except AttributeError:
raise RuntimeError("Secret missing: {}".format(name))
SECRET_KEY = import_secret("SECRET_KEY")
ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL")
DBNAME = import_secret("DBNAME")
DBUSER = import_secret("DBUSER")
DBPASSWD = import_secret("DBPASSWD")
ACCOUNT_CREATION_PASS = import_secret("ACCOUNT_CREATION_PASS")
BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
INSTALLED_APPS = [
"propositions",
"trombonoscope",
"actu",
"colorful",
@ -77,22 +89,15 @@ TEMPLATES = [
WSGI_APPLICATION = "Ernestophone.wsgi.application"
###
# Database configuration
# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
DATABASES = credentials.get_json(
"DATABASES",
{
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
},
)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": DBNAME,
"USER": DBUSER,
"PASSWORD": DBPASSWD,
"HOST": "localhost",
}
}
AUTH_PASSWORD_VALIDATORS = [
{
@ -123,7 +128,7 @@ USE_L10N = True
USE_TZ = True
LOCALE_PATHS = (BASE_DIR / "locale",)
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
AUTH_PROFILE_MODEL = "gestion.ErnestoUser"
@ -144,18 +149,3 @@ AVATAR_PROVIDERS = (
"avatar.providers.DefaultAvatarProvider",
)
AVATAR_THUMB_FORMAT = "JPEG"
###
# Staticfiles configuration
STATIC_ROOT = credentials["STATIC_ROOT"]
STATIC_URL = "/static/"
MEDIA_ROOT = credentials.get("MEDIA_ROOT", BASE_DIR / "media")
MEDIA_URL = "/media/"
if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE

View file

@ -0,0 +1,23 @@
import os
from .common import * # noqa
from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEBUG = True
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE
STATIC_URL = "/static/"
STATIC_ROOT = "static"
MEDIA_URL = "/media/"
MEDIA_ROOT = "media"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}

View file

@ -0,0 +1,17 @@
import os
from .common import * # noqa
from .common import BASE_DIR
DEBUG = False
ALLOWED_HOSTS = [
"ernestophone.ens.fr",
"www.ernestophone.ens.fr",
]
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "..", "..", "public", "ernesto", "static")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "..", "media")

View file

@ -0,0 +1,9 @@
SECRET_KEY = "vpb%-1@$ha98w5^ji#@9v2_kxj)zdk5+e!9!fqniu2$#eg+46="
ADMINS = None
SERVER_EMAIL = "ernesto@localhost"
DBNAME = "ernesto"
DBUSER = "ernesto"
DBPASSWD = "YNp2rrXowJnDAFF3"
ACCOUNT_CREATION_PASS = "dummy"

View file

@ -13,13 +13,13 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
from gestion import views as gestion_views
urlpatterns = []
@ -69,5 +69,4 @@ urlpatterns += i18n_patterns(
),
prefix_default_language=False,
)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -3,26 +3,14 @@ from django.contrib import admin
from .models import Event, Participants
class ParticipantsAdmin(admin.ModelAdmin):
fields = [
"event",
"participant",
"reponse",
"instrument",
"instrument_autre",
"dont_play_main",
"details",
"creationDate",
"updateDate",
]
readonly_fields = ["creationDate", "updateDate"]
list_display = ["participant", "event", "reponse", "creationDate", "updateDate"]
def has_add_permission(self, req):
return False
def has_change_permission(self,obj, change=False):
return False
# Add event by admin page return a 502 error
admin.site.register(Event)
class ParticipantsAdmin(admin.ModelAdmin):
autocomplete_fields = ["event", "participant"]
class EventAdmin(admin.ModelAdmin):
search_fields = ["nom", "nomcourt", "lieu", "description", "desc_users"]
admin.site.register(Event, EventAdmin)
admin.site.register(Participants, ParticipantsAdmin)

View file

@ -1,8 +1,8 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from gestion.models import ErnestoUser
from calendrier.models import Event, Participants
from gestion.models import ErnestoUser
class ModifEventForm(forms.ModelForm):

View file

@ -1,31 +0,0 @@
# Generated by Django 2.2.25 on 2022-03-14 23:20
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("calendrier", "0006_auto_20210929_1629"),
]
operations = [
migrations.AddField(
model_name="participants",
name="creationDate",
field=models.DateTimeField(
auto_now_add=True,
default=django.utils.timezone.now,
verbose_name="Date de création",
),
preserve_default=False,
),
migrations.AddField(
model_name="participants",
name="updateDate",
field=models.DateTimeField(
auto_now=True, verbose_name="Dernière mise à jour"
),
),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 2.2.27 on 2022-03-22 13:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendrier', '0007_auto_20220314_2320'),
]
operations = [
migrations.AddConstraint(
model_name='participants',
constraint=models.UniqueConstraint(fields=('event', 'participant'), name='reponse unique aux event'),
),
]

View file

@ -2,6 +2,7 @@ import uuid
from django.db import models
from django.utils.translation import gettext_lazy as _
from gestion.models import INSTRU_CHOICES, ErnestoUser
ANSWERS = (
@ -72,11 +73,3 @@ class Participants(models.Model):
choices=[("Non", _("Non")), ("Oui", _("Oui"))],
)
details = models.CharField(max_length=50, blank=True)
creationDate = models.DateTimeField(
auto_now_add=True, verbose_name=_("Date de création")
)
updateDate = models.DateTimeField(
auto_now=True, verbose_name=_("Dernière mise à jour")
)
class Meta:
constraints = [ models.UniqueConstraint(fields=['event', 'participant'], name='reponse unique aux event') ]

View file

@ -46,15 +46,7 @@
<a href="mailto:fanfare@ens.fr" class="icon fa-envelope fa-5x "><span
class="label">Mail</span></a>
&nbsp;
&nbsp;
&nbsp;
&nbsp;
<a target="_blank" href="https://www.youtube.com/channel/UC_RahrdZLoBGpJQHwegV4tQ" class="icon fa-youtube-play fa-5x"><span class="label">Youtube</span> </a> &nbsp;
&nbsp;
<a target="_blank" href="https://www.instagram.com/ernestophone/" class="icon fa-instagram fa-5x"><span class="label">Instagram</span></a>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
{% extends "gestion/base.html" %}
{% load static %}
{% load staticfiles %}
{% load frenchmonth %}
{% load i18n %}
{% load translate %}
@ -43,35 +43,35 @@
<tr>
<td width="20%" align="left">
&lt;&lt;
{% if current_language == "fr" %}
{% ifequal current_language "fr" %}
<a href="{% url "calendrier:view-month" PreviousYear PreviousMonth %}">{{PreviousMonthName|frenchmonth}} {{PreviousYear}}</a>
{% else %}
<a href="{% url "calendrier:view-month" PreviousYear PreviousMonth %}">{{PreviousMonthName}} {{PreviousYear}}</a>
{% endif %}
{% endifequal %}
</td>
<td width="20%" align="center"><a href="{% url "calendrier:home" %}">{% trans "Aujourd'hui" %}</a></td>
<td width="20%" align="right">
{% if current_language == "fr" %}
{% ifequal current_language "fr" %}
<a href="{% url "calendrier:view-month" NextYear NextMonth %}">{{NextMonthName|frenchmonth}} {{NextYear}}</a> &gt;&gt;
{% else %}
<a href="{% url "calendrier:view-month" NextYear NextMonth %}">{{NextMonthName}} {{NextYear}}</a> &gt;&gt;
{% endif %}
{% endifequal %}
</td>
</tr>
</table>
<div id="calendar">
{% if user.profile.is_chef_event or user.profile.is_chef %}
{% if current_language == "fr" %}
{% ifequal current_language "fr" %}
{{Calendar_chef|translate}}
{% else %}
{{Calendar_chef}}
{% endif %}
{% endifequal %}
{% else %}
{% if current_language == "fr" %}
{% ifequal current_language "fr" %}
{{Calendar|translate}}
{% else %}
{{Calendar}}
{% endif %}
{% endifequal %}
{% endif%}
</div>
{% if user.profile.is_chef_event or user.profile.is_chef %}

View file

@ -223,9 +223,6 @@
</div>
</section>
</div>
{% if event.id == 573 %}
<div class="fireworks" style="pointer-events: none; position:fixed; top: 0; left: 0; right: 0; bottom: 0; height: 100%; weight: 100%;"></div>
{% endif %}
{% endblock %}
{% block script %}
<script>
@ -261,19 +258,11 @@ singleEvent.createWidget('#single-normal', function() {
console.log('#single-normal widget has been created');
});
{% if current_language == "fr" %}
{% ifequal current_language "fr" %}
singleEvent.setOption({ lang: 'fr' });
{% else %}
singleEvent.setOption({ lang: 'en' });
{% endif %}
{% endifequal %}
</script>
{% if event.id == 573 %}
<script src="https://unpkg.com/fireworks-js@2.x/dist/index.umd.js"></script>
<script>
const container = document.querySelector('.fireworks');
const fireworks = new Fireworks.default(container);
fireworks.updateOptions({ acceleration: 1.01, traceSpeed: 5 });
fireworks.start();
</script>
{% endif %}
{% endblock %}

View file

@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
from django.template.defaultfilters import urlencode
from django.test import Client, TestCase
from django.utils import timezone
from gestion.models import ErnestoUser
from ..models import Event

View file

@ -4,20 +4,20 @@ from calendar import monthrange
from collections import defaultdict
from datetime import date, datetime
from actu.models import Actu
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.utils.safestring import mark_safe
from django.views.generic import DeleteView, TemplateView, UpdateView
from gestion.mixins import ChefEventRequiredMixin
from gestion.models import Photo
from actu.models import Actu
from calendrier.calend import EventCalendar
from calendrier.forms import (ChangeDoodleName, EventForm, ModifEventForm,
ParticipantsForm)
from calendrier.models import Event, Participants
from gestion.mixins import ChefEventRequiredMixin, ChefRequiredMixin
from gestion.models import Photo
def generer(*args):
@ -212,7 +212,6 @@ class ViewEvent(LoginRequiredMixin, TemplateView):
else:
instru = participant.instrument
instru = "" if instru is None else instru
sure, maybe, namesoui, namespe, namesnon = instrument_count[instru]
if participant.reponse == "oui":
@ -287,7 +286,7 @@ class ViewEvent(LoginRequiredMixin, TemplateView):
context["event"] = event
context["instrument_count"] = instrument_count_l
context["participants"] = participants.order_by('-creationDate')
context["participants"] = participants
context["nboui"] = len(participants.filter(reponse="oui"))
context["nbpe"] = len(participants.filter(reponse="pe"))
context["nbnon"] = len(participants.filter(reponse="non"))
@ -374,16 +373,16 @@ class ReponseEvent(LoginRequiredMixin, TemplateView):
return context
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
ev = get_object_or_404(Event, id=self.kwargs["id"])
part = request.user.profile
try:
p = Participants.objects.get(event=ev, participant=part)
except Participants.DoesNotExist:
p = None
form = self.form_class(request.POST, instance=p)
if form.is_valid():
try:
p = Participants.objects.get(event=ev, participant=part)
p.delete()
except Participants.DoesNotExist:
pass
obj = form.save(commit=False)
# Si la participation existe déjà, ces 2 ligne sont redondantes
obj.event = ev
obj.participant = part
obj.save()

View file

@ -114,8 +114,19 @@ class UserProfileAdmin(UserAdmin):
user.save()
class ErnestoUserAdmin(admin.ModelAdmin):
search_fields = [
"user__username",
"user__first_name",
"user__last_name",
"phone",
"instru",
]
admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
admin.site.register(ErnestoUser, ErnestoUserAdmin)
admin.site.register(VideoGallery)
admin.site.register(Photo)
admin.site.register(Actu)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View file

@ -124,11 +124,11 @@
{% endif %}
<li class="nav-item">
{% if current_language == "fr" %}
{% ifequal current_language "fr" %}
<a class="nav-link" href="{% changelang "en" %}" ><img src="{% static 'images\en_flag.jpg' %}" width="60" height="40" style="vertical-align: middle"></a>
{% else %}
<a class="nav-link" href="{% changelang "fr" %}" ><img src="{% static 'images\fr_flag.jpg' %}" width="60" height="40" style="vertical-align: middle"></a>
{% endif %}
{% endifequal %}
</li>
</ul>
@ -145,10 +145,6 @@
<!-- Footer -->
<footer id="footer" style="background-color:rgb(228, 82, 47);">
<div class="copyright">
<ul class="icons">
<li><a target="_blank" href="https://cvec.etudiant.gouv.fr/"><img alt="Logo de la CVEC" src='{% static "images/cvec.png" %}' width="100px"/></a></li>
</ul>
<ul class="icons">
<li><a target="_blank" href="https://www.facebook.com/ernestophone"

View file

@ -15,9 +15,9 @@
<div class="7u 12u$(small)">
<p>{% trans "Propriétaire : "%} {% if instru.owner %}{{instru.owner}} {% else %}-{% endif %}<br>
{% trans "Statut : "%} {{instru.statut}} <br>
{% if instru.statut == 'Prêté' %}
{% ifequal instru.statut 'Prêté' %}
{% trans "Utilisateur : "%} {% if instru.user %}{{instru.user}} {% else %}-{% endif %}<br>
{% endif %}
{% endifequal %}
{% trans "Marque : "%} {% if instru.marque %}{{instru.marque}} {% else %}-{% endif %} <br>
{% trans "Modele : "%} {% if instru.model %}{{instru.model}} {% else %}-{% endif %}<br>
{% trans "Numéro de série : "%} {% if instru.serial %}{{instru.serial}}{% else %}-{% endif %} <br>
@ -55,7 +55,7 @@
<tr>
<td> {{ rep.date }} </td>
<td> {% if current_language == "fr" %}
<td> {% ifequal current_language "fr" %}
{{ rep.description }}
{% else %}
{% if instru.description_en %}
@ -63,7 +63,7 @@
{% else %}
{{ rep.description }}
{% endif %}
{% endif %} </td>
{% endifequal %} </td>
<td> {{ rep.prix }} </td>
<td> {{ rep.lieux }} </td>
{%if user.profile.is_chef or user.profile.is_chef_instru %}

View file

@ -3,7 +3,7 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Ernestophone.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Ernestophone.settings.local")
from django.core.management import execute_from_command_line

View file

@ -1,80 +0,0 @@
# Generated by npins. Do not modify; will be overwritten regularly
let
data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version;
mkSource =
spec:
assert spec ? type;
let
path =
if spec.type == "Git" then
mkGitSource spec
else if spec.type == "GitRelease" then
mkGitSource spec
else if spec.type == "PyPi" then
mkPyPiSource spec
else if spec.type == "Channel" then
mkChannelSource spec
else
builtins.throw "Unknown source type ${spec.type}";
in
spec // { outPath = path; };
mkGitSource =
{
repository,
revision,
url ? null,
hash,
branch ? null,
...
}:
assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
# In the latter case, there we will always be an url to the tarball
if url != null then
(builtins.fetchTarball {
inherit url;
sha256 = hash; # FIXME: check nix version & use SRI hashes
})
else
assert repository.type == "Git";
let
urlToName =
url: rev:
let
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
short = builtins.substring 0 7 rev;
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
in
"${if matched == null then "source" else builtins.head matched}${appendShort}";
name = urlToName repository.url revision;
in
builtins.fetchGit {
url = repository.url;
rev = revision;
inherit name;
# hash = hash;
};
mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl {
inherit url;
sha256 = hash;
};
mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball {
inherit url;
sha256 = hash;
};
in
if version == 3 then
builtins.mapAttrs (_: mkSource) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"

View file

@ -1,22 +0,0 @@
{
"pins": {
"nix-pkgs": {
"type": "Git",
"repository": {
"type": "Git",
"url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs.git"
},
"branch": "main",
"revision": "cc01e1c2a6ecb1e38fde35ee54995a6a639fb057",
"url": null,
"hash": "17a9vlwrk9365ccyl7a5xspqsn9wizcpwdpvr3qdimvq4fpwhjal"
},
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.05pre723402.4989a246d7a3/nixexprs.tar.xz",
"hash": "0hjng6rhkjiql1dqbanjm6jl6npik29q2lmba032j897fhyzin91"
}
},
"version": 3
}

View file

View file

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestion', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Prop',
fields=[
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
('nom', models.CharField(max_length=100)),
('artiste', models.CharField(max_length=100, blank=True)),
('lien', models.URLField(blank=True)),
('nboui', models.IntegerField(verbose_name='oui', default=0)),
('nbnon', models.IntegerField(verbose_name='non', default=0)),
('user', models.ForeignKey(verbose_name='Proposé par', to='gestion.ErnestoUser', on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Proposition',
},
),
migrations.CreateModel(
name='Reponses',
fields=[
('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)),
('reponse', models.CharField(verbose_name='Réponse', choices=[('oui', 'Oui'), ('non', 'Non')], max_length=20, blank=True)),
('part', models.ForeignKey(to='gestion.ErnestoUser', on_delete=models.CASCADE)),
('prop', models.ForeignKey(to='propositions.Prop', on_delete=models.CASCADE)),
],
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 2.2.9 on 2020-01-04 23:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('propositions', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='prop',
name='nom',
field=models.CharField(max_length=100, verbose_name='nom du morceau'),
),
]

View file

@ -1,75 +0,0 @@
# Generated by Django 2.2.9 on 2020-01-05 13:32
from django.conf import settings
from django.db import migrations, models
def move_profile_to_user(apps, schema_editor):
Reponses = apps.get_model("propositions", "reponses")
for answer in Reponses.objects.all():
answer.user = answer.part.user
answer.save()
def move_user_to_profile(apps, schema_editor):
# One should do something similar to ``move_profile_to_user`` AND make the
# ``part`` field temporarily nullable in the operations below.
# => Grosse flemme
raise NotImplementedError("Who uses migrations backwards anyway?")
class Migration(migrations.Migration):
dependencies = [
("gestion", "0001_initial"),
("propositions", "0002_nom_verbose_name"),
]
operations = [
migrations.AlterModelOptions(
name="reponses",
options={
"verbose_name": "Réponse à une proposition",
"verbose_name_plural": "Réponses à une proposition",
},
),
migrations.RenameField(
model_name="reponses", old_name="prop", new_name="proposition",
),
migrations.AddField(
model_name="reponses",
name="user",
field=models.ForeignKey(
on_delete=models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
null=True,
),
),
migrations.RunPython(move_profile_to_user, move_user_to_profile),
migrations.AlterField(
model_name="reponses",
name="user",
field=models.ForeignKey(
on_delete=models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
null=False,
),
),
migrations.RemoveField(model_name="reponses", name="part"),
migrations.AddField(
model_name="reponses",
name="answer",
field=models.CharField(
choices=[("oui", "Oui"), ("non", "Non")],
default="non",
max_length=3,
verbose_name="Réponse",
),
preserve_default=False,
),
migrations.AlterUniqueTogether(
name="reponses", unique_together={("proposition", "user")},
),
migrations.RemoveField(model_name="reponses", name="reponse",),
migrations.RenameModel(old_name="reponses", new_name="answer"),
]

View file

@ -1,43 +0,0 @@
# Generated by Django 2.2.9 on 2020-01-05 14:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("propositions", "0003_reponse_renaming_and_cleaning"),
]
operations = [
migrations.AlterModelOptions(
name="prop",
options={
"verbose_name": "Proposition de morceau",
"verbose_name_plural": "Propositions de morceaux",
},
),
migrations.RenameField(
model_name="prop", old_name="artiste", new_name="artist",
),
migrations.RenameField(model_name="prop", old_name="lien", new_name="link"),
migrations.RenameField(model_name="prop", old_name="nom", new_name="name"),
migrations.RenameField(model_name="prop", old_name="nbnon", new_name="nb_no"),
migrations.RenameField(model_name="prop", old_name="nboui", new_name="nb_yes"),
migrations.AlterField(
model_name="prop",
name="nb_no",
field=models.IntegerField(default=0, verbose_name="nombre de réponses non"),
),
migrations.AlterField(
model_name="prop",
name="nb_yes",
field=models.IntegerField(default=0, verbose_name="nombre de réponses oui"),
),
migrations.RenameModel(old_name="prop", new_name="proposition"),
migrations.AlterField(
model_name='answer',
name='proposition',
field=models.ForeignKey(on_delete=models.deletion.CASCADE, to='propositions.Proposition'),
),
]

View file

@ -1,24 +0,0 @@
# Generated by Django 2.2.9 on 2020-01-05 15:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("propositions", "0004_prop_renaming_and_cleaning"),
]
operations = [
migrations.RemoveField(model_name="proposition", name="nb_no",),
migrations.RemoveField(model_name="proposition", name="nb_yes",),
migrations.AlterField(
model_name="answer",
name="proposition",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="propositions.Proposition",
),
),
]

View file

@ -1,53 +0,0 @@
# Generated by Django 2.2.9 on 2020-01-05 16:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def move_profile_to_user(apps, schema_editor):
Proposition = apps.get_model("propositions", "Proposition")
for proposition in Proposition.objects.all():
proposition.user = proposition.profile.user
proposition.save()
def move_user_to_profile(apps, schema_editor):
# One should do something similar to ``move_profile_to_user`` AND make the
# ``profile`` field temporarily nullable in the operations below.
# => Grosse flemme
raise NotImplementedError("Who uses migrations backwards anyway?")
class Migration(migrations.Migration):
dependencies = [
("propositions", "0005_remove_nb_yes_no_fields"),
]
operations = [
migrations.RenameField(
model_name="proposition", old_name="user", new_name="profile"
),
migrations.AddField(
model_name="proposition",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Proposé par",
null=True,
),
),
migrations.RunPython(move_profile_to_user, move_user_to_profile),
migrations.RemoveField(model_name="proposition", name="profile"),
migrations.AlterField(
model_name="proposition",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Proposé par",
),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 2.2.27 on 2022-03-22 13:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('propositions', '0006_proposition_profile_to_user'),
]
operations = [
migrations.AlterField(
model_name='answer',
name='proposition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 2.2.27 on 2022-09-14 10:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('propositions', '0007_auto_20220322_1455'),
]
operations = [
migrations.AlterField(
model_name='answer',
name='proposition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 2.2.28 on 2024-06-15 13:03
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('propositions', '0008_auto_20220914_1244'),
]
operations = [
migrations.AlterField(
model_name='answer',
name='proposition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='propositions.Proposition'),
),
]

View file

@ -1,34 +0,0 @@
from django.contrib.auth import get_user_model
from django.db import models
User = get_user_model()
class Proposition(models.Model):
name = models.CharField(max_length=100, verbose_name="nom du morceau")
artist = models.CharField(blank=True, max_length=100)
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Proposé par")
link = models.URLField(blank=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Proposition de morceau"
verbose_name_plural = "Propositions de morceaux"
class Answer(models.Model):
YES = "oui"
NO = "non"
REP_CHOICES = [(YES, "Oui"), (NO, "Non")]
proposition = models.ForeignKey(Proposition, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
answer = models.CharField("Réponse", max_length=3, choices=REP_CHOICES)
class Meta:
unique_together = ("proposition", "user")
verbose_name = "Réponse à une proposition"
verbose_name_plural = "Réponses à une proposition"

View file

@ -1,14 +0,0 @@
{% extends "gestion/base.html" %}
{% block titre %}Proposition de morceau{% endblock %}
{% block content %}
<p><a href="{% url "propositions:list" %}">Retour aux propositions</a></p>
<form action="{% url "propositions:create" %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer" />
</form>
{% endblock %}

View file

@ -1,10 +0,0 @@
{% extends "gestion/base.html" %}
{% block titre %}Suppression d'une proposition{% endblock %}
{% block content %}<form action="" method="post">
{% csrf_token %}
<p><a href="{% url "propositions:list" %}">Retour aux propositions</a></p>
<p>Voulez vous vraiment supprimer la proposition {{ object }}?</p>
<input type="submit" value="Oui" />
</form>
{% endblock %}

View file

@ -1,44 +0,0 @@
{% extends "gestion/base.html" %}
{% block titre %}Propositions de morceau{% endblock %}
{% block content %}
<h1>Liste des propositions</h1>
<p><a href="{% url "propositions:create" %}">Proposer un morceau</a></p>
{% if propositions.exists %}
<table>
<tr>
<th></th>
<th>Oui</th>
<th>Non</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
{% for p in propositions %}
<tr class="prop">
<td>
{% if p.link %}<a href={{ p.link }}>{% endif %}
<b>{{ p.name }}</b>{% if p.artist %} - {{ p.artist }}{% endif %}
{% if p.link %}</a>{% endif %}
</td>
<td>{{ p.nb_yes }}</td>
<td>{{ p.nb_no }}</td>
<td><a href="{% url "propositions:oui" p.id %}">Oui</a></td>
<td><a href="{% url "propositions:non" p.id %}">Non</a></td>
<td>{% if p.user_answer %}Vous avez voté {{ p.user_answer }}{% endif %}</td>
<td>
{% if p.user == request.user or request.user.profile.is_chef %}
<a class="supprimer" href="{% url "propositions:delete" p.id %}">Supprimer</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
Pas de proposition pour le moment
{% endif %}
{% endblock %}

View file

@ -1,165 +0,0 @@
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse_lazy, reverse
from gestion.models import ErnestoUser
from propositions.models import Answer, Proposition
User = get_user_model()
def new_user(username):
u = User.objects.create_user(username=username)
ErnestoUser.objects.create(user=u, slug=username, is_ernesto=True)
return u
class PropositionCreateTest(TestCase):
url = reverse_lazy("propositions:create")
def test_anonymous_cannot_get(self):
response = Client().get(self.url)
self.assertRedirects(response, "/login?next={}".format(self.url))
def test_ernesto_user_can_get(self):
user = new_user("toto")
client = Client()
client.force_login(user)
response = client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_ernesto_user_can_post(self):
user = new_user("toto")
client = Client()
client.force_login(user)
data = {"name": "foo", "artist": "bar", "link": "example.com"}
client.post(self.url, data)
proposition = Proposition.objects.all()
self.assertEqual(1, proposition.count())
proposition = proposition.get()
self.assertEqual(proposition.name, "foo")
self.assertEqual(proposition.artist, "bar")
self.assertEqual(proposition.link, "http://example.com")
self.assertEqual(proposition.user, user)
class PropositionDeleteTest(TestCase):
def setUp(self):
self.owner = new_user("owner")
self.random_user = new_user("toto")
self.chef = new_user("chef")
self.chef.profile.is_chef = True
self.chef.profile.save()
proposition = Proposition.objects.create(name="prop", user=self.owner)
self.url = reverse("propositions:delete", args=(proposition.id,))
def test_anonymous_cannot_get(self):
response = Client().get(self.url)
self.assertRedirects(response, "/login?next={}".format(self.url))
def test_anonymous_cannot_post(self):
response = Client().post(self.url, {})
self.assertRedirects(response, "/login?next={}".format(self.url))
self.assertTrue(Proposition.objects.exists())
def test_random_user_cannot_get(self):
client = Client()
client.force_login(self.random_user)
response = client.get(self.url)
self.assertEqual(response.status_code, 403)
def test_not_owner_cannot_post(self):
client = Client()
client.force_login(self.random_user)
response = client.post(self.url, {})
self.assertEqual(response.status_code, 403)
self.assertTrue(Proposition.objects.exists())
def test_chef_can_get(self):
client = Client()
client.force_login(self.chef)
response = client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_chef_can_post(self):
client = Client()
client.force_login(self.chef)
client.post(self.url, {})
self.assertFalse(Proposition.objects.exists())
def test_owner_can_get(self):
client = Client()
client.force_login(self.owner)
response = client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_owner_can_post(self):
client = Client()
client.force_login(self.owner)
client.post(self.url, {})
self.assertFalse(Proposition.objects.exists())
class PropositionListTest(TestCase):
url = reverse_lazy("propositions:list")
def setUp(self):
self.user = new_user("toto")
for name in ["foo", "bar", "baz"]:
p = Proposition.objects.create(name=name, user=self.user)
Answer.objects.create(proposition=p, user=self.user, answer=Answer.YES)
for name in ["oof", "rab", "zab"]:
p = Proposition.objects.create(name=name, user=self.user)
Answer.objects.create(proposition=p, user=self.user, answer=Answer.NO)
def test_anonymous_cannot_get(self):
response = Client().get(self.url)
self.assertRedirects(response, "/login?next={}".format(self.url))
def test_ernesto_user_can_get(self):
client = Client()
client.force_login(self.user)
response = client.get(self.url)
self.assertEqual(response.status_code, 200)
class ReponseTest(TestCase):
def setUp(self):
self.user = new_user("toto")
self.prop = Proposition.objects.create(name="foo", user=self.user)
def _url(self, rep):
assert rep in Answer.REP_CHOICES
return reverse("propositions:{}".format(rep), args=(self.prop.id,))
def test_anonymous_cannot_get(self):
client = Client()
url = reverse("propositions:oui", args=(self.prop.id,))
response = client.get(url)
self.assertRedirects(response, "/login?next={}".format(url))
url = reverse("propositions:non", args=(self.prop.id,))
response = client.get(url)
self.assertRedirects(response, "/login?next={}".format(url))
def test_ernesto_user_can_get(self):
client = Client()
client.force_login(self.user)
client.get(reverse("propositions:oui", args=(self.prop.id,)))
self.prop.refresh_from_db()
self.assertEqual(
list(self.prop.answer_set.values_list("answer", flat=True)), [Answer.YES],
)
client.get(reverse("propositions:non", args=(self.prop.id,)))
self.prop.refresh_from_db()
self.assertEqual(
list(self.prop.answer_set.values_list("answer", flat=True)), [Answer.NO]
)

View file

@ -1,13 +0,0 @@
from django.urls import path
from propositions import views
from propositions.models import Answer
app_name = "propositions"
urlpatterns = [
path("", views.PropositionList.as_view(), name="list"),
path("new", views.PropositionCreate.as_view(), name="create"),
path("<int:id>/oui", views.answer, {"ans": "oui"}, name=Answer.YES),
path("<int:id>/non", views.answer, {"ans": "non"}, name=Answer.NO),
path("<int:pk>/supprimer", views.PropositionDelete.as_view(), name="delete"),
]

View file

@ -1,8 +0,0 @@
import string
import random
def generer(*args):
caracteres = string.ascii_letters + string.digits
aleatoire = [random.choice(caracteres) for _ in range(6)]
return ''.join(aleatoire)

View file

@ -1,63 +0,0 @@
from django.shortcuts import redirect, get_object_or_404
from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import Count, OuterRef, Q, Subquery
from django.views.generic import CreateView, DeleteView, ListView
from django.http import HttpResponseRedirect
from propositions.models import Answer, Proposition
class PropositionCreate(LoginRequiredMixin, CreateView):
template_name = "propositions/create.html"
success_url = reverse_lazy("propositions:list")
model = Proposition
fields = ["name", "artist", "link"]
def form_valid(self, form):
proposition = form.save(commit=False)
proposition.user = self.request.user
proposition.save()
return HttpResponseRedirect(self.success_url)
class PropositionList(LoginRequiredMixin, ListView):
template_name = "propositions/liste.html"
context_object_name = "propositions"
model = Proposition
def get_queryset(self):
user = self.request.user
user_answers = (
Answer.objects
.filter(proposition=OuterRef("id"), user=user)
.values_list("answer", flat=True)
)
return (
Proposition.objects
.annotate(nb_yes=Count("answer", filter=Q(answer__answer=Answer.YES)))
.annotate(nb_no=Count("answer", filter=Q(answer__answer=Answer.NO)))
.annotate(user_answer=Subquery(user_answers[:1]))
.order_by("-nb_yes", "nb_no", "name")
)
class PropositionDelete(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Proposition
template_name = "propositions/delete.html"
success_url = reverse_lazy("propositions:list")
def test_func(self):
proposition = self.get_object()
user = self.request.user
return (proposition.user == user or user.profile.is_chef)
@login_required
def answer(request, id, ans):
proposition = get_object_or_404(Proposition, id=id)
user = request.user
Answer.objects.filter(proposition=proposition, user=user).delete()
Answer.objects.create(proposition=proposition, user=user, answer=ans)
return redirect("propositions:list")

4
requirements-devel.txt Normal file
View file

@ -0,0 +1,4 @@
-r requirements.txt
django-debug-toolbar
ipython

8
requirements.txt Normal file
View file

@ -0,0 +1,8 @@
Django==2.2.*
# Pour la prod
#psycopg2
gunicorn
django-colorful
Pillow
django-avatar==5.0.*

View file

@ -1,46 +0,0 @@
{
sources ? import ./npins,
pkgs ? import sources.nixpkgs {
overlays = [
(import "${sources.nix-pkgs}/overlay.nix").default
];
},
dev ? true,
}:
pkgs.mkShell {
packages = [
(pkgs.python3.withPackages (
ps:
[
ps.django
ps.django-avatar
ps.django-colorful
ps.gunicorn
ps.pillow
ps.loadcredential
]
++ (
if dev then
[
ps.ipython
ps.django-debug-toolbar
]
else
[ ]
)
))
];
env = {
DJANGO_SETTINGS_MODULE = "Ernestophone.settings";
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
ERNESTOPHONE_DEBUG = builtins.toJSON true;
ERNESTOPHONE_STATIC_ROOT = builtins.toString ./.static;
};
shellHook = ''
if [ ! -d .static ]; then
mkdir .static
fi
'';
}

View file

@ -12,7 +12,7 @@
<p>{% trans "Avatar courant: " %}</p>
{% avatar user %}
<form enctype="multipart/form-data" method="POST" action="{% url 'avatar:add' %}">
<form enctype="multipart/form-data" method="POST" action="{% url 'avatar_add' %}">
{{ upload_avatar_form.as_p }}
<p>{% csrf_token %}<input type="submit" value="{% trans "Ajouter une nouvelle image" %}" /></p>
</form>

View file

@ -19,7 +19,7 @@
<p>{% trans "Tu n'as pas encore ajouté d'avatar. Le logo Ernestophone sera utilisé par defaut." %}</p>
{% endif %}
<form enctype="multipart/form-data" method="POST" action="{% url 'avatar:add' %}">
<form enctype="multipart/form-data" method="POST" action="{% url 'avatar_add' %}">
{{ upload_avatar_form.as_p }}
<p>{% csrf_token %}<input type="submit" value="{% trans "Modifier mon avatar" %}" /></p>
</form>
@ -27,7 +27,7 @@
<a href="{% url "trombonoscope:change" %}" class="button alt">{% trans "Retour au profil"%}</a>
{% if avatars %}
&nbsp;<a class="button alt" href="{% url 'avatar:delete' %}">{% trans "Supprimer l'avatar "%}</a>
&nbsp;<a class="button alt" href="{% url 'avatar_delete' %}">{% trans "Supprimer l'avatar "%}</a>
{% endif %}
</div></div></div></section></div>
{% endblock %}

View file

@ -26,9 +26,9 @@
{% endif %}
<p>
<a class="button alt" href="{% url 'avatar:change' %}">{% trans "Changer l'avatar "%}</a>
<a class="button alt" href="{% url 'avatar_change' %}">{% trans "Changer l'avatar "%}</a>
{% if request.user|has_avatar %}
&nbsp;<a class="button alt" href="{% url 'avatar:delete' %}">{% trans "Supprimer l'avatar "%}</a>
&nbsp;<a class="button alt" href="{% url 'avatar_delete' %}">{% trans "Supprimer l'avatar "%}</a>
{% endif %}
</p>

View file

@ -10,12 +10,12 @@
<div class="6u 12u$(small)">
<h2>{% trans "Suppression de l'avatar" %} :</h2>
{% if not avatars %}
{% url 'avatar:change' as avatar_change_url %}
{% url 'avatar_change' as avatar_change_url %}
<p>{% trans "Vous n'avez pas d'avatar à supprimer."%}</p>
<a href="{{ avatar_change_url }}" class="button">{% trans "Télécharger un avatar" %}</a>&nbsp;<a href="{% url "trombonoscope:change" %}" class="button alt">{% trans "Retour au profil"%}</a>
{% else %}
<p>{% trans "Séléctione l'avatar pour le supprimer" %}</p>
<form method="POST" action="{% url 'avatar:delete' %}">
<form method="POST" action="{% url 'avatar_delete' %}">
{% for field in delete_avatar_form %}