forked from DGNum/gestioCOF
Merge branch 'aureplop/site-cof' into 'evarin/site-cof'
evarin/site-cof: style -- black + isort See merge request klub-dev-ens/gestioCOF!336
This commit is contained in:
commit
a6bf1fc16a
47 changed files with 2930 additions and 572 deletions
|
@ -27,10 +27,10 @@ test:
|
||||||
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
|
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
|
||||||
# Remove the old test database if it has not been done yet
|
# Remove the old test database if it has not been done yet
|
||||||
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
|
||||||
- pip install --upgrade -r requirements.txt coverage
|
- pip install --upgrade -r requirements.txt coverage tblib
|
||||||
- python --version
|
- python --version
|
||||||
script:
|
script:
|
||||||
- coverage run manage.py test
|
- coverage run manage.py test --parallel
|
||||||
after_script:
|
after_script:
|
||||||
- coverage report
|
- coverage report
|
||||||
services:
|
services:
|
||||||
|
@ -52,9 +52,9 @@ linters:
|
||||||
- pip install --upgrade black isort flake8
|
- pip install --upgrade black isort flake8
|
||||||
script:
|
script:
|
||||||
- black --check .
|
- black --check .
|
||||||
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
|
- isort --recursive --check-only --diff bda cof gestioncof kfet petitscours provisioning shared utils
|
||||||
# Print errors only
|
# Print errors only
|
||||||
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
|
- flake8 --exit-zero bda cof gestioncof kfet petitscours provisioning shared utils
|
||||||
cache:
|
cache:
|
||||||
key: linters
|
key: linters
|
||||||
paths:
|
paths:
|
||||||
|
|
|
@ -60,24 +60,20 @@ class BdATestHelpers:
|
||||||
def check_restricted_access(
|
def check_restricted_access(
|
||||||
self, url, validate_user=user_is_cof, redirect_url=None
|
self, url, validate_user=user_is_cof, redirect_url=None
|
||||||
):
|
):
|
||||||
def craft_redirect_url(user):
|
for (user, client) in self.client_matrix:
|
||||||
if redirect_url:
|
resp = client.get(url, follow=True)
|
||||||
return redirect_url
|
if validate_user(user):
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
elif redirect_url:
|
||||||
|
self.assertRedirects(resp, redirect_url)
|
||||||
elif user is None:
|
elif user is None:
|
||||||
# client is not logged in
|
# client is not logged in
|
||||||
login_url = "/login"
|
login_url = "/login"
|
||||||
if url:
|
if url:
|
||||||
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
|
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
|
||||||
return login_url
|
self.assertRedirects(resp, login_url)
|
||||||
else:
|
else:
|
||||||
return "/"
|
self.assertEqual(403, resp.status_code)
|
||||||
|
|
||||||
for (user, client) in self.client_matrix:
|
|
||||||
resp = client.get(url, follow=True)
|
|
||||||
if validate_user(user):
|
|
||||||
self.assertEqual(200, resp.status_code)
|
|
||||||
else:
|
|
||||||
self.assertRedirects(resp, craft_redirect_url(user))
|
|
||||||
|
|
||||||
|
|
||||||
class TestBdAViews(BdATestHelpers, TestCase):
|
class TestBdAViews(BdATestHelpers, TestCase):
|
||||||
|
|
|
@ -6,6 +6,6 @@ English formatting.
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
DATETIME_FORMAT = r'l N j, Y \a\t P'
|
DATETIME_FORMAT = r"l N j, Y \a\t P"
|
||||||
DATE_FORMAT = r'l N j, Y'
|
DATE_FORMAT = r"l N j, Y"
|
||||||
TIME_FORMAT = r'P'
|
TIME_FORMAT = r"P"
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
Formats français.
|
Formats français.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DATETIME_FORMAT = r'l j F Y \à H\hi'
|
DATETIME_FORMAT = r"l j F Y \à H\hi"
|
||||||
DATE_FORMAT = r'l j F Y'
|
DATE_FORMAT = r"l j F Y"
|
||||||
TIME_FORMAT = r'H\hi'
|
TIME_FORMAT = r"H\hi"
|
||||||
|
|
|
@ -57,64 +57,65 @@ INSTALLED_APPS = [
|
||||||
"gestioncof",
|
"gestioncof",
|
||||||
# Must be before 'django.contrib.admin'.
|
# Must be before 'django.contrib.admin'.
|
||||||
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
|
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
|
||||||
'dal',
|
"dal",
|
||||||
'dal_select2',
|
"dal_select2",
|
||||||
'django.contrib.auth',
|
"django.contrib.auth",
|
||||||
'django.contrib.contenttypes',
|
"django.contrib.contenttypes",
|
||||||
'django.contrib.sessions',
|
"django.contrib.sessions",
|
||||||
'django.contrib.sites',
|
"django.contrib.sites",
|
||||||
'django.contrib.messages',
|
"django.contrib.messages",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.staticfiles",
|
||||||
'django.contrib.admin',
|
"django.contrib.admin",
|
||||||
'django.contrib.admindocs',
|
"django.contrib.admindocs",
|
||||||
'bda',
|
"bda",
|
||||||
'captcha',
|
"petitscours",
|
||||||
'django_cas_ng',
|
"captcha",
|
||||||
'bootstrapform',
|
"django_cas_ng",
|
||||||
'kfet',
|
"bootstrapform",
|
||||||
'kfet.open',
|
"kfet",
|
||||||
'channels',
|
"kfet.open",
|
||||||
'widget_tweaks',
|
"channels",
|
||||||
'custommail',
|
"widget_tweaks",
|
||||||
'djconfig',
|
"custommail",
|
||||||
'wagtail.wagtailforms',
|
"djconfig",
|
||||||
'wagtail.wagtailredirects',
|
"wagtail.wagtailforms",
|
||||||
'wagtail.wagtailembeds',
|
"wagtail.wagtailredirects",
|
||||||
'wagtail.wagtailsites',
|
"wagtail.wagtailembeds",
|
||||||
'wagtail.wagtailusers',
|
"wagtail.wagtailsites",
|
||||||
'wagtail.wagtailsnippets',
|
"wagtail.wagtailusers",
|
||||||
'wagtail.wagtaildocs',
|
"wagtail.wagtailsnippets",
|
||||||
'wagtail.wagtailimages',
|
"wagtail.wagtaildocs",
|
||||||
'wagtail.wagtailsearch',
|
"wagtail.wagtailimages",
|
||||||
'wagtail.wagtailadmin',
|
"wagtail.wagtailsearch",
|
||||||
'wagtail.wagtailcore',
|
"wagtail.wagtailadmin",
|
||||||
'wagtail.contrib.modeladmin',
|
"wagtail.wagtailcore",
|
||||||
'wagtail.contrib.wagtailroutablepage',
|
"wagtail.contrib.modeladmin",
|
||||||
'wagtailmenus',
|
"wagtail.contrib.wagtailroutablepage",
|
||||||
'wagtail_modeltranslation',
|
"wagtailmenus",
|
||||||
'modelcluster',
|
"wagtail_modeltranslation",
|
||||||
'taggit',
|
"modelcluster",
|
||||||
'kfet.auth',
|
"taggit",
|
||||||
'kfet.cms',
|
"kfet.auth",
|
||||||
'gestioncof.cms',
|
"kfet.cms",
|
||||||
|
"gestioncof.cms",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
'django.middleware.common.CommonMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
"django.contrib.auth.middleware.SessionAuthenticationMiddleware",
|
||||||
'kfet.auth.middleware.TemporaryAuthMiddleware',
|
"kfet.auth.middleware.TemporaryAuthMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
'django.middleware.security.SecurityMiddleware',
|
"django.middleware.security.SecurityMiddleware",
|
||||||
'djconfig.middleware.DjConfigMiddleware',
|
"djconfig.middleware.DjConfigMiddleware",
|
||||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
"wagtail.wagtailcore.middleware.SiteMiddleware",
|
||||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
"wagtail.wagtailredirects.middleware.RedirectMiddleware",
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "cof.urls"
|
ROOT_URLCONF = "cof.urls"
|
||||||
|
@ -167,10 +168,7 @@ USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
LANGUAGES = (
|
LANGUAGES = (("fr", "Français"), ("en", "English"))
|
||||||
('fr', 'Français'),
|
|
||||||
('en', 'English'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Various additional settings
|
# Various additional settings
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
|
@ -21,7 +21,6 @@ from gestioncof.urls import (
|
||||||
clubs_patterns,
|
clubs_patterns,
|
||||||
events_patterns,
|
events_patterns,
|
||||||
export_patterns,
|
export_patterns,
|
||||||
petitcours_patterns,
|
|
||||||
surveys_patterns,
|
surveys_patterns,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +34,7 @@ urlpatterns = [
|
||||||
# Les exports
|
# Les exports
|
||||||
url(r"^export/", include(export_patterns)),
|
url(r"^export/", include(export_patterns)),
|
||||||
# Les petits cours
|
# Les petits cours
|
||||||
url(r"^petitcours/", include(petitcours_patterns)),
|
url(r"^petitcours/", include("petitscours.urls")),
|
||||||
# Les sondages
|
# Les sondages
|
||||||
url(r"^survey/", include(surveys_patterns)),
|
url(r"^survey/", include(surveys_patterns)),
|
||||||
# Evenements
|
# Evenements
|
||||||
|
@ -134,6 +133,5 @@ if settings.DEBUG:
|
||||||
|
|
||||||
# Wagtail for uncatched
|
# Wagtail for uncatched
|
||||||
urlpatterns += i18n_patterns(
|
urlpatterns += i18n_patterns(
|
||||||
url(r'', include(wagtail_urls)),
|
url(r"", include(wagtail_urls)), prefix_default_language=False
|
||||||
prefix_default_language=False
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,7 @@ from gestioncof.models import (
|
||||||
SurveyQuestion,
|
SurveyQuestion,
|
||||||
SurveyQuestionAnswer,
|
SurveyQuestionAnswer,
|
||||||
)
|
)
|
||||||
from gestioncof.petits_cours_models import (
|
from petitscours.models import (
|
||||||
PetitCoursAbility,
|
PetitCoursAbility,
|
||||||
PetitCoursAttribution,
|
PetitCoursAttribution,
|
||||||
PetitCoursAttributionCounter,
|
PetitCoursAttributionCounter,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
default_app_config = 'gestioncof.cms.apps.COFCMSAppConfig'
|
default_app_config = "gestioncof.cms.apps.COFCMSAppConfig"
|
||||||
|
|
|
@ -2,6 +2,6 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class COFCMSAppConfig(AppConfig):
|
class COFCMSAppConfig(AppConfig):
|
||||||
name = 'gestioncof.cms'
|
name = "gestioncof.cms"
|
||||||
label = 'cofcms'
|
label = "cofcms"
|
||||||
verbose_name = 'CMS COF'
|
verbose_name = "CMS COF"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,55 +2,159 @@
|
||||||
# Generated by Django 1.11.9 on 2018-04-28 13:46
|
# Generated by Django 1.11.9 on 2018-04-28 13:46
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import wagtail.contrib.wagtailroutablepage.models
|
import wagtail.contrib.wagtailroutablepage.models
|
||||||
import wagtail.wagtailcore.blocks
|
import wagtail.wagtailcore.blocks
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.wagtailcore.fields
|
||||||
import wagtail.wagtailimages.blocks
|
import wagtail.wagtailimages.blocks
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('wagtailcore', '0039_collectionviewrestriction'),
|
("wagtailcore", "0039_collectionviewrestriction"),
|
||||||
('cofcms', '0001_initial'),
|
("cofcms", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='COFUtilPage',
|
name="COFUtilPage",
|
||||||
fields=[
|
fields=[
|
||||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
(
|
||||||
|
"page_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="wagtailcore.Page",
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Page utilitaire',
|
"verbose_name": "Page utilitaire",
|
||||||
'verbose_name_plural': 'Pages utilitaires',
|
"verbose_name_plural": "Pages utilitaires",
|
||||||
},
|
},
|
||||||
bases=(wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin, 'wagtailcore.page'),
|
bases=(
|
||||||
|
wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin,
|
||||||
|
"wagtailcore.page",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='cofdirectoryentrypage',
|
name="cofdirectoryentrypage",
|
||||||
options={'verbose_name': "Entrée d'annuaire", 'verbose_name_plural': "Entrées d'annuaire"},
|
options={
|
||||||
|
"verbose_name": "Entrée d'annuaire",
|
||||||
|
"verbose_name_plural": "Entrées d'annuaire",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='cofdirectorypage',
|
model_name="cofdirectorypage",
|
||||||
name='alphabetique',
|
name="alphabetique",
|
||||||
field=models.BooleanField(default=True, verbose_name='Tri par ordre alphabétique ?'),
|
field=models.BooleanField(
|
||||||
|
default=True, verbose_name="Tri par ordre alphabétique ?"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='cofpage',
|
model_name="cofpage",
|
||||||
name='body',
|
name="body",
|
||||||
field=wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('iframe', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock('Adresse de la page')), ('height', wagtail.wagtailcore.blocks.CharBlock('Hauteur (en pixels)'))))))),
|
field=wagtail.wagtailcore.fields.StreamField(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"heading",
|
||||||
|
wagtail.wagtailcore.blocks.CharBlock(classname="full title"),
|
||||||
|
),
|
||||||
|
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
|
||||||
|
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
|
||||||
|
(
|
||||||
|
"iframe",
|
||||||
|
wagtail.wagtailcore.blocks.StructBlock(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"url",
|
||||||
|
wagtail.wagtailcore.blocks.URLBlock(
|
||||||
|
"Adresse de la page"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"height",
|
||||||
|
wagtail.wagtailcore.blocks.CharBlock(
|
||||||
|
"Hauteur (en pixels)"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='cofpage',
|
model_name="cofpage",
|
||||||
name='body_en',
|
name="body_en",
|
||||||
field=wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('iframe', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock('Adresse de la page')), ('height', wagtail.wagtailcore.blocks.CharBlock('Hauteur (en pixels)')))))), null=True),
|
field=wagtail.wagtailcore.fields.StreamField(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"heading",
|
||||||
|
wagtail.wagtailcore.blocks.CharBlock(classname="full title"),
|
||||||
|
),
|
||||||
|
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
|
||||||
|
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
|
||||||
|
(
|
||||||
|
"iframe",
|
||||||
|
wagtail.wagtailcore.blocks.StructBlock(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"url",
|
||||||
|
wagtail.wagtailcore.blocks.URLBlock(
|
||||||
|
"Adresse de la page"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"height",
|
||||||
|
wagtail.wagtailcore.blocks.CharBlock(
|
||||||
|
"Hauteur (en pixels)"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='cofpage',
|
model_name="cofpage",
|
||||||
name='body_fr',
|
name="body_fr",
|
||||||
field=wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock()), ('iframe', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock('Adresse de la page')), ('height', wagtail.wagtailcore.blocks.CharBlock('Hauteur (en pixels)')))))), null=True),
|
field=wagtail.wagtailcore.fields.StreamField(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"heading",
|
||||||
|
wagtail.wagtailcore.blocks.CharBlock(classname="full title"),
|
||||||
|
),
|
||||||
|
("paragraph", wagtail.wagtailcore.blocks.RichTextBlock()),
|
||||||
|
("image", wagtail.wagtailimages.blocks.ImageChooserBlock()),
|
||||||
|
(
|
||||||
|
"iframe",
|
||||||
|
wagtail.wagtailcore.blocks.StructBlock(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"url",
|
||||||
|
wagtail.wagtailcore.blocks.URLBlock(
|
||||||
|
"Adresse de la page"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"height",
|
||||||
|
wagtail.wagtailcore.blocks.CharBlock(
|
||||||
|
"Hauteur (en pixels)"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,7 +13,7 @@ from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
|
||||||
class COFActuIndexMixin:
|
class COFActuIndexMixin:
|
||||||
@property
|
@property
|
||||||
def actus(self):
|
def actus(self):
|
||||||
actus = COFActuPage.objects.live().order_by('-date_start').descendant_of(self)
|
actus = COFActuPage.objects.live().order_by("-date_start").descendant_of(self)
|
||||||
return actus
|
return actus
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@ class COFRootPage(Page, COFActuIndexMixin):
|
||||||
introduction = RichTextField("Introduction")
|
introduction = RichTextField("Introduction")
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel('introduction', classname="full"),
|
FieldPanel("introduction", classname="full")
|
||||||
]
|
]
|
||||||
|
|
||||||
subpage_types = ['COFActuIndexPage', 'COFPage', 'COFDirectoryPage', 'COFUtilPage']
|
subpage_types = ["COFActuIndexPage", "COFPage", "COFDirectoryPage", "COFUtilPage"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Racine site du COF"
|
verbose_name = "Racine site du COF"
|
||||||
|
@ -45,19 +45,19 @@ class IFrameBlock(blocks.StructBlock):
|
||||||
|
|
||||||
# Page lambda du site
|
# Page lambda du site
|
||||||
class COFPage(Page):
|
class COFPage(Page):
|
||||||
body = StreamField([
|
body = StreamField(
|
||||||
('heading', blocks.CharBlock(classname="full title")),
|
[
|
||||||
('paragraph', blocks.RichTextBlock()),
|
("heading", blocks.CharBlock(classname="full title")),
|
||||||
('image', ImageChooserBlock()),
|
("paragraph", blocks.RichTextBlock()),
|
||||||
('iframe', IFrameBlock()),
|
("image", ImageChooserBlock()),
|
||||||
])
|
("iframe", IFrameBlock()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [StreamFieldPanel("body")]
|
||||||
StreamFieldPanel('body'),
|
|
||||||
]
|
|
||||||
|
|
||||||
subpage_types = ['COFDirectoryPage', 'COFPage']
|
subpage_types = ["COFDirectoryPage", "COFPage"]
|
||||||
parent_page_types = ['COFPage', 'COFRootPage']
|
parent_page_types = ["COFPage", "COFRootPage"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Page normale COF"
|
verbose_name = "Page normale COF"
|
||||||
|
@ -66,8 +66,8 @@ class COFPage(Page):
|
||||||
|
|
||||||
# Actualités
|
# Actualités
|
||||||
class COFActuIndexPage(Page, COFActuIndexMixin):
|
class COFActuIndexPage(Page, COFActuIndexMixin):
|
||||||
subpage_types = ['COFActuPage']
|
subpage_types = ["COFActuPage"]
|
||||||
parent_page_types = ['COFRootPage']
|
parent_page_types = ["COFRootPage"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Index des actualités"
|
verbose_name = "Index des actualités"
|
||||||
|
@ -75,9 +75,9 @@ class COFActuIndexPage(Page, COFActuIndexMixin):
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
actus = COFActuPage.objects.live().descendant_of(self).order_by('-date_end')
|
actus = COFActuPage.objects.live().descendant_of(self).order_by("-date_end")
|
||||||
|
|
||||||
page = request.GET.get('page')
|
page = request.GET.get("page")
|
||||||
paginator = Paginator(actus, 5)
|
paginator = Paginator(actus, 5)
|
||||||
try:
|
try:
|
||||||
actus = paginator.page(page)
|
actus = paginator.page(page)
|
||||||
|
@ -86,7 +86,7 @@ class COFActuIndexPage(Page, COFActuIndexMixin):
|
||||||
except EmptyPage:
|
except EmptyPage:
|
||||||
actus = paginator.page(paginator.num_pages)
|
actus = paginator.page(paginator.num_pages)
|
||||||
|
|
||||||
context['actus'] = actus
|
context["actus"] = actus
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,20 +94,24 @@ class COFActuPage(RoutablePageMixin, Page):
|
||||||
chapo = models.TextField("Description rapide", blank=True)
|
chapo = models.TextField("Description rapide", blank=True)
|
||||||
body = RichTextField("Contenu")
|
body = RichTextField("Contenu")
|
||||||
image = models.ForeignKey(
|
image = models.ForeignKey(
|
||||||
'wagtailimages.Image', verbose_name="Image à la Une",
|
"wagtailimages.Image",
|
||||||
null=True, blank=True,
|
verbose_name="Image à la Une",
|
||||||
on_delete=models.SET_NULL, related_name='+'
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
)
|
)
|
||||||
is_event = models.BooleanField("Évènement", default=True, blank=True)
|
is_event = models.BooleanField("Évènement", default=True, blank=True)
|
||||||
date_start = models.DateTimeField("Date et heure de début")
|
date_start = models.DateTimeField("Date et heure de début")
|
||||||
date_end = models.DateTimeField("Date et heure de fin", blank=True,
|
date_end = models.DateTimeField(
|
||||||
default=None, null=True)
|
"Date et heure de fin", blank=True, default=None, null=True
|
||||||
|
)
|
||||||
all_day = models.BooleanField("Toute la journée", default=False, blank=True)
|
all_day = models.BooleanField("Toute la journée", default=False, blank=True)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
ImageChooserPanel('image'),
|
ImageChooserPanel("image"),
|
||||||
FieldPanel('chapo'),
|
FieldPanel("chapo"),
|
||||||
FieldPanel('body', classname="full"),
|
FieldPanel("body", classname="full"),
|
||||||
FieldPanel("is_event"),
|
FieldPanel("is_event"),
|
||||||
FieldPanel("date_start"),
|
FieldPanel("date_start"),
|
||||||
FieldPanel("date_end"),
|
FieldPanel("date_end"),
|
||||||
|
@ -115,7 +119,7 @@ class COFActuPage(RoutablePageMixin, Page):
|
||||||
]
|
]
|
||||||
|
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
parent_page_types = ['COFActuIndexPage']
|
parent_page_types = ["COFActuIndexPage"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Actualité"
|
verbose_name = "Actualité"
|
||||||
|
@ -125,21 +129,21 @@ class COFActuPage(RoutablePageMixin, Page):
|
||||||
# Annuaires (Clubs, partenaires, bonnes adresses)
|
# Annuaires (Clubs, partenaires, bonnes adresses)
|
||||||
class COFDirectoryPage(Page):
|
class COFDirectoryPage(Page):
|
||||||
introduction = RichTextField("Introduction")
|
introduction = RichTextField("Introduction")
|
||||||
alphabetique = models.BooleanField("Tri par ordre alphabétique ?",
|
alphabetique = models.BooleanField(
|
||||||
default=True, blank=True)
|
"Tri par ordre alphabétique ?", default=True, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
FieldPanel('introduction'),
|
FieldPanel("introduction"),
|
||||||
FieldPanel('alphabetique'),
|
FieldPanel("alphabetique"),
|
||||||
]
|
]
|
||||||
|
|
||||||
subpage_types = ['COFActuPage', 'COFDirectoryEntryPage']
|
subpage_types = ["COFActuPage", "COFDirectoryEntryPage"]
|
||||||
parent_page_types = ['COFRootPage', 'COFPage']
|
parent_page_types = ["COFRootPage", "COFPage"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entries(self):
|
def entries(self):
|
||||||
entries = COFDirectoryEntryPage.objects.live()\
|
entries = COFDirectoryEntryPage.objects.live().descendant_of(self)
|
||||||
.descendant_of(self)
|
|
||||||
if self.alphabetique:
|
if self.alphabetique:
|
||||||
entries = entries.order_by("title")
|
entries = entries.order_by("title")
|
||||||
return entries
|
return entries
|
||||||
|
@ -151,31 +155,46 @@ class COFDirectoryPage(Page):
|
||||||
|
|
||||||
class COFDirectoryEntryPage(Page):
|
class COFDirectoryEntryPage(Page):
|
||||||
body = RichTextField("Description")
|
body = RichTextField("Description")
|
||||||
links = StreamField([
|
links = StreamField(
|
||||||
('lien', blocks.StructBlock([
|
[
|
||||||
('url', blocks.URLBlock(required=True)),
|
(
|
||||||
('texte', blocks.CharBlock()),
|
"lien",
|
||||||
])),
|
blocks.StructBlock(
|
||||||
('contact', blocks.StructBlock([
|
[
|
||||||
('email', blocks.EmailBlock(required=True)),
|
("url", blocks.URLBlock(required=True)),
|
||||||
('texte', blocks.CharBlock()),
|
("texte", blocks.CharBlock()),
|
||||||
])),
|
]
|
||||||
])
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"contact",
|
||||||
|
blocks.StructBlock(
|
||||||
|
[
|
||||||
|
("email", blocks.EmailBlock(required=True)),
|
||||||
|
("texte", blocks.CharBlock()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
image = models.ForeignKey(
|
image = models.ForeignKey(
|
||||||
'wagtailimages.Image', verbose_name="Image",
|
"wagtailimages.Image",
|
||||||
null=True, blank=True,
|
verbose_name="Image",
|
||||||
on_delete=models.SET_NULL, related_name='+'
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
)
|
)
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
ImageChooserPanel('image'),
|
ImageChooserPanel("image"),
|
||||||
FieldPanel('body', classname="full"),
|
FieldPanel("body", classname="full"),
|
||||||
StreamFieldPanel("links"),
|
StreamFieldPanel("links"),
|
||||||
]
|
]
|
||||||
|
|
||||||
subpage_types = []
|
subpage_types = []
|
||||||
parent_page_types = ['COFDirectoryPage']
|
parent_page_types = ["COFDirectoryPage"]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Entrée d'annuaire"
|
verbose_name = "Entrée d'annuaire"
|
||||||
|
@ -186,9 +205,10 @@ class COFDirectoryEntryPage(Page):
|
||||||
class COFUtilPage(RoutablePageMixin, Page):
|
class COFUtilPage(RoutablePageMixin, Page):
|
||||||
|
|
||||||
# Mini calendrier
|
# Mini calendrier
|
||||||
@route(r'^calendar/(\d+)/(\d+)/$')
|
@route(r"^calendar/(\d+)/(\d+)/$")
|
||||||
def calendar(self, request, year, month):
|
def calendar(self, request, year, month):
|
||||||
from .views import raw_calendar_view
|
from .views import raw_calendar_view
|
||||||
|
|
||||||
return raw_calendar_view(request, int(year), int(month))
|
return raw_calendar_view(request, int(year), int(month))
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -203,6 +223,7 @@ class COFUtilPage(RoutablePageMixin, Page):
|
||||||
TODO : vérifier si ces problèmes ont été résolus dans les màj de wagtail
|
TODO : vérifier si ces problèmes ont été résolus dans les màj de wagtail
|
||||||
et modeltranslation
|
et modeltranslation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def debugged_get_url(self, request):
|
def debugged_get_url(self, request):
|
||||||
parent = COFRootPage.objects.parent_of(self).live().first()
|
parent = COFRootPage.objects.parent_of(self).live().first()
|
||||||
burl = parent.relative_url(request.site)
|
burl = parent.relative_url(request.site)
|
||||||
|
|
|
@ -11,7 +11,7 @@ register = template.Library()
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def obfuscate_mail(value):
|
def obfuscate_mail(value):
|
||||||
val = value.replace('', '-').replace('@', 'arbre').replace('.', 'pont')[1:]
|
val = value.replace("", "-").replace("@", "arbre").replace(".", "pont")[1:]
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,11 +27,11 @@ def calendar(context, month=None, year=None):
|
||||||
prev_month = month_start - timedelta(days=2)
|
prev_month = month_start - timedelta(days=2)
|
||||||
month_prestart = month_start - timedelta(days=month_start.weekday())
|
month_prestart = month_start - timedelta(days=month_start.weekday())
|
||||||
month_postend = next_month + timedelta(days=(next_month.weekday() + 6) % 7)
|
month_postend = next_month + timedelta(days=(next_month.weekday() + 6) % 7)
|
||||||
events = COFActuPage.objects.live()\
|
events = (
|
||||||
.filter(date_start__range=[month_prestart,
|
COFActuPage.objects.live()
|
||||||
month_postend],
|
.filter(date_start__range=[month_prestart, month_postend], is_event=True)
|
||||||
is_event=True)\
|
.order_by("-date_start")
|
||||||
.order_by('-date_start')
|
)
|
||||||
events = list(events)
|
events = list(events)
|
||||||
weeks = []
|
weeks = []
|
||||||
curday = month_prestart
|
curday = month_prestart
|
||||||
|
@ -48,14 +48,23 @@ def calendar(context, month=None, year=None):
|
||||||
del events[k]
|
del events[k]
|
||||||
else:
|
else:
|
||||||
curevents.append(e)
|
curevents.append(e)
|
||||||
day = {'day': curday.day,
|
day = {
|
||||||
'date': curday,
|
"day": curday.day,
|
||||||
'class': (('today ' if curday == now.date() else '')
|
"date": curday,
|
||||||
+ ('in ' if (curday.month == month_start.month
|
"class": (
|
||||||
and curday.year == month_start.year)
|
("today " if curday == now.date() else "")
|
||||||
else 'out ')
|
+ (
|
||||||
+ ('hasevent' if len(curevents) > 0 else '')),
|
"in "
|
||||||
'events': curevents}
|
if (
|
||||||
|
curday.month == month_start.month
|
||||||
|
and curday.year == month_start.year
|
||||||
|
)
|
||||||
|
else "out "
|
||||||
|
)
|
||||||
|
+ ("hasevent" if len(curevents) > 0 else "")
|
||||||
|
),
|
||||||
|
"events": curevents,
|
||||||
|
}
|
||||||
week.append(day)
|
week.append(day)
|
||||||
curday += deltaday
|
curday += deltaday
|
||||||
weeks.append(week)
|
weeks.append(week)
|
||||||
|
@ -65,16 +74,23 @@ def calendar(context, month=None, year=None):
|
||||||
utilpage = COFUtilPage.objects.live()[0]
|
utilpage = COFUtilPage.objects.live()[0]
|
||||||
except COFUtilPage.DoesNotExist:
|
except COFUtilPage.DoesNotExist:
|
||||||
utilpage = None
|
utilpage = None
|
||||||
request = context['request']
|
request = context["request"]
|
||||||
burl = utilpage.debugged_get_url(request) + "/"
|
burl = utilpage.debugged_get_url(request) + "/"
|
||||||
prev_url = burl + utilpage.reverse_subpage("calendar",
|
prev_url = burl + utilpage.reverse_subpage(
|
||||||
args=[str(prev_month.year),
|
"calendar", args=[str(prev_month.year), str(prev_month.month)]
|
||||||
str(prev_month.month)])
|
)
|
||||||
next_url = burl + utilpage.reverse_subpage("calendar",
|
next_url = burl + utilpage.reverse_subpage(
|
||||||
args=[str(next_month.year),
|
"calendar", args=[str(next_month.year), str(next_month.month)]
|
||||||
str(next_month.month)])
|
)
|
||||||
context.push({"events": events, "weeks": weeks, "this_month": month_start,
|
context.push(
|
||||||
"prev_month": prev_url, "next_month": next_url})
|
{
|
||||||
|
"events": events,
|
||||||
|
"weeks": weeks,
|
||||||
|
"this_month": month_start,
|
||||||
|
"prev_month": prev_url,
|
||||||
|
"next_month": next_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,9 +103,13 @@ def mini_calendar(event):
|
||||||
week_start = date_start - timedelta(days=date_start.weekday())
|
week_start = date_start - timedelta(days=date_start.weekday())
|
||||||
curday = week_start
|
curday = week_start
|
||||||
for i in range(7):
|
for i in range(7):
|
||||||
days.append({'day': curday.day,
|
days.append(
|
||||||
'hasevent': curday >= date_start and curday <= date_end,
|
{
|
||||||
'today': curday == today})
|
"day": curday.day,
|
||||||
|
"hasevent": curday >= date_start and curday <= date_end,
|
||||||
|
"today": curday == today,
|
||||||
|
}
|
||||||
|
)
|
||||||
curday += timedelta(days=1)
|
curday += timedelta(days=1)
|
||||||
return {"days": days}
|
return {"days": days}
|
||||||
|
|
||||||
|
@ -98,13 +118,13 @@ def mini_calendar(event):
|
||||||
def dates(event):
|
def dates(event):
|
||||||
def factorize_suffix(a, b):
|
def factorize_suffix(a, b):
|
||||||
i = -1
|
i = -1
|
||||||
imin = - min(len(a), len(b))
|
imin = -min(len(a), len(b))
|
||||||
while i > imin and a[i] == b[i]:
|
while i > imin and a[i] == b[i]:
|
||||||
i -= 1
|
i -= 1
|
||||||
if i == -1:
|
if i == -1:
|
||||||
return (a, b, '')
|
return (a, b, "")
|
||||||
else:
|
else:
|
||||||
return (a[:i + 1], b[:i + 1], a[i + 1:])
|
return (a[: i + 1], b[: i + 1], a[i + 1 :])
|
||||||
|
|
||||||
datestart_string = formats.date_format(event.date_start)
|
datestart_string = formats.date_format(event.date_start)
|
||||||
timestart_string = formats.time_format(event.date_start)
|
timestart_string = formats.time_format(event.date_start)
|
||||||
|
@ -113,22 +133,27 @@ def dates(event):
|
||||||
if event.all_day:
|
if event.all_day:
|
||||||
return _("le %s") % datestart_string
|
return _("le %s") % datestart_string
|
||||||
else:
|
else:
|
||||||
return _("le %s de %s à %s") % \
|
return _("le %s de %s à %s") % (
|
||||||
(datestart_string,
|
datestart_string,
|
||||||
timestart_string,
|
timestart_string,
|
||||||
formats.time_format(event.date_end))
|
formats.time_format(event.date_end),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
dateend_string = formats.date_format(event.date_end)
|
dateend_string = formats.date_format(event.date_end)
|
||||||
diffstart, diffend, common = factorize_suffix(datestart_string,
|
diffstart, diffend, common = factorize_suffix(
|
||||||
dateend_string)
|
datestart_string, dateend_string
|
||||||
|
)
|
||||||
if event.all_day:
|
if event.all_day:
|
||||||
return _("du %s au %s%s") % \
|
return _("du %s au %s%s") % (diffstart, diffend, common)
|
||||||
(diffstart, diffend, common)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return _("du %s%s à %s au %s à %s") % \
|
return _("du %s%s à %s au %s à %s") % (
|
||||||
(diffstart, common, timestart_string,
|
diffstart,
|
||||||
diffend, formats.time_format(event.date_end))
|
common,
|
||||||
|
timestart_string,
|
||||||
|
diffend,
|
||||||
|
formats.time_format(event.date_end),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if event.all_day:
|
if event.all_day:
|
||||||
return _("le %s") % datestart_string
|
return _("le %s") % datestart_string
|
||||||
|
|
|
@ -13,42 +13,29 @@ from .models import (
|
||||||
|
|
||||||
@register(COFRootPage)
|
@register(COFRootPage)
|
||||||
class COFRootPageTr(WagtailTranslationOptions):
|
class COFRootPageTr(WagtailTranslationOptions):
|
||||||
fields = (
|
fields = ("introduction",)
|
||||||
'introduction',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(COFPage)
|
@register(COFPage)
|
||||||
class COFPageTr(WagtailTranslationOptions):
|
class COFPageTr(WagtailTranslationOptions):
|
||||||
fields = (
|
fields = ("body",)
|
||||||
'body',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(COFActuIndexPage)
|
@register(COFActuIndexPage)
|
||||||
class COFActuIndexPageTr(WagtailTranslationOptions):
|
class COFActuIndexPageTr(WagtailTranslationOptions):
|
||||||
fields = (
|
fields = ()
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(COFActuPage)
|
@register(COFActuPage)
|
||||||
class COFActuPageTr(WagtailTranslationOptions):
|
class COFActuPageTr(WagtailTranslationOptions):
|
||||||
fields = (
|
fields = ("chapo", "body")
|
||||||
'chapo',
|
|
||||||
'body',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(COFDirectoryPage)
|
@register(COFDirectoryPage)
|
||||||
class COFDirectoryPageTr(WagtailTranslationOptions):
|
class COFDirectoryPageTr(WagtailTranslationOptions):
|
||||||
fields = (
|
fields = ("introduction",)
|
||||||
'introduction',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register(COFDirectoryEntryPage)
|
@register(COFDirectoryEntryPage)
|
||||||
class COFDirectoryEntryPageTr(WagtailTranslationOptions):
|
class COFDirectoryEntryPageTr(WagtailTranslationOptions):
|
||||||
fields = (
|
fields = ("body", "links")
|
||||||
'body',
|
|
||||||
'links',
|
|
||||||
)
|
|
||||||
|
|
|
@ -2,5 +2,4 @@ from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
def raw_calendar_view(request, year, month):
|
def raw_calendar_view(request, year, month):
|
||||||
return render(request, "cofcms/calendar_raw.html",
|
return render(request, "cofcms/calendar_raw.html", {"month": month, "year": year})
|
||||||
{"month": month, "year": year})
|
|
||||||
|
|
|
@ -1,23 +1,55 @@
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
def is_cof(user):
|
def cof_required(view_func):
|
||||||
try:
|
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du COF.
|
||||||
profile = user.profile
|
|
||||||
return profile.is_cof
|
- Si l'utilisteur n'est pas connecté, il est redirigé vers la page de
|
||||||
except Exception:
|
connexion
|
||||||
return False
|
- Si l'utilisateur est connecté mais pas membre du COF, il obtient une
|
||||||
|
page d'erreur lui demandant de s'inscrire au COF
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_cof(user):
|
||||||
|
try:
|
||||||
|
return user.profile.is_cof
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@wraps(view_func)
|
||||||
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
|
if is_cof(request.user):
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return render(request, "cof-denied.html", status=403)
|
||||||
|
|
||||||
|
return login_required(_wrapped_view)
|
||||||
|
|
||||||
|
|
||||||
cof_required = user_passes_test(is_cof)
|
def buro_required(view_func):
|
||||||
|
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du burô.
|
||||||
|
|
||||||
|
- Si l'utilisateur n'est pas connecté, il est redirigé vers la page de
|
||||||
|
connexion
|
||||||
|
- Si l'utilisateur est connecté mais pas membre du burô, il obtient une
|
||||||
|
page d'erreur 403 Forbidden
|
||||||
|
"""
|
||||||
|
|
||||||
def is_buro(user):
|
def is_buro(user):
|
||||||
try:
|
try:
|
||||||
profile = user.profile
|
return user.profile.is_buro
|
||||||
return profile.is_buro
|
except AttributeError:
|
||||||
except Exception:
|
return False
|
||||||
return False
|
|
||||||
|
|
||||||
|
@wraps(view_func)
|
||||||
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
|
if is_buro(request.user):
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
buro_required = user_passes_test(is_buro)
|
return render(request, "buro-denied.html", status=403)
|
||||||
|
|
||||||
|
return login_required(_wrapped_view)
|
||||||
|
|
|
@ -14,7 +14,7 @@ from django.contrib.auth.models import User
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
|
||||||
from gestioncof.management.base import MyBaseCommand
|
from gestioncof.management.base import MyBaseCommand
|
||||||
from gestioncof.petits_cours_models import (
|
from petitscours.models import (
|
||||||
LEVELS_CHOICES,
|
LEVELS_CHOICES,
|
||||||
PetitCoursAbility,
|
PetitCoursAbility,
|
||||||
PetitCoursAttributionCounter,
|
PetitCoursAttributionCounter,
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.dispatch import receiver
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from bda.models import Spectacle
|
from bda.models import Spectacle
|
||||||
from gestioncof.petits_cours_models import choices_length
|
from petitscours.models import choices_length
|
||||||
|
|
||||||
TYPE_COMMENT_FIELD = (("text", _("Texte long")), ("char", _("Texte court")))
|
TYPE_COMMENT_FIELD = (("text", _("Texte long")), ("char", _("Texte court")))
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ def messages_on_out_login(request, user, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@receiver(cas_user_authenticated)
|
@receiver(cas_user_authenticated)
|
||||||
def mesagges_on_cas_login(request, user, **kwargs):
|
def messages_on_cas_login(request, user, **kwargs):
|
||||||
msg = _("Connexion à GestioCOF par CAS réussie. Bienvenue {}.").format(
|
msg = _("Connexion à GestioCOF par CAS réussie. Bienvenue {}.").format(
|
||||||
user.get_short_name()
|
user.get_short_name()
|
||||||
)
|
)
|
||||||
|
|
5
gestioncof/templates/buro-denied.html
Normal file
5
gestioncof/templates/buro-denied.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "base_title.html" %}
|
||||||
|
|
||||||
|
{% block realcontent %}
|
||||||
|
<h2>Section réservée au Burô.</h2>
|
||||||
|
{% endblock %}
|
|
@ -76,7 +76,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
||||||
"last_name": "last",
|
"last_name": "last",
|
||||||
"email": "username@mail.net",
|
"email": "username@mail.net",
|
||||||
"is_cof": "1",
|
"is_cof": "1",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
||||||
"email": "user@mail.net",
|
"email": "user@mail.net",
|
||||||
"is_cof": "1",
|
"is_cof": "1",
|
||||||
"user_exists": "1",
|
"user_exists": "1",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
|
||||||
data = dict(
|
data = dict(
|
||||||
self._minimal_data,
|
self._minimal_data,
|
||||||
**{"username": u.username, "email": "user@mail.net", "user_exists": "1"}
|
**{"username": u.username, "email": "user@mail.net", "user_exists": "1"},
|
||||||
)
|
)
|
||||||
if is_cof:
|
if is_cof:
|
||||||
data["is_cof"] = "1"
|
data["is_cof"] = "1"
|
||||||
|
@ -197,7 +197,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
||||||
"events-0-option_{}".format(o2.pk): [str(oc3.pk)],
|
"events-0-option_{}".format(o2.pk): [str(oc3.pk)],
|
||||||
"events-0-comment_{}".format(cf1.pk): "comment 1",
|
"events-0-comment_{}".format(cf1.pk): "comment 1",
|
||||||
"events-0-comment_{}".format(cf2.pk): "",
|
"events-0-comment_{}".format(cf2.pk): "",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from gestioncof import petits_cours_views, views
|
from gestioncof import views
|
||||||
from gestioncof.decorators import buro_required
|
from gestioncof.decorators import buro_required
|
||||||
from gestioncof.petits_cours_views import DemandeDetailView, DemandeListView
|
|
||||||
|
|
||||||
export_patterns = [
|
export_patterns = [
|
||||||
url(r"^members$", views.export_members, name="cof.membres_export"),
|
url(r"^members$", views.export_members, name="cof.membres_export"),
|
||||||
|
@ -21,40 +20,6 @@ export_patterns = [
|
||||||
url(r"^mega$", views.export_mega, name="cof.mega_export"),
|
url(r"^mega$", views.export_mega, name="cof.mega_export"),
|
||||||
]
|
]
|
||||||
|
|
||||||
petitcours_patterns = [
|
|
||||||
url(
|
|
||||||
r"^inscription$",
|
|
||||||
petits_cours_views.inscription,
|
|
||||||
name="petits-cours-inscription",
|
|
||||||
),
|
|
||||||
url(r"^demande$", petits_cours_views.demande, name="petits-cours-demande"),
|
|
||||||
url(
|
|
||||||
r"^demande-raw$",
|
|
||||||
petits_cours_views.demande_raw,
|
|
||||||
name="petits-cours-demande-raw",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^demandes$",
|
|
||||||
buro_required(DemandeListView.as_view()),
|
|
||||||
name="petits-cours-demandes-list",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^demandes/(?P<pk>\d+)$",
|
|
||||||
buro_required(DemandeDetailView.as_view()),
|
|
||||||
name="petits-cours-demande-details",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^demandes/(?P<demande_id>\d+)/traitement$",
|
|
||||||
petits_cours_views.traitement,
|
|
||||||
name="petits-cours-demande-traitement",
|
|
||||||
),
|
|
||||||
url(
|
|
||||||
r"^demandes/(?P<demande_id>\d+)/retraitement$",
|
|
||||||
petits_cours_views.retraitement,
|
|
||||||
name="petits-cours-demande-retraitement",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
surveys_patterns = [
|
surveys_patterns = [
|
||||||
url(
|
url(
|
||||||
r"^(?P<survey_id>\d+)/status$",
|
r"^(?P<survey_id>\d+)/status$",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import threading
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@ from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from . import OpenKfet, kfet_open
|
from . import OpenKfet
|
||||||
from .consumers import OpenKfetConsumer
|
from .consumers import OpenKfetConsumer
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,10 +17,10 @@ class OpenKfetTest(ChannelTestCase):
|
||||||
"""OpenKfet object unit-tests suite."""
|
"""OpenKfet object unit-tests suite."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.kfet_open = OpenKfet()
|
self.kfet_open = OpenKfet(
|
||||||
|
cache_prefix="test_kfetopen_%s" % threading.get_ident()
|
||||||
def tearDown(self):
|
)
|
||||||
self.kfet_open.clear_cache()
|
self.addCleanup(self.kfet_open.clear_cache)
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
"""Default values."""
|
"""Default values."""
|
||||||
|
@ -136,8 +137,14 @@ class OpenKfetViewsTest(ChannelTestCase):
|
||||||
self.c_a = Client()
|
self.c_a = Client()
|
||||||
self.c_a.login(username="admin", password="admin")
|
self.c_a.login(username="admin", password="admin")
|
||||||
|
|
||||||
def tearDown(self):
|
self.kfet_open = OpenKfet(
|
||||||
kfet_open.clear_cache()
|
cache_prefix="test_kfetopen_%s" % threading.get_ident()
|
||||||
|
)
|
||||||
|
self.addCleanup(self.kfet_open.clear_cache)
|
||||||
|
|
||||||
|
views_patcher = mock.patch("kfet.open.views.kfet_open", self.kfet_open)
|
||||||
|
views_patcher.start()
|
||||||
|
self.addCleanup(views_patcher.stop)
|
||||||
|
|
||||||
def test_door(self):
|
def test_door(self):
|
||||||
"""Edit raw_status."""
|
"""Edit raw_status."""
|
||||||
|
@ -146,14 +153,14 @@ class OpenKfetViewsTest(ChannelTestCase):
|
||||||
"/k-fet/open/raw_open", {"raw_open": sent, "token": "plop"}
|
"/k-fet/open/raw_open", {"raw_open": sent, "token": "plop"}
|
||||||
)
|
)
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assertEqual(expected, kfet_open.raw_open)
|
self.assertEqual(expected, self.kfet_open.raw_open)
|
||||||
|
|
||||||
def test_force_close(self):
|
def test_force_close(self):
|
||||||
"""Edit force_close."""
|
"""Edit force_close."""
|
||||||
for sent, expected in [(1, True), (0, False)]:
|
for sent, expected in [(1, True), (0, False)]:
|
||||||
resp = self.c_a.post("/k-fet/open/force_close", {"force_close": sent})
|
resp = self.c_a.post("/k-fet/open/force_close", {"force_close": sent})
|
||||||
self.assertEqual(200, resp.status_code)
|
self.assertEqual(200, resp.status_code)
|
||||||
self.assertEqual(expected, kfet_open.force_close)
|
self.assertEqual(expected, self.kfet_open.force_close)
|
||||||
|
|
||||||
def test_force_close_forbidden(self):
|
def test_force_close_forbidden(self):
|
||||||
"""Can't edit force_close without kfet.can_force_close permission."""
|
"""Can't edit force_close without kfet.can_force_close permission."""
|
||||||
|
@ -236,8 +243,10 @@ class OpenKfetScenarioTest(ChannelTestCase):
|
||||||
self.r_c_ws = WSClient()
|
self.r_c_ws = WSClient()
|
||||||
self.r_c_ws.force_login(self.r)
|
self.r_c_ws.force_login(self.r)
|
||||||
|
|
||||||
def tearDown(self):
|
self.kfet_open = OpenKfet(
|
||||||
kfet_open.clear_cache()
|
cache_prefix="test_kfetopen_%s" % threading.get_ident()
|
||||||
|
)
|
||||||
|
self.addCleanup(self.kfet_open.clear_cache)
|
||||||
|
|
||||||
def ws_connect(self, ws_client):
|
def ws_connect(self, ws_client):
|
||||||
ws_client.send_and_consume(
|
ws_client.send_and_consume(
|
||||||
|
@ -288,8 +297,8 @@ class OpenKfetScenarioTest(ChannelTestCase):
|
||||||
|
|
||||||
def test_scenario_2(self):
|
def test_scenario_2(self):
|
||||||
"""Starting falsely closed, clients connect, disable force close."""
|
"""Starting falsely closed, clients connect, disable force close."""
|
||||||
kfet_open.raw_open = True
|
self.kfet_open.raw_open = True
|
||||||
kfet_open.force_close = True
|
self.kfet_open.force_close = True
|
||||||
|
|
||||||
msg = self.ws_connect(self.c_ws)
|
msg = self.ws_connect(self.c_ws)
|
||||||
self.assertEqual(OpenKfet.CLOSED, msg["status"])
|
self.assertEqual(OpenKfet.CLOSED, msg["status"])
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
from decimal import Decimal
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
@ -5,9 +8,16 @@ from django.test import TestCase
|
||||||
|
|
||||||
from gestioncof.models import CofProfile
|
from gestioncof.models import CofProfile
|
||||||
|
|
||||||
from ..models import Account
|
from ..models import Account, Article, ArticleCategory, Checkout, Operation
|
||||||
from .testcases import TestCaseMixin
|
from .testcases import TestCaseMixin
|
||||||
from .utils import create_root, create_team, create_user, get_perms, user_add_perms
|
from .utils import (
|
||||||
|
create_operation_group,
|
||||||
|
create_root,
|
||||||
|
create_team,
|
||||||
|
create_user,
|
||||||
|
get_perms,
|
||||||
|
user_add_perms,
|
||||||
|
)
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -86,3 +96,80 @@ class PermHelpersTest(TestCaseMixin, TestCase):
|
||||||
map(repr, [self.perm1, self.perm2, self.perm_team]),
|
map(repr, [self.perm1, self.perm2, self.perm_team]),
|
||||||
ordered=False,
|
ordered=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OperationHelpersTest(TestCase):
|
||||||
|
def test_create_operation_group(self):
|
||||||
|
operation_group = create_operation_group()
|
||||||
|
|
||||||
|
on_acc = Account.objects.get(cofprofile__user__username="user")
|
||||||
|
checkout = Checkout.objects.get(name="Checkout")
|
||||||
|
self.assertDictEqual(
|
||||||
|
operation_group.__dict__,
|
||||||
|
{
|
||||||
|
"_checkout_cache": checkout,
|
||||||
|
"_on_acc_cache": on_acc,
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"amount": 0,
|
||||||
|
"at": mock.ANY,
|
||||||
|
"checkout_id": checkout.pk,
|
||||||
|
"comment": "",
|
||||||
|
"id": mock.ANY,
|
||||||
|
"is_cof": False,
|
||||||
|
"on_acc_id": on_acc.pk,
|
||||||
|
"valid_by_id": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertFalse(operation_group.opes.all())
|
||||||
|
|
||||||
|
def test_create_operation_group_with_content(self):
|
||||||
|
article_category = ArticleCategory.objects.create(name="Category")
|
||||||
|
article1 = Article.objects.create(
|
||||||
|
category=article_category, name="Article 1", price=Decimal("2.50")
|
||||||
|
)
|
||||||
|
article2 = Article.objects.create(
|
||||||
|
category=article_category, name="Article 2", price=Decimal("4.00")
|
||||||
|
)
|
||||||
|
operation_group = create_operation_group(
|
||||||
|
content=[
|
||||||
|
{
|
||||||
|
"type": Operation.PURCHASE,
|
||||||
|
"amount": Decimal("-3.50"),
|
||||||
|
"article": article1,
|
||||||
|
"article_nb": 2,
|
||||||
|
},
|
||||||
|
{"type": Operation.PURCHASE, "article": article2, "article_nb": 2},
|
||||||
|
{"type": Operation.PURCHASE, "article": article2},
|
||||||
|
{"type": Operation.DEPOSIT, "amount": Decimal("10.00")},
|
||||||
|
{"type": Operation.WITHDRAW, "amount": Decimal("-1.00")},
|
||||||
|
{"type": Operation.EDIT, "amount": Decimal("7.00")},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(operation_group.amount, Decimal("0.50"))
|
||||||
|
|
||||||
|
operation_list = list(operation_group.opes.all())
|
||||||
|
# Passed args: with purchase, article, article_nb, amount
|
||||||
|
self.assertEqual(operation_list[0].type, Operation.PURCHASE)
|
||||||
|
self.assertEqual(operation_list[0].article, article1)
|
||||||
|
self.assertEqual(operation_list[0].article_nb, 2)
|
||||||
|
self.assertEqual(operation_list[0].amount, Decimal("-3.50"))
|
||||||
|
# Passed args: with purchase, article, article_nb; without amount
|
||||||
|
self.assertEqual(operation_list[1].type, Operation.PURCHASE)
|
||||||
|
self.assertEqual(operation_list[1].article, article2)
|
||||||
|
self.assertEqual(operation_list[1].article_nb, 2)
|
||||||
|
self.assertEqual(operation_list[1].amount, Decimal("-8.00"))
|
||||||
|
# Passed args: with purchase, article; without article_nb, amount
|
||||||
|
self.assertEqual(operation_list[2].type, Operation.PURCHASE)
|
||||||
|
self.assertEqual(operation_list[2].article, article2)
|
||||||
|
self.assertEqual(operation_list[2].article_nb, 1)
|
||||||
|
self.assertEqual(operation_list[2].amount, Decimal("-4.00"))
|
||||||
|
# Passed args: with deposit, amount
|
||||||
|
self.assertEqual(operation_list[3].type, Operation.DEPOSIT)
|
||||||
|
self.assertEqual(operation_list[3].amount, Decimal("10.00"))
|
||||||
|
# Passed args: with withdraw, amount
|
||||||
|
self.assertEqual(operation_list[4].type, Operation.WITHDRAW)
|
||||||
|
self.assertEqual(operation_list[4].amount, Decimal("-1.00"))
|
||||||
|
# Passed args: with edit, amount
|
||||||
|
self.assertEqual(operation_list[5].type, Operation.EDIT)
|
||||||
|
self.assertEqual(operation_list[5].amount, Decimal("7.00"))
|
||||||
|
|
|
@ -28,7 +28,16 @@ from ..models import (
|
||||||
TransferGroup,
|
TransferGroup,
|
||||||
)
|
)
|
||||||
from .testcases import ViewTestCaseMixin
|
from .testcases import ViewTestCaseMixin
|
||||||
from .utils import create_team, create_user, get_perms, user_add_perms
|
from .utils import (
|
||||||
|
create_checkout,
|
||||||
|
create_checkout_statement,
|
||||||
|
create_inventory_article,
|
||||||
|
create_operation_group,
|
||||||
|
create_team,
|
||||||
|
create_user,
|
||||||
|
get_perms,
|
||||||
|
user_add_perms,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccountListViewTests(ViewTestCaseMixin, TestCase):
|
class AccountListViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
@ -2952,6 +2961,21 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
|
||||||
|
|
||||||
class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
|
class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
|
||||||
|
"""
|
||||||
|
Test cases for kpsul_cancel_operations view.
|
||||||
|
|
||||||
|
To test valid requests, one should use '_assertResponseOk(response)' to get
|
||||||
|
hints about failure reasons, if any.
|
||||||
|
|
||||||
|
At least one test per operation type should test the complete response and
|
||||||
|
behavior (HTTP, WebSocket, object updates, and object creations)
|
||||||
|
Other tests of the same operation type can only assert the specific
|
||||||
|
behavior differences.
|
||||||
|
|
||||||
|
For invalid requests, response errors should be tested.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
url_name = "kfet.kpsul.cancel_operations"
|
url_name = "kfet.kpsul.cancel_operations"
|
||||||
url_expected = "/k-fet/k-psul/cancel_operations"
|
url_expected = "/k-fet/k-psul/cancel_operations"
|
||||||
|
|
||||||
|
@ -2960,8 +2984,790 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
|
||||||
auth_user = "team"
|
auth_user = "team"
|
||||||
auth_forbidden = [None, "user"]
|
auth_forbidden = [None, "user"]
|
||||||
|
|
||||||
def test_ok(self):
|
with_liq = True
|
||||||
pass
|
|
||||||
|
def setUp(self):
|
||||||
|
super(KPsulCancelOperationsViewTests, self).setUp()
|
||||||
|
|
||||||
|
self.checkout = create_checkout(balance=Decimal("100.00"))
|
||||||
|
# An Article, price=2.5, stock=20
|
||||||
|
self.article = Article.objects.create(
|
||||||
|
category=ArticleCategory.objects.create(name="Category"),
|
||||||
|
name="Article",
|
||||||
|
price=Decimal("2.5"),
|
||||||
|
stock=20,
|
||||||
|
)
|
||||||
|
# An Account, trigramme=000, balance=50
|
||||||
|
# Do not assume user is cof, nor not cof.
|
||||||
|
self.account = self.accounts["user"]
|
||||||
|
self.account.balance = Decimal("50.00")
|
||||||
|
self.account.save()
|
||||||
|
|
||||||
|
# Mock consumer of K-Psul websocket to catch what we're sending
|
||||||
|
kpsul_consumer_patcher = mock.patch("kfet.consumers.KPsul")
|
||||||
|
self.kpsul_consumer_mock = kpsul_consumer_patcher.start()
|
||||||
|
self.addCleanup(kpsul_consumer_patcher.stop)
|
||||||
|
|
||||||
|
def _assertResponseOk(self, response):
|
||||||
|
"""
|
||||||
|
Asserts that status code of 'response' is 200, and returns the
|
||||||
|
deserialized content of the JSONResponse.
|
||||||
|
|
||||||
|
In case status code is not 200, it prints the content of "errors" of
|
||||||
|
the response.
|
||||||
|
|
||||||
|
"""
|
||||||
|
json_data = json.loads(getattr(response, "content", b"{}").decode("utf-8"))
|
||||||
|
try:
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
except AssertionError as exc:
|
||||||
|
msg = "Expected response is 200, got {}. Errors: {}".format(
|
||||||
|
response.status_code, json_data.get("errors")
|
||||||
|
)
|
||||||
|
raise AssertionError(msg) from exc
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
def test_invalid_operation_not_int(self):
|
||||||
|
data = {"operations[]": ["a"]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
json_data = json.loads(resp.content.decode("utf-8"))
|
||||||
|
self.assertEqual(json_data["errors"], {})
|
||||||
|
|
||||||
|
def test_invalid_operation_not_exist(self):
|
||||||
|
data = {"operations[]": ["1000"]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 400)
|
||||||
|
json_data = json.loads(resp.content.decode("utf-8"))
|
||||||
|
self.assertEqual(json_data["errors"], {"opes_notexisting": [1000]})
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_purchase(self, now_mock):
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{"type": Operation.PURCHASE, "article": self.article, "article_nb": 2}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(seconds=15)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
group = OperationGroup.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
group.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"at": mock.ANY,
|
||||||
|
"checkout_id": self.checkout.pk,
|
||||||
|
"comment": "",
|
||||||
|
"id": mock.ANY,
|
||||||
|
"is_cof": False,
|
||||||
|
"on_acc_id": self.account.pk,
|
||||||
|
"valid_by_id": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
operation = Operation.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
operation.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"addcost_amount": None,
|
||||||
|
"addcost_for_id": None,
|
||||||
|
"amount": Decimal("-5.00"),
|
||||||
|
"article_id": self.article.pk,
|
||||||
|
"article_nb": 2,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
"canceled_by_id": None,
|
||||||
|
"group_id": group.pk,
|
||||||
|
"id": mock.ANY,
|
||||||
|
"type": Operation.PURCHASE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("55.00"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 22)
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("100.00"))
|
||||||
|
|
||||||
|
self.kpsul_consumer_mock.group_send.assert_called_with(
|
||||||
|
"kfet.kpsul",
|
||||||
|
{
|
||||||
|
"opegroups": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": group.pk,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"is_cof": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"opes": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": operation.pk,
|
||||||
|
"canceled_by__trigramme": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checkouts": [],
|
||||||
|
"articles": [{"id": self.article.pk, "stock": 22}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_purchase_with_addcost(self):
|
||||||
|
# TODO(AD): L'état de la balance du compte destinataire de la majoration ne
|
||||||
|
# devrait pas empêcher l'annulation d'une opération.
|
||||||
|
addcost_user = create_user(
|
||||||
|
"addcost", "ADD", account_attrs={"balance": Decimal("10.00")}
|
||||||
|
)
|
||||||
|
addcost_account = addcost_user.profile.account_kfet
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{
|
||||||
|
"type": Operation.PURCHASE,
|
||||||
|
"article": self.article,
|
||||||
|
"article_nb": 2,
|
||||||
|
"amount": Decimal("-6.00"),
|
||||||
|
"addcost_amount": Decimal("1.00"),
|
||||||
|
"addcost_for": addcost_account,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("56.00"))
|
||||||
|
addcost_account.refresh_from_db()
|
||||||
|
self.assertEqual(addcost_account.balance, Decimal("9.00"))
|
||||||
|
|
||||||
|
def test_purchase_cash(self):
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.accounts["liq"],
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{
|
||||||
|
"type": Operation.PURCHASE,
|
||||||
|
"article": self.article,
|
||||||
|
"article_nb": 2,
|
||||||
|
"amount": Decimal("-5.00"),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.assertEqual(self.accounts["liq"].balance, Decimal("0.00"))
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("95.00"))
|
||||||
|
|
||||||
|
ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][
|
||||||
|
"checkouts"
|
||||||
|
]
|
||||||
|
self.assertListEqual(
|
||||||
|
ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("95.00")}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_purchase_cash_with_addcost(self):
|
||||||
|
# TODO(AD): L'état de la balance du compte destinataire de la majoration ne
|
||||||
|
# devrait pas empêcher l'annulation d'une opération.
|
||||||
|
addcost_user = create_user(
|
||||||
|
"addcost", "ADD", account_attrs={"balance": Decimal("10.00")}
|
||||||
|
)
|
||||||
|
addcost_account = addcost_user.profile.account_kfet
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.accounts["liq"],
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{
|
||||||
|
"type": Operation.PURCHASE,
|
||||||
|
"article": self.article,
|
||||||
|
"article_nb": 2,
|
||||||
|
"amount": Decimal("-6.00"),
|
||||||
|
"addcost_amount": Decimal("1.00"),
|
||||||
|
"addcost_for": addcost_account,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("94.00"))
|
||||||
|
addcost_account.refresh_from_db()
|
||||||
|
self.assertEqual(addcost_account.balance, Decimal("9.00"))
|
||||||
|
|
||||||
|
ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][
|
||||||
|
"checkouts"
|
||||||
|
]
|
||||||
|
self.assertListEqual(
|
||||||
|
ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("94.00")}]
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_deposit(self, now_mock):
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(seconds=15)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
group = OperationGroup.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
group.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"at": mock.ANY,
|
||||||
|
"checkout_id": self.checkout.pk,
|
||||||
|
"comment": "",
|
||||||
|
"id": mock.ANY,
|
||||||
|
"is_cof": False,
|
||||||
|
"on_acc_id": self.account.pk,
|
||||||
|
"valid_by_id": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
operation = Operation.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
operation.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"addcost_amount": None,
|
||||||
|
"addcost_for_id": None,
|
||||||
|
"amount": Decimal("10.75"),
|
||||||
|
"article_id": None,
|
||||||
|
"article_nb": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
"canceled_by_id": None,
|
||||||
|
"group_id": group.pk,
|
||||||
|
"id": mock.ANY,
|
||||||
|
"type": Operation.DEPOSIT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("39.25"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 20)
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("89.25"))
|
||||||
|
|
||||||
|
self.kpsul_consumer_mock.group_send.assert_called_with(
|
||||||
|
"kfet.kpsul",
|
||||||
|
{
|
||||||
|
"opegroups": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": group.pk,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"is_cof": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"opes": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": operation.pk,
|
||||||
|
"canceled_by__trigramme": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}],
|
||||||
|
"articles": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_withdraw(self, now_mock):
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(seconds=15)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
group = OperationGroup.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
group.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"at": mock.ANY,
|
||||||
|
"checkout_id": self.checkout.pk,
|
||||||
|
"comment": "",
|
||||||
|
"id": mock.ANY,
|
||||||
|
"is_cof": False,
|
||||||
|
"on_acc_id": self.account.pk,
|
||||||
|
"valid_by_id": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
operation = Operation.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
operation.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"addcost_amount": None,
|
||||||
|
"addcost_for_id": None,
|
||||||
|
"amount": Decimal("-10.75"),
|
||||||
|
"article_id": None,
|
||||||
|
"article_nb": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
"canceled_by_id": None,
|
||||||
|
"group_id": group.pk,
|
||||||
|
"id": mock.ANY,
|
||||||
|
"type": Operation.WITHDRAW,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("60.75"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 20)
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("110.75"))
|
||||||
|
|
||||||
|
self.kpsul_consumer_mock.group_send.assert_called_with(
|
||||||
|
"kfet.kpsul",
|
||||||
|
{
|
||||||
|
"opegroups": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": group.pk,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"is_cof": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"opes": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": operation.pk,
|
||||||
|
"canceled_by__trigramme": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}],
|
||||||
|
"articles": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_edit(self, now_mock):
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.EDIT, "amount": Decimal("-10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(seconds=15)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
group = OperationGroup.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
group.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"at": mock.ANY,
|
||||||
|
"checkout_id": self.checkout.pk,
|
||||||
|
"comment": "",
|
||||||
|
"id": mock.ANY,
|
||||||
|
"is_cof": False,
|
||||||
|
"on_acc_id": self.account.pk,
|
||||||
|
"valid_by_id": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
operation = Operation.objects.get()
|
||||||
|
self.assertDictEqual(
|
||||||
|
operation.__dict__,
|
||||||
|
{
|
||||||
|
"_state": mock.ANY,
|
||||||
|
"addcost_amount": None,
|
||||||
|
"addcost_for_id": None,
|
||||||
|
"amount": Decimal("-10.75"),
|
||||||
|
"article_id": None,
|
||||||
|
"article_nb": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
"canceled_by_id": None,
|
||||||
|
"group_id": group.pk,
|
||||||
|
"id": mock.ANY,
|
||||||
|
"type": Operation.EDIT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("60.75"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 20)
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("100.00"))
|
||||||
|
|
||||||
|
self.kpsul_consumer_mock.group_send.assert_called_with(
|
||||||
|
"kfet.kpsul",
|
||||||
|
{
|
||||||
|
"opegroups": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": group.pk,
|
||||||
|
"amount": Decimal("0.00"),
|
||||||
|
"is_cof": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"opes": [
|
||||||
|
{
|
||||||
|
"cancellation": True,
|
||||||
|
"id": operation.pk,
|
||||||
|
"canceled_by__trigramme": None,
|
||||||
|
"canceled_at": self.now + timedelta(seconds=15),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checkouts": [],
|
||||||
|
"articles": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_old_operations(self, now_mock):
|
||||||
|
kfet_config.set(cancel_duration=timedelta(minutes=10))
|
||||||
|
user_add_perms(self.users["team"], ["kfet.cancel_old_operations"])
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
at=self.now,
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(minutes=10, seconds=1)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
self.assertEqual(len(json_data["canceled"]), 1)
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_invalid_old_operations_requires_perm(self, now_mock):
|
||||||
|
kfet_config.set(cancel_duration=timedelta(minutes=10))
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
at=self.now,
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(minutes=10, seconds=1)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 403)
|
||||||
|
json_data = json.loads(resp.content.decode("utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
json_data["errors"],
|
||||||
|
{"missing_perms": ["Annuler des commandes non récentes"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_already_canceled(self):
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{
|
||||||
|
"type": Operation.WITHDRAW,
|
||||||
|
"amount": Decimal("-10.75"),
|
||||||
|
"canceled_at": timezone.now(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
json_data["warnings"], {"already_canceled": [operation.pk]}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("50.00"))
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("100.00"))
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_checkout_before_last_statement(self, now_mock):
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
at=self.now,
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.WITHDRAW, "amount": Decimal("-10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(seconds=30)
|
||||||
|
create_checkout_statement(checkout=self.checkout)
|
||||||
|
now_mock.return_value += timedelta(seconds=30)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.assertEqual(len(json_data["canceled"]), 1)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("60.75"))
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("100.00"))
|
||||||
|
|
||||||
|
@mock.patch("django.utils.timezone.now")
|
||||||
|
def test_article_before_last_inventory(self, now_mock):
|
||||||
|
now_mock.return_value = self.now
|
||||||
|
group = create_operation_group(
|
||||||
|
at=self.now,
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{"type": Operation.PURCHASE, "article": self.article, "article_nb": 2}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
now_mock.return_value += timedelta(seconds=30)
|
||||||
|
create_inventory_article(article=self.article)
|
||||||
|
now_mock.return_value += timedelta(seconds=30)
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.assertEqual(len(json_data["canceled"]), 1)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("55.00"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 20)
|
||||||
|
|
||||||
|
def test_negative(self):
|
||||||
|
kfet_config.set(overdraft_amount=Decimal("40.00"))
|
||||||
|
user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
|
||||||
|
self.account.balance = Decimal("-20.00")
|
||||||
|
self.account.save()
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
self.assertEqual(len(json_data["canceled"]), 1)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("-30.75"))
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("89.25"))
|
||||||
|
|
||||||
|
def test_invalid_negative_above_thresholds(self):
|
||||||
|
kfet_config.set(overdraft_amount=Decimal("5.00"))
|
||||||
|
user_add_perms(self.users["team"], ["kfet.perform_negative_operations"])
|
||||||
|
self.account.balance = Decimal("-20.00")
|
||||||
|
self.account.save()
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 403)
|
||||||
|
json_data = json.loads(resp.content.decode("utf-8"))
|
||||||
|
self.assertEqual(json_data["errors"], {"negative": [self.account.trigramme]})
|
||||||
|
|
||||||
|
def test_invalid_negative_requires_perms(self):
|
||||||
|
kfet_config.set(overdraft_amount=Decimal("40.00"))
|
||||||
|
self.account.balance = Decimal("-20.00")
|
||||||
|
self.account.save()
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[{"type": Operation.DEPOSIT, "amount": Decimal("10.75")}],
|
||||||
|
)
|
||||||
|
operation = group.opes.get()
|
||||||
|
|
||||||
|
data = {"operations[]": [str(operation.pk)]}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 403)
|
||||||
|
json_data = json.loads(resp.content.decode("utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
json_data["errors"],
|
||||||
|
{"missing_perms": ["Enregistrer des commandes en négatif"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_partial_0(self):
|
||||||
|
group = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{"type": Operation.PURCHASE, "article": self.article, "article_nb": 2},
|
||||||
|
{"type": Operation.DEPOSIT, "amount": Decimal("10.75")},
|
||||||
|
{"type": Operation.EDIT, "amount": Decimal("-6.00")},
|
||||||
|
{
|
||||||
|
"type": Operation.WITHDRAW,
|
||||||
|
"amount": Decimal("-10.75"),
|
||||||
|
"canceled_at": timezone.now(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation1 = group.opes.get(type=Operation.PURCHASE)
|
||||||
|
operation2 = group.opes.get(type=Operation.EDIT)
|
||||||
|
operation3 = group.opes.get(type=Operation.WITHDRAW)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"operations[]": [str(operation1.pk), str(operation2.pk), str(operation3.pk)]
|
||||||
|
}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
group.refresh_from_db()
|
||||||
|
self.assertEqual(group.amount, Decimal("10.75"))
|
||||||
|
self.assertEqual(group.opes.exclude(canceled_at=None).count(), 3)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
json_data,
|
||||||
|
{
|
||||||
|
"canceled": [operation1.pk, operation2.pk],
|
||||||
|
"warnings": {"already_canceled": [operation3.pk]},
|
||||||
|
"errors": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("61.00"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 22)
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("100.00"))
|
||||||
|
|
||||||
|
def test_multi_0(self):
|
||||||
|
group1 = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{"type": Operation.PURCHASE, "article": self.article, "article_nb": 2},
|
||||||
|
{"type": Operation.DEPOSIT, "amount": Decimal("10.75")},
|
||||||
|
{"type": Operation.EDIT, "amount": Decimal("-6.00")},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation11 = group1.opes.get(type=Operation.PURCHASE)
|
||||||
|
group2 = create_operation_group(
|
||||||
|
on_acc=self.account,
|
||||||
|
checkout=self.checkout,
|
||||||
|
content=[
|
||||||
|
{"type": Operation.PURCHASE, "article": self.article, "article_nb": 5},
|
||||||
|
{"type": Operation.DEPOSIT, "amount": Decimal("3.00")},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
operation21 = group2.opes.get(type=Operation.PURCHASE)
|
||||||
|
operation22 = group2.opes.get(type=Operation.DEPOSIT)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"operations[]": [
|
||||||
|
str(operation11.pk),
|
||||||
|
str(operation21.pk),
|
||||||
|
str(operation22.pk),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
json_data = self._assertResponseOk(resp)
|
||||||
|
|
||||||
|
group1.refresh_from_db()
|
||||||
|
self.assertEqual(group1.amount, Decimal("4.75"))
|
||||||
|
self.assertEqual(group1.opes.exclude(canceled_at=None).count(), 1)
|
||||||
|
group2.refresh_from_db()
|
||||||
|
self.assertEqual(group2.amount, Decimal(0))
|
||||||
|
self.assertEqual(group2.opes.exclude(canceled_at=None).count(), 2)
|
||||||
|
|
||||||
|
self.assertEqual(len(json_data["canceled"]), 3)
|
||||||
|
|
||||||
|
self.account.refresh_from_db()
|
||||||
|
self.assertEqual(self.account.balance, Decimal("64.50"))
|
||||||
|
self.article.refresh_from_db()
|
||||||
|
self.assertEqual(self.article.stock, 27)
|
||||||
|
self.checkout.refresh_from_db()
|
||||||
|
self.assertEqual(self.checkout.balance, Decimal("97.00"))
|
||||||
|
|
||||||
|
|
||||||
class KPsulArticlesData(ViewTestCaseMixin, TestCase):
|
class KPsulArticlesData(ViewTestCaseMixin, TestCase):
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from ..models import Account
|
from ..models import (
|
||||||
|
Account,
|
||||||
|
Article,
|
||||||
|
ArticleCategory,
|
||||||
|
Checkout,
|
||||||
|
CheckoutStatement,
|
||||||
|
Inventory,
|
||||||
|
InventoryArticle,
|
||||||
|
Operation,
|
||||||
|
OperationGroup,
|
||||||
|
)
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -184,3 +198,180 @@ def user_add_perms(user, perms_labels):
|
||||||
# it to avoid using of the previous permissions cache.
|
# it to avoid using of the previous permissions cache.
|
||||||
# https://docs.djangoproject.com/en/dev/topics/auth/default/#permission-caching
|
# https://docs.djangoproject.com/en/dev/topics/auth/default/#permission-caching
|
||||||
return User.objects.get(pk=user.pk)
|
return User.objects.get(pk=user.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def create_checkout(**kwargs):
|
||||||
|
"""
|
||||||
|
Factory to create a checkout.
|
||||||
|
See defaults for unpassed arguments in code below.
|
||||||
|
"""
|
||||||
|
if "created_by" not in kwargs or "created_by_id" not in kwargs:
|
||||||
|
try:
|
||||||
|
team_account = Account.objects.get(cofprofile__user__username="team")
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
team_account = create_team().profile.account_kfet
|
||||||
|
kwargs["created_by"] = team_account
|
||||||
|
kwargs.setdefault("name", "Checkout")
|
||||||
|
kwargs.setdefault("valid_from", timezone.now() - timedelta(days=14))
|
||||||
|
kwargs.setdefault("valid_to", timezone.now() - timedelta(days=14))
|
||||||
|
|
||||||
|
return Checkout.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_operation_group(content=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Factory to create an OperationGroup and a set of related Operation.
|
||||||
|
|
||||||
|
It aims to get objects for testing purposes with minimal setup, and
|
||||||
|
preserving consistency.
|
||||||
|
For this, it uses, and creates if necessary, default objects for unpassed
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: list of dict
|
||||||
|
Describe set of Operation to create along the OperationGroup.
|
||||||
|
Each item is passed to the Operation factory.
|
||||||
|
kwargs:
|
||||||
|
Used to control OperationGroup creation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if content is None:
|
||||||
|
content = []
|
||||||
|
|
||||||
|
# Prepare OperationGroup creation.
|
||||||
|
|
||||||
|
# Set 'checkout' for OperationGroup if unpassed.
|
||||||
|
if "checkout" not in kwargs and "checkout_id" not in kwargs:
|
||||||
|
try:
|
||||||
|
checkout = Checkout.objects.get(name="Checkout")
|
||||||
|
except Checkout.DoesNotExist:
|
||||||
|
checkout = create_checkout()
|
||||||
|
kwargs["checkout"] = checkout
|
||||||
|
|
||||||
|
# Set 'on_acc' for OperationGroup if unpassed.
|
||||||
|
if "on_acc" not in kwargs and "on_acc_id" not in kwargs:
|
||||||
|
try:
|
||||||
|
on_acc = Account.objects.get(cofprofile__user__username="user")
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
on_acc = create_user().profile.account_kfet
|
||||||
|
kwargs["on_acc"] = on_acc
|
||||||
|
|
||||||
|
# Set 'is_cof' for OperationGroup if unpassed.
|
||||||
|
if "is_cof" not in kwargs:
|
||||||
|
# Use current is_cof status of 'on_acc'.
|
||||||
|
kwargs["is_cof"] = kwargs["on_acc"].cofprofile.is_cof
|
||||||
|
|
||||||
|
# Create OperationGroup.
|
||||||
|
group = OperationGroup.objects.create(**kwargs)
|
||||||
|
|
||||||
|
# We can now create objects referencing this OperationGroup.
|
||||||
|
|
||||||
|
# Process set of related Operation.
|
||||||
|
if content:
|
||||||
|
# Create them.
|
||||||
|
operation_list = []
|
||||||
|
for operation_kwargs in content:
|
||||||
|
operation = create_operation(group=group, **operation_kwargs)
|
||||||
|
operation_list.append(operation)
|
||||||
|
|
||||||
|
# Update OperationGroup accordingly, for consistency.
|
||||||
|
for operation in operation_list:
|
||||||
|
if not operation.canceled_at:
|
||||||
|
group.amount += operation.amount
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
def create_operation(**kwargs):
|
||||||
|
"""
|
||||||
|
Factory to create an Operation for testing purposes.
|
||||||
|
|
||||||
|
If you give a 'group' (OperationGroup), it won't update it, you have do
|
||||||
|
this "manually". Prefer using OperationGroup factory to get a consistent
|
||||||
|
group with operations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if "group" not in kwargs and "group_id" not in kwargs:
|
||||||
|
# To get a consistent OperationGroup (amount...) for the operation
|
||||||
|
# in-creation, prefer using create_operation_group factory with
|
||||||
|
# 'content'.
|
||||||
|
kwargs["group"] = create_operation_group()
|
||||||
|
|
||||||
|
if "type" not in kwargs:
|
||||||
|
raise RuntimeError("Can't create an Operation without 'type'.")
|
||||||
|
|
||||||
|
# Apply defaults for purchase
|
||||||
|
if kwargs["type"] == Operation.PURCHASE:
|
||||||
|
if "article" not in kwargs:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"One could write a create_article factory. Right now, you must"
|
||||||
|
"pass an 'article'."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unpassed 'article_nb' defaults to 1.
|
||||||
|
kwargs.setdefault("article_nb", 1)
|
||||||
|
|
||||||
|
# Unpassed 'amount' will use current article price and quantity.
|
||||||
|
if "amount" not in kwargs:
|
||||||
|
if "addcost_for" in kwargs or "addcost_amount" in kwargs:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"One could handle the case where 'amount' is missing and "
|
||||||
|
"addcost applies. Right now, please pass an 'amount'."
|
||||||
|
)
|
||||||
|
kwargs["amount"] = -kwargs["article"].price * kwargs["article_nb"]
|
||||||
|
|
||||||
|
return Operation.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_checkout_statement(**kwargs):
|
||||||
|
if "checkout" not in kwargs:
|
||||||
|
kwargs["checkout"] = create_checkout()
|
||||||
|
if "by" not in kwargs:
|
||||||
|
try:
|
||||||
|
team_account = Account.objects.get(cofprofile__user__username="team")
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
team_account = create_team().profile.account_kfet
|
||||||
|
kwargs["by"] = team_account
|
||||||
|
kwargs.setdefault("balance_new", kwargs["checkout"].balance)
|
||||||
|
kwargs.setdefault("balance_old", kwargs["checkout"].balance)
|
||||||
|
kwargs.setdefault("amount_taken", Decimal(0))
|
||||||
|
|
||||||
|
return CheckoutStatement.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_article(**kwargs):
|
||||||
|
kwargs.setdefault("name", "Article")
|
||||||
|
kwargs.setdefault("price", Decimal("2.50"))
|
||||||
|
kwargs.setdefault("stock", 20)
|
||||||
|
if "category" not in kwargs:
|
||||||
|
kwargs["category"] = create_article_category()
|
||||||
|
|
||||||
|
return Article.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_article_category(**kwargs):
|
||||||
|
kwargs.setdefault("name", "Category")
|
||||||
|
return ArticleCategory.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_inventory(**kwargs):
|
||||||
|
if "by" not in kwargs:
|
||||||
|
try:
|
||||||
|
team_account = Account.objects.get(cofprofile__user__username="team")
|
||||||
|
except Account.DoesNotExist:
|
||||||
|
team_account = create_team().profile.account_kfet
|
||||||
|
kwargs["by"] = team_account
|
||||||
|
|
||||||
|
return Inventory.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_inventory_article(**kwargs):
|
||||||
|
if "inventory" not in kwargs:
|
||||||
|
kwargs["inventory"] = create_inventory()
|
||||||
|
if "article" not in kwargs:
|
||||||
|
kwargs["article"] = create_article()
|
||||||
|
kwargs.setdefault("stock_old", kwargs["article"].stock)
|
||||||
|
kwargs.setdefault("stock_new", kwargs["article"].stock)
|
||||||
|
|
||||||
|
return InventoryArticle.objects.create(**kwargs)
|
||||||
|
|
0
petitscours/__init__.py
Normal file
0
petitscours/__init__.py
Normal file
|
@ -4,7 +4,7 @@ from django.contrib.auth.models import User
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.forms.models import BaseInlineFormSet, inlineformset_factory
|
from django.forms.models import BaseInlineFormSet, inlineformset_factory
|
||||||
|
|
||||||
from gestioncof.petits_cours_models import PetitCoursAbility, PetitCoursDemande
|
from petitscours.models import PetitCoursAbility, PetitCoursDemande
|
||||||
|
|
||||||
|
|
||||||
class BaseMatieresFormSet(BaseInlineFormSet):
|
class BaseMatieresFormSet(BaseInlineFormSet):
|
|
@ -3,6 +3,7 @@ from functools import reduce
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Min
|
from django.db.models import Min
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ LEVELS_CHOICES = (
|
||||||
("prepa1styear", _("Prépa 1ère année / L1")),
|
("prepa1styear", _("Prépa 1ère année / L1")),
|
||||||
("prepa2ndyear", _("Prépa 2ème année / L2")),
|
("prepa2ndyear", _("Prépa 2ème année / L2")),
|
||||||
("licence3", _("Licence 3")),
|
("licence3", _("Licence 3")),
|
||||||
|
("master1", _("Master (1ère ou 2ème année)")),
|
||||||
("other", _("Autre (préciser dans les commentaires)")),
|
("other", _("Autre (préciser dans les commentaires)")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +29,7 @@ class PetitCoursSubject(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
app_label = "gestioncof"
|
||||||
verbose_name = "Matière de petits cours"
|
verbose_name = "Matière de petits cours"
|
||||||
verbose_name_plural = "Matières des petits cours"
|
verbose_name_plural = "Matières des petits cours"
|
||||||
|
|
||||||
|
@ -45,6 +48,7 @@ class PetitCoursAbility(models.Model):
|
||||||
agrege = models.BooleanField(_("Agrégé"), default=False)
|
agrege = models.BooleanField(_("Agrégé"), default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
app_label = "gestioncof"
|
||||||
verbose_name = "Compétence petits cours"
|
verbose_name = "Compétence petits cours"
|
||||||
verbose_name_plural = "Compétences des petits cours"
|
verbose_name_plural = "Compétences des petits cours"
|
||||||
|
|
||||||
|
@ -53,6 +57,12 @@ class PetitCoursAbility(models.Model):
|
||||||
self.user.username, self.matiere, self.niveau
|
self.user.username, self.matiere, self.niveau
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def counter(self) -> int:
|
||||||
|
"""Le compteur d'attribution associé au professeur pour cette matière."""
|
||||||
|
|
||||||
|
return PetitCoursAttributionCounter.get_uptodate(self.user, self.matiere).count
|
||||||
|
|
||||||
|
|
||||||
class PetitCoursDemande(models.Model):
|
class PetitCoursDemande(models.Model):
|
||||||
name = models.CharField(_("Nom/prénom"), max_length=200)
|
name = models.CharField(_("Nom/prénom"), max_length=200)
|
||||||
|
@ -126,7 +136,44 @@ class PetitCoursDemande(models.Model):
|
||||||
candidates = candidates.order_by("?").select_related().all()
|
candidates = candidates.order_by("?").select_related().all()
|
||||||
yield (matiere, candidates)
|
yield (matiere, candidates)
|
||||||
|
|
||||||
|
def get_proposals(self, *, max_candidates: int = None, redo: bool = False):
|
||||||
|
"""Calcule une proposition de profs pour la demande.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_candidates (optionnel; défaut: `None`): Le nombre maximum de
|
||||||
|
candidats à proposer par demande. Si `None` ou non spécifié,
|
||||||
|
il n'y a pas de limite.
|
||||||
|
|
||||||
|
redo (optionel; défaut: `False`): Détermine si on re-calcule les
|
||||||
|
propositions pour la demande (les professeurs à qui on a déjà
|
||||||
|
proposé cette demande sont exclus).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
proposals: Le dictionnaire qui associe à chaque matière la liste
|
||||||
|
des professeurs proposés. Les matières pour lesquelles aucun
|
||||||
|
professeur n'est disponible ne sont pas présentes dans
|
||||||
|
`proposals`.
|
||||||
|
unsatisfied: La liste des matières pour lesquelles aucun
|
||||||
|
professeur n'est disponible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
proposals = {}
|
||||||
|
unsatisfied = []
|
||||||
|
for matiere, candidates in self.get_candidates(redo=redo):
|
||||||
|
if not candidates:
|
||||||
|
unsatisfied.append(matiere)
|
||||||
|
else:
|
||||||
|
proposals[matiere] = matiere_proposals = []
|
||||||
|
|
||||||
|
candidates = sorted(candidates, key=lambda c: c.counter)
|
||||||
|
candidates = candidates[:max_candidates]
|
||||||
|
for candidate in candidates[:max_candidates]:
|
||||||
|
matiere_proposals.append(candidate.user)
|
||||||
|
|
||||||
|
return proposals, unsatisfied
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
app_label = "gestioncof"
|
||||||
verbose_name = "Demande de petits cours"
|
verbose_name = "Demande de petits cours"
|
||||||
verbose_name_plural = "Demandes de petits cours"
|
verbose_name_plural = "Demandes de petits cours"
|
||||||
|
|
||||||
|
@ -147,6 +194,7 @@ class PetitCoursAttribution(models.Model):
|
||||||
selected = models.BooleanField(_("Sélectionné par le demandeur"), default=False)
|
selected = models.BooleanField(_("Sélectionné par le demandeur"), default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
app_label = "gestioncof"
|
||||||
verbose_name = "Attribution de petits cours"
|
verbose_name = "Attribution de petits cours"
|
||||||
verbose_name_plural = "Attributions de petits cours"
|
verbose_name_plural = "Attributions de petits cours"
|
||||||
|
|
||||||
|
@ -182,6 +230,7 @@ class PetitCoursAttributionCounter(models.Model):
|
||||||
return counter
|
return counter
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
app_label = "gestioncof"
|
||||||
verbose_name = "Compteur d'attribution de petits cours"
|
verbose_name = "Compteur d'attribution de petits cours"
|
||||||
verbose_name_plural = "Compteurs d'attributions de petits cours"
|
verbose_name_plural = "Compteurs d'attributions de petits cours"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{% extends "base_title_petitscours.html" %}
|
{% extends "petitscours/base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block page_size %}col-sm-8{% endblock %}
|
{% block page_size %}col-sm-8{% endblock %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Demande de petits cours</h2>
|
<h2>Demande de petits cours</h2>
|
||||||
{% include "details_demande_petit_cours_infos.html" %}
|
{% include "petitscours/details_demande_infos.html" %}
|
||||||
<hr />
|
<hr />
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr><td><strong>Traitée</strong></td><td> <img src="{% if demande.traitee %}{% static "images/yes.png" %}{% else %}{% static "images/no.png" %}{% endif %}" /></td></tr>
|
<tr><td><strong>Traitée</strong></td><td> <img src="{% if demande.traitee %}{% static "images/yes.png" %}{% else %}{% static "images/no.png" %}{% endif %}" /></td></tr>
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "base_title_petitscours.html" %}
|
{% extends "petitscours/base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
|
@ -94,7 +94,7 @@ var django = {
|
||||||
<form class="form-horizontal petit-cours_form" id="bda_form" method="post" action="{% url 'petits-cours-inscription' %}">
|
<form class="form-horizontal petit-cours_form" id="bda_form" method="post" action="{% url 'petits-cours-inscription' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-top" style="margin-left:0px; margin-right:0px; font-size: 1.25em; font-weight: bold; color: #DE826B;"><input type="checkbox" name="receive_proposals" {% if receive_proposals %}checked="checked"{% endif %} /> Recevoir des propositions de petits cours</div>
|
<div class="table-top" style="margin-left:0px; margin-right:0px; font-size: 1.25em; font-weight: bold; color: #DE826B;"><input type="checkbox" name="receive_proposals" {% if receive_proposals %}checked="checked"{% endif %} /> Recevoir des propositions de petits cours</div>
|
||||||
{% include "inscription-petit-cours-formset.html" %}
|
{% include "petitscours/inscription_formset.html" %}
|
||||||
<div class="inscription-bottom">
|
<div class="inscription-bottom">
|
||||||
<input type="button" class="btn btn-default pull-right" value="Ajouter une autre matière" id="add_more" />
|
<input type="button" class="btn btn-default pull-right" value="Ajouter une autre matière" id="add_more" />
|
||||||
<script>
|
<script>
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base_title_petitscours.html" %}
|
{% extends "petitscours/base_title.html" %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
|
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
|
||||||
{% include "details_demande_petit_cours_infos.html" %}
|
{% include "petitscours/details_demande_infos.html" %}
|
||||||
<hr />
|
<hr />
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
<div class="error">
|
<div class="error">
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "base_title_petitscours.html" %}
|
{% extends "petitscours/base_title.html" %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
|
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
|
||||||
{% include "details_demande_petit_cours_infos.html" %}
|
{% include "petitscours/details_demande_infos.html" %}
|
||||||
<hr />
|
<hr />
|
||||||
<div class="error">
|
<div class="error">
|
||||||
Attention: demande de petits cours spécifiant le niveau "Autre niveau": choisissez les candidats correspondant aux remarques de la demande. S'il y a moins de 3 candidats adaptés, ne mettre que ceux qui conviennent, pas besoin de faire du bourrage :)
|
Attention: demande de petits cours spécifiant le niveau "Autre niveau": choisissez les candidats correspondant aux remarques de la demande. S'il y a moins de 3 candidats adaptés, ne mettre que ceux qui conviennent, pas besoin de faire du bourrage :)
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "base_title_petitscours.html" %}
|
{% extends "petitscours/base_title.html" %}
|
||||||
|
|
||||||
{% block realcontent %}
|
{% block realcontent %}
|
||||||
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
|
<h2>Traitement de la demande de petits cours {{ demande.id }}</h2>
|
0
petitscours/tests/__init__.py
Normal file
0
petitscours/tests/__init__.py
Normal file
344
petitscours/tests/test_petitscours_views.py
Normal file
344
petitscours/tests/test_petitscours_views.py
Normal file
|
@ -0,0 +1,344 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import Client, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from gestioncof.tests.testcases import ViewTestCaseMixin
|
||||||
|
|
||||||
|
from .utils import (
|
||||||
|
PetitCoursTestHelpers,
|
||||||
|
create_petitcours_ability,
|
||||||
|
create_petitcours_demande,
|
||||||
|
create_petitcours_subject,
|
||||||
|
)
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursDemandeListViewTestCase(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "petits-cours-demandes-list"
|
||||||
|
url_expected = "/petitcours/demandes"
|
||||||
|
|
||||||
|
auth_user = "staff"
|
||||||
|
auth_forbidden = [None, "user", "member"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.demande1 = create_petitcours_demande()
|
||||||
|
self.demande2 = create_petitcours_demande()
|
||||||
|
self.demande3 = create_petitcours_demande()
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(len(resp.context["object_list"]), 3)
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursDemandeDetailListViewTestCase(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "petits-cours-demande-details"
|
||||||
|
|
||||||
|
auth_user = "staff"
|
||||||
|
auth_forbidden = [None, "user", "member"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"pk": self.demande.pk}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/petitcours/demandes/{}".format(self.demande.pk)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.demande = create_petitcours_demande()
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursInscriptionViewTestCase(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "petits-cours-inscription"
|
||||||
|
url_expected = "/petitcours/inscription"
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
auth_user = "member"
|
||||||
|
# Also forbidden for "user". Test below.
|
||||||
|
auth_forbidden = [None]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user = self.users["member"]
|
||||||
|
self.cofprofile = self.user.profile
|
||||||
|
|
||||||
|
self.subject1 = create_petitcours_subject(name="Matière 1")
|
||||||
|
self.subject2 = create_petitcours_subject(name="Matière 2")
|
||||||
|
|
||||||
|
def test_get_forbidden_user_not_cof(self):
|
||||||
|
self.client.force_login(self.users["user"])
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertRedirects(resp, reverse("cof-denied"))
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_post_data(self):
|
||||||
|
return {
|
||||||
|
"petitcoursability_set-TOTAL_FORMS": "3",
|
||||||
|
"petitcoursability_set-INITIAL_FORMS": "0",
|
||||||
|
"petitcoursability_set-MIN_NUM_FORMS": "0",
|
||||||
|
"petitcoursability_set-MAX_NUM_FORMS": "1000",
|
||||||
|
"remarques": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
data = dict(
|
||||||
|
self.base_post_data,
|
||||||
|
**{
|
||||||
|
"petitcoursability_set-TOTAL_FORMS": "2",
|
||||||
|
"petitcoursability_set-0-id": "",
|
||||||
|
"petitcoursability_set-0-user": "",
|
||||||
|
"petitcoursability_set-0-matiere": str(self.subject1.pk),
|
||||||
|
"petitcoursability_set-0-niveau": "college",
|
||||||
|
"petitcoursability_set-0-agrege": "1",
|
||||||
|
# "petitcoursability_set-0-DELETE": "1",
|
||||||
|
"petitcoursability_set-1-id": "",
|
||||||
|
"petitcoursability_set-1-user": "",
|
||||||
|
"petitcoursability_set-1-matiere": str(self.subject2.pk),
|
||||||
|
"petitcoursability_set-1-niveau": "lycee",
|
||||||
|
# "petitcoursability_set-1-agrege": "1",
|
||||||
|
# "petitcoursability_set-1-DELETE": "1",
|
||||||
|
# "receive_proposals": "1",
|
||||||
|
"remarques": "Une remarque",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.cofprofile.refresh_from_db()
|
||||||
|
self.assertEqual(self.cofprofile.petits_cours_accept, False)
|
||||||
|
self.assertEqual(self.cofprofile.petits_cours_remarques, "Une remarque")
|
||||||
|
self.assertEqual(self.user.petitcoursability_set.count(), 2)
|
||||||
|
ability1 = self.user.petitcoursability_set.get(matiere=self.subject1)
|
||||||
|
self.assertEqual(ability1.niveau, "college")
|
||||||
|
self.assertTrue(ability1.agrege)
|
||||||
|
ability2 = self.user.petitcoursability_set.get(matiere=self.subject2)
|
||||||
|
self.assertEqual(ability2.niveau, "lycee")
|
||||||
|
self.assertFalse(ability2.agrege)
|
||||||
|
|
||||||
|
def test_post_delete(self):
|
||||||
|
ability1 = create_petitcours_ability(user=self.user)
|
||||||
|
ability2 = create_petitcours_ability(user=self.user)
|
||||||
|
|
||||||
|
data = dict(
|
||||||
|
self.base_post_data,
|
||||||
|
**{
|
||||||
|
"petitcoursability_set-INITIAL_FORMS": "2",
|
||||||
|
"petitcoursability_set-TOTAL_FORMS": "2",
|
||||||
|
"petitcoursability_set-0-id": str(ability1.pk),
|
||||||
|
"petitcoursability_set-0-user": "",
|
||||||
|
"petitcoursability_set-0-matiere": str(self.subject1.pk),
|
||||||
|
"petitcoursability_set-0-niveau": "college",
|
||||||
|
"petitcoursability_set-0-agrege": "1",
|
||||||
|
"petitcoursability_set-0-DELETE": "1",
|
||||||
|
"petitcoursability_set-1-id": str(ability2.pk),
|
||||||
|
"petitcoursability_set-1-user": str(self.user.pk),
|
||||||
|
"petitcoursability_set-1-matiere": str(self.subject2.pk),
|
||||||
|
"petitcoursability_set-1-niveau": "lycee",
|
||||||
|
# "petitcoursability_set-1-agrege": "1",
|
||||||
|
"petitcoursability_set-1-DELETE": "1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertFalse(self.user.petitcoursability_set.all())
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursTraitementViewTestCase(
|
||||||
|
ViewTestCaseMixin, PetitCoursTestHelpers, TestCase
|
||||||
|
):
|
||||||
|
url_name = "petits-cours-demande-traitement"
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
auth_user = "staff"
|
||||||
|
auth_forbidden = [None, "user", "member"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"demande_id": self.demande.pk}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/petitcours/demandes/{}/traitement".format(self.demande.pk)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user = self.users["member"]
|
||||||
|
self.user.profile.petits_cours_accept = True
|
||||||
|
self.user.profile.save()
|
||||||
|
self.subject = create_petitcours_subject()
|
||||||
|
self.demande = create_petitcours_demande(niveau="college")
|
||||||
|
self.demande.matieres.add(self.subject)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.require_custommails()
|
||||||
|
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_get_with_match(self):
|
||||||
|
self.require_custommails()
|
||||||
|
|
||||||
|
create_petitcours_ability(
|
||||||
|
user=self.user, matiere=self.subject, niveau="college"
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertListEqual(
|
||||||
|
list(resp.context["proposals"]), [(self.subject, [self.user])]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
resp.context["attribdata"], json.dumps([(self.subject.id, [self.user.id])])
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_post_with_match(self):
|
||||||
|
self.require_custommails()
|
||||||
|
|
||||||
|
create_petitcours_ability(
|
||||||
|
user=self.user, matiere=self.subject, niveau="college"
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"attribdata": json.dumps([(self.subject.pk, [self.user.pk])]),
|
||||||
|
"extra": "",
|
||||||
|
}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.demande.refresh_from_db()
|
||||||
|
self.assertTrue(self.demande.traitee)
|
||||||
|
self.assertEqual(self.demande.traitee_par, self.users["staff"])
|
||||||
|
self.assertIsNotNone(self.demande.processed)
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursRetraitementViewTestCase(
|
||||||
|
ViewTestCaseMixin, PetitCoursTestHelpers, TestCase
|
||||||
|
):
|
||||||
|
url_name = "petits-cours-demande-retraitement"
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
auth_user = "staff"
|
||||||
|
auth_forbidden = [None, "user", "member"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_kwargs(self):
|
||||||
|
return {"demande_id": self.demande.pk}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url_expected(self):
|
||||||
|
return "/petitcours/demandes/{}/retraitement".format(self.demande.pk)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.demande = create_petitcours_demande()
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.require_custommails()
|
||||||
|
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursDemandeViewTestCase(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "petits-cours-demande"
|
||||||
|
url_expected = "/petitcours/demande"
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
auth_user = None
|
||||||
|
auth_forbidden = []
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
os.environ["RECAPTCHA_TESTING"] = "True"
|
||||||
|
self.subject1 = create_petitcours_subject()
|
||||||
|
self.subject2 = create_petitcours_subject()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.environ["RECAPTCHA_TESTING"] = "False"
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
data = {
|
||||||
|
"name": "Le nom",
|
||||||
|
"email": "lemail@mail.net",
|
||||||
|
"phone": "0123456789",
|
||||||
|
"quand": "matin, midi et soir",
|
||||||
|
"freq": "tous les jours",
|
||||||
|
"lieu": "partout",
|
||||||
|
"matieres": [str(self.subject1.pk), str(self.subject2.pk)],
|
||||||
|
"agrege_requis": "1",
|
||||||
|
"niveau": "lycee",
|
||||||
|
"remarques": "no comment",
|
||||||
|
"g-recaptcha-response": "PASSED",
|
||||||
|
}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(resp.context["success"], msg=str(resp.context["form"].errors))
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursDemandeRawViewTestCase(ViewTestCaseMixin, TestCase):
|
||||||
|
url_name = "petits-cours-demande-raw"
|
||||||
|
url_expected = "/petitcours/demande-raw"
|
||||||
|
|
||||||
|
http_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
auth_user = None
|
||||||
|
auth_forbidden = []
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
os.environ["RECAPTCHA_TESTING"] = "True"
|
||||||
|
self.subject1 = create_petitcours_subject()
|
||||||
|
self.subject2 = create_petitcours_subject()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.environ["RECAPTCHA_TESTING"] = "False"
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = self.client.get(self.url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
data = {
|
||||||
|
"name": "Le nom",
|
||||||
|
"email": "lemail@mail.net",
|
||||||
|
"phone": "0123456789",
|
||||||
|
"quand": "matin, midi et soir",
|
||||||
|
"freq": "tous les jours",
|
||||||
|
"lieu": "partout",
|
||||||
|
"matieres": [str(self.subject1.pk), str(self.subject2.pk)],
|
||||||
|
"agrege_requis": "1",
|
||||||
|
"niveau": "lycee",
|
||||||
|
"remarques": "no comment",
|
||||||
|
"g-recaptcha-response": "PASSED",
|
||||||
|
}
|
||||||
|
resp = self.client.post(self.url, data)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue(resp.context["success"], msg=str(resp.context["form"].errors))
|
39
petitscours/tests/utils.py
Normal file
39
petitscours/tests/utils.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
from petitscours.models import (
|
||||||
|
PetitCoursAbility,
|
||||||
|
PetitCoursAttributionCounter,
|
||||||
|
PetitCoursDemande,
|
||||||
|
PetitCoursSubject,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_petitcours_ability(**kwargs):
|
||||||
|
if "user" not in kwargs:
|
||||||
|
kwargs["user"] = create_user()
|
||||||
|
if "matiere" not in kwargs:
|
||||||
|
kwargs["matiere"] = create_petitcours_subject()
|
||||||
|
if "niveau" not in kwargs:
|
||||||
|
kwargs["niveau"] = "college"
|
||||||
|
ability = PetitCoursAbility.objects.create(**kwargs)
|
||||||
|
PetitCoursAttributionCounter.get_uptodate(ability.user, ability.matiere)
|
||||||
|
return ability
|
||||||
|
|
||||||
|
|
||||||
|
def create_petitcours_demande(**kwargs):
|
||||||
|
return PetitCoursDemande.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def create_petitcours_subject(**kwargs):
|
||||||
|
return PetitCoursSubject.objects.create(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PetitCoursTestHelpers:
|
||||||
|
def require_custommails(self):
|
||||||
|
data_file = os.path.join(
|
||||||
|
settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json"
|
||||||
|
)
|
||||||
|
call_command("syncmails", data_file, verbosity=0)
|
37
petitscours/urls.py
Normal file
37
petitscours/urls.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from gestioncof.decorators import buro_required
|
||||||
|
from petitscours import views
|
||||||
|
from petitscours.views import DemandeDetailView, DemandeListView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r"^inscription$", views.inscription, name="petits-cours-inscription"),
|
||||||
|
url(r"^demande$", views.demande, name="petits-cours-demande"),
|
||||||
|
url(
|
||||||
|
r"^demande-raw$",
|
||||||
|
views.demande,
|
||||||
|
kwargs={"raw": True},
|
||||||
|
name="petits-cours-demande-raw",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^demandes$",
|
||||||
|
buro_required(DemandeListView.as_view()),
|
||||||
|
name="petits-cours-demandes-list",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^demandes/(?P<pk>\d+)$",
|
||||||
|
buro_required(DemandeDetailView.as_view()),
|
||||||
|
name="petits-cours-demande-details",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^demandes/(?P<demande_id>\d+)/traitement$",
|
||||||
|
views.traitement,
|
||||||
|
name="petits-cours-demande-traitement",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^demandes/(?P<demande_id>\d+)/retraitement$",
|
||||||
|
views.traitement,
|
||||||
|
kwargs={"redo": True},
|
||||||
|
name="petits-cours-demande-retraitement",
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,8 +14,8 @@ from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
from gestioncof.decorators import buro_required
|
from gestioncof.decorators import buro_required
|
||||||
from gestioncof.models import CofProfile
|
from gestioncof.models import CofProfile
|
||||||
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
|
from petitscours.forms import DemandeForm, MatieresFormSet
|
||||||
from gestioncof.petits_cours_models import (
|
from petitscours.models import (
|
||||||
PetitCoursAbility,
|
PetitCoursAbility,
|
||||||
PetitCoursAttribution,
|
PetitCoursAttribution,
|
||||||
PetitCoursAttributionCounter,
|
PetitCoursAttributionCounter,
|
||||||
|
@ -27,7 +27,7 @@ class DemandeListView(ListView):
|
||||||
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
|
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
|
||||||
"traitee", "-id"
|
"traitee", "-id"
|
||||||
)
|
)
|
||||||
template_name = "petits_cours_demandes_list.html"
|
template_name = "petitscours/demande_list.html"
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class DemandeDetailView(DetailView):
|
||||||
queryset = PetitCoursDemande.objects.prefetch_related(
|
queryset = PetitCoursDemande.objects.prefetch_related(
|
||||||
"petitcoursattribution_set", "matieres"
|
"petitcoursattribution_set", "matieres"
|
||||||
)
|
)
|
||||||
template_name = "gestioncof/details_demande_petit_cours.html"
|
template_name = "petitscours/demande_detail.html"
|
||||||
context_object_name = "demande"
|
context_object_name = "demande"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -53,64 +53,27 @@ def traitement(request, demande_id, redo=False):
|
||||||
return _traitement_other(request, demande, redo)
|
return _traitement_other(request, demande, redo)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
return _traitement_post(request, demande)
|
return _traitement_post(request, demande)
|
||||||
proposals = {}
|
proposals, unsatisfied = demande.get_proposals(redo=redo, max_candidates=3)
|
||||||
proposed_for = {}
|
return _finalize_traitement(request, demande, proposals, unsatisfied, redo)
|
||||||
unsatisfied = []
|
|
||||||
attribdata = {}
|
|
||||||
for matiere, candidates in demande.get_candidates(redo):
|
|
||||||
if candidates:
|
|
||||||
tuples = []
|
|
||||||
for candidate in candidates:
|
|
||||||
user = candidate.user
|
|
||||||
tuples.append(
|
|
||||||
(
|
|
||||||
candidate,
|
|
||||||
PetitCoursAttributionCounter.get_uptodate(user, matiere),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
tuples = sorted(tuples, key=lambda c: c[1].count)
|
|
||||||
candidates, _ = zip(*tuples)
|
|
||||||
candidates = candidates[0 : min(3, len(candidates))]
|
|
||||||
attribdata[matiere.id] = []
|
|
||||||
proposals[matiere] = []
|
|
||||||
for candidate in candidates:
|
|
||||||
user = candidate.user
|
|
||||||
proposals[matiere].append(user)
|
|
||||||
attribdata[matiere.id].append(user.id)
|
|
||||||
if user not in proposed_for:
|
|
||||||
proposed_for[user] = [matiere]
|
|
||||||
else:
|
|
||||||
proposed_for[user].append(matiere)
|
|
||||||
else:
|
|
||||||
unsatisfied.append(matiere)
|
|
||||||
return _finalize_traitement(
|
|
||||||
request, demande, proposals, proposed_for, unsatisfied, attribdata, redo
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@buro_required
|
|
||||||
def retraitement(request, demande_id):
|
|
||||||
return traitement(request, demande_id, redo=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _finalize_traitement(
|
def _finalize_traitement(
|
||||||
request,
|
request, demande, proposals, unsatisfied, redo=False, errors=None
|
||||||
demande,
|
|
||||||
proposals,
|
|
||||||
proposed_for,
|
|
||||||
unsatisfied,
|
|
||||||
attribdata,
|
|
||||||
redo=False,
|
|
||||||
errors=None,
|
|
||||||
):
|
):
|
||||||
proposals = proposals.items()
|
attribdata = [
|
||||||
proposed_for = proposed_for.items()
|
(matiere.id, [user.id for user in users])
|
||||||
attribdata = list(attribdata.items())
|
for matiere, users in proposals.items()
|
||||||
|
]
|
||||||
|
proposed_for = {}
|
||||||
|
for matiere, users in proposals.items():
|
||||||
|
for user in users:
|
||||||
|
proposed_for.setdefault(user, []).append(matiere)
|
||||||
|
|
||||||
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
||||||
mainmail = render_custom_mail(
|
mainmail = render_custom_mail(
|
||||||
"petits-cours-mail-demandeur",
|
"petits-cours-mail-demandeur",
|
||||||
{
|
{
|
||||||
"proposals": proposals,
|
"proposals": proposals.items(),
|
||||||
"unsatisfied": unsatisfied,
|
"unsatisfied": unsatisfied,
|
||||||
"extra": '<textarea name="extra" '
|
"extra": '<textarea name="extra" '
|
||||||
'style="width:99%; height: 90px;">'
|
'style="width:99%; height: 90px;">'
|
||||||
|
@ -122,12 +85,12 @@ def _finalize_traitement(
|
||||||
messages.error(request, error)
|
messages.error(request, error)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"gestioncof/traitement_demande_petit_cours.html",
|
"petitscours/traitement_demande.html",
|
||||||
{
|
{
|
||||||
"demande": demande,
|
"demande": demande,
|
||||||
"unsatisfied": unsatisfied,
|
"unsatisfied": unsatisfied,
|
||||||
"proposals": proposals,
|
"proposals": proposals.items(),
|
||||||
"proposed_for": proposed_for,
|
"proposed_for": proposed_for.items(),
|
||||||
"proposed_mails": proposed_mails,
|
"proposed_mails": proposed_mails,
|
||||||
"mainmail": mainmail,
|
"mainmail": mainmail,
|
||||||
"attribdata": json.dumps(attribdata),
|
"attribdata": json.dumps(attribdata),
|
||||||
|
@ -144,7 +107,7 @@ def _generate_eleve_email(demande, proposed_for):
|
||||||
"petit-cours-mail-eleve", {"demande": demande, "matieres": matieres}
|
"petit-cours-mail-eleve", {"demande": demande, "matieres": matieres}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for user, matieres in proposed_for
|
for user, matieres in proposed_for.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,15 +115,12 @@ def _traitement_other_preparing(request, demande):
|
||||||
redo = "redo" in request.POST
|
redo = "redo" in request.POST
|
||||||
unsatisfied = []
|
unsatisfied = []
|
||||||
proposals = {}
|
proposals = {}
|
||||||
proposed_for = {}
|
|
||||||
attribdata = {}
|
|
||||||
errors = []
|
errors = []
|
||||||
for matiere, candidates in demande.get_candidates(redo):
|
for matiere, candidates in demande.get_candidates(redo):
|
||||||
if candidates:
|
if candidates:
|
||||||
candidates = dict(
|
candidates = dict(
|
||||||
[(candidate.user.id, candidate.user) for candidate in candidates]
|
[(candidate.user.id, candidate.user) for candidate in candidates]
|
||||||
)
|
)
|
||||||
attribdata[matiere.id] = []
|
|
||||||
proposals[matiere] = []
|
proposals[matiere] = []
|
||||||
for choice_id in range(min(3, len(candidates))):
|
for choice_id in range(min(3, len(candidates))):
|
||||||
choice = int(
|
choice = int(
|
||||||
|
@ -183,11 +143,6 @@ def _traitement_other_preparing(request, demande):
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
proposals[matiere].append(user)
|
proposals[matiere].append(user)
|
||||||
attribdata[matiere.id].append(user.id)
|
|
||||||
if user not in proposed_for:
|
|
||||||
proposed_for[user] = [matiere]
|
|
||||||
else:
|
|
||||||
proposed_for[user].append(matiere)
|
|
||||||
if not proposals[matiere]:
|
if not proposals[matiere]:
|
||||||
errors.append("Aucune proposition pour {!s}".format(matiere))
|
errors.append("Aucune proposition pour {!s}".format(matiere))
|
||||||
elif len(proposals[matiere]) < 3:
|
elif len(proposals[matiere]) < 3:
|
||||||
|
@ -200,15 +155,7 @@ def _traitement_other_preparing(request, demande):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
unsatisfied.append(matiere)
|
unsatisfied.append(matiere)
|
||||||
return _finalize_traitement(
|
return _finalize_traitement(request, demande, proposals, unsatisfied, errors=errors)
|
||||||
request,
|
|
||||||
demande,
|
|
||||||
proposals,
|
|
||||||
proposed_for,
|
|
||||||
unsatisfied,
|
|
||||||
attribdata,
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _traitement_other(request, demande, redo):
|
def _traitement_other(request, demande, redo):
|
||||||
|
@ -217,45 +164,14 @@ def _traitement_other(request, demande, redo):
|
||||||
return _traitement_other_preparing(request, demande)
|
return _traitement_other_preparing(request, demande)
|
||||||
else:
|
else:
|
||||||
return _traitement_post(request, demande)
|
return _traitement_post(request, demande)
|
||||||
proposals = {}
|
proposals, unsatisfied = demande.get_proposals(redo=redo)
|
||||||
proposed_for = {}
|
|
||||||
unsatisfied = []
|
|
||||||
attribdata = {}
|
|
||||||
for matiere, candidates in demande.get_candidates(redo):
|
|
||||||
if candidates:
|
|
||||||
tuples = []
|
|
||||||
for candidate in candidates:
|
|
||||||
user = candidate.user
|
|
||||||
tuples.append(
|
|
||||||
(
|
|
||||||
candidate,
|
|
||||||
PetitCoursAttributionCounter.get_uptodate(user, matiere),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
tuples = sorted(tuples, key=lambda c: c[1].count)
|
|
||||||
candidates, _ = zip(*tuples)
|
|
||||||
attribdata[matiere.id] = []
|
|
||||||
proposals[matiere] = []
|
|
||||||
for candidate in candidates:
|
|
||||||
user = candidate.user
|
|
||||||
proposals[matiere].append(user)
|
|
||||||
attribdata[matiere.id].append(user.id)
|
|
||||||
if user not in proposed_for:
|
|
||||||
proposed_for[user] = [matiere]
|
|
||||||
else:
|
|
||||||
proposed_for[user].append(matiere)
|
|
||||||
else:
|
|
||||||
unsatisfied.append(matiere)
|
|
||||||
proposals = proposals.items()
|
|
||||||
proposed_for = proposed_for.items()
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"gestioncof/traitement_demande_petit_cours_autre_niveau.html",
|
"petitscours/traitement_demande_autre_niveau.html",
|
||||||
{
|
{
|
||||||
"demande": demande,
|
"demande": demande,
|
||||||
"unsatisfied": unsatisfied,
|
"unsatisfied": unsatisfied,
|
||||||
"proposals": proposals,
|
"proposals": proposals.items(),
|
||||||
"proposed_for": proposed_for,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -280,12 +196,10 @@ def _traitement_post(request, demande):
|
||||||
proposed_for[user] = [matiere]
|
proposed_for[user] = [matiere]
|
||||||
else:
|
else:
|
||||||
proposed_for[user].append(matiere)
|
proposed_for[user].append(matiere)
|
||||||
proposals_list = proposals.items()
|
|
||||||
proposed_for = proposed_for.items()
|
|
||||||
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
||||||
mainmail_object, mainmail_body = render_custom_mail(
|
mainmail_object, mainmail_body = render_custom_mail(
|
||||||
"petits-cours-mail-demandeur",
|
"petits-cours-mail-demandeur",
|
||||||
{"proposals": proposals_list, "unsatisfied": unsatisfied, "extra": extra},
|
{"proposals": proposals.items(), "unsatisfied": unsatisfied, "extra": extra},
|
||||||
)
|
)
|
||||||
frommail = settings.MAIL_DATA["petits_cours"]["FROM"]
|
frommail = settings.MAIL_DATA["petits_cours"]["FROM"]
|
||||||
bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
|
bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
|
||||||
|
@ -314,8 +228,9 @@ def _traitement_post(request, demande):
|
||||||
connection = mail.get_connection(fail_silently=False)
|
connection = mail.get_connection(fail_silently=False)
|
||||||
connection.send_messages(mails_to_send)
|
connection.send_messages(mails_to_send)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for matiere in proposals:
|
for matiere, users in proposals.items():
|
||||||
for rank, user in enumerate(proposals[matiere]):
|
for rank, user in enumerate(users):
|
||||||
|
# TODO(AD): Prefer PetitCoursAttributionCounter.get_uptodate()
|
||||||
counter = PetitCoursAttributionCounter.objects.get(
|
counter = PetitCoursAttributionCounter.objects.get(
|
||||||
user=user, matiere=matiere
|
user=user, matiere=matiere
|
||||||
)
|
)
|
||||||
|
@ -331,7 +246,7 @@ def _traitement_post(request, demande):
|
||||||
demande.save()
|
demande.save()
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"gestioncof/traitement_demande_petit_cours_success.html",
|
"petitscours/traitement_demande_success.html",
|
||||||
{"demande": demande, "redo": redo},
|
{"demande": demande, "redo": redo},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -361,7 +276,7 @@ def inscription(request):
|
||||||
formset = MatieresFormSet(instance=request.user)
|
formset = MatieresFormSet(instance=request.user)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"inscription-petit-cours.html",
|
"petitscours/inscription.html",
|
||||||
{
|
{
|
||||||
"formset": formset,
|
"formset": formset,
|
||||||
"success": success,
|
"success": success,
|
||||||
|
@ -372,7 +287,7 @@ def inscription(request):
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def demande(request):
|
def demande(request, *, raw: bool = False):
|
||||||
success = False
|
success = False
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = DemandeForm(request.POST)
|
form = DemandeForm(request.POST)
|
||||||
|
@ -381,21 +296,7 @@ def demande(request):
|
||||||
success = True
|
success = True
|
||||||
else:
|
else:
|
||||||
form = DemandeForm()
|
form = DemandeForm()
|
||||||
return render(
|
template_name = "petitscours/demande.html"
|
||||||
request, "demande-petit-cours.html", {"form": form, "success": success}
|
if raw:
|
||||||
)
|
template_name = "petitscours/demande_raw.html"
|
||||||
|
return render(request, template_name, {"form": form, "success": success})
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def demande_raw(request):
|
|
||||||
success = False
|
|
||||||
if request.method == "POST":
|
|
||||||
form = DemandeForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
success = True
|
|
||||||
else:
|
|
||||||
form = DemandeForm()
|
|
||||||
return render(
|
|
||||||
request, "demande-petit-cours-raw.html", {"form": form, "success": success}
|
|
||||||
)
|
|
|
@ -3,9 +3,9 @@ Django==1.11.*
|
||||||
django-autocomplete-light==3.1.3
|
django-autocomplete-light==3.1.3
|
||||||
django-autoslug==1.9.3
|
django-autoslug==1.9.3
|
||||||
django-cas-ng==3.5.7
|
django-cas-ng==3.5.7
|
||||||
django-djconfig==0.5.3
|
django-djconfig==0.8.0
|
||||||
django-recaptcha==1.4.0
|
django-recaptcha==1.4.0
|
||||||
django-redis-cache==1.7.1
|
django-redis-cache==1.8.1
|
||||||
icalendar
|
icalendar
|
||||||
psycopg2
|
psycopg2
|
||||||
Pillow
|
Pillow
|
||||||
|
|
|
@ -4,6 +4,7 @@ source =
|
||||||
cof
|
cof
|
||||||
gestioncof
|
gestioncof
|
||||||
kfet
|
kfet
|
||||||
|
petitscours
|
||||||
shared
|
shared
|
||||||
utils
|
utils
|
||||||
omit =
|
omit =
|
||||||
|
@ -33,7 +34,7 @@ default_section = THIRDPARTY
|
||||||
force_grid_wrap = 0
|
force_grid_wrap = 0
|
||||||
include_trailing_comma = true
|
include_trailing_comma = true
|
||||||
known_django = django
|
known_django = django
|
||||||
known_first_party = bda,cof,gestioncof,kfet,shared,utils
|
known_first_party = bda,cof,gestioncof,kfet,petitscours,shared,utils
|
||||||
line_length = 88
|
line_length = 88
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
not_skip = __init__.py
|
not_skip = __init__.py
|
||||||
|
|
Loading…
Reference in a new issue