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
|
||||
# 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"
|
||||
- pip install --upgrade -r requirements.txt coverage
|
||||
- pip install --upgrade -r requirements.txt coverage tblib
|
||||
- python --version
|
||||
script:
|
||||
- coverage run manage.py test
|
||||
- coverage run manage.py test --parallel
|
||||
after_script:
|
||||
- coverage report
|
||||
services:
|
||||
|
@ -52,9 +52,9 @@ linters:
|
|||
- pip install --upgrade black isort flake8
|
||||
script:
|
||||
- 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
|
||||
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
|
||||
- flake8 --exit-zero bda cof gestioncof kfet petitscours provisioning shared utils
|
||||
cache:
|
||||
key: linters
|
||||
paths:
|
||||
|
|
|
@ -60,24 +60,20 @@ class BdATestHelpers:
|
|||
def check_restricted_access(
|
||||
self, url, validate_user=user_is_cof, redirect_url=None
|
||||
):
|
||||
def craft_redirect_url(user):
|
||||
if redirect_url:
|
||||
return redirect_url
|
||||
for (user, client) in self.client_matrix:
|
||||
resp = client.get(url, follow=True)
|
||||
if validate_user(user):
|
||||
self.assertEqual(200, resp.status_code)
|
||||
elif redirect_url:
|
||||
self.assertRedirects(resp, redirect_url)
|
||||
elif user is None:
|
||||
# client is not logged in
|
||||
login_url = "/login"
|
||||
if url:
|
||||
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
|
||||
return login_url
|
||||
self.assertRedirects(resp, login_url)
|
||||
else:
|
||||
return "/"
|
||||
|
||||
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))
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
|
||||
class TestBdAViews(BdATestHelpers, TestCase):
|
||||
|
|
|
@ -6,6 +6,6 @@ English formatting.
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
DATETIME_FORMAT = r'l N j, Y \a\t P'
|
||||
DATE_FORMAT = r'l N j, Y'
|
||||
TIME_FORMAT = r'P'
|
||||
DATETIME_FORMAT = r"l N j, Y \a\t P"
|
||||
DATE_FORMAT = r"l N j, Y"
|
||||
TIME_FORMAT = r"P"
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
Formats français.
|
||||
"""
|
||||
|
||||
DATETIME_FORMAT = r'l j F Y \à H\hi'
|
||||
DATE_FORMAT = r'l j F Y'
|
||||
TIME_FORMAT = r'H\hi'
|
||||
DATETIME_FORMAT = r"l j F Y \à H\hi"
|
||||
DATE_FORMAT = r"l j F Y"
|
||||
TIME_FORMAT = r"H\hi"
|
||||
|
|
|
@ -57,64 +57,65 @@ INSTALLED_APPS = [
|
|||
"gestioncof",
|
||||
# Must be before 'django.contrib.admin'.
|
||||
# https://django-autocomplete-light.readthedocs.io/en/master/install.html
|
||||
'dal',
|
||||
'dal_select2',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
'bda',
|
||||
'captcha',
|
||||
'django_cas_ng',
|
||||
'bootstrapform',
|
||||
'kfet',
|
||||
'kfet.open',
|
||||
'channels',
|
||||
'widget_tweaks',
|
||||
'custommail',
|
||||
'djconfig',
|
||||
'wagtail.wagtailforms',
|
||||
'wagtail.wagtailredirects',
|
||||
'wagtail.wagtailembeds',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailsnippets',
|
||||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtailsearch',
|
||||
'wagtail.wagtailadmin',
|
||||
'wagtail.wagtailcore',
|
||||
'wagtail.contrib.modeladmin',
|
||||
'wagtail.contrib.wagtailroutablepage',
|
||||
'wagtailmenus',
|
||||
'wagtail_modeltranslation',
|
||||
'modelcluster',
|
||||
'taggit',
|
||||
'kfet.auth',
|
||||
'kfet.cms',
|
||||
'gestioncof.cms',
|
||||
"dal",
|
||||
"dal_select2",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.admindocs",
|
||||
"bda",
|
||||
"petitscours",
|
||||
"captcha",
|
||||
"django_cas_ng",
|
||||
"bootstrapform",
|
||||
"kfet",
|
||||
"kfet.open",
|
||||
"channels",
|
||||
"widget_tweaks",
|
||||
"custommail",
|
||||
"djconfig",
|
||||
"wagtail.wagtailforms",
|
||||
"wagtail.wagtailredirects",
|
||||
"wagtail.wagtailembeds",
|
||||
"wagtail.wagtailsites",
|
||||
"wagtail.wagtailusers",
|
||||
"wagtail.wagtailsnippets",
|
||||
"wagtail.wagtaildocs",
|
||||
"wagtail.wagtailimages",
|
||||
"wagtail.wagtailsearch",
|
||||
"wagtail.wagtailadmin",
|
||||
"wagtail.wagtailcore",
|
||||
"wagtail.contrib.modeladmin",
|
||||
"wagtail.contrib.wagtailroutablepage",
|
||||
"wagtailmenus",
|
||||
"wagtail_modeltranslation",
|
||||
"modelcluster",
|
||||
"taggit",
|
||||
"kfet.auth",
|
||||
"kfet.cms",
|
||||
"gestioncof.cms",
|
||||
]
|
||||
|
||||
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'kfet.auth.middleware.TemporaryAuthMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'djconfig.middleware.DjConfigMiddleware',
|
||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.auth.middleware.SessionAuthenticationMiddleware",
|
||||
"kfet.auth.middleware.TemporaryAuthMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"djconfig.middleware.DjConfigMiddleware",
|
||||
"wagtail.wagtailcore.middleware.SiteMiddleware",
|
||||
"wagtail.wagtailredirects.middleware.RedirectMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "cof.urls"
|
||||
|
@ -167,10 +168,7 @@ USE_L10N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = (
|
||||
('fr', 'Français'),
|
||||
('en', 'English'),
|
||||
)
|
||||
LANGUAGES = (("fr", "Français"), ("en", "English"))
|
||||
|
||||
# Various additional settings
|
||||
SITE_ID = 1
|
||||
|
|
|
@ -21,7 +21,6 @@ from gestioncof.urls import (
|
|||
clubs_patterns,
|
||||
events_patterns,
|
||||
export_patterns,
|
||||
petitcours_patterns,
|
||||
surveys_patterns,
|
||||
)
|
||||
|
||||
|
@ -35,7 +34,7 @@ urlpatterns = [
|
|||
# Les exports
|
||||
url(r"^export/", include(export_patterns)),
|
||||
# Les petits cours
|
||||
url(r"^petitcours/", include(petitcours_patterns)),
|
||||
url(r"^petitcours/", include("petitscours.urls")),
|
||||
# Les sondages
|
||||
url(r"^survey/", include(surveys_patterns)),
|
||||
# Evenements
|
||||
|
@ -134,6 +133,5 @@ if settings.DEBUG:
|
|||
|
||||
# Wagtail for uncatched
|
||||
urlpatterns += i18n_patterns(
|
||||
url(r'', include(wagtail_urls)),
|
||||
prefix_default_language=False
|
||||
url(r"", include(wagtail_urls)), prefix_default_language=False
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ from gestioncof.models import (
|
|||
SurveyQuestion,
|
||||
SurveyQuestionAnswer,
|
||||
)
|
||||
from gestioncof.petits_cours_models import (
|
||||
from petitscours.models import (
|
||||
PetitCoursAbility,
|
||||
PetitCoursAttribution,
|
||||
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):
|
||||
name = 'gestioncof.cms'
|
||||
label = 'cofcms'
|
||||
verbose_name = 'CMS COF'
|
||||
name = "gestioncof.cms"
|
||||
label = "cofcms"
|
||||
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
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import wagtail.contrib.wagtailroutablepage.models
|
||||
import wagtail.wagtailcore.blocks
|
||||
import wagtail.wagtailcore.fields
|
||||
import wagtail.wagtailimages.blocks
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0039_collectionviewrestriction'),
|
||||
('cofcms', '0001_initial'),
|
||||
("wagtailcore", "0039_collectionviewrestriction"),
|
||||
("cofcms", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='COFUtilPage',
|
||||
name="COFUtilPage",
|
||||
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={
|
||||
'verbose_name': 'Page utilitaire',
|
||||
'verbose_name_plural': 'Pages utilitaires',
|
||||
"verbose_name": "Page utilitaire",
|
||||
"verbose_name_plural": "Pages utilitaires",
|
||||
},
|
||||
bases=(wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin, 'wagtailcore.page'),
|
||||
bases=(
|
||||
wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin,
|
||||
"wagtailcore.page",
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cofdirectoryentrypage',
|
||||
options={'verbose_name': "Entrée d'annuaire", 'verbose_name_plural': "Entrées d'annuaire"},
|
||||
name="cofdirectoryentrypage",
|
||||
options={
|
||||
"verbose_name": "Entrée d'annuaire",
|
||||
"verbose_name_plural": "Entrées d'annuaire",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cofdirectorypage',
|
||||
name='alphabetique',
|
||||
field=models.BooleanField(default=True, verbose_name='Tri par ordre alphabétique ?'),
|
||||
model_name="cofdirectorypage",
|
||||
name="alphabetique",
|
||||
field=models.BooleanField(
|
||||
default=True, verbose_name="Tri par ordre alphabétique ?"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cofpage',
|
||||
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)'))))))),
|
||||
model_name="cofpage",
|
||||
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)"
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cofpage',
|
||||
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),
|
||||
model_name="cofpage",
|
||||
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,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cofpage',
|
||||
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),
|
||||
model_name="cofpage",
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -13,7 +13,7 @@ from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
|
|||
class COFActuIndexMixin:
|
||||
@property
|
||||
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
|
||||
|
||||
|
||||
|
@ -22,10 +22,10 @@ class COFRootPage(Page, COFActuIndexMixin):
|
|||
introduction = RichTextField("Introduction")
|
||||
|
||||
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:
|
||||
verbose_name = "Racine site du COF"
|
||||
|
@ -45,19 +45,19 @@ class IFrameBlock(blocks.StructBlock):
|
|||
|
||||
# Page lambda du site
|
||||
class COFPage(Page):
|
||||
body = StreamField([
|
||||
('heading', blocks.CharBlock(classname="full title")),
|
||||
('paragraph', blocks.RichTextBlock()),
|
||||
('image', ImageChooserBlock()),
|
||||
('iframe', IFrameBlock()),
|
||||
])
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
StreamFieldPanel('body'),
|
||||
body = StreamField(
|
||||
[
|
||||
("heading", blocks.CharBlock(classname="full title")),
|
||||
("paragraph", blocks.RichTextBlock()),
|
||||
("image", ImageChooserBlock()),
|
||||
("iframe", IFrameBlock()),
|
||||
]
|
||||
)
|
||||
|
||||
subpage_types = ['COFDirectoryPage', 'COFPage']
|
||||
parent_page_types = ['COFPage', 'COFRootPage']
|
||||
content_panels = Page.content_panels + [StreamFieldPanel("body")]
|
||||
|
||||
subpage_types = ["COFDirectoryPage", "COFPage"]
|
||||
parent_page_types = ["COFPage", "COFRootPage"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page normale COF"
|
||||
|
@ -66,8 +66,8 @@ class COFPage(Page):
|
|||
|
||||
# Actualités
|
||||
class COFActuIndexPage(Page, COFActuIndexMixin):
|
||||
subpage_types = ['COFActuPage']
|
||||
parent_page_types = ['COFRootPage']
|
||||
subpage_types = ["COFActuPage"]
|
||||
parent_page_types = ["COFRootPage"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Index des actualités"
|
||||
|
@ -75,9 +75,9 @@ class COFActuIndexPage(Page, COFActuIndexMixin):
|
|||
|
||||
def get_context(self, 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)
|
||||
try:
|
||||
actus = paginator.page(page)
|
||||
|
@ -86,7 +86,7 @@ class COFActuIndexPage(Page, COFActuIndexMixin):
|
|||
except EmptyPage:
|
||||
actus = paginator.page(paginator.num_pages)
|
||||
|
||||
context['actus'] = actus
|
||||
context["actus"] = actus
|
||||
return context
|
||||
|
||||
|
||||
|
@ -94,20 +94,24 @@ class COFActuPage(RoutablePageMixin, Page):
|
|||
chapo = models.TextField("Description rapide", blank=True)
|
||||
body = RichTextField("Contenu")
|
||||
image = models.ForeignKey(
|
||||
'wagtailimages.Image', verbose_name="Image à la Une",
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='+'
|
||||
"wagtailimages.Image",
|
||||
verbose_name="Image à la Une",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
)
|
||||
is_event = models.BooleanField("Évènement", default=True, blank=True)
|
||||
date_start = models.DateTimeField("Date et heure de début")
|
||||
date_end = models.DateTimeField("Date et heure de fin", blank=True,
|
||||
default=None, null=True)
|
||||
date_end = models.DateTimeField(
|
||||
"Date et heure de fin", blank=True, default=None, null=True
|
||||
)
|
||||
all_day = models.BooleanField("Toute la journée", default=False, blank=True)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
ImageChooserPanel('image'),
|
||||
FieldPanel('chapo'),
|
||||
FieldPanel('body', classname="full"),
|
||||
ImageChooserPanel("image"),
|
||||
FieldPanel("chapo"),
|
||||
FieldPanel("body", classname="full"),
|
||||
FieldPanel("is_event"),
|
||||
FieldPanel("date_start"),
|
||||
FieldPanel("date_end"),
|
||||
|
@ -115,7 +119,7 @@ class COFActuPage(RoutablePageMixin, Page):
|
|||
]
|
||||
|
||||
subpage_types = []
|
||||
parent_page_types = ['COFActuIndexPage']
|
||||
parent_page_types = ["COFActuIndexPage"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Actualité"
|
||||
|
@ -125,21 +129,21 @@ class COFActuPage(RoutablePageMixin, Page):
|
|||
# Annuaires (Clubs, partenaires, bonnes adresses)
|
||||
class COFDirectoryPage(Page):
|
||||
introduction = RichTextField("Introduction")
|
||||
alphabetique = models.BooleanField("Tri par ordre alphabétique ?",
|
||||
default=True, blank=True)
|
||||
alphabetique = models.BooleanField(
|
||||
"Tri par ordre alphabétique ?", default=True, blank=True
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel('introduction'),
|
||||
FieldPanel('alphabetique'),
|
||||
FieldPanel("introduction"),
|
||||
FieldPanel("alphabetique"),
|
||||
]
|
||||
|
||||
subpage_types = ['COFActuPage', 'COFDirectoryEntryPage']
|
||||
parent_page_types = ['COFRootPage', 'COFPage']
|
||||
subpage_types = ["COFActuPage", "COFDirectoryEntryPage"]
|
||||
parent_page_types = ["COFRootPage", "COFPage"]
|
||||
|
||||
@property
|
||||
def entries(self):
|
||||
entries = COFDirectoryEntryPage.objects.live()\
|
||||
.descendant_of(self)
|
||||
entries = COFDirectoryEntryPage.objects.live().descendant_of(self)
|
||||
if self.alphabetique:
|
||||
entries = entries.order_by("title")
|
||||
return entries
|
||||
|
@ -151,31 +155,46 @@ class COFDirectoryPage(Page):
|
|||
|
||||
class COFDirectoryEntryPage(Page):
|
||||
body = RichTextField("Description")
|
||||
links = StreamField([
|
||||
('lien', blocks.StructBlock([
|
||||
('url', blocks.URLBlock(required=True)),
|
||||
('texte', blocks.CharBlock()),
|
||||
])),
|
||||
('contact', blocks.StructBlock([
|
||||
('email', blocks.EmailBlock(required=True)),
|
||||
('texte', blocks.CharBlock()),
|
||||
])),
|
||||
])
|
||||
links = StreamField(
|
||||
[
|
||||
(
|
||||
"lien",
|
||||
blocks.StructBlock(
|
||||
[
|
||||
("url", blocks.URLBlock(required=True)),
|
||||
("texte", blocks.CharBlock()),
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"contact",
|
||||
blocks.StructBlock(
|
||||
[
|
||||
("email", blocks.EmailBlock(required=True)),
|
||||
("texte", blocks.CharBlock()),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
image = models.ForeignKey(
|
||||
'wagtailimages.Image', verbose_name="Image",
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL, related_name='+'
|
||||
"wagtailimages.Image",
|
||||
verbose_name="Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
ImageChooserPanel('image'),
|
||||
FieldPanel('body', classname="full"),
|
||||
ImageChooserPanel("image"),
|
||||
FieldPanel("body", classname="full"),
|
||||
StreamFieldPanel("links"),
|
||||
]
|
||||
|
||||
subpage_types = []
|
||||
parent_page_types = ['COFDirectoryPage']
|
||||
parent_page_types = ["COFDirectoryPage"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Entrée d'annuaire"
|
||||
|
@ -186,9 +205,10 @@ class COFDirectoryEntryPage(Page):
|
|||
class COFUtilPage(RoutablePageMixin, Page):
|
||||
|
||||
# Mini calendrier
|
||||
@route(r'^calendar/(\d+)/(\d+)/$')
|
||||
@route(r"^calendar/(\d+)/(\d+)/$")
|
||||
def calendar(self, request, year, month):
|
||||
from .views import raw_calendar_view
|
||||
|
||||
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
|
||||
et modeltranslation
|
||||
"""
|
||||
|
||||
def debugged_get_url(self, request):
|
||||
parent = COFRootPage.objects.parent_of(self).live().first()
|
||||
burl = parent.relative_url(request.site)
|
||||
|
|
|
@ -11,7 +11,7 @@ register = template.Library()
|
|||
|
||||
@register.filter()
|
||||
def obfuscate_mail(value):
|
||||
val = value.replace('', '-').replace('@', 'arbre').replace('.', 'pont')[1:]
|
||||
val = value.replace("", "-").replace("@", "arbre").replace(".", "pont")[1:]
|
||||
return val
|
||||
|
||||
|
||||
|
@ -27,11 +27,11 @@ def calendar(context, month=None, year=None):
|
|||
prev_month = month_start - timedelta(days=2)
|
||||
month_prestart = month_start - timedelta(days=month_start.weekday())
|
||||
month_postend = next_month + timedelta(days=(next_month.weekday() + 6) % 7)
|
||||
events = COFActuPage.objects.live()\
|
||||
.filter(date_start__range=[month_prestart,
|
||||
month_postend],
|
||||
is_event=True)\
|
||||
.order_by('-date_start')
|
||||
events = (
|
||||
COFActuPage.objects.live()
|
||||
.filter(date_start__range=[month_prestart, month_postend], is_event=True)
|
||||
.order_by("-date_start")
|
||||
)
|
||||
events = list(events)
|
||||
weeks = []
|
||||
curday = month_prestart
|
||||
|
@ -48,14 +48,23 @@ def calendar(context, month=None, year=None):
|
|||
del events[k]
|
||||
else:
|
||||
curevents.append(e)
|
||||
day = {'day': curday.day,
|
||||
'date': curday,
|
||||
'class': (('today ' if curday == now.date() else '')
|
||||
+ ('in ' if (curday.month == month_start.month
|
||||
and curday.year == month_start.year)
|
||||
else 'out ')
|
||||
+ ('hasevent' if len(curevents) > 0 else '')),
|
||||
'events': curevents}
|
||||
day = {
|
||||
"day": curday.day,
|
||||
"date": curday,
|
||||
"class": (
|
||||
("today " if curday == now.date() else "")
|
||||
+ (
|
||||
"in "
|
||||
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)
|
||||
curday += deltaday
|
||||
weeks.append(week)
|
||||
|
@ -65,16 +74,23 @@ def calendar(context, month=None, year=None):
|
|||
utilpage = COFUtilPage.objects.live()[0]
|
||||
except COFUtilPage.DoesNotExist:
|
||||
utilpage = None
|
||||
request = context['request']
|
||||
request = context["request"]
|
||||
burl = utilpage.debugged_get_url(request) + "/"
|
||||
prev_url = burl + utilpage.reverse_subpage("calendar",
|
||||
args=[str(prev_month.year),
|
||||
str(prev_month.month)])
|
||||
next_url = burl + utilpage.reverse_subpage("calendar",
|
||||
args=[str(next_month.year),
|
||||
str(next_month.month)])
|
||||
context.push({"events": events, "weeks": weeks, "this_month": month_start,
|
||||
"prev_month": prev_url, "next_month": next_url})
|
||||
prev_url = burl + utilpage.reverse_subpage(
|
||||
"calendar", args=[str(prev_month.year), str(prev_month.month)]
|
||||
)
|
||||
next_url = burl + utilpage.reverse_subpage(
|
||||
"calendar", args=[str(next_month.year), str(next_month.month)]
|
||||
)
|
||||
context.push(
|
||||
{
|
||||
"events": events,
|
||||
"weeks": weeks,
|
||||
"this_month": month_start,
|
||||
"prev_month": prev_url,
|
||||
"next_month": next_url,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
|
@ -87,9 +103,13 @@ def mini_calendar(event):
|
|||
week_start = date_start - timedelta(days=date_start.weekday())
|
||||
curday = week_start
|
||||
for i in range(7):
|
||||
days.append({'day': curday.day,
|
||||
'hasevent': curday >= date_start and curday <= date_end,
|
||||
'today': curday == today})
|
||||
days.append(
|
||||
{
|
||||
"day": curday.day,
|
||||
"hasevent": curday >= date_start and curday <= date_end,
|
||||
"today": curday == today,
|
||||
}
|
||||
)
|
||||
curday += timedelta(days=1)
|
||||
return {"days": days}
|
||||
|
||||
|
@ -102,7 +122,7 @@ def dates(event):
|
|||
while i > imin and a[i] == b[i]:
|
||||
i -= 1
|
||||
if i == -1:
|
||||
return (a, b, '')
|
||||
return (a, b, "")
|
||||
else:
|
||||
return (a[: i + 1], b[: i + 1], a[i + 1 :])
|
||||
|
||||
|
@ -113,22 +133,27 @@ def dates(event):
|
|||
if event.all_day:
|
||||
return _("le %s") % datestart_string
|
||||
else:
|
||||
return _("le %s de %s à %s") % \
|
||||
(datestart_string,
|
||||
return _("le %s de %s à %s") % (
|
||||
datestart_string,
|
||||
timestart_string,
|
||||
formats.time_format(event.date_end))
|
||||
formats.time_format(event.date_end),
|
||||
)
|
||||
else:
|
||||
dateend_string = formats.date_format(event.date_end)
|
||||
diffstart, diffend, common = factorize_suffix(datestart_string,
|
||||
dateend_string)
|
||||
diffstart, diffend, common = factorize_suffix(
|
||||
datestart_string, dateend_string
|
||||
)
|
||||
if event.all_day:
|
||||
return _("du %s au %s%s") % \
|
||||
(diffstart, diffend, common)
|
||||
return _("du %s au %s%s") % (diffstart, diffend, common)
|
||||
|
||||
else:
|
||||
return _("du %s%s à %s au %s à %s") % \
|
||||
(diffstart, common, timestart_string,
|
||||
diffend, formats.time_format(event.date_end))
|
||||
return _("du %s%s à %s au %s à %s") % (
|
||||
diffstart,
|
||||
common,
|
||||
timestart_string,
|
||||
diffend,
|
||||
formats.time_format(event.date_end),
|
||||
)
|
||||
else:
|
||||
if event.all_day:
|
||||
return _("le %s") % datestart_string
|
||||
|
|
|
@ -13,42 +13,29 @@ from .models import (
|
|||
|
||||
@register(COFRootPage)
|
||||
class COFRootPageTr(WagtailTranslationOptions):
|
||||
fields = (
|
||||
'introduction',
|
||||
)
|
||||
fields = ("introduction",)
|
||||
|
||||
|
||||
@register(COFPage)
|
||||
class COFPageTr(WagtailTranslationOptions):
|
||||
fields = (
|
||||
'body',
|
||||
)
|
||||
fields = ("body",)
|
||||
|
||||
|
||||
@register(COFActuIndexPage)
|
||||
class COFActuIndexPageTr(WagtailTranslationOptions):
|
||||
fields = (
|
||||
)
|
||||
fields = ()
|
||||
|
||||
|
||||
@register(COFActuPage)
|
||||
class COFActuPageTr(WagtailTranslationOptions):
|
||||
fields = (
|
||||
'chapo',
|
||||
'body',
|
||||
)
|
||||
fields = ("chapo", "body")
|
||||
|
||||
|
||||
@register(COFDirectoryPage)
|
||||
class COFDirectoryPageTr(WagtailTranslationOptions):
|
||||
fields = (
|
||||
'introduction',
|
||||
)
|
||||
fields = ("introduction",)
|
||||
|
||||
|
||||
@register(COFDirectoryEntryPage)
|
||||
class COFDirectoryEntryPageTr(WagtailTranslationOptions):
|
||||
fields = (
|
||||
'body',
|
||||
'links',
|
||||
)
|
||||
fields = ("body", "links")
|
||||
|
|
|
@ -2,5 +2,4 @@ from django.shortcuts import render
|
|||
|
||||
|
||||
def raw_calendar_view(request, year, month):
|
||||
return render(request, "cofcms/calendar_raw.html",
|
||||
{"month": month, "year": year})
|
||||
return render(request, "cofcms/calendar_raw.html", {"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 cof_required(view_func):
|
||||
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du COF.
|
||||
|
||||
- Si l'utilisteur n'est pas connecté, il est redirigé vers la page de
|
||||
connexion
|
||||
- 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:
|
||||
profile = user.profile
|
||||
return profile.is_cof
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
cof_required = user_passes_test(is_cof)
|
||||
return render(request, "cof-denied.html", status=403)
|
||||
|
||||
return login_required(_wrapped_view)
|
||||
|
||||
|
||||
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):
|
||||
try:
|
||||
profile = user.profile
|
||||
return profile.is_buro
|
||||
except Exception:
|
||||
return user.profile.is_buro
|
||||
except AttributeError:
|
||||
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 gestioncof.management.base import MyBaseCommand
|
||||
from gestioncof.petits_cours_models import (
|
||||
from petitscours.models import (
|
||||
LEVELS_CHOICES,
|
||||
PetitCoursAbility,
|
||||
PetitCoursAttributionCounter,
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.dispatch import receiver
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
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")))
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ def messages_on_out_login(request, user, **kwargs):
|
|||
|
||||
|
||||
@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(
|
||||
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",
|
||||
"email": "username@mail.net",
|
||||
"is_cof": "1",
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -111,7 +111,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
|||
"email": "user@mail.net",
|
||||
"is_cof": "1",
|
||||
"user_exists": "1",
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -137,7 +137,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
|||
|
||||
data = dict(
|
||||
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:
|
||||
data["is_cof"] = "1"
|
||||
|
@ -197,7 +197,7 @@ class RegistrationViewTests(ViewTestCaseMixin, TestCase):
|
|||
"events-0-option_{}".format(o2.pk): [str(oc3.pk)],
|
||||
"events-0-comment_{}".format(cf1.pk): "comment 1",
|
||||
"events-0-comment_{}".format(cf2.pk): "",
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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.petits_cours_views import DemandeDetailView, DemandeListView
|
||||
|
||||
export_patterns = [
|
||||
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"),
|
||||
]
|
||||
|
||||
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 = [
|
||||
url(
|
||||
r"^(?P<survey_id>\d+)/status$",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
from unittest import mock
|
||||
|
||||
|
@ -8,7 +9,7 @@ from django.contrib.auth.models import AnonymousUser, Permission, User
|
|||
from django.test import Client
|
||||
from django.utils import timezone
|
||||
|
||||
from . import OpenKfet, kfet_open
|
||||
from . import OpenKfet
|
||||
from .consumers import OpenKfetConsumer
|
||||
|
||||
|
||||
|
@ -16,10 +17,10 @@ class OpenKfetTest(ChannelTestCase):
|
|||
"""OpenKfet object unit-tests suite."""
|
||||
|
||||
def setUp(self):
|
||||
self.kfet_open = OpenKfet()
|
||||
|
||||
def tearDown(self):
|
||||
self.kfet_open.clear_cache()
|
||||
self.kfet_open = OpenKfet(
|
||||
cache_prefix="test_kfetopen_%s" % threading.get_ident()
|
||||
)
|
||||
self.addCleanup(self.kfet_open.clear_cache)
|
||||
|
||||
def test_defaults(self):
|
||||
"""Default values."""
|
||||
|
@ -136,8 +137,14 @@ class OpenKfetViewsTest(ChannelTestCase):
|
|||
self.c_a = Client()
|
||||
self.c_a.login(username="admin", password="admin")
|
||||
|
||||
def tearDown(self):
|
||||
kfet_open.clear_cache()
|
||||
self.kfet_open = OpenKfet(
|
||||
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):
|
||||
"""Edit raw_status."""
|
||||
|
@ -146,14 +153,14 @@ class OpenKfetViewsTest(ChannelTestCase):
|
|||
"/k-fet/open/raw_open", {"raw_open": sent, "token": "plop"}
|
||||
)
|
||||
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):
|
||||
"""Edit force_close."""
|
||||
for sent, expected in [(1, True), (0, False)]:
|
||||
resp = self.c_a.post("/k-fet/open/force_close", {"force_close": sent})
|
||||
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):
|
||||
"""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.force_login(self.r)
|
||||
|
||||
def tearDown(self):
|
||||
kfet_open.clear_cache()
|
||||
self.kfet_open = OpenKfet(
|
||||
cache_prefix="test_kfetopen_%s" % threading.get_ident()
|
||||
)
|
||||
self.addCleanup(self.kfet_open.clear_cache)
|
||||
|
||||
def ws_connect(self, ws_client):
|
||||
ws_client.send_and_consume(
|
||||
|
@ -288,8 +297,8 @@ class OpenKfetScenarioTest(ChannelTestCase):
|
|||
|
||||
def test_scenario_2(self):
|
||||
"""Starting falsely closed, clients connect, disable force close."""
|
||||
kfet_open.raw_open = True
|
||||
kfet_open.force_close = True
|
||||
self.kfet_open.raw_open = True
|
||||
self.kfet_open.force_close = True
|
||||
|
||||
msg = self.ws_connect(self.c_ws)
|
||||
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.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -5,9 +8,16 @@ from django.test import TestCase
|
|||
|
||||
from gestioncof.models import CofProfile
|
||||
|
||||
from ..models import Account
|
||||
from ..models import Account, Article, ArticleCategory, Checkout, Operation
|
||||
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()
|
||||
|
||||
|
@ -86,3 +96,80 @@ class PermHelpersTest(TestCaseMixin, TestCase):
|
|||
map(repr, [self.perm1, self.perm2, self.perm_team]),
|
||||
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,
|
||||
)
|
||||
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):
|
||||
|
@ -2952,6 +2961,21 @@ class KPsulPerformOperationsViewTests(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_expected = "/k-fet/k-psul/cancel_operations"
|
||||
|
||||
|
@ -2960,8 +2984,790 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase):
|
|||
auth_user = "team"
|
||||
auth_forbidden = [None, "user"]
|
||||
|
||||
def test_ok(self):
|
||||
pass
|
||||
with_liq = True
|
||||
|
||||
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):
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
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()
|
||||
|
||||
|
@ -184,3 +198,180 @@ def user_add_perms(user, perms_labels):
|
|||
# it to avoid using of the previous permissions cache.
|
||||
# https://docs.djangoproject.com/en/dev/topics/auth/default/#permission-caching
|
||||
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.models import BaseInlineFormSet, inlineformset_factory
|
||||
|
||||
from gestioncof.petits_cours_models import PetitCoursAbility, PetitCoursDemande
|
||||
from petitscours.models import PetitCoursAbility, PetitCoursDemande
|
||||
|
||||
|
||||
class BaseMatieresFormSet(BaseInlineFormSet):
|
|
@ -3,6 +3,7 @@ from functools import reduce
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import Min
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
|
@ -16,6 +17,7 @@ LEVELS_CHOICES = (
|
|||
("prepa1styear", _("Prépa 1ère année / L1")),
|
||||
("prepa2ndyear", _("Prépa 2ème année / L2")),
|
||||
("licence3", _("Licence 3")),
|
||||
("master1", _("Master (1ère ou 2ème année)")),
|
||||
("other", _("Autre (préciser dans les commentaires)")),
|
||||
)
|
||||
|
||||
|
@ -27,6 +29,7 @@ class PetitCoursSubject(models.Model):
|
|||
)
|
||||
|
||||
class Meta:
|
||||
app_label = "gestioncof"
|
||||
verbose_name = "Matière de 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)
|
||||
|
||||
class Meta:
|
||||
app_label = "gestioncof"
|
||||
verbose_name = "Compétence 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
|
||||
)
|
||||
|
||||
@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):
|
||||
name = models.CharField(_("Nom/prénom"), max_length=200)
|
||||
|
@ -126,7 +136,44 @@ class PetitCoursDemande(models.Model):
|
|||
candidates = candidates.order_by("?").select_related().all()
|
||||
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:
|
||||
app_label = "gestioncof"
|
||||
verbose_name = "Demande 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)
|
||||
|
||||
class Meta:
|
||||
app_label = "gestioncof"
|
||||
verbose_name = "Attribution de petits cours"
|
||||
verbose_name_plural = "Attributions de petits cours"
|
||||
|
||||
|
@ -182,6 +230,7 @@ class PetitCoursAttributionCounter(models.Model):
|
|||
return counter
|
||||
|
||||
class Meta:
|
||||
app_label = "gestioncof"
|
||||
verbose_name = "Compteur d'attribution 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 %}
|
||||
|
||||
{% block page_size %}col-sm-8{% endblock %}
|
||||
|
||||
{% block realcontent %}
|
||||
<h2>Demande de petits cours</h2>
|
||||
{% include "details_demande_petit_cours_infos.html" %}
|
||||
{% include "petitscours/details_demande_infos.html" %}
|
||||
<hr />
|
||||
<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>
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base_title_petitscours.html" %}
|
||||
{% extends "petitscours/base_title.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% 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' %}">
|
||||
{% 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>
|
||||
{% include "inscription-petit-cours-formset.html" %}
|
||||
{% include "petitscours/inscription_formset.html" %}
|
||||
<div class="inscription-bottom">
|
||||
<input type="button" class="btn btn-default pull-right" value="Ajouter une autre matière" id="add_more" />
|
||||
<script>
|
|
@ -1,8 +1,8 @@
|
|||
{% extends "base_title_petitscours.html" %}
|
||||
{% extends "petitscours/base_title.html" %}
|
||||
|
||||
{% block realcontent %}
|
||||
<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 />
|
||||
{% if errors %}
|
||||
<div class="error">
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "base_title_petitscours.html" %}
|
||||
{% extends "petitscours/base_title.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block realcontent %}
|
||||
<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 />
|
||||
<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 :)
|
|
@ -1,4 +1,4 @@
|
|||
{% extends "base_title_petitscours.html" %}
|
||||
{% extends "petitscours/base_title.html" %}
|
||||
|
||||
{% block realcontent %}
|
||||
<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.models import CofProfile
|
||||
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
|
||||
from gestioncof.petits_cours_models import (
|
||||
from petitscours.forms import DemandeForm, MatieresFormSet
|
||||
from petitscours.models import (
|
||||
PetitCoursAbility,
|
||||
PetitCoursAttribution,
|
||||
PetitCoursAttributionCounter,
|
||||
|
@ -27,7 +27,7 @@ class DemandeListView(ListView):
|
|||
queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
|
||||
"traitee", "-id"
|
||||
)
|
||||
template_name = "petits_cours_demandes_list.html"
|
||||
template_name = "petitscours/demande_list.html"
|
||||
paginate_by = 20
|
||||
|
||||
|
||||
|
@ -36,7 +36,7 @@ class DemandeDetailView(DetailView):
|
|||
queryset = PetitCoursDemande.objects.prefetch_related(
|
||||
"petitcoursattribution_set", "matieres"
|
||||
)
|
||||
template_name = "gestioncof/details_demande_petit_cours.html"
|
||||
template_name = "petitscours/demande_detail.html"
|
||||
context_object_name = "demande"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -53,64 +53,27 @@ def traitement(request, demande_id, redo=False):
|
|||
return _traitement_other(request, demande, redo)
|
||||
if request.method == "POST":
|
||||
return _traitement_post(request, demande)
|
||||
proposals = {}
|
||||
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)
|
||||
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)
|
||||
proposals, unsatisfied = demande.get_proposals(redo=redo, max_candidates=3)
|
||||
return _finalize_traitement(request, demande, proposals, unsatisfied, redo)
|
||||
|
||||
|
||||
def _finalize_traitement(
|
||||
request,
|
||||
demande,
|
||||
proposals,
|
||||
proposed_for,
|
||||
unsatisfied,
|
||||
attribdata,
|
||||
redo=False,
|
||||
errors=None,
|
||||
request, demande, proposals, unsatisfied, redo=False, errors=None
|
||||
):
|
||||
proposals = proposals.items()
|
||||
proposed_for = proposed_for.items()
|
||||
attribdata = list(attribdata.items())
|
||||
attribdata = [
|
||||
(matiere.id, [user.id for user in users])
|
||||
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)
|
||||
mainmail = render_custom_mail(
|
||||
"petits-cours-mail-demandeur",
|
||||
{
|
||||
"proposals": proposals,
|
||||
"proposals": proposals.items(),
|
||||
"unsatisfied": unsatisfied,
|
||||
"extra": '<textarea name="extra" '
|
||||
'style="width:99%; height: 90px;">'
|
||||
|
@ -122,12 +85,12 @@ def _finalize_traitement(
|
|||
messages.error(request, error)
|
||||
return render(
|
||||
request,
|
||||
"gestioncof/traitement_demande_petit_cours.html",
|
||||
"petitscours/traitement_demande.html",
|
||||
{
|
||||
"demande": demande,
|
||||
"unsatisfied": unsatisfied,
|
||||
"proposals": proposals,
|
||||
"proposed_for": proposed_for,
|
||||
"proposals": proposals.items(),
|
||||
"proposed_for": proposed_for.items(),
|
||||
"proposed_mails": proposed_mails,
|
||||
"mainmail": mainmail,
|
||||
"attribdata": json.dumps(attribdata),
|
||||
|
@ -144,7 +107,7 @@ def _generate_eleve_email(demande, proposed_for):
|
|||
"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
|
||||
unsatisfied = []
|
||||
proposals = {}
|
||||
proposed_for = {}
|
||||
attribdata = {}
|
||||
errors = []
|
||||
for matiere, candidates in demande.get_candidates(redo):
|
||||
if candidates:
|
||||
candidates = dict(
|
||||
[(candidate.user.id, candidate.user) for candidate in candidates]
|
||||
)
|
||||
attribdata[matiere.id] = []
|
||||
proposals[matiere] = []
|
||||
for choice_id in range(min(3, len(candidates))):
|
||||
choice = int(
|
||||
|
@ -183,11 +143,6 @@ def _traitement_other_preparing(request, demande):
|
|||
)
|
||||
continue
|
||||
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]:
|
||||
errors.append("Aucune proposition pour {!s}".format(matiere))
|
||||
elif len(proposals[matiere]) < 3:
|
||||
|
@ -200,15 +155,7 @@ def _traitement_other_preparing(request, demande):
|
|||
)
|
||||
else:
|
||||
unsatisfied.append(matiere)
|
||||
return _finalize_traitement(
|
||||
request,
|
||||
demande,
|
||||
proposals,
|
||||
proposed_for,
|
||||
unsatisfied,
|
||||
attribdata,
|
||||
errors=errors,
|
||||
)
|
||||
return _finalize_traitement(request, demande, proposals, unsatisfied, errors=errors)
|
||||
|
||||
|
||||
def _traitement_other(request, demande, redo):
|
||||
|
@ -217,45 +164,14 @@ def _traitement_other(request, demande, redo):
|
|||
return _traitement_other_preparing(request, demande)
|
||||
else:
|
||||
return _traitement_post(request, demande)
|
||||
proposals = {}
|
||||
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()
|
||||
proposals, unsatisfied = demande.get_proposals(redo=redo)
|
||||
return render(
|
||||
request,
|
||||
"gestioncof/traitement_demande_petit_cours_autre_niveau.html",
|
||||
"petitscours/traitement_demande_autre_niveau.html",
|
||||
{
|
||||
"demande": demande,
|
||||
"unsatisfied": unsatisfied,
|
||||
"proposals": proposals,
|
||||
"proposed_for": proposed_for,
|
||||
"proposals": proposals.items(),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -280,12 +196,10 @@ def _traitement_post(request, demande):
|
|||
proposed_for[user] = [matiere]
|
||||
else:
|
||||
proposed_for[user].append(matiere)
|
||||
proposals_list = proposals.items()
|
||||
proposed_for = proposed_for.items()
|
||||
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
||||
mainmail_object, mainmail_body = render_custom_mail(
|
||||
"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"]
|
||||
bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
|
||||
|
@ -314,8 +228,9 @@ def _traitement_post(request, demande):
|
|||
connection = mail.get_connection(fail_silently=False)
|
||||
connection.send_messages(mails_to_send)
|
||||
with transaction.atomic():
|
||||
for matiere in proposals:
|
||||
for rank, user in enumerate(proposals[matiere]):
|
||||
for matiere, users in proposals.items():
|
||||
for rank, user in enumerate(users):
|
||||
# TODO(AD): Prefer PetitCoursAttributionCounter.get_uptodate()
|
||||
counter = PetitCoursAttributionCounter.objects.get(
|
||||
user=user, matiere=matiere
|
||||
)
|
||||
|
@ -331,7 +246,7 @@ def _traitement_post(request, demande):
|
|||
demande.save()
|
||||
return render(
|
||||
request,
|
||||
"gestioncof/traitement_demande_petit_cours_success.html",
|
||||
"petitscours/traitement_demande_success.html",
|
||||
{"demande": demande, "redo": redo},
|
||||
)
|
||||
|
||||
|
@ -361,7 +276,7 @@ def inscription(request):
|
|||
formset = MatieresFormSet(instance=request.user)
|
||||
return render(
|
||||
request,
|
||||
"inscription-petit-cours.html",
|
||||
"petitscours/inscription.html",
|
||||
{
|
||||
"formset": formset,
|
||||
"success": success,
|
||||
|
@ -372,7 +287,7 @@ def inscription(request):
|
|||
|
||||
|
||||
@csrf_exempt
|
||||
def demande(request):
|
||||
def demande(request, *, raw: bool = False):
|
||||
success = False
|
||||
if request.method == "POST":
|
||||
form = DemandeForm(request.POST)
|
||||
|
@ -381,21 +296,7 @@ def demande(request):
|
|||
success = True
|
||||
else:
|
||||
form = DemandeForm()
|
||||
return render(
|
||||
request, "demande-petit-cours.html", {"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}
|
||||
)
|
||||
template_name = "petitscours/demande.html"
|
||||
if raw:
|
||||
template_name = "petitscours/demande_raw.html"
|
||||
return render(request, template_name, {"form": form, "success": success})
|
|
@ -3,9 +3,9 @@ Django==1.11.*
|
|||
django-autocomplete-light==3.1.3
|
||||
django-autoslug==1.9.3
|
||||
django-cas-ng==3.5.7
|
||||
django-djconfig==0.5.3
|
||||
django-djconfig==0.8.0
|
||||
django-recaptcha==1.4.0
|
||||
django-redis-cache==1.7.1
|
||||
django-redis-cache==1.8.1
|
||||
icalendar
|
||||
psycopg2
|
||||
Pillow
|
||||
|
|
|
@ -4,6 +4,7 @@ source =
|
|||
cof
|
||||
gestioncof
|
||||
kfet
|
||||
petitscours
|
||||
shared
|
||||
utils
|
||||
omit =
|
||||
|
@ -33,7 +34,7 @@ default_section = THIRDPARTY
|
|||
force_grid_wrap = 0
|
||||
include_trailing_comma = true
|
||||
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
|
||||
multi_line_output = 3
|
||||
not_skip = __init__.py
|
||||
|
|
Loading…
Reference in a new issue