Merge branch 'master' into supportBDS

This commit is contained in:
Martin Pépin 2017-10-26 08:43:25 +02:00
commit 2aa2dafa13
214 changed files with 42452 additions and 3869 deletions

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from ldap3 import Connection
from django import shortcuts
@ -12,6 +10,10 @@ from django.conf import settings
class Clipper(object):
def __init__(self, clipper, fullname):
if fullname is None:
fullname = ""
assert isinstance(clipper, str)
assert isinstance(fullname, str)
self.clipper = clipper
self.fullname = fullname
@ -52,24 +54,28 @@ def autocomplete(request):
)
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
if getattr(settings, 'LDAP_SERVER_URL', None):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(bit=bit)
for bit in bits]
ldap_query = '(&{:s})'.format(''.join(
'(|(cn=*{bit:s}*)(uid=*{bit:s}*))'.format(bit=bit)
for bit in bits if bit.isalnum()
))
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
]
if ldap_query != "(&)":
# If none of the bits were legal, we do not perform the query
entries = None
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
entries = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(entry.uid.value, entry.cn.value)
for entry in entries
if entry.uid.value
and entry.uid.value not in usernames
]
# Resulting data
data.update(queries)

View file

@ -1,25 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from djconfig.forms import ConfigForm
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory
from django.db.models import Max
from django.core.validators import MinLengthValidator
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.utils.translation import ugettext_lazy as _
from bda.models import Spectacle
from gestion.models import Profile, EventCommentValue
from .models import CofProfile, CalendarSubscription
from .widgets import TriStateCheckbox
from gestion.models import Profile, EventCommentValue
from gestion.shared import lock_table, unlock_table
from bda.models import Spectacle
class SurveyForm(forms.Form):
def __init__(self, *args, **kwargs):
@ -136,8 +129,9 @@ class EventStatusFilterForm(forms.Form):
class RegistrationUserForm(forms.ModelForm):
def force_long_username(self):
self.fields['username'].validators = [MinLengthValidator(9)]
def __init__(self, *args, **kw):
super(RegistrationUserForm, self).__init__(*args, **kw)
self.fields['username'].help_text = ""
class Meta:
model = User
@ -182,21 +176,6 @@ class RegistrationCofProfileForm(forms.ModelForm):
"mailing", "mailing_bda", "mailing_bda_revente",
]
def save(self, *args, **kw):
instance = super(RegistrationCofProfileForm, self).save(*args, **kw)
if instance.is_cof and not instance.num:
# Generate new number
try:
lock_table(CofProfile)
aggregate = CofProfile.objects.aggregate(Max('num'))
instance.num = aggregate['num__max'] + 1
instance.save()
self.cleaned_data['num'] = instance.num
self.data['num'] = instance.num
finally:
unlock_table(CofProfile)
return instance
class RegistrationProfileForm(forms.ModelForm):
class Meta:
@ -318,3 +297,16 @@ class CalendarForm(forms.ModelForm):
model = CalendarSubscription
fields = ['subscribe_to_events', 'subscribe_to_my_shows',
'other_shows']
# ---
# Announcements banner
# TODO: move this to the `gestion` app once the supportBDS branch is merged
# ---
class GestioncofConfigForm(ConfigForm):
gestion_banner = forms.CharField(
label=_("Announcements banner"),
help_text=_("An empty banner disables annoucements"),
max_length=2048
)

View file

@ -1,41 +1,45 @@
# -*- coding: utf-8 -*-
import json
from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import mail
from django.db import transaction
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from django.views.generic import ListView, DetailView
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils import timezone
from .decorators import buro_required
from .models import CofProfile
from .petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject
PetitCoursAbility
)
from .decorators import buro_required
from .petits_cours_forms import DemandeForm, MatieresFormSet
from gestion.shared import lock_table, unlock_tables
class DemandeListView(ListView):
model = PetitCoursDemande
queryset = (
PetitCoursDemande.objects
.prefetch_related('matieres')
.order_by('traitee', '-id')
)
template_name = "petits_cours_demandes_list.html"
paginate_by = 20
def get_queryset(self):
return PetitCoursDemande.objects.order_by('traitee', '-id').all()
class DemandeDetailView(DetailView):
model = PetitCoursDemande
template_name = "cof/details_demande_petit_cours.html"
queryset = (
PetitCoursDemande.objects
.prefetch_related('petitcoursattribution_set',
'matieres')
)
context_object_name = "demande"
def get_context_data(self, **kwargs):
@ -103,8 +107,9 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
'style="width:99%; height: 90px;">'
'</textarea>'
})
for error in errors:
messages.error(request, error)
if errors is not None:
for error in errors:
messages.error(request, error)
return render(request, "cof/traitement_demande_petit_cours.html",
{"demande": demande,
"unsatisfied": unsatisfied,
@ -215,7 +220,7 @@ def _traitement_other(request, demande, redo):
proposals = proposals.items()
proposed_for = proposed_for.items()
return render(request,
"gestiocof/traitement_demande_petit_cours_autre_niveau.html",
"cof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
@ -246,37 +251,39 @@ def _traitement_post(request, demande):
proposals_list = proposals.items()
proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra,
})
mainmail_object, mainmail_body = render_custom_mail(
"petits-cours-mail-demandeur",
{
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra
}
)
frommail = settings.MAIL_DATA['petits_cours']['FROM']
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
mails_to_send = []
for (user, msg) in proposed_mails:
msg = mail.EmailMessage("Petits cours ENS par le COF", msg,
frommail, [user.email],
for (user, (mail_object, body)) in proposed_mails:
msg = mail.EmailMessage(mail_object, body, frommail, [user.email],
[bccaddress], headers={'Reply-To': replyto})
mails_to_send.append(msg)
mails_to_send.append(mail.EmailMessage("Cours particuliers ENS", mainmail,
mails_to_send.append(mail.EmailMessage(mainmail_object, mainmail_body,
frommail, [demande.email],
[bccaddress],
headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=True)
connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User)
for matiere in proposals:
for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(user=user,
matiere=matiere)
counter.count += 1
counter.save()
attrib = PetitCoursAttribution(user=user, matiere=matiere,
demande=demande, rank=rank + 1)
attrib.save()
unlock_tables()
with transaction.atomic():
for matiere in proposals:
for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(
user=user, matiere=matiere
)
counter.count += 1
counter.save()
attrib = PetitCoursAttribution(user=user, matiere=matiere,
demande=demande, rank=rank + 1)
attrib.save()
demande.traitee = True
demande.traitee_par = request.user
demande.processed = timezone.now()
@ -301,17 +308,15 @@ def inscription(request):
profile.petits_cours_accept = "receive_proposals" in request.POST
profile.petits_cours_remarques = request.POST["remarques"]
profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject)
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
with transaction.atomic():
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
unlock_tables()
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
success = True
formset = MatieresFormSet(instance=request.user)
else:

View file

@ -1 +0,0 @@
secret_example.py

View file

@ -40,8 +40,9 @@ a {
background: transparent;
}
div.empty-form {
padding-bottom: 2em;
}
a:hover {
color: #444;
@ -341,10 +342,12 @@ fieldset legend {
font-weight: 700;
font-size: large;
color:#DE826B;
padding-bottom: .5em;
}
#main-content h4 {
color:#DE826B;
padding-bottom: .5em;
}
#main-content h2,
@ -814,6 +817,17 @@ header .open > .dropdown-toggle.btn-default {
border-color: white;
}
/* Announcements banner ------------------ */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* FORMS --------------------------------- */
@ -836,7 +850,7 @@ input#search_autocomplete {
height: 40px;
padding: 10px 8px;
margin: 0 auto;
margin-top: 20px;
margin-top: 0px;
display: block;
color: #aaa;
}
@ -1155,3 +1169,10 @@ div.messages div.alert-success div.container {
div.messages div.alert div.container a {
color: inherit;
}
/* Help text */
p.help-block {
margin: 5px auto;
width: 90%;
}

View file

@ -8,12 +8,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# CSS #}
{# CSS #}
<link type="text/css" rel="stylesheet" href="{% static "css/bootstrap.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/cof.css" %}" />
<link href="https://fonts.googleapis.com/css?family=Dosis|Dosis:700|Raleway|Roboto:300,300i,700" rel="stylesheet">
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
{# JS #}
{# JS #}
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
{% block extra_head %}{% endblock %}

View file

@ -1,5 +1,6 @@
{% extends "cof/base_header.html" %}
{% load i18n %}
{% load wagtailcore_tags %}
{% block homelink %}
{% endblock %}
@ -41,7 +42,21 @@
{% endfor %}
</div>
{% endif %}
{% endif %}
<h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3>
<div class="hm-block">
<ul>
{# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
<li><a href="{% slugurl "k-fet" %}">Page d'accueil</a></li>
<li><a href="https://www.cof.ens.fr/k-fet/calendrier">Calendrier</a></li>
{% if perms.kfet.is_team %}
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>
{% endif %}
</ul>
</div>
{% if user.profile.is_cof %}
<h3 class="block-title">Divers<span class="pull-right glyphicon glyphicon-question-sign"></span></h3>
<div class="hm-block">
<ul>

View file

@ -9,7 +9,9 @@
{% block realcontent %}
<h2>Inscription d'un nouveau membre</h2>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" />
<p class="help-block">Les mots contenant des caractères non alphanumériques seront ignorés</p>
<input type="text" name="q" id="search_autocomplete" spellcheck="false"
placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" />
<div id="form-placeholder"></div>
<div class="yourlabs-autocomplete"></div>
<script type="text/javascript">
@ -20,7 +22,6 @@
minimumCharacters: 3,
id: 'search_autocomplete',
choiceSelector: 'li:has(a)',
placeholder: "Chercher un utilisateur par nom, prénom ou identifiant clipper",
box: $(".yourlabs-autocomplete"),
});
$('input#search_autocomplete').bind(

View file

@ -23,7 +23,7 @@ def key(d, key_name):
def highlight_text(text, q):
q2 = "|".join(q.split())
q2 = "|".join(re.escape(word) for word in q.split())
pattern = re.compile(r"(?P<filter>%s)" % q2, re.IGNORECASE)
return mark_safe(re.sub(pattern,
r"<span class='highlight'>\g<filter></span>",

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url
from .petits_cours_views import DemandeListView, DemandeDetailView
from .decorators import buro_required

View file

@ -1,38 +1,38 @@
# -*- coding: utf-8 -*-
import unicodecsv
import uuid
from custommail.shortcuts import send_custom_mail
from datetime import timedelta
from icalendar import Calendar, Event as Vevent
from custommail.shortcuts import send_custom_mail
from django.shortcuts import get_object_or_404, render
from django.http import Http404, HttpResponse
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse_lazy
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.contrib import messages
import django.utils.six as six
from django.views.generic import FormView
from bda.models import Tirage, Spectacle
from gestion.models import (
Event, EventRegistration, EventOption, EventOptionChoice,
EventCommentField, EventCommentValue
EventCommentField, EventCommentValue, Profile
)
from .models import Survey, SurveyAnswer, SurveyQuestion, \
SurveyQuestionAnswer, CalendarSubscription
from .models import CofProfile
from .decorators import buro_required, cof_required
from .forms import (
SurveyForm, SurveyStatusFilterForm,
RegistrationUserForm, RegistrationProfileForm, RegistrationCofProfileForm,
CalendarForm, EventFormset, RegistrationPassUserForm
CalendarForm, EventFormset, RegistrationPassUserForm,
GestioncofConfigForm
)
from .models import (
Survey, SurveyAnswer, SurveyQuestion, SurveyQuestionAnswer,
CalendarSubscription, CofProfile
)
from bda.models import Tirage, Spectacle
from gestion.models import Profile
@login_required
@ -52,7 +52,10 @@ def home(request):
@login_required
def survey(request, survey_id):
survey = get_object_or_404(Survey, id=survey_id)
survey = get_object_or_404(
Survey.objects.prefetch_related('questions', 'questions__answers'),
id=survey_id,
)
if not survey.survey_open or survey.old:
raise Http404
success = False
@ -224,7 +227,6 @@ def registration_form2(request, login_clipper=None, username=None,
elif not login_clipper:
# new user
user_form = RegistrationPassUserForm()
user_form.force_long_username()
profile_form = RegistrationProfileForm()
cofprofile_form = RegistrationCofProfileForm()
event_formset = EventFormset(events=events, prefix='events')
@ -273,12 +275,8 @@ def update_event_form_comments(event, form, registration):
def registration(request):
if request.POST:
request_dict = request.POST.copy()
# num ne peut pas être défini manuellement
if "num" in request_dict:
del request_dict["num"]
member = None
login_clipper = None
success = False
# -----
# Remplissage des formulaires
@ -300,12 +298,10 @@ def registration(request):
user_form = RegistrationUserForm(request_dict, instance=member)
if member.profile.login_clipper:
login_clipper = member.profile.login_clipper
else:
user_form.force_long_username()
except User.DoesNotExist:
user_form.force_long_username()
pass
else:
user_form.force_long_username()
pass
# -----
# Validation des formulaires
@ -313,10 +309,11 @@ def registration(request):
if user_form.is_valid():
member = user_form.save()
cofprofile, _ = (CofProfile.objects
.get_or_create(profile=member.profile))
cofprofile, _ = (
CofProfile.objects
.get_or_create(profile=member.profile)
)
was_cof = cofprofile.is_cof
request_dict["num"] = cofprofile.num
# Maintenant on remplit le formulaire de profil
cofprofile_form = RegistrationCofProfileForm(
request_dict,
@ -375,16 +372,17 @@ def registration(request):
# l'inscription aux événements et/ou donner la
# possibilité d'associer un mail aux événements
# send_custom_mail(...)
success = True
# Messages
if success:
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
"enregistrée avec succès"
.format(member.get_full_name(), member.email))
if member.profile.is_cof:
msg += "Il est désormais membre du COF n°{:d} !".format(
member.profile.num)
messages.success(request, msg, extra_tags='safe')
# ---
# Success
# ---
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
"enregistrée avec succès."
.format(member.get_full_name(), member.email))
if profile.is_cof:
msg += "\nIl est désormais membre du COF n°{:d} !".format(
member.profile.id)
messages.success(request, msg, extra_tags='safe')
return render(request, "cof/registration_post.html",
{"user_form": user_form,
"profile_form": profile_form,
@ -405,10 +403,10 @@ def export_members(request):
for profile in CofProfile.objects.filter(
profile__user__groups__name='cof_members').all():
user = profile.user
bits = [profile.num, user.username, user.first_name, user.last_name,
bits = [user.id, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation,
profile.departement, profile.type_cotiz]
writer.writerow([six.text_type(bit) for bit in bits])
writer.writerow([str(bit) for bit in bits])
return response
@ -424,78 +422,80 @@ def csv_export_mega(filename, qs):
comments = "---".join(
[comment.content for comment in reg.comments.all()])
bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num,
profile.phone, user.id,
profile.comments if profile.comments else "", comments]
writer.writerow([six.text_type(bit) for bit in bits])
writer.writerow([str(bit) for bit in bits])
return response
@buro_required
def export_mega_remarksonly(request):
filename = 'remarques_mega_2016.csv'
filename = 'remarques_mega_2017.csv'
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=' + filename
writer = unicodecsv.writer(response)
event = Event.objects.get(title="Mega 2016")
commentfield = event.commentfields.get(name="Commentaires")
event = Event.objects.get(title="MEGA 2017")
commentfield = event.commentfields.get(name="Commentaire")
for val in commentfield.values.all():
reg = val.registration
user = reg.user
profile = user.profile
bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.comments, val.content]
writer.writerow([six.text_type(bit) for bit in bits])
profile.phone, profile.id, profile.comments, val.content]
writer.writerow([str(bit) for bit in bits])
return response
@buro_required
def export_mega_bytype(request, type):
types = {"orga-actif": "Orga élève",
"orga-branleur": "Orga étudiant",
"conscrit-eleve": "Conscrit élève",
"conscrit-etudiant": "Conscrit étudiant"}
if type not in types:
raise Http404
event = Event.objects.get(title="Mega 2016")
type_option = event.options.get(name="Type")
participant_type = type_option.choices.get(value=types[type]).id
qs = EventRegistration.objects.filter(event=event).filter(
options__id__exact=participant_type)
return csv_export_mega(type + '_mega_2016.csv', qs)
# @buro_required
# def export_mega_bytype(request, type):
# types = {"orga-actif": "Orga élève",
# "orga-branleur": "Orga étudiant",
# "conscrit-eleve": "Conscrit élève",
# "conscrit-etudiant": "Conscrit étudiant"}
#
# if type not in types:
# raise Http404
#
# event = Event.objects.get(title="MEGA 2017")
# type_option = event.options.get(name="Type")
# participant_type = type_option.choices.get(value=types[type]).id
# qs = EventRegistration.objects.filter(event=event).filter(
# options__id__exact=participant_type)
# return csv_export_mega(type + '_mega_2017.csv', qs)
@buro_required
def export_mega_orgas(request):
event = Event.objects.get(title="Mega 2016")
type_option = event.options.get(name="Conscrit ou orga ?")
participant_type = type_option.choices.get(value="Vieux").id
qs = EventRegistration.objects.filter(event=event).exclude(
options__id=participant_type)
return csv_export_mega('orgas_mega_2016.csv', qs)
event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Orga").id
qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type
)
return csv_export_mega('orgas_mega_2017.csv', qs)
@buro_required
def export_mega_participants(request):
event = Event.objects.get(title="Mega 2016")
type_option = event.options.get(name="Conscrit ou orga ?")
event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Conscrit").id
qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type)
return csv_export_mega('participants_mega_2016.csv', qs)
options__id=participant_type
)
return csv_export_mega('participants_mega_2017.csv', qs)
@buro_required
def export_mega(request):
event = Event.objects.filter(title="Mega 2016")
event = Event.objects.filter(title="MEGA 2017")
qs = EventRegistration.objects.filter(event=event) \
.order_by("user__username")
return csv_export_mega('all_mega_2016.csv', qs)
return csv_export_mega('all_mega_2017.csv', qs)
@buro_required
@ -600,3 +600,18 @@ def calendar_ics(request, token):
response = HttpResponse(content=vcal.to_ical())
response['Content-Type'] = "text/calendar"
return response
class ConfigUpdate(FormView):
form_class = GestioncofConfigForm
template_name = "gestioncof/banner_update.html"
success_url = reverse_lazy("home")
def dispatch(self, request, *args, **kwargs):
if request.user is None or not request.user.is_superuser:
raise Http404
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.save()
return super().form_valid(form)