Compare commits

...

69 commits

Author SHA1 Message Date
0209ad53ca
feat: Merge settings 2024-10-23 20:05:39 +02:00
4d75efe7c5
fix(manage.py): Make executable 2024-10-23 20:05:39 +02:00
d7b80ea06a
feat: Add nix tooling 2024-10-23 20:05:39 +02:00
1491956e30
chore: Rename experiENS -> app 2024-10-23 20:05:39 +02:00
Robin Champenois
25f965c750 debug search 2021-08-29 14:00:27 +02:00
Robin Champenois
db1d35fbb6 Merge branch 'thubrecht/format' into 'master'
Thubrecht/format

See merge request klub-dev-ens/experiENS!15
2021-07-11 20:52:47 +00:00
02f7b3c8c3 Use search views 2021-07-11 21:20:10 +02:00
d9111cc8cb Import settings in dev mode 2021-07-11 21:18:14 +02:00
732a6a08da More updates 2021-06-29 00:11:18 +02:00
32ba0e6111 flake8 2021-06-28 23:58:02 +02:00
b53170feae On enlève les u devant les strings 2021-06-28 23:57:16 +02:00
5bc518eba6 Isort 2021-06-28 23:57:16 +02:00
7cfc85f1fc Black 2021-06-28 23:57:13 +02:00
Robin Champenois
26ad68ff69 Fix search 2021-06-28 23:29:58 +02:00
Robin Champenois
c40a91fb67 Better search 2021-06-28 23:25:16 +02:00
Robin Champenois
46eacc94da Fix search2? 2021-06-28 23:00:32 +02:00
Robin Champenois
370447d355 Fix search? 2021-06-28 22:23:00 +02:00
Robin Champenois
22b5016687 Remove standard useless token filter 2021-06-28 22:09:08 +02:00
Robin Champenois
3c2f93bccb MàJ ElasticSearch 2021-06-28 21:44:47 +02:00
Robin Champenois
1956f38176 Useless import in prod settings 2021-02-07 19:08:29 +01:00
Robin Champenois
9c1092cf8f Merge branch 'maj2021' into 'master'
Mise à jour 2021

Un certain nombre de changements qu'il était temps d'apporter :
- passage à Django 2.2
- basculement de Allauth à AuthENS (=> gestion des adresses mail avec django-simple-email-confirmation)
- statut "en scolarité/archicube" automatique
- débugs divers (carte des lieux, ...)
- mise à jour des dépendances

See merge request klub-dev-ens/experiENS!13
2021-02-07 18:23:24 +01:00
Robin Champenois
e470a2a268 Mise à jour 2021 2021-02-07 18:23:24 +01:00
Evarin
18d1d53c45 Répare la carte 2021-01-01 19:37:58 +01:00
Robin Champenois
2e92d5aa8a Merge branch 'Evarin/hotfix_allauth' into 'master'
Hotfix allauth deprecation clipper

See merge request klub-dev-ens/experiENS!12
2020-01-29 15:20:49 +01:00
Evarin
5726ff2692 Hotfix allauth deprecation clipper 2020-01-29 15:17:57 +01:00
Robin Champenois
45b72a9a77 Merge branch 'kerl/update_readme' into 'master'
Mise à jour de README

See merge request klub-dev-ens/experiENS!7
2020-01-29 15:04:31 +01:00
Martin Pépin
d3f5c3df70
README: add missing mandatory secrets 2019-10-04 23:43:28 +02:00
Evarin
318b1dda78 Hotfix assurer un profil aux gens 2019-08-07 18:14:33 +02:00
Evarin
66c278341c hotfix mail 2019-08-07 17:29:59 +02:00
Evarin
3a65c2a815 Debug Feedback 2019-07-26 17:00:38 +02:00
Evarin
b443073921 Passage à MapBox 2019-02-25 17:17:58 +01:00
Evarin
76c7b1c642 Default FROM email 2019-01-27 23:52:21 +01:00
Evarin
804dc0fb96 Toggle debug toolbar setting 2019-01-14 23:03:15 +01:00
Robin Champenois
092c373f2a Merge branch 'no-es-required' into 'master'
Développer sans ElasticSearch

See merge request klub-dev-ens/experiENS!6
2019-01-14 22:40:10 +01:00
Evarin
c0cbff5070 Meilleure recherche sans elasticsearch 2019-01-14 22:37:13 +01:00
Evarin
8f3c02d292 Possibilité de développer sans ElasticSearch 2019-01-14 22:32:01 +01:00
Evarin
833a8367cf 404 plutôt que 500 sur profil inexistant 2019-01-07 22:11:51 +01:00
Evarin
5275e9036a Formulaire choix de lieu plus clair 2018-12-30 20:33:44 +01:00
Evarin
da3ce8f464 Test obsolescence des comptes 2018-12-30 16:29:36 +01:00
Evarin
5f49ecf270 Fuis la police de Google (ou presque) 2018-12-30 16:23:27 +01:00
Evarin
941294cf93 Remove debug prints 2018-12-30 00:06:43 +01:00
Evarin
40b65c7a7b Ordre des résultats quand la recherche est en cache 2018-12-30 00:03:31 +01:00
Evarin
eb2d4bd274 Taille des titres 2018-12-29 23:42:09 +01:00
Evarin
6ffa35948f Alerte absence de lieu 2018-12-29 23:36:54 +01:00
Robin Champenois
bc17ff9e7c Merge branch 'archicubes' into 'master'
Accès archicubes

See merge request klub-dev-ens/experiENS!5
2018-12-29 23:30:51 +01:00
Evarin
cb7f9187cf Tests de visibilité 2018-12-29 23:23:36 +01:00
Evarin
04f56ec0af Oublis : script et ldap 2018-12-29 16:30:28 +01:00
Evarin
b9e128cafb Optimisations BDD 2018-12-29 16:23:57 +01:00
Evarin
70ff24c708 Script to copy mail addresses 2018-12-29 00:42:35 +01:00
Evarin
d28e195873 CSS et FAQ 2018-12-29 00:23:13 +01:00
Evarin
78f7fd5afd Confidentialité API + debug 2018-12-29 00:12:36 +01:00
Evarin
2b94a28670 Gestion des accès et interface 2018-12-28 23:46:24 +01:00
Evarin
52f574678d Interfaces et scolarité 2018-12-28 00:20:14 +01:00
Evarin
9bdc6c277f Installation d'allauth ENS 2018-12-27 21:05:59 +01:00
Evarin
754034cd57 Fix responsive css 2018-12-27 17:39:52 +01:00
Robin Champenois
85de4f0245 Merge branch 'py3' into 'master'
Passage à python 3

See merge request klub-dev-ens/experiENS!4
2018-12-26 23:39:47 +01:00
Evarin
09f1cb0c91 Minor fix README 2018-12-26 23:39:09 +01:00
Evarin
32e15134e5 Update README 2018-12-26 23:35:38 +01:00
Evarin
a2e3665f50 Requirements prod 2018-12-26 23:33:07 +01:00
Evarin
ca14bf09fc Passage à python 3 2018-12-26 22:00:36 +01:00
Evarin
a3f12a22f8 Indexation lowercase 2018-12-26 21:34:48 +01:00
Robin Champenois
4dc201e572 Merge branch 'Pagination' into 'master'
Pagination + améliorations de la recherche

See merge request klub-dev-ens/experiENS!3
2018-12-26 20:35:07 +01:00
Evarin
78d4d7c624 Journalisation des recherches 2018-12-26 20:23:20 +01:00
Evarin
bdca70964a KDEns 2018-12-26 19:41:57 +01:00
Evarin
e9af6f5cfd Fix things + memorisation interface_mode 2018-12-26 19:39:34 +01:00
Evarin
f9f62bd1b6 Pagination et mise en cache des résultats 2018-12-26 19:06:18 +01:00
Evarin
4c97c8e420 Stats décroissantes 2018-09-09 00:22:28 +02:00
Evarin
d8df1064e7 Fix bug stats 2018-09-09 00:21:10 +02:00
Evarin
c716862c7d Statistiques plus jolies et complètes 2018-09-09 00:17:11 +02:00
94 changed files with 5559 additions and 1852 deletions

1
.credentials/SECRET_KEY Normal file
View file

@ -0,0 +1 @@
insecure-secret-key

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

4
.gitignore vendored
View file

@ -108,6 +108,6 @@ test.py
.#*
*.sqlite3
.sass-cache
/static/
settings.py
secrets.py
.direnv
.pre-commit-config.yaml

View file

@ -3,11 +3,13 @@ ExpériENS : partagez votre stage
ExpériENS est un projet visant à faire un "Annuaire de stage", afin de partager vos ressentis concernant les lieux, les personnes, tout ce qui a fait votre séjour.
Il est visible sur https://www.eleves.ens.fr/experiens/
## Développer sur son ordinateur
Clonez le dépôt. Installez les pré-requis :
sudo apt-get install libxlst-dev python2.7-dev
sudo apt-get install libxlst-dev libsals2-dev libxml2-dev libldap2-dev libssl-dev
On a besoin de SpatiaLite pour une base de données GIS. Essayez
@ -23,15 +25,17 @@ Ensuite, paramétrez les settings :
cd experiENS/
echo 'SECRET_KEY="toto"' > secrets.py
echo 'GOOGLE_API_KEY="toto"' >> secrets.py
echo 'MAPBOX_API_KEY="toto"' >> secrets.py
ln -s settings_dev.py settings.py
cd ../
Enfin, installez les autres dépendances :
virtualenv venv
python3 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
pip install -r requirements_dev.txt
pip install --update pip
pip install -r requirements-dev.txt
python manage.py makemigrations
python manage.py migrate
@ -39,9 +43,13 @@ Vous pouvez alors lancez le serveur de développement
python manage.py runserver
C'est bon, vous pouvez développer sur ExpériENS !
## Configuration de la recherche
Il faut installer elasticsearch 5.*. C'est compliqué. Mais en suivant https://www.elastic.co/guide/en/elasticsearch/reference/5.4/deb.html ça va.
**Cette partie n'est pas obligatoire pour faire fonctionner un serveur de développement en local.** Elle n'est utile que si vous voulez toucher aux fonctionnalités de recherche.
Il faut installer elasticsearch 5.*. C'est compliqué. Mais en suivant https://www.elastic.co/guide/en/elasticsearch/reference/5.4/deb.html c'est faisable.
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
sudo apt-get install apt-transport-https
@ -52,11 +60,13 @@ Il faut installer elasticsearch 5.*. C'est compliqué. Mais en suivant https://w
sudo systemctl enable elasticsearch.service
sudo systemctl start elasticsearch.service
Et puis, de retour dans le virtualenv python
Vous devez ensuite activer ElasticSearch dans vos paramètres locaux, en changeant `USE_ELASTICSEARCH = True` à la fin du fichier `experiENS/settings_dev.py`.
python manage.py search_index --rebuild
Enfin, de retour dans la console et le virtualenv python, vous pouvez faire
Si des erreurs s'affichent, il y a une cachuète dans le beurre.
python manage.py search_index --rebuild
Si des erreurs s'affichent, demandez de l'aide sur Merle ou par e-mail.
## Changer le CSS

13
app/auth.py Normal file
View file

@ -0,0 +1,13 @@
from authens.backends import ENSCASBackend as AuthENSBackend
from authens.utils import parse_entrance_year
class ENSCASBackend(AuthENSBackend):
# Override AuthENS backend user creation to implement the @<promo> logic
def get_free_username(self, cas_login, attributes):
entrance_year = parse_entrance_year(attributes.get("homeDirectory"))
if entrance_year is None:
return super().get_free_username(cas_login, attributes)
entrance_year %= 100
return "%s@%02d" % (cas_login, entrance_year)

242
app/settings.py Normal file
View file

@ -0,0 +1,242 @@
"""
Django settings for the experiENS project
"""
from pathlib import Path
from loadcredential import Credentials
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
credentials = Credentials(env_prefix="EXPERIENS_")
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# WARNING: keep the secret key used in production secret!
SECRET_KEY = credentials["SECRET_KEY"]
# WARNING: don't run with debug turned on in production!
DEBUG = credentials.get_json("DEBUG", False)
ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", [])
ADMINS = credentials.get_json("ADMINS", [])
SITE_ID = 1
###
# ElasticSearch configuration
USE_ELASTICSEARCH = credentials.get_json("USE_ELASTICSEARCH", False)
ELASTICSEARCH_DSL = credentials.get_json(
"ELASTICSEARCH_DSL", {"default": {"hosts": "127.0.0.1:9200"}}
)
###
# Libraries configuration
GDAL_LIBRARY_PATH = credentials.get("GDAL_LIBRARY_PATH")
GEOS_LIBRARY_PATH = credentials.get("GEOS_LIBRARY_PATH")
###
# List the installed applications
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.gis",
"django.contrib.sites",
*(["django_elasticsearch_dsl"] if USE_ELASTICSEARCH else []),
"simple_email_confirmation",
"authens",
"tastypie",
"braces",
"tinymce",
"taggit",
"taggit_autosuggest",
"avisstage",
]
###
# List the installed middlewares
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
###
# The main url configuration
ROOT_URLCONF = "app.urls"
###
# Template configuration:
# - Django Templating Language is used
# - Application directories can be used
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
],
},
},
]
###
# Database configuration
# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
DATABASES = credentials.get_json(
"DATABASES",
{
"default": {
"ENGINE": "django.contrib.gis.db.backends.spatialite",
"NAME": BASE_DIR / "db.sqlite3",
}
},
)
CACHES = credentials.get_json(
"CACHES",
default={
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
},
)
###
# WSGI application configuration
WSGI_APPLICATION = "app.wsgi.application"
###
# Staticfiles configuration
STATIC_ROOT = credentials["STATIC_ROOT"]
STATIC_URL = "/static/"
MEDIA_ROOT = credentials.get("MEDIA_ROOT", BASE_DIR / "media")
MEDIA_URL = "/media/"
###
# Internationalization configuration
# -> https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "fr-fr"
TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
("fr", _("Français")),
]
###
# Authentication configuration
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"experiENS.auth.ENSCASBackend",
]
CAS_SERVER_URL = "https://cas.eleves.ens.fr/" # SPI CAS
AUTHENS_USE_OLDCAS = False
LOGIN_URL = reverse_lazy("authens:login")
LOGOUT_URL = reverse_lazy("authens:logout")
LOGIN_REDIRECT_URL = reverse_lazy("avisstage:perso")
LOGOUT_REDIRECT_URL = reverse_lazy("avisstage:index")
AUTH_PASSWORD_VALIDATORS = [
{"NAME": f"django.contrib.auth.password_validation.{v}"}
for v in [
"UserAttributeSimilarityValidator",
"MinimumLengthValidator",
"CommonPasswordValidator",
"NumericPasswordValidator",
]
]
###
# Logging configuration
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"file": {
"level": "INFO",
"class": "logging.FileHandler",
"filename": credentials.get(
"RECHERCHE_LOG_FILE", BASE_DIR / "recherche.log"
),
},
},
"loggers": {
"recherche": {
"handlers": ["file"],
"level": "INFO",
"propagate": True,
},
},
}
###
# LDAP configuration
CLIPPER_LDAP_SERVER = credentials.get("CLIPPER_LDAP_SERVER", "ldaps://localhost:636")
# Development settings
if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
INSTALLED_APPS += [
"debug_toolbar",
]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE]

18
app/urls.py Normal file
View file

@ -0,0 +1,18 @@
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("avisstage.urls")),
path("authens/", include("authens.urls")),
path("tinymce/", include("tinymce.urls")),
path("taggit_autosuggest/", include("taggit_autosuggest.urls")),
path("admin/", admin.site.urls),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

View file

@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "experiENS.settings")
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
application = get_wsgi_application()

View file

@ -1,34 +1,47 @@
import authens.models as authmod
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from avisstage.models import *
from avisstage.models import AvisLieu, AvisStage, Lieu, Normalien, Stage, StageMatiere
class NormalienInline(admin.StackedInline):
model = Normalien
inline_classes = ("collapse open",)
class UserAdmin(UserAdmin):
inlines = (NormalienInline, )
inlines = (NormalienInline,)
class AvisLieuInline(admin.StackedInline):
model = AvisLieu
inline_classes = ("collapse open",)
extra = 0
class AvisStageInline(admin.StackedInline):
model = AvisStage
inline_classes = ("collapse open",)
extra = 0
class StageAdmin(admin.ModelAdmin):
inlines = (AvisLieuInline, AvisStageInline)
class StageMatiereAdmin(admin.ModelAdmin):
model = StageMatiere
prepopulated_fields = {"slug": ('nom',)}
prepopulated_fields = {"slug": ("nom",)}
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.register(Lieu)
admin.site.register(StageMatiere, StageMatiereAdmin)
admin.site.register(Stage, StageAdmin)
admin.site.register(authmod.CASAccount)
admin.site.register(authmod.OldCASAccount)

View file

@ -1,25 +1,32 @@
# coding: utf-8
from tastypie.resources import ModelResource
from tastypie import fields
from tastypie.authentication import SessionAuthentication
from tastypie import fields, utils
from tastypie.resources import ModelResource
from django.contrib.gis import geos
from django.urls import reverse
from .models import Lieu, Stage, Normalien, StageMatiere
from .models import Lieu, Normalien, Stage
from .utils import approximate_distance
class EnScolariteAuthentication(SessionAuthentication):
def is_authenticated(self, request, **kwargs):
if super().is_authenticated(request, **kwargs):
return request.user.profil.en_scolarite
return False
# API principale pour les lieux
class LieuResource(ModelResource):
stages = fields.ToManyField("avisstage.api.StageResource",
"stages", use_in="detail", full=True)
# stages = fields.ToManyField("avisstage.api.StageResource",
# "stages", use_in="detail", full=True)
class Meta:
queryset = Lieu.objects.all()
resource_name = "lieu"
fields = ["nom", "ville", "pays", "coord", "type_lieu", "id"]
#login_required
# login_required
authentication = SessionAuthentication()
# Filtres personnalisés
@ -30,42 +37,44 @@ class LieuResource(ModelResource):
# Trouver les lieux à proximités d'un point donné
if "lng" in filters and "lat" in filters:
lat = float(filters['lat'])
lng = float(filters['lng'])
pt = geos.Point((lng,lat), srid=4326)
lat = float(filters["lat"])
lng = float(filters["lng"])
pt = geos.Point((lng, lat), srid=4326)
self.reference_point = pt
orm_filters['coord__distance_lte'] = (pt, 10000)
orm_filters["coord__distance_lte"] = (pt, 10000)
# Filtrer les lieux qui ont déjà des stages
if "has_stage" in filters:
orm_filters['stages__public'] = True
orm_filters["stages__public"] = True
return orm_filters
# Custom apply filters pour ajouter le "distinct"
def apply_filters(self, request, applicable_filters):
return self.get_object_list(request).filter(**applicable_filters).distinct()
# Ajout d'informations
def dehydrate(self, bundle):
bundle = super(LieuResource, self).dehydrate(bundle)
obj = bundle.obj
bundle.data['coord'] = {'lat': float(obj.coord.y),
'lng': float(obj.coord.x)}
# Distance au point recherché (inutile en fait)
#if "lat" in bundle.request.GET and "lng" in bundle.request.GET:
# bundle.data['distance'] = self.reference_point.distance(bundle.obj.coord)
obj = bundle.obj
bundle.data["coord"] = {"lat": float(obj.coord.y), "lng": float(obj.coord.x)}
# Distance au point recherché
if "lat" in bundle.request.GET and "lng" in bundle.request.GET:
bundle.data["distance"] = approximate_distance(
self.reference_point, bundle.obj.coord
)
# Autres infos utiles
bundle.data["pays_nom"] = obj.get_pays_display()
bundle.data["type_lieu_nom"] = obj.type_lieu_fancy
# TODO use annotate?
bundle.data["num_stages"] = obj.stages.filter(public=True).count()
bundle.data["num_stages"] = obj.stages.filter(public=True).count()
return bundle
# API sur un stage
class StageResource(ModelResource):
class Meta:
@ -73,8 +82,8 @@ class StageResource(ModelResource):
resource_name = "stage"
fields = ["sujet", "date_debut", "date_fin", "matieres", "id"]
#login_required
authentication = SessionAuthentication()
# login_required
authentication = EnScolariteAuthentication()
# Filtres personnalisés
def build_filters(self, filters=None, **kwargs):
@ -84,9 +93,9 @@ class StageResource(ModelResource):
# Récupération des stages à un lieu donné
if "lieux" in filters:
flieux = map(int, filters['lieux'].split(','))
orm_filters['lieux__id__in'] = flieux
flieux = map(int, filters["lieux"].split(","))
orm_filters["lieux__id__in"] = flieux
return orm_filters
# Informations à ajouter
@ -95,23 +104,27 @@ class StageResource(ModelResource):
obj = bundle.obj
# Affichage des manytomany en condensé
bundle.data['auteur'] = obj.auteur.nom
bundle.data['thematiques'] = list(obj.thematiques.all().values_list("name", flat=True))
bundle.data['matieres'] = list(obj.matieres.all().values_list("nom", flat=True))
bundle.data["auteur"] = obj.auteur.nom
bundle.data["thematiques"] = list(
obj.thematiques.all().values_list("name", flat=True)
)
bundle.data["matieres"] = list(obj.matieres.all().values_list("nom", flat=True))
# Adresse de la fiche de stage
bundle.data['url'] = reverse("avisstage:stage", kwargs={"pk": obj.id});
bundle.data["url"] = reverse("avisstage:stage", kwargs={"pk": obj.id})
return bundle
# Auteurs des fiches (TODO supprimer ?)
class AuteurResource(ModelResource):
stages = fields.ToManyField("avisstage.api.StageResource",
"stages", use_in="detail")
stages = fields.ToManyField(
"avisstage.api.StageResource", "stages", use_in="detail"
)
class Meta:
queryset = Normalien.objects.all()
resource_name = "profil"
fields = ["id", "nom", "stages"]
#login_required
authentication = SessionAuthentication()
# login_required
authentication = EnScolariteAuthentication()

View file

@ -1,7 +1,5 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class AvisstageConfig(AppConfig):
name = 'avisstage'
name = "avisstage"

14
avisstage/decorators.py Normal file
View file

@ -0,0 +1,14 @@
from functools import wraps
from django.shortcuts import redirect
from django.urls import reverse
def en_scolarite_required(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if request.user.profil.en_scolarite:
return view_func(request, *args, **kwargs)
return redirect(reverse("avisstage:403-archicubes"))
return _wrapped_view

View file

@ -1,72 +1,83 @@
from django_elasticsearch_dsl import DocType, Index, fields
from elasticsearch_dsl import analyzer, token_filter, tokenizer
from django_elasticsearch_dsl import Document, Index, fields
from elasticsearch_dsl import analyzer, token_filter
from .models import Stage, AvisStage, AvisLieu
from .models import Stage
from .statics import PAYS_OPTIONS
PAYS_DICT = dict(PAYS_OPTIONS)
stage = Index('stages')
stage.settings(
number_of_shards=1,
number_of_replicas=0
)
stage = Index("stages")
stage.settings(number_of_shards=1, number_of_replicas=0)
text_analyzer = analyzer(
'default',
"default",
tokenizer="standard",
filter=['lowercase', 'standard', 'asciifolding',
token_filter("frstop", type="stop", stopwords="_french_"),
token_filter("frsnow", type="snowball", language="French")])
filter=[
"lowercase",
"asciifolding",
token_filter("frstop", type="stop", stopwords="_french_"),
token_filter("frsnow", type="snowball", language="French"),
],
)
stage.analyzer(text_analyzer)
@stage.doc_type
class StageDocument(DocType):
lieux = fields.ObjectField(properties={
'nom': fields.StringField(),
'ville': fields.StringField(),
'pays': fields.StringField(),
})
auteur = fields.ObjectField(properties={
'nom': fields.StringField(),
})
thematiques = fields.StringField()
matieres = fields.StringField()
class Meta:
class StageDocument(Document):
lieux = fields.ObjectField(
properties={
"nom": fields.TextField(),
"ville": fields.TextField(),
"pays": fields.TextField(),
}
)
auteur = fields.ObjectField(
properties={
"nom": fields.TextField(),
}
)
thematiques = fields.TextField()
matieres = fields.TextField()
class Django:
model = Stage
fields = [
'sujet',
'encadrants',
'type_stage',
'niveau_scol',
'structure',
'date_debut',
'date_fin'
"sujet",
"encadrants",
"type_stage",
"niveau_scol",
"structure",
"date_debut",
"date_fin",
]
def prepare_thematiques(self, instance):
return ", ".join(instance.thematiques.all().values_list("name", flat=True))
return ", ".join(
instance.thematiques.all().values_list("name", flat=True)
).lower()
def prepare_matieres(self, instance):
return ", ".join(instance.matieres.all().values_list("nom", flat=True))
return ", ".join(instance.matieres.all().values_list("nom", flat=True)).lower()
def prepare_niveau_scol(self, instance):
return instance.get_niveau_scol_display()
return instance.get_niveau_scol_display().lower()
def prepare_type_stage(self, instance):
return instance.type_stage_fancy
return instance.type_stage_fancy.lower()
def prepare_date_fin(self, instance):
return instance.date_fin.year
def prepare_date_debut(self, instance):
return instance.date_debut.year
def prepare_sujet(self, instance):
return instance.sujet.lower()
# Hook pour l'affichage des noms de pays
def prepare(self, instance):
data = super(StageDocument, self).prepare(instance)
for lieu in data['lieux']:
lieu['pays'] = PAYS_DICT[lieu['pays']]
for lieu in data["lieux"]:
lieu["pays"] = PAYS_DICT[lieu["pays"]].lower()
return data

View file

@ -1,46 +1,71 @@
# coding: utf-8
import re
import unicodedata
from simple_email_confirmation.models import EmailAddress
from django import forms
from django.contrib.auth.forms import PasswordResetForm
from django.utils import timezone
import re
from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from .models import AvisLieu, AvisStage, Lieu, Stage, User
from .widgets import LatLonField
# Sur-classe utile
class HTMLTrimmerForm(forms.ModelForm):
def clean(self):
# Suppression des espaces blanc avant et après le texte pour les champs html
leading_white = re.compile(r"^( \t\n)*(<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>( \t\n)*)+?", re.IGNORECASE)
trailing_white = re.compile(r"(( \t\n)*<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>)+?( \t\n)*$", re.IGNORECASE)
leading_white = re.compile(
r"^( \t\n)*(<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>( \t\n)*)+?", re.IGNORECASE
)
trailing_white = re.compile(
r"(( \t\n)*<p>(&nbsp;|[ \n\t]|<br[ /]*>)*</p>)+?( \t\n)*$", re.IGNORECASE
)
cleaned_data = super(HTMLTrimmerForm, self).clean()
for (fname, fval) in cleaned_data.items():
# Heuristique : les champs commençant par "avis_" sont des champs html
if fname[:5] == "avis_":
cleaned_data[fname] = leading_white.sub("", trailing_white.sub("", fval))
cleaned_data[fname] = leading_white.sub(
"", trailing_white.sub("", fval)
)
return cleaned_data
# Infos sur un stage
class StageForm(forms.ModelForm):
date_widget = forms.DateInput(attrs={"class":"datepicker",
"placeholder":"JJ/MM/AAAA"})
date_debut = forms.DateField(label=u"Date de début",
input_formats=["%d/%m/%Y"], widget=date_widget)
date_fin = forms.DateField(label=u"Date de fin",
input_formats=["%d/%m/%Y"], widget=date_widget)
date_widget = forms.DateInput(
attrs={"class": "datepicker", "placeholder": "JJ/MM/AAAA"}
)
date_debut = forms.DateField(
label="Date de début", input_formats=["%d/%m/%Y"], widget=date_widget
)
date_fin = forms.DateField(
label="Date de fin", input_formats=["%d/%m/%Y"], widget=date_widget
)
class Meta:
model = Stage
fields = ['sujet', 'date_debut', 'date_fin', 'type_stage', 'niveau_scol', 'thematiques', 'matieres', 'structure', 'encadrants']
fields = [
"sujet",
"date_debut",
"date_fin",
"type_stage",
"niveau_scol",
"thematiques",
"matieres",
"structure",
"encadrants",
]
help_texts = {
"thematiques": u"Mettez une virgule pour valider votre thématique si la suggestion ne correspond pas ou si elle n'existe pas encore",
"structure": u"Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit pas)"
"thematiques": "Mettez une virgule pour valider votre thématique si la suggestion ne "
"correspond pas ou si elle n'existe pas encore",
"structure": "Nom de l'équipe, du laboratoire, de la startup... (si le lieu ne suffit "
"pas)",
}
labels = {
"date_debut": u"Date de début",
"date_debut": "Date de début",
}
def __init__(self, *args, **kwargs):
@ -48,62 +73,158 @@ class StageForm(forms.ModelForm):
if "request" in kwargs:
self.request = kwargs.pop("request")
super(StageForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
# Lors de la création : attribution à l'utilisateur connecté
if self.instance.id is None and hasattr(self, 'request'):
if self.instance.id is None and hasattr(self, "request"):
self.instance.auteur = self.request.user.profil
# Date de modification
self.instance.date_maj = timezone.now()
self.instance.update_stats(False)
stage = super(StageForm, self).save(commit=commit)
return stage
# Sous-formulaire des avis sur le stage
class AvisStageForm(HTMLTrimmerForm):
class Meta:
model = AvisStage
fields = ['chapo', 'avis_sujet', 'avis_ambiance', 'avis_admin', 'avis_prestage', 'les_plus', 'les_moins']
fields = [
"chapo",
"avis_sujet",
"avis_ambiance",
"avis_admin",
"avis_prestage",
"les_plus",
"les_moins",
]
help_texts = {
"chapo": u"\"Trop long, pas lu\" : une accroche résumant ce que vous avez pensé de ce séjour",
"avis_ambiance": u"Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé⋅e ? Aviez-vous un bon contact avec vos encadrant⋅e⋅s ? Y avait-il une bonne ambiance dans l'équipe ?",
"avis_sujet": u"Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, trop facile ?",
"avis_admin": u"Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration de l'établissement vous a-t-elle aidé⋅e ? Étiez-vous rémunéré⋅e ?",
"avis_prestage": u"Comment avez-vous trouvé où aller pour cette expérience ? À quel moment avez-vous commencé à chercher ? Avez-vous eu des entretiens pour obtenir votre place ? Avez-vous eu d'autres pistes, pourquoi avez-vous choisi cette option ?",
"les_plus": u"Les principaux points positifs de cette expérience",
"les_moins": u"Ce qui aurait pu être mieux",
"chapo": (
'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de ce séjour'
),
"avis_ambiance": (
"Avez-vous passé un bon moment à ce travail ? Étiez-vous assez guidé·e ? "
"Aviez-vous un bon contact avec vos encadrant·e·s ? Y avait-il une bonne "
"ambiance dans l'équipe ?"
),
"avis_sujet": (
"Quelle était votre mission ? Qu'en avez-vous retiré ? Le travail "
"correspondait-il à vos attentes ? Était-ce à votre niveau, trop dur, "
"trop facile ?"
),
"avis_admin": (
"Avez-vous commencé votre travail à la date prévue ? Était-ce compliqué "
"d'obtenir les documents nécessaires (visa, contrats, etc) ? L'administration "
"de l'établissement vous a-t-elle aidé·e ? Étiez-vous rémunéré·e ?"
),
"avis_prestage": (
"Comment avez-vous trouvé où aller pour cette expérience ? À quel moment "
"avez-vous commencé à chercher ? Avez-vous eu des entretiens pour obtenir "
"votre place ? Avez-vous eu d'autres pistes, pourquoi avez-vous choisi "
"cette option ?"
),
"les_plus": "Les principaux points positifs de cette expérience",
"les_moins": "Ce qui aurait pu être mieux",
}
class AvisLieuForm(HTMLTrimmerForm):
class Meta:
model = AvisLieu
fields = ['lieu', 'chapo', 'avis_lieustage', 'avis_pratique', 'avis_tourisme', 'les_plus', 'les_moins']
fields = [
"lieu",
"chapo",
"avis_lieustage",
"avis_pratique",
"avis_tourisme",
"les_plus",
"les_moins",
]
help_texts = {
"chapo": u"\"Trop long, pas lu\" : une accroche résumant ce que vous avez pensé de cet endroit",
"avis_lieustage": u"Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments étaient-ils modernes ? Était-il agréable d'y travailler ?",
"avis_pratique": u"Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez apprises sur place qu'il vous aurait été utile de savoir avant de partir ?",
"avis_tourisme": u"Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué des activités sportives ? Est-il facile de faire des rencontres ?",
"les_plus": u"Les meilleures raisons de partir à cet endroit",
"les_moins": u"Ce qui vous a gêné ou manqué là-bas",
}
widgets = {
"lieu": forms.HiddenInput(attrs={"class":"lieu-hidden"})
"chapo": (
'"Trop long, pas lu" : une accroche résumant ce que vous avez pensé de cet endroit'
),
"avis_lieustage": (
"Qu'avez-vous pensé des lieux où vous travailliez ? Les bâtiments "
"étaient-ils modernes ? Était-il agréable d'y travailler ?"
),
"avis_pratique": (
"Avez-vous eu du mal à trouver un logement ? Y-a-t-il des choses que vous avez "
"apprises sur place qu'il vous aurait été utile de savoir avant de partir ?"
),
"avis_tourisme": (
"Y-a-t-il des lieux à visiter dans cette zone ? Avez-vous pratiqué "
"des activités sportives ? Est-il facile de faire des rencontres ?"
),
"les_plus": "Les meilleures raisons de partir à cet endroit",
"les_moins": "Ce qui vous a gêné ou manqué là-bas",
}
widgets = {"lieu": forms.HiddenInput(attrs={"class": "lieu-hidden"})}
# Création d'un nouveau lieu
class LieuForm(forms.ModelForm):
coord = LatLonField()
id = forms.IntegerField(widget=forms.widgets.HiddenInput(), required=False)
class Meta:
model = Lieu
fields = ['id', 'nom', 'type_lieu', 'ville', 'pays', 'coord']
fields = ["id", "nom", "type_lieu", "ville", "pays", "coord"]
# Widget de feedback
class FeedbackForm(forms.Form):
objet = forms.CharField(label="Objet", required=True)
message = forms.CharField(label="Message", required=True, widget=forms.widgets.Textarea())
message = forms.CharField(
label="Message", required=True, widget=forms.widgets.Textarea()
)
# Nouvelle adresse mail
class AdresseEmailForm(forms.Form):
def __init__(self, _user, **kwargs):
self._user = _user
super().__init__(**kwargs)
email = forms.EmailField(
widget=forms.widgets.EmailInput(attrs={"placeholder": "Nouvelle adresse"})
)
def clean_email(self):
email = self.cleaned_data["email"]
if EmailAddress.objects.filter(user=self._user, email=email).exists():
raise forms.ValidationError("Cette adresse est déjà associée à ce compte")
return email
def _unicode_ci_compare(s1, s2):
"""
Perform case-insensitive comparison of two identifiers, using the
recommended algorithm from Unicode Technical Report 36, section
2.11.2(B)(2).
"""
return (
unicodedata.normalize("NFKC", s1).casefold()
== unicodedata.normalize("NFKC", s2).casefold()
)
# (Ré)initialisation du mot de passe
class ReinitMdpForm(PasswordResetForm):
def get_users(self, email):
"""Override default method to allow unusable passwords"""
email_field_name = User.get_email_field_name()
active_users = User._default_manager.filter(
**{
"%s__iexact" % email_field_name: email,
"is_active": True,
}
)
return (
u
for u in active_users
if _unicode_ci_compare(email, getattr(u, email_field_name))
)

View file

@ -1,43 +1,59 @@
#coding: utf-8
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count
from avisstage.models import Stage, Lieu
from django.core.management.base import BaseCommand
from avisstage.models import Lieu
class Command(BaseCommand):
help = 'Nettoie les stages à plusieurs lieux identiques'
help = "Nettoie les stages à plusieurs lieux identiques"
def add_arguments(self, parser):
parser.add_argument('min_lieu', nargs='?', default=0, type=int)
parser.add_argument("min_lieu", nargs="?", default=0, type=int)
parser.add_argument(
'--apply',
action='store_true',
"--apply",
action="store_true",
default=False,
help='Applies the modifications',
help="Applies the modifications",
)
def handle(self, *args, **options):
rundb = False
if options.get('apply', False):
if options.get("apply", False):
rundb = True
else:
print u"Les modifications ne seront pas appliquées"
min_lieu = options.get('min_lieu', 0)
print("Les modifications ne seront pas appliquées")
for lieu in Lieu.objects.filter(id__gte=min_lieu).order_by('-id'):
lproches = Lieu.objects.filter(id__lt=lieu.id, coord__distance_lte=(lieu.coord, 5))
min_lieu = options.get("min_lieu", 0)
for lieu in Lieu.objects.filter(id__gte=min_lieu).order_by("-id"):
lproches = Lieu.objects.filter(
id__lt=lieu.id, coord__distance_lte=(lieu.coord, 5)
)
if len(lproches) == 0:
continue
print u"Doublons possibles pour %s (id=%d, %d avis) :" % (lieu, lieu.id, lieu.avislieu_set.count())
print(
"Doublons possibles pour %s (id=%d, %d avis) :"
% (lieu, lieu.id, lieu.avislieu_set.count())
)
for plieu in lproches:
pprint = u" > %s (id=%d, %d avis)" % (plieu, plieu.id, plieu.avislieu_set.count())
if plieu.nom == lieu.nom and plieu.ville == lieu.ville and plieu.type_lieu == lieu.type_lieu:
print u"%s %s" % (pprint, self.style.SUCCESS(u'-> Suppression'))
pprint = " > %s (id=%d, %d avis)" % (
plieu,
plieu.id,
plieu.avislieu_set.count(),
)
if (
plieu.nom == lieu.nom
and plieu.ville == lieu.ville
and plieu.type_lieu == lieu.type_lieu
):
print("%s %s" % (pprint, self.style.SUCCESS("-> Suppression")))
if rundb:
for avis in plieu.avislieu_set.all():
avis.lieu = lieu
avis.save()
plieu.delete()
else:
print u"%s %s" % (pprint, self.style.WARNING(u'-> À supprimer manuellement'))
self.stdout.write(self.style.SUCCESS(u'Nettoyage des lieux effectué'))
print(
"%s %s"
% (pprint, self.style.WARNING("-> À supprimer manuellement"))
)
self.stdout.write(self.style.SUCCESS("Nettoyage des lieux effectué"))

View file

@ -1,18 +1,19 @@
#coding: utf-8
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from django.db.models import Count
from avisstage.models import Stage, Lieu
from avisstage.models import Stage
class Command(BaseCommand):
help = 'Nettoie les stages à plusieurs lieux identiques'
help = "Nettoie les stages à plusieurs lieux identiques"
def add_arguments(self, parser):
parser.add_argument('min_stage', nargs='?', default=0, type=int)
parser.add_argument("min_stage", nargs="?", default=0, type=int)
parser.add_argument(
'--apply',
action='store_true',
"--apply",
action="store_true",
default=False,
help='Applies the modifications',
help="Applies the modifications",
)
def handle(self, *args, **options):
@ -27,15 +28,16 @@ class Command(BaseCommand):
return length
rundb = False
if options.get('apply', False):
if options.get("apply", False):
rundb = True
else:
print u"Les modifications ne seront pas appliquées"
min_stage = options.get('min_stage', 0)
print("Les modifications ne seront pas appliquées")
for stage in Stage.objects.annotate(c=Count("lieux"))\
.filter(c__gte=2, id__gte=min_stage):
min_stage = options.get("min_stage", 0)
for stage in Stage.objects.annotate(c=Count("lieux")).filter(
c__gte=2, id__gte=min_stage
):
lieuset = {}
todel = []
problems = []
@ -52,15 +54,19 @@ class Command(BaseCommand):
problems += [(avis, alen), lieuset[aid]]
lieuset[aid] = (avis, alen)
if len(todel) > 0:
print u"Doublons détectés dans %s" % (stage,)
print("Doublons détectés dans %s" % (stage,))
for avis, alen in todel:
print u" > Suppression de l'avis sur %s de %d mots" % \
(avis.lieu, alen)
print(
" > Suppression de l'avis sur %s de %d mots" % (avis.lieu, alen)
)
if rundb:
avis.delete()
if len(problems) > 0:
self.stdout.write(self.style.WARNING(u"Réparation impossible de %s (id=%d)" % (stage, stage.id)))
self.stdout.write(
self.style.WARNING(
"Réparation impossible de %s (id=%d)" % (stage, stage.id)
)
)
for avis, alen in problems:
print u" > Avis sur %s de %d mots" % \
(avis.lieu, alen)
self.stdout.write(self.style.SUCCESS(u'Nettoyage des stages effectué'))
print(" > Avis sur %s de %d mots" % (avis.lieu, alen))
self.stdout.write(self.style.SUCCESS("Nettoyage des stages effectué"))

View file

@ -1,35 +1,41 @@
#coding: utf-8
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Count
from avisstage.models import Stage, Lieu
from django.core.management.base import BaseCommand
from avisstage.models import Lieu
class Command(BaseCommand):
help = 'Nettoie les stages à plusieurs lieux identiques'
help = "Nettoie les stages à plusieurs lieux identiques"
def add_arguments(self, parser):
parser.add_argument('del_lieu', type=int, help='Lieu à supprimer')
parser.add_argument('repl_lieu', type=int, help='Lieu le remplaçant')
parser.add_argument("del_lieu", type=int, help="Lieu à supprimer")
parser.add_argument("repl_lieu", type=int, help="Lieu le remplaçant")
parser.add_argument(
'--apply',
action='store_true',
"--apply",
action="store_true",
default=False,
help='Applies the modifications',
help="Applies the modifications",
)
def handle(self, *args, **options):
rundb = False
if options.get('apply', False):
if options.get("apply", False):
rundb = True
else:
print u"Les modifications ne seront pas appliquées"
print("Les modifications ne seront pas appliquées")
plieu = Lieu.objects.get(id=options['del_lieu'])
lieu = Lieu.objects.get(id=options['repl_lieu'])
print u"Suppression de %s (id=%d, %d avis)" % (plieu, plieu.id, plieu.avislieu_set.count())
print u"Remplacement par %s (id=%d, %d avis)" % (lieu, lieu.id, lieu.avislieu_set.count())
plieu = Lieu.objects.get(id=options["del_lieu"])
lieu = Lieu.objects.get(id=options["repl_lieu"])
print(
"Suppression de %s (id=%d, %d avis)"
% (plieu, plieu.id, plieu.avislieu_set.count())
)
print(
"Remplacement par %s (id=%d, %d avis)"
% (lieu, lieu.id, lieu.avislieu_set.count())
)
if rundb:
for avis in plieu.avislieu_set.all():
avis.lieu = lieu
avis.save()
plieu.delete()
self.stdout.write(self.style.SUCCESS(u'Terminé'))
self.stdout.write(self.style.SUCCESS("Terminé"))

View file

@ -0,0 +1,18 @@
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from avisstage.models import Normalien
class Command(BaseCommand):
help = 'Réinitialise les statuts "en scolarité" de tout le monde'
def add_arguments(self, parser):
return
def handle(self, *args, **options):
t = timezone.now() - timedelta(days=365)
Normalien.objects.all().update(last_cas_connect=t)
self.stdout.write(self.style.SUCCESS("Terminé"))

File diff suppressed because one or more lines are too long

View file

@ -2,30 +2,37 @@
# Generated by Django 1.11.2 on 2017-10-02 20:43
from __future__ import unicode_literals
from django.db import migrations, models
import tinymce.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('avisstage', '0001_initial'),
("avisstage", "0001_initial"),
]
operations = [
migrations.AddField(
model_name='avisstage',
name='avis_prestage',
field=tinymce.models.HTMLField(blank=True, default='', verbose_name='Avant le stage'),
model_name="avisstage",
name="avis_prestage",
field=tinymce.models.HTMLField(
blank=True, default="", verbose_name="Avant le stage"
),
),
migrations.AddField(
model_name='stage',
name='len_avis_lieux',
field=models.IntegerField(default=0, verbose_name='Longueur des avis de lieu'),
model_name="stage",
name="len_avis_lieux",
field=models.IntegerField(
default=0, verbose_name="Longueur des avis de lieu"
),
),
migrations.AddField(
model_name='stage',
name='len_avis_stage',
field=models.IntegerField(default=0, verbose_name='Longueur des avis de stage'),
model_name="stage",
name="len_avis_stage",
field=models.IntegerField(
default=0, verbose_name="Longueur des avis de stage"
),
),
]

View file

@ -0,0 +1,357 @@
# Generated by Django 2.2.17 on 2021-01-17 11:08
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("avisstage", "0002_auto_20171002_2243"),
]
operations = [
migrations.AlterField(
model_name="lieu",
name="pays",
field=models.CharField(
choices=[
("AF", "Afghanistan"),
("AL", "Albanie"),
("AQ", "Antarctique"),
("DZ", "Algérie"),
("AS", "Samoa Américaines"),
("AD", "Andorre"),
("AO", "Angola"),
("AG", "Antigua-et-Barbuda"),
("AZ", "Azerbaïdjan"),
("AR", "Argentine"),
("AU", "Australie"),
("AT", "Autriche"),
("BS", "Bahamas"),
("BH", "Bahreïn"),
("BD", "Bangladesh"),
("AM", "Arménie"),
("BB", "Barbade"),
("BE", "Belgique"),
("BM", "Bermudes"),
("BT", "Bhoutan"),
("BO", "Bolivie"),
("BA", "Bosnie-Herzégovine"),
("BW", "Botswana"),
("BV", "Île Bouvet"),
("BR", "Brésil"),
("BZ", "Belize"),
("IO", "Territoire Britannique de l'Océan Indien"),
("SB", "Îles Salomon"),
("VG", "Îles Vierges Britanniques"),
("BN", "Brunéi Darussalam"),
("BG", "Bulgarie"),
("MM", "Myanmar"),
("BI", "Burundi"),
("BY", "Bélarus"),
("KH", "Cambodge"),
("CM", "Cameroun"),
("CA", "Canada"),
("CV", "Cap-vert"),
("KY", "Îles Caïmanes"),
("CF", "République Centrafricaine"),
("LK", "Sri Lanka"),
("TD", "Tchad"),
("CL", "Chili"),
("CN", "Chine"),
("TW", "Taïwan"),
("CX", "Île Christmas"),
("CC", "Îles Cocos (Keeling)"),
("CO", "Colombie"),
("KM", "Comores"),
("YT", "Mayotte"),
("CG", "République du Congo"),
("CD", "République Démocratique du Congo"),
("CK", "Îles Cook"),
("CR", "Costa Rica"),
("HR", "Croatie"),
("CU", "Cuba"),
("CY", "Chypre"),
("CZ", "République Tchèque"),
("BJ", "Bénin"),
("DK", "Danemark"),
("DM", "Dominique"),
("DO", "République Dominicaine"),
("EC", "Équateur"),
("SV", "El Salvador"),
("GQ", "Guinée Équatoriale"),
("ET", "Éthiopie"),
("ER", "Érythrée"),
("EE", "Estonie"),
("FO", "Îles Féroé"),
("FK", "Îles (malvinas) Falkland"),
("GS", "Géorgie du Sud et les Îles Sandwich du Sud"),
("FJ", "Fidji"),
("FI", "Finlande"),
("AX", "Îles Åland"),
("FR", "France"),
("GF", "Guyane Française"),
("PF", "Polynésie Française"),
("TF", "Terres Australes Françaises"),
("DJ", "Djibouti"),
("GA", "Gabon"),
("GE", "Géorgie"),
("GM", "Gambie"),
("PS", "Territoire Palestinien Occupé"),
("DE", "Allemagne"),
("GH", "Ghana"),
("GI", "Gibraltar"),
("KI", "Kiribati"),
("GR", "Grèce"),
("GL", "Groenland"),
("GD", "Grenade"),
("GP", "Guadeloupe"),
("GU", "Guam"),
("GT", "Guatemala"),
("GN", "Guinée"),
("GY", "Guyana"),
("HT", "Haïti"),
("HM", "Îles Heard et Mcdonald"),
("VA", "Saint-Siège (état de la Cité du Vatican)"),
("HN", "Honduras"),
("HK", "Hong-Kong"),
("HU", "Hongrie"),
("IS", "Islande"),
("IN", "Inde"),
("ID", "Indonésie"),
("IR", "République Islamique d'Iran"),
("IQ", "Iraq"),
("IE", "Irlande"),
("IL", "Israël"),
("IT", "Italie"),
("CI", "Côte d'Ivoire"),
("JM", "Jamaïque"),
("JP", "Japon"),
("KZ", "Kazakhstan"),
("JO", "Jordanie"),
("KE", "Kenya"),
("KP", "République Populaire Démocratique de Corée"),
("KR", "République de Corée"),
("KW", "Koweït"),
("KG", "Kirghizistan"),
("LA", "République Démocratique Populaire Lao"),
("LB", "Liban"),
("LS", "Lesotho"),
("LV", "Lettonie"),
("LR", "Libéria"),
("LY", "Jamahiriya Arabe Libyenne"),
("LI", "Liechtenstein"),
("LT", "Lituanie"),
("LU", "Luxembourg"),
("MO", "Macao"),
("MG", "Madagascar"),
("MW", "Malawi"),
("MY", "Malaisie"),
("MV", "Maldives"),
("ML", "Mali"),
("MT", "Malte"),
("MQ", "Martinique"),
("MR", "Mauritanie"),
("MU", "Maurice"),
("MX", "Mexique"),
("MC", "Monaco"),
("MN", "Mongolie"),
("MD", "République de Moldova"),
("MS", "Montserrat"),
("MA", "Maroc"),
("MZ", "Mozambique"),
("OM", "Oman"),
("NA", "Namibie"),
("NR", "Nauru"),
("NP", "Népal"),
("NL", "Pays-Bas"),
("AN", "Antilles Néerlandaises"),
("AW", "Aruba"),
("NC", "Nouvelle-Calédonie"),
("VU", "Vanuatu"),
("NZ", "Nouvelle-Zélande"),
("NI", "Nicaragua"),
("NE", "Niger"),
("NG", "Nigéria"),
("NU", "Niué"),
("NF", "Île Norfolk"),
("NO", "Norvège"),
("MP", "Îles Mariannes du Nord"),
("UM", "Îles Mineures Éloignées des États-Unis"),
("FM", "États Fédérés de Micronésie"),
("MH", "Îles Marshall"),
("PW", "Palaos"),
("PK", "Pakistan"),
("PA", "Panama"),
("PG", "Papouasie-Nouvelle-Guinée"),
("PY", "Paraguay"),
("PE", "Pérou"),
("PH", "Philippines"),
("PN", "Pitcairn"),
("PL", "Pologne"),
("PT", "Portugal"),
("GW", "Guinée-Bissau"),
("TL", "Timor-Leste"),
("PR", "Porto Rico"),
("QA", "Qatar"),
("RE", "Réunion"),
("RO", "Roumanie"),
("RU", "Fédération de Russie"),
("RW", "Rwanda"),
("SH", "Sainte-Hélène"),
("KN", "Saint-Kitts-et-Nevis"),
("AI", "Anguilla"),
("LC", "Sainte-Lucie"),
("PM", "Saint-Pierre-et-Miquelon"),
("VC", "Saint-Vincent-et-les Grenadines"),
("SM", "Saint-Marin"),
("ST", "Sao Tomé-et-Principe"),
("SA", "Arabie Saoudite"),
("SN", "Sénégal"),
("SC", "Seychelles"),
("SL", "Sierra Leone"),
("SG", "Singapour"),
("SK", "Slovaquie"),
("VN", "Viet Nam"),
("SI", "Slovénie"),
("SO", "Somalie"),
("ZA", "Afrique du Sud"),
("ZW", "Zimbabwe"),
("ES", "Espagne"),
("EH", "Sahara Occidental"),
("SD", "Soudan"),
("SR", "Suriname"),
("SJ", "Svalbard etÎle Jan Mayen"),
("SZ", "Swaziland"),
("SE", "Suède"),
("CH", "Suisse"),
("SY", "République Arabe Syrienne"),
("TJ", "Tadjikistan"),
("TH", "Thaïlande"),
("TG", "Togo"),
("TK", "Tokelau"),
("TO", "Tonga"),
("TT", "Trinité-et-Tobago"),
("AE", "Émirats Arabes Unis"),
("TN", "Tunisie"),
("TR", "Turquie"),
("TM", "Turkménistan"),
("TC", "Îles Turks et Caïques"),
("TV", "Tuvalu"),
("UG", "Ouganda"),
("UA", "Ukraine"),
("MK", "L'ex-République Yougoslave de Macédoine"),
("EG", "Égypte"),
("GB", "Royaume-Uni"),
("IM", "Île de Man"),
("TZ", "République-Unie de Tanzanie"),
("US", "États-Unis"),
("VI", "Îles Vierges des États-Unis"),
("BF", "Burkina Faso"),
("UY", "Uruguay"),
("UZ", "Ouzbékistan"),
("VE", "Venezuela"),
("WF", "Wallis et Futuna"),
("WS", "Samoa"),
("YE", "Yémen"),
("CS", "Serbie-et-Monténégro"),
("ZM", "Zambie"),
],
max_length=2,
verbose_name="Pays",
),
),
migrations.AlterField(
model_name="lieu",
name="type_lieu",
field=models.CharField(
choices=[
("universite", "Université"),
("entreprise", "Entreprise"),
("centrerecherche", "Centre de recherche"),
("administration", "Administration"),
("autre", "Autre"),
],
default="universite",
max_length=15,
verbose_name="Type de structure d'accueil",
),
),
migrations.AlterField(
model_name="normalien",
name="user",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="profil",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="stage",
name="auteur",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="stages",
to="avisstage.Normalien",
),
),
migrations.AlterField(
model_name="stage",
name="niveau_scol",
field=models.CharField(
blank=True,
choices=[
("L3", "Licence 3"),
("M1", "Master 1"),
("M2", "Master 2"),
("DOC", "Pré-doctorat"),
("CST", "Césure"),
("BLA", "Année blanche"),
("VAC", "Vacances scolaires"),
("MIT", "Mi-temps en parallèle des études"),
("", "Autre"),
],
default="",
max_length=3,
verbose_name="Année de scolarité",
),
),
migrations.AlterField(
model_name="stage",
name="type_stage",
field=models.CharField(
choices=[
(
"Recherche :",
(
("recherche", "Stage académique"),
("recherche_autre", "Stage non-académique"),
("sejour_dri", "Séjour de recherche DRI"),
),
),
(
"Stage sans visée de recherche :",
(
("pro", "Stage en entreprise"),
("admin", "Stage en admin./ONG/orga. internationale"),
),
),
(
"Enseignement :",
(
("lectorat", "Lectorat DRI"),
("autre_teach", "Autre expérience d'enseignement"),
),
),
("autre", "Autre"),
],
default="stage",
max_length=31,
verbose_name="Type",
),
),
]

View file

@ -0,0 +1,107 @@
from django.apps import apps as global_apps
from django.db import migrations
from django.utils import timezone
def forwards(apps, schema_editor):
User = apps.get_model("auth", "User")
try:
CASAccount = apps.get_model("authens", "CASAccount")
except LookupError:
return
try:
SocialAccount = apps.get_model("socialaccount", "SocialAccount")
OldEmailAddress = apps.get_model("account", "EmailAddress")
except LookupError:
# Allauth not installed
# Simply create CAS accounts for every profile
# This procedure is not meant to be fast
from authens.shortcuts import fetch_cas_account
def migrate_user(user):
ldap_info = fetch_cas_account(user.username)
if ldap_info:
entrance_year = ldap_info["entrance_year"]
CASAccount.objects.create(
user=user, cas_login=user.username, entrance_year=entrance_year
)
for user in User.objects.all():
migrate_user(user)
return
NewEmailAddress = apps.get_model("simple_email_confirmation", "EmailAddress")
from simple_email_confirmation.models import EmailAddressManager
# Transfer from allauth to authens
# Assumes usernames have the format <clipper>@<promo>
# Assumes no clashing clipper accounts have ever been found
oldusers = User.objects.all().prefetch_related(
"emailaddress_set", "socialaccount_set"
)
is_ens_mail = lambda mail: (
mail is not None and (mail.endswith("ens.fr") or mail.endswith("ens.psl.eu"))
)
new_conns = []
new_mails = []
for user in oldusers:
# Move EmailAddress to new model
addresses = user.emailaddress_set.all()
for addr in addresses:
newaddr = NewEmailAddress(
user=user,
email=addr.email,
set_at=timezone.now(),
confirmed_at=(timezone.now() if addr.verified else None),
key=EmailAddressManager().generate_key(),
)
if addr.primary and user.email != addr.email:
print("Adresse principale inconsistante", user.email, addr.email)
new_mails.append(newaddr)
# Create new CASAccount connexion
saccounts = user.socialaccount_set.all()
if not saccounts:
continue
if len(saccounts) > 1:
print(saccounts)
saccount = saccounts[0]
clipper = saccount.uid
if "@" not in user.username:
print(user.username)
continue
entrance_year = saccount.extra_data.get(
"entrance_year", user.username.split("@")[1]
)
try:
entrance_year = 2000 + int(entrance_year)
except ValueError:
print(entrance_year)
continue
new_conns.append(
CASAccount(user=user, cas_login=clipper, entrance_year=int(entrance_year))
)
NewEmailAddress.objects.bulk_create(new_mails)
CASAccount.objects.bulk_create(new_conns)
class Migration(migrations.Migration):
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop),
]
dependencies = [
("avisstage", "0003_auto_20210117_1208"),
("authens", "0002_old_cas_account"),
]
if global_apps.is_installed("allauth"):
dependencies.append(("socialaccount", "0003_extra_data_default_dict"))
if global_apps.is_installed("simple_email_confirmation"):
dependencies.append(("simple_email_confirmation", "0001_initial"))

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.17 on 2021-01-17 20:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("avisstage", "0004_allauth_to_authens"),
]
operations = [
migrations.AddField(
model_name="normalien",
name="en_scolarite",
field=models.BooleanField(blank=True, default=False),
),
]

View file

@ -0,0 +1,37 @@
# Generated by Django 2.2.17 on 2021-01-31 18:54
from django.db import migrations, models
import avisstage.models
class Migration(migrations.Migration):
dependencies = [
("avisstage", "0005_normalien_en_scolarite"),
]
operations = [
migrations.RemoveField(
model_name="normalien",
name="en_scolarite",
),
migrations.RemoveField(
model_name="normalien",
name="mail",
),
migrations.AddField(
model_name="normalien",
name="last_cas_login",
field=models.DateField(default=avisstage.models._default_cas_login),
),
migrations.AlterField(
model_name="normalien",
name="contactez_moi",
field=models.BooleanField(
default=True,
help_text="Affiche votre adresse e-mail principale sur votre profil public",
verbose_name="Inviter les visiteurs à me contacter",
),
),
]

View file

@ -1,126 +1,154 @@
# coding: utf-8
from __future__ import unicode_literals
from django.db import models
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.contrib.gis.db import models as geomodels
from django.template.defaultfilters import slugify
from django.forms.widgets import DateInput
from django.urls import reverse
from django.utils import timezone
from django.utils.html import strip_tags
from datetime import timedelta
from authens.signals import post_cas_connect
from taggit_autosuggest.managers import TaggableManager
from tinymce.models import HTMLField as RichTextField
from .utils import choices_length
from .statics import DEPARTEMENTS_DEFAUT, PAYS_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, TYPE_LIEU_DICT, TYPE_STAGE_DICT, NIVEAU_SCOL_OPTIONS, NIVEAU_SCOL_DICT
from django.contrib.auth.models import User
from django.contrib.gis.db import models as geomodels
from django.db import models
from django.db.models.signals import post_save
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from .statics import (
DEPARTEMENTS_DEFAUT,
NIVEAU_SCOL_DICT,
NIVEAU_SCOL_OPTIONS,
PAYS_OPTIONS,
TYPE_LIEU_DICT,
TYPE_LIEU_OPTIONS,
TYPE_STAGE_DICT,
TYPE_STAGE_OPTIONS,
)
from .utils import choices_length
def _default_cas_login():
return (timezone.now() - timedelta(days=365)).date()
import ldap
#
# Profil Normalien (extension du modèle User)
#
class Normalien(models.Model):
user = models.OneToOneField(User, related_name="profil")
user = models.OneToOneField(
User, related_name="profil", on_delete=models.SET_NULL, null=True
)
# Infos spécifiques
nom = models.CharField(u"Nom complet", max_length=255, blank=True)
promotion = models.CharField(u"Promotion", max_length=40, blank=True)
mail = models.EmailField(u"Adresse e-mail permanente",
max_length=200, blank=True)
contactez_moi = models.BooleanField(u"Inviter les visiteurs à me contacter",
default=True)
bio = models.TextField(u"À propos de moi", blank=True, default="");
nom = models.CharField("Nom complet", max_length=255, blank=True)
promotion = models.CharField("Promotion", max_length=40, blank=True)
contactez_moi = models.BooleanField(
"Inviter les visiteurs à me contacter",
default=True,
help_text="Affiche votre adresse e-mail principale sur votre profil public",
)
bio = models.TextField("À propos de moi", blank=True, default="")
last_cas_login = models.DateField(default=_default_cas_login)
class Meta:
verbose_name = u"Profil élève"
verbose_name_plural = u"Profils élèves"
verbose_name = "Profil élève"
verbose_name_plural = "Profils élèves"
def __unicode__(self):
return u"%s (%s)" % (self.nom, self.user.username)
def __str__(self):
return "%s (%s)" % (self.nom, self.user.username)
# Liste des stages publiés
def stages_publics(self):
return self.stages.filter(public=True).order_by('-date_debut')
return self.stages.filter(public=True).order_by("-date_debut")
# Hook à la création d'un nouvel utilisateur : récupération de ses infos par LDAP
def create_user_profile(sender, instance, created, **kwargs):
def has_nonENS_email(self):
return (
self.user.email_address_set.exclude(confirmed_at__isnull=True)
.exclude(email__endswith="ens.fr")
.exclude(email__endswith="ens.psl.eu")
.exists()
)
def nom_complet(self):
if self.nom.strip():
return self.nom
return self.user.username
@property
def en_scolarite(self):
return self.last_cas_login > (timezone.now() - timedelta(days=60)).date()
@property
def preferred_email(self):
return self.user.email
# Hook à la création d'un nouvel utilisateur : information de base
def create_basic_user_profile(sender, instance, created, **kwargs):
if created:
profil, created = Normalien.objects.get_or_create(user=instance)
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
l = ldap.initialize("ldaps://ldap.spi.ens.fr:636")
l.set_option(ldap.OPT_REFERRALS, 0)
l.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
l.set_option(ldap.OPT_X_TLS,ldap.OPT_X_TLS_DEMAND)
l.set_option(ldap.OPT_X_TLS_DEMAND, True)
l.set_option(ldap.OPT_DEBUG_LEVEL, 255)
l.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
l.set_option(ldap.OPT_TIMEOUT, 10)
info = l.search_s('dc=spi,dc=ens,dc=fr',
ldap.SCOPE_SUBTREE,
('(uid=%s)' % (instance.username,)),
[str("cn"),
str("mailRoutingAddress"),
str("homeDirectory")])
# Si des informations sont disponibles
if len(info) > 0:
infos = info[0][1]
# Nom
profil.nom = infos.get('cn', [''])[0]
# Parsing du homeDirectory pour la promotion
if 'homeDirectory' in infos:
dirs = infos['homeDirectory'][0].split('/')
if dirs[1] == 'users':
annee = dirs[2]
dep = dirs[3]
dep = dict(DEPARTEMENTS_DEFAUT).get(dep.lower(), '')
profil.promotion = u'%s %s' % (dep, annee)
# Mail
pmail = infos.get('mailRoutingAddress',
['%s@clipper.ens.fr'%instance.username])
if len(pmail) > 0:
profil.mail = pmail[0]
if not created and profil.promotion != "":
return
if "@" in instance.username:
profil.promotion = instance.username.split("@")[1]
profil.save()
except ldap.LDAPError:
pass
post_save.connect(create_user_profile, sender=User)
post_save.connect(create_basic_user_profile, sender=User)
# Hook d'authENS : information du CAS
def handle_cas_connection(sender, instance, created, cas_login, attributes, **kwargs):
profil, created = Normalien.objects.get_or_create(user=instance)
profil.last_cas_login = timezone.now().date()
if not created:
profil.save()
return
dirs = attributes.get("homeDirectory", "").split("/")
if len(dirs) < 4:
print("HomeDirectory invalide", dirs)
return
year = dirs[2]
departement = dirs[3]
dep = dict(DEPARTEMENTS_DEFAUT).get(departement.lower(), "")
profil.promotion = "%s %s" % (dep, year)
profil.nom = attributes.get("name", "")
profil.save()
post_cas_connect.connect(handle_cas_connection, sender=User)
#
# Lieu de stage
#
class Lieu(models.Model):
# Général
nom = models.CharField(u"Nom de l'institution d'accueil",
max_length=250)
type_lieu = models.CharField(u"Type de structure d'accueil",
default="universite",
choices=TYPE_LIEU_OPTIONS,
max_length=choices_length(TYPE_LIEU_OPTIONS))
nom = models.CharField("Nom de l'institution d'accueil", max_length=250)
type_lieu = models.CharField(
"Type de structure d'accueil",
default="universite",
choices=TYPE_LIEU_OPTIONS,
max_length=choices_length(TYPE_LIEU_OPTIONS),
)
# Infos géographiques
ville = models.CharField(u"Ville",
max_length=200)
pays = models.CharField(u"Pays",
choices=PAYS_OPTIONS,
max_length=choices_length(PAYS_OPTIONS))
ville = models.CharField("Ville", max_length=200)
pays = models.CharField(
"Pays", choices=PAYS_OPTIONS, max_length=choices_length(PAYS_OPTIONS)
)
# Coordonnées
objects = geomodels.GeoManager() # Requis par GeoDjango
coord = geomodels.PointField(u"Coordonnées",
geography=True,
srid = 4326)
# objects = geomodels.GeoManager() # Requis par GeoDjango
coord = geomodels.PointField("Coordonnées", geography=True, srid=4326)
# Type du lieu en plus joli
@property
@ -131,70 +159,83 @@ class Lieu(models.Model):
def type_lieu_fem(self):
return TYPE_LIEU_DICT.get(self.type_lieu, ("lieu", False))[1]
def __unicode__(self):
return u"%s (%s)" % (self.nom, self.ville)
def __str__(self):
return "%s (%s)" % (self.nom, self.ville)
class Meta:
verbose_name = "Lieu"
verbose_name_plural = "Lieux"
#
# Matières des stages
#
class StageMatiere(models.Model):
nom = models.CharField(u"Nom", max_length=30)
nom = models.CharField("Nom", max_length=30)
slug = models.SlugField()
class Meta:
verbose_name = "Matière des stages"
verbose_name_plural = "Matières des stages"
def __unicode__(self):
def __str__(self):
return self.nom
#
# Un stage
#
class Stage(models.Model):
# Misc
auteur = models.ForeignKey(Normalien, related_name="stages")
public = models.BooleanField(u"Visible publiquement", default=False)
date_creation = models.DateTimeField(u"Créé le", default=timezone.now)
date_maj = models.DateTimeField(u"Mis à jour le", default=timezone.now)
len_avis_stage = models.IntegerField(u"Longueur des avis de stage", default=0)
len_avis_lieux = models.IntegerField(u"Longueur des avis de lieu", default=0)
auteur = models.ForeignKey(
Normalien, related_name="stages", on_delete=models.SET_NULL, null=True
)
public = models.BooleanField("Visible publiquement", default=False)
date_creation = models.DateTimeField("Créé le", default=timezone.now)
date_maj = models.DateTimeField("Mis à jour le", default=timezone.now)
len_avis_stage = models.IntegerField("Longueur des avis de stage", default=0)
len_avis_lieux = models.IntegerField("Longueur des avis de lieu", default=0)
# Caractéristiques du stage
sujet = models.CharField(u"Sujet", max_length=500)
sujet = models.CharField("Sujet", max_length=500)
date_debut = models.DateField(u"Date de début", null=True)
date_fin = models.DateField(u"Date de fin", null=True)
date_debut = models.DateField("Date de début", null=True)
date_fin = models.DateField("Date de fin", null=True)
type_stage = models.CharField(u"Type",
default="stage",
choices=TYPE_STAGE_OPTIONS,
max_length=choices_length(TYPE_STAGE_OPTIONS))
niveau_scol = models.CharField(u"Année de scolarité",
default="",
choices=NIVEAU_SCOL_OPTIONS,
max_length=choices_length(NIVEAU_SCOL_OPTIONS),
blank=True)
type_stage = models.CharField(
"Type",
default="stage",
choices=TYPE_STAGE_OPTIONS,
max_length=choices_length(TYPE_STAGE_OPTIONS),
)
niveau_scol = models.CharField(
"Année de scolarité",
default="",
choices=NIVEAU_SCOL_OPTIONS,
max_length=choices_length(NIVEAU_SCOL_OPTIONS),
blank=True,
)
thematiques = TaggableManager(u"Thématiques", blank=True)
matieres = models.ManyToManyField(StageMatiere, verbose_name=u"Matière(s)", related_name="stages")
encadrants = models.CharField(u"Encadrant⋅e⋅s", max_length=500, blank=True)
structure = models.CharField(u"Structure d'accueil", max_length=500, blank=True)
thematiques = TaggableManager("Thématiques", blank=True)
matieres = models.ManyToManyField(
StageMatiere, verbose_name="Matière(s)", related_name="stages"
)
encadrants = models.CharField("Encadrant⋅e⋅s", max_length=500, blank=True)
structure = models.CharField("Structure d'accueil", max_length=500, blank=True)
# Avis
lieux = models.ManyToManyField(Lieu, related_name="stages",
through="AvisLieu", blank=True)
lieux = models.ManyToManyField(
Lieu, related_name="stages", through="AvisLieu", blank=True
)
# Affichage des avis ordonnés
@property
def avis_lieux(self):
return self.avislieu_set.order_by('order')
return self.avislieu_set.order_by("order")
# Shortcut pour affichage rapide
@property
@ -208,6 +249,7 @@ class Stage(models.Model):
@property
def type_stage_fancy(self):
return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[0]
@property
def type_stage_fem(self):
return TYPE_STAGE_DICT.get(self.type_stage, ("stage", False))[1]
@ -216,13 +258,17 @@ class Stage(models.Model):
@property
def niveau_scol_fancy(self):
return NIVEAU_SCOL_DICT.get(self.niveau_scol, "")
# Optimisation de requêtes
@cached_property
def all_lieux(self):
return self.lieux.all()
def get_absolute_url(self):
return reverse('avisstage:stage', self)
def __unicode__(self):
return u"%s (par %s)" % (self.sujet, self.auteur.user.username)
return reverse("avisstage:stage", self)
def __str__(self):
return "%s (par %s)" % (self.sujet, self.auteur.user.username)
def update_stats(self, save=True):
def get_len(obj):
@ -234,70 +280,80 @@ class Stage(models.Model):
length += len(obj.les_plus.split())
length += len(obj.les_moins.split())
return length
if self.avis_stage:
self.len_avis_stage = get_len(self.avis_stage)
self.len_avis_lieux = 0
for avis in self.avislieu_set.all():
self.len_avis_lieux += get_len(avis)
if save:
self.save()
class Meta:
verbose_name = "Stage"
#
# Les avis
#
class AvisStage(models.Model):
stage = models.OneToOneField(Stage, related_name="avis_stage")
stage = models.OneToOneField(
Stage, related_name="avis_stage", on_delete=models.CASCADE
)
chapo = models.TextField(u"En quelques mots", blank=True)
avis_ambiance = RichTextField(u"L'ambiance de travail", blank=True)
avis_sujet = RichTextField(u"La mission", blank=True)
avis_admin = RichTextField(u"Formalités et administration", blank=True)
avis_prestage = RichTextField(u"Avant le stage", blank=True, default="")
chapo = models.TextField("En quelques mots", blank=True)
avis_ambiance = RichTextField("L'ambiance de travail", blank=True)
avis_sujet = RichTextField("La mission", blank=True)
avis_admin = RichTextField("Formalités et administration", blank=True)
avis_prestage = RichTextField("Avant le stage", blank=True, default="")
les_plus = models.TextField(u"Les plus de cette expérience", blank=True)
les_moins = models.TextField(u"Les moins de cette expérience", blank=True)
les_plus = models.TextField("Les plus de cette expérience", blank=True)
les_moins = models.TextField("Les moins de cette expérience", blank=True)
def __unicode__(self):
return u"Avis sur {%s} par %s" % (self.stage.sujet, self.stage.auteur.user.username)
def __str__(self):
return "Avis sur {%s} par %s" % (
self.stage.sujet,
self.stage.auteur.user.username,
)
# Liste des champs d'avis, couplés à leur nom (pour l'affichage)
@property
def avis_all(self):
fields = ['avis_sujet', 'avis_ambiance', 'avis_admin', 'avis_prestage']
return [(AvisStage._meta.get_field(field).verbose_name,
getattr(self, field, '')) for field in fields]
fields = ["avis_sujet", "avis_ambiance", "avis_admin", "avis_prestage"]
return [
(AvisStage._meta.get_field(field).verbose_name, getattr(self, field, ""))
for field in fields
]
class AvisLieu(models.Model):
stage = models.ForeignKey(Stage)
lieu = models.ForeignKey(Lieu)
stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
lieu = models.ForeignKey(Lieu, on_delete=models.CASCADE)
order = models.IntegerField("Ordre", default=0)
chapo = models.TextField(u"En quelques mots", blank=True)
avis_lieustage = RichTextField(u"Les lieux de travail", blank=True)
avis_pratique = RichTextField(u"S'installer - conseils pratiques",
blank=True)
avis_tourisme = RichTextField(u"Dans les parages", blank=True)
chapo = models.TextField("En quelques mots", blank=True)
avis_lieustage = RichTextField("Les lieux de travail", blank=True)
avis_pratique = RichTextField("S'installer - conseils pratiques", blank=True)
avis_tourisme = RichTextField("Dans les parages", blank=True)
les_plus = models.TextField(u"Les plus du lieu", blank=True)
les_moins = models.TextField(u"Les moins du lieu", blank=True)
les_plus = models.TextField("Les plus du lieu", blank=True)
les_moins = models.TextField("Les moins du lieu", blank=True)
class Meta:
verbose_name = "Avis sur un lieu de stage"
verbose_name_plural = "Avis sur un lieu de stage"
def __unicode__(self):
return u"Avis sur {%s} par %s" % (self.lieu.nom, self.stage.auteur.user_id)
def __str__(self):
return "Avis sur {%s} par %s" % (self.lieu.nom, self.stage.auteur.user_id)
# Liste des champs d'avis, couplés à leur nom (pour l'affichage)
@property
def avis_all(self):
fields = ['avis_lieustage', 'avis_pratique', 'avis_tourisme']
return [(AvisLieu._meta.get_field(field).verbose_name,
getattr(self, field, '')) for field in fields]
fields = ["avis_lieustage", "avis_pratique", "avis_tourisme"]
return [
(AvisLieu._meta.get_field(field).verbose_name, getattr(self, field, ""))
for field in fields
]

View file

@ -0,0 +1,71 @@
@import "_definitions.scss";
/* alegreya-700 - latin */
@font-face {
font-family: 'Alegreya';
font-style: normal;
font-weight: 700;
src: local('Alegreya Bold'), local('Alegreya-Bold'),
url('' + $staticurl + 'fonts/alegreya-v11-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl + 'fonts/alegreya-v11-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* dosis-300 - latin */
@font-face {
font-family: 'Dosis';
font-style: normal;
font-weight: 300;
src: local('Dosis Light'), local('Dosis-Light'),
url('' + $staticurl +'fonts/dosis-v7-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl +'fonts/dosis-v7-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* dosis-500 - latin */
@font-face {
font-family: 'Dosis';
font-style: normal;
font-weight: 500;
src: local('Dosis Medium'), local('Dosis-Medium'),
url('' + $staticurl +'fonts/dosis-v7-latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl +'fonts/dosis-v7-latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* dosis-700 - latin */
@font-face {
font-family: 'Dosis';
font-style: normal;
font-weight: 700;
src: local('Dosis Bold'), local('Dosis-Bold'),
url('' + $staticurl +'fonts/dosis-v7-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl +'fonts/dosis-v7-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* lato-300 - latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 300;
src: local('Lato Light'), local('Lato-Light'),
url('' + $staticurl + 'fonts/lato-v14-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl + 'fonts/lato-v14-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* lato-300italic - latin */
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 300;
src: local('Lato Light Italic'), local('Lato-LightItalic'),
url('' + $staticurl + 'fonts/lato-v14-latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl + 'fonts/lato-v14-latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* lato-700 - latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'),
url('' + $staticurl + 'fonts/lato-v14-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('' + $staticurl + 'fonts/lato-v14-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

View file

@ -0,0 +1,63 @@
@mixin miniHeader() {
header {
z-index: 40;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
display: block;
max-height:100vh;
overflow-y: auto;
h1 {
padding: 10px;
}
#showmenu {
display: block;
float: right;
padding: 10px;
img {
width: 35px;
}
}
nav {
clear: both;
text-align: right;
ul {
display: none;
li {
display: block;
a {
display: block;
padding: 10px 20px;
height: auto;
text-align:right;
br {
display: none;
}
.username:after {
content: " | ";
}
}
}
}
}
}
header.expanded {
nav ul {
display:block;
}
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.6em;
}
}

View file

@ -1,93 +1,98 @@
section.content.recherche {
form.recherche {
.generale {
display: inline-block;
text-align: right;
position: relative;
left: 50%;
transform: translateX(-50%);
width: 500px;
max-width: 100%;
white-space: nowrap;
@import "_miniheader.scss";
span {
display:flex;
}
input[type="text"] {
max-width: 500px;
padding: 10px;
border: 1px solid $fond;
margin:0 5px;
}
input {
display: inline;
}
}
body.recherche {
.avancee {
background: #fff;
display: none;
padding: 15px;
margin-bottom: 15px;
&.expanded {
display:block;
}
section.content {
form.recherche {
.generale {
display: inline-block;
text-align: right;
position: relative;
left: 50%;
transform: translateX(-50%);
width: 500px;
max-width: 100%;
white-space: nowrap;
.help_text {
font-style: italic;
font-size: 0.9em;
}
ul {
margin: 0 -5px;
display: flexbox;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
span {
display:flex;
}
li {
flex-grow: 1;
width: 22%;
min-width: 150px;
margin: 5px 0;
padding: 0 10px;
input[type="text"] {
max-width: 500px;
padding: 10px;
border: 1px solid $fond;
margin:0 5px;
}
input {
display: inline;
}
}
label {
font-weight: bold;
font-size: 0.9em;
}
.avancee {
background: #fff;
display: none;
padding: 15px;
margin-bottom: 15px;
&.expanded {
display:block;
}
.help_text {
font-style: italic;
font-size: 0.9em;
}
ul {
margin: 0 -5px;
display: flexbox;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
input[type="text"], input[type='number'], select {
display: block;
li {
flex-grow: 1;
width: 22%;
min-width: 150px;
display: inline-block;
width: 100%;
font-size: 0.9em;
background-color: #f8f8f8;
margin: 5px 0;
padding: 0 10px;
label {
font-weight: bold;
font-size: 0.9em;
}
input[type="text"], input[type='number'], select {
display: block;
min-width: 150px;
display: inline-block;
width: 100%;
font-size: 0.9em;
background-color: #f8f8f8;
}
&.btnsubmit {
text-align:right;
}
&.field__sujet, &.field__contexte {
width:45%;
min-width: 300px;
}
}
&.btnsubmit {
text-align:right;
}
&.field__sujet, &.field__contexte {
width:45%;
min-width: 300px;
}
}
}
}
}
.recherche-carte, .recherche-liste {
position:relative;
}
.recherche-carte, .recherche-liste {
position:relative;
}
.numresults {
font-size: 0.9em;
font-weight: bold;
.numresults {
font-size: 0.9em;
font-weight: bold;
}
}
&.vue-hybride #voir_hybride,
@ -124,14 +129,14 @@ section.content.recherche {
left: 60px;
}
&.vue-hybride, &.vue-carte {
&.vue-hybride section.content,
&.vue-carte section.content {
width: 100%;
min-width: unset;
max-width: unset;
min-height: unset;
max-height: unset;
height: 90vh;
height: calc(100vh - 30px);
height: 100vh;
padding: 0;
margin: 0;
@ -145,7 +150,8 @@ section.content.recherche {
}
&.vue-liste .recherche-carte,
&.vue-carte .recherche-liste {
&.vue-carte .recherche-liste,
&.vue-carte header {
display: none;
}
@ -156,31 +162,42 @@ section.content.recherche {
}
&.vue-hybride {
display: flex;
.recherche-liste {
@include miniHeader;
header {
position: fixed;
top: 0;
left: 0;
width: 100%;
min-width: 400px;
max-width: 500px;
flex: 1;
.dates {
display:none;
}
ul.infos li {
font-size: 0.8em;
font-weight: normal;
&.year {
display: inline-block;
z-index: 15;
}
section.content {
display: flex;
.recherche-liste {
padding-top: 60px;
width: 500px;
.dates {
display:none;
}
ul.infos li {
font-size: 0.8em;
font-weight: normal;
&.year {
display: inline-block;
}
}
}
}
.recherche-carte {
flex: 1.5;
width: 100%;
.recherche-carte {
flex: 1.5;
width: 100%;
.vue-options {
display:none;
.vue-options {
display:none;
}
}
}
}

View file

@ -1,3 +1,5 @@
@import "_miniheader.scss";
@media screen and (max-width: 850px) {
header {
font-size: 0.9em;
@ -18,68 +20,9 @@
}
@media screen and (max-width: 600px) {
header {
z-index: 40;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
display: block;
max-height:100vh;
overflow-y: auto;
h1 {
padding: 10px;
}
#showmenu {
display: block;
float: right;
padding: 10px;
img {
width: 35px;
}
}
nav {
clear: both;
text-align: right;
ul {
display: none;
li {
display: block;
a {
display: block;
padding: 10px 20px;
height: auto;
text-align:right;
br {
display: none;
}
.username:after {
content: " | ";
}
}
}
}
}
}
header.expanded {
nav ul {
display:block;
}
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.6em;
}
@include miniHeader;
#feedback-button {
transform: unset;
top: 0px;
@ -234,14 +177,14 @@
display: block;
text-align: left;
font-size: 0.95em;
color: $compl * 0.8;
color: darken($compl, 20%);
margin-top: 0;
width: auto;
}
.help_text {
text-align: right;
color: $fond * 0.4;
color: darken($fond, 60%);
}
.input {

View file

@ -1,6 +1,7 @@
@import "compass/reset";
@import "_definitions.scss";
@import url('https://fonts.googleapis.com/css?family=Dosis:300,500,700|Alegreya:700|Lato:300,300i,700');
@import "_fonts.scss";
// @import url('https://fonts.googleapis.com/css?family=Dosis:300,500,700|Alegreya:700|Lato:300,300i,700');
// Général
@ -45,7 +46,7 @@ em, i {
a {
font-weight: bold;
color: $compl * 0.9;
color: darken($compl, 10%);
text-decoration: none;
}
@ -106,7 +107,7 @@ header {
color: lighten($fond, 40%);
&:hover {
background: $barre * 0.6;
background: darken($barre, 40%);
}
}
}
@ -161,13 +162,25 @@ header {
}
}
p.warning {
background-color: #c20;
font-weight: bold;
color: #fff;
padding: 3px;
}
.footer {
margin-top: 15px;
font-size: 0.8em;
text-align: right;
}
// Liste des stages condensée sur le profil
.condensed-stages {
li {
display: table;
width: 100%;
//border: 1px solid $fond * 1.3;
background: #fff;
margin: 12px;
@ -256,19 +269,19 @@ header {
}
}
}
a.hoverlink {
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
}
}
a.hoverlink {
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
ul.infos {
margin: 0 -3px;
padding: 0;
@ -349,6 +362,57 @@ section.profil {
}
}
section.two-cols {
display: flex;
display: flexbox;
align-items: center;
& > * {
flex: 1;
width: 50%;
margin: 10px;
}
}
ul.mes-emails {
li {
display: flex;
background: #fff;
margin: 5px;
padding: 10px;
min-height: 70px;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
& > * {
flex: 1;
text-align: center;
}
.adresse {
text-align: left;
font-weight: bold;
}
.confirmee {
width: 20px;
}
.supprimer {
flex: 0.7;
}
form {
display: flex;
align-items: center;
justify-content: space-around;
.field {
flex: 1;
}
}
}
}
//
//
// Détail d'un stage
@ -418,7 +482,7 @@ input[type="submit"], .btn {
font: $textfontsize $textfont;
background-color: $fond;
color: #fff;
border: 1px solid $fond * 0.7;
border: 1px solid darken($fond, 30%);
border-radius: 5px;
padding: 8px 12px;
display: inline-block;
@ -502,15 +566,20 @@ form {
// taggit autosuggest
ul.as-selections {
ul.as-selections,
.selectize-control.multi {
display: flex;
flex-wrap: wrap;
li {
display:inline-block;
}
.as-selection-item {
.selectize-input, .selectize-dropdown {
font-size: 100%;
line-height: 1.1;
}
.as-selection-item,
.selectize-input > div {
padding: 0 5px;
background: $compl;
color: #fff;
@ -542,6 +611,8 @@ ul.as-selections {
div.as-results {
position: relative;
z-index: 2;
ul {
position: absolute;
width: 100%;
@ -632,8 +703,16 @@ div.as-results {
// Widget choix et ajout de lieux
#lieu_widget {
.window-content {
max-width: 800px;
}
.lieu-ui {
position: relative;
width: 100%;
min-width: 150px;
flex: 2;
.map {
height: 400px;
width: 100%;
@ -646,9 +725,66 @@ div.as-results {
}
}
.lieu-choixmodif {
.lieu-choixmodif, .lieu-options {
display: none;
}
.lieu-global {
display: flex;
width: 100%;
flex-wrap: wrap;
&.with-options {
.lieu-options {
display: block;
}
}
}
&.modif, &.edit {
.lieu-global.with-options .lieu-options {
display: none;
}
}
.lieu-options {
padding: 7px;
max-width: 350px;
flex: 3;
.lieu-suggestions {
max-height: 300px;
overflow-y: auto;
li {
position: relative;
background: #fff;
margin: 2px;
padding: 4px;
font-size: 0.9em;
&:hover {
background: #ccc;
}
p {
margin: 2px 0;
}
.lieu-nom {
font-weight: bold;
}
.lieu-infos {
font-size: 0.8em;
display:flex;
width: 100%;
justify-content: space-between;
span {
display: inline-block;
text-overflow: ellipsis;
overlow: hidden;
}
}
}
}
}
&.modif {
.lieu-choixmodif {
@ -751,6 +887,13 @@ a.lieu-change {
padding: 15px;
text-align: center;
margin: 15px auto;
.archicubes {
border-top: 2px solid $vert;
margin-top: 5px;
padding-top: 5px;
font-size: 0.9em;
}
}
article.promo {
@ -857,6 +1000,27 @@ article.promo {
}
}
//
//
// Modération
table.stats {
width: 100%;
background: #fff;
margin: 20px 0;
cellspacing: 1px;
th {
font-weight: bold;
border-top: 1px solid #000;
border-bottom: 1px solid #999;
}
td, th {
padding: 5px 3px;
text-align: center;
}
}
//
//
// Recherche

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,4 @@
function initEditStage(STATIC_URL, API_URL) {
function initEditStage(STATIC_URL, API_URL, MAPBOX_API_KEY) {
var has_changes = true;
var stage_object_id = $("#stage_object_id").val();
$(window).on('beforeunload',
@ -39,7 +39,7 @@ function initEditStage(STATIC_URL, API_URL) {
var slts = $("select[multiple]").selectize();
// CHOIX DU LIEU
var lieu_select = new SelectLieuWidget(STATIC_URL, API_URL,
var lieu_select = new SelectLieuWidget(STATIC_URL, API_URL, MAPBOX_API_KEY,
$("#lieu_widget"), lieuChoisi);
var avis_lieu_template = $("#avis_lieu_vide").remove().html();
var lieux_liste = $("#lieux-selector");

View file

@ -1,7 +1,8 @@
function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
function InterfaceRecherche(STATIC_ROOT, API_LIEU, MAPBOX_API_KEY, ITEMS_URL, lieux) {
var interface_mode, main_container;
var lieux_map = {}, lieux_list = [], stages_map = {}, lieux_db = {};
var stages_data = {};
var stages_db = {};
var details_liste_data;
var marqueurs = L.markerClusterGroup();
var marqueurs_db = {};
var changevue;
@ -13,8 +14,10 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
// TODO se souvenir des préférences d'affichage
function initInterface() {
main_container = $(".content.recherche");
if (main_container.hasClass("vue-liste")) {
main_container = $("body");
if (sessionStorage && sessionStorage.interface_mode) {
interface_mode = sessionStorage.interface_mode;
} else if (main_container.hasClass("vue-liste")) {
interface_mode = "liste";
} else if (main_container.hasClass("vue-carte")) {
interface_mode = "carte";
@ -43,6 +46,17 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
});
changeInterface(interface_mode);
initLoadMoreAJAX();
referenceStageItems($("#resultats").children());
}
function referenceStageItems (stages, hard) {
$.each(stages, function(i, item) {
if (item.id === undefined) return;
var iid = Number(item.id.split("-")[2]);
if (stages_db[iid] !== undefined && !hard) return;
stages_db[iid] = $(item);
});
}
// Changement d'affichage : mise à jour des classes et démarrage de la carte si nécessaire
@ -53,8 +67,11 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
function changeInterface(mode) {
interface_mode = mode;
$(".content.recherche").removeClass("vue-carte vue-hybride vue-liste")
.addClass("vue-"+mode);
main_container.removeClass("vue-carte vue-hybride vue-liste")
.addClass("vue-"+mode);
if (sessionStorage) {
sessionStorage.interface_mode = mode;
}
if (mode=="hybride" || mode=="carte") {
initCarte();
map.invalidateSize();
@ -66,7 +83,12 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
function initCarte() {
if (map !== undefined) return;
map = L.map("carte").panTo([30, 15]).setZoom(1);
var layer = new L.TileLayer("https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}", {attribution: 'Map tiles by <a href="http://korona.geog.uni-heidelberg.de/">GIScience Heidelberg</a>'});
var layer = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
accessToken: MAPBOX_API_KEY
});
map.addLayer(layer);
$.getJSON(API_LIEU + "set/"+lieux_list.join(';')+"/?format=json", onLoadLieux);
@ -84,7 +106,7 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
var greenIcon = makeIcon('red');
var blueIcon = makeIcon('blue', 1.2);
// Chargeùent des infos
// Chargement des infos
function onLoadLieux(data){
console.log(data);
var lieux = data.objects;
@ -133,7 +155,7 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
var html = $("<div>").html(marqueur._popup_header);
var stageliste = $("<ul>");
$.each(lieux_map[data.id], function(i, item) {
var stage_el = $("#resultat-stage-"+item);
var stage_el = stages_db[item];
var url = stage_el.find('a.stage-sujet').attr('href');
var sujet = stage_el.find('a.stage-sujet').text();
var auteur = stage_el.find('.auteur').text();
@ -146,19 +168,42 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
marqueur.setPopupContent(html[0]);
}
// Affichage de la liste hybride au survol + chargement asynchrone
function showDetailsListeListener (evt) {
showDetailsListe(this._lieu_data);
}
function showDetailsListe (data) {
function showDetailsListe (data, is_callback) {
main_container.addClass("vue-details");
var to_load = [];
var liste_el = $("#resultats-details");
$.each(liste_el.children(), function(i, item){$(item).remove();});
$.each(lieux_map[data.id], function(i, item) {
var stage_el = $("#resultat-stage-"+item);
var stage_el = stages_db[item];
if (stage_el === undefined) {
to_load.push(item);
return;
}
var new_el = $("<li>", {class:"stage"}).html(stage_el.html());
liste_el.append(new_el);
});
if (to_load.length > 0 && !is_callback) { // On évite la boucle si erreur
loadDetailsListe(to_load, data);
liste_el.append($("<li>", {class:"stage"}).html("Chargement..."));
}
}
function loadDetailsListe (liste, data) {
details_liste_data = data;
$.get(ITEMS_URL, {ids: liste.join(";")},
function (html) {
var temp_el = $("<ul>").html(html);
referenceStageItems(temp_el.children());
if (details_liste_data == data) {
showDetailsListe(data, true);
}
});
}
function unlockDetailsListe () {
@ -167,6 +212,7 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
}
function hideDetailsListeListener () {
details_liste_data = undefined;
if (details_lock === false)
main_container.removeClass("vue-details");
else
@ -195,6 +241,33 @@ function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
$("li.stage.expanded").removeClass("expanded");
}
//
// Pagination et chargement automatique
//
function initLoadMoreAJAX () {
var btn = $("#next-page-btn");
btn.on("click", loadMoreAJAX);
}
function loadMoreAJAX () {
var btn = this;
var url = btn.href;
if (btn.__is_loading) return false;
btn.innerHTML = "Chargement...";
$.get(url+"&format=raw", {}, function(html) {
$(btn).remove();
var new_els = $("<ul>").html(html).children();
$("#resultats").append(new_els);
referenceStageItems(new_els);
new_els.filter(".stage")
.on("mouseover touchdown", showLieuxFromStage)
.on("mouseout", hideLieuxSurvol);
initLoadMoreAJAX();
});
return false;
}
// __init__
initInterface();

View file

@ -1,4 +1,4 @@
function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
function SelectLieuWidget(STATIC_ROOT, API_LIEU, MAPBOX_API_KEY, target, callback) {
//
// INITIALISATION
@ -15,6 +15,10 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
var message_el = $el.find(".lieu-message");
var closer = $el.find(".window-closer");
var cmodif_el = $el.find(".lieu-choixmodif");
var lieuglobal_el = $el.find(".lieu-global");
var lieuoptions_el = $el.find(".lieu-options");
var suggestions_el = $el.find(".lieu-suggestions");
var newlieubtn_el = $el.find(".new-lieu-btn");
var marqueurs = L.markerClusterGroup();
// Variables globales
@ -59,7 +63,12 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
// Affiche la carte
map = L.map(map_el[0]).setView([48.8422411,2.3430553], 15);
var layer = new L.TileLayer("https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}", {attribution: 'Map tiles by <a href="http://korona.geog.uni-heidelberg.de/">GIScience Heidelberg</a>'});
var layer = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
accessToken: MAPBOX_API_KEY
});
map.addLayer(layer);
map.addLayer(marqueurs);
@ -149,8 +158,10 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
lieux_db.suggestion = data;
map.panTo(data.coord);
panMapTo(data.coord);
lieuSurCarte(data, redIcon);
newlieubtn_el.prop("_lieustage_data", data)
.on("click", choixLieuStage);
// Affichage des suggestions
askForSuggestions(place.geometry.location);
@ -166,17 +177,52 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
// Callback suggestions
function showPropositions(data) {
showMessage("Cliquez sur un des lieux déjà existants (en bleu) pour le choisir, ou sur votre recherche (en rouge) pour créer un nouveau lieu");
showMessage("Cliquez sur un des lieux déjà existants (en bleu) " +
"pour le choisir, ou sur votre recherche (en rouge) " +
"pour créer un nouveau lieu");
suggestions_el.html("");
lieuglobal_el.addClass("with-options");
map.invalidateSize();
data.objects.sort(function(a,b){ return a.distance-b.distance; });
function showdistance (dist) {
if(dist<1000) return Math.round(dist) + "m";
else return (Math.round(dist/100)/10) + "km";
}
// Affichage sur la carte
$.each(data.objects, function(i, item) {
var plieu = lieux_db[item.id];
if(plieu !== undefined)
if(plieu !== undefined) {
plieu.distance = item.distance;
item = plieu;
else
} else
lieux_db[item.id] = item;
var option_el = $("<li>", {class: "lieu-option"})
.append($("<p>", {class: "lieu-nom"}).text(item.nom))
.append($("<p>", {class: "lieu-infos"})
.append($("<span>", {class: "numstages"})
.text((item.num_stages || 0) + " stage"
+ (item.num_stages>1 ? "s" : "") + " ici"))
.append($("<span>", {class: "ville"})
.text(item.ville+", "+item.pays_nom))
.append($("<span>", {class: "distance"})
.text("à "+showdistance(item.distance))))
.append($("<a>", {class: "hoverlink",
href: "javascript:void(0)"}))
.prop("_lieustage_data", item)
.on("click", choixLieuStage);
suggestions_el.append(option_el);
lieuSurCarte(item, blueIcon);
});
if (data.objects.length == 0) {
var option_el = $("<li>", {class: "lieu-option"})
.append($("<p>", {class: "lieu-infos"})
.text("Aucun lieu déjà connu à proximité"));
suggestions_el.append(option_el);
}
panMapTo(lieux_db.suggestion.coord);
}
// Affiche un lieu sur la carte
@ -201,10 +247,14 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
data.marqueur = marqueur;
// Création de la description
var desc = $("<div>").append($("<h3>").text(data.nom))
.append($("<p>").text(data.ville+", "+data.pays_nom));
var desc = $("<div>").append($("<h4>").text(data.nom))
.append($("<p>")
.text(data.ville+", "+data.pays_nom));
if (data.num_stages !== undefined)
desc.append($("<p>").text(data.num_stages + (data.num_stages > 1 ? " stages" : " stage") + " à cet endroit"));
desc.append($("<p>")
.text(data.num_stages
+ (data.num_stages > 1 ? " stages" : " stage")
+ " à cet endroit"));
var activeBtn = $("<a>", {href:"javascript:void(0);"})
.prop("_lieustage_data", data)
@ -229,7 +279,7 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
function resetOrigLieu() {
var data = this._lieustage_data;
data.marqueur.setLatLng(data.orig_coord);
map.panTo(data.orig_coord);
panMapTo(data.orig_coord);
}
//
@ -248,12 +298,15 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
if (modification == true) {
setUIMode("ajout");
form_el.find("#id_id").val(choix.id);
form_el.find("h3").text("Modifier un lieu");
form_el.find(".form-title").text("Modifier un lieu");
} else {
setUIMode("edit");
form_el.find("#id_id").val('');
form_el.find("h3").text("Créer un lieu");
form_el.find(".form-title").text("Créer un lieu");
}
map.invalidateSize();
panMapTo(choix.coord);
}
// Envoi du formulaire
@ -306,9 +359,9 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
var lieu = lieux_db[modiflieu_id];
lieu.fromSuggestion = true;
map.panTo(lieu.coord);
panMapTo(lieu.coord);
lieuSurCarte(lieu, greenIcon);
showForm(lieu, true);
}
@ -318,9 +371,13 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
var lieu = lieux_db[modiflieu_id];
lieu.fromSuggestion = false;
map.panTo(lieu.coord);
lieuSurCarte(lieu, greenIcon);
map.invalidateSize();
panMapTo(lieu.coord);
lieuSurCarte(lieu, greenIcon);
newlieubtn_el.prop("_lieustage_data", lieu)
.on("click", choixLieuStage);
lieux_db.suggestion = lieu;
askForSuggestions(lieu.coord);
}
@ -332,6 +389,7 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
initUI();
input.val('');
lieuglobal_el.removeClass("with-options");
// Nettoyage
hideMessage();
@ -344,14 +402,14 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
if (lieu_id === undefined) {
// Choix d'un nouveau lieu : pas grand-chose à faire
ui_mode_creation = true;
$el.find("h3").text("Ajouter un lieu");
$el.find(".window-title").text("Ajouter un lieu");
map_el.addClass("masked");
} else {
// Lieu déjà existant
lieu_id = lieu_id * 1;
ui_mode_creation = false;
modiflieu_id = lieu_id;
$el.find("h3").text("Modifier le lieu");
$el.find(".window-title").text("Modifier le lieu");
// Chargement des infos
if(lieux_db[lieu_id] === undefined) {
@ -364,6 +422,10 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
}
}
function panMapTo(coord) {
map.panTo(coord);
}
// Fermeture du widget
function closeWidget () {
$el.removeClass("visible");
@ -373,7 +435,7 @@ function SelectLieuWidget(STATIC_ROOT, API_LIEU, target, callback) {
// Le lieu est choisi, on appelle le callback
function choixLieuStage() {
var choix = this._lieustage_data;
if(!choix.fromSuggestion)
if(!choix.fromSuggestion && this !== newlieubtn_el[0])
callback(choix);
else
showForm(choix);

View file

@ -1,330 +1,337 @@
# coding: utf-8
DEPARTEMENTS_DEFAUT = (
('phy', u'Physique'),
('maths', u'Maths'),
('bio', u'Biologie'),
('chimie', u'Chimie'),
('geol', u'Géosciences'),
('dec', u'DEC'),
('info', u'Informatique'),
('litt', u'Littéraire'),
('guests', u'Pensionnaires étrangers'),
('pei', u'PEI'),
("phy", "Physique"),
("maths", "Maths"),
("bio", "Biologie"),
("chimie", "Chimie"),
("geol", "Géosciences"),
("dec", "DEC"),
("info", "Informatique"),
("litt", "Littéraire"),
("guests", "Pensionnaires étrangers"),
("pei", "PEI"),
)
TYPE_STAGE_OPTIONS = (
(u'Recherche :', (
('recherche', u"Stage académique"),
('recherche_autre', u"Stage non-académique"),
('sejour_dri', u"Séjour de recherche DRI"),
)),
(u'Stage sans visée de recherche :', (
('pro', u"Stage en entreprise"),
('admin', u"Stage en admin./ONG/orga. internationale"),
)),
(u'Enseignement :', (
('lectorat', u"Lectorat DRI"),
('autre_teach', u"Autre expérience d'enseignement"),
)),
('autre', u"Autre"),
(
"Recherche :",
(
("recherche", "Stage académique"),
("recherche_autre", "Stage non-académique"),
("sejour_dri", "Séjour de recherche DRI"),
),
),
(
"Stage sans visée de recherche :",
(
("pro", "Stage en entreprise"),
("admin", "Stage en admin./ONG/orga. internationale"),
),
),
(
"Enseignement :",
(
("lectorat", "Lectorat DRI"),
("autre_teach", "Autre expérience d'enseignement"),
),
),
("autre", "Autre"),
)
# Dictionnaire des type de stage (et de leur genre, True=féminin)
TYPE_STAGE_DICT = {
'recherche': (u"stage de recherche académique", False),
'recherche_autre': (u"stage de recherche non-académique", False),
'sejour_dri': (u"séjour de recherche DRI", False),
'pro': (u"stage en entreprise sans visée de recherche", False),
'admin': (u"stage en administration, ONG ou organisation internationale", False),
'lectorat': (u"lectorat DRI", False),
'autre_teach': (u"expérience de recherche", True),
'autre': (u"expérience", True),
"recherche": ("stage de recherche académique", False),
"recherche_autre": ("stage de recherche non-académique", False),
"sejour_dri": ("séjour de recherche DRI", False),
"pro": ("stage en entreprise sans visée de recherche", False),
"admin": ("stage en administration, ONG ou organisation internationale", False),
"lectorat": ("lectorat DRI", False),
"autre_teach": ("expérience de recherche", True),
"autre": ("expérience", True),
}
TYPE_LIEU_OPTIONS = (
('universite', u"Université"),
('entreprise', u"Entreprise"),
('centrerecherche', u"Centre de recherche"),
('administration', u"Administration"),
('autre', u"Autre"),
("universite", "Université"),
("entreprise", "Entreprise"),
("centrerecherche", "Centre de recherche"),
("administration", "Administration"),
("autre", "Autre"),
)
# Place du stage dans le cursus
NIVEAU_SCOL_OPTIONS = (
('L3', u"Licence 3"),
('M1', u"Master 1"),
('M2', u"Master 2"),
('DOC', u"Pré-doctorat"),
('CST', u"Césure"),
('BLA', u"Année blanche"),
('VAC', u"Vacances scolaires"),
('MIT', u"Mi-temps en parallèle des études"),
('', u"Autre"),
("L3", "Licence 3"),
("M1", "Master 1"),
("M2", "Master 2"),
("DOC", "Pré-doctorat"),
("CST", "Césure"),
("BLA", "Année blanche"),
("VAC", "Vacances scolaires"),
("MIT", "Mi-temps en parallèle des études"),
("", "Autre"),
)
NIVEAU_SCOL_DICT = {
"L3": u"pendant sa troisième année de Licence",
"M1": u"pendant sa première année de Master",
"M2": u"pendant sa deuxième année de Master",
"DOC": u"pendant son année de pré-doctorat",
"CST": u"pendant une année de césure",
"BLA": u"pendant une année blanche",
"VAC": u"pendant des vacances scolaires",
"MIT": u"à mi-temps en parallèle des études",
"L3": "pendant sa troisième année de Licence",
"M1": "pendant sa première année de Master",
"M2": "pendant sa deuxième année de Master",
"DOC": "pendant son année de pré-doctorat",
"CST": "pendant une année de césure",
"BLA": "pendant une année blanche",
"VAC": "pendant des vacances scolaires",
"MIT": "à mi-temps en parallèle des études",
}
# Dictionnaire des noms de lieux (et de leur genre, True=féminin)
TYPE_LIEU_DICT = {
'universite': (u"université", True),
'entreprise': (u"entreprise", True),
'centrerecherche': (u"centre de recherche", False),
'administration': (u"administration", True),
'autre': (u"lieu", False),
"universite": ("université", True),
"entreprise": ("entreprise", True),
"centrerecherche": ("centre de recherche", False),
"administration": ("administration", True),
"autre": ("lieu", False),
}
PAYS_OPTIONS = (
("AF", u"Afghanistan"),
("AL", u"Albanie"),
("AQ", u"Antarctique"),
("DZ", u"Algérie"),
("AS", u"Samoa Américaines"),
("AD", u"Andorre"),
("AO", u"Angola"),
("AG", u"Antigua-et-Barbuda"),
("AZ", u"Azerbaïdjan"),
("AR", u"Argentine"),
("AU", u"Australie"),
("AT", u"Autriche"),
("BS", u"Bahamas"),
("BH", u"Bahreïn"),
("BD", u"Bangladesh"),
("AM", u"Arménie"),
("BB", u"Barbade"),
("BE", u"Belgique"),
("BM", u"Bermudes"),
("BT", u"Bhoutan"),
("BO", u"Bolivie"),
("BA", u"Bosnie-Herzégovine"),
("BW", u"Botswana"),
("BV", u"Île Bouvet"),
("BR", u"Brésil"),
("BZ", u"Belize"),
("IO", u"Territoire Britannique de l'Océan Indien"),
("SB", u"Îles Salomon"),
("VG", u"Îles Vierges Britanniques"),
("BN", u"Brunéi Darussalam"),
("BG", u"Bulgarie"),
("MM", u"Myanmar"),
("BI", u"Burundi"),
("BY", u"Bélarus"),
("KH", u"Cambodge"),
("CM", u"Cameroun"),
("CA", u"Canada"),
("CV", u"Cap-vert"),
("KY", u"Îles Caïmanes"),
("CF", u"République Centrafricaine"),
("LK", u"Sri Lanka"),
("TD", u"Tchad"),
("CL", u"Chili"),
("CN", u"Chine"),
("TW", u"Taïwan"),
("CX", u"Île Christmas"),
("CC", u"Îles Cocos (Keeling)"),
("CO", u"Colombie"),
("KM", u"Comores"),
("YT", u"Mayotte"),
("CG", u"République du Congo"),
("CD", u"République Démocratique du Congo"),
("CK", u"Îles Cook"),
("CR", u"Costa Rica"),
("HR", u"Croatie"),
("CU", u"Cuba"),
("CY", u"Chypre"),
("CZ", u"République Tchèque"),
("BJ", u"Bénin"),
("DK", u"Danemark"),
("DM", u"Dominique"),
("DO", u"République Dominicaine"),
("EC", u"Équateur"),
("SV", u"El Salvador"),
("GQ", u"Guinée Équatoriale"),
("ET", u"Éthiopie"),
("ER", u"Érythrée"),
("EE", u"Estonie"),
("FO", u"Îles Féroé"),
("FK", u"Îles (malvinas) Falkland"),
("GS", u"Géorgie du Sud et les Îles Sandwich du Sud"),
("FJ", u"Fidji"),
("FI", u"Finlande"),
("AX", u"Îles Åland"),
("FR", u"France"),
("GF", u"Guyane Française"),
("PF", u"Polynésie Française"),
("TF", u"Terres Australes Françaises"),
("DJ", u"Djibouti"),
("GA", u"Gabon"),
("GE", u"Géorgie"),
("GM", u"Gambie"),
("PS", u"Territoire Palestinien Occupé"),
("DE", u"Allemagne"),
("GH", u"Ghana"),
("GI", u"Gibraltar"),
("KI", u"Kiribati"),
("GR", u"Grèce"),
("GL", u"Groenland"),
("GD", u"Grenade"),
("GP", u"Guadeloupe"),
("GU", u"Guam"),
("GT", u"Guatemala"),
("GN", u"Guinée"),
("GY", u"Guyana"),
("HT", u"Haïti"),
("HM", u"Îles Heard et Mcdonald"),
("VA", u"Saint-Siège (état de la Cité du Vatican)"),
("HN", u"Honduras"),
("HK", u"Hong-Kong"),
("HU", u"Hongrie"),
("IS", u"Islande"),
("IN", u"Inde"),
("ID", u"Indonésie"),
("IR", u"République Islamique d'Iran"),
("IQ", u"Iraq"),
("IE", u"Irlande"),
("IL", u"Israël"),
("IT", u"Italie"),
("CI", u"Côte d'Ivoire"),
("JM", u"Jamaïque"),
("JP", u"Japon"),
("KZ", u"Kazakhstan"),
("JO", u"Jordanie"),
("KE", u"Kenya"),
("KP", u"République Populaire Démocratique de Corée"),
("KR", u"République de Corée"),
("KW", u"Koweït"),
("KG", u"Kirghizistan"),
("LA", u"République Démocratique Populaire Lao"),
("LB", u"Liban"),
("LS", u"Lesotho"),
("LV", u"Lettonie"),
("LR", u"Libéria"),
("LY", u"Jamahiriya Arabe Libyenne"),
("LI", u"Liechtenstein"),
("LT", u"Lituanie"),
("LU", u"Luxembourg"),
("MO", u"Macao"),
("MG", u"Madagascar"),
("MW", u"Malawi"),
("MY", u"Malaisie"),
("MV", u"Maldives"),
("ML", u"Mali"),
("MT", u"Malte"),
("MQ", u"Martinique"),
("MR", u"Mauritanie"),
("MU", u"Maurice"),
("MX", u"Mexique"),
("MC", u"Monaco"),
("MN", u"Mongolie"),
("MD", u"République de Moldova"),
("MS", u"Montserrat"),
("MA", u"Maroc"),
("MZ", u"Mozambique"),
("OM", u"Oman"),
("NA", u"Namibie"),
("NR", u"Nauru"),
("NP", u"Népal"),
("NL", u"Pays-Bas"),
("AN", u"Antilles Néerlandaises"),
("AW", u"Aruba"),
("NC", u"Nouvelle-Calédonie"),
("VU", u"Vanuatu"),
("NZ", u"Nouvelle-Zélande"),
("NI", u"Nicaragua"),
("NE", u"Niger"),
("NG", u"Nigéria"),
("NU", u"Niué"),
("NF", u"Île Norfolk"),
("NO", u"Norvège"),
("MP", u"Îles Mariannes du Nord"),
("UM", u"Îles Mineures Éloignées des États-Unis"),
("FM", u"États Fédérés de Micronésie"),
("MH", u"Îles Marshall"),
("PW", u"Palaos"),
("PK", u"Pakistan"),
("PA", u"Panama"),
("PG", u"Papouasie-Nouvelle-Guinée"),
("PY", u"Paraguay"),
("PE", u"Pérou"),
("PH", u"Philippines"),
("PN", u"Pitcairn"),
("PL", u"Pologne"),
("PT", u"Portugal"),
("GW", u"Guinée-Bissau"),
("TL", u"Timor-Leste"),
("PR", u"Porto Rico"),
("QA", u"Qatar"),
("RE", u"Réunion"),
("RO", u"Roumanie"),
("RU", u"Fédération de Russie"),
("RW", u"Rwanda"),
("SH", u"Sainte-Hélène"),
("KN", u"Saint-Kitts-et-Nevis"),
("AI", u"Anguilla"),
("LC", u"Sainte-Lucie"),
("PM", u"Saint-Pierre-et-Miquelon"),
("VC", u"Saint-Vincent-et-les Grenadines"),
("SM", u"Saint-Marin"),
("ST", u"Sao Tomé-et-Principe"),
("SA", u"Arabie Saoudite"),
("SN", u"Sénégal"),
("SC", u"Seychelles"),
("SL", u"Sierra Leone"),
("SG", u"Singapour"),
("SK", u"Slovaquie"),
("VN", u"Viet Nam"),
("SI", u"Slovénie"),
("SO", u"Somalie"),
("ZA", u"Afrique du Sud"),
("ZW", u"Zimbabwe"),
("ES", u"Espagne"),
("EH", u"Sahara Occidental"),
("SD", u"Soudan"),
("SR", u"Suriname"),
("SJ", u"Svalbard etÎle Jan Mayen"),
("SZ", u"Swaziland"),
("SE", u"Suède"),
("CH", u"Suisse"),
("SY", u"République Arabe Syrienne"),
("TJ", u"Tadjikistan"),
("TH", u"Thaïlande"),
("TG", u"Togo"),
("TK", u"Tokelau"),
("TO", u"Tonga"),
("TT", u"Trinité-et-Tobago"),
("AE", u"Émirats Arabes Unis"),
("TN", u"Tunisie"),
("TR", u"Turquie"),
("TM", u"Turkménistan"),
("TC", u"Îles Turks et Caïques"),
("TV", u"Tuvalu"),
("UG", u"Ouganda"),
("UA", u"Ukraine"),
("MK", u"L'ex-République Yougoslave de Macédoine"),
("EG", u"Égypte"),
("GB", u"Royaume-Uni"),
("IM", u"Île de Man"),
("TZ", u"République-Unie de Tanzanie"),
("US", u"États-Unis"),
("VI", u"Îles Vierges des États-Unis"),
("BF", u"Burkina Faso"),
("UY", u"Uruguay"),
("UZ", u"Ouzbékistan"),
("VE", u"Venezuela"),
("WF", u"Wallis et Futuna"),
("WS", u"Samoa"),
("YE", u"Yémen"),
("CS", u"Serbie-et-Monténégro"),
("ZM", u"Zambie"),
("AF", "Afghanistan"),
("AL", "Albanie"),
("AQ", "Antarctique"),
("DZ", "Algérie"),
("AS", "Samoa Américaines"),
("AD", "Andorre"),
("AO", "Angola"),
("AG", "Antigua-et-Barbuda"),
("AZ", "Azerbaïdjan"),
("AR", "Argentine"),
("AU", "Australie"),
("AT", "Autriche"),
("BS", "Bahamas"),
("BH", "Bahreïn"),
("BD", "Bangladesh"),
("AM", "Arménie"),
("BB", "Barbade"),
("BE", "Belgique"),
("BM", "Bermudes"),
("BT", "Bhoutan"),
("BO", "Bolivie"),
("BA", "Bosnie-Herzégovine"),
("BW", "Botswana"),
("BV", "Île Bouvet"),
("BR", "Brésil"),
("BZ", "Belize"),
("IO", "Territoire Britannique de l'Océan Indien"),
("SB", "Îles Salomon"),
("VG", "Îles Vierges Britanniques"),
("BN", "Brunéi Darussalam"),
("BG", "Bulgarie"),
("MM", "Myanmar"),
("BI", "Burundi"),
("BY", "Bélarus"),
("KH", "Cambodge"),
("CM", "Cameroun"),
("CA", "Canada"),
("CV", "Cap-vert"),
("KY", "Îles Caïmanes"),
("CF", "République Centrafricaine"),
("LK", "Sri Lanka"),
("TD", "Tchad"),
("CL", "Chili"),
("CN", "Chine"),
("TW", "Taïwan"),
("CX", "Île Christmas"),
("CC", "Îles Cocos (Keeling)"),
("CO", "Colombie"),
("KM", "Comores"),
("YT", "Mayotte"),
("CG", "République du Congo"),
("CD", "République Démocratique du Congo"),
("CK", "Îles Cook"),
("CR", "Costa Rica"),
("HR", "Croatie"),
("CU", "Cuba"),
("CY", "Chypre"),
("CZ", "République Tchèque"),
("BJ", "Bénin"),
("DK", "Danemark"),
("DM", "Dominique"),
("DO", "République Dominicaine"),
("EC", "Équateur"),
("SV", "El Salvador"),
("GQ", "Guinée Équatoriale"),
("ET", "Éthiopie"),
("ER", "Érythrée"),
("EE", "Estonie"),
("FO", "Îles Féroé"),
("FK", "Îles (malvinas) Falkland"),
("GS", "Géorgie du Sud et les Îles Sandwich du Sud"),
("FJ", "Fidji"),
("FI", "Finlande"),
("AX", "Îles Åland"),
("FR", "France"),
("GF", "Guyane Française"),
("PF", "Polynésie Française"),
("TF", "Terres Australes Françaises"),
("DJ", "Djibouti"),
("GA", "Gabon"),
("GE", "Géorgie"),
("GM", "Gambie"),
("PS", "Territoire Palestinien Occupé"),
("DE", "Allemagne"),
("GH", "Ghana"),
("GI", "Gibraltar"),
("KI", "Kiribati"),
("GR", "Grèce"),
("GL", "Groenland"),
("GD", "Grenade"),
("GP", "Guadeloupe"),
("GU", "Guam"),
("GT", "Guatemala"),
("GN", "Guinée"),
("GY", "Guyana"),
("HT", "Haïti"),
("HM", "Îles Heard et Mcdonald"),
("VA", "Saint-Siège (état de la Cité du Vatican)"),
("HN", "Honduras"),
("HK", "Hong-Kong"),
("HU", "Hongrie"),
("IS", "Islande"),
("IN", "Inde"),
("ID", "Indonésie"),
("IR", "République Islamique d'Iran"),
("IQ", "Iraq"),
("IE", "Irlande"),
("IL", "Israël"),
("IT", "Italie"),
("CI", "Côte d'Ivoire"),
("JM", "Jamaïque"),
("JP", "Japon"),
("KZ", "Kazakhstan"),
("JO", "Jordanie"),
("KE", "Kenya"),
("KP", "République Populaire Démocratique de Corée"),
("KR", "République de Corée"),
("KW", "Koweït"),
("KG", "Kirghizistan"),
("LA", "République Démocratique Populaire Lao"),
("LB", "Liban"),
("LS", "Lesotho"),
("LV", "Lettonie"),
("LR", "Libéria"),
("LY", "Jamahiriya Arabe Libyenne"),
("LI", "Liechtenstein"),
("LT", "Lituanie"),
("LU", "Luxembourg"),
("MO", "Macao"),
("MG", "Madagascar"),
("MW", "Malawi"),
("MY", "Malaisie"),
("MV", "Maldives"),
("ML", "Mali"),
("MT", "Malte"),
("MQ", "Martinique"),
("MR", "Mauritanie"),
("MU", "Maurice"),
("MX", "Mexique"),
("MC", "Monaco"),
("MN", "Mongolie"),
("MD", "République de Moldova"),
("MS", "Montserrat"),
("MA", "Maroc"),
("MZ", "Mozambique"),
("OM", "Oman"),
("NA", "Namibie"),
("NR", "Nauru"),
("NP", "Népal"),
("NL", "Pays-Bas"),
("AN", "Antilles Néerlandaises"),
("AW", "Aruba"),
("NC", "Nouvelle-Calédonie"),
("VU", "Vanuatu"),
("NZ", "Nouvelle-Zélande"),
("NI", "Nicaragua"),
("NE", "Niger"),
("NG", "Nigéria"),
("NU", "Niué"),
("NF", "Île Norfolk"),
("NO", "Norvège"),
("MP", "Îles Mariannes du Nord"),
("UM", "Îles Mineures Éloignées des États-Unis"),
("FM", "États Fédérés de Micronésie"),
("MH", "Îles Marshall"),
("PW", "Palaos"),
("PK", "Pakistan"),
("PA", "Panama"),
("PG", "Papouasie-Nouvelle-Guinée"),
("PY", "Paraguay"),
("PE", "Pérou"),
("PH", "Philippines"),
("PN", "Pitcairn"),
("PL", "Pologne"),
("PT", "Portugal"),
("GW", "Guinée-Bissau"),
("TL", "Timor-Leste"),
("PR", "Porto Rico"),
("QA", "Qatar"),
("RE", "Réunion"),
("RO", "Roumanie"),
("RU", "Fédération de Russie"),
("RW", "Rwanda"),
("SH", "Sainte-Hélène"),
("KN", "Saint-Kitts-et-Nevis"),
("AI", "Anguilla"),
("LC", "Sainte-Lucie"),
("PM", "Saint-Pierre-et-Miquelon"),
("VC", "Saint-Vincent-et-les Grenadines"),
("SM", "Saint-Marin"),
("ST", "Sao Tomé-et-Principe"),
("SA", "Arabie Saoudite"),
("SN", "Sénégal"),
("SC", "Seychelles"),
("SL", "Sierra Leone"),
("SG", "Singapour"),
("SK", "Slovaquie"),
("VN", "Viet Nam"),
("SI", "Slovénie"),
("SO", "Somalie"),
("ZA", "Afrique du Sud"),
("ZW", "Zimbabwe"),
("ES", "Espagne"),
("EH", "Sahara Occidental"),
("SD", "Soudan"),
("SR", "Suriname"),
("SJ", "Svalbard etÎle Jan Mayen"),
("SZ", "Swaziland"),
("SE", "Suède"),
("CH", "Suisse"),
("SY", "République Arabe Syrienne"),
("TJ", "Tadjikistan"),
("TH", "Thaïlande"),
("TG", "Togo"),
("TK", "Tokelau"),
("TO", "Tonga"),
("TT", "Trinité-et-Tobago"),
("AE", "Émirats Arabes Unis"),
("TN", "Tunisie"),
("TR", "Turquie"),
("TM", "Turkménistan"),
("TC", "Îles Turks et Caïques"),
("TV", "Tuvalu"),
("UG", "Ouganda"),
("UA", "Ukraine"),
("MK", "L'ex-République Yougoslave de Macédoine"),
("EG", "Égypte"),
("GB", "Royaume-Uni"),
("IM", "Île de Man"),
("TZ", "République-Unie de Tanzanie"),
("US", "États-Unis"),
("VI", "Îles Vierges des États-Unis"),
("BF", "Burkina Faso"),
("UY", "Uruguay"),
("UZ", "Ouzbékistan"),
("VE", "Venezuela"),
("WF", "Wallis et Futuna"),
("WS", "Samoa"),
("YE", "Yémen"),
("CS", "Serbie-et-Monténégro"),
("ZM", "Zambie"),
)

View file

@ -0,0 +1,13 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Page non trouvée{% endblock %}
{% block content %}
<article>
<section>
<h1>Page non trouvée</h1>
<p>Cette page n'existe pas, ou peut-être que vous n'y avez pas accès.</p>
</section>
</article>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Accès interdit{% endblock %}
{% block content %}
<article>
<section>
<h1>Accès réservé aux personnes en scolarité</h1>
<p>Vous pouvez pas consulter cette page : après la fin de votre scolarité, vous ne pouvez accéder qu'à votre profil et vos fiches de stages pour les tenir à jour.</p>
<p><a href="{% url "avisstage:perso" %}">Aller à mon tableau de bord</a></p>
</section>
</article>
{% endblock %}

View file

@ -5,8 +5,6 @@
<meta charset="utf-8" />
<title>{% block title %}ExperiENS{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0; maximum-scale=1.0; minimum-scale=1.0;">
<link type="text/css" rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static 'css/screen.css' %}" />
<script type="text/javascript" src="{% static "js/jquery-3.2.0.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery-ui.min.js" %}"></script>
{% if request.user.is_authenticated %}
@ -16,9 +14,12 @@
{% endif %}
{% block extra_head %}{% endblock %}
<link type="text/css" rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static 'css/screen.css' %}" />
</head>
<body>
<body class="{% block bodyclass %}{% endblock %}">
<header>
<h1><a href="{% url 'avisstage:index' %}">ExperiENS{# <span class='beta'>beta</span>#}</a></h1>
@ -27,16 +28,18 @@
<ul id="menu">
{% if user.is_authenticated %}
<li><a href="{% url 'avisstage:perso' %}">Mon expérience</a></li>
<li><a href="{% url 'avisstage:recherche' %}">Recherche</a></li>
{% if user.profil.en_scolarite %}
<li><a href="{% url 'avisstage:recherche' %}">Recherche</a></li>
{% endif %}
{% endif %}
<li><a href="{% url 'avisstage:faq' %}">FAQ</a></li>
{% if user.is_staff %}
<li><a href="{% url 'avisstage:moderation' %}">Modo</a></li>
{% endif %}
{% if user.is_authenticated %}
<li><a href="{% url 'logout' %}"><span class="username">{{ user.username }}</span><br/> Déconnexion</a></li>
<li><a href="{% url "authens:logout" %}"><span class="username">{{ user.username }}</span><br/> Déconnexion</a></li>
{% else %}
<li><a href="{% url 'login' %}">Connexion</a></li>
<li><a href="{% url "authens:login" %}">Connexion</a></li>
{% endif %}
</ul>
</nav>

View file

@ -0,0 +1,19 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Confirmation requise - ExperiENS{% endblock %}
{% block content %}
<h1>Confirmation requise</h1>
<article>
{% if object.confirmed_at %}
<p>L'adresse {{ object.email }} a déjà été confirmée.</p>
{% else %}
<p>Un mail de confirmation vous a été envoyé à l'adresse {{ object.email }} pour la vérifier.</p>
<p>Merci de cliquer sur le lien inclus pour confirmer qu'elle est correcte.</p>
<p>Si vous ne recevez rien, vérifier dans vos indésirables.</p>
{% endif %}
<p><a href="{% url "avisstage:parametres" %}">Retour</a></p>
</article>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block content %}
<h1>Définir un mot de passe</h1>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="field">
<label>Nom d'utilisateur</label>
<div class="input">
{{ user.username }}
</div>
</div>
{% for field in form %}
{{ field.errors }}
<div class="field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="input">
{{ field }}
{% if field.help_text %}
<p class="help_text">{{ field.help_text }}</p>
{% endif %}
</div>
</div>
{% endfor %}
<input type="submit" value="Enregistrer" />
</form>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Supprimer une adresse mail - ExperiENS{% endblock %}
{% block content %}
<h1>Supprimer une adresse mail</h1>
<article>
<section class="profil">
<form action="" method="POST">
{% csrf_token %}
<p>Êtes-vous sûr⋅e de vouloir supprimer l'adresse mail {{ object.email }} ?</p>
<p><a href="{% url "avisstage:parametres" %}">Retour</a> &nbsp; <input type="submit" value="Confirmer la suppression"></p>
</form>
</section>
</article>
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Mes paramètres - ExperiENS{% endblock %}
{% block content %}
<h1>Mes paramètres</h1>
<article>
<h2>Adresses e-mail</h2>
<ul class="mes-emails">
{% for email in request.user.email_address_set.all %}
<li>
<span class="adresse">{{ email.email }}
<span class="confirmee">{{ email.confirmed_at|yesno:"&#10003;,&#10007;"|safe }}</span></span>
{% if email.confirmed_at %}
<span class="principale">
{% if email.email == user.email %}
Principale
{% else %}
<form action="{% url "avisstage:emails_principal" email.email %}" method="POST">
{% csrf_token %}
<input type="submit" value="Rendre principale">
</form>
{% endif %}
</span>
{% else %}
<span class="confirmer">
<form action="{% url "avisstage:emails_reconfirme" email.email %}" method="POST">
{% csrf_token %}
<input type="submit" value="Renvoyer le lien de confirmation">
</form>
</span>
{% endif %}
<span class="supprimer">
{% if not email.email == user.email %}
<a href="{% url "avisstage:emails_supprime" email.email %}">Supprimer</a>
{% endif %}
</span>
</li>
{% endfor %}
<li>
<form action="" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field.errors }}
<div class="field">
<div class="input">
{{ field }}
</div>
</div>
{% endfor %}
<input type="submit" value="Ajouter l'adresse">
</form>
</li>
</ul>
</article>
<article>
<h2>Mot de passe</h2>
<section class="profil">
{% if request.user.password and request.user.has_usable_password %}
<p>Un mot de passe interne est déjà défini pour ce compte.</p>
{% else %}
<p>Aucun mot de passe n'est défini pour ce compte. Créez-en un pour pouvoir vous connecter après la fin de votre scolarité à l'ENS.</p>
{% endif %}
<form action="{% url "avisstage:mdp_demande" %}" method="POST">
{% csrf_token %}
<input type="submit" value="Définir un nouveau mot de passe" />
</form>
<p>En cliquant sur ce bouton, un lien unique vous sera envoyé à votre adresse e-mail principale ({{ request.user.email }}) qui vous donnera accès au formulaire d'édition du mot de passe.</p>
</section>
</article>
{% endblock %}

View file

@ -5,10 +5,10 @@
<script type="text/javascript" src="{% static "js/render.js" %}"></script>
{% endblock %}
{% block title %}Profil de {{ object.nom }} - ExperiENS{% endblock %}
{% block title %}Profil de {{ object.nom_complet }} - ExperiENS{% endblock %}
{% block content %}
<h1>Profil de {{ object.nom }}
<h1>Profil de {{ object.nom_complet }}
{% if object.user == user %}
<a href="{% url "avisstage:profil_edit" %}" class="btn edit-btn">Modifier mes infos</a>
{% endif %}
@ -18,7 +18,7 @@
<p class="promo">Promotion : <b>{{ object.promotion }}</b></p>
<p class="contact">
{% if object.contactez_moi %}
Contact : <a href="mailto:{{ object.mail }}">{{ object.mail }}</a>
Contact : <a href="mailto:{{ object.preferred_email }}">{{ object.preferred_email }}</a>
{% endif %}
</p>
</div>

View file

@ -6,6 +6,9 @@
{% block extra_head %}
<script src="{% static 'js/toc.min.js' %}" type="text/javascript"></script>
<script type="text/javascript" src="{% static "js/leaflet.js" %}"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.css' rel='stylesheet' />
<script src="https://cdn.maptiler.com/mapbox-gl-leaflet/latest/leaflet-mapbox-gl.js"></script>
<link rel="stylesheet" type="text/css" href="{% static "css/leaflet.css" %}" />
<script type="text/javascript" src="{% static "js/tile.stamen.js" %}"></script>
<script type="text/javascript">
@ -13,7 +16,12 @@
var STATIC_ROOT = "{{ STATIC_URL|escapejs }}";
function initStageMap(lieux) {
var map = L.map("stage-map");
var layer = new L.TileLayer("https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}", {attribution: 'Map tiles by <a href="http://korona.geog.uni-heidelberg.de/">GIScience Heidelberg</a>'});
var layer = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
accessToken: "{{ MAPBOX_API_KEY|escapejs }}"
});
map.addLayer(layer);
function makeIcon(couleur){
@ -51,22 +59,25 @@
<p>Cette page n'est qu'un brouillon, vous seul pouvez le voir. <input type="submit" value="Publier ce stage" name="publier" /></p>
{% endif %}
</form>
{% if not object.all_lieux %}
<p class="warning">Vous n'avez pas indiqué de lieu pour cette expérience. Pensez à en <a href="{% url "avisstage:stage_edit" object.id %}">ajouter un</a> pour pouvoir donner un avis sur cet endroit, et que votre expérience apparaisse sur la carte.</p>
{% endif %}
</div>
{% endif %}
<article class="stage">
<section class="misc">
<div class="misc-content {% if object.lieux.all %}withmap{% endif %}">
<div class="misc-content {% if object.all_lieux %}withmap{% endif %}">
<div class="desc">
<div class="misc-hdr">
<h1>{{ object.sujet }}</h1>
<p class="dates"><span class="year">{{ object.date_debut|date:"Y" }}</span><span class="debut">{{ object.date_debut|date:"d/m" }}</span><span class="fin">{{ object.date_fin|date:"d/m" }}</span></p>
</div>
<p><a href="{% url "avisstage:profil" object.auteur.user.username %}">{{ object.auteur.nom }}</a>
<p><a href="{% url "avisstage:profil" object.auteur.user.username %}">{{ object.auteur.nom_complet }}</a>
a fait {{ object.type_stage_fem|yesno:"cette,ce" }} <b>{{ object.type_stage_fancy }}</b>
{% if object.niveau_scol %}{{ object.niveau_scol_fancy }},{% endif %}
{% if object.structure %}au sein de {{ object.structure }}{% endif %}{% if object.encadrants %}, supervisé par {{ object.encadrants }}{% endif %}.</p>
{% if object.lieux.all %}<p>Cela s'est passé à :
{% for lieu in object.lieux.all %}{{ lieu.nom }} ({{ lieu.ville }}){% if not forloop.last %}, {% endif %}{% endfor %}.</p>
{% if object.all_lieux %}<p>Cela s'est passé à :
{% for lieu in object.all_lieux %}{{ lieu.nom }} ({{ lieu.ville }}){% if not forloop.last %}, {% endif %}{% endfor %}.</p>
{% endif %}
<ul class="infos">
@ -79,12 +90,12 @@
</ul>
</div>
{% if object.lieux.all %}
{% if object.all_lieux %}
<div class="map">
<div id="stage-map"></div>
<script type="text/javascript">
var lieux = [
{% for lieu in object.lieux.all %}
{% for lieu in object.all_lieux %}
{
coord: {lat: "{{ lieu.coord.y|escapejs }}", lon: "{{ lieu.coord.x|escapejs }}" },
popup: "<h3>{{ lieu.nom|escapejs }}</h3>" +

View file

@ -74,7 +74,8 @@
<h3>Je n'ai plus de compte clipper, puis-je accéder à ce site&nbsp;?</h3>
<p>Pour conserver l'accès à ce site limité, et garantir une certaine liberté de parole, seuls les normalien⋅ne⋅s en scolarité ont accès au site entier.</p>
<p>En revanche, si vous écrivez des avis ici, vous pourrez toujours les modifier ou les supprimer. Une procédure de connexion spécifique est prévue, mais pas encore implémentée.</p>
<p>En revanche, si vous écrivez des avis ici, vous pourrez toujours les modifier ou les supprimer. Il suffira d'utiliser l'accès archicubes, avec des identifiants spécifiques à ce site.</p>
<p>Si vous aviez un compte dont vous avez perdu l'accès, vous pouvez contacter le <a href="https://www.eleves.ens.fr/home/klub-dev/">Klub Dev ENS</a> pour qu'on vous donne des accès.
</article>
<article>
@ -88,7 +89,7 @@
<p>Faites-en part en cliquant sur le bouton feedback, on est preneur&nbsp;!</p>
<h3>Qui est derrière&nbsp;?</h3>
<p>Cette plateforme a été réalisée par <a href="http://www.robin-champenois.fr">Robin Champenois</a> (Info 2012) en django, sur une idée originale de Damien Moulin (Physique 2013). Le code source est disponible <a href="https://git.eleves.ens.fr/champeno/experiENS">ici</a>. Le site est hébergé sur le serveur des élèves.</p>
<p>Cette plateforme a été lancée en 2017 par <a href="http://www.robin-champenois.fr">Robin Champenois</a> (Info 2012) en django, sur une idée originale de Damien Moulin (Physique 2013). Il est désormais maintenu et développé au sein du <a href="https://www.eleves.ens.fr/home/klub-dev/">Klub Dev ENS</a>. Le code source est disponible <a href="https://git.eleves.ens.fr/klub-dev-ens/experiENS">ici</a>. Le site est hébergé sur le serveur des élèves.</p>
</article>
</section>

View file

@ -18,6 +18,13 @@
</div>
</div>
{% endfor %}
<input type="submit" />
<div class="field">
<label>Adresse e-mail</label>
<div class="input">
{{ request.user.email }}
<p class="help_text">Allez dans <a href="{% url "avisstage:parametres" %}">les paramètres de connexion</a> pour modifier votre adresse principale</p>
</div>
</div>
<input type="submit" value="Enregistrer" />
</form>
{% endblock %}

View file

@ -4,7 +4,7 @@
{% block title %}{% if creation %}Nouvelle expérience{% else %}Modification d'une expérience{% endif %}{% endblock %}
{% block extra_head %}
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?libraries=places&key=AIzaSyDd4innPShfHcW8KDJB833vZHZSsqt-ACw"></script>
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?libraries=places&key={{ GOOGLE_API_KEY }}"></script>
<script type="text/javascript" src="{% static "js/leaflet.js" %}"></script>
<script type="text/javascript" src="{% static "js/leaflet-gplaces-autocomplete.js" %}"></script>
<script type="text/javascript" src="{% static "js/leaflet.markercluster.js" %}"></script>
@ -16,14 +16,19 @@
<link rel="stylesheet" type="text/css" href="{% static "css/MarkerCluster.Default.css" %}" />
<script type="text/javascript" src="{% static "js/selectize.min.js" %}"></script>
<link rel="stylesheet" type="text/css" href="{% static "css/selectize.css" %}" />
<script type="text/javascript" src="{% static "js/editstage.js" %}"></script>
<link href="{% static "jquery-autosuggest/css/autoSuggest-upshot.css" %}" type="text/css" media="all" rel="stylesheet" />
<script type="text/javascript" src="{% static "js/editstage.js" %}?v2"></script>
<script type="text/javascript">
$(function(){
initEditStage(
"{{ STATIC_URL|escapejs }}",
"{% url 'avisstage:api_dispatch_list' resource_name="lieu" api_name="v1" %}");
"{% url 'avisstage:api_dispatch_list' resource_name="lieu" api_name="v1" %}",
"{{ MAPBOX_API_KEY|escapejs }}");
});
var django = {};
django.jQuery = $;
</script>
<script type="text/javascript" src="{% static "jquery-autosuggest/js/jquery.autoSuggest.minified.js" %}"> </script>
{% endblock %}
{% block content %}

View file

@ -11,8 +11,9 @@
{% if not user.is_authenticated %}
<div class="entrer">
<p><a href="{% url 'login' %}" class="btn">Connexion</a></p>
<p class="helptext">Connexion via le serveur central d'authentification ENS (identifiants clipper)</p>
<p><a href="{% url "authens:login.cas" %}" class="btn">Connexion</a></p>
<p class="helptext">Connexion via le serveur central d'authentification ENS <br />(identifiants clipper)</p>
<p class="archicubes"><a href="{% url "authens:login.pwd" %}">Accès archicubes</a> <br /><i>Pour continuer à tenir à jour ses fiches, sans voir celles des autres</i></p>
</div>
{% endif %}
@ -23,7 +24,13 @@
</div>
<div>
<p>Ne partez plus en stage en terre inconnue : nourrissez-vous des {{ num_stages }} expériences de séjours effectués par la communauté normalienne, repérez les bons plans, et ne faites pas les mêmes erreurs&nbsp;!</p>
{% if user.is_authenticated %}<p><a href="{% url 'avisstage:recherche' %}" class="btn">Rechercher des stages</a></p>{% endif %}
{% if user.is_authenticated %}
{% if user.profil.en_scolarite %}
<p><a href="{% url 'avisstage:recherche' %}" class="btn">Rechercher des stages</a></p>
{% else %}
<p><i>Accès restreint aux personnes en scolarité</i></p>
{% endif %}
{% endif %}
</div>
</div>
<div class="explications">
@ -36,8 +43,8 @@
</div>
</div>
<div class="betacadre">
Ce site est en développement actif, et fait l'objet de mises à jours régulières. N'hésitez pas à donner votre avis en utilisant le bouton "feedback".
<div class="footer">
Un projet du <a href="https://www.eleves.ens.fr/home/klub-dev/">Klub Dev ENS</a>
</div>
</article>
{% endblock %}

View file

@ -0,0 +1,8 @@
Bonjour,
Pour créer ou modifier le mot de passe associé à votre compte {{ user.get_username }}, merci de cliquer sur le lien suivant ou de le copier dans votre navigateur :
{{ protocol }}://{{ domain }}{% url 'avisstage:mdp_edit' uidb64=uid token=token %}
Cordialement,
L'équipe ExperiENS

View file

@ -0,0 +1 @@
[ExperiENS] Définition du mot de passe

View file

@ -11,10 +11,22 @@
<article>
<h2>Stages</h2>
<p>{{ num_stages }} stages créés, {{ num_stages_pub }} stages publiés</p>
<p>{% for npm in num_par_matiere %}
{{ npm.scount }} en {{ npm.matieres__nom }},
{% endfor %}
</p>
<table class="stats">
<tbody>
<tr><th>Matière</th><th>Fiches publiques</th><th>Brouillons</th></tr>
{% for npm in num_par_matiere %}
<tr><td>{{ npm.matiere }}</td><td>{{ npm.publics }}</td><td>{{ npm.drafts }}</td></tr>
{% endfor %}
</tbody>
</table>
<table class="stats">
<tbody>
<tr><th>Longueur des avis</th><th>Sur le stage</th><th>Sur les lieux</th></tr>
{% for longueur, nlstages, nllieux in num_par_longueur %}
<tr><td>{{ longueur }}</td><td>{{ nlstages }}</td><td>{{ nllieux }}</td></tr>
{% endfor %}
</tbody>
</table>
<h2>Utilisateurs</h2>
<p>{{ num_users }} utilisateurs connectés au moins une fois, {{ num_auteurs }} ont écrit une fiche</p>
<p>{% for nsta, naut in num_par_auteur %}

View file

@ -4,8 +4,61 @@
{% block title %}Espace personnel - ExperiENS{% endblock %}
{% block content %}
<h1>Bonjour {{ user.profil.nom }} !</h1>
<h1>Bonjour {{ user.profil.nom_complet }} !</h1>
<article>
<h2>Mon compte</h2>
<section class="two-cols">
<section class="profil">
{% if user.profil.en_scolarite %}
<h3 class="scolarite">Statut : En scolarité</h3>
<p>Vous pouvez accéder à l'ensemble du site, et aux fiches de stages.</p>
<p>Quand vous n'aurez plus de compte clipper (après votre scolarité), votre accès sera restreint à vos propres expériences, que vous pourrez ajouter, modifier, supprimer.</p>
<p>Pensez à renseigner une adresse e-mail non-ENS pour conserver cet accès, et permettre aux futur⋅e⋅s normalien⋅ne⋅s de toujours vous contacter !</p>
{% else %}
<h3 class="scolarite">Statut : Archicube</h3>
<p>Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.</p>
<p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "authens:login.cas" %}">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
{% endif %}
<hr />
<p><i>Le statut est mis à jour automatiquement tous les deux mois selon le mode de connexion que vous utilisez.</i></p>
</section>
<section class="profil">
<h3>Connexion</h3>
<p><b>Adresse e-mail principale :</b><br/> {{ user.email }}</p>
{% if not user.profil.has_nonENS_email %}<p align="center" class="warning">Vous n'avez pas renseigné d'adresse mail autre que celle de l'ENS. Pensez à le faire, pour que les générations futures puissent toujours vous contacter !</p>{% endif %}
<hr/>
<p><b>Mot de passe interne :</b> {% if user.password and user.has_usable_password %}Défini{% else %}Non défini{% endif %}</p>
{% if not user.password or not user.has_usable_password %}<p class="warning" align="center">Pensez à définir un mot de passe propre à ExperiENS pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
{% if user.profil.en_scolarite %}<p>En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec votre login <b>{{ user.username }}</b> et le mot de passe spécifique à ExperiENS que vous aurez défini.</p>{% endif %}
<hr/>
<p><a href="{% url "avisstage:parametres" %}">Gérer mes paramètres de connexion</a></p>
</section>
</section>
</article>
<article>
<h2>Mon profil public <a href="{% url "avisstage:profil" user.username %}" class="btn">Voir</a> <a href="{% url "avisstage:profil_edit" %}" class="edit-btn btn">Modifier mes infos</a></h2>
{% with object=user.profil %}
<section class="profil">
<div class="infos">
<p class="promo">Promotion : <b>{{ object.promotion }}</b></p>
<p class="contact">
{% if object.contactez_moi %}
Contact : {{ object.preferred_email }}
{% endif %}
</p>
</div>
{% if object.bio %}
<div class="bio">{{ object.bio|linebreaks }}</div>
{% else %}
<div class="bio"><p><i>Vous n'avez rien mis ici. <a href="{% url "avisstage:profil_edit" %}">Écrivez un peu à propos de vous !</a></i></p></div>
{% endif %}
</section>
{% endwith %}
</article>
<article>
<h2>Mes stages</h2>
<ul class="condensed-stages">
@ -23,25 +76,4 @@
</li>
</ul>
</article>
<article>
<h2><a href="{% url "avisstage:profil" user.username %}">Mon profil public</a> <a href="{% url "avisstage:profil_edit" %}" class="edit-btn btn">Modifier mes infos</a></h2>
{% with object=user.profil %}
<section class="profil">
<div class="infos">
<p class="promo">Promotion : <b>{{ object.promotion }}</b></p>
<p class="contact">
{% if object.contactez_moi %}
Contact : {{ object.mail }}
{% endif %}
</p>
</div>
{% if object.bio %}
<div class="bio">{{ object.bio|linebreaks }}</div>
{% else %}
<div class="bio"><p><i>Vous n'avez rien mis ici. <a href="{% url "avisstage:profil_edit" %}">Écrivez un peu à propos de vous !</a></i></p></div>
{% endif %}
</section>
{% endwith %}
</article>
{% endblock %}

View file

@ -3,7 +3,7 @@
<p class="generale">
<span>
{{ form.generique }}
<input type="submit" action="submit" value="Chercher un stage"/>
<input type="submit" action="submit" value="Chercher" class="submitSearch" />
</span>
<a class="toggle_avancee" href="#" onclick="$('#recherche_avancee').toggleClass('expanded'); return false;">Recherche avancée</a>
</p>
@ -20,7 +20,7 @@
{% endif %}
{% endfor %}
<li class="btnsubmit">
<input type="submit" action="submit" value="Chercher un stage"/>
<input type="submit" action="submit" value="Chercher" class="submitSearch"/>
</li>
</ul>
</div>

View file

@ -3,7 +3,7 @@
{% block title %}Chercher un stage - ExperiENS{% endblock %}
{% block extra_content_class %}recherche{% endblock %}
{% block bodyclass %}recherche{% endblock %}
{% block content %}
<h1>Chercher un stage</h1>

View file

@ -6,14 +6,17 @@
{% block extra_head %}
<script type="text/javascript" src="{% static 'js/leaflet.js' %}"></script>
<script type="text/javascript" src="{% static 'js/leaflet.markercluster.js' %}"></script>
<script type="text/javascript" src="{% static 'js/recherche.js' %}"></script>
<script type="text/javascript" src="{% static 'js/recherche.js' %}?v2"></script>
<link rel="stylesheet" type="text/css" href="{% static 'css/leaflet.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/MarkerCluster.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/MarkerCluster.Default.css' %}" />
<script src='https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.css' rel='stylesheet' />
<script src="https://cdn.maptiler.com/mapbox-gl-leaflet/latest/leaflet-mapbox-gl.js"></script>
<script type="text/javascript" src="{% static "js/render.js" %}"></script>
{% endblock %}
{% block extra_content_class %}recherche {{ vue }}{% endblock %}
{% block bodyclass %}recherche {{ vue }}{% endblock %}
{% block content %}
<section class="recherche-liste" id="recherche-liste">
@ -30,42 +33,11 @@
<li><a href="javascript:void(0);" id="voir_carte">Carte</a></li>
</ul>
</div>
<p class="numresults">{{ stages|length }} expérience{{ stages|length|pluralize }} trouvée{{ stages|length|pluralize }}</p>
<p class="numresults">{{ paginator.paginator.count }} expérience{{ paginator.paginator.count|pluralize }} trouvée{{ paginator.paginator.count|pluralize }}</p>
{% endif %}
<ul class="stage-liste" id="resultats">
{% for stage in stages %}
{% if tri == '-date_maj' %}
{% ifchanged stage.date_maj.date %}<li class="date-maj">Mis à jour le {{ stage.date_maj.date }}</li>{% endifchanged %}
{% endif %}
<li class="stage" id="resultat-stage-{{ stage.id }}">
<div class="misc-hdr">
<h3><a href="{% url "avisstage:stage" stage.id %}" class="stage-sujet">{{ stage.sujet }}</a><span class="auteur"> par <span class="stage-auteur">{{ stage.auteur.nom }}</span></span></h3>
<p class="dates" c-radius="30"><span class="detail"><span class="debut">{{ stage.date_debut|date:"d/m" }}</span><span class="fin">{{ stage.date_fin|date:"d/m" }}</span></span><span class="year">{{ stage.date_debut|date:"Y" }}</span></p>
</div>
<div>
<ul class="infos">
<li class="type">{{ stage.get_type_stage_display }}</li>
{% if stage.structure %}<li class="structure">{{ stage.structure }}</li>{% endif %}
{% for lieu in stage.lieux.all %}<li class="lieu">{{ lieu.nom }}</li>{% endfor %}
{% for matiere in stage.matieres.all %}
<li class="matiere">{{ matiere.nom }}</li>
{% endfor %}
{% for thematique in stage.thematiques.all %}
<li class="thematique">{{ thematique.name }}</li>
{% endfor %}
<li class="year">{{ stage.date_debut|date:"Y" }}</li>
<li class="avis-len avis-{{ stage.len_avis_stage|avis_len }}">Avis stage {{ stage.len_avis_stage|avis_len }}</li>
<li class="avis-len avis-{{ stage.len_avis_lieux|avis_len }}">Avis lieux {{ stage.len_avis_lieux|avis_len }}</li>
</ul>
</div>
<a href="{% url "avisstage:stage" stage.id %}" class="hoverlink">&nbsp;</a>
</li>
{% empty %}
<li class="stage">Aucun stage ne correspond à votre recherche et vos critères</li>
{% endfor %}
{% include "avisstage/recherche/stage_items.html" %}
</ul>
</article>
@ -82,12 +54,17 @@
<div id="carte"></div>
<div id="vue-options2" class="vue-options">
<ul>
<li><a href="javascript:void(0);" id="voir_hybride">Afficher la liste</a></li>
<li><a href="javascript:void(0);" id="voir_hybride">Afficher la liste et les menus</a></li>
</ul>
</div>
<script type="text/javascript">
var lieux = [{{ lieux|join:',' }}];
var interfaceRecherche = new InterfaceRecherche("{{ STATIC_URL|escapejs }}", "{% url 'avisstage:api_dispatch_list' resource_name="lieu" api_name="v1" %}", lieux);
var interfaceRecherche = new InterfaceRecherche(
"{{ STATIC_URL|escapejs }}",
"{% url 'avisstage:api_dispatch_list' resource_name="lieu" api_name="v1" %}",
"{{ MAPBOX_API_KEY }}",
"{% url 'avisstage:stage_items' %}",
lieux);
</script>
</section>
{% endif %}

View file

@ -0,0 +1,38 @@
{% load avisstage_tags %}
{% for stage in stages %}
{% if tri == '-date_maj' %}
{% ifchanged stage.date_maj.date %}<li class="date-maj">Mis à jour le {{ stage.date_maj.date }}</li>{% endifchanged %}
{% endif %}
<li class="stage" id="resultat-stage-{{ stage.id }}">
<div class="misc-hdr">
<h3><a href="{% url "avisstage:stage" stage.id %}" class="stage-sujet">{{ stage.sujet }}</a><span class="auteur"> par <span class="stage-auteur">{{ stage.auteur.nom }}</span></span></h3>
<p class="dates" c-radius="30"><span class="detail"><span class="debut">{{ stage.date_debut|date:"d/m" }}</span><span class="fin">{{ stage.date_fin|date:"d/m" }}</span></span><span class="year">{{ stage.date_debut|date:"Y" }}</span></p>
</div>
<div>
<ul class="infos">
<li class="type">{{ stage.get_type_stage_display }}</li>
{% if stage.structure %}<li class="structure">{{ stage.structure }}</li>{% endif %}
{% for lieu in stage.lieux.all %}<li class="lieu">{{ lieu.nom }}</li>{% endfor %}
{% for matiere in stage.matieres.all %}
<li class="matiere">{{ matiere.nom }}</li>
{% endfor %}
{% for thematique in stage.thematiques.all %}
<li class="thematique">{{ thematique.name }}</li>
{% endfor %}
<li class="year">{{ stage.date_debut|date:"Y" }}</li>
<li class="avis-len avis-{{ stage.len_avis_stage|avis_len }}">Avis stage {{ stage.len_avis_stage|avis_len }}</li>
<li class="avis-len avis-{{ stage.len_avis_lieux|avis_len }}">Avis lieux {{ stage.len_avis_lieux|avis_len }}</li>
</ul>
</div>
<a href="{% url "avisstage:stage" stage.id %}" class="hoverlink">&nbsp;</a>
</li>
{% empty %}
<li class="stage">Aucun stage ne correspond à votre recherche et vos critères</li>
{% endfor %}
{% if paginator %}
<li class="pagination">
{% if paginator.has_next %}
<a href="?{% url_replace request 'page' paginator.next_page_number %}" id="next-page-btn" class="btn">Plus de résultats</a>
{% endif %}
</li>
{% endif %}

View file

@ -4,14 +4,25 @@
<div class="window-bg"></div>
<div class="window-content">
<a class="window-closer" href="javascript:void(0);"></a>
<h2>Choisir un lieu</h2>
<h2 class="window-title">Choisir un lieu</h2>
<p>Restez général dans le lieu : choisissez l'université plutôt que le laboratoire, l'incubateur plutôt que la startup...</p>
<div class="message"></div>
{# UI avec carte et autocomplete #}
<div class="lieu-ui">
<div class="lieu-global">
<div class="lieu-ui">
</div>
<div class="lieu-options">
<h3>Choisir un lieu existant</h3>
<ul class="lieu-suggestions">
</ul>
<h3>Aucune suggestion correcte ?</h3>
<a href="javascript:void(0)" class="btn new-lieu-btn">
Créer un nouveau lieu ici</a>
</div>
</div>
{# En cas de modification #}
@ -26,7 +37,7 @@
{# Formulaire de création/modification #}
<div class="lieu-form">{% load staticfiles %}
<form action="{% url 'avisstage:lieu_ajout' %}" method="post" id="lieu_ajout">
<h2>Ajouter un lieu</h2>
<h2 class="form-title">Ajouter un lieu</h2>
<p class="help_text">Vous pouvez déplacer le curseur pour indiquer précisément la bonne position</p>
{% csrf_token %}
{% for field in form.hidden_fields %}

View file

@ -1,30 +1,33 @@
# coding: utf-8
import re
from django import template
from avisstage.forms import LieuForm, FeedbackForm
import re
from avisstage.forms import FeedbackForm, LieuForm
register = template.Library()
@register.inclusion_tag('avisstage/templatetags/widget_lieu.html')
@register.inclusion_tag("avisstage/templatetags/widget_lieu.html")
def lieu_widget():
form = LieuForm()
return {"form": form}
@register.inclusion_tag('avisstage/templatetags/widget_feedback.html')
@register.inclusion_tag("avisstage/templatetags/widget_feedback.html")
def feedback_widget():
form = FeedbackForm()
return {"form": form}
@register.filter
def typonazisme(value):
#print value
#return value
value = re.sub(r'(\w)\s*([?!:])', u'\\1\\2', value)
value = re.sub(r'(\w)\s*([,.])', u'\\1\\2', value)
value = re.sub(r'([?!:,.])(\w)', u'\\1 \\2', value)
value = re.sub(r"(\w)\s*([?!:])", "\\1\\2", value)
value = re.sub(r"(\w)\s*([,.])", "\\1\\2", value)
value = re.sub(r"([?!:,.])(\w)", "\\1 \\2", value)
return value
@register.filter
def avis_len(value):
if value < 5:
@ -33,3 +36,10 @@ def avis_len(value):
return "court"
else:
return "long"
@register.simple_tag
def url_replace(request, field, value):
dict_ = request.GET.copy()
dict_[field] = value
return dict_.urlencode()

View file

@ -1,3 +1,565 @@
from django.test import TestCase
from datetime import date, timedelta
from unittest import mock
# Create your tests here.
from authens.models import CASAccount, OldCASAccount
from authens.tests.cas_utils import FakeCASClient
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from .models import AvisLieu, Lieu, Stage, StageMatiere, User
class ExperiENSTestCase(TestCase):
# Dummy database
def setUp(self):
self.u_conscrit = User.objects.create_user(
"conscrit", "conscrit@ens.fr", "conscrit"
)
self.p_conscrit = self.u_conscrit.profil
self.p_conscrit.nom = "Petit conscrit"
self.p_conscrit.promotion = "Serpentard 2020"
self.p_conscrit.bio = "Je suis un petit conscrit"
self.p_conscrit.save()
self.sa_conscrit = CASAccount(
user=self.u_conscrit,
cas_login="conscrit",
entrance_year=2020,
)
self.sa_conscrit.save()
self.u_archi = User.objects.create_user(
"archicube", "archicube@ens.fr", "archicube"
)
self.p_archi = self.u_archi.profil
self.p_archi.nom = "Vieil archicube"
self.p_archi.promotion = "Gryffondor 2014"
self.p_archi.bio = "Je suis un vieil archicube"
self.lieu1 = Lieu(
nom="Beaux-Bâtons",
type_lieu="universite",
ville="Brocéliande",
pays="FR",
coord="POINT(-1.63971 48.116382)",
)
self.lieu1.save()
self.lieu2 = Lieu(
nom="Durmstrang",
type_lieu="universite",
ville="Edimbourg",
pays="GB",
coord="POINT(56.32153 -1.259715)",
)
self.lieu2.save()
self.matiere1 = StageMatiere(nom="Arithmancie", slug="arithmancie")
self.matiere1.save()
self.matiere2 = StageMatiere(nom="Sortilège", slug="sortilege")
self.matiere2.save()
self.cstage1 = Stage(
auteur=self.p_conscrit,
sujet="Wingardium Leviosa",
date_debut=date(2020, 5, 10),
date_fin=date(2020, 8, 26),
type_stage="recherche",
niveau_scol="M1",
public=True,
)
self.cstage1.save()
self.cstage1.matieres.add(self.matiere1)
alieu1 = AvisLieu(stage=self.cstage1, lieu=self.lieu1, chapo="Trop bien")
alieu1.save()
self.cstage2 = Stage(
auteur=self.p_conscrit,
sujet="Avada Kedavra",
date_debut=date(2021, 5, 10),
date_fin=date(2021, 8, 26),
type_stage="sejour_dri",
niveau_scol="M2",
public=False,
)
self.cstage2.save()
self.cstage2.matieres.add(self.matiere2)
alieu2 = AvisLieu(stage=self.cstage2, lieu=self.lieu2, chapo="Trop nul")
alieu2.save()
self.astage1 = Stage(
auteur=self.p_archi,
sujet="Alohomora",
date_debut=date(2014, 5, 10),
date_fin=date(2014, 8, 26),
type_stage="recherche",
niveau_scol="M2",
public=True,
)
self.astage1.save()
self.astage1.matieres.add(self.matiere2)
alieu3 = AvisLieu(stage=self.astage1, lieu=self.lieu1, chapo="Trop moyen")
alieu3.save()
def assertRedirectToLogin(self, testurl):
r = self.client.get(testurl)
return self.assertRedirects(r, settings.LOGIN_URL + "?next=" + testurl)
def assertPageNotFound(self, testurl):
r = self.client.get(testurl)
self.assertEqual(r.status_code, 404)
"""
ACCÈS PUBLIC
"""
class PublicViewsTest(ExperiENSTestCase):
"""
Vérifie que les fiches de stages ne sont pas visibles hors connexion
"""
def test_stage_visibility_public(self):
self.assertRedirectToLogin(
reverse("avisstage:stage", kwargs={"pk": self.cstage1.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage", kwargs={"pk": self.cstage2.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage", kwargs={"pk": self.astage1.id})
)
"""
Vérifie que les profils de normaliens ne sont pas visibles hors connexion
"""
def test_profil_visibility_public(self):
self.assertRedirectToLogin(
reverse("avisstage:profil", kwargs={"username": self.u_conscrit.username})
)
self.assertRedirectToLogin(
reverse("avisstage:profil", kwargs={"username": self.u_archi.username})
)
"""
Vérifie que la recherche n'est pas accessible hors connexion
"""
def test_pages_visibility_public(self):
self.assertRedirectToLogin(reverse("avisstage:recherche"))
self.assertRedirectToLogin(reverse("avisstage:recherche_resultats"))
self.assertRedirectToLogin(reverse("avisstage:stage_items"))
self.assertRedirectToLogin(reverse("avisstage:feedback"))
self.assertRedirectToLogin(reverse("avisstage:moderation"))
"""
Vérifie que l'API n'est pas accessible hors connexion
"""
def test_api_visibility_public(self):
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "lieu", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "stage", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "profil", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
"""
Vérifie que les pages d'édition ne sont pas accessible hors connexion
"""
def test_edit_visibility_public(self):
self.assertRedirectToLogin(
reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id})
)
self.assertRedirectToLogin(
reverse("avisstage:stage_publication", kwargs={"pk": self.astage1.id})
)
self.assertRedirectToLogin(reverse("avisstage:stage_ajout"))
self.assertRedirectToLogin(reverse("avisstage:profil_edit"))
"""
ACCÈS ARCHICUBE
"""
class ArchicubeViewsTest(ExperiENSTestCase):
def setUp(self):
super().setUp()
# Connexion with password
self.client.login(username="archicube", password="archicube")
def assert403Archicubes(self, testurl):
r = self.client.get(testurl)
return self.assertRedirects(r, reverse("avisstage:403-archicubes"))
"""
Vérifie que les seules fiches de stages visibles sont les siennes
"""
def test_stage_visibility_archi(self):
self.assertPageNotFound(
reverse("avisstage:stage", kwargs={"pk": self.cstage1.id})
)
self.assertPageNotFound(
reverse("avisstage:stage", kwargs={"pk": self.cstage2.id})
)
testurl = reverse("avisstage:stage", kwargs={"pk": self.astage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
"""
Vérifie que le seul profil visible est le sien
"""
def test_profil_visibility_archi(self):
self.assertPageNotFound(
reverse("avisstage:profil", kwargs={"username": self.u_conscrit.username})
)
testurl = reverse(
"avisstage:profil", kwargs={"username": self.u_archi.username}
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
"""
Vérifie que la recherche n'est pas accessible
"""
def test_pages_visibility_archi(self):
self.assert403Archicubes(reverse("avisstage:recherche"))
self.assert403Archicubes(reverse("avisstage:recherche_resultats"))
self.assert403Archicubes(reverse("avisstage:stage_items"))
testurl = reverse("avisstage:feedback")
r = self.client.post(
testurl, {"objet": "Contact", "message": "Ceci est un texte"}
)
self.assertRedirects(r, reverse("avisstage:index"))
testurl = reverse("avisstage:moderation")
r = self.client.get(testurl)
self.assertRedirects(r, reverse("admin:login") + "?next=" + testurl)
"""
Vérifie que la seule API accessible est celle des lieux
"""
def test_api_visibility_archi(self):
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "lieu", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "stage", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "profil", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 401)
"""
Vérifie que le seul stage modifiable est le sien
"""
def test_edit_visibility_archi(self):
testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 403)
testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id})
r = self.client.post(testurl, {"publier": True})
self.assertEqual(r.status_code, 403)
testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.astage1.id})
r = self.client.post(testurl, {"publier": True})
self.assertRedirects(
r, reverse("avisstage:stage", kwargs={"pk": self.astage1.id})
)
testurl = reverse("avisstage:stage_ajout")
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse("avisstage:profil_edit")
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
# TODO : test post()
class DeprecatedArchicubeViewsTest(ArchicubeViewsTest):
@mock.patch("authens.backends.get_cas_client")
def setUp(self, mock_cas_client):
super().setUp()
fake_cas_client = FakeCASClient(cas_login="archicube", entrance_year=2012)
mock_cas_client.return_value = fake_cas_client
self.sa_archi = OldCASAccount(
user=self.u_archi,
cas_login="archicube",
entrance_year=2012,
)
self.sa_archi.save()
# First connexion through CAS
self.client.login(ticket="dummy")
self.client.logout()
# Time flies
self.p_archi.last_cas_login = (timezone.now() - timedelta(days=365)).date()
self.p_archi.save()
# New connexion with password
self.client.login(username="archicube", password="archicube")
"""
ACCÈS EN SCOLARITE
"""
class ScolariteViewsTest(ExperiENSTestCase):
@mock.patch("authens.backends.get_cas_client")
def setUp(self, mock_cas_client):
super().setUp()
fake_cas_client = FakeCASClient(cas_login="vieuxcon", entrance_year=2017)
mock_cas_client.return_value = fake_cas_client
self.u_vieuxcon = User.objects.create_user(
"vieuxcon", "vieuxcon@ens.fr", "vieuxcon"
)
self.p_vieuxcon = self.u_vieuxcon.profil
self.p_vieuxcon.nom = "Vieux con"
self.p_vieuxcon.promotion = "Poufsouffle 2017"
self.p_vieuxcon.bio = "Je suis un vieux con encore en scolarité"
self.p_vieuxcon.save()
self.sa_vieuxcon = CASAccount(
user=self.u_vieuxcon,
cas_login="vieuxcon",
entrance_year=2017,
)
self.sa_vieuxcon.save()
self.vstage1 = Stage(
auteur=self.p_vieuxcon,
sujet="Oubliettes",
date_debut=date(2018, 5, 10),
date_fin=date(2018, 8, 26),
type_stage="recherche",
niveau_scol="M1",
public=False,
)
self.vstage1.save()
self.vstage1.matieres.add(self.matiere2)
alieu1 = AvisLieu(stage=self.vstage1, lieu=self.lieu2, chapo="Pas si mal")
alieu1.save()
# Connexion through CAS
self.client.login(ticket="dummy")
"""
Vérifie que les seules fiches de stages visibles sont les siennes ou celles
publiques
"""
def test_stage_visibility_scolarite(self):
testurl = reverse("avisstage:stage", kwargs={"pk": self.cstage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
self.assertPageNotFound(
reverse("avisstage:stage", kwargs={"pk": self.cstage2.id})
)
testurl = reverse("avisstage:stage", kwargs={"pk": self.vstage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
"""
Vérifie que tous les profils sont visibles
"""
def test_profil_visibility_scolarite(self):
testurl = reverse(
"avisstage:profil", kwargs={"username": self.u_conscrit.username}
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = reverse(
"avisstage:profil", kwargs={"username": self.u_archi.username}
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse(
"avisstage:profil", kwargs={"username": self.u_vieuxcon.username}
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
"""
Vérifie que la recherche et les autres pages sont accessibles
"""
def test_pages_visibility_scolarite(self):
testurl = reverse("avisstage:recherche")
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse("avisstage:recherche_resultats")
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = (
reverse("avisstage:stage_items")
+ "?ids="
+ ";".join(
("%d" % k.id) for k in [self.cstage1, self.cstage2, self.astage1]
)
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = reverse("avisstage:feedback")
r = self.client.post(
testurl, {"objet": "Contact", "message": "Ceci est un texte"}
)
self.assertRedirects(r, reverse("avisstage:index"))
testurl = reverse("avisstage:moderation")
r = self.client.get(testurl)
self.assertRedirects(r, reverse("admin:login") + "?next=" + testurl)
"""
Vérifie que toutes les API sont accessibles et qu'elles ne montrent que les
stages publics
"""
def test_api_visibility_scolarite(self):
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "lieu", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "stage", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "Wingardium Leviosa") # Public
self.assertNotContains(r, "Avada Kedavra") # Brouillon
testurl = reverse(
"avisstage:api_dispatch_list",
kwargs={"resource_name": "profil", "api_name": "v1"},
)
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
"""
Vérifie que le seul stage modifiable est le sien
"""
def test_edit_visibility_scolarite(self):
testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.cstage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 403)
testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.astage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 403)
testurl = reverse("avisstage:stage_edit", kwargs={"pk": self.vstage1.id})
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.cstage1.id})
r = self.client.post(testurl, {"publier": True})
self.assertEqual(r.status_code, 403)
testurl = reverse("avisstage:stage_publication", kwargs={"pk": self.vstage1.id})
r = self.client.post(testurl, {"publier": True})
self.assertRedirects(
r, reverse("avisstage:stage", kwargs={"pk": self.vstage1.id})
)
testurl = reverse("avisstage:stage_ajout")
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
testurl = reverse("avisstage:profil_edit")
r = self.client.get(testurl)
self.assertEqual(r.status_code, 200)
# TODO : test post()

View file

@ -1,29 +1,69 @@
from django.conf.urls import include, url
from . import views, api
from tastypie.api import Api
v1_api = Api(api_name='v1')
from django.urls import include, path
from . import api, views, views_search
v1_api = Api(api_name="v1")
v1_api.register(api.LieuResource())
v1_api.register(api.StageResource())
v1_api.register(api.AuteurResource())
app_name = "avisstage"
urlpatterns = [
url(r'^$', views.index, name='index'),
url(r'^perso/$', views.perso, name='perso'),
url(r'^faq/$', views.faq, name='faq'),
url(r'^stage/nouveau/$', views.manage_stage, name='stage_ajout'),
url(r'^stage/(?P<pk>\w+)/$', views.StageView.as_view(), name='stage'),
url(r'^stage/(?P<pk>\w+)/edit/$', views.manage_stage, name='stage_edit'),
url(r'^stage/(?P<pk>\w+)/publication/$', views.publier_stage, name='stage_publication'),
url(r'^stages/majs/$', views.StageListe.as_view(), name='stage_majs'),
url(r'^lieu/save/$', views.save_lieu, name='lieu_ajout'),
url(r'^profil/show/(?P<username>\w+)/$', views.ProfilView.as_view(),
name='profil'),
url(r'^profil/edit/$', views.ProfilEdit.as_view(), name='profil_edit'),
url(r'^recherche/$', views.recherche, name='recherche'),
url(r'^recherche/resultats/$', views.recherche_resultats, name='recherche_resultats'),
url(r'^feedback/$', views.feedback, name='feedback'),
url(r'^moderation/$', views.statistiques, name='moderation'),
url(r'^api/', include(v1_api.urls)),
path("", views.index, name="index"),
path("perso/", views.perso, name="perso"),
path("faq/", views.faq, name="faq"),
path("stage/nouveau/", views.manage_stage, name="stage_ajout"),
path("stage/<int:pk>/", views.StageView.as_view(), name="stage"),
path("stage/<int:pk>/edit/", views.manage_stage, name="stage_edit"),
path("stage/<int:pk>/publication/", views.publier_stage, name="stage_publication"),
path("403/archicubes/", views.archicubes_interdits, name="403-archicubes"),
path("lieu/save/", views.save_lieu, name="lieu_ajout"),
path("profil/show/<str:username>/", views.ProfilView.as_view(), name="profil"),
path("profil/edit/", views.ProfilEdit.as_view(), name="profil_edit"),
path("profil/parametres/", views.MesParametres.as_view(), name="parametres"),
path(
"profil/emails/<str:email>/aconfirmer/",
views.AdresseAConfirmer.as_view(),
name="emails_aconfirmer",
),
path(
"profil/emails/<str:email>/supprime/",
views.SupprimeAdresse.as_view(),
name="emails_supprime",
),
path(
"profil/emails/<str:email>/reconfirme/",
views.ReConfirmeAdresse.as_view(),
name="emails_reconfirme",
),
path(
"profil/emails/<str:email>/principal/",
views.RendAdressePrincipale.as_view(),
name="emails_principal",
),
path(
"profil/emails/confirme/<str:key>/",
views.ConfirmeAdresse.as_view(),
name="emails_confirme",
),
path(
"profil/mdp/demande/", views.EnvoieLienMotDePasse.as_view(), name="mdp_demande"
),
path(
"profil/mdp/<str:uidb64>/<str:token>/",
views.DefinirMotDePasse.as_view(),
name="mdp_edit",
),
path("recherche/", views_search.recherche, name="recherche"),
path(
"recherche/resultats/",
views_search.recherche_resultats,
name="recherche_resultats",
),
path("recherche/items/", views_search.stage_items, name="stage_items"),
path("feedback/", views.feedback, name="feedback"),
path("moderation/", views.statistiques, name="moderation"),
path("api/", include(v1_api.urls)),
]

View file

@ -1,4 +1,26 @@
# coding: utf-8
from functools import reduce
from math import cos, radians, sqrt
def choices_length (choices):
return reduce (lambda m, choice: max (m, len (choice[0])), choices, 0)
def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
def en_scolarite(user):
return user.profil.en_scolarite
def approximate_distance(a, b):
lat_a = radians(a.y)
lat_b = radians(b.y)
dlon = radians(b.x - a.x)
dlon = dlon * cos((lat_a + lat_b) / 2)
dlat = lat_a - lat_b
distance = 6371000 * sqrt(dlon * dlon + dlat * dlat)
return distance
def is_email_ens(mail, none=False):
if mail is None:
return none
return mail.endswith("ens.fr") or mail.endswith("ens.psl.eu")

View file

@ -1,92 +1,139 @@
# coding: utf-8
import math
import random
from collections import Counter, defaultdict
from django.shortcuts import render, redirect, get_object_or_404
from braces.views import LoginRequiredMixin
from simple_email_confirmation.models import EmailAddress
from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView, CreateView
from django import forms
from django.urls import reverse
from django.conf import settings
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
from braces.views import LoginRequiredMixin
from django.http import JsonResponse, HttpResponseForbidden
from django.contrib.auth.views import PasswordResetConfirmView
from django.core.mail import send_mail
from django.db.models import Q, Count
from collections import Counter
from django.db.models import Count, Q
from django.http import Http404, HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
FormView,
UpdateView,
View,
)
from django.views.generic.detail import SingleObjectMixin
from avisstage.models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from avisstage.forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm
from avisstage.views_search import *
import random, math
from .forms import (
AdresseEmailForm,
AvisLieuForm,
AvisStageForm,
FeedbackForm,
LieuForm,
ReinitMdpForm,
StageForm,
)
from .models import AvisLieu, AvisStage, Lieu, Normalien, Stage
from .utils import en_scolarite
#
# LECTURE
#
# Page d'accueil
def index(request):
num_stages = Stage.objects.filter(public=True).count()
return render(request, 'avisstage/index.html',
{"num_stages": num_stages})
return render(request, "avisstage/index.html", {"num_stages": num_stages})
# Espace personnel
@login_required
def perso(request):
return render(request, 'avisstage/perso.html')
# HOTFIX (TODO rendre ça plus propre)
# Vérifie que le profil existe bien
# (suite à un cas où il n'avait pas été initialisé)
if not hasattr(request.user, "profil"):
profil, created = Normalien.objects.get_or_create(user=request.user)
profil.save()
return render(request, "avisstage/perso.html")
# 403 Archicubes
@login_required
def archicubes_interdits(request):
return render(request, "avisstage/403-archicubes.html")
# Profil
#login_required
# login_required
class ProfilView(LoginRequiredMixin, DetailView):
model = Normalien
template_name = 'avisstage/detail/profil.html'
template_name = "avisstage/detail/profil.html"
# Récupération du profil
def get_object(self):
return Normalien.objects.get(user__username=self.kwargs.get('username'))
# Restriction d'accès pour les archicubes
if (
en_scolarite(self.request.user)
or self.kwargs.get("username") == self.request.user.username
):
return get_object_or_404(
Normalien, user__username=self.kwargs.get("username")
)
else:
raise Http404
# Stage
#login_required
# login_required
class StageView(LoginRequiredMixin, DetailView):
model = Stage
template_name = 'avisstage/detail/stage.html'
template_name = "avisstage/detail/stage.html"
# Restriction aux stages publics ou personnels
def get_queryset(self):
filtre = Q(auteur__user_id=self.request.user.id) | Q(public=True)
filtre = Q(auteur__user_id=self.request.user.id)
# Restriction d'accès pour les archicubes
if en_scolarite(self.request.user):
filtre |= Q(public=True)
return Stage.objects.filter(filtre)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["MAPBOX_API_KEY"] = settings.MAPBOX_API_KEY
return context
# Liste des stages par dernière modification
#login_required
class StageListe(LoginRequiredMixin, ListView):
model = Stage
template_name = 'avisstage/recherche/stage.html'
def get_queryset(self):
return Stage.objects.filter(public=True).order_by('-date_maj')
# FAQ
def faq(request):
return render(request, 'avisstage/faq.html')
return render(request, "avisstage/faq.html")
#
# EDITION
#
# Profil
#login_required
# login_required
class ProfilEdit(LoginRequiredMixin, UpdateView):
model = Normalien
fields = ['nom', 'promotion', 'mail', 'contactez_moi', 'bio']
template_name = 'avisstage/formulaires/profil.html'
fields = ["nom", "promotion", "contactez_moi", "bio"]
template_name = "avisstage/formulaires/profil.html"
# Limitation à son propre profil
def get_object(self):
return self.request.user.profil
def get_success_url(self):
return reverse('avisstage:perso')
return reverse("avisstage:perso")
# Stage
@login_required
@ -97,8 +144,9 @@ def manage_stage(request, pk=None):
stage = Stage(auteur=request.user.profil)
avis_stage = AvisStage(stage=stage)
c_del = False
last_creation = Stage.objects.filter(auteur=request.user.profil)\
.order_by("-date_creation")[:1]
last_creation = Stage.objects.filter(auteur=request.user.profil).order_by(
"-date_creation"
)[:1]
if len(last_creation) != 0:
last_maj = last_creation[0].date_creation
else:
@ -112,55 +160,71 @@ def manage_stage(request, pk=None):
# Formset pour les avis des lieux
AvisLieuFormSet = forms.inlineformset_factory(
Stage, AvisLieu, form=AvisLieuForm, can_delete=c_del, extra=0)
Stage, AvisLieu, form=AvisLieuForm, can_delete=c_del, extra=0
)
if request.method == "POST":
# Lecture des données
form = StageForm(request.POST, request=request, instance=stage, prefix="stage")
avis_stage_form = AvisStageForm(request.POST,
instance=avis_stage, prefix="avis")
avis_lieu_formset = AvisLieuFormSet(request.POST, instance=stage,
prefix="lieux")
avis_stage_form = AvisStageForm(
request.POST, instance=avis_stage, prefix="avis"
)
avis_lieu_formset = AvisLieuFormSet(
request.POST, instance=stage, prefix="lieux"
)
# Validation et enregistrement
if (form.is_valid() and
avis_stage_form.is_valid() and
avis_lieu_formset.is_valid()):
if (
form.is_valid()
and avis_stage_form.is_valid()
and avis_lieu_formset.is_valid()
):
stage = form.save()
avis_stage_form.instance.stage = stage
avis_stage_form.save()
avis_lieu_formset.save()
print request.POST
# print(request.POST)
if "continuer" in request.POST:
if pk is None:
return redirect(reverse('avisstage:stage_edit',kwargs={'pk':stage.id}))
return redirect(
reverse("avisstage:stage_edit", kwargs={"pk": stage.id})
)
else:
return redirect(reverse('avisstage:stage',
kwargs={'pk':stage.id}))
return redirect(reverse("avisstage:stage", kwargs={"pk": stage.id}))
else:
form = StageForm(instance=stage, prefix="stage")
avis_stage_form = AvisStageForm(instance=avis_stage, prefix="avis")
avis_lieu_formset = AvisLieuFormSet(instance=stage, prefix="lieux")
# Affichage du formulaire
return render(request, "avisstage/formulaires/stage.html",
{'form': form, 'avis_stage_form': avis_stage_form,
'avis_lieu_formset': avis_lieu_formset,
'creation': pk is None, "last_maj": last_maj})
return render(
request,
"avisstage/formulaires/stage.html",
{
"form": form,
"avis_stage_form": avis_stage_form,
"avis_lieu_formset": avis_lieu_formset,
"creation": pk is None,
"last_maj": last_maj,
"GOOGLE_API_KEY": settings.GOOGLE_API_KEY,
"MAPBOX_API_KEY": settings.MAPBOX_API_KEY,
},
)
# Ajout d'un lieu de stage
#login_required
# login_required
# Stage
@login_required
def save_lieu(request):
normalien = request.user.profil
if request.method == "POST":
pk = request.POST.get("id", None)
print request.POST
# print(request.POST)
jitter = False
if pk is None or pk == '':
if pk is None or pk == "":
lieu = Lieu()
else:
# Modification du lieu
@ -169,12 +233,13 @@ def save_lieu(request):
# On regarde si les stages associés à ce lieu "appartiennent" tous à l'utilisateur
not_same_user = lieu.stages.exclude(auteur=normalien).count()
# Si d'autres personnes ont un stage à cet endroit, on crée un nouveau lieu, un peu à côté
# Si d'autres personnes ont un stage à cet endroit,
# on crée un nouveau lieu, un peu à côté
if not_same_user > 0:
lieu = Lieu()
# Servira à bouger un peu le lieu
jitter = True
# Lecture des données
form = LieuForm(request.POST, instance=lieu)
@ -183,51 +248,54 @@ def save_lieu(request):
lieu = form.save(commit=False)
if jitter:
cdx, cdy = lieu.coord.get_coords()
ang = random.random() * 6.29;
ang = random.random() * 6.29
rad = (random.random() + 0.5) * 3e-4
cdx += math.cos(ang) * rad;
cdy += math.sin(ang) * rad;
cdx += math.cos(ang) * rad
cdy += math.sin(ang) * rad
lieu.coord.set_coords((cdx, cdy))
lieu.save()
# Élimination des doublons
if pk is None or pk == "":
olieux = Lieu.objects.filter(nom=lieu.nom, coord__distance_lte=(lieu.coord, 10))
olieux = Lieu.objects.filter(
nom=lieu.nom, coord__distance_lte=(lieu.coord, 10)
)
for olieu in olieux:
if olieu.type_lieu == lieu.type_lieu and \
olieu.ville == lieu.ville and \
olieu.pays == lieu.pays:
if (
olieu.type_lieu == lieu.type_lieu
and olieu.ville == lieu.ville
and olieu.pays == lieu.pays
):
return JsonResponse({"success": True, "id": olieu.id})
lieu.save()
return JsonResponse({"success": True, "id": lieu.id})
else:
return JsonResponse({"success": False,
"errors": form.errors})
return JsonResponse({"success": False, "errors": form.errors})
else:
return JsonResponse({"erreur": "Aucune donnée POST"})
class LieuAjout(LoginRequiredMixin, CreateView):
model = Lieu
form_class = LieuForm
template_name = 'avisstage/formulaires/lieu.html'
template_name = "avisstage/formulaires/lieu.html"
# Retourne d'un JSON si requête AJAX
def form_valid(self, form):
if self.request.GET.get("format", "") == "json":
self.object = form.save()
return JsonResponse({"success": True,
"id": self.object.id})
return JsonResponse({"success": True, "id": self.object.id})
else:
super(LieuAjout, self).form_valid(form)
def form_invalid(self, form):
if self.request.GET.get("format", "") == "json":
return JsonResponse({"success": False,
"errors": form.errors})
return JsonResponse({"success": False, "errors": form.errors})
else:
super(LieuAjout, self).form_valid(form)
# Passage d'un stage en mode public
@login_required
def publier_stage(request, pk):
@ -246,25 +314,28 @@ def publier_stage(request, pk):
stage.public = False
stage.save()
return redirect(reverse("avisstage:stage", kwargs={"pk": pk}))
#
# FEEDBACK
#
@login_required
def feedback(request):
if request.method == "POST":
form = FeedbackForm(request.POST)
if form.is_valid():
objet = form.cleaned_data['objet']
message = form.cleaned_data['message']
objet = form.cleaned_data["objet"]
header = "[From : %s <%s>]\n" % (request.user, request.user.email)
message = header + form.cleaned_data["message"]
send_mail(
"[experiENS] "+ objet,
"[experiENS] " + objet,
message,
request.user.username + "@clipper.ens.fr",
['champeno@clipper.ens.fr'],
request.user.email,
["robin.champenois@ens.fr"],
fail_silently=False,
)
if request.GET.get("format", None) == "json":
@ -272,32 +343,221 @@ def feedback(request):
return redirect(reverse("avisstage:index"))
else:
if request.GET.get("format", None) == "json":
return JsonResponse({"success": False,
"errors": form.errors})
return JsonResponse({"success": False, "errors": form.errors})
else:
form = FeedbackForm()
return render(request, 'avisstage/formulaire/feedback.html', {"form": form})
raise Http404()
#
# STATISTIQUES
#
@login_required
@staff_member_required
def statistiques(request):
nstages = Stage.objects.count()
npubstages = Stage.objects.filter(public=True).count()
nbymatiere = Stage.objects.values('matieres__nom').annotate(scount=Count('matieres__nom'))
nbymatiere_raw = Stage.objects.values("matieres__nom", "public").annotate(
scount=Count("matieres__nom")
)
nbymatiere = defaultdict(dict)
for npm in nbymatiere_raw:
nbymatiere[npm["matieres__nom"]][
"publics" if npm["public"] else "drafts"
] = npm["scount"]
for mat, npm in nbymatiere.items():
npm["matiere"] = mat
nbymatiere = sorted(
list(nbymatiere.values()), key=lambda npm: -npm.get("publics", 0)
)
nbylength = [
(
"Vide",
Stage.objects.filter(len_avis_stage__lt=5).count(),
Stage.objects.filter(len_avis_lieux__lt=5).count(),
),
(
"Court",
Stage.objects.filter(len_avis_stage__lt=30, len_avis_stage__gt=4).count(),
Stage.objects.filter(len_avis_lieux__lt=30, len_avis_lieux__gt=4).count(),
),
(
"Moyen",
Stage.objects.filter(len_avis_stage__lt=100, len_avis_stage__gt=29).count(),
Stage.objects.filter(len_avis_lieux__lt=100, len_avis_lieux__gt=29).count(),
),
(
"Long",
Stage.objects.filter(len_avis_stage__gt=99).count(),
Stage.objects.filter(len_avis_lieux__gt=99).count(),
),
]
nusers = Normalien.objects.count()
nauts = Normalien.objects.filter(stages__isnull=False).distinct().count()
nbyaut = Counter(Normalien.objects.filter(stages__isnull=False).annotate(scount=Count('stages')).values_list('scount', flat="True")).items()
nbyaut = Counter(
Normalien.objects.filter(stages__isnull=False)
.annotate(scount=Count("stages"))
.values_list("scount", flat="True")
).items()
nlieux = Lieu.objects.filter(stages__isnull=False).distinct().count()
return render(request, 'avisstage/moderation/statistiques.html',
{'num_stages': nstages,
'num_stages_pub': npubstages,
'num_par_matiere': nbymatiere,
'num_users': nusers,
'num_auteurs': nauts,
'num_par_auteur': nbyaut,
'num_lieux_utiles': nlieux})
return render(
request,
"avisstage/moderation/statistiques.html",
{
"num_stages": nstages,
"num_stages_pub": npubstages,
"num_par_matiere": nbymatiere,
"num_users": nusers,
"num_auteurs": nauts,
"num_par_auteur": nbyaut,
"num_lieux_utiles": nlieux,
"num_par_longueur": nbylength,
},
)
#
# Compte
#
class MesAdressesMixin(LoginRequiredMixin):
slug_url_kwarg = "email"
slug_field = "email"
confirmed_only = False
def get_queryset(self, *args, **kwargs):
qs = self.request.user.email_address_set.all()
if self.confirmed_only:
qs = qs.filter(confirmed_at__isnull=False)
return qs
def _send_confirm_mail(email, request):
confirm_url = request.build_absolute_uri(
reverse("avisstage:emails_confirme", kwargs={"key": email.key})
)
send_mail(
"[ExperiENS] Confirmez votre adresse a-mail",
"""Bonjour,
Vous venez d'ajouter cette adresse e-mail à votre compte ExperiENS.
Pour la vérifier, merci de cliquer sur le lien suivant, ou de copier l'adresse dans votre navigateur :
{confirm_url}
Cordialement,
L'équipe ExperiENS""".format(
confirm_url=confirm_url
),
"experiens-nepasrepondre@eleves.ens.fr",
[email.email],
fail_silently=False,
)
return redirect(
reverse("avisstage:emails_aconfirmer", kwargs={"email": email.email})
)
class MesParametres(LoginRequiredMixin, FormView):
model = EmailAddress
template_name = "avisstage/compte/parametres.html"
form_class = AdresseEmailForm
def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs["_user"] = self.request.user
return kwargs
def form_valid(self, form):
new = EmailAddress.objects.create_unconfirmed(
form.cleaned_data["email"], self.request.user
)
return _send_confirm_mail(new, self.request)
class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View):
model = EmailAddress
confirmed_only = True
def post(self, *args, **kwargs):
if not hasattr(self, "object"):
self.object = self.get_object()
self.request.user.email = self.object.email
self.request.user.save()
return redirect(reverse("avisstage:parametres"))
class AdresseAConfirmer(MesAdressesMixin, DetailView):
model = EmailAddress
template_name = "avisstage/compte/aconfirmer.html"
class ReConfirmeAdresse(MesAdressesMixin, DetailView):
model = EmailAddress
def post(self, *args, **kwargs):
email = self.get_object()
if email.confirmed_at is None:
return _send_confirm_mail(email, self.request)
return redirect(reverse("avisstage:parametres"))
class ConfirmeAdresse(LoginRequiredMixin, View):
def get(self, *args, **kwargs):
try:
email = EmailAddress.objects.confirm(
self.kwargs["key"], self.request.user, True
)
except Exception:
raise Http404()
messages.add_message(
self.request,
messages.SUCCESS,
"L'adresse email {email} a bien été confirmée".format(email=email.email),
)
return redirect(reverse("avisstage:parametres"))
class SupprimeAdresse(MesAdressesMixin, DeleteView):
model = EmailAddress
template_name = "avisstage/compte/email_supprime.html"
success_url = reverse_lazy("avisstage:parametres")
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
return qs.exclude(email=self.request.user.email)
class EnvoieLienMotDePasse(LoginRequiredMixin, View):
def post(self, *args, **kwargs):
form = ReinitMdpForm({"email": self.request.user.email})
form.is_valid()
form.save(
email_template_name="avisstage/mails/reinit_mdp.html",
from_email="experiens-nepasrepondre@eleves.ens.fr",
subject_template_name="avisstage/mails/reinit_mdp.txt",
)
messages.add_message(
self.request,
messages.INFO,
(
"Un mail a été envoyé à {email}. Merci de vérifier vos indésirables "
"si vous ne le recevez pas bientôt"
).format(email=self.request.user.email),
)
return redirect(reverse("avisstage:parametres"))
class DefinirMotDePasse(PasswordResetConfirmView):
template_name = "avisstage/compte/edit_mdp.html"
success_url = reverse_lazy("avisstage:perso")
def get_user(self, *args, **kwargs):
user = super().get_user(*args, **kwargs)
if self.request.user.is_authenticated and user != self.request.user:
raise Http404("Ce token n'est pas valide pour votre compte")
return user

View file

@ -1,149 +1,305 @@
# coding: utf-8
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
import json
import logging
from datetime import date
from django import forms
from django.db.models import Q
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.cache import cache
from django.core.paginator import InvalidPage, Paginator
from django.db.models import Case, Q, When
from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import render
from .documents import StageDocument
USE_ELASTICSEARCH = getattr(settings, "USE_ELASTICSEARCH", True)
if USE_ELASTICSEARCH:
from .documents import StageDocument
from .decorators import en_scolarite_required
from .models import Stage
from .statics import TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, NIVEAU_SCOL_OPTIONS
from .statics import NIVEAU_SCOL_OPTIONS, TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS
logger = logging.getLogger("recherche")
from datetime import date
# Recherche
class SearchForm(forms.Form):
generique = forms.CharField(required=False)
sujet = forms.CharField(label=u'À propos de', required=False)
contexte = forms.CharField(label=u'Contexte (lieu, encadrant⋅e⋅s, structure)',
required=False)
apres_annee = forms.IntegerField(label=u'Après cette année', required=False)
avant_annee = forms.IntegerField(label=u'Avant cette année', required=False)
sujet = forms.CharField(label="À propos de", required=False)
contexte = forms.CharField(
label="Contexte (lieu, encadrant·e·s, structure)", required=False
)
type_stage = forms.ChoiceField(label="Type de stage", choices=([('', u'')]
+ list(TYPE_STAGE_OPTIONS)),
required=False)
niveau_scol = forms.ChoiceField(label="Année d'étude", choices=([('', u'')]
+ list(NIVEAU_SCOL_OPTIONS)),
required=False)
type_lieu = forms.ChoiceField(label=u"Type de lieu d'accueil",
choices=([('', u'')]
+ list(TYPE_LIEU_OPTIONS)),
required=False)
tri = forms.ChoiceField(label=u'Tri par',
choices=[('pertinence', u'Pertinence'),
('-date_maj',u'Dernière mise à jour')],
required=False, initial='pertinence')
apres_annee = forms.IntegerField(label="Après cette année", required=False)
avant_annee = forms.IntegerField(label="Avant cette année", required=False)
type_stage = forms.ChoiceField(
label="Type de stage",
choices=([("", "")] + list(TYPE_STAGE_OPTIONS)),
required=False,
)
niveau_scol = forms.ChoiceField(
label="Année d'étude",
choices=([("", "")] + list(NIVEAU_SCOL_OPTIONS)),
required=False,
)
type_lieu = forms.ChoiceField(
label="Type de lieu d'accueil",
choices=([("", "")] + list(TYPE_LIEU_OPTIONS)),
required=False,
)
tri = forms.ChoiceField(
label="Tri par",
choices=[("pertinence", "Pertinence"), ("-date_maj", "Dernière mise à jour")],
required=False,
initial="pertinence",
)
def cherche(**kwargs):
filtres = Q(public=True)
dsl = StageDocument.search()
use_dsl = False
def field_relevant(field, test_string=True):
return field in kwargs and \
kwargs[field] is not None and \
((not test_string) or kwargs[field].strip() != '')
return (
field in kwargs
and kwargs[field] is not None
and ((not test_string) or kwargs[field].strip() != "")
)
#
# Recherche libre
#
# Champ générique : recherche dans tous les champs
if field_relevant("generique"):
#print "Filtre generique", kwargs['generique']
dsl = dsl.query(
"match",
_all={"query": kwargs["generique"],
"fuzziness": "auto"})
use_dsl = True
if USE_ELASTICSEARCH:
dsl = StageDocument.search()
# Sujet -> Recherche dan les noms de sujets et les thématiques
if field_relevant("sujet"):
dsl = dsl.query("multi_match",
query = kwargs["sujet"],
fields = ['sujet^2', 'thematiques', 'matieres'],
fuzziness = "auto")
use_dsl = True
#
# Recherche libre AVEC ELASTICSEARCH
#
# Contexte -> Encadrants, structure, lieu
if field_relevant("contexte"):
dsl = dsl.query("multi_match",
query = kwargs["contexte"],
fields = ['encadrants', 'structure^2',
'lieux.nom', 'lieux.pays', 'lieux.ville'],
fuzziness = "auto")
use_dsl = True
# Champ générique : recherche dans tous les champs
if field_relevant("generique"):
# print("Filtre generique", kwargs['generique'])
dsl = dsl.query(
"multi_match",
query=kwargs["generique"],
fuzziness="auto",
fields=[
"sujet^3",
"encadrants",
"type_stage",
"niveau_scol",
"structure",
"lieux.*^2",
"auteur.nom^2",
"thematiques^2",
"matieres",
],
)
use_dsl = True
# Sujet -> Recherche dan les noms de sujets et les thématiques
if field_relevant("sujet"):
dsl = dsl.query(
"multi_match",
query=kwargs["sujet"],
fields=["sujet^2", "thematiques", "matieres"],
fuzziness="auto",
)
use_dsl = True
# Contexte -> Encadrants, structure, lieu
if field_relevant("contexte"):
dsl = dsl.query(
"multi_match",
query=kwargs["contexte"],
fields=[
"encadrants",
"structure^2",
"lieux.nom",
"lieux.pays",
"lieux.ville",
],
fuzziness="auto",
)
use_dsl = True
else:
# Sans ElasticSearch, on active quand même une approximation de
# recherche en base de données
if field_relevant("generique"):
generique = kwargs["generique"]
filtres = (
Q(sujet__icontains=generique)
| Q(thematiques__name__icontains=generique)
| Q(matieres__nom__icontains=generique)
| Q(lieux__nom__icontains=generique)
)
# Autres champs -> non fonctionnels
if field_relevant("sujet") or field_relevant("contexte"):
raise NotImplementedError(
"ElasticSearch doit être activé pour ce type de recherche"
)
#
# Filtres directs db
#
# Dates
if field_relevant('avant_annee', False):
dte = date(kwargs['avant_annee']+1, 1, 1)
if field_relevant("avant_annee", False):
dte = date(min(2100, kwargs["avant_annee"]) + 1, 1, 1)
filtres &= Q(date_fin__lt=dte)
if field_relevant('apres_annee', False):
dte = date(kwargs['apres_annee'], 1, 1)
if field_relevant("apres_annee", False):
dte = date(max(2000, kwargs["apres_annee"]), 1, 1)
filtres &= Q(date_debut__gte=dte)
# Type de stage
if field_relevant('type_stage'):
if field_relevant("type_stage"):
filtres &= Q(type_stage=kwargs["type_stage"])
if field_relevant('niveau_scol'):
if field_relevant("niveau_scol"):
filtres &= Q(niveau_scol=kwargs["niveau_scol"])
# Type de lieu
if field_relevant('type_lieu'):
if field_relevant("type_lieu"):
filtres &= Q(lieux__type_lieu=kwargs["type_lieu"])
# Application
if use_dsl:
filtres &= Q(id__in=[s.meta.id for s in dsl.scan()])
# Tri
tri = "pertinence"
#print filtres
resultat = Stage.objects.filter(filtres)
tri = 'pertinence'
if field_relevant("tri") and kwargs["tri"] in ["-date_maj"]:
tri = kwargs["tri"]
if not use_dsl:
kwargs['tri'] = '-date_maj'
if field_relevant('tri') and kwargs['tri'] != 'pertinence':
tri = kwargs['tri']
resultat = resultat.order_by(kwargs['tri'])
tri = "-date_maj"
# Application
resultat = Stage.objects.filter(filtres).distinct()
if USE_ELASTICSEARCH and use_dsl:
dsl_res = [s.meta.id for s in dsl.scan()]
resultat = resultat.filter(id__in=dsl_res)
if tri == "pertinence":
resultat = resultat.order_by(
Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(dsl_res)])
)
else:
resultat = resultat.order_by(tri)
else:
resultat = resultat.order_by(tri)
return resultat, tri
@login_required
def recherche(request):
form = SearchForm()
return render(request, 'avisstage/recherche/recherche.html',
{"form": form})
@login_required
@en_scolarite_required
def recherche(request):
form = SearchForm()
return render(request, "avisstage/recherche/recherche.html", {"form": form})
@login_required
@en_scolarite_required
def recherche_resultats(request):
stages = []
tri = ''
vue = 'vue-liste'
tri = ""
vue = "vue-liste"
lieux = []
stageids = []
if request.method == "GET":
form = SearchForm(request.GET)
if form.is_valid():
stages, tri = cherche(**form.cleaned_data)
stages = stages.prefetch_related('lieux', 'auteur', 'matieres', 'thematiques')
lieux = [[stageid, lieuid] for (stageid, lieuid) in stages.values_list('id', 'lieux') if lieuid is not None]
page = request.GET.get("page", 1)
search_args = form.cleaned_data
# Gestion du cache
lsearch_args = {
key: val
for key, val in search_args.items()
if val != "" and val is not None
}
cache_key = json.dumps(lsearch_args, sort_keys=True)
cached = cache.get(cache_key)
if cached is None:
# Requête effective
stages, tri = cherche(**search_args)
stageids = list(stages.values_list("id", flat=True))
lieux = [
[stageid, lieuid]
for (stageid, lieuid) in stages.values_list("id", "lieux")
if lieuid is not None
]
# Sauvegarde dans le cache
to_cache = {"stages": stageids, "lieux": lieux, "tri": tri}
cache.set(cache_key, to_cache, 600)
logger.info(cache_key)
else:
# Lecture du cache
stageids = cached["stages"]
lieux = cached["lieux"]
tri = cached["tri"]
logger.info("recherche en cache")
# Pagination
paginator = Paginator(stageids, 25)
try:
stageids = paginator.page(page)
except InvalidPage:
stageids = []
if cached is None:
stages = stages[
max(0, stageids.start_index() - 1) : stageids.end_index()
]
else:
orderer = Case(
*[When(pk=pk, then=pos) for pos, pk in enumerate(stageids)]
)
stages = Stage.objects.filter(id__in=stageids).order_by(orderer)
stages = stages.prefetch_related(
"lieux", "auteur", "matieres", "thematiques"
)
else:
form = SearchForm()
if stages:
vue = 'vue-hybride'
return render(request, 'avisstage/recherche/resultats.html',
{"form": form, "stages":stages,
"tri": tri, "vue": vue, "lieux": lieux})
vue = "vue-hybride"
# Version JSON pour recherche dynamique
if request.GET.get("format") == "json":
return JsonResponse(
{"stages": stages, "page": page, "num_pages": paginator.num_pages}
)
template_name = "avisstage/recherche/resultats.html"
if request.GET.get("format") == "raw":
template_name = "avisstage/recherche/stage_items.html"
return render(
request,
template_name,
{
"form": form,
"stages": stages,
"paginator": stageids,
"tri": tri,
"vue": vue,
"lieux": lieux,
"MAPBOX_API_KEY": settings.MAPBOX_API_KEY,
},
)
@login_required
@en_scolarite_required
def stage_items(request):
try:
stageids = [int(a) for a in request.GET.get("ids", "").split(";")]
except ValueError:
return HttpResponseBadRequest("Paramètre incorrect")
stages = Stage.objects.filter(id__in=stageids).prefetch_related(
"lieux", "auteur", "matieres", "thematiques"
)
return render(request, "avisstage/recherche/stage_items.html", {"stages": stages})

View file

@ -1,14 +1,14 @@
from django import forms
from django.core import validators
class LatLonWidget(forms.MultiWidget):
"""
A Widget that splits Point input into two latitude/longitude boxes.
"""
def __init__(self, attrs=None, date_format=None, time_format=None):
widgets = (forms.HiddenInput(attrs=attrs),
forms.HiddenInput(attrs=attrs))
widgets = (forms.HiddenInput(attrs=attrs), forms.HiddenInput(attrs=attrs))
super(LatLonWidget, self).__init__(widgets, attrs)
def decompress(self, value):
@ -23,13 +23,15 @@ class LatLonField(forms.MultiValueField):
srid = 4326
default_error_messages = {
'invalid_latitude' : (u'Entrez une latitude valide.'),
'invalid_longitude' : (u'Entrez une longitude valide.'),
"invalid_latitude": ("Entrez une latitude valide."),
"invalid_longitude": ("Entrez une longitude valide."),
}
def __init__(self, *args, **kwargs):
fields = (forms.FloatField(min_value=-90, max_value=90),
forms.FloatField(min_value=-180, max_value=180))
fields = (
forms.FloatField(min_value=-90, max_value=90),
forms.FloatField(min_value=-180, max_value=180),
)
super(LatLonField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
@ -37,11 +39,11 @@ class LatLonField(forms.MultiValueField):
# Raise a validation error if latitude or longitude is empty
# (possible if LatLongField has required=False).
if data_list[0] in validators.EMPTY_VALUES:
raise forms.ValidationError(self.error_messages['invalid_latitude'])
raise forms.ValidationError(self.error_messages["invalid_latitude"])
if data_list[1] in validators.EMPTY_VALUES:
raise forms.ValidationError(self.error_messages['invalid_longitude'])
raise forms.ValidationError(self.error_messages["invalid_longitude"])
# SRID=4326;POINT(1.12345789 1.123456789)
srid_str = 'SRID=%d'%self.srid
point_str = 'POINT(%f %f)'%tuple(reversed(data_list))
return ';'.join([srid_str, point_str])
srid_str = "SRID=%d" % self.srid
point_str = "POINT(%f %f)" % tuple(reversed(data_list))
return ";".join([srid_str, point_str])
return None

94
default.nix Normal file
View file

@ -0,0 +1,94 @@
{
sources ? import ./npins,
pkgs ? import sources.nixpkgs { },
}:
let
nix-pkgs = import sources.nix-pkgs { inherit pkgs; };
check = (import sources.git-hooks).run {
src = ./.;
hooks = {
# Python hooks
black = {
enable = true;
stages = [ "pre-push" ];
};
isort = {
enable = true;
stages = [ "pre-push" ];
};
ruff = {
enable = true;
stages = [ "pre-push" ];
};
# Misc Hooks
commitizen.enable = true;
};
};
python3 = pkgs.python3.override {
packageOverrides = _: _: {
inherit (nix-pkgs)
authens
django-braces
django-elasticsearch-dsl
django-simple-email-confirmation
django-taggit-autosuggest
django-tinymce
loadcredential
spatialite
;
};
};
in
{
devShell = pkgs.mkShell {
name = "annuaire.dev";
packages = [
(python3.withPackages (ps: [
ps.authens
ps.django
ps.django-braces
ps.django-elasticsearch-dsl
ps.django-simple-email-confirmation
ps.django-taggit
ps.django-taggit-autosuggest
ps.django-tastypie
ps.django-tinymce
ps.loadcredential
# Dev packages
ps.django-debug-toolbar
ps.django-stubs
ps.spatialite
]))
];
env = {
DJANGO_SETTINGS_MODULE = "app.settings";
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
EXPERIENS_DEBUG = builtins.toJSON true;
EXPERIENS_STATIC_ROOT = builtins.toString ./.static;
EXPERIENS_GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
EXPERIENS_GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
};
shellHook = ''
${check.shellHook}
if [ ! -d .static ]; then
mkdir .static
fi
'';
};
}

View file

@ -1,5 +0,0 @@
from django_cas_ng.backends import CASBackend
class ENSCASBackend(CASBackend):
def clean_username(self, username):
return username.lower().strip()

View file

@ -1,116 +0,0 @@
"""
Django settings for experiENS project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from django.core.urlresolvers import reverse_lazy
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'django_elasticsearch_dsl',
'tastypie',
'django_cas_ng',
'braces',
'tinymce',
'taggit',
'taggit_autosuggest',
'avisstage'
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
# insert your TEMPLATE_DIRS here
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
},
},
]
ROOT_URLCONF = 'experiENS.urls'
WSGI_APPLICATION = 'experiENS.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'experiENS.auth.ENSCASBackend',
)
CAS_SERVER_URL = "https://cas.eleves.ens.fr/" #SPI CAS
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = reverse_lazy('avisstage:perso')
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
CAS_FORCE_CHANGE_USERNAME_CASE = "lower"
CAS_VERSION = 'CAS_2_SAML_1_0'
LOGIN_URL = reverse_lazy('login')
LOGOUT_URL = reverse_lazy('logout')

View file

@ -1,36 +0,0 @@
from settings_base import *
from secrets import SECRET_KEY
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.spatialite',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
INSTALLED_APPS += (
'debug_toolbar',
)
MIDDLEWARE_CLASSES = (
'debug_toolbar.middleware.DebugToolbarMiddleware',
) + MIDDLEWARE_CLASSES
INTERNAL_IPS = ['127.0.0.1']
SPATIALITE_LIBRARY_PATH = 'mod_spatialite'
STATIC_ROOT = "/home/evarin/Bureau/experiENS/static/"
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
STATIC_URL = "/experiens/static/"
ELASTICSEARCH_DSL = {
'default': {
'hosts': 'localhost:9200'
},
}

View file

@ -1,52 +0,0 @@
from settings_base import *
from secrets import SECRET_KEY
import os, sys
from django.core.urlresolvers import reverse_lazy
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(PROJECT_DIR)
DEBUG = False
ALLOWED_HOSTS = ["www.eleves.ens.fr"]
ADMINS = (
('Robin Champenois', 'champeno@clipper.ens.fr'),
)
ADMIN_LOGINS = [
"champeno",
]
SERVER_EMAIL = "experiens@www.eleves.ens.fr"
ROOT_URL = "/experiens/"
WSGI_APPLICATION = 'experiENS.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'experiens',
'USER': 'experiens',
'PASSWORD': '',
'HOST': '',
'PORT': '5432',
}
}
STATIC_URL = ROOT_URL + 'static/'
MEDIA_URL = ROOT_URL + 'media/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
EMAIL_HOST = "nef.ens.fr"
ELASTICSEARCH_DSL = {
'default': {
'hosts': '127.0.0.1:9200'
},
}

View file

@ -1,21 +0,0 @@
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from django_cas_ng import views as django_cas_views
urlpatterns = [
url(r'^', include('avisstage.urls', namespace='avisstage')),
url(r'^login/$', django_cas_views.login, name = "login"),
url(r'^logout/$', django_cas_views.logout, name = "logout"),
url(r'^tinymce/', include('tinymce.urls')),
url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')),
url(r'^admin/', include(admin.site.urls)),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

0
manage.py Normal file → Executable file
View file

80
npins/default.nix Normal file
View file

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

34
npins/sources.json Normal file
View file

@ -0,0 +1,34 @@
{
"pins": {
"git-hooks": {
"type": "Git",
"repository": {
"type": "GitHub",
"owner": "cachix",
"repo": "git-hooks.nix"
},
"branch": "master",
"revision": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
"url": "https://github.com/cachix/git-hooks.nix/archive/3c3e88f0f544d6bb54329832616af7eb971b6be6.tar.gz",
"hash": "04pwjz423iq2nkazkys905gvsm5j39722ngavrnx42b8msr5k555"
},
"nix-pkgs": {
"type": "Git",
"repository": {
"type": "Git",
"url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs"
},
"branch": "main",
"revision": "024f0d09d4ff1a62e11f5fdd74f2d00d0a77da5c",
"url": null,
"hash": "0abpyf4pclslg24wmwl3q6y8x5fmhq9winpgkpbb99yw2815j2iz"
},
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre694416.ccc0c2126893/nixexprs.tar.xz",
"hash": "0cn1z4wzps8nfqxzr6l5mbn81adcqy2cy2ic70z13fhzicmxfsbx"
}
},
"version": 3
}

View file

@ -1,2 +1,3 @@
-r requirements.txt
#spatialite-bin
django-debug-toolbar

View file

@ -1 +1,2 @@
psycopg2
-r requirements.txt
psycopg2-binary==2.7.*

View file

@ -1,11 +1,11 @@
django
django-cas-ng
django-taggit
python-ldap
django-tinymce
django-braces
django-taggit-autosuggest
pytz
django-tastypie
lxml
git+https://github.com/sabricot/django-elasticsearch-dsl
django==2.2.*
django-taggit==1.3.*
django-tinymce==3.2.*
django-braces==1.14.*
django-taggit-autosuggest==0.3.*
pytz==2020.*
django-tastypie==0.14.*
lxml==4.6.*
django-elasticsearch-dsl==7.1.*
authens
django-simple-email-confirmation==0.*

View file

@ -0,0 +1,53 @@
import sys
from collections import defaultdict
from allauth.account.models import EmailAddress
from allauth.socialaccount.models import SocialAccount
from avisstage.models import Normalien
accounts = SocialAccount.objects.all().prefetch_related("user")
profils = Normalien.objects.all()
addresses = EmailAddress.objects.all()
addr_dict = defaultdict(set)
for addr in addresses:
addr_dict[addr.user_id].add(addr.email)
profil_dict = {profil.user_id: profil for profil in profils}
addr_to_create = []
for acc in accounts:
u = acc.user
try:
profil = profil_dict[u.id]
except KeyError:
continue
to_addr = set()
if profil.mail:
to_addr.add(profil.mail)
cp_ml = "%s@clipper.ens.fr" % acc.uid
try:
cp_ml = acc.extra_data["ldap"]["email"]
except KeyError:
pass
to_addr.add(cp_ml)
addrs = addr_dict[u.id]
print(u.username, ";".join(list(to_addr)), ";".join(list(addrs)))
to_addr -= addrs
has_prim = len(addrs) > 0
for addr in to_addr:
ml = EmailAddress(email=addr, user=u, verified=True, primary=has_prim)
has_prim = False
addr_to_create.append(ml)
if "--fake" not in sys.argv:
EmailAddress.objects.bulk_create(addr_to_create)

10
setup.cfg Normal file
View file

@ -0,0 +1,10 @@
[flake8]
max-line-length = 99
exclude = .git, *.pyc, __pycache__, migrations
extend-ignore = E231, E203, E402
[isort]
profile = black
known_django = django
known_first_party = avisstage
sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER

1
shell.nix Normal file
View file

@ -0,0 +1 @@
(import ./. { }).devShell