Merge branch 'archicubes' into 'master'

Accès archicubes

See merge request klub-dev-ens/experiENS!5
This commit is contained in:
Robin Champenois 2018-12-29 23:30:51 +01:00
commit bc17ff9e7c
25 changed files with 958 additions and 232 deletions

View file

@ -0,0 +1,20 @@
from allauth.account.adapter import DefaultAccountAdapter
from allauth_ens.adapter import LongTermClipperAccountAdapter
class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return False
class SocialAccountAdapter(LongTermClipperAccountAdapter):
def is_open_for_signup(self, request, sociallogin):
# sociallogin.account is a SocialAccount instance.
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py
if sociallogin.account.provider == 'clipper':
return True
# It returns AccountAdapter.is_open_for_signup().
# See https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/adapter.py
return super().is_open_for_signup(request, sociallogin)

View file

@ -9,10 +9,16 @@ from django.urls import reverse
from .models import Lieu, Stage, Normalien, StageMatiere
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()
@ -74,7 +80,7 @@ class StageResource(ModelResource):
fields = ["sujet", "date_debut", "date_fin", "matieres", "id"]
#login_required
authentication = SessionAuthentication()
authentication = EnScolariteAuthentication()
# Filtres personnalisés
def build_filters(self, filters=None, **kwargs):
@ -114,4 +120,4 @@ class AuteurResource(ModelResource):
fields = ["id", "nom", "stages"]
#login_required
authentication = SessionAuthentication()
authentication = EnScolariteAuthentication()

12
avisstage/decorators.py Normal file
View file

@ -0,0 +1,12 @@
from functools import wraps
from django.urls import reverse
from django.shortcuts import redirect
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

@ -2,6 +2,9 @@
from __future__ import unicode_literals
from allauth.account.models import EmailAddress
from allauth.socialaccount.models import SocialAccount
from django.db import models
from django.db.models.signals import post_save
from django.contrib.auth.models import User
@ -9,7 +12,9 @@ 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.functional import cached_property
from django.utils.html import strip_tags
from taggit_autosuggest.managers import TaggableManager
@ -47,54 +52,52 @@ class Normalien(models.Model):
def stages_publics(self):
return self.stages.filter(public=True).order_by('-date_debut')
@cached_property
def en_scolarite(self):
return SocialAccount.objects.filter(user_id=self.user_id,
provider="clipper").exists()
def has_nonENS_email(self):
a = EmailAddress.objects.filter(user_id=self.user_id,
verified=True) \
.exclude(email__endswith="ens.fr")
return a.exists()
@property
def preferred_email(self):
a = EmailAddress.objects.filter(user_id=self.user_id,
verified=True) \
.exclude(email__endswith="ens.fr")\
.order_by('-primary')
if len(a) == 0:
a = EmailAddress.objects.filter(user_id=self.user_id,
verified=True) \
.order_by('-primary')
if len(a) == 0:
return ""
else:
return a[0].email
# Hook à la création d'un nouvel utilisateur : récupération de ses infos par LDAP
def create_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]
saccount = SocialAccount.objects.get(user=instance,
provider="clipper")
except SocialAccount.DoesNotExist:
return
edata = saccount.extra_data.get("ldap", {})
dep = ""
if "department_code" in edata:
dep = dict(DEPARTEMENTS_DEFAUT).get(
edata["department_code"].lower(), '')
profil.promotion = "%s %s" % (dep, edata["entrance_year"])
profil.nom = edata.get("name", "")
profil.save()
except ldap.LDAPError:
pass
#post_save.connect(create_user_profile, sender=User)
post_save.connect(create_user_profile, sender=User)
#
# Lieu de stage
@ -217,6 +220,10 @@ class Stage(models.Model):
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)

View file

@ -161,6 +161,19 @@ 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 {
@ -753,6 +766,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 {

View file

@ -229,29 +229,44 @@ header h1 {
color: #fff;
}
/* line 167, ../../sass/screen.scss */
/* line 164, ../../sass/screen.scss */
p.warning {
background-color: #c20;
font-weight: bold;
color: #fff;
padding: 3px;
}
/* line 171, ../../sass/screen.scss */
.footer {
margin-top: 15px;
font-size: 0.8em;
text-align: right;
}
/* line 180, ../../sass/screen.scss */
.condensed-stages li {
display: table;
width: 100%;
background: #fff;
margin: 12px;
}
/* line 174, ../../sass/screen.scss */
/* line 187, ../../sass/screen.scss */
.condensed-stages li > *, .condensed-stages li:before {
display: table-cell;
vertical-align: middle;
padding: 15px;
}
/* line 179, ../../sass/screen.scss */
/* line 192, ../../sass/screen.scss */
.condensed-stages li a {
width: auto;
}
/* line 181, ../../sass/screen.scss */
/* line 194, ../../sass/screen.scss */
.condensed-stages li a:hover {
background: #e08206;
color: #fff;
}
/* line 186, ../../sass/screen.scss */
/* line 199, ../../sass/screen.scss */
.condensed-stages li:before {
content: "";
text-align: right;
@ -259,77 +274,77 @@ header h1 {
opacity: 0.8;
color: #000;
}
/* line 193, ../../sass/screen.scss */
/* line 206, ../../sass/screen.scss */
.condensed-stages li.stage-brouillon:before {
content: "Brouillon";
background: #f93a93;
}
/* line 197, ../../sass/screen.scss */
/* line 210, ../../sass/screen.scss */
.condensed-stages li.stage-publie:before {
content: "Publié";
background: #419be9;
}
/* line 201, ../../sass/screen.scss */
/* line 214, ../../sass/screen.scss */
.condensed-stages li.stage-ajout:before {
content: "+";
color: #000;
}
/* line 209, ../../sass/screen.scss */
/* line 222, ../../sass/screen.scss */
.stage-liste li {
display: block;
position: relative;
}
/* line 213, ../../sass/screen.scss */
/* line 226, ../../sass/screen.scss */
.stage-liste li.date-maj {
font-weight: 300;
font-size: 0.9em;
padding: 3px 0;
font-style: italic;
}
/* line 219, ../../sass/screen.scss */
/* line 232, ../../sass/screen.scss */
.stage-liste li.stage {
padding: 10px;
background: #fff;
margin: 10px;
border-left: 5px solid #f99b20;
}
/* line 225, ../../sass/screen.scss */
/* line 238, ../../sass/screen.scss */
.stage-liste li.stage h3 {
font-size: 1.4em;
padding-left: 5px;
}
/* line 229, ../../sass/screen.scss */
/* line 242, ../../sass/screen.scss */
.stage-liste li.stage h3 > a {
color: #0f4c82;
}
/* line 233, ../../sass/screen.scss */
/* line 246, ../../sass/screen.scss */
.stage-liste li.stage h3 .auteur {
font-size: 0.8em;
}
/* line 236, ../../sass/screen.scss */
/* line 249, ../../sass/screen.scss */
.stage-liste li.stage h3 .auteur, .stage-liste li.stage h3 .auteur a {
font-family: "Dosis", sans-serif;
font-weight: normal;
}
/* line 243, ../../sass/screen.scss */
/* line 256, ../../sass/screen.scss */
.stage-liste li .misc-hdr {
margin-bottom: 10px;
}
/* line 247, ../../sass/screen.scss */
/* line 260, ../../sass/screen.scss */
.stage-liste li .misc-hdr .dates > span {
display: table-cell;
vertical-align: middle;
}
/* line 251, ../../sass/screen.scss */
/* line 264, ../../sass/screen.scss */
.stage-liste li .misc-hdr .dates .year {
padding-left: 8px;
}
/* line 254, ../../sass/screen.scss */
/* line 267, ../../sass/screen.scss */
.stage-liste li .misc-hdr .dates svg text {
font-size: 0.8;
}
/* line 260, ../../sass/screen.scss */
/* line 273, ../../sass/screen.scss */
.stage-liste li a.hoverlink {
position: absolute;
display: block;
@ -340,7 +355,7 @@ header h1 {
z-index: 2;
}
/* line 272, ../../sass/screen.scss */
/* line 285, ../../sass/screen.scss */
ul.infos {
margin: 0 -3px;
padding: 0;
@ -349,7 +364,7 @@ ul.infos {
justify-content: space-between;
width: 100;
}
/* line 280, ../../sass/screen.scss */
/* line 293, ../../sass/screen.scss */
ul.infos li {
display: inline-block;
padding: 5px;
@ -361,28 +376,28 @@ ul.infos li {
text-align: center;
background-color: #ddd;
}
/* line 291, ../../sass/screen.scss */
/* line 304, ../../sass/screen.scss */
ul.infos li.thematique {
color: #0d3f6b;
background-color: #86bff1;
}
/* line 295, ../../sass/screen.scss */
/* line 308, ../../sass/screen.scss */
ul.infos li.matiere {
color: #395214;
background-color: #c7e699;
}
/* line 299, ../../sass/screen.scss */
/* line 312, ../../sass/screen.scss */
ul.infos li.lieu {
color: #7c043c;
background-color: #fb84bc;
}
/* line 303, ../../sass/screen.scss */
/* line 316, ../../sass/screen.scss */
ul.infos li.year {
background-color: #950548;
color: #fff;
display: none;
}
/* line 308, ../../sass/screen.scss */
/* line 321, ../../sass/screen.scss */
ul.infos li.avis-len {
background-color: transparent;
border: 1px solid #eee;
@ -390,29 +405,29 @@ ul.infos li.avis-len {
padding: 4px;
padding-bottom: 2px;
}
/* line 315, ../../sass/screen.scss */
/* line 328, ../../sass/screen.scss */
ul.infos li.avis-len.avis-vide {
border-bottom-color: #ddd;
}
/* line 318, ../../sass/screen.scss */
/* line 331, ../../sass/screen.scss */
ul.infos li.avis-len.avis-court {
border-bottom-color: #ffff66;
}
/* line 321, ../../sass/screen.scss */
/* line 334, ../../sass/screen.scss */
ul.infos li.avis-len.avis-moyen {
border-bottom-color: #86bff1;
}
/* line 324, ../../sass/screen.scss */
/* line 337, ../../sass/screen.scss */
ul.infos li.avis-len.avis-long {
border-bottom-color: #a5d65c;
}
/* line 330, ../../sass/screen.scss */
/* line 343, ../../sass/screen.scss */
ul.infos:after {
content: "";
flex: 1000;
}
/* line 336, ../../sass/screen.scss */
/* line 349, ../../sass/screen.scss */
section.profil {
background: #fff;
max-width: 600px;
@ -420,7 +435,7 @@ section.profil {
margin: 5px auto;
margin-bottom: 15px;
}
/* line 343, ../../sass/screen.scss */
/* line 356, ../../sass/screen.scss */
section.profil div.infos {
border-bottom: 3px solid #1a82dd;
display: flex;
@ -712,7 +727,7 @@ article.stage .section-wrapper .toc .toc-active a {
border: 1px solid #ad0654;
}
/* line 364, ../../sass/screen.scss */
/* line 377, ../../sass/screen.scss */
input, textarea, select, div.tinymce, option, optgroup:before {
background: #fff;
font-size: 1em;
@ -722,13 +737,13 @@ input, textarea, select, div.tinymce, option, optgroup:before {
padding: 5px;
text-align: left;
}
/* line 373, ../../sass/screen.scss */
/* line 386, ../../sass/screen.scss */
input:focus, input.mce-edit-focus, textarea:focus, textarea.mce-edit-focus, select:focus, select.mce-edit-focus, div.tinymce:focus, div.tinymce.mce-edit-focus, option:focus, option.mce-edit-focus, optgroup:before:focus, optgroup:before.mce-edit-focus {
background-color: #e9f5d6;
outline: none;
}
/* line 380, ../../sass/screen.scss */
/* line 393, ../../sass/screen.scss */
input[type='text'], input[type='password'],
input[type='email'], input[type='number'], textarea, select {
border: none;
@ -738,7 +753,7 @@ input[type='email'], input[type='number'], textarea, select {
transition: border 1s ease-out, background 1s ease-out;
}
/* line 389, ../../sass/screen.scss */
/* line 402, ../../sass/screen.scss */
select {
-moz-appearance: none;
appearance: none;
@ -752,21 +767,21 @@ select {
background-color: #fff;
background-size: contain;
}
/* line 402, ../../sass/screen.scss */
/* line 415, ../../sass/screen.scss */
select option {
padding: 3px;
white-space: pre-wrap;
}
/* line 408, ../../sass/screen.scss */
/* line 421, ../../sass/screen.scss */
select optgroup option {
padding-left: 10px;
}
/* line 411, ../../sass/screen.scss */
/* line 424, ../../sass/screen.scss */
select optgroup:before {
font-weight: bold;
}
/* line 417, ../../sass/screen.scss */
/* line 430, ../../sass/screen.scss */
input[type="submit"], .btn {
font: 19px "Dosis", sans-serif;
background-color: #8fcc33;
@ -777,17 +792,17 @@ input[type="submit"], .btn {
display: inline-block;
}
/* line 427, ../../sass/screen.scss */
/* line 440, ../../sass/screen.scss */
p.submits {
text-align: right;
}
/* line 431, ../../sass/screen.scss */
/* line 444, ../../sass/screen.scss */
form .commentaire {
font-style: italic;
}
/* line 435, ../../sass/screen.scss */
/* line 448, ../../sass/screen.scss */
.edit-btn {
border-color: #706c00;
color: #000;
@ -796,14 +811,14 @@ form .commentaire {
background-origin: content-box;
background-size: contain;
}
/* line 443, ../../sass/screen.scss */
/* line 456, ../../sass/screen.scss */
.edit-btn:after {
content: "";
width: 30px;
display: inline-block;
}
/* line 450, ../../sass/screen.scss */
/* line 463, ../../sass/screen.scss */
textarea, div.tinymce {
font-family: "Lato", sans-serif;
border: none;
@ -813,20 +828,20 @@ textarea, div.tinymce {
transition: border 1s ease-out, background 1s ease-out;
}
/* line 459, ../../sass/screen.scss */
/* line 472, ../../sass/screen.scss */
textarea {
height: 200px;
resize: vertical;
}
/* line 467, ../../sass/screen.scss */
/* line 480, ../../sass/screen.scss */
form .field {
margin: 5px 0;
display: flex;
background: #fff;
padding: 10px;
}
/* line 473, ../../sass/screen.scss */
/* line 486, ../../sass/screen.scss */
form .field label, form .field .label {
display: inline-block;
width: 250px;
@ -835,39 +850,39 @@ form .field label, form .field .label {
padding-top: 5px;
flex-shrink: 0;
}
/* line 481, ../../sass/screen.scss */
/* line 494, ../../sass/screen.scss */
form .field label.required:before, form .field .label.required:before {
margin-right: 5px;
content: "*";
color: #f70978;
}
/* line 487, ../../sass/screen.scss */
/* line 500, ../../sass/screen.scss */
form .field label {
font-family: Alegreya, serif;
font-weight: bold;
}
/* line 491, ../../sass/screen.scss */
/* line 504, ../../sass/screen.scss */
form .field .help_text {
font-style: italic;
font-size: 0.9em;
}
/* line 495, ../../sass/screen.scss */
/* line 508, ../../sass/screen.scss */
form .field .input {
display: inline-block;
flex-grow: 1;
margin-right: 10px;
}
/* line 505, ../../sass/screen.scss */
/* line 518, ../../sass/screen.scss */
ul.as-selections {
display: flex;
flex-wrap: wrap;
}
/* line 509, ../../sass/screen.scss */
/* line 522, ../../sass/screen.scss */
ul.as-selections li {
display: inline-block;
}
/* line 513, ../../sass/screen.scss */
/* line 526, ../../sass/screen.scss */
ul.as-selections .as-selection-item {
padding: 0 5px;
background: #f99b20;
@ -876,53 +891,53 @@ ul.as-selections .as-selection-item {
border-radius: 2px;
font-weight: 500;
}
/* line 521, ../../sass/screen.scss */
/* line 534, ../../sass/screen.scss */
ul.as-selections .as-selection-item a.as-close {
color: #fff;
-webkit-cursor: pointer;
cursor: pointer;
margin-right: 5px;
}
/* line 528, ../../sass/screen.scss */
/* line 541, ../../sass/screen.scss */
ul.as-selections .as-selection-item.selected {
background: #8fcc33;
}
/* line 533, ../../sass/screen.scss */
/* line 546, ../../sass/screen.scss */
ul.as-selections .as-original {
flex-grow: 1;
min-width: 200px;
}
/* line 537, ../../sass/screen.scss */
/* line 550, ../../sass/screen.scss */
ul.as-selections .as-original input {
width: 100%;
}
/* line 543, ../../sass/screen.scss */
/* line 556, ../../sass/screen.scss */
div.as-results {
position: relative;
z-index: 2;
}
/* line 547, ../../sass/screen.scss */
/* line 560, ../../sass/screen.scss */
div.as-results ul {
position: absolute;
width: 100%;
background: #fff;
border: 1px solid #d2ebad;
}
/* line 554, ../../sass/screen.scss */
/* line 567, ../../sass/screen.scss */
div.as-results ul li {
padding: 3px 5px;
}
/* line 560, ../../sass/screen.scss */
/* line 573, ../../sass/screen.scss */
div.as-results ul li.as-result-item.active {
background: #fddeb5;
}
/* line 565, ../../sass/screen.scss */
/* line 578, ../../sass/screen.scss */
div.as-results ul li.as-message {
font-style: italic;
}
/* line 575, ../../sass/screen.scss */
/* line 588, ../../sass/screen.scss */
.window {
display: none;
position: fixed;
@ -933,11 +948,11 @@ div.as-results ul li.as-message {
left: 0;
z-index: 50;
}
/* line 585, ../../sass/screen.scss */
/* line 598, ../../sass/screen.scss */
.window.visible {
display: block;
}
/* line 589, ../../sass/screen.scss */
/* line 602, ../../sass/screen.scss */
.window .window-bg {
background: #000;
opacity: 0.7;
@ -948,7 +963,7 @@ div.as-results ul li.as-message {
top: 0;
z-index: -1;
}
/* line 600, ../../sass/screen.scss */
/* line 613, ../../sass/screen.scss */
.window .window-content {
position: relative;
margin: 0 auto;
@ -962,11 +977,11 @@ div.as-results ul li.as-message {
max-height: 100%;
overflow: auto;
}
/* line 614, ../../sass/screen.scss */
/* line 627, ../../sass/screen.scss */
.window .window-content form label, .window .window-content form .label {
width: 150px;
}
/* line 620, ../../sass/screen.scss */
/* line 633, ../../sass/screen.scss */
.window .window-closer {
position: absolute;
top: 0;
@ -974,65 +989,65 @@ div.as-results ul li.as-message {
padding: 12px;
z-index: 3;
}
/* line 626, ../../sass/screen.scss */
/* line 639, ../../sass/screen.scss */
.window .window-closer:after {
content: "×";
}
/* line 637, ../../sass/screen.scss */
/* line 650, ../../sass/screen.scss */
#lieu_widget .lieu-ui {
position: relative;
}
/* line 639, ../../sass/screen.scss */
/* line 652, ../../sass/screen.scss */
#lieu_widget .lieu-ui .map {
height: 400px;
width: 100%;
}
/* line 643, ../../sass/screen.scss */
/* line 656, ../../sass/screen.scss */
#lieu_widget .lieu-ui.hidden {
display: none;
}
/* line 646, ../../sass/screen.scss */
/* line 659, ../../sass/screen.scss */
#lieu_widget .lieu-ui .masked {
visibility: hidden;
}
/* line 651, ../../sass/screen.scss */
/* line 664, ../../sass/screen.scss */
#lieu_widget .lieu-choixmodif {
display: none;
}
/* line 656, ../../sass/screen.scss */
/* line 669, ../../sass/screen.scss */
#lieu_widget.modif .lieu-choixmodif {
display: unset;
}
/* line 661, ../../sass/screen.scss */
/* line 674, ../../sass/screen.scss */
#lieu_widget.modif .lieu-ui, #lieu_widget.attente .lieu-ui {
display: none;
}
/* line 668, ../../sass/screen.scss */
/* line 681, ../../sass/screen.scss */
#lieu_widget.edit .lieu-ui .lieu-acinput {
display: none;
}
/* line 671, ../../sass/screen.scss */
/* line 684, ../../sass/screen.scss */
#lieu_widget.edit .lieu-ui .map {
height: 200px;
}
/* line 677, ../../sass/screen.scss */
/* line 690, ../../sass/screen.scss */
#lieu_widget #avis_lieu_vide {
display: none;
}
/* line 681, ../../sass/screen.scss */
/* line 694, ../../sass/screen.scss */
#lieu_widget .message {
background: #fddeb5;
padding: 5px;
font-style: italic;
font-size: 0.9em;
}
/* line 687, ../../sass/screen.scss */
/* line 700, ../../sass/screen.scss */
#lieu_widget .message.hidden {
display: none;
}
/* line 693, ../../sass/screen.scss */
/* line 706, ../../sass/screen.scss */
a.lieu-change {
color: #fff;
background: #f99b20;
@ -1045,25 +1060,25 @@ a.lieu-change {
border-radius: 5px;
margin-right: 7px;
}
/* line 705, ../../sass/screen.scss */
/* line 718, ../../sass/screen.scss */
a.lieu-change.ajout:before {
content: "+";
margin-right: 5px;
}
/* line 711, ../../sass/screen.scss */
/* line 724, ../../sass/screen.scss */
#stages-map {
width: 100%;
height: 600px;
max-height: 90vh;
}
/* line 718, ../../sass/screen.scss */
/* line 731, ../../sass/screen.scss */
#id_stage-thematiques {
display: none;
}
/* line 724, ../../sass/screen.scss */
/* line 737, ../../sass/screen.scss */
.homeh1 {
display: flex;
justify-content: space-between;
@ -1073,26 +1088,26 @@ a.lieu-change.ajout:before {
border-bottom: 3px solid #000;
margin-bottom: 15px;
}
/* line 733, ../../sass/screen.scss */
/* line 746, ../../sass/screen.scss */
.homeh1 h1 {
margin-bottom: 3px;
}
/* line 737, ../../sass/screen.scss */
/* line 750, ../../sass/screen.scss */
.homeh1 > * {
display: inline-block;
}
/* line 740, ../../sass/screen.scss */
/* line 753, ../../sass/screen.scss */
.homeh1 p {
text-align: right;
}
/* line 745, ../../sass/screen.scss */
/* line 758, ../../sass/screen.scss */
.betacadre {
background: #fa6cae;
padding: 10px;
}
/* line 750, ../../sass/screen.scss */
/* line 763, ../../sass/screen.scss */
.entrer {
background: #fff;
max-width: 500px;
@ -1100,85 +1115,92 @@ a.lieu-change.ajout:before {
text-align: center;
margin: 15px auto;
}
/* line 770, ../../sass/screen.scss */
.entrer .archicubes {
border-top: 2px solid #1a82dd;
margin-top: 5px;
padding-top: 5px;
font-size: 0.9em;
}
/* line 758, ../../sass/screen.scss */
/* line 778, ../../sass/screen.scss */
article.promo {
display: block;
font-size: 1.1em;
}
/* line 762, ../../sass/screen.scss */
/* line 782, ../../sass/screen.scss */
article.promo .explications {
display: table;
}
/* line 765, ../../sass/screen.scss */
/* line 785, ../../sass/screen.scss */
article.promo .explications:first-child {
direction: rtl;
}
/* line 767, ../../sass/screen.scss */
/* line 787, ../../sass/screen.scss */
article.promo .explications:first-child > * {
direction: ltr;
}
/* line 772, ../../sass/screen.scss */
/* line 792, ../../sass/screen.scss */
article.promo .explications > div {
display: table-cell;
vertical-align: middle;
text-align: center;
}
/* line 777, ../../sass/screen.scss */
/* line 797, ../../sass/screen.scss */
article.promo .explications > div p {
margin: 15px 15px;
}
/* line 785, ../../sass/screen.scss */
/* line 805, ../../sass/screen.scss */
.faq-toc {
font-family: "Lato", sans-serif;
display: block;
max-width: 700px;
margin: 0 auto;
}
/* line 790, ../../sass/screen.scss */
/* line 810, ../../sass/screen.scss */
.faq-toc ul {
margin: 20px;
}
/* line 794, ../../sass/screen.scss */
/* line 814, ../../sass/screen.scss */
.faq-toc ul li a {
color: #000;
display: block;
padding: 5px;
}
/* line 800, ../../sass/screen.scss */
/* line 820, ../../sass/screen.scss */
.faq-toc ul li.toc-h1 {
display: none;
}
/* line 804, ../../sass/screen.scss */
/* line 824, ../../sass/screen.scss */
.faq-toc ul li.toc-h2 a {
background: #fcc883;
}
/* line 808, ../../sass/screen.scss */
/* line 828, ../../sass/screen.scss */
.faq-toc ul li.toc-h3 a {
padding-left: 10px;
background: #fff;
font-weight: normal;
}
/* line 814, ../../sass/screen.scss */
/* line 834, ../../sass/screen.scss */
.faq-toc ul li a:hover {
color: #395214;
background: #bce085 !important;
}
/* line 823, ../../sass/screen.scss */
/* line 843, ../../sass/screen.scss */
.faq article {
background: #fff;
padding: 15px;
}
/* line 826, ../../sass/screen.scss */
/* line 846, ../../sass/screen.scss */
.faq article h2 {
background-color: #fcc883;
color: #ae6505;
margin: -15px;
padding: 15px;
}
/* line 833, ../../sass/screen.scss */
/* line 853, ../../sass/screen.scss */
.faq article h3 {
color: #0f4c82;
background-color: #9dcbf3;
@ -1186,19 +1208,19 @@ article.promo .explications > div p {
margin-top: 30px;
padding: 10px 15px;
}
/* line 840, ../../sass/screen.scss */
/* line 860, ../../sass/screen.scss */
.faq article h3:nth-child(2) {
margin-top: 0;
}
/* line 845, ../../sass/screen.scss */
/* line 865, ../../sass/screen.scss */
.faq article ul {
padding-left: 20px;
}
/* line 847, ../../sass/screen.scss */
/* line 867, ../../sass/screen.scss */
.faq article ul li {
list-style: initial;
}
/* line 852, ../../sass/screen.scss */
/* line 872, ../../sass/screen.scss */
.faq article p, .faq article ul {
font-family: "Lato", sans-serif;
font-size: 18px;
@ -1207,20 +1229,20 @@ article.promo .explications > div p {
margin-right: 5%;
}
/* line 866, ../../sass/screen.scss */
/* line 886, ../../sass/screen.scss */
table.stats {
width: 100%;
background: #fff;
margin: 20px 0;
cellspacing: 1px;
}
/* line 871, ../../sass/screen.scss */
/* line 891, ../../sass/screen.scss */
table.stats th {
font-weight: bold;
border-top: 1px solid #000;
border-bottom: 1px solid #999;
}
/* line 876, ../../sass/screen.scss */
/* line 896, ../../sass/screen.scss */
table.stats td, table.stats th {
padding: 5px 3px;
text-align: center;

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

@ -27,16 +27,18 @@
<ul id="menu">
{% if user.is_authenticated %}
<li><a href="{% url 'avisstage:perso' %}">Mon expérience</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 "account_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 "account_login" %}">Connexion</a></li>
{% endif %}
</ul>
</nav>

View file

@ -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

@ -55,7 +55,7 @@
{% 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>
@ -65,8 +65,8 @@
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 +79,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

@ -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 "clipper_login" %}" 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 "account_login" %}">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">

View file

@ -6,6 +6,55 @@
{% block content %}
<h1>Bonjour {{ user.profil.nom }} !</h1>
<article>
<h2>Mon compte</h2>
<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 "clipper_login" %}?process=connect">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
{% endif %}
<p><i>Le statut est mis à jour automatiquement chaque année selon le mode de connexion que vous utilisez.</i></p>
</section>
<section class="profil">
<h3>Adresses e-mail</h3>
{% 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 %}
<p><a href="{% url "account_email" %}">Gérer les adresses e-mail liées à mon compte</a></p>
</section>
<section class="profil">
<h3>Mode de connexion</h3>
{% 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 l'identifiant {{ user.username }}</p>{% endif %}
{% if not user.password %}<p class="warning" align="center">Vous n'avez pas créé de mot de passe interne à ExperiENS. Pensez-y pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
<p><a href="{% url "account_change_password" %}">Créer / changer mon mot de passe ExperiENS</a></p>
</section>
</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.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 +72,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

@ -1,3 +1,482 @@
from django.test import TestCase
from allauth.socialaccount.models import SocialAccount
from allauth_cas.test.testcases import CASTestCase
# Create your tests here.
from datetime import date
from django.test import TestCase
from django.urls import reverse
from django.conf import settings
from .models import User, Normalien, Lieu, Stage, StageMatiere, AvisLieu
class ExperiENSTestCase(CASTestCase):
# 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 2000"
self.p_conscrit.bio="Je suis un petit conscrit"
self.p_conscrit.save()
self.sa_conscrit = SocialAccount(user=self.u_conscrit,
provider="clipper",
uid="conscrit")
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 1994"
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(2000, 5, 10),
date_fin=date(2000, 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(2001, 5, 10),
date_fin=date(2001, 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(1994, 5, 10),
date_fin=date(1994, 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 PUBLICS
"""
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()
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()
"""
ACCÈS EN SCOLARITE
"""
class ScolariteViewsTest(ExperiENSTestCase):
def setUp(self):
super().setUp()
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 1997"
self.p_vieuxcon.bio="Je suis un vieux con encore en scolarité"
self.p_vieuxcon.save()
self.sa_vieuxcon = SocialAccount(user=self.u_vieuxcon,
provider="clipper",
uid="vieuxcon")
self.sa_vieuxcon.save()
self.vstage1 = Stage(auteur=self.p_vieuxcon, sujet="Oubliettes",
date_debut=date(1998, 5, 10),
date_fin=date(1998, 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()
self.client.login(username='vieuxcon', password='vieuxcon')
"""
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)
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 accessible
"""
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

@ -14,15 +14,18 @@ urlpatterns = [
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'^stage/(?P<pk>\w+)/publication/$', views.publier_stage,
name='stage_publication'),
url(r'^403/archicubes/$', views.archicubes_interdits,
name='403-archicubes'),
url(r'^lieu/save/$', views.save_lieu, name='lieu_ajout'),
url(r'^profil/show/(?P<username>\w+)/$', views.ProfilView.as_view(),
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'^recherche/resultats/$', views.recherche_resultats,
name='recherche_resultats'),
url(r'^recherche/items/$', views.stage_items, name='stage_items'),
url(r'^feedback/$', views.feedback, name='feedback'),
url(r'^moderation/$', views.statistiques, name='moderation'),

View file

@ -1,6 +1,10 @@
# coding: utf-8
from allauth.socialaccount.models import SocialAccount
from functools import reduce
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

View file

@ -9,14 +9,16 @@ from django.urls import reverse
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.http import JsonResponse, HttpResponseForbidden, Http404
from django.core.mail import send_mail
from django.db.models import Q, Count
from collections import Counter, defaultdict
from avisstage.models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from avisstage.forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm
from avisstage.views_search import *
from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from .forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm
from .utils import en_scolarite
from .views_search import *
import random, math
@ -35,6 +37,11 @@ def index(request):
def perso(request):
return render(request, 'avisstage/perso.html')
# 403 Archicubes
@login_required
def archicubes_interdits(request):
return render(request, 'avisstage/403-archicubes.html')
# Profil
#login_required
class ProfilView(LoginRequiredMixin, DetailView):
@ -43,7 +50,14 @@ class ProfilView(LoginRequiredMixin, DetailView):
# 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 Normalien.objects.get(user__username=
self.kwargs.get('username'))
else:
raise Http404
# Stage
#login_required
@ -53,19 +67,15 @@ class StageView(LoginRequiredMixin, DetailView):
# 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)
# 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')
@ -78,7 +88,7 @@ def faq(request):
#login_required
class ProfilEdit(LoginRequiredMixin, UpdateView):
model = Normalien
fields = ['nom', 'promotion', 'mail', 'contactez_moi', 'bio']
fields = ['nom', 'promotion', 'contactez_moi', 'bio']
template_name = 'avisstage/formulaires/profil.html'
# Limitation à son propre profil
@ -276,7 +286,7 @@ def feedback(request):
"errors": form.errors})
else:
form = FeedbackForm()
return render(request, 'avisstage/formulaire/feedback.html', {"form": form})
raise Http404()
#

View file

@ -14,6 +14,7 @@ import json
import logging
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
@ -131,12 +132,14 @@ def cherche(**kwargs):
return resultat, tri
@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 = ''
@ -203,6 +206,7 @@ def recherche_resultats(request):
"tri": tri, "vue": vue, "lieux": lieux})
@login_required
@en_scolarite_required
def stage_items(request):
try:
stageids = [int(a) for a in request.GET.get("ids", "").split(';')]

View file

@ -29,11 +29,21 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.gis',
'django.contrib.sites',
'django_elasticsearch_dsl',
'widget_tweaks',
'allauth_ens',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth_cas',
'allauth_ens.providers.clipper',
'tastypie',
'django_cas_ng',
'braces',
'tinymce',
'taggit',
@ -94,6 +104,7 @@ USE_L10N = True
USE_TZ = True
SITE_ID = 1
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
@ -113,9 +124,13 @@ 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')
ACCOUNT_ADAPTER = 'avisstage.allauth_adapter.AccountAdapter'
SOCIALACCOUNT_ADAPTER = 'avisstage.allauth_adapter.SocialAccountAdapter'
LOGIN_URL = reverse_lazy('account_login')
LOGOUT_URL = reverse_lazy('account_logout')
LOGIN_REDIRECT_URL = reverse_lazy('avisstage:perso')
ACCOUNT_HOME_URL = reverse_lazy('avisstage:index')
LOGGING = {
'version': 1,

View file

@ -34,3 +34,6 @@ ELASTICSEARCH_DSL = {
'hosts': 'localhost:9200'
},
}
CLIPPER_LDAP_SERVER = 'ldaps://localhost:636'

View file

@ -50,3 +50,6 @@ ELASTICSEARCH_DSL = {
'hosts': '127.0.0.1:9200'
},
}
CLIPPER_LDAP_SERVER = 'ldaps://ldap.spi.ens.fr:636'

View file

@ -2,13 +2,11 @@ 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'^account/', include('allauth_ens.urls')),
url(r'^tinymce/', include('tinymce.urls')),
url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')),
url(r'^admin/', include(admin.site.urls)),

View file

@ -9,3 +9,4 @@ pytz==2018.*
django-tastypie==0.14.*
lxml==4.2.*
django-elasticsearch-dsl==0.4.*
django-allauth-ens==1.1.*

View file

@ -0,0 +1,54 @@
import sys
from allauth.account.models import EmailAddress
from allauth.socialaccount.models import SocialAccount
from avisstage.models import Normalien
from collections import defaultdict
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)