Merge branch 'Kerl/fix_autocomplete' into supportBDS

This commit is contained in:
Qwann 2017-02-23 18:53:55 +01:00
commit 486d3c4ced
95 changed files with 2102 additions and 3000 deletions

View file

@ -4,7 +4,7 @@ services:
variables: variables:
# GestioCOF settings # GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings_dev" DJANGO_SETTINGS_MODULE: "gestioCOF.settings_dev"
DBNAME: "cof_gestion" DBNAME: "cof_gestion"
DBUSER: "cof_gestion" DBUSER: "cof_gestion"
DBPASSWD: "cof_password" DBPASSWD: "cof_password"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 cof-geek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -171,6 +171,9 @@ Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande
python manage.py migrate python manage.py migrate
Charger les mails indispensables au bon fonctionnement de GestioCOF :
python manage.py syncmails
Une base de donnée pré-remplie est disponible en lançant les commandes : Une base de donnée pré-remplie est disponible en lançant les commandes :

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division import autocomplete_light
from __future__ import print_function from datetime import timedelta
from __future__ import unicode_literals from custommail.shortcuts import send_mass_custom_mail
from django.core.mail import send_mail
from django.contrib import admin from django.contrib import admin
from django.db.models import Sum, Count from django.db.models import Sum, Count
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
@ -13,10 +12,6 @@ from django import forms
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
from datetime import timedelta
import autocomplete_light
class ChoixSpectacleInline(admin.TabularInline): class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle model = ChoixSpectacle
@ -72,66 +67,20 @@ class ParticipantAdmin(admin.ModelAdmin):
readonly_fields = ("total",) readonly_fields = ("total",)
def send_attribs(self, request, queryset): def send_attribs(self, request, queryset):
datatuple = []
for member in queryset.all(): for member in queryset.all():
attribs = member.attributions.all() attribs = member.attributions.all()
context = {'member': member.user}
shortname = ""
if len(attribs) == 0: if len(attribs) == 0:
mail = """Cher-e %s, shortname = "bda-attributions-decus"
Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as
obtenu aucune place.
Nous proposons cependant de nombreuses offres hors-tirage tout au long de
l'année, et nous t'invitons à nous contacter si l'une d'entre elles
t'intéresse !
--
Le Bureau des Arts
"""
name = member.user.get_full_name()
mail = mail % name
else: else:
mail = """Cher-e %s, shortname = "bda-attributions"
context['places'] = attribs
Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e print(context)
pour les spectacles suivants : datatuple.append((shortname, context, "bda@ens.fr",
[member.user.email]))
%s send_mass_custom_mail(datatuple)
*Paiement*
L'intégralité de ces places de spectacles est à régler dès maintenant et AVANT
le %s, au bureau du COF pendant les heures de permanences (du lundi au vendredi
entre 12h et 14h, et entre 18h et 20h). Des facilités de paiement sont bien
évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement,
ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas
venir payer au bureau, merci de nous contacter par mail.
*Mode de retrait des places*
Au moment du paiement, certaines places vous seront remises directement,
d'autres seront à récupérer au cours de l'année, d'autres encore seront
nominatives et à retirer le soir même dans les theâtres correspondants.
Pour chaque spectacle, vous recevrez un mail quelques jours avant la
représentation vous indiquant le mode de retrait.
Nous vous rappelons que l'obtention de places du BdA vous engage à
respecter les règles de fonctionnement :
http://www.cof.ens.fr/bda/?page_id=1370
Le système de revente des places via les mails BdA-revente sera très
prochainement disponible, directement sur votre compte GestioCOF.
En vous souhaitant de très beaux spectacles tout au long de l'année,
--
Le Bureau des Arts
"""
attribs_text = ""
name = member.user.get_full_name()
for attrib in attribs:
attribs_text += "- 1 place pour %s\n" % attrib
deadline = member.tirage.fermeture + timedelta(days=7)
mail = mail % (name, attribs_text,
deadline.strftime('%d %b %Y'))
send_mail("Résultats du tirage au sort", mail,
"bda@ens.fr", [member.user.email],
fail_silently=True)
count = len(queryset.all()) count = len(queryset.all())
if count == 1: if count == 1:
message_bit = "1 membre a" message_bit = "1 membre a"

View file

@ -8,7 +8,6 @@ from datetime import timedelta
from django import forms from django import forms
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from bda.models import Attribution, Spectacle from bda.models import Attribution, Spectacle

View file

@ -6,7 +6,7 @@ import os
import random import random
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.models import User from django.contrib.auth.models import Group
from cof.management.base import MyBaseCommand from cof.management.base import MyBaseCommand
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
@ -77,7 +77,8 @@ class Command(MyBaseCommand):
self.stdout.write("Inscription des utilisateurs aux tirages") self.stdout.write("Inscription des utilisateurs aux tirages")
ChoixSpectacle.objects.all().delete() ChoixSpectacle.objects.all().delete()
choices = [] choices = []
for user in User.objects.filter(profile__cof__is_cof=True): cof_members = Group.objects.get(name="cof_members")
for user in cof_members.user_set.all():
for tirage in tirages: for tirage in tirages:
part, _ = Participant.objects.get_or_create( part, _ = Participant.objects.get_or_create(
user=user, user=user,

View file

@ -3,12 +3,11 @@
import calendar import calendar
import random import random
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.template import loader
from django.core import mail
from django.conf import settings from django.conf import settings
from django.utils import timezone, formats from django.utils import timezone, formats
@ -91,33 +90,30 @@ class Spectacle(models.Model):
if member.id in members: if member.id in members:
members[member.id][1] = 2 members[member.id][1] = 2
else: else:
members[member.id] = [member.first_name, 1, member.email] members[member.id] = [member, 1]
# Pour le BdA # FIXME : faire quelque chose de ça, un utilisateur bda_generic ?
members[0] = ['BdA', 1, 'bda@ens.fr'] # # Pour le BdA
members[-1] = ['BdA', 2, 'bda@ens.fr'] # members[0] = ['BdA', 1, 'bda@ens.fr']
# members[-1] = ['BdA', 2, 'bda@ens.fr']
# On écrit un mail personnalisé à chaque participant # On écrit un mail personnalisé à chaque participant
mails_to_send = [] datatuple = [(
mail_object = str(self) 'bda-rappel',
for member in members.values(): {'member': member[0], 'nb_attr': member[1], 'show': self},
mail_body = loader.render_to_string('bda/mails/rappel.txt', { settings.MAIL_DATA['rappels']['FROM'],
'name': member[0], [member[0].email])
'nb_attr': member[1], for member in members.values()
'show': self}) ]
mail_tot = mail.EmailMessage( send_mass_custom_mail(datatuple)
mail_object, mail_body,
settings.MAIL_DATA['rappels']['FROM'], [member[2]],
[], headers={
'Reply-To': settings.MAIL_DATA['rappels']['REPLYTO']})
mails_to_send.append(mail_tot)
# On envoie les mails
connection = mail.get_connection()
connection.send_messages(mails_to_send)
# On enregistre le fait que l'envoi a bien eu lieu # On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now() self.rappel_sent = timezone.now()
self.save() self.save()
# On renvoie la liste des destinataires # On renvoie la liste des destinataires
return members.values() return members.values()
@property
def is_past(self):
return self.date < timezone.now()
class Quote(models.Model): class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle) spectacle = models.ForeignKey(Spectacle)
@ -238,26 +234,24 @@ class SpectacleRevente(models.Model):
verbose_name = "Revente" verbose_name = "Revente"
def send_notif(self): def send_notif(self):
"""
Envoie une notification pour indiquer la mise en vente d'une place sur
BdA-Revente à tous les intéressés.
"""
inscrits = self.attribution.spectacle.subscribed.select_related('user') inscrits = self.attribution.spectacle.subscribed.select_related('user')
datatuple = [(
mails_to_send = [] 'bda-revente',
mail_object = "%s" % (self.attribution.spectacle) {
for participant in inscrits: 'member': participant.user,
mail_body = loader.render_to_string('bda/mails/revente.txt', { 'show': self.attribution.spectacle,
'user': participant.user,
'spectacle': self.attribution.spectacle,
'revente': self, 'revente': self,
'domain': Site.objects.get_current().domain}) 'site': Site.objects.get_current()
mail_tot = mail.EmailMessage( },
mail_object, mail_body,
settings.MAIL_DATA['revente']['FROM'], settings.MAIL_DATA['revente']['FROM'],
[participant.user.email], [participant.user.email])
[], headers={ for participant in inscrits
'Reply-To': settings.MAIL_DATA['revente']['REPLYTO']}) ]
mails_to_send.append(mail_tot) send_mass_custom_mail(datatuple)
connection = mail.get_connection()
connection.send_messages(mails_to_send)
self.notif_sent = True self.notif_sent = True
self.save() self.save()
@ -267,25 +261,18 @@ class SpectacleRevente(models.Model):
leur indiquer qu'il est désormais disponible au shotgun. leur indiquer qu'il est désormais disponible au shotgun.
""" """
inscrits = self.attribution.spectacle.subscribed.select_related('user') inscrits = self.attribution.spectacle.subscribed.select_related('user')
datatuple = [(
mails_to_send = [] 'bda-shotgun',
mail_object = "%s" % (self.attribution.spectacle) {
for participant in inscrits: 'member': participant.user,
mail_body = loader.render_to_string('bda/mails/shotgun.txt', { 'show': self.attribution.spectacle,
'user': participant.user, 'site': Site.objects.get_current(),
'spectacle': self.attribution.spectacle, },
'domain': Site.objects.get_current(),
'mail': self.attribution.participant.user.email})
mail_tot = mail.EmailMessage(
mail_object, mail_body,
settings.MAIL_DATA['revente']['FROM'], settings.MAIL_DATA['revente']['FROM'],
[participant.user.email], [participant.user.email])
[], headers={ for participant in inscrits
'Reply-To': settings.MAIL_DATA['revente']['REPLYTO']}) ]
mails_to_send.append(mail_tot) send_mass_custom_mail(datatuple)
connection = mail.get_connection()
connection.send_messages(mails_to_send)
self.notif_sent = True self.notif_sent = True
# Flag inutile, sauf si l'horloge interne merde # Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True self.tirage_done = True
@ -303,56 +290,41 @@ class SpectacleRevente(models.Model):
seller = self.seller seller = self.seller
if inscrits: if inscrits:
mails = []
mail_subject = "BdA-Revente : {:s}".format(spectacle.title)
# Envoie un mail au gagnant et au vendeur # Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits) winner = random.choice(inscrits)
self.soldTo = winner self.soldTo = winner
datatuple = []
context = { context = {
'acheteur': winner.user, 'acheteur': winner.user,
'vendeur': seller.user, 'vendeur': seller.user,
'spectacle': spectacle, 'show': spectacle,
} }
mails.append(mail.EmailMessage( datatuple.append((
mail_subject, 'bda-revente-winner',
loader.render_to_string('bda/mails/revente-winner.txt', context,
context), settings.MAIL_DATA['revente']['FROM'],
from_email=settings.MAIL_DATA['revente']['FROM'], [winner.user.email],
to=[winner.user.email],
reply_to=[seller.user.email],
)) ))
mails.append(mail.EmailMessage( datatuple.append((
mail_subject, 'bda-revente-seller',
loader.render_to_string('bda/mails/revente-seller.txt', context,
context), settings.MAIL_DATA['revente']['FROM'],
from_email=settings.MAIL_DATA['revente']['FROM'], [seller.user.email]
to=[seller.user.email],
reply_to=[winner.user.email],
)) ))
# Envoie un mail aux perdants # Envoie un mail aux perdants
for inscrit in inscrits: for inscrit in inscrits:
if inscrit == winner: if inscrit != winner:
continue context['acheteur'] = inscrit.user
datatuple.append((
mail_body = loader.render_to_string( 'bda-revente-loser',
'bda/mails/revente-loser.txt', context,
{'acheteur': inscrit.user, settings.MAIL_DATA['revente']['FROM'],
'vendeur': seller.user, [inscrit.user.email]
'spectacle': spectacle}
)
mails.append(mail.EmailMessage(
mail_subject, mail_body,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[inscrit.user.email],
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']],
)) ))
mail.get_connection().send_messages(mails) send_mass_custom_mail(datatuple)
# Si personne ne veut de la place, elle part au shotgun # Si personne ne veut de la place, elle part au shotgun
else: else:
self.shotgun = True self.shotgun = True
self.tirage_done = True self.tirage_done = True
self.save() self.save()

File diff suppressed because one or more lines are too long

View file

@ -1,347 +0,0 @@
/*!
* jQuery UI 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI
*/
(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.15",
keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({propAttr:c.fn.prop||c.fn.attr,_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=
this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,
"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart":
"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,
outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a,
"tabindex"),d=isNaN(b);return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&
a.element[0].parentNode)for(var e=0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element,d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&&
c.ui.isOverAxis(b,e,i)}})}})(jQuery);
;/*!
* jQuery UI Widget 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Widget
*/
(function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)b(d).triggerHandler("remove");k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){b(this).triggerHandler("remove")});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]=function(h){return!!b.data(h,
a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)):d;if(e&&d.charAt(0)==="_")return h;
e?this.each(function(){var g=b.data(this,a),i=g&&b.isFunction(g[d])?g[d].apply(g,f):g;if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options=b.extend(true,{},this.options,
this._getCreateOptions(),a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){return b.metadata&&b.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},
widget:function(){return this.element},option:function(a,c){var d=a;if(arguments.length===0)return b.extend({},this.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}this._setOptions(d);return this},_setOptions:function(a){var c=this;b.each(a,function(d,e){c._setOption(d,e)});return this},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",c);return this},
enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery);
;/*!
* jQuery UI Mouse 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Mouse
*
* Depends:
* jquery.ui.widget.js
*/
(function(b){b.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var a=this;this.element.bind("mousedown."+this.widgetName,function(c){return a._mouseDown(c)}).bind("click."+this.widgetName,function(c){if(true===b.data(c.target,a.widgetName+".preventClickEvent")){b.removeData(c.target,a.widgetName+".preventClickEvent");c.stopImmediatePropagation();return false}});this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName)},_mouseDown:function(a){a.originalEvent=
a.originalEvent||{};if(!a.originalEvent.mouseHandled){this._mouseStarted&&this._mouseUp(a);this._mouseDownEvent=a;var c=this,e=a.which==1,f=typeof this.options.cancel=="string"?b(a.target).closest(this.options.cancel).length:false;if(!e||f||!this._mouseCapture(a))return true;this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet)this._mouseDelayTimer=setTimeout(function(){c.mouseDelayMet=true},this.options.delay);if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a)){this._mouseStarted=this._mouseStart(a)!==
false;if(!this._mouseStarted){a.preventDefault();return true}}true===b.data(a.target,this.widgetName+".preventClickEvent")&&b.removeData(a.target,this.widgetName+".preventClickEvent");this._mouseMoveDelegate=function(d){return c._mouseMove(d)};this._mouseUpDelegate=function(d){return c._mouseUp(d)};b(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);a.preventDefault();return a.originalEvent.mouseHandled=true}},_mouseMove:function(a){if(b.browser.msie&&
!(document.documentMode>=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=
false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery);
;/*
* jQuery UI Position 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Position
*/
(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.setTimeout){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j={top:b.of.pageY,
left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/2;if(b.at[1]==="bottom")j.top+=
k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+(parseInt(c.curCSS(this,"marginRight",true))||0),w=m+q+(parseInt(c.curCSS(this,"marginBottom",true))||0),i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]==="center")i.top-=
m/2;i.left=Math.round(i.left);i.top=Math.round(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();b.left=
d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];b.left+=
a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=c(b),
g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery);
;/*
* jQuery UI Sortable 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Sortables
*
* Depends:
* jquery.ui.core.js
* jquery.ui.mouse.js
* jquery.ui.widget.js
*/
(function(d){d.widget("ui.sortable",d.ui.mouse,{widgetEventPrefix:"sort",options:{appendTo:"parent",axis:false,connectWith:false,containment:false,cursor:"auto",cursorAt:false,dropOnEmpty:true,forcePlaceholderSize:false,forceHelperSize:false,grid:false,handle:false,helper:"original",items:"> *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){var a=this.options;this.containerCache={};this.element.addClass("ui-sortable");
this.refresh();this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a===
"disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&&
!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem=c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,
left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};
this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment();if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!=
document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start",a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a);
return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY<b.scrollSensitivity)this.scrollParent[0].scrollTop=c=this.scrollParent[0].scrollTop+b.scrollSpeed;else if(a.pageY-this.overflowOffset.top<
b.scrollSensitivity)this.scrollParent[0].scrollTop=c=this.scrollParent[0].scrollTop-b.scrollSpeed;if(this.overflowOffset.left+this.scrollParent[0].offsetWidth-a.pageX<b.scrollSensitivity)this.scrollParent[0].scrollLeft=c=this.scrollParent[0].scrollLeft+b.scrollSpeed;else if(a.pageX-this.overflowOffset.left<b.scrollSensitivity)this.scrollParent[0].scrollLeft=c=this.scrollParent[0].scrollLeft-b.scrollSpeed}else{if(a.pageY-d(document).scrollTop()<b.scrollSensitivity)c=d(document).scrollTop(d(document).scrollTop()-
b.scrollSpeed);else if(d(window).height()-(a.pageY-d(document).scrollTop())<b.scrollSensitivity)c=d(document).scrollTop(d(document).scrollTop()+b.scrollSpeed);if(a.pageX-d(document).scrollLeft()<b.scrollSensitivity)c=d(document).scrollLeft(d(document).scrollLeft()-b.scrollSpeed);else if(d(window).width()-(a.pageX-d(document).scrollLeft())<b.scrollSensitivity)c=d(document).scrollLeft(d(document).scrollLeft()+b.scrollSpeed)}c!==false&&d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,
a)}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";for(b=this.items.length-1;b>=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0],
e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a,c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset();
c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp({target:null});this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):
this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate",null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}if(this.placeholder){this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null,
dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem):d(this.domPosition.parent).prepend(this.currentItem)}return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")},
toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute||"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+j<k&&b+l>g&&b+l<h;return this.options.tolerance=="pointer"||this.options.forcePointerForContainers||
this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>a[this.floating?"width":"height"]?j:g<b+this.helperProportions.width/2&&c-this.helperProportions.width/2<h&&i<e+this.helperProportions.height/2&&f-this.helperProportions.height/2<k},_intersectsWithPointer:function(a){var b=d.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,a.top,a.height);a=d.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,a.left,a.width);b=b&&a;a=this._getDragVerticalDirection();
var c=this._getDragHorizontalDirection();if(!b)return false;return this.floating?c&&c=="right"||a=="down"?2:1:a&&(a=="down"?2:1)},_intersectsWithSides:function(a){var b=d.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,a.top+a.height/2,a.height);a=d.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,a.left+a.width/2,a.width);var c=this._getDragVerticalDirection(),e=this._getDragHorizontalDirection();return this.floating&&e?e=="right"&&a||e=="left"&&!a:c&&(c=="down"&&b||c=="up"&&!b)},
_getDragVerticalDirection:function(){var a=this.positionAbs.top-this.lastPositionAbs.top;return a!=0&&(a>0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith();
if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!=this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),
this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a=this.currentItem.find(":data(sortable-item)"),b=0;b<this.items.length;b++)for(var c=0;c<a.length;c++)a[c]==this.items[b].item[0]&&this.items.splice(b,1)},_refreshItems:function(a){this.items=[];this.containers=[this];var b=this.items,c=[[d.isFunction(this.options.items)?this.options.items.call(this.element[0],a,{item:this.currentItem}):d(this.options.items,this.element),
this]],e=this._connectWith();if(e)for(var f=e.length-1;f>=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable");if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h<g;h++){i=d(e[h]);i.data("sortable-item",a);b.push({item:i,instance:a,width:0,height:0,left:0,top:0})}}},refreshPositions:function(a){if(this.offsetParent&&
this.helper)this.offset.parent=this._getParentOffset();for(var b=this.items.length-1;b>=0;b--){var c=this.items[b];if(!(c.instance!=this.currentContainer&&this.currentContainer&&c.item[0]!=this.currentItem[0])){var e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b=
this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width=this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f=
d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f},update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||
0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b=null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out",
a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h-
f)<b){b=Math.abs(h-f);e=this.items[g]}}if(e||this.options.dropOnEmpty){this.currentContainer=this.containers[c];e?this._rearrange(a,e,null,true):this._rearrange(a,null,this.containers[c].element,true);this._trigger("change",a,this._uiHash());this.containers[c]._trigger("change",a,this._uiHash(this));this.options.placeholder.update(this.currentContainer,this.placeholder);this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}}},_createHelper:function(a){var b=
this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a,this.currentItem])):b.helper=="clone"?this.currentItem.clone():this.currentItem;a.parents("body").length||d(b.appendTo!="parent"?b.appendTo:this.currentItem[0].parentNode)[0].appendChild(a[0]);if(a[0]==this.currentItem[0])this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")};if(a[0].style.width==
""||b.forceHelperSize)a.width(this.currentItem.width());if(a[0].style.height==""||b.forceHelperSize)a.height(this.currentItem.height());return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=
this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a=
{top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),
10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,d(a.containment=="document"?
document:window).width()-this.helperProportions.width-this.margins.left,(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)){var b=d(a.containment)[0];a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"),
10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(a,b){if(!b)b=
this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&
this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(c[0].tagName);if(this.cssPosition=="relative"&&!(this.scrollParent[0]!=document&&this.scrollParent[0]!=this.offsetParent[0]))this.offset.relative=this._getRelativeOffset();
var f=a.pageX,g=a.pageY;if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.left<this.containment[0])f=this.containment[0]+this.offset.click.left;if(a.pageY-this.offset.click.top<this.containment[1])g=this.containment[1]+this.offset.click.top;if(a.pageX-this.offset.click.left>this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-
this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.top<this.containment[1]||g-this.offset.click.top>this.containment[3])?g:!(g-this.offset.click.top<this.containment[1])?g-b.grid[1]:g+b.grid[1]:g;f=this.originalPageX+Math.round((f-this.originalPageX)/b.grid[0])*b.grid[0];f=this.containment?!(f-this.offset.click.left<this.containment[0]||f-this.offset.click.left>this.containment[2])?f:!(f-this.offset.click.left<this.containment[0])?f-b.grid[0]:f+b.grid[0]:f}}return{top:g-
this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(d.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:c.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(d.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:c.scrollLeft())}},_rearrange:function(a,b,c,e){c?c[0].appendChild(this.placeholder[0]):b.item[0].parentNode.insertBefore(this.placeholder[0],
this.direction=="down"?b.item[0]:b.item[0].nextSibling);this.counter=this.counter?++this.counter:1;var f=this,g=this.counter;window.setTimeout(function(){g==f.counter&&f.refreshPositions(!e)},0)},_clear:function(a,b){this.reverting=false;var c=[];!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem);this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var e in this._storedCSS)if(this._storedCSS[e]=="auto"||this._storedCSS[e]=="static")this._storedCSS[e]=
"";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!b&&c.push(function(f){this._trigger("receive",f,this._uiHash(this.fromOutside))});if((this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!b)c.push(function(f){this._trigger("update",f,this._uiHash())});if(!d.ui.contains(this.element[0],this.currentItem[0])){b||c.push(function(f){this._trigger("remove",
f,this._uiHash())});for(e=this.containers.length-1;e>=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive",g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this,
this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over=0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop",
a,this._uiHash());for(e=0;e<c.length;e++)c[e].call(this,a);this._trigger("stop",a,this._uiHash())}return false}b||this._trigger("beforeStop",a,this._uiHash());this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.helper[0]!=this.currentItem[0]&&this.helper.remove();this.helper=null;if(!b){for(e=0;e<c.length;e++)c[e].call(this,a);this._trigger("stop",a,this._uiHash())}this.fromOutside=false;return true},_trigger:function(){d.Widget.prototype._trigger.apply(this,arguments)===false&&this.cancel()},
_uiHash:function(a){var b=a||this;return{helper:b.helper,placeholder:b.placeholder||d([]),position:b.position,originalPosition:b.originalPosition,offset:b.positionAbs,item:b.currentItem,sender:a?a.element:null}}});d.extend(d.ui.sortable,{version:"1.8.15"})})(jQuery);
;/*
* jQuery UI Accordion 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Accordion
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
*/
(function(c){c.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:true,clearStyle:false,collapsible:false,event:"click",fillSpace:false,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix");
a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");
if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var h=d.closest(".ui-accordion-header");a.active=h.length?h:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion",
function(f){return a._keydown(f)}).next().attr("role","tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(f){a._clickHandler.call(a,f,this);f.preventDefault()})},_createIcons:function(){var a=
this.options;if(a.icons){c("<span></span>").addClass("ui-icon "+a.icons.header).prependTo(this.headers);this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex");
this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons();
b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,h=this.headers.index(a.target),f=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:f=this.headers[(h+1)%d];break;case b.LEFT:case b.UP:f=this.headers[(h-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target);
a.preventDefault()}if(f){c(a.target).attr("tabIndex",-1);c(f).attr("tabIndex",0);f.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+
c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options;
if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){var h=this.active;j=a.next();g=this.active.next();e={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):j,oldContent:g};var f=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(j,g,e,b,f);h.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);
if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected);a.next().addClass("ui-accordion-content-active")}}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);this.active.next().addClass("ui-accordion-content-active");var g=this.active.next(),
e={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:g},j=this.active=c([]);this._toggle(j,g,e)}},_toggle:function(a,b,d,h,f){var g=this,e=g.options;g.toShow=a;g.toHide=b;g.data=d;var j=function(){if(g)return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data);g.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&h?{toShow:c([]),toHide:b,complete:j,down:f,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:f,autoHeight:e.autoHeight||
e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;h=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!h[k]&&!c.easing[k])k="slide";h[k]||(h[k]=function(l){this.slide(l,{easing:k,duration:i||700})});h[k](d)}else{if(e.collapsible&&h)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false",
"aria-selected":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");if(this.toHide.length)this.toHide.parent()[0].className=this.toHide.parent()[0].className;this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.15",
animations:{slide:function(a,b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),h=0,f={},g={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){g[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/);
f[i]={value:j[1],unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(g,{step:function(j,i){if(i.prop=="height")h=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=h*f[i.prop].value+f[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide",
paddingTop:"hide",paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery);
;/*
* jQuery UI Autocomplete 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Autocomplete
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
* jquery.ui.position.js
*/
(function(d){var e=0;d.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:false,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var a=this,b=this.element[0].ownerDocument,g;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!(a.options.disabled||a.element.propAttr("readOnly"))){g=
false;var f=d.ui.keyCode;switch(c.keyCode){case f.PAGE_UP:a._move("previousPage",c);break;case f.PAGE_DOWN:a._move("nextPage",c);break;case f.UP:a._move("previous",c);c.preventDefault();break;case f.DOWN:a._move("next",c);c.preventDefault();break;case f.ENTER:case f.NUMPAD_ENTER:if(a.menu.active){g=true;c.preventDefault()}case f.TAB:if(!a.menu.active)return;a.menu.select(c);break;case f.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=
a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);break}}}).bind("keypress.autocomplete",function(c){if(g){g=false;c.preventDefault()}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};
this.menu=d("<ul></ul>").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&&
a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");
d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&&
b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source=
this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search",b)!==false)return this._search(a)},_search:function(a){this.pending++;this.element.addClass("ui-autocomplete-loading");this.source({term:a},this.response)},_response:function(a){if(!this.options.disabled&&a&&a.length){a=this._normalize(a);this._suggest(a);this._trigger("open")}else this.close();
this.pending--;this.pending||this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this.menu.element.hide();this.menu.deactivate();this._trigger("close",a)}},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(a){if(a.length&&a[0].label&&a[0].value)return a;return d.map(a,function(b){if(typeof b==="string")return{label:b,value:b};return d.extend({label:b.label||
b.value,value:b.value||b.label},b)})},_suggest:function(a){var b=this.menu.element.empty().zIndex(this.element.zIndex()+1);this._renderMenu(b,a);this.menu.deactivate();this.menu.refresh();b.show();this._resizeMenu();b.position(d.extend({of:this.element},this.options.position));this.options.autoFocus&&this.menu.next(new d.Event("mouseover"))},_resizeMenu:function(){var a=this.menu.element;a.outerWidth(Math.max(a.width("").outerWidth(),this.element.outerWidth()))},_renderMenu:function(a,b){var g=this;
d.each(b,function(c,f){g._renderItem(a,f)})},_renderItem:function(a,b){return d("<li></li>").data("item.autocomplete",b).append(d("<a></a>").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,
"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery);
(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex",
-1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");
this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b,
this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active||
this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active||
this.first()?":last":":first"))},hasScroll:function(){return this.element.height()<this.element[d.fn.prop?"prop":"attr"]("scrollHeight")},select:function(e){this._trigger("selected",e,{item:this.active})}})})(jQuery);
;/*
* jQuery UI Dialog 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Dialog
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
* jquery.ui.button.js
* jquery.ui.draggable.js
* jquery.ui.mouse.js
* jquery.ui.position.js
* jquery.ui.resizable.js
*/
(function(c,l){var m={buttons:true,height:true,maxHeight:true,maxWidth:true,minHeight:true,minWidth:true,width:true},n={maxHeight:true,maxWidth:true,minHeight:true,minWidth:true},o=c.attrFn||{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true,click:true};c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,
position:{my:"center",at:"center",collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||"&#160;",e=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("<div></div>")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+
b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog","aria-labelledby":e}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var f=(a.uiDialogTitlebar=c("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),
h=c('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);return false}).appendTo(f);(a.uiDialogTitlebarCloseText=c("<span></span>")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("<span></span>").addClass("ui-dialog-title").attr("id",
e).html(d).prependTo(f);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;f.find("*").add(f).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");
a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d,e;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==
b.uiDialog[0]){e=c(this).css("z-index");isNaN(e)||(d=Math.max(d,e))}});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,e=d.options;if(e.modal&&!a||!e.stack&&!e.modal)return d._trigger("focus",b);if(e.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=e.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()};c.ui.dialog.maxZ+=1;
d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;a._size();a._position(b.position);d.show(b.show);a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(e){if(e.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),f=g.filter(":first");g=g.filter(":last");if(e.target===g[0]&&!e.shiftKey){f.focus(1);return false}else if(e.target===
f[0]&&e.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,e=c("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("<div></div>").addClass("ui-dialog-buttonset").appendTo(e);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,
function(){return!(d=true)});if(d){c.each(a,function(f,h){h=c.isFunction(h)?{click:h,text:f}:h;var i=c('<button type="button"></button>').click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.each(h,function(j,k){if(j!=="click")j in o?i[j](k):i.attr(j,k)});c.fn.button&&i.button()});e.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(f){return{position:f.position,offset:f.offset}}var b=this,d=b.options,e=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",
handle:".ui-dialog-titlebar",containment:"document",start:function(f,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",f,a(h))},drag:function(f,h){b._trigger("drag",f,a(h))},stop:function(f,h){d.position=[h.position.left-e.scrollLeft(),h.position.top-e.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);b._trigger("dragStop",f,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(f){return{originalPosition:f.originalPosition,
originalSize:f.originalSize,position:f.position,size:f.size}}a=a===l?this.options.resizable:a;var d=this,e=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:a,start:function(f,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",f,b(h))},resize:function(f,h){d._trigger("resize",
f,b(h))},stop:function(f,h){c(this).removeClass("ui-dialog-resizing");e.height=c(this).height();e.width=c(this).width();d._trigger("resizeStop",f,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(a){var b=[],d=[0,0],e;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):
[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,f){if(+b[g]===b[g]){d[g]=b[g];b[g]=f}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(e=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(c.extend({of:window},a));e||this.uiDialog.hide()},_setOptions:function(a){var b=this,d={},e=false;c.each(a,function(g,f){b._setOption(g,f);
if(g in m)e=true;if(g in n)d[g]=f});e&&this._size();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",d)},_setOption:function(a,b){var d=this,e=d.uiDialog;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":e.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?e.addClass("ui-dialog-disabled"):
e.removeClass("ui-dialog-disabled");break;case "draggable":var g=e.is(":data(draggable)");g&&!b&&e.draggable("destroy");!g&&b&&d._makeDraggable();break;case "position":d._position(b);break;case "resizable":(g=e.is(":data(resizable)"))&&!b&&e.resizable("destroy");g&&typeof b==="string"&&e.resizable("option","handles",b);!g&&b!==false&&d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||"&#160;"));break}c.Widget.prototype._setOption.apply(d,arguments)},_size:function(){var a=
this.options,b,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();d=Math.max(0,a.minHeight-b);if(a.height==="auto")if(c.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();a=this.element.css("height","auto").height();e||this.uiDialog.hide();this.element.height(Math.max(a,d))}else this.element.height(Math.max(a.height-
b,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.15",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "),
create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()<c.ui.dialog.overlay.maxZ)return false})},1);c(document).bind("keydown.dialog-overlay",function(d){if(a.options.closeOnEscape&&d.keyCode&&d.keyCode===c.ui.keyCode.ESCAPE){a.close(d);d.preventDefault()}});c(window).bind("resize.dialog-overlay",c.ui.dialog.overlay.resize)}var b=(this.oldInstances.pop()||c("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),
height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){var b=c.inArray(a,this.instances);b!=-1&&this.oldInstances.push(this.instances.splice(b,1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var d=0;c.each(this.instances,function(){d=Math.max(d,this.css("z-index"))});this.maxZ=d},height:function(){var a,b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);
b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a<b?c(window).height()+"px":a+"px"}else return c(document).height()+"px"},width:function(){var a,b;if(c.browser.msie){a=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth);b=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth);return a<b?c(window).width()+"px":a+"px"}else return c(document).width()+"px"},resize:function(){var a=c([]);c.each(c.ui.dialog.overlay.instances,function(){a=
a.add(this)});a.css({width:0,height:0}).css({width:c.ui.dialog.overlay.width(),height:c.ui.dialog.overlay.height()})}});c.extend(c.ui.dialog.overlay.prototype,{destroy:function(){c.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);
;/*
* jQuery UI Tabs 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Tabs
*
* Depends:
* jquery.ui.core.js
* jquery.ui.widget.js
*/
(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"<div></div>",remove:null,select:null,show:null,spinner:"<em>Loading&#8230;</em>",tabTemplate:"<li><a href='#{href}'><span>#{label}</span></a></li>"},_create:function(){this._tabify(true)},_setOption:function(b,e){if(b=="selected")this.options.collapsible&&
e==this.options.selected||this.select(e);else{this.options[b]=e;this._tabify()}},_tabId:function(b){return b.title&&b.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(b){return b.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[b].concat(d.makeArray(arguments)))},_ui:function(b,e){return{tab:b,panel:e,index:this.anchors.index(b)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=
d(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(b){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var a=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]||
(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))a.panels=a.panels.add(a.element.find(a._sanitizeSelector(i)));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=a._tabId(f);f.href="#"+i;f=a.element.find("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(a.panels[g-1]||a.list);f.data("destroy.tabs",true)}a.panels=a.panels.add(f)}else c.disabled.push(g)});if(b){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all");
this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(a._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected=
this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return a.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active");
if(c.selected>=0&&this.anchors.length){a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash)).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");a.element.queue("tabs",function(){a._trigger("show",null,a._ui(a.anchors[c.selected],a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash))[0]))});this.load(c.selected)}d(window).bind("unload",function(){a.lis.add(a.anchors).unbind(".tabs");a.lis=a.anchors=a.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"));
this.element[c.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);b=0;for(var j;j=this.lis[b];b++)d(j)[d.inArray(b,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+
g)};this.lis.bind("mouseover.tabs",function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal",
function(){e(f,o);a._trigger("show",null,a._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");a._trigger("show",null,a._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);a.element.dequeue("tabs")})}:function(g,f){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");a.element.dequeue("tabs")};
this.anchors.bind(c.event+".tabs",function(){var g=this,f=d(g).closest("li"),i=a.panels.filter(":not(.ui-tabs-hide)"),l=a.element.find(a._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||a.panels.filter(":animated").length||a._trigger("select",null,a._ui(this,l[0]))===false){this.blur();return false}c.selected=a.anchors.index(this);a.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected=
-1;c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this));this.blur();return false}c.cookie&&a._cookie(c.selected,c.cookie);if(l.length){i.length&&a.element.queue("tabs",function(){s(g,i)});a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier.";
d.browser.msie&&this.blur()});this.anchors.bind("click.tabs",function(){return false})},_getIndex:function(b){if(typeof b=="string")b=this.anchors.index(this.anchors.filter("[href$="+b+"]"));return b},destroy:function(){var b=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e=
d.data(this,"href.tabs");if(e)this.href=e;var a=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){a.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});b.cookie&&this._cookie(null,b.cookie);return this},add:function(b,
e,a){if(a===p)a=this.anchors.length;var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,b).replace(/#\{label\}/g,e));b=!b.indexOf("#")?b.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=c.element.find("#"+b);j.length||(j=d(h.panelTemplate).attr("id",b).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(a>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[a]);
j.insertBefore(this.panels[a])}h.disabled=d.map(h.disabled,function(k){return k>=a?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[a],this.panels[a]));return this},remove:function(b){b=this._getIndex(b);var e=this.options,a=this.lis.eq(b).remove(),c=this.panels.eq(b).remove();
if(a.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(b+(b+1<this.anchors.length?1:-1));e.disabled=d.map(d.grep(e.disabled,function(h){return h!=b}),function(h){return h>=b?--h:h});this._tabify();this._trigger("remove",null,this._ui(a.find("a")[0],c[0]));return this},enable:function(b){b=this._getIndex(b);var e=this.options;if(d.inArray(b,e.disabled)!=-1){this.lis.eq(b).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(a){return a!=b});this._trigger("enable",null,
this._ui(this.anchors[b],this.panels[b]));return this}},disable:function(b){b=this._getIndex(b);var e=this.options;if(b!=e.selected){this.lis.eq(b).addClass("ui-state-disabled");e.disabled.push(b);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[b],this.panels[b]))}return this},select:function(b){b=this._getIndex(b);if(b==-1)if(this.options.collapsible&&this.options.selected!=-1)b=this.options.selected;else return this;this.anchors.eq(b).trigger(this.options.event+".tabs");return this},
load:function(b){b=this._getIndex(b);var e=this,a=this.options,c=this.anchors.eq(b)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(b).addClass("ui-state-processing");if(a.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(a.spinner)}this.xhr=d.ajax(d.extend({},a.ajaxOptions,{url:h,success:function(k,n){e.element.find(e._sanitizeSelector(c.hash)).html(k);e._cleanup();a.cache&&d.data(c,
"cache.tabs",true);e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.error(k,n,b,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this},
url:function(b,e){this.anchors.eq(b).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.15"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(b,e){var a=this,c=this.options,h=a._rotate||(a._rotate=function(j){clearTimeout(a.rotation);a.rotation=setTimeout(function(){var k=c.selected;a.select(++k<a.anchors.length?k:0)},b);j&&j.stopPropagation()});e=a._unrotate||(a._unrotate=!e?function(j){j.clientX&&
a.rotate(null)}:function(){t=c.selected;h()});if(b){this.element.bind("tabsshow",h);this.anchors.bind(c.event+".tabs",e);h()}else{clearTimeout(a.rotation);this.element.unbind("tabsshow",h);this.anchors.unbind(c.event+".tabs",e);delete this._rotate;delete this._unrotate}return this}})})(jQuery);
;/*
* jQuery UI Datepicker 1.8.15
*
* Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* http://docs.jquery.com/UI/Datepicker
*
* Depends:
* jquery.ui.core.js
*/
(function(d,C){function M(){this.debug=false;this._curInst=null;this._keyEvent=false;this._disabledInputs=[];this._inDialog=this._datepickerShowing=false;this._mainDivId="ui-datepicker-div";this._inlineClass="ui-datepicker-inline";this._appendClass="ui-datepicker-append";this._triggerClass="ui-datepicker-trigger";this._dialogClass="ui-datepicker-dialog";this._disableClass="ui-datepicker-disabled";this._unselectableClass="ui-datepicker-unselectable";this._currentClass="ui-datepicker-current-day";this._dayOverClass=
"ui-datepicker-days-cell-over";this.regional=[];this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su",
"Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:false,showMonthAfterYear:false,yearSuffix:""};this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:false,hideIfNoPrevNext:false,navigationAsDateFormat:false,gotoCurrent:false,changeMonth:false,changeYear:false,yearRange:"c-10:c+10",showOtherMonths:false,selectOtherMonths:false,showWeek:false,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",
minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:true,showButtonPanel:false,autoSize:false,disabled:false};d.extend(this._defaults,this.regional[""]);this.dpDiv=N(d('<div id="'+this._mainDivId+'" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}function N(a){return a.bind("mouseout",
function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");b.length&&b.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");if(!(d.datepicker._isDisabledDatepicker(J.inline?a.parent()[0]:J.input[0])||!b.length)){b.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");
b.addClass("ui-state-hover");b.hasClass("ui-datepicker-prev")&&b.addClass("ui-datepicker-prev-hover");b.hasClass("ui-datepicker-next")&&b.addClass("ui-datepicker-next-hover")}})}function H(a,b){d.extend(a,b);for(var c in b)if(b[c]==null||b[c]==C)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.15"}});var B=(new Date).getTime(),J;d.extend(M.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},
setDefaults:function(a){H(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]=f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_-])/g,
"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:N(d('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}},_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",
function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b);b.settings.disabled&&this._disableDatepicker(a)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&&b.append.remove();if(c){b.append=d('<span class="'+this._appendClass+'">'+c+"</span>");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c==
"focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("<img/>").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('<button type="button"></button>').addClass(this._triggerClass).html(f==""?c:d("<img/>").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker():
d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;g<f.length;g++)if(f[g].length>h){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,
b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b),true);this._updateDatepicker(b);this._updateAlternate(b);b.settings.disabled&&this._disableDatepicker(a);b.dpDiv.css("display","block")}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+=
1;this._dialogInput=d('<input type="text" id="'+("dp"+this.uuid)+'" style="position: absolute; top: -100px; width: 0px; z-index: -10;"/>');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}H(a.settings,e||{});b=b&&b.constructor==Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/
2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]);d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b=
d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}},_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=
a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().removeClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b=d(a),c=d.data(a,
"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().addClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f==
a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false;for(var b=0;b<this._disabledInputs.length;b++)if(this._disabledInputs[b]==a)return true;return false},_getInst:function(a){try{return d.data(a,"datepicker")}catch(b){throw"Missing instance data for this datepicker";}},_optionDatepicker:function(a,b,c){var e=this._getInst(a);if(arguments.length==2&&typeof b=="string")return b=="defaults"?d.extend({},d.datepicker._defaults):e?b=="all"?
d.extend({},e.settings):this._get(e,b):null;var f=b||{};if(typeof b=="string"){f={};f[b]=c}if(e){this._curInst==e&&this._hideDatepicker();var h=this._getDateDatepicker(a,true),i=this._getMinMaxDate(e,"min"),g=this._getMinMaxDate(e,"max");H(e.settings,f);if(i!==null&&f.dateFormat!==C&&f.minDate===C)e.settings.minDate=this._formatDate(e,i);if(g!==null&&f.dateFormat!==C&&f.maxDate===C)e.settings.maxDate=this._formatDate(e,g);this._attachments(d(a),e);this._autoSize(e);this._setDate(e,h);this._updateAlternate(e);
this._updateDatepicker(e)}},_changeDatepicker:function(a,b,c){this._optionDatepicker(a,b,c)},_refreshDatepicker:function(a){(a=this._getInst(a))&&this._updateDatepicker(a)},_setDateDatepicker:function(a,b){if(a=this._getInst(a)){this._setDate(a,b);this._updateDatepicker(a);this._updateAlternate(a)}},_getDateDatepicker:function(a,b){(a=this._getInst(a))&&!a.inline&&this._setDateFromField(a,b);return a?this._getDate(a):null},_doKeyDown:function(a){var b=d.datepicker._getInst(a.target),c=true,e=b.dpDiv.is(".ui-datepicker-rtl");
b._keyEvent=true;if(d.datepicker._datepickerShowing)switch(a.keyCode){case 9:d.datepicker._hideDatepicker();c=false;break;case 13:c=d("td."+d.datepicker._dayOverClass+":not(."+d.datepicker._currentClass+")",b.dpDiv);c[0]&&d.datepicker._selectDay(a.target,b.selectedMonth,b.selectedYear,c[0]);if(a=d.datepicker._get(b,"onSelect")){c=d.datepicker._formatDate(b);a.apply(b.input?b.input[0]:null,[c,b])}else d.datepicker._hideDatepicker();return false;case 27:d.datepicker._hideDatepicker();break;case 33:d.datepicker._adjustDate(a.target,
a.ctrlKey?-d.datepicker._get(b,"stepBigMonths"):-d.datepicker._get(b,"stepMonths"),"M");break;case 34:d.datepicker._adjustDate(a.target,a.ctrlKey?+d.datepicker._get(b,"stepBigMonths"):+d.datepicker._get(b,"stepMonths"),"M");break;case 35:if(a.ctrlKey||a.metaKey)d.datepicker._clearDate(a.target);c=a.ctrlKey||a.metaKey;break;case 36:if(a.ctrlKey||a.metaKey)d.datepicker._gotoToday(a.target);c=a.ctrlKey||a.metaKey;break;case 37:if(a.ctrlKey||a.metaKey)d.datepicker._adjustDate(a.target,e?+1:-1,"D");c=
a.ctrlKey||a.metaKey;if(a.originalEvent.altKey)d.datepicker._adjustDate(a.target,a.ctrlKey?-d.datepicker._get(b,"stepBigMonths"):-d.datepicker._get(b,"stepMonths"),"M");break;case 38:if(a.ctrlKey||a.metaKey)d.datepicker._adjustDate(a.target,-7,"D");c=a.ctrlKey||a.metaKey;break;case 39:if(a.ctrlKey||a.metaKey)d.datepicker._adjustDate(a.target,e?-1:+1,"D");c=a.ctrlKey||a.metaKey;if(a.originalEvent.altKey)d.datepicker._adjustDate(a.target,a.ctrlKey?+d.datepicker._get(b,"stepBigMonths"):+d.datepicker._get(b,
"stepMonths"),"M");break;case 40:if(a.ctrlKey||a.metaKey)d.datepicker._adjustDate(a.target,+7,"D");c=a.ctrlKey||a.metaKey;break;default:c=false}else if(a.keyCode==36&&a.ctrlKey)d.datepicker._showDatepicker(this);else c=false;if(c){a.preventDefault();a.stopPropagation()}},_doKeyPress:function(a){var b=d.datepicker._getInst(a.target);if(d.datepicker._get(b,"constrainInput")){b=d.datepicker._possibleChars(d.datepicker._get(b,"dateFormat"));var c=String.fromCharCode(a.charCode==C?a.keyCode:a.charCode);
return a.ctrlKey||a.metaKey||c<" "||!b||b.indexOf(c)>-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target||a;if(a.nodeName.toLowerCase()!="input")a=d("input",
a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);if(d.datepicker._curInst&&d.datepicker._curInst!=b){d.datepicker._datepickerShowing&&d.datepicker._triggerOnClose(d.datepicker._curInst);d.datepicker._curInst.dpDiv.stop(true,true)}var c=d.datepicker._get(b,"beforeShow");H(b.settings,c?c.apply(a,[a,b]):{});b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value="";if(!d.datepicker._pos){d.datepicker._pos=
d.datepicker._findPos(a);d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.empty();b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b);c=d.datepicker._checkOffset(b,
c,e);b.dpDiv.css({position:d.datepicker._inDialog&&d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){var i=b.dpDiv.find("iframe.ui-datepicker-cover");if(i.length){var g=d.datepicker._getBorders(b.dpDiv);i.css({left:-g[0],top:-g[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex(d(a).zIndex()+1);d.datepicker._datepickerShowing=true;d.effects&&
d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f,h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}},_updateDatepicker:function(a){this.maxRows=4;var b=d.datepicker._getBorders(a.dpDiv);J=a;a.dpDiv.empty().append(this._generateHTML(a));var c=a.dpDiv.find("iframe.ui-datepicker-cover");c.length&&c.css({left:-b[0],top:-b[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()});a.dpDiv.find("."+
this._dayOverClass+" a").mouseover();b=this._getNumberOfMonths(a);c=b[1];a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");c>1&&a.dpDiv.addClass("ui-datepicker-multi-"+c).css("width",17*c+"em");a.dpDiv[(b[0]!=1||b[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl");a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&
a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var e=a.yearshtml;setTimeout(function(){e===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml);e=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(),h=a.input?a.input.outerWidth():
0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(),j=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>j&&j>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b=this._get(this._getInst(a),
"isRTL");a&&(a.type=="hidden"||a.nodeType!=1||d.expr.filters.hidden(a));)a=a[b?"previousSibling":"nextSibling"];a=d(a).offset();return[a.left,a.top]},_triggerOnClose:function(a){var b=this._get(a,"onClose");if(b)b.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a])},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b);this._curInst=
null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();d.datepicker._triggerOnClose(b);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},
_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&&!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):
0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth;b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e["selected"+(c=="M"?
"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a=d(a);
this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a,"altField");if(b){var c=this._get(a,
"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"?b.toString():b+"";if(b==
"")return null;var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;e=typeof e!="string"?e:(new Date).getFullYear()%100+parseInt(e,10);for(var f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,j=c=-1,l=-1,u=-1,k=false,o=function(p){(p=A+1<a.length&&a.charAt(A+1)==p)&&A++;return p},m=function(p){var D=o(p);p=new RegExp("^\\d{1,"+
(p=="@"?14:p=="!"?20:p=="y"&&D?4:p=="o"?3:2)+"}");p=b.substring(q).match(p);if(!p)throw"Missing number at position "+q;q+=p[0].length;return parseInt(p[0],10)},n=function(p,D,K){p=d.map(o(p)?K:D,function(w,x){return[[x,w]]}).sort(function(w,x){return-(w[1].length-x[1].length)});var E=-1;d.each(p,function(w,x){w=x[1];if(b.substr(q,w.length).toLowerCase()==w.toLowerCase()){E=x[0];q+=w.length;return false}});if(E!=-1)return E+1;else throw"Unknown name at position "+q;},s=function(){if(b.charAt(q)!=a.charAt(A))throw"Unexpected literal at position "+
q;q++},q=0,A=0;A<a.length;A++)if(k)if(a.charAt(A)=="'"&&!o("'"))k=false;else s();else switch(a.charAt(A)){case "d":l=m("d");break;case "D":n("D",f,h);break;case "o":u=m("o");break;case "m":j=m("m");break;case "M":j=n("M",i,g);break;case "y":c=m("y");break;case "@":var v=new Date(m("@"));c=v.getFullYear();j=v.getMonth()+1;l=v.getDate();break;case "!":v=new Date((m("!")-this._ticksTo1970)/1E4);c=v.getFullYear();j=v.getMonth()+1;l=v.getDate();break;case "'":if(o("'"))s();else k=true;break;default:s()}if(q<
b.length)throw"Extra/unparsed characters found in date: "+b.substring(q);if(c==-1)c=(new Date).getFullYear();else if(c<100)c+=(new Date).getFullYear()-(new Date).getFullYear()%100+(c<=e?0:-100);if(u>-1){j=1;l=u;do{e=this._getDaysInMonth(c,j-1);if(l<=e)break;j++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,j-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=j||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",
RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames:null)||this._defaults.monthNames;var i=function(o){(o=k+1<a.length&&
a.charAt(k+1)==o)&&k++;return o},g=function(o,m,n){m=""+m;if(i(o))for(;m.length<n;)m="0"+m;return m},j=function(o,m,n,s){return i(o)?s[m]:n[m]},l="",u=false;if(b)for(var k=0;k<a.length;k++)if(u)if(a.charAt(k)=="'"&&!i("'"))u=false;else l+=a.charAt(k);else switch(a.charAt(k)){case "d":l+=g("d",b.getDate(),2);break;case "D":l+=j("D",b.getDay(),e,f);break;case "o":l+=g("o",Math.round(((new Date(b.getFullYear(),b.getMonth(),b.getDate())).getTime()-(new Date(b.getFullYear(),0,0)).getTime())/864E5),3);
break;case "m":l+=g("m",b.getMonth()+1,2);break;case "M":l+=j("M",b.getMonth(),h,c);break;case "y":l+=i("y")?b.getFullYear():(b.getYear()%100<10?"0":"")+b.getYear()%100;break;case "@":l+=b.getTime();break;case "!":l+=b.getTime()*1E4+this._ticksTo1970;break;case "'":if(i("'"))l+="'";else u=true;break;default:l+=a.charAt(k)}return l},_possibleChars:function(a){for(var b="",c=false,e=function(h){(h=f+1<a.length&&a.charAt(f+1)==h)&&f++;return h},f=0;f<a.length;f++)if(c)if(a.charAt(f)=="'"&&!e("'"))c=
false;else b+=a.charAt(f);else switch(a.charAt(f)){case "d":case "m":case "y":case "@":b+="0123456789";break;case "D":case "M":return null;case "'":if(e("'"))b+="'";else c=true;break;default:b+=a.charAt(f)}return b},_get:function(a,b){return a.settings[b]!==C?a.settings[b]:this._defaults[b]},_setDateFromField:function(a,b){if(a.input.val()!=a.lastVal){var c=this._get(a,"dateFormat"),e=a.lastVal=a.input?a.input.val():null,f,h;f=h=this._getDefaultDate(a);var i=this._getFormatConfig(a);try{f=this.parseDate(c,
e,i)||h}catch(g){this.log(g);e=b?"":e}a.selectedDay=f.getDate();a.drawMonth=a.selectedMonth=f.getMonth();a.drawYear=a.selectedYear=f.getFullYear();a.currentDay=e?f.getDate():0;a.currentMonth=e?f.getMonth():0;a.currentYear=e?f.getFullYear():0;this._adjustInstDate(a)}},_getDefaultDate:function(a){return this._restrictMinMax(a,this._determineDate(a,this._get(a,"defaultDate"),new Date))},_determineDate:function(a,b,c){var e=function(h){var i=new Date;i.setDate(i.getDate()+h);return i},f=function(h){try{return d.datepicker.parseDate(d.datepicker._get(a,
"dateFormat"),h,d.datepicker._getFormatConfig(a))}catch(i){}var g=(h.toLowerCase().match(/^c/)?d.datepicker._getDate(a):null)||new Date,j=g.getFullYear(),l=g.getMonth();g=g.getDate();for(var u=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,k=u.exec(h);k;){switch(k[2]||"d"){case "d":case "D":g+=parseInt(k[1],10);break;case "w":case "W":g+=parseInt(k[1],10)*7;break;case "m":case "M":l+=parseInt(k[1],10);g=Math.min(g,d.datepicker._getDaysInMonth(j,l));break;case "y":case "Y":j+=parseInt(k[1],10);g=Math.min(g,
d.datepicker._getDaysInMonth(j,l));break}k=u.exec(h)}return new Date(j,l,g)};if(b=(b=b==null||b===""?c:typeof b=="string"?f(b):typeof b=="number"?isNaN(b)?c:e(b):new Date(b.getTime()))&&b.toString()=="Invalid Date"?c:b){b.setHours(0);b.setMinutes(0);b.setSeconds(0);b.setMilliseconds(0)}return this._daylightSavingAdjust(b)},_daylightSavingAdjust:function(a){if(!a)return null;a.setHours(a.getHours()>12?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;
b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e?"":this._formatDate(a));if(c=this._get(a,"onSelect")){e=this._formatDate(a);c.apply(a.input?a.input[0]:null,[e,a])}},_getDate:function(a){return!a.currentYear||a.input&&a.input.val()==
""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),j=this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay?new Date(9999,
9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),k=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=k&&n<k?k:n;this._daylightSavingAdjust(new Date(m,g,1))>n;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a,"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-j,1)),this._getFormatConfig(a));
n=this._canAdjustMonth(a,-1,m,g)?'<a class="ui-datepicker-prev ui-corner-all" onclick="DP_jQuery_'+B+".datepicker._adjustDate('#"+a.id+"', -"+j+", 'M');\" title=\""+n+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+n+"</span></a>":f?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+n+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+n+"</span></a>";var s=this._get(a,"nextText");s=!h?s:this.formatDate(s,this._daylightSavingAdjust(new Date(m,
g+j,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?'<a class="ui-datepicker-next ui-corner-all" onclick="DP_jQuery_'+B+".datepicker._adjustDate('#"+a.id+"', +"+j+", 'M');\" title=\""+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>":f?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>";j=this._get(a,"currentText");s=this._get(a,"gotoCurrent")&&
a.currentDay?u:b;j=!h?j:this.formatDate(j,s,this._getFormatConfig(a));h=!a.inline?'<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="DP_jQuery_'+B+'.datepicker._hideDatepicker();">'+this._get(a,"closeText")+"</button>":"";e=e?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(c?h:"")+(this._isInRange(a,s)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" onclick="DP_jQuery_'+
B+".datepicker._gotoToday('#"+a.id+"');\">"+j+"</button>":"")+(c?"":h)+"</div>":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;j=this._get(a,"showWeek");s=this._get(a,"dayNames");this._get(a,"dayNamesShort");var q=this._get(a,"dayNamesMin"),A=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),D=this._get(a,"showOtherMonths"),K=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var E=this._getDefaultDate(a),w="",x=0;x<i[0];x++){var O=
"";this.maxRows=4;for(var G=0;G<i[1];G++){var P=this._daylightSavingAdjust(new Date(m,g,a.selectedDay)),t=" ui-corner-all",y="";if(l){y+='<div class="ui-datepicker-group';if(i[1]>1)switch(G){case 0:y+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:y+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:y+=" ui-datepicker-group-middle";t="";break}y+='">'}y+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+t+'">'+(/all|left/.test(t)&&
x==0?c?f:n:"")+(/all|right/.test(t)&&x==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,k,o,x>0||G>0,A,v)+'</div><table class="ui-datepicker-calendar"><thead><tr>';var z=j?'<th class="ui-datepicker-week-col">'+this._get(a,"weekHeader")+"</th>":"";for(t=0;t<7;t++){var r=(t+h)%7;z+="<th"+((t+h+6)%7>=5?' class="ui-datepicker-week-end"':"")+'><span title="'+s[r]+'">'+q[r]+"</span></th>"}y+=z+"</tr></thead><tbody>";z=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay,
z);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;z=Math.ceil((t+z)/7);this.maxRows=z=l?this.maxRows>z?this.maxRows:z:z;r=this._daylightSavingAdjust(new Date(m,g,1-t));for(var Q=0;Q<z;Q++){y+="<tr>";var R=!j?"":'<td class="ui-datepicker-week-col">'+this._get(a,"calculateWeek")(r)+"</td>";for(t=0;t<7;t++){var I=p?p.apply(a.input?a.input[0]:null,[r]):[true,""],F=r.getMonth()!=g,L=F&&!K||!I[0]||k&&r<k||o&&r>o;R+='<td class="'+((t+h+6)%7>=5?" ui-datepicker-week-end":"")+(F?" ui-datepicker-other-month":"")+(r.getTime()==
P.getTime()&&g==a.selectedMonth&&a._keyEvent||E.getTime()==r.getTime()&&E.getTime()==P.getTime()?" "+this._dayOverClass:"")+(L?" "+this._unselectableClass+" ui-state-disabled":"")+(F&&!D?"":" "+I[1]+(r.getTime()==u.getTime()?" "+this._currentClass:"")+(r.getTime()==b.getTime()?" ui-datepicker-today":""))+'"'+((!F||D)&&I[2]?' title="'+I[2]+'"':"")+(L?"":' onclick="DP_jQuery_'+B+".datepicker._selectDay('#"+a.id+"',"+r.getMonth()+","+r.getFullYear()+', this);return false;"')+">"+(F&&!D?"&#xa0;":L?'<span class="ui-state-default">'+
r.getDate()+"</span>":'<a class="ui-state-default'+(r.getTime()==b.getTime()?" ui-state-highlight":"")+(r.getTime()==u.getTime()?" ui-state-active":"")+(F?" ui-priority-secondary":"")+'" href="#">'+r.getDate()+"</a>")+"</td>";r.setDate(r.getDate()+1);r=this._daylightSavingAdjust(r)}y+=R+"</tr>"}g++;if(g>11){g=0;m++}y+="</tbody></table>"+(l?"</div>"+(i[0]>0&&G==i[1]-1?'<div class="ui-datepicker-row-break"></div>':""):"");O+=y}w+=O}w+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':
"");a._keyEvent=false;return w},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var j=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),k='<div class="ui-datepicker-title">',o="";if(h||!j)o+='<span class="ui-datepicker-month">'+i[b]+"</span>";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='<select class="ui-datepicker-month" onchange="DP_jQuery_'+B+".datepicker._selectMonthYear('#"+a.id+"', this, 'M');\" >";for(var n=0;n<12;n++)if((!i||n>=e.getMonth())&&
(!m||n<=f.getMonth()))o+='<option value="'+n+'"'+(n==b?' selected="selected"':"")+">"+g[n]+"</option>";o+="</select>"}u||(k+=o+(h||!(j&&l)?"&#xa0;":""));if(!a.yearshtml){a.yearshtml="";if(h||!l)k+='<span class="ui-datepicker-year">'+c+"</span>";else{g=this._get(a,"yearRange").split(":");var s=(new Date).getFullYear();i=function(q){q=q.match(/c[+-].*/)?c+parseInt(q.substring(1),10):q.match(/[+-].*/)?s+parseInt(q,10):parseInt(q,10);return isNaN(q)?s:q};b=i(g[0]);g=Math.max(b,i(g[1]||""));b=e?Math.max(b,
e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(a.yearshtml+='<select class="ui-datepicker-year" onchange="DP_jQuery_'+B+".datepicker._selectMonthYear('#"+a.id+"', this, 'Y');\" >";b<=g;b++)a.yearshtml+='<option value="'+b+'"'+(b==c?' selected="selected"':"")+">"+b+"</option>";a.yearshtml+="</select>";k+=a.yearshtml;a.yearshtml=null}}k+=this._get(a,"yearSuffix");if(u)k+=(h||!(j&&l)?"&#xa0;":"")+o;k+="</div>";return k},_adjustInstDate:function(a,b,c){var e=a.drawYear+(c=="Y"?b:0),f=a.drawMonth+
(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&b<c?c:b;return b=a&&b>a?a:b},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");if(b)b.apply(a.input?
a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a);c=this._daylightSavingAdjust(new Date(c,
e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,
"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker=function(a){if(!this.length)return this;
if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));return this.each(function(){typeof a==
"string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new M;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.15";window["DP_jQuery_"+B]=d})(jQuery);
;

View file

@ -43,3 +43,6 @@ td {
margin: 10px 0px; margin: 10px 0px;
} }
.spectacle-passe {
opacity:0.5;
}

View file

@ -47,9 +47,6 @@
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places {% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
{% endfor %} {% endfor %}
</pre> </pre>
<script type="text/javascript"
src="{% static "js/jquery.min.js" %}"></script>
<script type="text/javascript" <script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script> src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script> <script>

View file

@ -43,9 +43,8 @@
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }} sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
</span> </span>
<script type="text/javascript" <script type="text/javascript"
src="{% static "js/jquery.min.js" %}"></script> src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
<script type="text/javascript" </script>
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function(){ $(function(){
$("table.etat-bda").stupidtable(); $("table.etat-bda").stupidtable();

View file

@ -2,8 +2,9 @@
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<script src="{% static 'bda/js/jquery-1.6.2.min.js'%}" type="text/javascript"></script> <script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
<script src="{% static 'bda/js/jquery-ui-1.8.15.custom.min.js' %}" type="text/javascript"></script> <script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %} {% endblock %}
@ -91,12 +92,6 @@ var django = {
</script> </script>
<h2 class="no-bottom-margin">Inscription au tirage au sort du BdA</h2> <h2 class="no-bottom-margin">Inscription au tirage au sort du BdA</h2>
{% if success %}
<p class="success table-top">Votre inscription a été mise à jour avec succès !</p>
{% endif %}
{% if stateerror %}
<p class="error table-top">Impossible d'enregistrer vos modifications: vous avez apporté d'autres modifications entre temps</p>
{% endif %}
<form class="form-horizontal" id="bda_form" method="post" action="{% url 'bda-tirage-inscription' tirage.id %}"> <form class="form-horizontal" id="bda_form" method="post" action="{% url 'bda-tirage-inscription' tirage.id %}">
{% csrf_token %} {% csrf_token %}
{% include "bda/inscription-formset.html" %} {% include "bda/inscription-formset.html" %}
@ -113,7 +108,7 @@ var django = {
<input type="submit" class="btn btn-primary" value="Enregistrer" /> <input type="submit" class="btn btn-primary" value="Enregistrer" />
</div> </div>
<p class="footnotes"> <p class="footnotes">
<sup>1</sup>: cette liste de v&oelig;u est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque v&oelig;u.<br /> <sup>1</sup>: cette liste de v&oelig;ux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque v&oelig;u.<br />
</p> </p>
</div> </div>
</form> </form>

View file

@ -3,15 +3,6 @@
{% block realcontent %} {% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2> <h2>Inscriptions pour BdA-Revente</h2>
{% if success %}
<p class="success">Ton inscription a bien été prise en compte !</p>
{% endif %}
{% if deja_revente %}
<p class="success">Des reventes existent déjà pour certains de ces spectacles ; vérifie les places disponibles sans tirage !</p>
{% elif inscrit_revente %}
<p class="success">Tu as été inscrit à une revente en cours pour ce spectacle !</p>
{% endif %}
<form action="" class="form-horizontal" method="post"> <form action="" class="form-horizontal" method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">

View file

@ -0,0 +1,43 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Mails de rappels</h2>
{% if sent %}
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
<ul>
{% for member in members %}
<li>{{ member.get_full_name }} ({{ member.email }})</li>
{% endfor %}
</ul>
{% else %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle
{{ show.title }}&nbsp;?</h3>
{% if show.rappel_sent %}
<p class="error">Attention, les mails ont déjà été envoyés le
{{ show.rappel_sent }}</p>
{% endif %}
{% endif %}
{% if not sent %}
<form action="" method="post">
{% csrf_token %}
<div class="pull-right">
<input class="btn btn-primary" type="submit" value="Envoyer" />
</div>
</form>
{% endif %}
<br/>
<hr/>
<h3>Forme des mails</h3>
<h4>Une seule place</h4>
{% for part in exemple_mail_1place %}
<pre>{{ part }}</pre>
{% endfor %}
<h4>Deux places</h4>
{% for part in exemple_mail_2places %}
<pre>{{ part }}</pre>
{% endfor %}
{% endblock %}

View file

@ -1,6 +0,0 @@
Bonjour {{ vendeur.first_name }} !
Je souhaiterais racheter ta place pour {{ spectacle.title }} le {{ spectacle.date }} ({{ spectacle.location }}) à {{ spectacle.price|floatformat:2 }}€.
Contacte-moi si tu es toujours intéressé·e !
{{ acheteur.get_full_name }} ({{ acheteur.email }})

View file

@ -1,23 +0,0 @@
Bonjour {{ name }},
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
{% if nb_attr == 2 %}
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
ces places sont strictement réservées aux personnes de moins de 28 ans.
{% endif %}
{% if show.listing %}Pour ce spectacle, tu as reçu des places sur
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
pour retirer {{ nb_attr|pluralize:"ta place,tes places" }}.
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
été distribués au burô.
{% endif %}
Si tu ne peux plus assister à cette représentation, tu peux
revendre ta place via BdA-revente, accessible directement sur
GestioCOF (lien "revendre une place du premier tirage" sur la page
d'accueil https://www.cof.ens.fr/gestion/).
En te souhaitant un excellent spectacle,
Le Bureau des Arts

View file

@ -1,9 +0,0 @@
Bonjour {{ acheteur.first_name }},
Tu t'étais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}
pour {{ spectacle.title }}.
Malheureusement, une autre personne a été tirée au sort pour racheter la place.
Tu pourras certainement retenter ta chance pour une autre revente !
À très bientôt,
Le Bureau des Arts

View file

@ -1,13 +0,0 @@
Bonjour {{ vendeur.first_name }},
Tu tes bien inscrit-e pour la revente de {{ spectacle.title }}.
{% with revente.date_tirage as time %}
Le tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
Si personne ne sest inscrit pour racheter la place, celle-ci apparaitra parmi
les « Places disponibles immédiatement à la revente » sur GestioCOF.
{% endwith %}
Bonne revente !
Le Bureau des Arts

View file

@ -1,7 +0,0 @@
Bonjour {{ vendeur.first_name }},
La personne tirée au sort pour racheter ta place pour {{ spectacle.title }} est {{ acheteur.get_full_name }}.
Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail.
Chaleureusement,
Le BdA

View file

@ -1,7 +0,0 @@
Bonjour {{ acheteur.first_name }},
Tu as été tiré-e au sort pour racheter une place pour {{ spectacle.title }} le {{ spectacle.date }} ({{ spectacle.location }}) à {{ spectacle.price|floatformat:2 }}€.
Tu peux contacter le/la vendeur-se à l'adresse {{ vendeur.email }}, ou en répondant à ce mail.
Chaleureusement,
Le BdA

View file

@ -1,14 +0,0 @@
Bonjour {{ user.first_name }}
Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }})
a été postée sur BdA-Revente.
{% with revente.date_tirage as time %}
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
sur ce lien : http://{{ domain }}/gestion{% url "bda-revente-interested" revente.id %}.
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
un tirage au sort le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil}}).
{% endwith %}
Chaleureusement,
Le BdA

View file

@ -1,11 +0,0 @@
Bonjour {{ user.first_name }}
Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }})
a été postée sur BdA-Revente.
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour
cette place : elle est disponible immédiatement à l'adresse
http://{{ domain }}/gestion{% url "bda-buy-revente" spectacle.id %}, à la disposition de tous.
Chaleureusement,
Le BdA

View file

@ -1,8 +1,6 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% block realcontent %} {% block realcontent %}
<h1><strong>{{ error_title }}</strong></h1>
<p>{{ error_description }}</p>
{% if choices %} {% if choices %}
<h3>Vos v&oelig;ux:</h3> <h3>Vos v&oelig;ux:</h3>
<ol> <ol>

View file

@ -2,9 +2,6 @@
{% block realcontent %} {% block realcontent %}
<h2><strong>Places attribuées</strong></h3> <h2><strong>Places attribuées</strong></h3>
{% if warning %}
<h3 class="error">Attention, vous avez reçu plusieurs places pour des spectacles différents à la même date !</h3>
{% endif %}
{% if places %} {% if places %}
<table class="table table-striped"> <table class="table table-striped">
{% for place in places %} {% for place in places %}

View file

@ -1,35 +0,0 @@
{% extends "base_title.html" %}
{% block realcontent %}
{% if sent %}
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
<ul>
{% for member in members %}
<li>{{ member.get_full_name }} ({{ member.email }})</li>
{% endfor %}
</ul>
{% else %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle
{{ show.title }}&nbsp;?</h3>
{% if show.rappel_sent %}
<p class="error">Attention, les mails ont déjà été envoyés le
{{ show.rappel_sent }}</p>
{% endif %}
{% endif %}
{% if not sent %}
<form action="" method="post">
{% csrf_token %}
<br />
<input type="submit" value="Envoyer" />
</form>
{% endif %}
<h3>Forme des mails</h3>
<br />Une seule place<br /><br />
<pre>{{ exemple_mail_1place }}</pre>
<br />Deux places<br /><br />
<pre>{{ exemple_mail_2places }}</pre>
{% endblock %}

View file

@ -1,6 +1,10 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %} {% block realcontent %}
<h2><strong>{{tirage_name}}</strong></h2> <h2><strong>{{tirage_name}}</strong></h2>
<h3>Liste des spectacles</h3> <h3>Liste des spectacles</h3>
@ -17,9 +21,9 @@
</thead> </thead>
<tbody> <tbody>
{% for spectacle in object_list %} {% for spectacle in object_list %}
<tr class="clickable-row" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}"> <tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td> <td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td> <td data-sort-value="{{ spectacle.timestamp }}"">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td> <td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}"> <td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
{{ spectacle.price |floatformat }}€ {{ spectacle.price |floatformat }}€
@ -29,9 +33,8 @@
</tbody> </tbody>
</table> </table>
<script type="text/javascript" <script type="text/javascript"
src="{% static "js/jquery.min.js" %}"></script> src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
<script type="text/javascript" </script>
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(function(){ $(function(){
$("table.etat-bda").stupidtable(); $("table.etat-bda").stupidtable();

View file

@ -3,21 +3,22 @@
import random import random
import hashlib import hashlib
import time import time
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import (
send_mass_custom_mail, send_custom_mail, render_custom_mail
)
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db import models, transaction from django.db import models, transaction
from django.core import serializers
from django.db.models import Count, Q, Sum from django.db.models import Count, Q, Sum
from django.core import serializers, mail
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.core.mail import send_mail from django.utils import timezone, formats
from django.template import loader
from django.utils import timezone
from django.views.generic.list import ListView from django.views.generic.list import ListView
from cof.decorators import cof_required, buro_required from cof.decorators import cof_required, buro_required
@ -111,33 +112,50 @@ def places(request, tirage_id):
warning = True warning = True
else: else:
dates.append(date) dates.append(date)
return render(request, "resume_places.html", # On prévient l'utilisateur s'il a deux places à la même date
if warning:
messages.warning(request, "Attention, vous avez reçu des places pour "
"des spectacles différents à la même date.")
return render(request, "bda/resume_places.html",
{"participant": participant, {"participant": participant,
"places": filtered_places, "places": filtered_places,
"tirage": tirage, "tirage": tirage,
"total": total, "total": total})
"warning": warning})
@cof_required @cof_required
def inscription(request, tirage_id): def inscription(request, tirage_id):
"""
Vue d'inscription à un tirage BdA.
- On vérifie qu'on se situe bien entre la date d'ouverture et la date de
fermeture des inscriptions.
- On vérifie que l'inscription n'a pas été modifiée entre le moment le
client demande le formulaire et le moment il soumet son inscription
(autre session par exemple).
"""
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
if timezone.now() < tirage.ouverture: if timezone.now() < tirage.ouverture:
error_desc = tirage.ouverture.strftime('Ouverture le %d %b %Y à %H:%M') # Le tirage n'est pas encore ouvert.
return render(request, 'bda/resume-inscription-tirage.html', opening = formats.localize(
{"error_title": "Le tirage n'est pas encore ouvert !", timezone.template_localtime(tirage.ouverture))
"error_description": error_desc}) messages.error(request, "Le tirage n'est pas encore ouvert : "
"ouverture le {:s}".format(opening))
return render(request, 'bda/resume-inscription-tirage.html', {})
if timezone.now() > tirage.fermeture: if timezone.now() > tirage.fermeture:
# Le tirage est fermé.
participant, created = Participant.objects.get_or_create( participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
choices = participant.choixspectacle_set.order_by("priority").all() choices = participant.choixspectacle_set.order_by("priority").all()
messages.error(request,
" C'est fini : tirage au sort dans la journée !")
return render(request, "bda/resume-inscription-tirage.html", return render(request, "bda/resume-inscription-tirage.html",
{"error_title": "C'est fini !", {"choices": choices})
"error_description":
"Tirage au sort dans la journée !",
"choices": choices})
def formfield_callback(f, **kwargs): def formfield_callback(f, **kwargs):
"""
Fonction utilisée par inlineformset_factory ci dessous.
Restreint les spectacles proposés aux spectacles du bo tirage.
"""
if f.name == "spectacle": if f.name == "spectacle":
kwargs['queryset'] = tirage.spectacle_set kwargs['queryset'] = tirage.spectacle_set
return f.formfield(**kwargs) return f.formfield(**kwargs)
@ -170,13 +188,19 @@ def inscription(request, tirage_id):
total_price += choice.spectacle.price total_price += choice.spectacle.price
if choice.double: if choice.double:
total_price += choice.spectacle.price total_price += choice.spectacle.price
# Messages
if success:
messages.success(request, "Votre inscription a été mise à jour avec "
"succès !")
if stateerror:
messages.error(request, "Impossible d'enregistrer vos modifications "
": vous avez apporté d'autres modifications "
"entre temps.")
return render(request, "bda/inscription-tirage.html", return render(request, "bda/inscription-tirage.html",
{"formset": formset, {"formset": formset,
"success": success,
"total_price": total_price, "total_price": total_price,
"dbstate": dbstate, "dbstate": dbstate,
'tirage': tirage, 'tirage': tirage})
"stateerror": stateerror})
def do_tirage(tirage_elt, token): def do_tirage(tirage_elt, token):
@ -300,7 +324,7 @@ def revente(request, tirage_id):
resellform = ResellForm(participant, request.POST, prefix='resell') resellform = ResellForm(participant, request.POST, prefix='resell')
annulform = AnnulForm(participant, prefix='annul') annulform = AnnulForm(participant, prefix='annul')
if resellform.is_valid(): if resellform.is_valid():
mails = [] datatuple = []
attributions = resellform.cleaned_data["attributions"] attributions = resellform.cleaned_data["attributions"]
with transaction.atomic(): with transaction.atomic():
for attribution in attributions: for attribution in attributions:
@ -315,24 +339,18 @@ def revente(request, tirage_id):
revente.notif_sent = False revente.notif_sent = False
revente.tirage_done = False revente.tirage_done = False
revente.shotgun = False revente.shotgun = False
mail_subject = "BdA-Revente : {:s}".format( context = {
attribution.spectacle.title) 'vendeur': participant.user,
mail_body = loader.render_to_string( 'show': attribution.spectacle,
'bda/mails/revente-new.txt', 'revente': revente
{'vendeur': participant.user, }
'spectacle': attribution.spectacle, datatuple.append((
'revente': revente} 'bda-revente-new', context,
) settings.MAIL_DATA['revente']['FROM'],
mails.append(mail.EmailMessage( [participant.user.email]
mail_subject, mail_body,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[participant.user.email],
reply_to=[
settings.MAIL_DATA['revente']['REPLYTO']
],
)) ))
revente.save() revente.save()
mail.get_connection().send_messages(mails) send_mass_custom_mail(datatuple)
# On annule une revente # On annule une revente
elif 'annul' in request.POST: elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul') annulform = AnnulForm(participant, request.POST, prefix='annul')
@ -402,7 +420,7 @@ def revente(request, tirage_id):
@login_required @login_required
def revente_interested(request, revente_id): def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id) revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, created = Participant.objects.get_or_create( participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage) user=request.user, tirage=revente.attribution.spectacle.tirage)
if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
return render(request, "bda-wrongtime.html", return render(request, "bda-wrongtime.html",
@ -417,11 +435,11 @@ def revente_interested(request, revente_id):
@login_required @login_required
def list_revente(request, tirage_id): def list_revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create( participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
deja_revente = False deja_revente = False
success = False success = False
inscrit_revente = False inscrit_revente = []
if request.method == 'POST': if request.method == 'POST':
form = InscriptionReventeForm(tirage, request.POST) form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid(): if form.is_valid():
@ -448,24 +466,36 @@ def list_revente(request, tirage_id):
if min_resell is not None: if min_resell is not None:
min_resell.answered_mail.add(participant) min_resell.answered_mail.add(participant)
min_resell.save() min_resell.save()
inscrit_revente = True inscrit_revente.append(spectacle)
success = True success = True
else: else:
form = InscriptionReventeForm( form = InscriptionReventeForm(
tirage, tirage,
initial={'spectacles': participant.choicesrevente.all()}) initial={'spectacles': participant.choicesrevente.all()}
)
# Messages
if success:
messages.success(request, "Ton inscription a bien été prise en compte")
if deja_revente:
messages.info(request, "Des reventes existent déjà pour certains de "
"ces spectacles, vérifie les places "
"disponibles sans tirage !")
if inscrit_revente:
shows = map("<li>{!s}</li>".format, inscrit_revente)
msg = (
"Tu as été inscrit à des reventes en cours pour les spectacles "
"<ul>{:s}</ul>".format('\n'.join(shows))
)
messages.info(request, msg, extra_tags="safe")
return render(request, "liste-reventes.html", return render(request, "bda/liste-reventes.html", {"form": form})
{"form": form,
"deja_revente": deja_revente, "success": success,
"inscrit_revente": inscrit_revente})
@login_required @login_required
def buy_revente(request, spectacle_id): def buy_revente(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id) spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage tirage = spectacle.tirage
participant, created = Participant.objects.get_or_create( participant, _ = Participant.objects.get_or_create(
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
reventes = SpectacleRevente.objects.filter( reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle, attribution__spectacle=spectacle,
@ -488,15 +518,17 @@ def buy_revente(request, spectacle_id):
revente = random.choice(reventes_shotgun) revente = random.choice(reventes_shotgun)
revente.soldTo = participant revente.soldTo = participant
revente.save() revente.save()
mail = loader.render_to_string('bda/mails/buy-shotgun.txt', { context = {
'spectacle': spectacle, 'show': spectacle,
'acheteur': request.user, 'acheteur': request.user,
'vendeur': revente.seller.user, 'vendeur': revente.seller.user
}) }
send_mail("BdA-Revente : %s" % spectacle.title, mail, send_custom_mail(
request.user.email, 'bda-buy-shotgun',
'bda@ens.fr',
[revente.seller.user.email], [revente.seller.user.email],
fail_silently=False) context=context,
)
return render(request, "bda-success.html", return render(request, "bda-success.html",
{"seller": revente.attribution.participant.user, {"seller": revente.attribution.participant.user,
"spectacle": spectacle}) "spectacle": spectacle})
@ -580,15 +612,16 @@ def unpaid(request, tirage_id):
def send_rappel(request, spectacle_id): def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id) show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples # Mails d'exemples
fake_member = request.user exemple_mail_1place = render_custom_mail('bda-rappel', {
fake_member.nb_attr = 1 'member': request.user,
exemple_mail_1place = loader.render_to_string('bda/mails/rappel.txt', { 'show': show,
'member': fake_member, 'nb_attr': 1
'show': show}) })
fake_member.nb_attr = 2 exemple_mail_2places = render_custom_mail('bda-rappel', {
exemple_mail_2places = loader.render_to_string('bda/mails/rappel.txt', { 'member': request.user,
'member': fake_member, 'show': show,
'show': show}) 'nb_attr': 2
})
# Contexte # Contexte
ctxt = {'show': show, ctxt = {'show': show,
'exemple_mail_1place': exemple_mail_1place, 'exemple_mail_1place': exemple_mail_1place,
@ -601,7 +634,7 @@ def send_rappel(request, spectacle_id):
# Demande de confirmation # Demande de confirmation
else: else:
ctxt['sent'] = False ctxt['sent'] = False
return render(request, "mails-rappel.html", ctxt) return render(request, "bda/mails-rappel.html", ctxt)
def descriptions_spectacles(request, tirage_id): def descriptions_spectacles(request, tirage_id):

View file

@ -1,27 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from .models import SurveyQuestionAnswer, SurveyQuestion, \
CofProfile, EventOption, EventOptionChoice, Event, Club, CustomMail, \
Survey, EventCommentField, EventRegistration
from .petits_cours_models import PetitCoursDemande, \
PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
PetitCoursAttributionCounter
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models import Q
import django.utils.six as six import django.utils.six as six
import autocomplete_light import autocomplete_light
from .petits_cours_models import PetitCoursDemande, \
PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
PetitCoursAttributionCounter
from .models import (
SurveyQuestionAnswer, SurveyQuestion, CofProfile, EventOption,
EventOptionChoice, Event, Club, EventCommentField, EventRegistration,
Survey
)
def add_link_field(target_model='', field='', link_text=six.text_type, def add_link_field(target_model='', field='', link_text=six.text_type,
desc_text=six.text_type): desc_text=six.text_type):
@ -99,137 +94,6 @@ class EventAdmin(admin.ModelAdmin):
] ]
class CofProfileInline(admin.StackedInline):
model = CofProfile
inline_classes = ("collapse open",)
class FkeyLookup(object):
def __init__(self, fkeydecl, short_description=None,
admin_order_field=None):
self.fk, fkattrs = fkeydecl.split('__', 1)
self.fkattrs = fkattrs.split('__')
self.short_description = short_description or self.fkattrs[-1]
self.admin_order_field = admin_order_field or fkeydecl
def __get__(self, obj, klass):
if obj is None:
"""
hack required to make Django validate (if obj is
None, then we're a class, and classes are callable
<wink>)
"""
return self
item = getattr(obj, self.fk)
for attr in self.fkattrs:
item = getattr(item, attr)
return item
def ProfileInfo(field, short_description, boolean=False):
def getter(self):
try:
return getattr(self.profile, field)
except Profile.DoesNotExist:
return ""
getter.short_description = short_description
getter.boolean = boolean
return getter
User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper")
User.profile_num = FkeyLookup("profile__cofprofile__num",
"Numéro")
User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement")
User.profile_mailing_cof = ProfileInfo("cof__mailing", "ML COF", True)
User.profile_mailing_bda = ProfileInfo("cof__mailing_bda", "ML BdA", True)
User.profile_mailing_bda_revente = ProfileInfo("cof__mailing_bda_revente",
"ML BdA-R", True)
class UserProfileAdmin(UserAdmin):
def is_buro(self, obj):
try:
return obj.profile.is_buro
except CofProfile.DoesNotExist:
return False
is_buro.short_description = 'Membre du Buro'
is_buro.boolean = True
def is_cof(self, obj):
try:
return obj.profile.is_cof
except CofProfile.DoesNotExist:
return False
is_cof.short_description = 'Membre du COF'
is_cof.boolean = True
list_display = ('profile_num',) + UserAdmin.list_display \
+ ('profile_login_clipper', 'profile_phone', 'profile_occupation',
'profile_mailing_cof', 'profile_mailing_bda',
'profile_mailing_bda_revente', 'is_buro', )
list_display_links = ('username', 'email', 'first_name', 'last_name')
list_filter = UserAdmin.list_filter \
+ (
# 'profile__cof__is_cof',
'profile__cof__is_buro',
'profile__cof__mailing',
'profile__cof__mailing_bda')
search_fields = UserAdmin.search_fields + ('profile__phone',)
# inlines = [
# CofProfileInline,
# ]
staff_fieldsets = [
(None, {'fields': ['username', 'password']}),
(_('Personal info'), {'fields': ['first_name', 'last_name', 'email']}),
]
def get_fieldsets(self, request, user=None):
if not request.user.is_superuser:
return self.staff_fieldsets
return super(UserProfileAdmin, self).get_fieldsets(request, user)
def save_model(self, request, user, form, change):
cof_group, created = Group.objects.get_or_create(name='COF')
if created:
# Si le groupe COF n'était pas déjà dans la bdd
# On lui assigne les bonnes permissions
perms = Permission.objects.filter(
Q(content_type__app_label='cof')
| Q(content_type__app_label='bda')
| (Q(content_type__app_label='auth')
& Q(content_type__model='user')))
cof_group.permissions = perms
# On y associe les membres du Burô
cof_group.user_set = User.objects.filter(profile__is_buro=True)
# Sauvegarde
cof_group.save()
# le Burô est staff et appartient au groupe COF
if user.profile.is_buro:
user.is_staff = True
user.groups.add(cof_group)
else:
user.is_staff = False
user.groups.remove(cof_group)
user.save()
# FIXME: This is absolutely horrible.
def user_unicode(self):
if self.first_name and self.last_name:
return "%s %s (%s)" % (self.first_name, self.last_name, self.username)
else:
return self.username
if six.PY2:
User.__unicode__ = user_unicode
else:
User.__str__ = user_unicode
class EventRegistrationAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[]) form = autocomplete_light.modelform_factory(EventRegistration, exclude=[])
list_display = ('__unicode__' if six.PY2 else '__str__', 'event', 'user', list_display = ('__unicode__' if six.PY2 else '__str__', 'event', 'user',
@ -271,10 +135,6 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin):
search_fields = ('name', 'email', 'phone', 'lieu', 'remarques') search_fields = ('name', 'email', 'phone', 'lieu', 'remarques')
class CustomMailAdmin(admin.ModelAdmin):
search_fields = ('shortname', 'title')
class ClubAdminForm(forms.ModelForm): class ClubAdminForm(forms.ModelForm):
def clean(self): def clean(self):
cleaned_data = super(ClubAdminForm, self).clean() cleaned_data = super(ClubAdminForm, self).clean()
@ -297,11 +157,8 @@ admin.site.register(Survey, SurveyAdmin)
admin.site.register(SurveyQuestion, SurveyQuestionAdmin) admin.site.register(SurveyQuestion, SurveyQuestionAdmin)
admin.site.register(Event, EventAdmin) admin.site.register(Event, EventAdmin)
admin.site.register(EventOption, EventOptionAdmin) admin.site.register(EventOption, EventOptionAdmin)
admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
admin.site.register(CofProfile) admin.site.register(CofProfile)
admin.site.register(Club, ClubAdmin) admin.site.register(Club, ClubAdmin)
admin.site.register(CustomMail)
admin.site.register(PetitCoursSubject) admin.site.register(PetitCoursSubject)
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin) admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin) admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)

View file

@ -1,16 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from ldap3 import Connection
from __future__ import print_function
from __future__ import unicode_literals
from django import shortcuts from django import shortcuts
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import User
from .models import CofProfile, Clipper
from .decorators import buro_required from .decorators import buro_required
from django.conf import settings
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@buro_required @buro_required
@ -18,44 +21,58 @@ def autocomplete(request):
if "q" not in request.GET: if "q" not in request.GET:
raise Http404 raise Http404
q = request.GET['q'] q = request.GET['q']
data = { data = {'q': q}
'q': q, cof_members = Group.objects.get(name="cof_members")
}
queries = {} queries = {}
bits = q.split() bits = q.split()
queries['members'] = CofProfile.objects.filter(Q(is_cof=True)) # Fetching data from User and Profile tables
queries['users'] = User.objects.filter(Q(profile__cof__is_cof=False)) queries['members'] = User.objects.filter(groups=cof_members)
queries['clippers'] = Clipper.objects queries['users'] = User.objects.exclude(groups=cof_members)
for bit in bits: for bit in bits:
queries['members'] = queries['members'].filter( queries['members'] = queries['members'].filter(
Q(profile__user__first_name__icontains=bit) Q(first_name__icontains=bit)
| Q(profile__user__last_name__icontains=bit) | Q(last_name__icontains=bit)
| Q(profile__user__username__icontains=bit) | Q(username__icontains=bit)
| Q(profile__login_clipper__icontains=bit)) | Q(profile__login_clipper__icontains=bit))
queries['users'] = queries['users'].filter( queries['users'] = queries['users'].filter(
Q(first_name__icontains=bit) Q(first_name__icontains=bit)
| Q(last_name__icontains=bit) | Q(last_name__icontains=bit)
| Q(username__icontains=bit)) | Q(username__icontains=bit))
queries['clippers'] = queries['clippers'].filter(
Q(fullname__icontains=bit)
| Q(username__icontains=bit))
queries['members'] = queries['members'].distinct() queries['members'] = queries['members'].distinct()
queries['users'] = queries['users'].distinct() queries['users'] = queries['users'].distinct()
usernames = list(queries['members'].values_list('profile__login_clipper',
flat='True')) \ # Clearing redundancies
+ list(queries['users'].values_list('profile__login_clipper', usernames = (
set(queries['members'].values_list('profile__login_clipper',
flat='True')) flat='True'))
queries['clippers'] = queries['clippers'] \ | set(queries['users'].values_list('profile__login_clipper',
.exclude(username__in=usernames).distinct() flat='True'))
# add clippers )
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(bit=bit)
for bit in bits]
))
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
]
# Resulting data
data.update(queries) data.update(queries)
data['options'] = any(bool(query) for query in queries.values())
options = 0 return shortcuts.render(request, "cof/autocomplete_user.html", data)
for query in queries.values():
options += len(query)
data['options'] = options
return shortcuts.render(request, "autocomplete_user.html", data)

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light import autocomplete_light
from django.contrib.auth.models import User from django.contrib.auth.models import User
autocomplete_light.register( autocomplete_light.register(
User, search_fields=('username', 'first_name', 'last_name'), User, search_fields=('username', 'first_name', 'last_name'),
autocomplete_js_attributes={'placeholder': 'membre...'}) attrs={'placeholder': 'membre...'}
)

View file

@ -1,29 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from django.contrib.auth.decorators import permission_required
from __future__ import print_function
from __future__ import unicode_literals
from django_cas_ng.decorators import user_passes_test cof_required = permission_required('cof.member')
cof_required_customdenied = permission_required('cof.member',
def is_cof(user):
try:
cofprofile = user.profile.cof
return cofprofile.is_cof
except:
return False
cof_required = user_passes_test(lambda u: is_cof(u))
cof_required_customdenied = user_passes_test(lambda u: is_cof(u),
login_url="cof-denied") login_url="cof-denied")
buro_required = permission_required('cof.buro')
def is_buro(user):
try:
cofprofile = user.profile.cof
return cofprofile.is_buro
except:
return False
buro_required = user_passes_test(lambda u: is_buro(u))

View file

@ -15,9 +15,9 @@ from django.core.validators import MinLengthValidator
from .models import CofProfile, EventCommentValue, \ from .models import CofProfile, EventCommentValue, \
CalendarSubscription, Club CalendarSubscription, Club
from .widgets import TriStateCheckbox from .widgets import TriStateCheckbox
from .shared import lock_table, unlock_table
from gestion.models import Profile from gestion.models import Profile
from gestion.shared import lock_table, unlock_table
from bda.models import Spectacle from bda.models import Spectacle
@ -185,7 +185,7 @@ class RegistrationUserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ("username", "first_name", "last_name", "email") fields = ["username", "first_name", "last_name", "email"]
class RegistrationPassUserForm(RegistrationUserForm): class RegistrationPassUserForm(RegistrationUserForm):
@ -222,7 +222,7 @@ class RegistrationCofProfileForm(forms.ModelForm):
class Meta: class Meta:
model = CofProfile model = CofProfile
fields = [ fields = [
"num", "type_cotiz", "is_buro", "num", "type_cotiz",
"mailing", "mailing_bda", "mailing_bda_revente", "mailing", "mailing_bda", "mailing_bda_revente",
] ]
@ -250,12 +250,6 @@ class RegistrationProfileForm(forms.ModelForm):
] ]
class RegistrationUserForm(forms.ModelForm):
class Meta:
model = User
fields = ["first_name", "last_name"]
STATUS_CHOICES = (('no', 'Non'), STATUS_CHOICES = (('no', 'Non'),
('wait', 'Oui mais attente paiement'), ('wait', 'Oui mais attente paiement'),
('paid', 'Oui payé'),) ('paid', 'Oui payé'),)

View file

@ -49,10 +49,10 @@ class Command(MyBaseCommand):
# Gaulois # Gaulois
gaulois = self.from_json('gaulois.json', DATA_DIR, User) gaulois = self.from_json('gaulois.json', DATA_DIR, User)
for user in gaulois: for user in gaulois:
CofProfile.objects.create( cofprofile = CofProfile.objects.create(
profile=user.profile, profile=user.profile,
is_cof=True
) )
cofprofile.is_cof = True
# Romains # Romains
self.from_json('romains.json', DATA_DIR, User) self.from_json('romains.json', DATA_DIR, User)
@ -113,3 +113,9 @@ class Command(MyBaseCommand):
# --- # ---
call_command('loadbdadevdata') call_command('loadbdadevdata')
# ---
# La K-Fêt
# ---
call_command('loadkfetdevdata')

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth.models import Group, Permission
from django.db import migrations, models from django.db import migrations, models
@ -21,9 +22,36 @@ def create_profile(apps, schema_editor):
p.save() p.save()
def preserve_perms(apps, schema_editor):
from django.contrib.auth.management import create_permissions
apps.models_module = True
create_permissions(apps, verbosity=0)
apps.models_module = None
CofProfile = apps.get_model("cof", "CofProfile")
memberp = Permission.objects.get(codename='member')
burop = Permission.objects.get(codename='buro')
# creates the groups for COF members and
member = Group.objects.create(name='cof_members')
buro = Group.objects.create(name='cof_buro')
# associate permissions to the respective groups.
buro.permissions = [burop, memberp]
member.permissions = [memberp]
for cofp in CofProfile.objects.filter(is_cof=True):
cofp.profile.user.groups.add(member)
for cofp in CofProfile.objects.filter(is_buro=True):
cofp.profile.user.groups.add(buro)
def remove_profile(apps, schema_editor): def remove_profile(apps, schema_editor):
raise NotImplementedError raise NotImplementedError
fuckup_perms = remove_profile
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -48,7 +76,23 @@ class Migration(migrations.Migration):
), ),
preserve_default=False, preserve_default=False,
), ),
migrations.AlterModelOptions(
name='cofprofile',
options={
'permissions': (('member', 'Is a COF member'),
('buro', 'Is part of COF staff')),
'verbose_name': 'Profil COF',
'verbose_name_plural': 'Profils COF'},
),
migrations.RunPython(create_profile, remove_profile), migrations.RunPython(create_profile, remove_profile),
migrations.AlterField(
model_name='cofprofile',
name='profile',
field=models.OneToOneField(
to='gestion.Profile',
related_name='cof'
),
),
migrations.RemoveField( migrations.RemoveField(
model_name='cofprofile', model_name='cofprofile',
name='comments', name='comments',
@ -69,13 +113,14 @@ class Migration(migrations.Migration):
model_name='cofprofile', model_name='cofprofile',
name='phone', name='phone',
), ),
migrations.AlterField( migrations.RunPython(preserve_perms, fuckup_perms),
migrations.RemoveField(
model_name='cofprofile', model_name='cofprofile',
name='profile', name='is_cof',
field=models.OneToOneField(
to='gestion.Profile',
related_name='cof'
), ),
migrations.RemoveField(
model_name='cofprofile',
name='is_buro',
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='cofprofile', model_name='cofprofile',

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_cof_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="cof_members")
def create_buro_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="cof_buro")
class Migration(migrations.Migration):
dependencies = [
('cof', '0009_generic_profiles'),
]
operations = [
migrations.RunPython(create_cof_group, migrations.RunPython.noop),
migrations.RunPython(create_buro_group, migrations.RunPython.noop),
]

View file

@ -7,12 +7,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cof', '0009_generic_profiles'), ('cof', '0010_create_cof_group'),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.DeleteModel(
model_name='cofprofile', name='Clipper',
name='is_cof', ),
migrations.DeleteModel(
name='CustomMail',
), ),
] ]

View file

@ -1,16 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.dispatch import receiver from django.contrib.auth.models import Group, User
from django.contrib.auth.models import Group, Permission, User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
import django.utils.six as six import django.utils.six as six
from django.db.models.signals import post_save, post_delete
from gestion.models import Profile from gestion.models import Profile
from bda.models import Spectacle from bda.models import Spectacle
@ -41,8 +35,6 @@ class CofProfile(models.Model):
max_length=choices_length( max_length=choices_length(
TYPE_COTIZ_CHOICES)) TYPE_COTIZ_CHOICES))
mailing = models.BooleanField("Recevoir les mails COF", default=False) mailing = models.BooleanField("Recevoir les mails COF", default=False)
# XXX. remove the following and use django auth.
is_buro = models.BooleanField("Membre du Burô", default=False)
# XXX. remove the following and put in a BDA profile # XXX. remove the following and put in a BDA profile
mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False) mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False)
mailing_bda_revente = models.BooleanField( mailing_bda_revente = models.BooleanField(
@ -54,8 +46,14 @@ class CofProfile(models.Model):
_("Remarques et précisions pour les petits cours"), _("Remarques et précisions pour les petits cours"),
blank=True, default="") blank=True, default="")
class Meta:
verbose_name = "Profil COF"
verbose_name_plural = "Profils COF"
permissions = (
('member', 'Is a COF member'),
('buro', 'Is part of COF staff'),
)
# is_cof = models.BooleanField("Membre du COF", default=False)
@property @property
def is_cof(self): def is_cof(self):
return self.profile.user.has_perm('cof.member') return self.profile.user.has_perm('cof.member')
@ -64,12 +62,18 @@ class CofProfile(models.Model):
def is_cof(self, really): def is_cof(self, really):
if really: if really:
g = Group.objects.get(name='cof_members') g = Group.objects.get(name='cof_members')
self.groups.add(g) self.profile.user.groups.add(g)
# XXX. remove the following and use django auth.
@property
def is_buro(self):
return self.profile.user.has_perm('cof.buro')
class Meta: @is_buro.setter
verbose_name = "Profil COF" def is_buro(self, really):
verbose_name_plural = "Profils COF" if really:
g = Group.objects.get(name='cof_buro')
self.profile.user.groups.add(g)
def __str__(self): def __str__(self):
return self.profile.user.username return self.profile.user.username
@ -87,22 +91,6 @@ class Club(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class CustomMail(models.Model):
shortname = models.SlugField(max_length=50, blank=False)
title = models.CharField("Titre", max_length=200, blank=False)
content = models.TextField("Contenu", blank=False)
comments = models.TextField("Informations contextuelles sur le mail",
blank=True)
class Meta:
verbose_name = "Mail personnalisable"
verbose_name_plural = "Mails personnalisables"
def __str__(self):
return "%s: %s" % (self.shortname, self.title)
@python_2_unicode_compatible @python_2_unicode_compatible
class Event(models.Model): class Event(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
@ -249,15 +237,6 @@ class SurveyAnswer(models.Model):
self.survey.title) self.survey.title)
#XXX. this needs to be removed according to Martin
class Clipper(models.Model):
username = models.CharField("Identifiant", max_length=20)
fullname = models.CharField("Nom complet", max_length=200)
def __str__(self):
return "Clipper %s" % self.username
@python_2_unicode_compatible @python_2_unicode_compatible
class CalendarSubscription(models.Model): class CalendarSubscription(models.Model):
token = models.UUIDField() token = models.UUIDField()

54
cof/petits_cours_forms.py Normal file
View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from captcha.fields import ReCaptchaField
from django import forms
from django.forms import ModelForm
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User
from .petits_cours_models import PetitCoursDemande, PetitCoursAbility
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super(BaseMatieresFormSet, self).clean()
if any(self.errors):
# Don't bother validating the formset unless each form is
# valid on its own
return
matieres = []
for i in range(0, self.total_form_count()):
form = self.forms[i]
if not form.cleaned_data:
continue
matiere = form.cleaned_data['matiere']
niveau = form.cleaned_data['niveau']
delete = form.cleaned_data['DELETE']
if not delete and (matiere, niveau) in matieres:
raise forms.ValidationError(
"Vous ne pouvez pas vous inscrire deux fois pour la "
"même matiere avec le même niveau.")
matieres.append((matiere, niveau))
class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
def __init__(self, *args, **kwargs):
super(DemandeForm, self).__init__(*args, **kwargs)
self.fields['matieres'].help_text = ''
class Meta:
model = PetitCoursDemande
fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu',
'matieres', 'agrege_requis', 'niveau', 'remarques')
widgets = {'matieres': forms.CheckboxSelectMultiple}
MatieresFormSet = inlineformset_factory(
User,
PetitCoursAbility,
fields=("matiere", "niveau", "agrege"),
formset=BaseMatieresFormSet
)

View file

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from functools import reduce
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models import Min
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.utils.six.moves import reduce
def choices_length(choices): def choices_length(choices):
@ -24,7 +21,6 @@ LEVELS_CHOICES = (
) )
@python_2_unicode_compatible
class PetitCoursSubject(models.Model): class PetitCoursSubject(models.Model):
name = models.CharField(_("Matière"), max_length=30) name = models.CharField(_("Matière"), max_length=30)
users = models.ManyToManyField(User, related_name="petits_cours_matieres", users = models.ManyToManyField(User, related_name="petits_cours_matieres",
@ -38,7 +34,6 @@ class PetitCoursSubject(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class PetitCoursAbility(models.Model): class PetitCoursAbility(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière")) matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
@ -52,11 +47,11 @@ class PetitCoursAbility(models.Model):
verbose_name_plural = "Compétences des petits cours" verbose_name_plural = "Compétences des petits cours"
def __str__(self): def __str__(self):
return "%s - %s - %s" % (self.user.username, return "{:s} - {!s} - {:s}".format(
self.matiere, self.niveau) self.user.username, self.matiere, self.niveau
)
@python_2_unicode_compatible
class PetitCoursDemande(models.Model): class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200) name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300) email = models.CharField(_("Adresse email"), max_length=300)
@ -70,7 +65,7 @@ class PetitCoursDemande(models.Model):
freq = models.CharField( freq = models.CharField(
_("Fréquence"), _("Fréquence"),
help_text=_("Indiquez ici la fréquence envisagée " help_text=_("Indiquez ici la fréquence envisagée "
+ "(hebdomadaire, 2 fois par semaine, ...)"), "(hebdomadaire, 2 fois par semaine, ...)"),
max_length=300, blank=True) max_length=300, blank=True)
lieu = models.CharField( lieu = models.CharField(
_("Lieu (si préférence)"), _("Lieu (si préférence)"),
@ -94,16 +89,42 @@ class PetitCoursDemande(models.Model):
blank=True, null=True) blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=True) created = models.DateTimeField(_("Date de création"), auto_now_add=True)
def get_candidates(self, redo=False):
"""
Donne la liste des profs disponibles pour chaque matière de la demande.
- On ne donne que les agrégés si c'est demandé
- Si ``redo`` vaut ``True``, cela signifie qu'on retraite la demande et
il ne faut pas proposer à nouveau des noms qui ont déjà été proposés
"""
for matiere in self.matieres.all():
candidates = PetitCoursAbility.objects.filter(
matiere=matiere,
niveau=self.niveau,
user__profile__is_cof=True,
user__profile__petits_cours_accept=True
)
if self.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attrs = self.petitcoursattribution_set.filter(matiere=matiere)
already_proposed = [
attr.user
for attr in attrs
]
candidates = candidates.exclude(user__in=already_proposed)
candidates = candidates.order_by('?').select_related().all()
yield (matiere, candidates)
class Meta: class Meta:
verbose_name = "Demande de petits cours" verbose_name = "Demande de petits cours"
verbose_name_plural = "Demandes de petits cours" verbose_name_plural = "Demandes de petits cours"
def __str__(self): def __str__(self):
return "Demande %d du %s" % (self.id, return "Demande {:d} du {:s}".format(
self.created.strftime("%d %b %Y")) self.id, self.created.strftime("%d %b %Y")
)
@python_2_unicode_compatible
class PetitCoursAttribution(models.Model): class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande")) demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande"))
@ -118,20 +139,40 @@ class PetitCoursAttribution(models.Model):
verbose_name_plural = "Attributions de petits cours" verbose_name_plural = "Attributions de petits cours"
def __str__(self): def __str__(self):
return "Attribution de la demande %d à %s pour %s" \ return "Attribution de la demande {:d} à {:s} pour {!s}".format(
% (self.demande.id, self.user.username, self.matiere) self.demande.id, self.user.username, self.matiere
)
@python_2_unicode_compatible
class PetitCoursAttributionCounter(models.Model): class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere")) matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere"))
count = models.IntegerField("Nombre d'envois", default=0) count = models.IntegerField("Nombre d'envois", default=0)
@classmethod
def get_uptodate(cls, user, matiere):
"""
Donne le compteur de l'utilisateur pour cette matière. Si le compteur
n'existe pas encore, il est initialisé avec le minimum des valeurs des
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(
user=user, matiere=matiere)
if created:
mincount = (
cls.objects.filter(matiere=matiere).exclude(user=user)
.aggregate(Min('count'))
['count__min']
)
counter.count = mincount
counter.save()
return counter
class Meta: class Meta:
verbose_name = "Compteur d'attribution de petits cours" verbose_name = "Compteur d'attribution de petits cours"
verbose_name_plural = "Compteurs d'attributions de petits cours" verbose_name_plural = "Compteurs d'attributions de petits cours"
def __str__(self): def __str__(self):
return "%d demandes envoyées à %s pour %s" \ return "{:d} demandes envoyées à {:s} pour {!s}".format(
% (self.count, self.user.username, self.matiere) self.count, self.user.username, self.matiere
)

View file

@ -1,36 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division import json
from __future__ import print_function from datetime import datetime
from __future__ import unicode_literals from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail from django.core import mail
from django.core.mail import EmailMessage
from django.forms import ModelForm
from django import forms
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.views.generic import ListView from django.views.generic import ListView, DetailView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.template import loader
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Min from django.contrib import messages
from .models import CofProfile from .models import CofProfile
from .petits_cours_models import PetitCoursDemande, \ from .petits_cours_models import (
PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursAbility, \ PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursSubject PetitCoursAbility, PetitCoursSubject
)
from .decorators import buro_required from .decorators import buro_required
from .shared import lock_table, unlock_tables from .petits_cours_forms import DemandeForm, MatieresFormSet
from captcha.fields import ReCaptchaField from gestion.shared import lock_table, unlock_tables
from datetime import datetime
import base64
import json
class DemandeListView(ListView): class DemandeListView(ListView):
@ -41,47 +32,17 @@ class DemandeListView(ListView):
def get_queryset(self): def get_queryset(self):
return PetitCoursDemande.objects.order_by('traitee', '-id').all() return PetitCoursDemande.objects.order_by('traitee', '-id').all()
@method_decorator(buro_required)
def dispatch(self, *args, **kwargs):
return super(DemandeListView, self).dispatch(*args, **kwargs)
class DemandeDetailView(DetailView):
model = PetitCoursDemande
template_name = "cof/details_demande_petit_cours.html"
context_object_name = "demande"
@buro_required def get_context_data(self, **kwargs):
def details(request, demande_id): context = super(DemandeDetailView, self).get_context_data(**kwargs)
demande = get_object_or_404(PetitCoursDemande, id=demande_id) obj = self.object
attributions = PetitCoursAttribution.objects.filter(demande=demande).all() context['attributions'] = obj.petitcoursattribution_set.all()
return render(request, "details_demande_petit_cours.html", return context
{"demande": demande,
"attributions": attributions})
def _get_attrib_counter(user, matiere):
counter, created = PetitCoursAttributionCounter \
.objects.get_or_create(user=user, matiere=matiere)
if created:
mincount = PetitCoursAttributionCounter.objects \
.filter(matiere=matiere).exclude(user=user).all() \
.aggregate(Min('count'))
counter.count = mincount['count__min']
counter.save()
return counter
def _get_demande_candidates(demande, redo=False):
for matiere in demande.matieres.all():
candidates = PetitCoursAbility.objects.filter(matiere=matiere,
niveau=demande.niveau)
candidates = candidates.filter(user__profile__is_cof=True,
user__profile__petits_cours_accept=True)
if demande.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attributions = PetitCoursAttribution.objects \
.filter(demande=demande, matiere=matiere).all()
for attrib in attributions:
candidates = candidates.exclude(user=attrib.user)
candidates = candidates.order_by('?').select_related().all()
yield (matiere, candidates)
@buro_required @buro_required
@ -95,12 +56,15 @@ def traitement(request, demande_id, redo=False):
proposed_for = {} proposed_for = {}
unsatisfied = [] unsatisfied = []
attribdata = {} attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
tuples = [] tuples = []
for candidate in candidates: for candidate in candidates:
user = candidate.user user = candidate.user
tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count) tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples) candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))] candidates = candidates[0:min(3, len(candidates))]
@ -131,7 +95,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
proposed_for = proposed_for.items() proposed_for = proposed_for.items()
attribdata = list(attribdata.items()) attribdata = list(attribdata.items())
proposed_mails = _generate_eleve_email(demande, proposed_for) proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", { mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals, "proposals": proposals,
"unsatisfied": unsatisfied, "unsatisfied": unsatisfied,
"extra": "extra":
@ -139,7 +103,9 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
'style="width:99%; height: 90px;">' 'style="width:99%; height: 90px;">'
'</textarea>' '</textarea>'
}) })
return render(request, "traitement_demande_petit_cours.html", for error in errors:
messages.error(request, error)
return render(request, "cof/traitement_demande_petit_cours.html",
{"demande": demande, {"demande": demande,
"unsatisfied": unsatisfied, "unsatisfied": unsatisfied,
"proposals": proposals, "proposals": proposals,
@ -148,19 +114,20 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
"mainmail": mainmail, "mainmail": mainmail,
"attribdata": json.dumps(attribdata), "attribdata": json.dumps(attribdata),
"redo": redo, "redo": redo,
"errors": errors,
}) })
def _generate_eleve_email(demande, proposed_for): def _generate_eleve_email(demande, proposed_for):
proposed_mails = [] return [
for user, matieres in proposed_for: (
msg = loader.render_to_string("petits-cours-mail-eleve.txt", { user,
render_custom_mail('petit-cours-mail-eleve', {
"demande": demande, "demande": demande,
"matieres": matieres "matieres": matieres
}) })
proposed_mails.append((user, msg)) )
return proposed_mails for user, matieres in proposed_for
]
def _traitement_other_preparing(request, demande): def _traitement_other_preparing(request, demande):
@ -170,7 +137,7 @@ def _traitement_other_preparing(request, demande):
proposed_for = {} proposed_for = {}
attribdata = {} attribdata = {}
errors = [] errors = []
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
candidates = dict([(candidate.user.id, candidate.user) candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates]) for candidate in candidates])
@ -178,17 +145,19 @@ def _traitement_other_preparing(request, demande):
proposals[matiere] = [] proposals[matiere] = []
for choice_id in range(min(3, len(candidates))): for choice_id in range(min(3, len(candidates))):
choice = int( choice = int(
request.POST["proposal-%d-%d" % (matiere.id, choice_id)]) request.POST["proposal-{:d}-{:d}"
.format(matiere.id, choice_id)]
)
if choice == -1: if choice == -1:
continue continue
if choice not in candidates: if choice not in candidates:
errors.append("Choix invalide pour la proposition %d" errors.append("Choix invalide pour la proposition {:d}"
"en %s" % (choice_id + 1, matiere)) "en {!s}".format(choice_id + 1, matiere))
continue continue
user = candidates[choice] user = candidates[choice]
if user in proposals[matiere]: if user in proposals[matiere]:
errors.append("La proposition %d en %s est un doublon" errors.append("La proposition {:d} en {!s} est un doublon"
% (choice_id + 1, matiere)) .format(choice_id + 1, matiere))
continue continue
proposals[matiere].append(user) proposals[matiere].append(user)
attribdata[matiere.id].append(user.id) attribdata[matiere.id].append(user.id)
@ -197,10 +166,11 @@ def _traitement_other_preparing(request, demande):
else: else:
proposed_for[user].append(matiere) proposed_for[user].append(matiere)
if not proposals[matiere]: if not proposals[matiere]:
errors.append("Aucune proposition pour %s" % (matiere,)) errors.append("Aucune proposition pour {!s}".format(matiere))
elif len(proposals[matiere]) < 3: elif len(proposals[matiere]) < 3:
errors.append("Seulement %d proposition%s pour %s" errors.append("Seulement {:d} proposition{:s} pour {!s}"
% (len(proposals[matiere]), .format(
len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "", "s" if len(proposals[matiere]) > 1 else "",
matiere)) matiere))
else: else:
@ -219,12 +189,15 @@ def _traitement_other(request, demande, redo):
proposed_for = {} proposed_for = {}
unsatisfied = [] unsatisfied = []
attribdata = {} attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
tuples = [] tuples = []
for candidate in candidates: for candidate in candidates:
user = candidate.user user = candidate.user
tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count) tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples) candidates, _ = zip(*tuples)
attribdata[matiere.id] = [] attribdata[matiere.id] = []
@ -241,7 +214,8 @@ def _traitement_other(request, demande, redo):
unsatisfied.append(matiere) unsatisfied.append(matiere)
proposals = proposals.items() proposals = proposals.items()
proposed_for = proposed_for.items() proposed_for = proposed_for.items()
return render(request, "traitement_demande_petit_cours_autre_niveau.html", return render(request,
"gestiocof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande, {"demande": demande,
"unsatisfied": unsatisfied, "unsatisfied": unsatisfied,
"proposals": proposals, "proposals": proposals,
@ -272,7 +246,7 @@ def _traitement_post(request, demande):
proposals_list = proposals.items() proposals_list = proposals.items()
proposed_for = proposed_for.items() proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for) proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", { mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals_list, "proposals": proposals_list,
"unsatisfied": unsatisfied, "unsatisfied": unsatisfied,
"extra": extra, "extra": extra,
@ -282,11 +256,11 @@ def _traitement_post(request, demande):
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO'] replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
mails_to_send = [] mails_to_send = []
for (user, msg) in proposed_mails: for (user, msg) in proposed_mails:
msg = EmailMessage("Petits cours ENS par le COF", msg, msg = mail.EmailMessage("Petits cours ENS par le COF", msg,
frommail, [user.email], frommail, [user.email],
[bccaddress], headers={'Reply-To': replyto}) [bccaddress], headers={'Reply-To': replyto})
mails_to_send.append(msg) mails_to_send.append(msg)
mails_to_send.append(EmailMessage("Cours particuliers ENS", mainmail, mails_to_send.append(mail.EmailMessage("Cours particuliers ENS", mainmail,
frommail, [demande.email], frommail, [demande.email],
[bccaddress], [bccaddress],
headers={'Reply-To': replyto})) headers={'Reply-To': replyto}))
@ -307,43 +281,18 @@ def _traitement_post(request, demande):
demande.traitee_par = request.user demande.traitee_par = request.user
demande.processed = datetime.now() demande.processed = datetime.now()
demande.save() demande.save()
return render(request, "traitement_demande_petit_cours_success.html", return render(request,
"cof/traitement_demande_petit_cours_success.html",
{"demande": demande, {"demande": demande,
"redo": redo, "redo": redo,
}) })
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super(BaseMatieresFormSet, self).clean()
if any(self.errors):
# Don't bother validating the formset unless each form is
# valid on its own
return
matieres = []
for i in range(0, self.total_form_count()):
form = self.forms[i]
if not form.cleaned_data:
continue
matiere = form.cleaned_data['matiere']
niveau = form.cleaned_data['niveau']
delete = form.cleaned_data['DELETE']
if not delete and (matiere, niveau) in matieres:
raise forms.ValidationError(
"Vous ne pouvez pas vous inscrire deux fois pour la "
"même matiere avec le même niveau.")
matieres.append((matiere, niveau))
@login_required @login_required
def inscription(request): def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user) profile, created = CofProfile.objects.get_or_create(user=request.user)
if not profile.is_cof: if not profile.is_cof:
return redirect("cof-denied") return redirect("cof-denied")
MatieresFormSet = inlineformset_factory(User, PetitCoursAbility,
fields=("matiere", "niveau",
"agrege",),
formset=BaseMatieresFormSet)
success = False success = False
if request.method == "POST": if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user) formset = MatieresFormSet(request.POST, instance=request.user)
@ -354,10 +303,14 @@ def inscription(request):
profile.save() profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject) PetitCoursSubject)
abilities = PetitCoursAbility.objects \ abilities = (
.filter(user=request.user).all() PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities: for ability in abilities:
_get_attrib_counter(ability.user, ability.matiere) PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
unlock_tables() unlock_tables()
success = True success = True
formset = MatieresFormSet(instance=request.user) formset = MatieresFormSet(instance=request.user)
@ -369,20 +322,6 @@ def inscription(request):
"remarques": profile.petits_cours_remarques}) "remarques": profile.petits_cours_remarques})
class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
def __init__(self, *args, **kwargs):
super(DemandeForm, self).__init__(*args, **kwargs)
self.fields['matieres'].help_text = ''
class Meta:
model = PetitCoursDemande
fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu',
'matieres', 'agrege_requis', 'niveau', 'remarques')
widgets = {'matieres': forms.CheckboxSelectMultiple}
@csrf_exempt @csrf_exempt
def demande(request): def demande(request):
success = False success = False

View file

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.contrib.sites.models import Site
from django.conf import settings
from django_cas_ng.backends import CASBackend
from django_cas_ng.utils import get_cas_client
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User as DjangoUser
from django.db import connection
from django.core.mail import send_mail
from django.template import Template, Context
from .models import CofProfile, CustomMail
User = get_user_model()
class COFCASBackend(CASBackend):
def authenticate_cas(self, ticket, service, request):
"""Verifies CAS ticket and gets or creates User object"""
client = get_cas_client(service_url=service)
username, attributes, _ = client.verify_ticket(ticket)
if attributes:
request.session['attributes'] = attributes
if not username:
return None
# Le CAS de l'ENS accepte les logins avec des espaces au début
# et à la fin, ainsi quavec une casse variable. On normalise pour
# éviter les doublons.
username = username.strip().lower()
profiles = CofProfile.objects.filter(login_clipper=username)
if len(profiles) > 0:
profile = profiles.order_by('-is_cof')[0]
user = profile.user
return user
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
# user will have an "unusable" password
user = User.objects.create_user(username, '')
user.save()
return user
def authenticate(self, ticket, service, request):
"""Authenticates CAS ticket and retrieves user data"""
user = self.authenticate_cas(ticket, service, request)
if user is None:
return user
try:
profile = user.profile
except CofProfile.DoesNotExist:
profile, created = CofProfile.objects.get_or_create(user=user)
profile.save()
if not profile.login_clipper:
profile.login_clipper = user.username
profile.save()
if not user.email:
user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper
user.save()
if profile.is_buro and not user.is_staff:
user.is_staff = True
user.save()
return user
def context_processor(request):
'''Append extra data to the context of the given request'''
data = {
"user": request.user,
"site": Site.objects.get_current(),
}
return data
def lock_table(*models):
query = "LOCK TABLES "
for i, model in enumerate(models):
table = model._meta.db_table
if i > 0:
query += ", "
query += "%s WRITE" % table
cursor = connection.cursor()
cursor.execute(query)
row = cursor.fetchone()
return row
def unlock_tables(*models):
cursor = connection.cursor()
cursor.execute("UNLOCK TABLES")
row = cursor.fetchone()
return row
unlock_table = unlock_tables
def send_custom_mail(to, shortname, context=None, from_email="cof@ens.fr"):
if context is None:
context = {}
if isinstance(to, DjangoUser):
context["nom"] = to.get_full_name()
context["prenom"] = to.first_name
to = to.email
mail = CustomMail.objects.get(shortname=shortname)
template = Template(mail.content)
message = template.render(Context(context))
send_mail(mail.title, message,
from_email, [to],
fail_silently=True)

View file

@ -520,22 +520,6 @@ h4.block-title {
list-style: none; list-style: none;
} }
.success {
font-weight: bold;
color: #7AB85F;
background-color: transparent;
}
.success a {
color: inherit;
}
.error {
font-weight: bold;
color: #F6BEBE;
background-color: transparent;
}
#main form ul.errorlist li { #main form ul.errorlist li {
font-weight: bold; font-weight: bold;
color: #B00000; color: #B00000;
@ -1088,3 +1072,50 @@ tr.awesome{
color: white; color: white;
padding: 20px; padding: 20px;
} }
.petitcours-raw {
padding:20px;
background:#fff;
}
/* Messages */
.messages .alert .close {
top:0;
right:0;
}
.messages .alert {
padding:10px 15px;
margin:0;
border:0;
border-radius:0;
}
div.messages div.alert-info {
background-color: #659C94;
}
div.messages div.alert-success {
background-color: #41C342;
}
div.messages div.alert-warning {
background-color: #efa50f;
}
div.messages div.alert-error {
background-color: #e14b4b;
}
div.messages div.alert-info div.container,
div.messages div.alert-error div.container,
div.messages div.alert-warning div.container,
div.messages div.alert-success div.container {
color: white;
}
div.messages div.alert div.container a {
color: inherit;
}

View file

@ -1,29 +0,0 @@
{% load utils %}
<ul>
{% if members %}
<li class="autocomplete-header">Membres du COF</li>
{% for member in members %}{% if forloop.counter < 5 %}
<li class="autocomplete-value"><a href="{% url 'user-registration' member.profile.user.username %}">{{ member.profile.user|highlight_user:q }}</a></li>
{% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %}
{% endif %}
{% if users %}
<li class="autocomplete-header">Utilisateurs de GestioCOF</li>
{% for user in users %}{% if forloop.counter < 5 %}
<li class="autocomplete-value"><a href="{% url 'user-registration' user.username %}">{{ user|highlight_user:q }}</a></li>
{% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %}
{% endif %}
{% if clippers %}
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
{% for clipper in clippers %}{% if forloop.counter < 5 %}
<li class="autocomplete-value"><a href="{% url 'clipper-registration' clipper.username %}">{{ clipper|highlight_clipper:q }}</a></li>
{% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %}
{% endif %}
{% if not options %}
<li class="autocomplete-header">Aucune correspondance trouvée</li>
{% else %}
<li class="autocomplete-header">Pas dans la liste ?</li>
{% endif %}
<li><a href="{% url 'empty-registration' %}">Créer un compte</a></li>
</ul>

View file

@ -4,12 +4,18 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr"> <html xmlns="http://www.w3.org/1999/xhtml" lang="fr">
<head> <head>
<title>{{ site.name }}</title> <title>{{ site.name }}</title>
<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">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
{# 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">
{# 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 %} {% block extra_head %}{% endblock %}
</head> </head>
<body> <body>

View file

@ -1,4 +1,4 @@
{% extends "base_header.html" %} {% extends "cof/base_header.html" %}
{% block interm_content %} {% block interm_content %}
<div class="container hidden-xs espace"></div> <div class="container hidden-xs espace"></div>

View file

@ -0,0 +1,57 @@
{% load utils %}
<ul>
{% if members %}
<li class="autocomplete-header">Membres du COF</li>
{% for member in members %}
{% if forloop.counter < 5 %}
<li class="autocomplete-value">
<a href="{% url 'user-registration' member.username %}">
{{ member|highlight_user:q }}
</a>
</li>
{% elif forloop.counter == 5 %}
<li class="autocomplete-more">...</li>
{% endif %}
{% endfor %}
{% endif %}
{% if users %}
<li class="autocomplete-header">Utilisateurs de GestioCOF</li>
{% for user in users %}
{% if forloop.counter < 5 %}
<li class="autocomplete-value">
<a href="{% url 'user-registration' user.username %}">
{{ user|highlight_user:q }}
</a>
</li>
{% elif forloop.counter == 5 %}
<li class="autocomplete-more">...</li>
{% endif %}
{% endfor %}
{% endif %}
{% if clippers %}
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
{% for clipper in clippers %}
{% if forloop.counter < 5 %}
<li class="autocomplete-value">
<a href="{% url 'clipper-registration' clipper.clipper clipper.fullname %}">
{{ clipper|highlight_clipper:q }}
</a>
</li>
{% elif forloop.counter == 5 %}
<li class="autocomplete-more">...</li>
{% endif %}
{% endfor %}
{% endif %}
{% if not options %}
<li class="autocomplete-header">Aucune correspondance trouvée</li>
{% else %}
<li class="autocomplete-header">Pas dans la liste ?</li>
{% endif %}
<li><a href="{% url 'empty-registration' %}">Créer un compte</a></li>
</ul>

View file

@ -11,7 +11,9 @@
</a> </a>
<div class="secondary"> <div class="secondary">
<span class="hidden-xxs">&nbsp;&nbsp;|&nbsp; </span> <span class="hidden-xxs">&nbsp;&nbsp;|&nbsp; </span>
<span><a href="{% url "cof.views.logout" %}">Se déconnecter&nbsp;<span class="glyphicon glyphicon-log-out"></span></a></span> <span><a href="{% url "gestion:logout" %}">
Se déconnecter&nbsp;<span class="glyphicon glyphicon-log-out"></span>
</a></span>
</div> </div>
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2> <h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
</div><!-- /.container --> </div><!-- /.container -->

View file

@ -5,14 +5,6 @@
<h2>Calendrier dynamique</h2> <h2>Calendrier dynamique</h2>
{% if success %}
<p class="success">Calendrier mis à jour avec succès</p>
{% endif %}
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<p>Ce formulaire vous permet de définir un calendrier dynamique compatible avec <p>Ce formulaire vous permet de définir un calendrier dynamique compatible avec
n'importe quel logiciel ou application d'agenda. Vous pouvez choisir de n'importe quel logiciel ou application d'agenda. Vous pouvez choisir de
souscrire aux événements du COF et/ou aux spectacles BdA. souscrire aux événements du COF et/ou aux spectacles BdA.

View file

@ -7,6 +7,6 @@
{% if member.profile.is_cof %}Il est désormais membre du COF n°{{ member.profile.num }} !{% endif %}</p> {% if member.profile.is_cof %}Il est désormais membre du COF n°{{ member.profile.num }} !{% endif %}</p>
{% endif %} {% endif %}
<div id="form-placeholder"> <div id="form-placeholder">
{% include "registration_form.html" %} {% include "cof/registration_form.html" %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -30,10 +30,16 @@
<h4>Mails pour les membres proposés :</h4> <h4>Mails pour les membres proposés :</h4>
{% for proposeduser, mail in proposed_mails %} {% for proposeduser, mail in proposed_mails %}
<h5>Pour {{ proposeduser }}:</h5> <h5>Pour {{ proposeduser }}:</h5>
<pre>{{ mail }}</pre> {% with object=mail.0 content=mail.1 %}
<pre>{{ object }}</pre>
<pre>{{ content }}</pre>
{% endwith %}
{% endfor %} {% endfor %}
<h4>Mail pour l'auteur de la demande :</h4> <h4>Mail pour l'auteur de la demande :</h4>
<pre style="margin-top: 15px;">{{ mainmail|safe }}</pre> {% with object=mainmail.0 content=mainmail.1 %}
<pre style="margin-top: 15px;">{{ object }}</pre>
<pre style="margin-top: 15px;">{{ content|safe }}</pre>
{% endwith %}
<input type="hidden" name="attribdata" value="{{ attribdata }}" /> <input type="hidden" name="attribdata" value="{{ attribdata }}" />
{% if redo %}<input type="hidden" name="redo" value="1" />{% endif %} {% if redo %}<input type="hidden" name="redo" value="1" />{% endif %}
<input class="btn btn-primary pull-right" type="submit" value="Valider le {% if redo %}re{% endif %}traitement de la demande" /> <input class="btn btn-primary pull-right" type="submit" value="Valider le {% if redo %}re{% endif %}traitement de la demande" />

View file

@ -16,7 +16,6 @@
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
<script src="{% static "js/jquery.min.js" %}" type="text/javascript"></script>
{% if proposals %} {% if proposals %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}

View file

@ -1,11 +1,19 @@
{% extends "base.html" %}
{% load bootstrap %}
{% block content %}
<div class="petitcours-raw">
{% if success %} {% if success %}
<p class="success">Votre demande a été enregistrée avec succès !</p> <p class="success">Votre demande a été enregistrée avec succès !</p>
{% else %} {% else %}
<form id="demandecours" method="post" action="{% url "cof.petits_cours_views.demande_raw" %}"> <form id="demandecours" method="post" action="{% url "cof.petits_cours_views.demande_raw" %}">
{% csrf_token %} {% csrf_token %}
<table> <table>
{{ form.as_table }} {{ form | bootstrap }}
</table> </table>
<input type="submit" class="btn-submit" value="Enregistrer" /> <input type="submit" class="btn-submit" value="Enregistrer" />
</form> </form>
{% endif %} {% endif %}
</div>
{% endblock %}

View file

@ -1,14 +0,0 @@
{% extends "base_title.html" %}
{% block realcontent %}
{% if error_type == "use_clipper_login" %}
<h2><strong>Votre identifiant est lié à un compte <tt>clipper</tt></strong></h2>
<p>Veuillez vous connecter à l'aide de votre <a href="{% url 'cas_login_view' %}">compte <tt>clipper</tt></a></p>
{% elif error_type == "no_password" %}
<h2><strong>Votre compte n'a pas de mot de passe associé</strong></h2>
<p>Veuillez <a href="mailto:cof@clipper.ens.fr">nous contacter</a> pour que nous en définissions un et que nous vous le transmettions !</p>
{% else %}
<h1><strong>{{ error_title }}</strong></h1>
<p>{{ error_description }}</p>
{% endif %}
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends "base_header.html" %} {% extends "cof/base_header.html" %}
{% block homelink %} {% block homelink %}
{% endblock %} {% endblock %}

View file

@ -2,11 +2,10 @@
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link href="{% static "grappelli/jquery/ui/css/custom-theme/jquery-ui-1.8.custom.css" %}" rel="stylesheet" type="text/css" media="screen" title="no title" charset="utf-8" /> <script src="{% static "js/jquery-ui.min.js" %}" type="text/javascript"></script>
<script src="{% static "grappelli/jquery/jquery-1.6.2.min.js" %}" type="text/javascript"></script> <script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<script src="{% static "grappelli/jquery/ui/js/jquery-ui-1.8.15.custom.min.js" %}" type="text/javascript"></script> <link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link href="{% static "grappelli/css/tools.css" %}" rel="stylesheet" type="text/css" />
<link href="{% static "grappelli/css/jquery-ui-grappelli-extensions.css" %}" rel="stylesheet" type="text/css" />
{% endblock %} {% endblock %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,17 +0,0 @@
Bonjour,
Je vous contacte au sujet de votre annonce passée sur le site du COF pour rentrer en contact avec un élève normalien pour des cours particuliers. Voici les coordonnées d'élèves qui sont motivés par de tels cours et correspondent aux critères que vous nous aviez transmis :
{% for matiere, proposed in proposals %}¤ {{ matiere }} :{% for user in proposed %}
¤ {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}
{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'élève disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.
{% endif %}Si pour une raison ou une autre ces numéros ne suffisaient pas, n'hésitez pas à répondre à cet e-mail et je vous en ferai parvenir d'autres sans problème.
{% if extra|length > 0 %}
{{ extra|safe }}
{% endif %}
Cordialement,
--
Le COF, BdE de l'ENS

View file

@ -1,28 +0,0 @@
Salut,
Le COF a reçu une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonnées, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les numéros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :
¤ Nom : {{ demande.name }}
¤ Période : {{ demande.quand }}
¤ Fréquence : {{ demande.freq }}
¤ Lieu (si préféré) : {{ demande.lieu }}
¤ Niveau : {{ demande.get_niveau_display }}
¤ Remarques diverses (désolé pour les balises HTML) : {{ demande.remarques }}
{% if matieres|length > 1 %}¤ Matières :
{% for matiere in matieres %} ¤ {{ matiere }}
{% endfor %}{% else %}¤ Matière : {% for matiere in matieres %}{{ matiere }}
{% endfor %}{% endif %}
Voilà, cette personne te contactera peut-être sous peu, tu pourras voir les détails directement avec elle (prix, modalités, ...). Pour indication, 30 Euro/h semble être la moyenne.
Si tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, ça serait cool que tu décoches la case "Recevoir des propositions de petits cours" sur GestioCOF. Ensuite dès que tu voudras réapparaître tu pourras recocher la case et tu seras à nouveau sur la liste.
À bientôt,
--
Le COF, pour les petits cours

View file

@ -4,7 +4,6 @@
{% block page_size %}col-sm-8{% endblock %} {% block page_size %}col-sm-8{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "js/jquery.min.js" %}" type="text/javascript"></script>
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}

View file

@ -43,7 +43,7 @@ def highlight_user(user, q):
@register.filter @register.filter
def highlight_clipper(clipper, q): def highlight_clipper(clipper, q):
if clipper.fullname: if clipper.fullname:
text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.username) text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.clipper)
else: else:
text = clipper.username text = clipper.clipper
return highlight_text(text, q) return highlight_text(text, q)

View file

@ -6,30 +6,20 @@ from cof.models import CofProfile
from gestion.tests import create_profile from gestion.tests import create_profile
def create_cofprofile(username): def create_cofprofile(username):
p = create_profile(username) p = create_profile(username)
return CofProfile.objects.create(profile=p) return CofProfile.objects.create(profile=p)
class TestCofProfile(TestCase): class TestCofProfile(TestCase):
def test_str(self): def test_str(self):
# creates a group of cof members
group = Group.objects.create(name='cof_members')
# create a specific permission for all COF members.
ct = ContentType.objects.get(app_label='cof', model='CofProfile')
perm = Permission.objects.create(name='Cof Member',
codename='member',
content_type=ct)
# bind the two mutherfucker.
group.permissions = [perm]
# now test it for real
cofp = create_cofprofile('foo') cofp = create_cofprofile('foo')
# XXX. should by default new CofProfiles be COF members? # XXX. should by default new CofProfiles be COF members?
self.assertFalse(cofp.profile.user.has_perm('cof.member')) self.assertFalse(cofp.profile.user.has_perm('cof.member'))
# adding/removing a user from the group should impact the # adding/removing a user from the group should impact the
# permission # permission
group = Group.objects.get(name='cof_members')
cofp.profile.user.groups.add(group) cofp.profile.user.groups.add(group)
cofp.save() cofp.save()

View file

@ -1,13 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .petits_cours_views import DemandeListView from .petits_cours_views import DemandeListView, DemandeDetailView
from .decorators import buro_required
from . import views, petits_cours_views from . import views, petits_cours_views
export_patterns = [ export_patterns = [
url(r'^members$', views.export_members), url(r'^members$', views.export_members),
url(r'^mega/avecremarques$', views.export_mega_remarksonly), url(r'^mega/avecremarques$', views.export_mega_remarksonly),
@ -24,10 +22,11 @@ petitcours_patterns = [
name='petits-cours-demande'), name='petits-cours-demande'),
url(r'^demande-raw$', petits_cours_views.demande_raw, url(r'^demande-raw$', petits_cours_views.demande_raw,
name='petits-cours-demande-raw'), name='petits-cours-demande-raw'),
url(r'^demandes$', DemandeListView.as_view(), url(r'^demandes$',
buro_required(DemandeListView.as_view()),
name='petits-cours-demandes-list'), name='petits-cours-demandes-list'),
url(r'^demandes/(?P<demande_id>\d+)$', url(r'^demandes/(?P<pk>\d+)$',
petits_cours_views.details, buro_required(DemandeDetailView.as_view()),
name='petits-cours-demande-details'), name='petits-cours-demande-details'),
url(r'^demandes/(?P<demande_id>\d+)/traitement$', url(r'^demandes/(?P<demande_id>\d+)/traitement$',
petits_cours_views.traitement, petits_cours_views.traitement,

View file

@ -1,21 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import unicodecsv import unicodecsv
import uuid import uuid
from datetime import timedelta from datetime import timedelta
from icalendar import Calendar, Event as Vevent from icalendar import Calendar, Event as Vevent
from custommail.shortcuts import send_custom_mail
from django.shortcuts import redirect, get_object_or_404, render from django.shortcuts import redirect, get_object_or_404, render
from django.http import Http404, HttpResponse, HttpResponseForbidden from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import login as django_login_view
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.utils import timezone from django.utils import timezone
from django.contrib import messages
import django.utils.six as six import django.utils.six as six
from .models import Survey, SurveyAnswer, SurveyQuestion, \ from .models import Survey, SurveyAnswer, SurveyQuestion, \
@ -24,8 +21,7 @@ from .models import Event, EventRegistration, EventOption, \
EventOptionChoice EventOptionChoice
from .models import EventCommentField, EventCommentValue, \ from .models import EventCommentField, EventCommentValue, \
CalendarSubscription CalendarSubscription
from .shared import send_custom_mail from .models import CofProfile, Club
from .models import CofProfile, Clipper, Club
from .decorators import buro_required, cof_required from .decorators import buro_required, cof_required
from .forms import ( from .forms import (
EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm,
@ -54,50 +50,6 @@ def home(request):
return render(request, "home.html", data) return render(request, "home.html", data)
def login(request):
if request.user.is_authenticated():
return redirect("cof.views.home")
context = {}
if request.method == "GET" and 'next' in request.GET:
context['next'] = request.GET['next']
return render(request, "login_switch.html", context)
def login_ext(request):
if request.method == "POST" and "username" in request.POST:
try:
user = User.objects.get(username=request.POST["username"])
if not user.has_usable_password() or user.password in ("", "!"):
profile, created = CofProfile.objects.get_or_create(user=user)
if profile.login_clipper:
return render(request, "error.html",
{"error_type": "use_clipper_login"})
else:
return render(request, "error.html",
{"error_type": "no_password"})
except User.DoesNotExist:
pass
context = {}
if request.method == "GET" and 'next' in request.GET:
context['next'] = request.GET['next']
if request.method == "POST" and 'next' in request.POST:
context['next'] = request.POST['next']
return django_login_view(request, template_name='login.html',
extra_context=context)
@login_required
def logout(request):
try:
profile = request.user.profile
except CofProfile.DoesNotExist:
profile, created = CofProfile.objects.get_or_create(user=request.user)
if profile.login_clipper:
return redirect("django_cas_ng.views.logout")
else:
return redirect("django.contrib.auth.views.logout")
@login_required @login_required
def survey(request, survey_id): def survey(request, survey_id):
survey = get_object_or_404(Survey, id=survey_id) survey = get_object_or_404(Survey, id=survey_id)
@ -156,10 +108,21 @@ def survey(request, survey_id):
except SurveyAnswer.DoesNotExist: except SurveyAnswer.DoesNotExist:
current_answer = None current_answer = None
form = SurveyForm(survey=survey) form = SurveyForm(survey=survey)
return render(request, "survey.html", {"survey": survey, "form": form, # Messages
"success": success, if success:
"deleted": deleted, if deleted:
"current_answer": current_answer}) messages.success(request,
"Votre réponse a bien été supprimée")
else:
messages.success(request,
"Votre réponse a bien été enregistrée ! Vous "
"pouvez cependant la modifier jusqu'à la fin "
"du sondage.")
return render(request, "cof/survey.html", {
"survey": survey,
"form": form,
"current_answer": current_answer
})
def get_event_form_choices(event, form): def get_event_form_choices(event, form):
@ -219,8 +182,13 @@ def event(request, event_id):
current_choices=current_registration.options) current_choices=current_registration.options)
except EventRegistration.DoesNotExist: except EventRegistration.DoesNotExist:
form = EventForm(event=event) form = EventForm(event=event)
return render(request, "event.html", # Messages
{"event": event, "form": form, "success": success}) if success:
messages.success(request, "Votre inscription a bien été enregistrée ! "
"Vous pouvez cependant la modifier jusqu'à "
"la fin des inscriptions.")
return render(request, "cof/event.html",
{"event": event, "form": form})
def clean_post_for_status(initial): def clean_post_for_status(initial):
@ -311,11 +279,11 @@ def registration_set_ro_fields(user_form, profile_form):
@buro_required @buro_required
def registration_form2(request, login_clipper=None, username=None): def registration_form2(request, login_clipper=None, username=None,
fullname=None):
events = Event.objects.filter(old=False).all() events = Event.objects.filter(old=False).all()
member = None member = None
if login_clipper: if login_clipper:
clipper = get_object_or_404(Clipper, username=login_clipper)
try: # check if the given user is already registered try: # check if the given user is already registered
member = User.objects.get(username=login_clipper) member = User.objects.get(username=login_clipper)
username = member.username username = member.username
@ -326,8 +294,8 @@ def registration_form2(request, login_clipper=None, username=None):
user_form = RegistrationUserForm(initial={ user_form = RegistrationUserForm(initial={
'username': login_clipper, 'username': login_clipper,
'email': "%s@clipper.ens.fr" % login_clipper}) 'email': "%s@clipper.ens.fr" % login_clipper})
if clipper.fullname: if fullname:
bits = clipper.fullname.split(" ") bits = fullname.split(" ")
user_form.fields['first_name'].initial = bits[0] user_form.fields['first_name'].initial = bits[0]
if len(bits) > 1: if len(bits) > 1:
user_form.fields['last_name'].initial = " ".join(bits[1:]) user_form.fields['last_name'].initial = " ".join(bits[1:])
@ -370,7 +338,7 @@ def registration_form2(request, login_clipper=None, username=None):
cofprofile_form = RegistrationCofProfileForm() cofprofile_form = RegistrationCofProfileForm()
event_formset = EventFormset(events=events, prefix='events') event_formset = EventFormset(events=events, prefix='events')
clubs_form = ClubsForm() clubs_form = ClubsForm()
return render(request, "registration_form.html", return render(request, "cof/registration_form.html",
{"member": member, "login_clipper": login_clipper, {"member": member, "login_clipper": login_clipper,
"user_form": user_form, "user_form": user_form,
"profile_form": profile_form, "profile_form": profile_form,
@ -409,11 +377,11 @@ def registration(request):
try: try:
member = User.objects.get(username=username) member = User.objects.get(username=username)
user_form = RegistrationUserForm(request_dict, instance=member) 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: except User.DoesNotExist:
try:
clipper = Clipper.objects.get(username=username)
login_clipper = clipper.username
except Clipper.DoesNotExist:
user_form.force_long_username() user_form.force_long_username()
else: else:
user_form.force_long_username() user_form.force_long_username()
@ -445,10 +413,12 @@ def registration(request):
)) ))
if forms_are_valid: if forms_are_valid:
# Enregistrement du profil # Enregistrement du profil
profile_form.save() profile = profile_form.save()
cofprofile = cofprofile_form.save() if profile.is_cof and not was_cof:
if cofprofile.is_cof and not was_cof: send_custom_mail(
send_custom_mail(member, "bienvenue") "welcome", "cof@ens.fr", [member.email],
context={'member': member},
)
# Enregistrement des inscriptions aux événements # Enregistrement des inscriptions aux événements
for form in event_formset: for form in event_formset:
if 'status' not in form.cleaned_data: if 'status' not in form.cleaned_data:
@ -471,26 +441,37 @@ def registration(request):
current_registration.paid = \ current_registration.paid = \
(form.cleaned_data['status'] == 'paid') (form.cleaned_data['status'] == 'paid')
current_registration.save() current_registration.save()
if form.event.title == "Mega 15" and created_reg: # if form.event.title == "Mega 15" and created_reg:
field = EventCommentField.objects.get( # field = EventCommentField.objects.get(
event=form.event, name="Commentaires") # event=form.event, name="Commentaires")
try: # try:
comments = EventCommentValue.objects.get( # comments = EventCommentValue.objects.get(
commentfield=field, # commentfield=field,
registration=current_registration).content # registration=current_registration).content
except EventCommentValue.DoesNotExist: # except EventCommentValue.DoesNotExist:
comments = field.default # comments = field.default
send_custom_mail(member, "mega", # FIXME : il faut faire quelque chose de propre ici,
{"remarques": comments}) # par exemple écrire un mail générique pour
# l'inscription aux événements et/ou donner la
# possibilité d'associer un mail aux événements
# send_custom_mail(...)
# Enregistrement des inscriptions aux clubs # Enregistrement des inscriptions aux clubs
member.clubs.clear() member.clubs.clear()
for club in clubs_form.cleaned_data['clubs']: for club in clubs_form.cleaned_data['clubs']:
club.membres.add(member) club.membres.add(member)
club.save() club.save()
success = True success = True
return render(request, "registration_post.html", # Messages
{"success": success, if success:
"user_form": user_form, 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')
return render(request, "cof/registration_post.html",
{"user_form": user_form,
"profile_form": profile_form, "profile_form": profile_form,
"cofprofile_form": cofprofile_form, "cofprofile_form": cofprofile_form,
"member": member, "member": member,
@ -551,7 +532,7 @@ def export_members(request):
response['Content-Disposition'] = 'attachment; filename=membres_cof.csv' response['Content-Disposition'] = 'attachment; filename=membres_cof.csv'
writer = unicodecsv.writer(response) writer = unicodecsv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all(): for profile in CofProfile.objects.filter(profile__user__groups__name='cof_members').all():
user = profile.user user = profile.user
bits = [profile.num, user.username, user.first_name, user.last_name, bits = [profile.num, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation, user.email, profile.phone, profile.occupation,
@ -660,7 +641,7 @@ def utile_bda(request):
@buro_required @buro_required
def liste_bdadiff(request): def liste_bdadiff(request):
titre = "BdA diffusion" titre = "BdA diffusion"
personnes = CofProfile.objects.filter(mailing_bda=True, is_cof=True).all() personnes = CofProfile.objects.filter(mailing_bda=True, profile__user__groups__name='cof_members').all()
return render(request, "liste_mails.html", return render(request, "liste_mails.html",
{"titre": titre, "personnes": personnes}) {"titre": titre, "personnes": personnes})
@ -669,7 +650,7 @@ def liste_bdadiff(request):
def liste_bdarevente(request): def liste_bdarevente(request):
titre = "BdA revente" titre = "BdA revente"
personnes = CofProfile.objects.filter(mailing_bda_revente=True, personnes = CofProfile.objects.filter(mailing_bda_revente=True,
is_cof=True).all() profile__user__groups__name='cof_members').all()
return render(request, "liste_mails.html", {"titre": titre, return render(request, "liste_mails.html", {"titre": titre,
"personnes": personnes}) "personnes": personnes})
@ -677,7 +658,7 @@ def liste_bdarevente(request):
@buro_required @buro_required
def liste_diffcof(request): def liste_diffcof(request):
titre = "Diffusion COF" titre = "Diffusion COF"
personnes = CofProfile.objects.filter(mailing_cof=True, is_cof=True).all() personnes = CofProfile.objects.filter(mailing_cof=True, user__groups__name='cof_members').all()
return render(request, "liste_mails.html", {"titre": titre, return render(request, "liste_mails.html", {"titre": titre,
"personnes": personnes}) "personnes": personnes})
@ -697,13 +678,15 @@ def calendar(request):
subscription.token = uuid.uuid4() subscription.token = uuid.uuid4()
subscription.save() subscription.save()
form.save_m2m() form.save_m2m()
messages.success(request,
"Calendrier mis à jour avec succès.")
return render(request, "cof/calendar_subscription.html", return render(request, "cof/calendar_subscription.html",
{'form': form, {'form': form,
'success': True,
'token': str(subscription.token)}) 'token': str(subscription.token)})
else: else:
messages.error(request, "Formulaire incorrect.")
return render(request, "cof/calendar_subscription.html", return render(request, "cof/calendar_subscription.html",
{'form': form, 'error': "Formulaire incorrect"}) {'form': form})
else: else:
return render(request, "cof/calendar_subscription.html", return render(request, "cof/calendar_subscription.html",
{'form': CalendarForm(instance=instance), {'form': CalendarForm(instance=instance),

View file

@ -9,10 +9,6 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/ https://docs.djangoproject.com/en/1.8/ref/settings/
""" """
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
@ -37,7 +33,6 @@ INSTALLED_APPS = (
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'grappelli',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.admindocs', 'django.contrib.admindocs',
'autocomplete_light', 'autocomplete_light',
@ -52,6 +47,7 @@ INSTALLED_APPS = (
'cof', 'cof',
'gestion', 'gestion',
'kfet', 'kfet',
'custommail',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
@ -83,7 +79,7 @@ TEMPLATES = [
'django.core.context_processors.i18n', 'django.core.context_processors.i18n',
'django.core.context_processors.media', 'django.core.context_processors.media',
'django.core.context_processors.static', 'django.core.context_processors.static',
'cof.shared.context_processor', 'gestion.context_processors.context_processor',
'kfet.context_processors.auth', 'kfet.context_processors.auth',
], ],
}, },
@ -133,9 +129,6 @@ MEDIA_URL = '/media/'
# Various additional settings # Various additional settings
SITE_ID = 1 SITE_ID = 1
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
MAIL_DATA = { MAIL_DATA = {
'petits_cours': { 'petits_cours': {
'FROM': "Le COF <cof@ens.fr>", 'FROM': "Le COF <cof@ens.fr>",
@ -149,7 +142,7 @@ MAIL_DATA = {
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'}, 'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'},
} }
LOGIN_URL = "cof-login" LOGIN_URL = "gestion:login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
@ -158,10 +151,12 @@ CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'cof.shared.COFCASBackend', 'gestion.backends.COFCASBackend',
'kfet.backends.GenericTeamBackend', 'kfet.backends.GenericTeamBackend',
) )
# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636'
# EMAIL_HOST="nef.ens.fr" # EMAIL_HOST="nef.ens.fr"
RECAPTCHA_PUBLIC_KEY = "DUMMY" RECAPTCHA_PUBLIC_KEY = "DUMMY"

View file

@ -4,26 +4,22 @@
Fichier principal de configuration des urls du projet GestioCOF Fichier principal de configuration des urls du projet GestioCOF
""" """
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light import autocomplete_light
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.views.generic.base import TemplateView
from django.contrib.auth import views as django_views from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views
from cof import views as cof_views, csv_views from cof import views as cof_views
from cof.urls import export_patterns, petitcours_patterns, \ from cof.urls import export_patterns, petitcours_patterns, \
surveys_patterns, events_patterns, calendar_patterns, \ surveys_patterns, events_patterns, calendar_patterns, \
clubs_patterns clubs_patterns
from cof.autocomplete import autocomplete from cof.autocomplete import autocomplete
from gestion import views as gestion_views
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
admin.autodiscover() admin.autodiscover()
@ -32,6 +28,10 @@ urlpatterns = [
url(r'^$', cof_views.home, name='home'), url(r'^$', cof_views.home, name='home'),
# The common views # The common views
url(r"^", include("gestion.urls", namespace='gestion')), url(r"^", include("gestion.urls", namespace='gestion')),
# Admin urls
url(r'^admin/logout/', gestion_views.logout),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)),
# Le BdA # Le BdA
url(r'^bda/', include('bda.urls')), url(r'^bda/', include('bda.urls')),
# Les exports # Les exports
@ -46,15 +46,6 @@ urlpatterns = [
url(r'^calendar/', include(calendar_patterns)), url(r'^calendar/', include(calendar_patterns)),
# Clubs # Clubs
url(r'^clubs/', include(clubs_patterns)), url(r'^clubs/', include(clubs_patterns)),
# Authentification
url(r'^cof/denied$', TemplateView.as_view(template_name='cof-denied.html'),
name="cof-denied"),
url(r'^cas/login$', django_cas_views.login, name="cas_login_view"),
url(r'^cas/logout$', django_cas_views.logout),
url(r'^outsider/login$', cof_views.login_ext),
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
url(r'^login$', cof_views.login, name="cof-login"),
url(r'^logout$', cof_views.logout),
# Infos persos # Infos persos
url(r'^outsider/password-change$', django_views.password_change), url(r'^outsider/password-change$', django_views.password_change),
url(r'^outsider/password-change-done$', url(r'^outsider/password-change-done$',
@ -71,14 +62,6 @@ urlpatterns = [
# Autocompletion # Autocompletion
url(r'^autocomplete/registration$', autocomplete), url(r'^autocomplete/registration$', autocomplete),
url(r'^autocomplete/', include('autocomplete_light.urls')), url(r'^autocomplete/', include('autocomplete_light.urls')),
# Interface admin
url(r'^admin/logout/', cof_views.logout),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/(?P<app_label>[\d\w]+)/(?P<model_name>[\d\w]+)/csv/',
csv_views.admin_list_export,
{'fields': ['username', ]}),
url(r'^admin/', include(admin.site.urls)),
url(r'^grappelli/', include('grappelli.urls')),
# Liens utiles du COF et du BdA # Liens utiles du COF et du BdA
url(r'^utile_cof$', cof_views.utile_cof), url(r'^utile_cof$', cof_views.utile_cof),
url(r'^utile_bda$', cof_views.utile_bda), url(r'^utile_bda$', cof_views.utile_bda),

View file

@ -1,3 +1,20 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
# Register your models here. from .models import Profile
class ProfileInline(admin.StackedInline):
model = Profile
inline_classes = ["collapse open"]
class UserProfileAdmin(UserAdmin):
inlines = [
ProfileInline,
]
admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)

57
gestion/backends.py Normal file
View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django_cas_ng.backends import CASBackend
from django_cas_ng.utils import get_cas_client
from django.contrib.auth import get_user_model
from gestion.models import Profile
User = get_user_model()
class COFCASBackend(CASBackend):
def authenticate_cas(self, ticket, service, request):
"""Verifies CAS ticket and gets or creates User object"""
client = get_cas_client(service_url=service)
username, attributes, _ = client.verify_ticket(ticket)
if attributes:
request.session['attributes'] = attributes
if not username:
return None
# Le CAS de l'ENS accepte les logins avec des espaces au début
# et à la fin, ainsi quavec une casse variable. On normalise pour
# éviter les doublons.
username = username.strip().lower()
profiles = Profile.objects.filter(login_clipper=username)
if len(profiles) > 0:
# XXX. We have to deal with multiple profiles, this should not
# happen
# profile = profiles.order_by('-is_cof')[0]
profile = profiles.first()
user = profile.user
return user
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
# user will have an "unusable" password
user = User.objects.create_user(username, '')
user.save()
return user
def authenticate(self, ticket, service, request):
"""Authenticates CAS ticket and retrieves user data"""
user = self.authenticate_cas(ticket, service, request)
if user is None:
return user
profile = user.profile
if not profile.login_clipper:
profile.login_clipper = user.username
profile.save()
if not user.email:
user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper
user.save()
return user

View file

@ -0,0 +1,10 @@
from django.contrib.sites.models import Site
def context_processor(request):
'''Append extra data to the context of the given request'''
data = {
"user": request.user,
"site": Site.objects.get_current(),
}
return data

View file

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
"""
Import des mails de GestioCOF dans la base de donnée
"""
import json
import os
from custommail.models import Type, CustomMail, Variable
from django.core.management.base import BaseCommand
from django.contrib.contenttypes.models import ContentType
class Command(BaseCommand):
help = ("Va chercher les données mails de GestioCOF stocké au format json "
"dans /gestion/management/data/custommails.json. Le format des "
"données est celui donné par la commande :"
" `python manage.py dumpdata custommail --natural-foreign` "
"La bonne façon de mettre à jour ce fichier est donc de le "
"charger à l'aide de syncmails, le faire les modifications à "
"l'aide de l'interface administration et/ou du shell puis de le "
"remplacer par le nouveau résultat de la commande précédente.")
def handle(self, *args, **options):
path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'data', 'custommail.json')
with open(path, 'r') as jsonfile:
mail_data = json.load(jsonfile)
# On se souvient à quel objet correspond quel pk du json
assoc = {'types': {}, 'mails': {}}
status = {'synced': 0, 'unchanged': 0}
for obj in mail_data:
fields = obj['fields']
# Pour les trois types d'objets :
# - On récupère les objets référencés par les clefs étrangères
# - On crée l'objet si nécessaire
# - On le stocke éventuellement dans les deux dictionnaires définis
# plus haut
# Variable types
if obj['model'] == 'custommail.variabletype':
fields['inner1'] = assoc['types'].get(fields['inner1'])
fields['inner2'] = assoc['types'].get(fields['inner2'])
if fields['kind'] == 'model':
fields['content_type'] = (
ContentType.objects
.get_by_natural_key(*fields['content_type'])
)
var_type, _ = Type.objects.get_or_create(**fields)
assoc['types'][obj['pk']] = var_type
# Custom mails
if obj['model'] == 'custommail.custommail':
mail = None
try:
mail = CustomMail.objects.get(
shortname=fields['shortname'])
status['unchanged'] += 1
except CustomMail.DoesNotExist:
mail = CustomMail.objects.create(**fields)
status['synced'] += 1
self.stdout.write(
'SYNCED {:s}'.format(fields['shortname']))
assoc['mails'][obj['pk']] = mail
# Variables
if obj['model'] == 'custommail.custommailvariable':
fields['custommail'] = assoc['mails'].get(fields['custommail'])
fields['type'] = assoc['types'].get(fields['type'])
try:
Variable.objects.get(
custommail=fields['custommail'],
name=fields['name']
)
except Variable.DoesNotExist:
Variable.objects.create(**fields)
# C'est agréable d'avoir le résultat affiché
self.stdout.write(
'{synced:d} mails synchronized {unchanged:d} unchanged'
.format(**status)
)

View file

@ -0,0 +1,587 @@
[
{
"model": "custommail.variabletype",
"pk": 1,
"fields": {
"content_type": [
"auth",
"user"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 2,
"fields": {
"content_type": null,
"inner1": null,
"kind": "int",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 3,
"fields": {
"content_type": [
"bda",
"spectacle"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 4,
"fields": {
"content_type": [
"bda",
"spectaclerevente"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 5,
"fields": {
"content_type": [
"sites",
"site"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 6,
"fields": {
"content_type": [
"cof",
"petitcoursdemande"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 7,
"fields": {
"content_type": null,
"inner1": null,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 8,
"fields": {
"content_type": null,
"inner1": 1,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 9,
"fields": {
"content_type": null,
"inner1": null,
"kind": "pair",
"inner2": 8
}
},
{
"model": "custommail.variabletype",
"pk": 10,
"fields": {
"content_type": null,
"inner1": 9,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 11,
"fields": {
"content_type": null,
"inner1": 3,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.custommail",
"pk": 1,
"fields": {
"shortname": "welcome",
"subject": "Bienvenue au COF",
"description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre",
"body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime."
}
},
{
"model": "custommail.custommail",
"pk": 2,
"fields": {
"shortname": "bda-rappel",
"subject": "{{ show }}",
"description": "Mail de rappel pour les spectacles BdA",
"body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 3,
"fields": {
"shortname": "bda-revente",
"subject": "{{ show }}",
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 4,
"fields": {
"shortname": "bda-shotgun",
"subject": "{{ show }}",
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 5,
"fields": {
"shortname": "bda-revente-winner",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente",
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 6,
"fields": {
"shortname": "bda-revente-loser",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Notification envoy\u00e9e aux perdants d'un tirage de revente.",
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 7,
"fields": {
"shortname": "bda-revente-seller",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e",
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 8,
"fields": {
"shortname": "bda-revente-new",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires.",
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 9,
"fields": {
"shortname": "bda-buy-shotgun",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun.",
"body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})"
}
},
{
"model": "custommail.custommail",
"pk": 10,
"fields": {
"shortname": "petit-cours-mail-eleve",
"subject": "Petits cours ENS par le COF",
"description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours",
"body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours"
}
},
{
"model": "custommail.custommail",
"pk": 11,
"fields": {
"shortname": "petits-cours-mail-demandeur",
"subject": "Cours particuliers ENS",
"description": "Mail envoy\u00e9 aux personnent qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})",
"body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS"
}
},
{
"model": "custommail.custommail",
"pk": 12,
"fields": {
"shortname": "bda-attributions",
"subject": "R\u00e9sultats du tirage au sort",
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places",
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 13,
"fields": {
"shortname": "bda-attributions-decus",
"subject": "R\u00e9sultats du tirage au sort",
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place",
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommailvariable",
"pk": 1,
"fields": {
"name": "member",
"description": "Utilisateur de GestioCOF",
"custommail": 1,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 2,
"fields": {
"name": "member",
"description": "Utilisateur ayant eu une place pour ce spectacle",
"custommail": 2,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 3,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 2,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 4,
"fields": {
"name": "nb_attr",
"description": "Nombre de places obtenues",
"custommail": 2,
"type": 2
}
},
{
"model": "custommail.custommailvariable",
"pk": 5,
"fields": {
"name": "revente",
"description": "Revente mentionn\u00e9e dans le mail",
"custommail": 3,
"type": 4
}
},
{
"model": "custommail.custommailvariable",
"pk": 6,
"fields": {
"name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place",
"custommail": 3,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 7,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 3,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 8,
"fields": {
"name": "site",
"description": "Site web (gestioCOF)",
"custommail": 3,
"type": 5
}
},
{
"model": "custommail.custommailvariable",
"pk": 9,
"fields": {
"name": "site",
"description": "Site web (gestioCOF)",
"custommail": 4,
"type": 5
}
},
{
"model": "custommail.custommailvariable",
"pk": 10,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 4,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 11,
"fields": {
"name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place",
"custommail": 4,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 12,
"fields": {
"name": "acheteur",
"description": "Gagnant-e du tirage",
"custommail": 5,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 13,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 5,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 14,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 5,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 15,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 6,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 16,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 6,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 17,
"fields": {
"name": "acheteur",
"description": "Personne inscrite au tirage qui n'a pas eu la place",
"custommail": 6,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 18,
"fields": {
"name": "acheteur",
"description": "Gagnant-e du tirage",
"custommail": 7,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 19,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 7,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 20,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 7,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 21,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 8,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 22,
"fields": {
"name": "vendeur",
"description": "Personne qui vend la place",
"custommail": 8,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 23,
"fields": {
"name": "revente",
"description": "Revente mentionn\u00e9e dans le mail",
"custommail": 8,
"type": 4
}
},
{
"model": "custommail.custommailvariable",
"pk": 24,
"fields": {
"name": "vendeur",
"description": "Personne qui vend la place",
"custommail": 9,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 25,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 9,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 26,
"fields": {
"name": "acheteur",
"description": "Personne qui prend la place au shotgun",
"custommail": 9,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 27,
"fields": {
"name": "demande",
"description": "Demande de petit cours",
"custommail": 10,
"type": 6
}
},
{
"model": "custommail.custommailvariable",
"pk": 28,
"fields": {
"name": "matieres",
"description": "Liste des mati\u00e8res concern\u00e9es par la demande",
"custommail": 10,
"type": 7
}
},
{
"model": "custommail.custommailvariable",
"pk": 29,
"fields": {
"name": "proposals",
"description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re",
"custommail": 11,
"type": 10
}
},
{
"model": "custommail.custommailvariable",
"pk": 30,
"fields": {
"name": "unsatisfied",
"description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer",
"custommail": 11,
"type": 7
}
},
{
"model": "custommail.custommailvariable",
"pk": 31,
"fields": {
"name": "places",
"description": "Places de spectacle du participant",
"custommail": 12,
"type": 11
}
},
{
"model": "custommail.custommailvariable",
"pk": 32,
"fields": {
"name": "member",
"description": "Participant du tirage au sort",
"custommail": 12,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 33,
"fields": {
"name": "member",
"description": "Participant du tirage au sort",
"custommail": 13,
"type": 1
}
}
]

27
gestion/shared.py Normal file
View file

@ -0,0 +1,27 @@
"""
Locking/unlocking tools to prevent tables to be corrupted
"""
from django.db import connection
def lock_table(*models):
query = "LOCK TABLES "
for i, model in enumerate(models):
table = model._meta.db_table
if i > 0:
query += ", "
query += "%s WRITE" % table
cursor = connection.cursor()
cursor.execute(query)
row = cursor.fetchone()
return row
def unlock_tables(*models):
cursor = connection.cursor()
cursor.execute("UNLOCK TABLES")
row = cursor.fetchone()
return row
unlock_table = unlock_tables

View file

@ -0,0 +1,20 @@
{% extends "base_title.html" %}
{% block realcontent %}
{% if error_type == "use_clipper_login" %}
<h2><strong>Votre identifiant est lié à un compte <tt>clipper</tt></strong></h2>
<p>
Veuillez vous connecter à l'aide de votre
<a href="{% url 'gestion:cas_login' %}">compte <tt>clipper</tt></a>
</p>
{% elif error_type == "no_password" %}
<h2><strong>Votre compte n'a pas de mot de passe associé</strong></h2>
<p>
Veuillez <a href="mailto:cof@clipper.ens.fr">nous contacter</a> pour que
nous en définissions un et que nous vous le transmettions !
</p>
{% else %}
<h1><strong>{{ error_title }}</strong></h1>
<p>{{ error_description }}</p>
{% endif %}
{% endblock %}

View file

@ -15,7 +15,7 @@
<p class="error">Identifiants incorrects.</p> <p class="error">Identifiants incorrects.</p>
{% endif %} {% endif %}
<form class="form-horizontal" method="post" <form class="form-horizontal" method="post"
action="{% url 'cof.views.login_ext' %}?next={{ next|urlencode }}"> action="{% url 'gestion:login_ext' %}?next={{ next|urlencode }}">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<input class="form-control" id="id_username" maxlength="254" name="username" type="text" placeholder="Nom d'utilisateur"> <input class="form-control" id="id_username" maxlength="254" name="username" type="text" placeholder="Nom d'utilisateur">

View file

@ -12,13 +12,13 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row" style="margin:0;"> <div class="row" style="margin:0;">
<a aria-label="Compte clipper" <a aria-label="Compte clipper"
href="{% url 'django_cas_ng.views.login' %}?next={{ next|urlencode }}"> href="{% url 'gestion:cas_login' %}?next={{ next|urlencode }}">
<div class="col-xs-12 col-sm-6" id="login_clipper"> <div class="col-xs-12 col-sm-6" id="login_clipper">
Compte clipper Compte clipper
</div> </div>
</a> </a>
<a aria-label="Extérieur" <a aria-label="Extérieur"
href="{% url 'cof.views.login_ext' %}?next={{ next|urlencode }}"> href="{% url 'gestion:login_ext' %}?next={{ next|urlencode }}">
<div class="col-xs-12 col-sm-6" id="login_outsider"> <div class="col-xs-12 col-sm-6" id="login_outsider">
Extérieur Extérieur
</div> </div>

View file

@ -18,7 +18,7 @@ def create_profile(username):
In order to create a profile, I need to create a User first. In order to create a profile, I need to create a User first.
This is annoying in most situations, as I just want a profile. This is annoying in most situations, as I just want a profile.
""" """
u = User.objects.create(username=username, User.objects.create(username=username,
first_name=username.title(), first_name=username.title(),
last_name=username.title()) last_name=username.title())
p, = Profile.objects.filter(user__username=username) p, = Profile.objects.filter(user__username=username)
@ -55,9 +55,9 @@ class SimpleTest(TestCase):
# each profile is linked to at most one account of the same type. # each profile is linked to at most one account of the same type.
p1 = create_profile('bar') p1 = create_profile('bar')
p2 = create_profile('baz') p2 = create_profile('baz')
pkfet1 = Account.objects.create(profile=p1, trigramme='BAR') Account.objects.create(profile=p1, trigramme='BAR')
pcof2 = CofProfile.objects.create(profile=p2) CofProfile.objects.create(profile=p2)
pkfet2 = Account.objects.create(profile=p2, trigramme='BAZ') Account.objects.create(profile=p2, trigramme='BAZ')
self.assertRaises(IntegrityError, self.assertRaises(IntegrityError,
CofProfile.objects.create, profile=p2) CofProfile.objects.create, profile=p2)
@ -65,8 +65,58 @@ class SimpleTest(TestCase):
class TestProfile(TestCase): class TestProfile(TestCase):
@patch('kfet.signals.messages') @patch('kfet.signals.messages')
def test_profile(self, mock_messages): def test_profile(self, mock_messages):
root = User.objects.create_superuser('root', 'foo@bar.com', 'root') # Test that the root user can log in
User.objects.create_superuser('root', 'foo@bar.com', 'root')
c = Client() c = Client()
c.login(username='root', password='root') c.login(username='root', password='root')
response = c.get('/') response = c.get('/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Test the profile edition
post_data = {
"first_name": "foo",
"last_name": "bar",
"phone": "0123456789",
"departement": "baz"
}
c.post("/profile", post_data)
user = User.objects.get(username="root")
self.assertEqual(user.first_name, post_data['first_name'])
self.assertEqual(user.last_name, post_data["last_name"])
self.assertEqual(user.profile.phone, post_data["phone"])
self.assertEqual(user.profile.departement, post_data["departement"])
class AuthTest(TestCase):
def test_login(self):
client = Client()
# Setup a regular Django user
user = User.objects.create(username="foo")
user.set_password("bar")
user.save()
# Test the django auth
resp = client.post(
"/outsider/login",
{"username": "foo", "password": "bar"},
follow=True
)
self.assertEqual(resp.status_code, 200)
resp = client.get("/logout")
self.assertEqual(resp.status_code, 200)
# Give the user a clipper login
user.profile.login_clipper = "foo"
user.profile.save()
# Assert that it cannot use the regular auth
resp = client.post(
"/outsider/login",
{"username": "foo", "password": "bar"}
)
self.assertIn("error_type", resp.context)
self.assertEqual(resp.context["error_type"], "use_clipper_login")
# Test the CAS redirect
resp = client.get("/cas/login")
self.assertEqual(resp.status_code, 302)
self.assertEqual(
resp.url.split('?')[0],
"https://cas.eleves.ens.fr/login"
)

View file

@ -1,8 +1,24 @@
from django.conf.urls import url from django.conf.urls import url, include
from django.views.generic.base import TemplateView
from django.contrib.auth import views as django_views
from django.contrib import admin
from django_cas_ng import views as django_cas_views
from . import views from . import views
urlpatterns = [ urlpatterns = [
# Profile edition
url(r"^profile/?$", views.profile, name="profile"), url(r"^profile/?$", views.profile, name="profile"),
# Authentication
url(r'^cof/denied$',
TemplateView.as_view(template_name='cof-denied.html'),
name="denied"),
url(r'^cas/login$', django_cas_views.login, name="cas_login"),
url(r'^cas/logout$', django_cas_views.logout, name="cas_logout"),
url(r'^outsider/login$', views.login_ext, name="login_ext"),
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
url(r'^login$', views.login, name="login"),
url(r'^logout$', views.logout, name="logout"),
] ]

View file

@ -1,9 +1,59 @@
from django.shortcuts import render """
The common views of the different organisations.
- Authentication
- Profile edition
"""
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib.auth.views import (
login as django_login, logout as django_logout
)
from .forms import ProfileForm, UserForm from .forms import ProfileForm, UserForm
def login(request):
if request.user.is_authenticated():
return redirect("cof.views.home")
context = {}
# Fetch the next page from the request data
if request.method == "GET" and 'next' in request.GET:
context['next'] = request.GET['next']
return render(request, "gestion/login_switch.html", context)
def login_ext(request):
if request.method == "POST" and "username" in request.POST:
try:
user = User.objects.get(username=request.POST["username"])
if user.profile.login_clipper:
return render(request, "gestion/error.html",
{"error_type": "use_clipper_login"})
if not user.has_usable_password() or user.password in ("", "!"):
return render(request, "gestion/error.html",
{"error_type": "no_password"})
except User.DoesNotExist:
pass
context = {}
# Fetch the next page from the request data
if request.method == "GET" and 'next' in request.GET:
context['next'] = request.GET['next']
if request.method == "POST" and 'next' in request.POST:
context['next'] = request.POST['next']
return django_login(request, template_name='gestion/login.html',
extra_context=context)
@login_required
def logout(request):
if request.user.profile.login_clipper:
return redirect("gestion:cas_logout")
else:
return django_logout(request)
@login_required @login_required
def profile(request): def profile(request):
success = False success = False
@ -11,7 +61,7 @@ def profile(request):
if request.method == "POST": if request.method == "POST":
user_form = UserForm(request.POST, instance=user) user_form = UserForm(request.POST, instance=user)
profile_form = ProfileForm(request.POST, instance=user.profile) profile_form = ProfileForm(request.POST, instance=user.profile)
if all((user_form.is_valid(), profile_form.is_valid())): if (user_form.is_valid() and profile_form.is_valid()):
user_form.save() user_form.save()
profile_form.save() profile_form.save()
success = True success = True

View file

@ -1,21 +1,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, from ldap3 import Connection
print_function, unicode_literals)
from builtins import *
from django.shortcuts import render from django.shortcuts import render
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
from cof.models import User, Clipper from django.conf import settings
from django.contrib.auth.models import User, Group
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import Account from kfet.models import Account
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@teamkfet_required @teamkfet_required
def account_create(request): def account_create(request):
if "q" not in request.GET: if "q" not in request.GET:
raise Http404 raise Http404
q = request.GET.get("q") q = request.GET.get("q")
cof_members = Group.objects.get(name="cof_members")
if (len(q) == 0): if (len(q) == 0):
return render(request, "kfet/account_create_autocomplete.html") return render(request, "kfet/account_create_autocomplete.html")
@ -25,58 +33,73 @@ def account_create(request):
queries = {} queries = {}
search_words = q.split() search_words = q.split()
# Fetching data from User and Account tables
queries['kfet'] = Account.objects queries['kfet'] = Account.objects
queries['users_cof'] = User.objects.filter(Q(profile__cof__is_cof = True)) queries['users_cof'] = User.objects.filter(groups=cof_members)
queries['users_notcof'] = User.objects.filter(Q(profile__cof__is_cof = False)) queries['users_notcof'] = User.objects.exclude(groups=cof_members)
queries['clippers'] = Clipper.objects
for word in search_words: for word in search_words:
queries['kfet'] = queries['kfet'].filter( queries['kfet'] = queries['kfet'].filter(
Q(profile__user__username__icontains = word) Q(profile__user__username__icontains=word)
| Q(profile__user__first_name__icontains = word) | Q(profile__user__first_name__icontains=word)
| Q(profile__user__last_name__icontains = word) | Q(profile__user__last_name__icontains=word)
) )
queries['users_cof'] = queries['users_cof'].filter( queries['users_cof'] = queries['users_cof'].filter(
Q(username__icontains = word) Q(username__icontains=word)
| Q(first_name__icontains = word) | Q(first_name__icontains=word)
| Q(last_name__icontains = word) | Q(last_name__icontains=word)
) )
queries['users_notcof'] = queries['users_notcof'].filter( queries['users_notcof'] = queries['users_notcof'].filter(
Q(username__icontains = word) Q(username__icontains=word)
| Q(first_name__icontains = word) | Q(first_name__icontains=word)
| Q(last_name__icontains = word) | Q(last_name__icontains=word)
)
queries['clippers'] = queries['clippers'].filter(
Q(username__icontains = word)
| Q(fullname__icontains = word)
) )
# Clearing redundancies
queries['kfet'] = queries['kfet'].distinct() queries['kfet'] = queries['kfet'].distinct()
usernames = set(
queries['kfet'].values_list('profile__user__username', flat=True)
)
queries['kfet'] = [
(account, account.profile.user)
for account in queries['kfet']
]
usernames = list( \ queries['users_cof'] = (
queries['kfet'].values_list('profile__user__username', flat=True))
queries['kfet'] = [ (account, account.profile.user) \
for account in queries['kfet'] ]
queries['users_cof'] = \
queries['users_cof'].exclude(username__in=usernames).distinct() queries['users_cof'].exclude(username__in=usernames).distinct()
queries['users_notcof'] = \ )
queries['users_notcof'] = (
queries['users_notcof'].exclude(username__in=usernames).distinct() queries['users_notcof'].exclude(username__in=usernames).distinct()
)
usernames |= set(
queries['users_cof'].values_list('username', flat=True)
)
usernames |= set(
queries['users_notcof'].values_list('username', flat=True)
)
usernames += list( \ # Fetching data from the SPI
queries['users_cof'].values_list('username', flat=True)) if hasattr(settings, 'LDAP_SERVER_URL'):
usernames += list( \ # Fetching
queries['users_notcof'].values_list('username', flat=True)) ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(bit=bit)
queries['clippers'] = \ for bit in search_words]
queries['clippers'].exclude(username__in=usernames).distinct() ))
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
]
# Resulting data
data.update(queries) data.update(queries)
data['options'] = any(queries.values())
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
return render(request, "kfet/account_create_autocomplete.html", data) return render(request, "kfet/account_create_autocomplete.html", data)

File diff suppressed because it is too large Load diff

View file

@ -1,98 +0,0 @@
[
{
"model": "auth.group",
"pk": 1,
"fields": {
"name": "K-F\u00eat chef",
"permissions": [
115,
116,
117,
118,
119,
120,
133,
134,
135,
130,
131,
132,
136,
137,
138,
121,
122,
123,
127,
128,
129,
124,
125,
126,
188,
189,
190,
169,
176,
183,
170,
171,
182,
172,
178,
177,
181,
175,
179,
173,
174,
184,
180,
139,
140,
141,
142,
143,
144,
166,
167,
168,
163,
164,
165,
151,
152,
153,
154,
155,
156,
185,
186,
187,
145,
146,
147,
148,
149,
150,
160,
161,
162,
157,
158,
159
]
}
},
{
"model": "auth.group",
"pk": 2,
"fields": {
"name": "K-F\u00eat girl",
"permissions": [
172,
173
]
}
}
]

View file

@ -0,0 +1,170 @@
"""
Crée des utilisateurs, des articles et des opérations aléatoires
"""
import os
import random
from datetime import timedelta
from decimal import Decimal
from django.utils import timezone
from django.contrib.auth.models import User, Group, Permission, ContentType
from cof.management.base import MyBaseCommand
from gestion.models import Profile
from kfet.models import Account, Article, OperationGroup, Operation, Checkout
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'data')
class Command(MyBaseCommand):
help = "Crée des utilisateurs, des articles et des opérations aléatoires"
def handle(self, *args, **options):
# ---
# Groupes
# ---
Group.objects.filter(name__icontains='K-Fêt').delete()
group_chef = Group(name="K-Fêt César")
group_boy = Group(name="K-Fêt Légionnaire")
group_chef.save()
group_boy.save()
permissions_chef = Permission.objects.filter(
content_type__in=ContentType.objects.filter(
app_label='kfet'))
permissions_boy = Permission.objects.filter(
codename__in=['is_team', 'perform_deposit'])
group_chef.permissions.add(*permissions_chef)
group_boy.permissions.add(*permissions_boy)
# ---
# Comptes
# ---
self.stdout.write("Création des comptes K-Fêt")
gaulois = Profile.objects.filter(user__last_name='Gaulois')
gaulois_trigramme = map('{:03d}'.format, range(50))
romains = Profile.objects.filter(user__last_name='Romain')
romains_trigramme = map(lambda x: str(100+x), range(99))
created_accounts = 0
team_accounts = 0
for (profile, trigramme) in zip(gaulois, gaulois_trigramme):
account, created = Account.objects.get_or_create(
trigramme=trigramme,
profile=profile,
defaults={'balance': random.randint(1, 999)/10}
)
created_accounts += int(created)
if profile.user.first_name == 'Abraracourcix':
profile.user.groups.add(group_chef)
for (profile, trigramme) in zip(romains, romains_trigramme):
account, created = Account.objects.get_or_create(
trigramme=trigramme,
profile=profile,
defaults={'balance': random.randint(1, 999)/10}
)
created_accounts += int(created)
if random.random() > 0.75 and created:
profile.user.groups.add(group_boy)
team_accounts += 1
self.stdout.write("- {:d} comptes créés, {:d} dans l'équipe K-Fêt"
.format(created_accounts, team_accounts))
# Compte liquide
liq_user, _ = User.objects.get_or_create(username='liquide')
liq_profile, _ = Profile.objects.get_or_create(user=liq_user)
liq_account, _ = Account.objects.get_or_create(profile=liq_profile,
trigramme='LIQ')
# ---
# Caisse
# ---
checkout, _ = Checkout.objects.get_or_create(
created_by=Account.objects.get(trigramme='000'),
name='Chaudron',
valid_from=timezone.now(),
valid_to=timezone.now() + timedelta(days=365)
)
# ---
# Opérations
# ---
self.stdout.write("Génération d'opérations")
articles = Article.objects.all()
accounts = Account.objects.exclude(trigramme='LIQ')
num_op = 100
# Operations are put uniformly over the span of a week
past_date = 3600*24*7
for i in range(num_op):
if random.random() > 0.25:
account = random.choice(accounts)
else:
account = liq_account
amount = Decimal('0')
at = timezone.now() - timedelta(
seconds=random.randint(0, past_date))
opegroup = OperationGroup(
on_acc=account,
checkout=checkout,
at=at,
is_cof=False
)
if hasattr(account.profile, "cof"):
opegroup.is_cof = account.profile.cof.is_cof
opegroup.save()
opegroup.save()
for j in range(random.randint(1, 4)):
typevar = random.random()
if typevar > 0.9 and account != liq_account:
ope = Operation(
group=opegroup,
type=Operation.DEPOSIT,
amount=Decimal(random.randint(1, 99)/10,)
)
elif typevar > 0.8 and account != liq_account:
ope = Operation(
group=opegroup,
type=Operation.WITHDRAW,
amount=-Decimal(random.randint(1, 99)/10,)
)
else:
article = random.choice(articles)
nb = random.randint(1, 5)
ope = Operation(
group=opegroup,
type=Operation.PURCHASE,
amount=-article.price*nb,
article=article,
article_nb=nb
)
ope.save()
amount += ope.amount
opegroup.amount = amount
opegroup.save()

View file

@ -36,7 +36,7 @@
<li class="user_category"><span class="text">Utilisateurs clipper</span></li> <li class="user_category"><span class="text">Utilisateurs clipper</span></li>
{% for clipper in clippers %} {% for clipper in clippers %}
<li> <li>
<a href="{% url "kfet.account.create.fromclipper" clipper.username %}"> <a href="{% url "kfet.account.create.fromclipper" clipper.clipper clipper.fullname%}">
{{ clipper|highlight_clipper:q }} {{ clipper|highlight_clipper:q }}
</a> </a>
</li> </li>

View file

@ -51,7 +51,7 @@
</li> </li>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="{% url 'cof.views.logout' %}?next=/k-fet/" title="Déconnexion"><span class="glyphicon glyphicon-log-out"></span></a></li> <li><a href="{% url 'gestion:logout' %}?next=/k-fet/" title="Déconnexion"><span class="glyphicon glyphicon-log-out"></span></a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View file

@ -35,7 +35,8 @@ urlpatterns = [
name = 'kfet.account.create_special'), name = 'kfet.account.create_special'),
url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax, url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax,
name = 'kfet.account.create.fromuser'), name = 'kfet.account.create.fromuser'),
url(r'^accounts/new/clipper/(?P<login_clipper>.+)$', views.account_create_ajax, url(r'^accounts/new/clipper/(?P<login_clipper>[\w-]+)/(?P<fullname>.*)$',
views.account_create_ajax,
name = 'kfet.account.create.fromclipper'), name = 'kfet.account.create.fromclipper'),
url(r'^accounts/new/empty$', views.account_create_ajax, url(r'^accounts/new/empty$', views.account_create_ajax,
name = 'kfet.account.create.empty'), name = 'kfet.account.create.empty'),

View file

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import timedelta
import hashlib import hashlib
@ -10,29 +7,30 @@ import heapq
import statistics import statistics
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied
from django.core.cache import cache from django.core.cache import cache
from django.views.generic import ListView, DetailView from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.models import User, Permission, Group from django.contrib.auth.models import User, Permission, Group
from django.http import HttpResponse, JsonResponse, Http404 from django.http import JsonResponse, Http404
from django.forms import modelformset_factory, formset_factory from django.forms import formset_factory
from django.db import IntegrityError, transaction from django.db import transaction
from django.db.models import F, Sum, Prefetch, Count, Func from django.db.models import F, Sum, Prefetch, Count
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from gestion.models import Profile from gestion.models import Profile
from cof.models import Clipper
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative, from kfet.models import (
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, Account, Checkout, Article, Settings, AccountNegative, CheckoutStatement,
InventoryArticle, Order, OrderArticle) GenericTeamToken, Supplier, SupplierArticle, Inventory, InventoryArticle,
Order, OrderArticle
)
from kfet.forms import * from kfet.forms import *
from kfet import consumers from kfet import consumers
import django_cas_ng import django_cas_ng
@ -223,19 +221,20 @@ def account_form_set_readonly_fields(user_form, cof_form):
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
cof_form.fields['is_cof'].widget.attrs['disabled'] = True cof_form.fields['is_cof'].widget.attrs['disabled'] = True
def get_account_create_forms(request=None, username=None, login_clipper=None): def get_account_create_forms(request=None, username=None, login_clipper=None,
fullname=None):
user = None user = None
clipper = None clipper = False
if login_clipper and (login_clipper == username or not username): if login_clipper and (login_clipper == username or not username):
# à partir d'un clipper # à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore exister # le user associé à ce clipper ne devrait pas encore exister
clipper = get_object_or_404(Clipper, username = login_clipper) clipper = True
try: try:
# Vérification que clipper ne soit pas déjà dans User # Vérification que clipper ne soit pas déjà dans User
user = User.objects.get(username=login_clipper) user = User.objects.get(username=login_clipper)
# Ici, on nous a menti, le user existe déjà # Ici, on nous a menti, le user existe déjà
username = user.username username = user.username
clipper = None clipper = False
except User.DoesNotExist: except User.DoesNotExist:
# Clipper (sans user déjà existant) # Clipper (sans user déjà existant)
@ -243,9 +242,9 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
user_initial = { user_initial = {
'username' : login_clipper, 'username' : login_clipper,
'email' : "%s@clipper.ens.fr" % login_clipper} 'email' : "%s@clipper.ens.fr" % login_clipper}
if clipper.fullname: if fullname:
# Prefill du nom et prénom # Prefill du nom et prénom
names = clipper.fullname.split() names = fullname.split()
# Le premier, c'est le prénom # Le premier, c'est le prénom
user_initial['first_name'] = names[0] user_initial['first_name'] = names[0]
if len(names) > 1: if len(names) > 1:
@ -309,8 +308,11 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
@login_required @login_required
@teamkfet_required @teamkfet_required
def account_create_ajax(request, username=None, login_clipper=None): def account_create_ajax(request, username=None, login_clipper=None,
forms = get_account_create_forms(request=None, username=username, login_clipper=login_clipper) fullname=None):
forms = get_account_create_forms(
request=None, username=username, login_clipper=login_clipper,
fullname=fullname)
return render(request, "kfet/account_create_form.html", { return render(request, "kfet/account_create_form.html", {
'account_form' : forms['account_form'], 'account_form' : forms['account_form'],
'cof_form' : forms['cof_form'], 'cof_form' : forms['cof_form'],

View file

@ -21,7 +21,7 @@ echo "mysql-server mysql-server/root_password_again password $DBPASSWD" | debcon
apt-get install -y mysql-server apt-get install -y mysql-server
mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'" mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'"
mysql -uroot -p$DBPASSWD -e "CREATE DATABASE test_$DBNAME; GRANT ALL PRIVILEGES ON test_$DBNAME.* TO '$DBUSER'@'localhost'" mysql -uroot -p$DBPASSWD -e "GRANT ALL PRIVILEGES ON test_$DBNAME.* TO '$DBUSER'@'localhost'"
# Installation et configuration d'Apache # Installation et configuration d'Apache
apt-get install -y apache2 apt-get install -y apache2

View file

@ -3,6 +3,7 @@
source ~/venv/bin/activate source ~/venv/bin/activate
python manage.py migrate python manage.py migrate
python manage.py loaddata gestion sites python manage.py loaddata gestion sites articles
python manage.py loaddevdata python manage.py loaddevdata
python manage.py syncmails
python manage.py collectstatic --noinput python manage.py collectstatic --noinput

View file

@ -17,4 +17,6 @@ asgi-redis==0.14.0
statistics==1.0.3.5 statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3
git+https://github.com/Aureplop/channels.git#egg=channels git+https://github.com/Aureplop/channels.git#egg=channels

View file

@ -1,34 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioCOF.settings")
from cof.models import Clipper
current = {}
print("[ FETCHING ]")
for clipper in Clipper.objects.all():
current[clipper.username] = clipper
print("[ SYNCING ]")
for line in sys.stdin:
bits = line.split(":")
username = bits[0]
fullname = bits[4]
if username in current:
clipper = current[username]
if clipper.fullname != fullname:
clipper.fullname = fullname
clipper.save()
print("Updated", username)
else:
clipper = Clipper(username=username, fullname=fullname)
clipper.save()
print("Created", username)
print("[ DONE ]")

View file

@ -1,2 +0,0 @@
#!/bin/sh
ssh cof@sas.eleves.ens.fr ypcat passwd | python sync_clipper.py