définition de l'application socle, modèle de donnée et page d'administration pour les développeurs
This commit is contained in:
parent
20c932e355
commit
5c97d6c0d0
|
@ -0,0 +1,17 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.flatpages.admin import FlatPageAdmin
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
from django.contrib.admin import site
|
||||
from tinymce.widgets import TinyMCE
|
||||
|
||||
class TinyMCEFlatPageAdmin(FlatPageAdmin):
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
if db_field.name == 'content':
|
||||
return db_field.formfield(widget=TinyMCE(
|
||||
attrs={'cols': 80, 'rows': 30},
|
||||
mce_attrs={'theme': 'advanced', 'external_link_list_url': reverse('tinymce.views.flatpages_link_list'), 'plugins': 'table,contextmenu'},
|
||||
))
|
||||
return super(TinyMCEFlatPageAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
site.unregister(FlatPage)
|
||||
site.register(FlatPage, TinyMCEFlatPageAdmin)
|
|
@ -0,0 +1,15 @@
|
|||
# vim:spell:spelllang=fr
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from settings import FormulaireParametres
|
||||
|
||||
class VueParametres(FormView):
|
||||
template_name = 'admin/parametres.html'
|
||||
form_class = FormulaireParametres
|
||||
success_url = '..'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save_form()
|
||||
return super(VueParametres, self).form_valid(form)
|
|
@ -0,0 +1,152 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import urllib2
|
||||
from urlparse import urljoin
|
||||
import ldap
|
||||
from ldap.filter import filter_format
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.conf import settings
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from models import (ProfilRecherche, ProfilRechercheTemporaire, ProfilOffre,
|
||||
ProfilAdmin, UtilisateurCAS)
|
||||
|
||||
class ProfilMoteurAuthentification(ModelBackend):
|
||||
'''Classe de base pour les moteurs d'authentification des profils.
|
||||
|
||||
Il renvoie des classes filles du modèle User et pas le modèle User lui
|
||||
même.
|
||||
'''
|
||||
def authenticate(self, email, password):
|
||||
try:
|
||||
user = self.profil_classe.objects.get(email=email)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
except self.profil_classe.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
return self.profil_classe.get(pk=user_id)
|
||||
except self.profil_classes.DoesNotExist:
|
||||
return None
|
||||
|
||||
class ProfilRechercheTemporaireAuthentification(ProfilMoteurAuthentification):
|
||||
profil_classe = ProfilRechercheTemporaire
|
||||
|
||||
class ProfilOffreMoteurAuthentification(ProfilMoteurAuthentification):
|
||||
profil_classe = ProfilOffre
|
||||
|
||||
def catch_ldap_error(function):
|
||||
'''Décorateur qui transforme n'importe quelle fonction de telle manière que
|
||||
si une exception du type LDAPError est levée, celle-ci est envoyé dans les
|
||||
logs.
|
||||
'''
|
||||
def f(*args, **kwargs):
|
||||
try:
|
||||
return function(*args, **kwargs)
|
||||
except ldap.LDAPError:
|
||||
logging.exception("Call to the LDAP server failed")
|
||||
return None
|
||||
return f
|
||||
|
||||
class ProfilRechercheAuthentification(ProfilMoteurAuthentification):
|
||||
'''Moteur d'authentification pour le profil recherche.
|
||||
|
||||
À partir d'un ticket CAS on vérifie sur le serveur CAS que
|
||||
l'authentification a réussie puis on vérifie que l'identifiant CAS
|
||||
correspond à un compte LDAP existant. Si un compte CAS existe déjà pour
|
||||
cet identifiant, on récupère l'email du LDAP est on les synchronise.
|
||||
Si aucun compte n'existe pour cet identifiant, on vérifie qu'il
|
||||
appartient aux groupes autorisés et on lui crée un compte.
|
||||
|
||||
Les paramètres à configurer dans le settings.py sont:
|
||||
|
||||
LDAP_URL: l'url du serveur LDAP
|
||||
LDAP_BIND_DN: le DN pour le bind administratif
|
||||
LDAP_BIND_PASSWORD: le mot de passe pour le bind administratif
|
||||
LDAP_BASE: l'arbre dans lequel faire les recherches
|
||||
'''
|
||||
profil_classe = ProfilRecherche
|
||||
_ldap_connection = None
|
||||
EDU_PERSON_AFFILIATION = 'eduPersonAffiliation'
|
||||
MAIL = 'mail'
|
||||
SUPANN_ALIAS_LOGIN = 'supannAliasLogin'
|
||||
STUDENT = 'student'
|
||||
|
||||
attributes = (MAIL, EDU_PERSON_AFFILIATION)
|
||||
|
||||
@property
|
||||
def connection_ldap(self):
|
||||
'''Retourne une connection authentifiée au serveur LDAP de l'UPD
|
||||
|
||||
Après le premier appel la connection est mise en cache.'''
|
||||
if self._ldap_connection is None:
|
||||
ldap_url = settings.LDAP_URL
|
||||
ldap_bind_dn = settings.LDAP_BIND_DN
|
||||
ldap_bind_password = settings.LDAP_BIND_PASSWORD
|
||||
self._ldap_connection = ldap.initialize(ldap_url)
|
||||
self._ldap_connection.simple_bind_s(ldap_bind_dn, ldap_bind_password)
|
||||
return self._ldap_connection
|
||||
|
||||
def recherche_ldap(self, _filter):
|
||||
'''Exécute une recherche LDAP sur le serveur LDAP de l'UPD'''
|
||||
ldap_base = settings.LDAP_BASE
|
||||
return self.connection_ldap.search_s(ldap_base, ldap.SCOPE_SUBTREE,
|
||||
_filter, self.attributes)
|
||||
|
||||
@catch_ldap_error
|
||||
def recherche_utilisateur(self, identifiant):
|
||||
'''Recherche une utilisateur sur le serveur LDAP de l'UPD'''
|
||||
return self.recherche_ldap(filter_format('(%s=%s)',
|
||||
(self.SUPANN_ALIAS_LOGIN, identifiant)))
|
||||
|
||||
def creation_automatique_du_compte(self, identifiant, cas_url, ldap_resultat):
|
||||
'''Si le compte LDAP pour l'identifiant CAS identifiant est un compte d'étudiant,
|
||||
on lui crée automatiquement un profil recherche.
|
||||
'''
|
||||
if len(ldap_resultat) and 'student' in \
|
||||
ldap_resultat[0][1][self.EDU_PERSON_AFFILIATION]:
|
||||
return self.profil_classe.new_cas_user(identifiant,
|
||||
ldap_resultat[0][1][self.EMAIL], cas_url)
|
||||
return None
|
||||
|
||||
@catch_ldap_error
|
||||
def authenticate(self, cas_ticket, service):
|
||||
'''Valide et authentifie un ticket CAS'''
|
||||
cas_url = settings.CAS_URL
|
||||
validation_url = '%s?%s'% (urljoin(cas_url, '/validate'),
|
||||
urlencode({ 'ticket': cas_ticket, 'service': service }))
|
||||
resultat, identifiant = urllib2.urlopen(validation_url).read().splitlines()
|
||||
if resultat == 'yes':
|
||||
ldap_resultat = self.recherche_utilisateur(identifiant)
|
||||
if not ldap_resultat:
|
||||
logging.error("CAS sent a username %s which is absent from LDAP",
|
||||
identifiant)
|
||||
return None
|
||||
try:
|
||||
utilisateur_cas = self.profil_classe.objects.get(utilisateur_cas__identifiant=identifiant)
|
||||
except UtilisateurCAS.DoesNotExist:
|
||||
utilisateur_cas = self.creation_automatique_du_compte(identifiant, cas_url, ldap_resultat)
|
||||
if utilisateur_cas is None:
|
||||
return None
|
||||
# synchronisation de l'email
|
||||
ldap_email = ldap_resultat[0][1].get(self.EMAIL)
|
||||
if ldap_email and ldap_email != utilisateur_cas.email:
|
||||
utilisateur_cas.email = ldap_email
|
||||
utilisateur_cas.save()
|
||||
return utilisateur_cas
|
||||
|
||||
class ProfilAdminAuthentification(ProfilMoteurAuthentification):
|
||||
profil_classe = ProfilAdmin
|
||||
|
||||
def authenticate(self, username, password):
|
||||
try:
|
||||
user = self.profil_classe.objects.get(username=username)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
return None
|
||||
except self.profil_classe.DoesNotExist:
|
||||
return None
|
|
@ -0,0 +1,2 @@
|
|||
[{"pk": 1, "model": "appli_socle.typeoffre", "fields": {"nom": "prix du march\u00e9"}},
|
||||
{"pk": 1, "model": "auth.user", "fields": {"username": "bdauvergne", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2012-03-30T17:14:30.492", "groups": [], "user_permissions": [], "password": "pbkdf2_sha256$10000$afpBXL5NjWRq$h6S33ykY6HIC2bCF5V/TeYzFz7tFCz1vcV7Xa8LVg/0=", "email": "bdauvergne@entrouvert.com", "date_joined": "2012-03-30T16:28:27.793"}}, {"pk": 2, "model": "auth.user", "fields": {"username": "test", "first_name": "", "last_name": "", "is_active": true, "is_superuser": false, "is_staff": false, "last_login": "2012-03-30T17:25:37.782", "groups": [], "user_permissions": [], "password": "pbkdf2_sha256$10000$eDkJuEKkb8nj$5aU8tuqvspLAujk8TjjeTMq06iWp9qlb2vGY1oHBU4Y=", "email": "", "date_joined": "2012-03-30T17:25:37.782"}}, {"pk": 2, "model": "appli_socle.profiloffre", "fields": {"accepte_notif_depublication": true, "acceptation_charte_qualite": false, "sans_validation": false, "groups": [], "user_permissions": [], "acceptation_cgu": false, "accepte_notif_alertes": false}}]
|
|
@ -0,0 +1,97 @@
|
|||
# vim:spell:spelllang=fr
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from django.forms import Form, ModelChoiceField, IntegerField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from models import Parametre, EmailModele
|
||||
|
||||
def dict_diff(a, b):
|
||||
'''Calcule la différence entre le dictionnaire a et le dictionnaire b'''
|
||||
c = {}
|
||||
for key in a:
|
||||
if key in b:
|
||||
if a[key] == b[key]:
|
||||
continue
|
||||
else:
|
||||
c[key] = u'valeur modifiée de %s à %s' % (a[key], b[key])
|
||||
else:
|
||||
c[key] = u'valeur supprimée %s' % a[key]
|
||||
for key in b:
|
||||
if key not in a:
|
||||
c[key] = u'valeur ajoutée %s' % b[key]
|
||||
return c
|
||||
|
||||
class FormulaireParametres(Form):
|
||||
duree_maximum_de_publication = IntegerField(label=_(u'Durée maximum de publication'),
|
||||
initial=3600*24*31*3, min_value=0)
|
||||
modele_depublication = ModelChoiceField(queryset=EmailModele.objects.all(),
|
||||
label=_(u"Modèle d'email pour les notifications de dépublication"),
|
||||
initial=None, required=False)
|
||||
modele_validation = ModelChoiceField(queryset=EmailModele.objects.all(),
|
||||
label=_(u"Modèle d'email pour les notifications de validation"),
|
||||
initial=None, required=False)
|
||||
modele_invalidation = ModelChoiceField(queryset=EmailModele.objects.all(),
|
||||
label=_(u"Modèle d'email pour les notifications d'invalidation"),
|
||||
initial=None, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['initial'] = Parametres().as_dict()
|
||||
super(FormulaireParametres, self).__init__(*args, **kwargs)
|
||||
|
||||
def save_form(self):
|
||||
p = Parametres()
|
||||
old_p = Parametres()
|
||||
for key, value in self.cleaned_data.iteritems():
|
||||
setattr(p, key, value)
|
||||
log = u'modification des paramètres %s' % dict_diff(old_p.as_dict(),
|
||||
p.as_dict())
|
||||
logging.info(log.encode('utf-8'))
|
||||
|
||||
class Parametres(object):
|
||||
'''Met en cache la valeur des paramètres et offre une interface d'objet
|
||||
Python vers ceux-ci.'''
|
||||
|
||||
def __init__(self, model_class=Parametre):
|
||||
self.model_class = Parametre
|
||||
self._cache = dict(((p.nom, p) for p in self.model_class.objects.all().select_related()))
|
||||
|
||||
def as_dict(self):
|
||||
return dict(((key, getattr(self, key)) for key in FormulaireParametres.base_fields))
|
||||
|
||||
def get_model_field_name(self, field):
|
||||
model = field.queryset.model
|
||||
for model_field in self.model_class._meta.fields:
|
||||
if hasattr(model_field, 'rel') and hasattr(model_field.rel, 'to'):
|
||||
if model is model_field.rel.to:
|
||||
break
|
||||
else:
|
||||
raise ValueError('There is no field in %s class to hold a value for form field %s',
|
||||
self.model_class, field)
|
||||
return model_field.name
|
||||
|
||||
def __getattr__(self, nom):
|
||||
field = FormulaireParametres.base_fields[nom]
|
||||
parametre = self._cache.get(nom)
|
||||
if parametre is None:
|
||||
return field.clean(field.initial)
|
||||
if isinstance(field, ModelChoiceField):
|
||||
model_field_name = self.get_model_field_name(field)
|
||||
return getattr(parametre, model_field_name)
|
||||
else:
|
||||
return parametre.valeur
|
||||
|
||||
def __setattr__(self, nom, valeur):
|
||||
if nom in ('_cache', 'model_class'):
|
||||
return super(Parametres, self).__setattr__(nom, valeur)
|
||||
field = FormulaireParametres.base_fields[nom]
|
||||
parametre, created = self.model_class.objects.get_or_create(nom=nom)
|
||||
if isinstance(field, ModelChoiceField):
|
||||
model_field_name = self.get_model_field_name(field)
|
||||
setattr(parametre, model_field_name, valeur)
|
||||
else:
|
||||
parametre.valeur = valeur
|
||||
parametre.save()
|
||||
self._cache[parametre.nom] = parametre
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url admin:index %}">{% trans 'Home' %}</a>
|
||||
› Paramètres
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" name="Sauver"/>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "appli_socle/base.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "appli_socle/base.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
{% extends "appli_socle/base.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
{% end block %}
|
|
@ -0,0 +1,4 @@
|
|||
<html>
|
||||
<head></head>
|
||||
<body>{% block body %}{% endblock %}</body>
|
||||
</html>
|
|
@ -0,0 +1,38 @@
|
|||
{% block extrastyle %}
|
||||
{% load static %}
|
||||
<style type="text/css">
|
||||
#{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; }
|
||||
#{{ id }}_map .aligned label { float:inherit; }
|
||||
#{{ id }}_admin_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; }
|
||||
{% if not display_wkt %}#{{ id }} { display: none; }{% endif %}
|
||||
.olControlEditingToolbar .olControlModifyFeatureItemActive {
|
||||
background-image: url("{% static "admin/img/gis/move_vertex_on.png" %}");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.olControlEditingToolbar .olControlModifyFeatureItemInactive {
|
||||
background-image: url("{% static "admin/img/gis/move_vertex_off.png" %}");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
</style>
|
||||
<!--[if IE]>
|
||||
<style type="text/css">
|
||||
/* This fixes the mouse offset issues in IE. */
|
||||
#{{ id }}_admin_map { position: static; vertical-align: top; }
|
||||
/* `font-size: 0` fixes the 1px border between tiles, but borks LayerSwitcher.
|
||||
Thus, this is disabled until a better fix is found.
|
||||
#{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; font-size: 0; } */
|
||||
</style>
|
||||
<![endif]-->
|
||||
{% endblock %}
|
||||
<span id="{{ id }}_admin_map">
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
{% block openlayers %}{% include "gis/admin/openlayers.js" %}{% endblock %}
|
||||
//]]>
|
||||
</script>
|
||||
<div id="{{ id }}_map"{% if LANGUAGE_BIDI %} dir="ltr"{% endif %}></div>
|
||||
<a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a>
|
||||
{% if display_wkt %}<p> WKT debugging window:</p>{% endif %}
|
||||
<textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea>
|
||||
<script type="text/javascript">{% block init_function %}{{ module }}.init();{% endblock %}</script>
|
||||
</span>
|
|
@ -0,0 +1,209 @@
|
|||
{% load l10n %}{# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #}
|
||||
OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.SphericalMercator.projectForward);
|
||||
{% block vars %}var {{ module }} = {};
|
||||
{{ module }}.more_wkt = "{{ more_wkt }}";
|
||||
{{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\\d+;(.+)", "i"); {{ module }}.layers = {};
|
||||
{{ module }}.modifiable = {{ modifiable|yesno:"true,false" }};
|
||||
{{ module }}.wkt_f = new OpenLayers.Format.WKT();
|
||||
{{ module }}.is_collection = {{ is_collection|yesno:"true,false" }};
|
||||
{{ module }}.collection_type = '{{ collection_type }}';
|
||||
{{ module }}.is_linestring = {{ is_linestring|yesno:"true,false" }};
|
||||
{{ module }}.is_polygon = {{ is_polygon|yesno:"true,false" }};
|
||||
{{ module }}.is_point = {{ is_point|yesno:"true,false" }};
|
||||
{% endblock %}
|
||||
{{ module }}.get_ewkt = function(feat){return 'SRID={{ srid }};' + {{ module }}.wkt_f.write(feat);}
|
||||
{{ module }}.read_wkt = function(wkt){
|
||||
// OpenLayers cannot handle EWKT -- we make sure to strip it out.
|
||||
// EWKT is only exposed to OL if there's a validation error in the admin.
|
||||
var match = {{ module }}.re.exec(wkt);
|
||||
if (match){wkt = match[1];}
|
||||
return {{ module }}.wkt_f.read(wkt);
|
||||
}
|
||||
{{ module }}.write_wkt = function(feat){
|
||||
if ({{ module }}.is_collection){ {{ module }}.num_geom = feat.geometry.components.length;}
|
||||
else { {{ module }}.num_geom = 1;}
|
||||
document.getElementById('{{ id }}').value = {{ module }}.get_ewkt(feat);
|
||||
}
|
||||
{{ module }}.add_wkt = function(event){
|
||||
// This function will sync the contents of the `vector` layer with the
|
||||
// WKT in the text field.
|
||||
if ({{ module }}.is_collection){
|
||||
var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}());
|
||||
for (var i = 0; i < {{ module }}.layers.vector.features.length; i++){
|
||||
feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]);
|
||||
}
|
||||
{{ module }}.write_wkt(feat);
|
||||
} else {
|
||||
// Make sure to remove any previously added features.
|
||||
if ({{ module }}.layers.vector.features.length > 1){
|
||||
old_feats = [{{ module }}.layers.vector.features[0]];
|
||||
{{ module }}.layers.vector.removeFeatures(old_feats);
|
||||
{{ module }}.layers.vector.destroyFeatures(old_feats);
|
||||
}
|
||||
{{ module }}.write_wkt(event.feature);
|
||||
}
|
||||
}
|
||||
{{ module }}.modify_wkt = function(event){
|
||||
if ({{ module }}.is_collection){
|
||||
if ({{ module }}.is_point){
|
||||
{{ module }}.add_wkt(event);
|
||||
return;
|
||||
} else {
|
||||
// When modifying the selected components are added to the
|
||||
// vector layer so we only increment to the `num_geom` value.
|
||||
var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}());
|
||||
for (var i = 0; i < {{ module }}.num_geom; i++){
|
||||
feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]);
|
||||
}
|
||||
{{ module }}.write_wkt(feat);
|
||||
}
|
||||
} else {
|
||||
{{ module }}.write_wkt(event.feature);
|
||||
}
|
||||
}
|
||||
// Function to clear vector features and purge wkt from div
|
||||
{{ module }}.deleteFeatures = function(){
|
||||
{{ module }}.layers.vector.removeFeatures({{ module }}.layers.vector.features);
|
||||
{{ module }}.layers.vector.destroyFeatures();
|
||||
}
|
||||
{{ module }}.clearFeatures = function (){
|
||||
{{ module }}.deleteFeatures();
|
||||
document.getElementById('{{ id }}').value = '';
|
||||
{% localize off %}
|
||||
{{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }});
|
||||
{% endlocalize %}
|
||||
}
|
||||
// Add Select control
|
||||
{{ module }}.addSelectControl = function(){
|
||||
var select = new OpenLayers.Control.SelectFeature({{ module }}.layers.vector, {'toggle' : true, 'clickout' : true});
|
||||
{{ module }}.map.addControl(select);
|
||||
select.activate();
|
||||
}
|
||||
{{ module }}.enableDrawing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate();}
|
||||
{{ module }}.enableEditing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate();}
|
||||
// Create an array of controls based on geometry type
|
||||
{{ module }}.getControls = function(lyr){
|
||||
{{ module }}.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'});
|
||||
var nav = new OpenLayers.Control.Navigation();
|
||||
var draw_ctl;
|
||||
if ({{ module }}.is_linestring){
|
||||
draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'});
|
||||
} else if ({{ module }}.is_polygon){
|
||||
draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'});
|
||||
} else if ({{ module }}.is_point){
|
||||
draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'});
|
||||
}
|
||||
if ({{ module }}.modifiable){
|
||||
var mod = new OpenLayers.Control.ModifyFeature(lyr, {'displayClass': 'olControlModifyFeature'});
|
||||
{{ module }}.controls = [nav, draw_ctl, mod];
|
||||
} else {
|
||||
if(!lyr.features.length){
|
||||
{{ module }}.controls = [nav, draw_ctl];
|
||||
} else {
|
||||
{{ module }}.controls = [nav];
|
||||
}
|
||||
}
|
||||
}
|
||||
{{ module }}.init = function(){
|
||||
{% block map_options %}// The options hash, w/ zoom, resolution, and projection settings.
|
||||
var options = {
|
||||
{% autoescape off %}{% for item in map_options.items %} '{{ item.0 }}' : {{ item.1 }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}{% endautoescape %} };{% endblock %}
|
||||
// The admin map for this geometry field.
|
||||
{{ module }}.map = new OpenLayers.Map('{{ id }}_map', options);
|
||||
// Base Layer
|
||||
{{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS( "{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'} );{% endblock %}
|
||||
{{ module }}.map.addLayer({{ module }}.layers.base);
|
||||
{% block extra_layers %}{% endblock %}
|
||||
{% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %}
|
||||
// >added for appli
|
||||
{{ module }}.layers.background = new OpenLayers.Layer.Vector("background");
|
||||
{{ module }}.layers.background.style = {
|
||||
'fillColor': '#999999',
|
||||
'fillOpacity': 0.2,
|
||||
'strokeColor': '#999999',
|
||||
'strokeOpacity': 0.8,
|
||||
};
|
||||
|
||||
{{ module }}.map.addLayer({{ module }}.layers.background);
|
||||
var styles = new OpenLayers.StyleMap({
|
||||
"default": {
|
||||
graphicName: "dot",
|
||||
pointRadius: 10,
|
||||
strokeColor: "fuchsia",
|
||||
strokeWidth: 2,
|
||||
fillColor: "lime",
|
||||
fillOpacity: 0.6
|
||||
},
|
||||
"select": {
|
||||
pointRadius: 20,
|
||||
fillOpacity: 1,
|
||||
rotation: 45
|
||||
}
|
||||
});
|
||||
// <added for appli
|
||||
{{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}", { styleMap: styles });
|
||||
{{ module }}.map.addLayer({{ module }}.layers.vector);
|
||||
// >added for appli
|
||||
{{ module }}.layers.vector.style = { graphicName: "cross", }
|
||||
// <added for appli
|
||||
// Read WKT from the text field.
|
||||
var wkt = document.getElementById('{{ id }}').value;
|
||||
if (wkt){
|
||||
// After reading into geometry, immediately write back to
|
||||
// WKT <textarea> as EWKT (so that SRID is included).
|
||||
var admin_geom = {{ module }}.read_wkt(wkt);
|
||||
{{ module }}.write_wkt(admin_geom);
|
||||
if ({{ module }}.is_collection){
|
||||
// If geometry collection, add each component individually so they may be
|
||||
// edited individually.
|
||||
for (var i = 0; i < {{ module }}.num_geom; i++){
|
||||
{{ module }}.layers.vector.addFeatures([new OpenLayers.Feature.Vector(admin_geom.geometry.components[i].clone())]);
|
||||
}
|
||||
} else {
|
||||
{{ module }}.layers.vector.addFeatures([admin_geom]);
|
||||
}
|
||||
// Zooming to the bounds.
|
||||
{{ module }}.map.zoomToExtent(admin_geom.geometry.getBounds());
|
||||
if ({{ module }}.is_point){
|
||||
{{ module }}.map.zoomTo({{ point_zoom }});
|
||||
}
|
||||
} else {
|
||||
{% localize off %}
|
||||
{{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }});
|
||||
{% endlocalize %}
|
||||
}
|
||||
// added for
|
||||
if ({{module}}.more_wkt) {
|
||||
var more_geom = {{ module }}.more_geom = {{ module }}.read_wkt({{ module }}.more_wkt);
|
||||
{{ module }}.layers.background.addFeatures([more_geom]);
|
||||
}
|
||||
|
||||
|
||||
// This allows editing of the geographic fields -- the modified WKT is
|
||||
// written back to the content field (as EWKT, so that the ORM will know
|
||||
// to transform back to original SRID).
|
||||
{{ module }}.layers.vector.events.on({"featuremodified" : {{ module }}.modify_wkt});
|
||||
{{ module }}.layers.vector.events.on({"featureadded" : {{ module }}.add_wkt});
|
||||
{% block controls %}
|
||||
// Map controls:
|
||||
// Add geometry specific panel of toolbar controls
|
||||
{{ module }}.getControls({{ module }}.layers.vector);
|
||||
{{ module }}.panel.addControls({{ module }}.controls);
|
||||
{{ module }}.map.addControl({{ module }}.panel);
|
||||
{{ module }}.addSelectControl();
|
||||
// Then add optional visual controls
|
||||
{% if mouse_position %}{{ module }}.map.addControl(new OpenLayers.Control.MousePosition());{% endif %}
|
||||
{% if scale_text %}{{ module }}.map.addControl(new OpenLayers.Control.Scale());{% endif %}
|
||||
{% if layerswitcher %}{{ module }}.map.addControl(new OpenLayers.Control.LayerSwitcher());{% endif %}
|
||||
// Then add optional behavior controls
|
||||
{% if not scrollable %}{{ module }}.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel();{% endif %}
|
||||
{% endblock %}
|
||||
if (wkt){
|
||||
if ({{ module }}.modifiable){
|
||||
{{ module }}.enableEditing();
|
||||
}
|
||||
} else {
|
||||
{{ module }}.enableDrawing();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
{% extends "gis/admin/openlayers.html" %}
|
||||
{% block openlayers %}{% include "gis/admin/osm.js" %}{% endblock %}
|
|
@ -0,0 +1,2 @@
|
|||
{% extends "gis/admin/openlayers.js" %}
|
||||
{% block base_layer %}new OpenLayers.Layer.OSM("OpenStreetMap (Mapnik)");{% endblock %}
|
|
@ -1,25 +1,28 @@
|
|||
# Django settings for sdldd_project project.
|
||||
# vim:spell:spelllang=fr
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
import os.path
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@example.com'),
|
||||
PROJECT_ROOT = os.path.join(os.path.dirname(__file__), '..')
|
||||
|
||||
# Applications Django utilisées
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
# Uncomment the next line to enable the admin:
|
||||
'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
'appli_project.appli_socle',
|
||||
'django.contrib.gis',
|
||||
'django.contrib.flatpages',
|
||||
'tinymce',
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': 'dauphine.db', # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
|
@ -27,11 +30,11 @@ DATABASES = {
|
|||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'America/Chicago'
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
|
@ -43,30 +46,6 @@ USE_I18N = True
|
|||
# calendars according to the current locale
|
||||
USE_L10N = True
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = ''
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# URL prefix for admin static files -- CSS, JavaScript and images.
|
||||
# Make sure to use a trailing slash.
|
||||
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = (
|
||||
# Put strings here, like "/home/html/static" or "C:/www/django/static".
|
||||
|
@ -82,6 +61,30 @@ STATICFILES_FINDERS = (
|
|||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media')
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# URL prefix for admin static files -- CSS, JavaScript and images.
|
||||
# Make sure to use a trailing slash.
|
||||
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||
ADMIN_MEDIA_PREFIX = '/static/grappelli/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'te9pqmis@-oz8#s2qj#$w!jz0kcmy3hbf3&7+!om-oxek^fiw$'
|
||||
|
||||
|
@ -98,53 +101,62 @@ MIDDLEWARE_CLASSES = (
|
|||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
# Django CMS
|
||||
# 'cms.middleware.multilingual.MultilingualURLMiddleware',
|
||||
# 'cms.middleware.page.CurrentPageMiddleware',
|
||||
# 'cms.middleware.user.CurrentUserMiddleware',
|
||||
# 'cms.middleware.toolbar.ToolbarMiddleware',
|
||||
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'sdldd_project.urls'
|
||||
ROOT_URLCONF = 'appli_project.urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
os.path.join(PROJECT_ROOT, "templates"),
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# Uncomment the next line to enable the admin:
|
||||
'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
'django_extensions',
|
||||
'sdldd_project.sdldd_socle',
|
||||
'sdldd_project.sdldd_offre',
|
||||
'sdldd_project.sdldd_recherche',
|
||||
'sdldd_project.sdldd_admin',
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.core.context_processors.i18n',
|
||||
'django.core.context_processors.request',
|
||||
'django.core.context_processors.media',
|
||||
'django.core.context_processors.static',
|
||||
# Django CMS
|
||||
# 'cms.context_processors.media',
|
||||
# 'sekizai.context_processors.sekizai',
|
||||
)
|
||||
|
||||
# A sample logging configuration. The only tangible logging
|
||||
# performed by this configuration is to send an email to
|
||||
# the site admins on every HTTP 500 error.
|
||||
# See http://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'django.request': {
|
||||
'handlers': ['mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': True,
|
||||
},
|
||||
# Django CMS
|
||||
#CMS_TEMPLATES = (
|
||||
# ('cms_template.html', u'Modèle de page par défaut'),
|
||||
#)
|
||||
#
|
||||
#LANGUAGES = (
|
||||
# ('fr', u'Français'),
|
||||
#)
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
}
|
||||
}
|
||||
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
|
||||
# Configuration du serveur LDAP pour l'authentification CAS
|
||||
LDAP_URL = 'ldaps://ldap.ent.dauphine.fr'
|
||||
LDAP_BIND_DN = 'uid=logement,ou=bindusers,dc=dauphine,dc=fr'
|
||||
LDAP_BIND_PASSWORD = 'FIXME'
|
||||
LDAP_BASE = 'dc=dauphine,dc=fr'
|
||||
|
||||
#
|
||||
CAS_URL = 'https://www.ent.dauphine.fr/cas/'
|
||||
|
||||
# Domaine utilisé par défaut pour créer des objets du type UtilisateurCAS, i.e.
|
||||
# le lien entre un utiisateur Django et un compte CAS/LDAP/ENT
|
||||
UTILISATEUR_CAS_DOMAINE_PAR_DEFAUT = 'dauphine'
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# Paramètre pour le développement de l'application Django
|
||||
|
||||
import os.path
|
||||
from commun import *
|
||||
|
||||
INSTALLED_APPS += ('debug_toolbar',)
|
||||
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@example.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.contrib.gis.db.backends.spatialite', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': os.path.join(PROJECT_ROOT, 'dev-dauphine.db'), # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
# A sample logging configuration. The only tangible logging
|
||||
# performed by this configuration is to send an email to
|
||||
# the site admins on every HTTP 500 error.
|
||||
# See http://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
}
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG',
|
||||
}
|
||||
}
|
||||
try:
|
||||
from local_settings_dev import *
|
||||
except ImportError:
|
||||
pass
|
|
@ -0,0 +1,76 @@
|
|||
{% load admin_static %}{% load url from future %}<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}" />
|
||||
{% block extrastyle %}{% endblock %}
|
||||
<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="{% block stylesheet_ie %}{% static "admin/css/ie.css" %}{% endblock %}" /><![endif]-->
|
||||
{% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %}
|
||||
<script type="text/javascript">window.__admin_media_prefix__ = "{% filter escapejs %}{% static "admin/" %}{% endfilter %}";</script>
|
||||
{% block extrahead %}{% endblock %}
|
||||
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
|
||||
</head>
|
||||
{% load i18n %}
|
||||
|
||||
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}">
|
||||
<!-- Container -->
|
||||
<div id="container">
|
||||
|
||||
{% if not is_popup %}
|
||||
<!-- Header -->
|
||||
<div id="header">
|
||||
<div id="branding">
|
||||
{% block branding %}{% endblock %}
|
||||
</div>
|
||||
{% if user.is_active and user.is_staff %}
|
||||
<div id="user-tools">
|
||||
{% trans 'Welcome,' %}
|
||||
<strong>{% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}</strong>.
|
||||
{% block userlinks %}
|
||||
{% url 'django-admindocs-docroot' as docsroot %}
|
||||
{% if docsroot %}
|
||||
<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a> /
|
||||
<a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block nav-global %}{% endblock %}
|
||||
</div>
|
||||
<!-- END Header -->
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
{% if title %} › {{ title }}{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% block messages %}
|
||||
{% if messages %}
|
||||
<ul class="messagelist">{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}</ul>
|
||||
{% endif %}
|
||||
{% endblock messages %}
|
||||
|
||||
<!-- Content -->
|
||||
<div id="content" class="{% block coltype %}colM{% endblock %}">
|
||||
{% block pretitle %}{% endblock %}
|
||||
{% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
{% block object-tools %}{% endblock %}
|
||||
{{ content }}
|
||||
{% endblock %}
|
||||
{% block sidebar %}{% endblock %}
|
||||
<br class="clear" />
|
||||
</div>
|
||||
<!-- END Content -->
|
||||
|
||||
{% block footer %}<div id="footer"></div>{% endblock %}
|
||||
</div>
|
||||
<!-- END Container -->
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "admin/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<h1 id="site-name">{% trans 'Django administration' %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block nav-global %}{% endblock %}
|
|
@ -0,0 +1,80 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_static %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}
|
||||
|
||||
{% block coltype %}colMS{% endblock %}
|
||||
|
||||
{% block bodyclass %}dashboard{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
|
||||
{% if app_list %}
|
||||
{% for app in app_list %}
|
||||
<div class="module">
|
||||
<table summary="{% blocktrans with name=app.name %}Models available in the {{ name }} application.{% endblocktrans %}">
|
||||
<caption><a href="{{ app.app_url }}" class="section">{% blocktrans with name=app.name %}{{ name }}{% endblocktrans %}</a></caption>
|
||||
{% for model in app.models %}
|
||||
<tr>
|
||||
{% if model.admin_url %}
|
||||
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
|
||||
{% else %}
|
||||
<th scope="row">{{ model.name }}</th>
|
||||
{% endif %}
|
||||
|
||||
{% if model.add_url %}
|
||||
<td><a href="{{ model.add_url }}" class="addlink">{% trans 'Add' %}</a></td>
|
||||
{% else %}
|
||||
<td> </td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.admin_url %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
|
||||
{% else %}
|
||||
<td> </td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% trans "You don't have permission to edit anything." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div id="content-related">
|
||||
<div class="module" id="recent-actions-module">
|
||||
<h2>{% trans 'Recent Actions' %}</h2>
|
||||
<h3>{% trans 'My Actions' %}</h3>
|
||||
{% load log %}
|
||||
{% get_admin_log 10 as admin_log for_user user %}
|
||||
{% if not admin_log %}
|
||||
<p>{% trans 'None available' %}</p>
|
||||
{% else %}
|
||||
<ul class="actionlist">
|
||||
{% for entry in admin_log %}
|
||||
<li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
|
||||
{% if entry.is_deletion or not entry.get_admin_url %}
|
||||
{{ entry.object_repr }}
|
||||
{% else %}
|
||||
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
|
||||
{% endif %}
|
||||
<br/>
|
||||
{% if entry.content_type %}
|
||||
<span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
|
||||
{% else %}
|
||||
<span class="mini quiet">{% trans 'Unknown content' %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
{% cms_toolbar %}
|
||||
{% placeholder base_content %}
|
||||
{% block base_content%}{% endblock %}
|
||||
{% render_block "js" %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{{ flatpage.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ flatpage.h1 }}</h1>
|
||||
<div>
|
||||
{{ flatpage.content }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,2 @@
|
|||
all:
|
||||
rst2odt -l fr --table-border-thickness=0 --create-links --odf-config-file=map.ini --stylesheet=styles.odt spécification_technique_détaillée.rst>spécification_technique_détaillée.odt
|
|
@ -0,0 +1,5 @@
|
|||
[latex2e writer]
|
||||
documentclass: article
|
||||
documentoptions: 10pt,a4paper,dvipdfmx,times
|
||||
output-encoding: utf-8
|
||||
stylesheet: docutils.tex
|
|
@ -0,0 +1,8 @@
|
|||
\usepackage[top=2.54cm, bottom=2.54cm, left=2.54cm, right=2.54cm]{geometry}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{fncychap}
|
||||
\pagestyle{fancy}
|
||||
\renewcommand{\sectionmark}[1]{\markboth{}{\thesection. \ #1}}
|
||||
\fancyhead[LO,L]{\leftmark}
|
||||
\fancyhead[RO,R]{\rightmark}
|
||||
\fancyfoot[CO,C]{\thepage}
|
|
@ -0,0 +1,3 @@
|
|||
[Formats]
|
||||
blockquote-bulletlist: List 1
|
||||
bulletlist: List 1
|
|
@ -59,7 +59,7 @@ L'application sera découpée en 4 modules fonctionnels :
|
|||
d'une liste d'annonces de logements à louer ;
|
||||
|
||||
- un module administrateur pour le personnel du service logement de
|
||||
l'Université Paris en charge de l'application.
|
||||
l'Université Paris Dauphine en charge de l'application.
|
||||
|
||||
------------------------------------
|
||||
Notations et normes de développement
|
||||
|
@ -86,9 +86,9 @@ Documentation du code source
|
|||
============================
|
||||
|
||||
Toutes les méthodes, fonctions, classes et modules devront contenir une
|
||||
documentation sous forme de commentaire comme indiqué par le PEP8 pour les
|
||||
fichiers Python. Pour les éventuels développement en javascript on suivra une
|
||||
méthodologie équivalent.
|
||||
documentation sous forme de commentaires comme indiqué par le PEP8 pour les
|
||||
fichiers Python. Pour les éventuels développements en javascript on suivra une
|
||||
méthodologie équivalente.
|
||||
|
||||
Les fichiers HTML servant de modèle pour la génération des pages de
|
||||
l'application devront contenir des commentaires indiquant le rôle des
|
||||
|
@ -122,7 +122,7 @@ Le livrable code source contiendra deux parties distinctes:
|
|||
- le code source de l'application dans un répertoire nommé ``src`` ;
|
||||
|
||||
- les modules Python tiers dont l'application dépendra et qui ne seront pas
|
||||
disponibles au format RPM, installé dans ce qu'on appelle un ``virtualenv``
|
||||
disponibles au format RPM, seront installés dans ce qu'on appelle un ``virtualenv``
|
||||
dans le répertoire ``virtualenv``.
|
||||
|
||||
L'arborescence des sources obéira à l'organisation tradionnelle d'une
|
||||
|
@ -136,11 +136,13 @@ application Django.
|
|||
|
||||
- ``documentation_utilisateur.rst``
|
||||
|
||||
La documentation utilisateur qui sera affichée en ligne. Elle sera aussi disponible au format PDF.
|
||||
La documentation utilisateur qui sera affichée en ligne. Elle sera aussi
|
||||
disponible au format PDF.
|
||||
|
||||
- ``documentation_administrateur.rst``
|
||||
|
||||
La documentation administrateur qui sera affichée en ligne. Elle sera aussi disponible au format PDF.
|
||||
La documentation administrateur qui sera affichée en ligne. Elle sera
|
||||
aussi disponible au format PDF.
|
||||
|
||||
- ``appli_project/``
|
||||
|
||||
|
@ -167,7 +169,7 @@ application Django.
|
|||
- ``manage.py``
|
||||
|
||||
Ce fichier est utilisable en ligne de commande pour accomplir des actions
|
||||
sur la base de donnée de l'application. Elle servira par exemple pour
|
||||
sur la base de données de l'application. Elle servira par exemple pour
|
||||
importer des comptes en masse depuis un fichier CSV.
|
||||
|
||||
- ``appli_[socle,recherche,offre,admin]/``
|
||||
|
@ -214,7 +216,8 @@ application Django.
|
|||
|
||||
- ``appli/``
|
||||
|
||||
Fichiers statiques du projet, feuilles de style CSS, fichiers javascript, images JPEG et PNG.
|
||||
Fichiers statiques du projet, feuilles de style CSS, fichiers
|
||||
javascript, images JPEG et PNG.
|
||||
|
||||
- ``css/``
|
||||
|
||||
|
@ -260,8 +263,9 @@ paradigme modèle-vue-contrôleur. Les requêtes sont d'abord prises en charge p
|
|||
un routeur qui détermine la vue responsable de l'URL contenue dans la requête.
|
||||
La vue est une fonction Python qui reçoit en paramètre la requête HTTP. Elle
|
||||
analyse cette requête, et en déduit des actions à accomplir sur les modèles ou
|
||||
des données à afficher à l'utilisateur extraites des modèles. Les données brutes
|
||||
à afficher sont ensuite associé à un modèle (template) pour générer une page HTML.
|
||||
des données à afficher à l'utilisateur extraites des modèles. Les données
|
||||
brutes à afficher sont ensuite associées à un modèle (template) pour générer
|
||||
une page HTML.
|
||||
|
||||
Logiciels tiers utilisés par le projet
|
||||
======================================
|
||||
|
@ -288,12 +292,12 @@ django-filebrowser 3.4.0 BSD Intégré au livrable [#integ
|
|||
============================ ======== ============ ==========
|
||||
|
||||
|
||||
Tous les licences citées dans ce tableau sont des licences libres.
|
||||
Toutes les licences citées dans ce tableau sont des licences libres.
|
||||
|
||||
.. [#integre] Le code source et compilé du module sera intégré au livrable code
|
||||
.. [#integre] Le code source compilé du module sera intégré au livrable code
|
||||
source du site du logement
|
||||
.. [#rc1] La version 1.4 n'est pas encore stabilisée, la dernière étant la 1.4
|
||||
RC1 [#rc]. Mais les annonces des meneurs du projet laissent à penser que la
|
||||
RC1. Mais les annonces des meneurs du projet laissent à penser que la
|
||||
version sera stabilisée à la fin du mois de mars 2012, avant la livraison du
|
||||
socle applicatif.
|
||||
|
||||
|
@ -304,25 +308,6 @@ arborescence virtualenv. Virtualenv permet d'installer localement des
|
|||
modules Python sans impacter le système hôte. Le manuel de création de
|
||||
l'archive *virtualenv* sera fourni dans la documentation d'installation.
|
||||
|
||||
.. important:: Django est normalement agnostique concernant la base de donnée utilisée en
|
||||
dehors de la présence dans les modules Python pour cette base de donnée, mais
|
||||
certains modules tiers tel ``django-gis`` [#gis]_ dépendent de fonctionnalités
|
||||
avancées qui sont disponibles dans PostgreSQL mais pas dans MySQL.
|
||||
|
||||
.. [#gis] gis signifie Geographic Information System; cela indique généralement
|
||||
un logiciel capable de travailler avec des objets géographiques et aussi
|
||||
souvent géométriques, tel que points, lignes, projections cartographiques,
|
||||
surfaces ou polygones.
|
||||
|
||||
|
||||
Choix de la base de donnée
|
||||
==========================
|
||||
|
||||
Le choix entre les bases de données MySQL et PostgreSQL dépendra principalement
|
||||
du besoin concernant les données géographique. Si l'utilisation d'une carte
|
||||
pour le positionnement des annonces est retenu, PostgreSQL devient
|
||||
indispensable.
|
||||
|
||||
------------------------
|
||||
Architecture du logiciel
|
||||
------------------------
|
||||
|
@ -341,12 +326,11 @@ Les objets métiers manipulés par l'application seront:
|
|||
|
||||
- les annonces.
|
||||
|
||||
|
||||
À ces classes d'objet s'ajouteront les classes techniques suivantes:
|
||||
|
||||
- concernant les annonces:
|
||||
|
||||
- les types de prix pour les annonces de logement ;
|
||||
- les types d'offre pour les annonces de logement ;
|
||||
|
||||
- les prestations associées à un logement ;
|
||||
|
||||
|
@ -367,19 +351,20 @@ Les objets métiers manipulés par l'application seront:
|
|||
- une table des paramètres tels que les durées de validités pour certains
|
||||
objets,
|
||||
|
||||
- des objets décrivant les emails d'alerte pour les période d'affluence avec leur date d'envoi
|
||||
automatique.
|
||||
- des objets décrivant les emails d'alerte pour les périodes d'affluence avec
|
||||
leur date d'envoi automatique.
|
||||
|
||||
- une table pour les tickets de validation des mails |---| lorsqu'un utilisateur s'authentifiant par
|
||||
mail modifie son email, il ne faut accomplir l'action de modification de l'email qu'une fois celle-ci
|
||||
validée, cette table fait le lien entre le secret envoyé par mail, la nouvelle adresse email et le
|
||||
compte utilisateur concerné.
|
||||
- une table pour les tickets de validation des mails |---| lorsqu'un
|
||||
utilisateur s'authentifiant par mail modifie son email, il ne faut
|
||||
accomplir l'action de modification de l'email qu'une fois celle-ci validée,
|
||||
cette table fait le lien entre le secret envoyé par mail, la nouvelle
|
||||
adresse email et le compte utilisateur concerné.
|
||||
|
||||
Espace de nommage des identifiants de profil
|
||||
--------------------------------------------
|
||||
|
||||
Les modèles des profils hériteront du modèle ``User`` fourni par Django. Django
|
||||
autorise l'héritage entre modèle qui se traduit par une table parente et une
|
||||
autorise l'héritage entre modèles qui se traduit par une table parente et une
|
||||
table fille, liées par une clé étrangère avec une cardinalité 1-1.
|
||||
|
||||
L'utilisation du modèle ``User`` impose l'existence et l'unicité du champ
|
||||
|
@ -402,9 +387,9 @@ Les étudiants admis non-inscrits ayant un profil recherche temporaire
|
|||
s'identifieront en utilisant leur email comme les profils offre.
|
||||
|
||||
Le caractère ``#`` est normalement interdit dans les identifiants de compte
|
||||
mais la contrainte se situe dans les formulaires et non dans la base, ce qui
|
||||
permet de créer des identifiants programmatiquement, qui ne sont pas accessibles
|
||||
via le formulaire de création de compte.
|
||||
mais la contrainte se situe dans les formulaires et non dans la base de donnée,
|
||||
ce qui permet de créer des identifiants programmatiquement, qui ne sont pas
|
||||
accessibles via le formulaire de création de compte.
|
||||
|
||||
Modèle de données: détail des données métiers
|
||||
---------------------------------------------
|
||||
|
@ -420,13 +405,13 @@ Champ Type Description
|
|||
``username`` [#framework]_ chaîne, identifiant interne, il sera inutilisé par les
|
||||
longueur 30 utilisateurs qui s'identifieront avec leur email,
|
||||
mais étant hérité du modèle ``User`` il est
|
||||
obligatoire. Il devra contenir le préfixe
|
||||
obligatoire. Il aura la forme
|
||||
``offre#[chaîne aléatoire]``.
|
||||
``email`` [#framework]_ email, email de l'utilisateur; c'est l'identifiant
|
||||
longueur 128 utilisé pour se connecter; il est validé pour
|
||||
activer le compte; tout changement nécessitera
|
||||
une revalidation, le compte restant accessible
|
||||
avec l'ancien email tant que la validation n'a
|
||||
avec l'ancien email tant que la validation n'aura
|
||||
pas été effectuée.
|
||||
``password`` [#framework]_ chaîne, format mot de passe du compte
|
||||
interne à
|
||||
|
@ -467,7 +452,7 @@ Champ Type Description
|
|||
``rechtmp#[chaîne unique aléatoire]``.
|
||||
``email`` [#framework]_ email, email de l'utilisateur; c'est l'identifiant
|
||||
longueur 128 utilisé pour se connecter pour les profils
|
||||
temporaires; pour les profils issues
|
||||
temporaires; pour les profils issus
|
||||
du LDAP ce sera l'email du compte LDAP.
|
||||
``password`` [#framework]_ chaîne, mot de passe du compte pour les comptes
|
||||
format interne temporaires; les comptes normaux utilisent
|
||||
|
@ -478,7 +463,7 @@ Champ Type Description
|
|||
l'accès à un compte
|
||||
``acceptation_cgu`` booléen drapeau d'acceptation des conditions
|
||||
générales d'utilisation
|
||||
``type_d_offre`` relation spécifie les types de prix d'annonce auxquels
|
||||
``type_d_offre`` relation spécifie les types d'offre auxquels
|
||||
multiple l'utilisateur aura accès; si absent l'utilisateur
|
||||
[#manytomany]_ n'a accès qu'aux prix du marché.
|
||||
, optionnel,
|
||||
|
@ -510,16 +495,13 @@ n'étant pas présent. Les administrateurs seront simplement des modèles du ty
|
|||
Champ Type Description
|
||||
=========================== ============== =================================================
|
||||
``username`` [#framework]_ chaîne, Il s'agit de comptes locaux. L'authentification
|
||||
longueur 30 se faisant via l'email et l'identifiant sera
|
||||
longueur 30 se fera via l'email, l'identifiant sera
|
||||
``offre#[chaîne unique aléatoire]``.
|
||||
``email`` [#framework]_ email, email de l'utilisateur; c'est l'identifiant
|
||||
longueur 128 utilisé pour se connecter pour les profils
|
||||
temporaires; pour les profils issues
|
||||
du LDAP ce sera l'email du compte LDAP.
|
||||
``password`` [#framework]_ chaîne, mot de passe du compte pour les comptes
|
||||
format interne temporaires; les comptes normaux utilisent
|
||||
à Django CAS pour l'authentification et LDAP pour
|
||||
la récupération de leurs attributs |---| mail.
|
||||
``email`` [#framework]_ email, email de l'administrateur
|
||||
longueur 128
|
||||
``password`` [#framework]_ chaîne, mot de passe du compte
|
||||
format interne
|
||||
à Django
|
||||
``is_staff`` [#framework]_ booléen, ce booléen détermine l'appartenance d'un
|
||||
format interne compte utilisateur au profil des administrateurs
|
||||
à Django
|
||||
|
@ -543,13 +525,13 @@ Champ Type Description
|
|||
``derniere_publication`` date et heure, la date de dernière publication; servira à la mise hors
|
||||
optionnel ligne automatique
|
||||
``derniere_validation`` date et heure, la date de dernière validation
|
||||
optionnel
|
||||
optionnel
|
||||
``date_de_creation`` date et heure la date de création
|
||||
``validation`` chaînne, statut de la validation; une annonce mise en ligne
|
||||
longueur 8, par son auteur et donc le status de validation est
|
||||
longueur 8, par son auteur et donc le statut de validation est
|
||||
énumération ``inconnu`` est soumise à la validation.
|
||||
parmi Tout modification par un annonceur d'une annonce ayant un statut
|
||||
``inconnu``, ``valide`` entraine son passage au statut
|
||||
parmi Toute modification par un annonceur d'une annonce ayant un statut
|
||||
``inconnu``, ``valide`` entraîne son passage au statut
|
||||
``invalide``, de validation ``inconnu``.
|
||||
``valide``,
|
||||
défaut
|
||||
|
@ -595,10 +577,10 @@ Champ Type Description
|
|||
``SE``,
|
||||
``SO``
|
||||
``position_geographique`` point l'utilisation de ce champ dépendra de la base de donnée
|
||||
géographique, emploiée
|
||||
géographique, employée
|
||||
optionnel
|
||||
``type_de_prix`` clé spécifie le type de prix dans lequel se classe
|
||||
externe une annonce, si absent le prix est normal
|
||||
``type_de_prix`` clé spécifie le type d'offre dans lequel se classe
|
||||
externe une annonce, par défaut la valeur est « prix du marché ».
|
||||
[#manytomany]_
|
||||
, optionnel,
|
||||
vers
|
||||
|
@ -632,20 +614,6 @@ Champ Type Description
|
|||
longueur 128
|
||||
========================= ============== =================================================
|
||||
|
||||
Groupe des types de prestations
|
||||
-------------------------------
|
||||
|
||||
Nom de la classe: ``GroupeDeTypeDePrestation``
|
||||
|
||||
========================= ============== =================================================
|
||||
Champ Type Description
|
||||
========================= ============== =================================================
|
||||
``nom`` chaîne, description du groupe
|
||||
longueur 128
|
||||
``ordre`` entier relatif ce nombre permet de trier les groupes de
|
||||
prestations dans les affichages
|
||||
========================= ============== =================================================
|
||||
|
||||
Type de prestation
|
||||
------------------
|
||||
|
||||
|
@ -659,7 +627,7 @@ Champ Type Description
|
|||
longueur 128
|
||||
``ordre`` entier relatif ce nombre permet de trier les types de
|
||||
prestations dans les affichages
|
||||
``type_de_valeur`` chaîne, indique si la valeur associer à la prestation
|
||||
``type_de_valeur`` chaîne, indique si la valeur associée à la prestation
|
||||
longueur 1, est un booléen |---| ``B`` |---| ou un nombre
|
||||
énumération entier |---| ``N``. Dans les interfaces d'édition
|
||||
un booléen deviendra une case
|
||||
|
@ -682,7 +650,7 @@ Champ Type Description
|
|||
, optionnel numérique, ce champ contient la valeur associée.
|
||||
========================= ============== =================================================
|
||||
|
||||
Pour les types de prestation ayant un type de valeur booléen la simple existance d'un
|
||||
Pour les types de prestation ayant un type de valeur booléen la simple existence d'un
|
||||
modèle du type ``Prestation`` associé à une annonce signifie que la valeur pour la
|
||||
prestation est vrai |---| ``True``.
|
||||
|
||||
|
@ -707,10 +675,10 @@ Champ Type Description
|
|||
optionnel
|
||||
========================= ================= =================================================
|
||||
|
||||
La liste et le type des paramètres possible seront configurée dans la configuration statique de l'application |---| le fichier ``settings.py``. Une interface graphique d'édition des paramètres
|
||||
sera automatiquement généré à partir de cette description. Deux types seront supportés:
|
||||
La liste et le type des paramètres possible seront configurés dans la configuration statique de l'application |---| le fichier ``settings.py``. Une interface graphique d'édition des paramètres
|
||||
sera automatiquement générée à partir de cette description. Deux types seront supportés:
|
||||
|
||||
- les entiers positif, pour les durée de validitié en nombre de jours,
|
||||
- les entiers positifs, pour les durées de validitié en nombre de jours,
|
||||
|
||||
- une référence à un modèle d'email.
|
||||
|
||||
|
@ -751,8 +719,8 @@ Modèle des rôles et des droits
|
|||
------------------------------
|
||||
|
||||
Django permet d'associer des utilisateurs ou des groupes à une liste de
|
||||
permission. Des permissions pour la création, l'édition et la suppression de
|
||||
modèles sont automatiquement créés pour chaque type de modèle.
|
||||
permission. Des permissions pour la création, l'édition et la suppression des
|
||||
modèles sont automatiquement créées pour chaque type de modèle.
|
||||
|
||||
Nous proposons de baser la gestion des permissions dans le site du logement de
|
||||
l'Université Paris Dauphine sur deux principes:
|
||||
|
@ -761,8 +729,8 @@ l'Université Paris Dauphine sur deux principes:
|
|||
l'appartenance à chacun de ces profils implique automatiquement le droit
|
||||
d'accéder aux modules les concernant,
|
||||
|
||||
- des permissions explicites concernant les différents fonctionnalités du
|
||||
module d'administration. Ces permissions seront associés à divers groupes qui
|
||||
- des permissions explicites concernant les différentes fonctionnalités du
|
||||
module d'administration. Ces permissions seront associées à divers groupes qui
|
||||
pourront être affectés aux utilisateurs administrateurs permettant ainsi de
|
||||
couvrir tout le spectre de répartition des droits possibles.
|
||||
|
||||
|
@ -771,24 +739,23 @@ Nous détaillons les droits d'administration dans le tableau suivant.
|
|||
============================== ========================================================
|
||||
Identifiant du droit Définition
|
||||
============================== ========================================================
|
||||
``gestion_profil_offre`` permet la recherche, l'affichage, le bloquage et le
|
||||
débloquage des profils offre.
|
||||
``gestion_annonce`` permet la recherche, l'affichag, le bloquage, débloquage,
|
||||
``gestion_profil_offre`` permet la recherche, l'affichage, le blocage et le
|
||||
déblocage des profils offre.
|
||||
``gestion_annonce`` permet la recherche, l'affichag, le blocage, déblocage,
|
||||
et la validation des annonces.
|
||||
``gestion_profil_recherche`` permet la recherche, l'affichage, le bloquage et le
|
||||
débloquage, ainsi que la création de profils
|
||||
recherches temporaires et le raccordement d'un
|
||||
profil temporaire à une identité LDAP existante,
|
||||
autorisation du compte CAS pour les chercheurs de passage
|
||||
``gestion_profil_recherche`` permet la recherche, l'affichage, le blocage et le
|
||||
déblocage, ainsi que la création de profils
|
||||
recherche temporaires et l'autorisation du compte
|
||||
CAS pour les chercheurs de passage
|
||||
``gestion_type_d_offre`` permet la modification du champ type d'offre des
|
||||
profils recherche
|
||||
profils offre
|
||||
``gestion_paramètres`` permet la modification des paramètres d'expiration
|
||||
des comptes et des annonces, et du contenu des mails
|
||||
automatiques ainsi que des dates d'envoi.
|
||||
``gestion_contenu`` permet la gestion des contenus des pages statiques du
|
||||
site via le CMS intégré.
|
||||
``gestion_prestations`` permet la gestion des listes de prestations que les
|
||||
annonceurs pourront associés à leurs annonces
|
||||
annonceurs pourront associer à leurs annonces
|
||||
============================== ========================================================
|
||||
|
||||
================================================ ==============================
|
||||
|
@ -811,9 +778,9 @@ Administrateur service relations internationales ``gestion_type_d_offre``
|
|||
Système d'authentification
|
||||
==========================
|
||||
|
||||
Le projet emploiera deux systèmes d'authentification distinct :
|
||||
Le projet emploiera deux systèmes d'authentification distincts :
|
||||
|
||||
- un système classique de login et mot de passe, donnant accès à des comptes
|
||||
- un système classique via email et mot de passe, donnant accès à des comptes
|
||||
dits locaux; il y en aura de trois types:
|
||||
|
||||
- les profils offre,
|
||||
|
@ -830,54 +797,48 @@ Le projet emploiera deux systèmes d'authentification distinct :
|
|||
Authentification par login et mot de passe
|
||||
------------------------------------------
|
||||
|
||||
L'authentification par login et mot de passe sera une modification du système
|
||||
L'authentification via email et mot de passe sera une modification du système
|
||||
d'authentification fourni par Django dans le module
|
||||
``django.contrib.auth.backends``. Elle se basera sur deux extensions du système d'authentification générique:
|
||||
``django.contrib.auth.backends``. Elle se basera sur deux moteurs
|
||||
d'authentification spécifiques:
|
||||
|
||||
- deux moteurs d'authentification spécifiques:
|
||||
- le premier nommé ``ProfilOffreMoteurAuthentification``, acceptera deux clés
|
||||
d'authentification ``email`` et ``password`` et retournera un objet
|
||||
de la classe ``ProfilOffre`` au lieu d'un objet ``User``. Il recherchera
|
||||
ce profil via son champ ``email``. La classe
|
||||
``ProfilOffreMoteurAuthentification`` sera placée dans le module
|
||||
``appli_project.appli_offre.backends``.
|
||||
|
||||
- le premier nommé ``ProfilOffreMoteurAuthentification``, acceptera deux clés d'authentification
|
||||
``email_offre`` et ``password`` et retournea un objet de la classe ``ProfilOffre`` au lieu d'un objet
|
||||
``User``. Il recherchera ce profil via son champ ``email``. La classe
|
||||
``ProfilOffreMoteurAuthentification`` sera placé dans le module ``appli_project.appli_offre.backends``.
|
||||
- le second nommé ``ProfilRechercheTemporaireMoteurAuthentification``,
|
||||
acceptera deux clés d'authentification ``email`` et ``password`` et
|
||||
retournera un objet de la classe ``ProfilRecherche`` au lieu d'un objet
|
||||
``User``. Il recherchera ce profil via son champ ``email``, seul les profils
|
||||
ayant le préfixe ``rechtmp#`` seront considérés. La classe
|
||||
``ProfilRechercheTemporaireMoteurAuthentification`` sera placée dans le
|
||||
module ``appli_project.appli_offre.backends``.
|
||||
|
||||
- le second nommé ``ProfilRechercheTemporaireMoteurAuthentification``, acceptera deux clés d'authentification
|
||||
``email_offre`` et ``password`` et retournea un objet de la classe ``ProfilRecherche`` au lieu d'un
|
||||
objet ``User``. Il recherchera ce profil via son champ ``email``, seul les profils ayant le préfixe
|
||||
``rechtmp#`` seront considérés. La classe ``ProfilRechercheTemporaireMoteurAuthentification`` sera
|
||||
placé dans le module
|
||||
``appli_project.appli_offre.backends``.
|
||||
|
||||
- deux formulaires d'authentification:
|
||||
|
||||
- le formulaire ``ProfileOffreConnexionFormulaire`` qui contiendra un champ pour l'email et le mot de
|
||||
passe et qui passera l'email à la fonction d'authentification ``django.contrib.auth.authenticate()``
|
||||
dans un argument nommé ``email_offre`` |---| l'objectif étant de ne faire réagier que le moteur
|
||||
d'authentification ``ProfilOffreMoteurAuthentification``.
|
||||
|
||||
- le formulaire ``ProfileRechercheTemporaireConnexionFormulaire`` qui contiendra un champ pour l'email
|
||||
et le mot de passe et qui passera l'email à la fonction d'authentification
|
||||
``django.contrib.auth.authenticate()`` dans un argument nommé ``email_offre`` |---| l'objectif étant
|
||||
de ne faire réagier que le moteur d'authentification
|
||||
``ProfilRechercheTemporaireMoteurAuthentification``.
|
||||
Les deux moteurs seront appelés par les différents formulaires
|
||||
d'authentification présents en page d'accueil et dans les modules offre et
|
||||
recherche.
|
||||
|
||||
Authentification CAS
|
||||
--------------------
|
||||
|
||||
La page de connexion du module recherche permettra la connexion aux étudiants ou chercheur de passage
|
||||
pourvu d'un compte ENT via le serveur d'authentification CAS de l'Université Paris Dauphine.
|
||||
S'ils n'ont pas encore de compte
|
||||
dans l'application un nouveau leur sera créer automatiquement. Les utilisateurs du type chercheur n'auront
|
||||
accès que si préalablement ils en ont fait la demande au service du logement, qui pourra leur créer un
|
||||
compte via le module d'administration.
|
||||
La page de connexion du module recherche permettra la connexion aux étudiants
|
||||
ou chercheur de passage pourvus d'un compte ENT via le serveur
|
||||
d'authentification CAS de l'Université Paris Dauphine. S'ils n'ont pas encore
|
||||
de compte dans l'application un nouveau leur sera créé automatiquement. Les
|
||||
utilisateurs du type chercheur n'auront accès que si préalablement ils en ont
|
||||
fait la demande au service du logement, qui pourra leur créer un compte via le
|
||||
module d'administration.
|
||||
|
||||
|
||||
La vue de connexion prendra en charge les échanges CAS. La création automatique des
|
||||
comptes sera gérée par un moteur d'authentification spécifique nommé
|
||||
``ProfilRechercheAuthenticationBackend`` qui implémentera ces règles métier.
|
||||
L'authentification se fera selon une clé nommée ``cas_ticket``. La classe
|
||||
``ProfilRechercheAuthenticationBackend`` sera placé dans le module
|
||||
``appli_project.appli_recherche.backends``.
|
||||
La vue de connexion prendra en charge les échanges CAS. La création automatique
|
||||
des comptes sera gérée par un moteur d'authentification spécifique nommé
|
||||
``ProfilRechercheAuthenticationBackend`` qui procèdera à la validation du
|
||||
compte ENT par rapport aux serveurs LDAP avant connexion ou création d'un
|
||||
nouveau profil recherche. L'authentification se fera selon une clé nommée
|
||||
``cas_ticket``. La classe ``ProfilRechercheAuthenticationBackend`` sera placée
|
||||
dans le module ``appli_project.appli_recherche.backends``.
|
||||
|
||||
------------
|
||||
Module socle
|
||||
|
@ -894,18 +855,18 @@ Chemin Description
|
|||
======================================== ======================================
|
||||
``/`` page d'accueil du site
|
||||
``/valider-email`` formulaire de validation des emails
|
||||
``/reinitialiser-mot-de-passe`` formulaire de demande de reinitialisation du mot de passe
|
||||
``/reinitialiser-mot-de-passe`` formulaire de demande de réinitialisation du mot de passe
|
||||
``/changement-mot-de-passe`` formulaire de changement de mot de passe
|
||||
``/acceptation-cgu`` formulaire d'acceptation des conditions générales d'utilisation
|
||||
``/acceptation-charte-qualité`` formulaire d'acceptation des conditions générales d'utilisation
|
||||
``/acceptation-charte-qualite`` formulaire d'acceptation de la charte qualité
|
||||
``/authentification-demande-de-bourse`` formulaire de vérification des demandes de bourse
|
||||
======================================== ======================================
|
||||
|
||||
Vue ``/``
|
||||
---------
|
||||
|
||||
Ce chemin ainsi que tous les chemins qui ne seront pas géré directement par
|
||||
l'application seront transmises au module CMS. Si une page a été défini pour le
|
||||
Ce chemin ainsi que tous les chemins qui ne seront pas gérés directement par
|
||||
l'application seront transmis au module CMS. Si une page a été définie pour le
|
||||
chemin elle sera affichée.
|
||||
|
||||
Cette vue devrait contenir:
|
||||
|
@ -920,7 +881,7 @@ Cette vue devrait contenir:
|
|||
vers le module offre ou le module recherche introduit par la mention « Vous
|
||||
possédez déjà un compte sur cette plateforme... »,
|
||||
|
||||
- un bouton de connection pour les comptes ENT déclenchant une requête
|
||||
- un bouton de connexion pour les comptes ENT déclenchant une requête
|
||||
d'authentification CAS renvoyant vers le module recherche en cas
|
||||
d'authentification réussie; le bouton est intitulé « J'ai un compte ENT de
|
||||
l'Université Paris Dauphine ». Lorsqu'on passe la souris sur le bouton une
|
||||
|
@ -931,7 +892,7 @@ Vue ``/valider-email``
|
|||
----------------------
|
||||
|
||||
Lors d'un changement de mail ou de l'ouverture d'un compte un mail contenant un
|
||||
jeton de confirmation sera envoié à l'utilisateur pour lui permettre de confirmer
|
||||
jeton de confirmation sera envoyé à l'utilisateur pour lui permettre de confirmer
|
||||
sa nouvelle adresse mail.
|
||||
|
||||
Vue ``/reinitialisation-mot-de-passe``
|
||||
|
@ -945,19 +906,22 @@ Vue ``/acceptation-cgu``
|
|||
------------------------
|
||||
|
||||
Toutes les vues des modules recherches et offres nécessitant une
|
||||
authentification ne sont accessible que si l'utilisateur a bien validé les
|
||||
authentification ne sont accessibles que si l'utilisateur a bien validé les
|
||||
conditions générales d'utilisation. Dans le cas contraire l'utilisateur est
|
||||
redirigé vers la vue ``/acceptation-cgu`` qui lui demandera d'accepter
|
||||
celles-ci. Il sera ensuite redirigé sur la page qu'il essayait d'accéder
|
||||
initialement.
|
||||
|
||||
Vue ``/acceptation-charte-qualité``
|
||||
Vue ``/acceptation-charte-qualite``
|
||||
-----------------------------------
|
||||
|
||||
Le module offre ne deviendra accessible qu'un fois validé la charte qualité. Le
|
||||
texte de la charte qualité pourra contenir deux variables ``{{surface_min}}``
|
||||
et ``{{prix_max_par_m2}}`` qui reprendront les valeurs définies par
|
||||
l'administrateur.
|
||||
Cette vue affichera la charte qualité du site du logement de l'Université Paris
|
||||
Dauphine. Il contiendra aussi un formulaire d'acceptation de la dite charte.
|
||||
L'acceptation de cette charte sera obligatoire pour pouvoir utiliser un profil
|
||||
offre. Tout accès à une vue du module offre par un utilisateur authentifié
|
||||
n'ayant pas encore validé la charte qualité sera redirigé sur cette vue.
|
||||
L'acceptation de la charte redirigera l'utilisateur vers la page initialement
|
||||
accédée.
|
||||
|
||||
Vue ``/authentification-demande-de-bourse``
|
||||
-------------------------------------------
|
||||
|
@ -975,7 +939,7 @@ Scénarios fonctionnels
|
|||
|
||||
Dans le module offre un utilisateur doit pouvoir:
|
||||
|
||||
1. ouvrir un compte et accepter la charque qualité
|
||||
1. ouvrir un compte
|
||||
|
||||
2. valider son email
|
||||
|
||||
|
@ -1003,16 +967,23 @@ Dans le module offre un utilisateur doit pouvoir:
|
|||
|
||||
14. supprimer son compte
|
||||
|
||||
15. accepter la charte qualité
|
||||
|
||||
16. accepter les conditions générales d'utilisation
|
||||
|
||||
Les scénario 2 et 4 en partie sont remplis par la vue du socle ``/valider-email``.
|
||||
|
||||
Les scénarios 12 et 13 sont gérés par un processus en tâche de fond.
|
||||
Les scénarios 12 et 13 sont remplis par un processus en tâche de fond.
|
||||
|
||||
Le scénario 11 est accessible depuis toutes les vues.
|
||||
Le scénario 11 est réalisable depuis toutes les vues
|
||||
|
||||
Les scénarios 15 et 16 sont remplis par les vues du socle ``/acceptation-cgu``
|
||||
et ``/acceptation-charte-qualite``.
|
||||
|
||||
Arborescence du module
|
||||
======================
|
||||
|
||||
Pour chaque vue on rappellera le ou les scénarios fonctionnels qui sont pris en charge.
|
||||
Pour chaque vue on rappellera le ou les scénarios fonctionnels qui sont remplis.
|
||||
|
||||
======================================== ======================================
|
||||
Chemin Description
|
||||
|
@ -1023,7 +994,6 @@ Chemin Description
|
|||
``/offre/inscription`` formulaire de création de compte [1]
|
||||
``/offre/confirmation-inscription/`` formulaire de confirmation d'inscription [1]
|
||||
``/offre/mon-email`` formulaire de changement d'email [4]
|
||||
``/offre/mes-alertes`` formulaire d'acceptation des mails d'alerte pendant les période d'affluence
|
||||
``/offre/nouvelle-annonce`` formulaire de création d'une nouvelle annonce [5]
|
||||
``/offre/annonces/xxx`` vue d'édition d'une annonce pour le profil offre [6]
|
||||
``/offre/annonces/xxx/supprimer`` formulaire de suppression d'une annonce [9]
|
||||
|
@ -1039,13 +1009,9 @@ Vue ``/offre``
|
|||
L'entête de page rappellera l'email actuellement configuré pour le compte de
|
||||
l'utilisateur ainsi qu'un lien pour modifier celui-ci.
|
||||
|
||||
Il y aura aussi un lien vers la vue ``/offre/mes-alertes`` pour permettre à
|
||||
l'utilisateur de donner son accord concernant les alertes pendant les périodes
|
||||
d'affluence.
|
||||
|
||||
Cette vue est la page d'accueil des utilisateurs du profil offre authentifiés.
|
||||
Elle présentera une liste paginée de 10 en 10 des annonces créées par
|
||||
l'utilisateur. Tous les détails d'une annonce seront directement affiché dans
|
||||
l'utilisateur. Tous les détails d'une annonce seront directement affichés dans
|
||||
le listing. Un code couleur permettra de différencier les annonces publiées des
|
||||
annonces non publiées.
|
||||
|
||||
|
@ -1066,8 +1032,13 @@ d'annonce à valider dans le module d'administration.
|
|||
Vue ``/offre/connexion``
|
||||
------------------------
|
||||
|
||||
Cette vue présentera le formulaire ``ProfilOffreConnexionFormulaire`` défini plus haut.
|
||||
Elle contiendra aussi un lien vers le formulaire de récupération de mot de passe.
|
||||
Cette vue présentera:
|
||||
|
||||
- un formulaire de connexion au module offre,
|
||||
|
||||
- un lien vers la vue de création d'un profil offre,
|
||||
|
||||
- un lien pour récupérer un mot de passe perdu.
|
||||
|
||||
Vue ``/offre/inscription``
|
||||
--------------------------
|
||||
|
@ -1077,17 +1048,18 @@ Cette vue présentera le formulaire de création d'un nouveau compte pour le pro
|
|||
- le mail de l'utilisateur,
|
||||
- un mot de passe,
|
||||
- la confirmation du mot de passe,
|
||||
- d'accetper en cochant une case les conditions générales d'utilisation, vers lesquelles il y aura un lien,
|
||||
- d'accepter en cochant la charte de qualité, vers laquelle il y aura un lien.
|
||||
|
||||
Si l'email n'est pas déjà utilisé le compte sera créé à l'état inactif et un mail de confirmation sera envoyé à l'adresse email fournie.
|
||||
En cas de non confirmation dans un délai paramétrable, le compte sera supprimé.
|
||||
Si l'email n'est pas déjà utilisé le compte sera créé à l'état inactif et un
|
||||
mail de confirmation sera envoyé à l'adresse email fournie. En cas de non
|
||||
confirmation dans un délai paramétrable, le compte sera supprimé.
|
||||
|
||||
Vue ``/offre/confirmation-inscription/[ticket]``
|
||||
------------------------------------------------
|
||||
|
||||
Le mail de confirmation envoyé à l'inscription contiendra un lien vers cette vue contenant un ticket de confirmation. Lorsqu'il accèdera à cette vue le compte sera activé et l'utilisateur automatiquement authentifié puis renvoyé vers sa page d'accueil.
|
||||
|
||||
Le mail de confirmation envoyé à l'inscription contiendra un lien vers cette
|
||||
vue contenant un ticket de confirmation. Lorsqu'il accédera à cette vue le
|
||||
compte sera activé et l'utilisateur automatiquement authentifié puis redirigé
|
||||
vers sa page d'accueil.
|
||||
|
||||
Vue ``/offre/mon-email``
|
||||
------------------------
|
||||
|
@ -1102,19 +1074,11 @@ Le formulaire contiendra aussi deux cases à cocher permettant de contrôler
|
|||
l'opt-in ou l'opt-out concernant les mails de notification envoyés par la
|
||||
plateforme.
|
||||
|
||||
Vue ``/offre/mes-alertes``
|
||||
-----------------------------
|
||||
|
||||
Cette vue proposera un formulaire contenant une seul case à cocher demandant à l'utilisateur s'il souhaite
|
||||
recevoir des emails d'alerte pendant les périodes d'affluence du site.
|
||||
|
||||
Le formulaire aura deux boutons ``Accepter`` et ``Annuler``. Dans les deux cas l'utilisateur sera redirigé sur sa page d'accueil.
|
||||
|
||||
Vue ``/offre/nouvelle-annonce``
|
||||
-------------------------------
|
||||
|
||||
En introduction les critères de validation automatique de l'annonce sont
|
||||
rapplé, i.e. la surface minimale et le prix maximal par m².
|
||||
rappelé, i.e. la surface minimale et le prix maximal par m².
|
||||
|
||||
Le formulaire de création d'annonce permettra d'initialiser les champs suivant
|
||||
du modèle ``Annonce``:
|
||||
|
@ -1135,30 +1099,34 @@ du modèle ``Annonce``:
|
|||
|
||||
- ``description``
|
||||
|
||||
Tous les autres champs adopteront leur valeur par défaut à l'issue de la
|
||||
Tous les autres champs adopteront leurs valeurs par défaut à l'issue de la
|
||||
création.
|
||||
|
||||
Par ailleurs le formulaire permettra d'associer des prestations à l'annonce via
|
||||
une liste ordonnée et groupée de case à cocher et de champs numérique. Cette
|
||||
partie du formulaire permettra la création d'un object ``Prestation`` pour
|
||||
chaque case cochée et chaque champ rempli.
|
||||
une liste ordonnée de case à cocher et de champs numériques.
|
||||
|
||||
Si l'annonce soumise ne remplit pas les critères de validation automatique
|
||||
spécifiés dans les paramètres du module d'administration elle est immédiatement
|
||||
refusée et un message d'explication est affiché avant le formulaire.
|
||||
|
||||
Vue ``/offre/annonces/xxx``
|
||||
-------------------------------
|
||||
|
||||
Cette vue réutilisera le formulaire de création d'annonce mais en intialisant son contenu avec celui d'une annonce existante. Toute soumissions valide du formulaire repassera ĺe statut de validation de l'annonce dans l'état ``inconnu``.
|
||||
Cette vue affichera un formulaire d'édition pour une annonce existante sur le
|
||||
même modèle que la vue de création.
|
||||
|
||||
Vue ``/offre/annonces/xxx/supprimer``
|
||||
---------------------------------------
|
||||
|
||||
Le lien de suppressions accompagnant l'affichage des annonces en page d'accueil pointera vers cette vue.
|
||||
Un formulaire y demandera confirmation de la suppressions de l'annonce. L'acceptation ou le refus ramènera
|
||||
l'utilisateur en page d'accueil.
|
||||
Le lien de suppressions accompagnant l'affichage des annonces en page d'accueil
|
||||
pointera vers cette vue. Un formulaire y demandera confirmation de la
|
||||
suppression de l'annonce. L'acceptation ou le refus ramènera l'utilisateur en
|
||||
page d'accueil.
|
||||
|
||||
Vue ``/offre/supprimer``
|
||||
------------------------
|
||||
|
||||
Cette vue affiche une formulaire de confirmation pour la suppression du profil
|
||||
Cette vue affiche un formulaire de confirmation pour la suppression du profil
|
||||
offre de l'utilisateur. En cas de confirmation le compte ainsi que toutes ses
|
||||
annonces sont supprimés.
|
||||
|
||||
|
@ -1169,12 +1137,15 @@ Module recherche
|
|||
Scénarios fonctionnels
|
||||
======================
|
||||
|
||||
Un utilisateur ayant un profil recherche peut:
|
||||
|
||||
1. s'authentifier via CAS ou un formulaire local ;
|
||||
|
||||
2. fusionner son compte local et son compte LDAP
|
||||
|
||||
3. chercher des annonces selon deux critères: prix maximum et emplacement
|
||||
(banlieue, Paris- NO, Paris-NE, Paris-SO, Paris-SE) ;
|
||||
3. chercher des annonces selon trois critères: prix maximum, paris ou paris et
|
||||
alentours et le quadrant géographique de l'Ile de France |---| nord-ouest,
|
||||
nord-est, sud-ouest et sud-est.
|
||||
|
||||
4. sauvegarder une annonce ;
|
||||
|
||||
|
@ -1187,7 +1158,7 @@ Scénarios fonctionnels
|
|||
|
||||
8. lister les recherches sauvegardées comme alertes ;
|
||||
|
||||
9. supprimer un recherche sauvegardée comme alerte ;
|
||||
9. supprimer une recherche sauvegardée comme alerte ;
|
||||
|
||||
10. demander à être mis en relation avec un propriétaire-annonceur concernant
|
||||
une annonce ;
|
||||
|
@ -1195,7 +1166,7 @@ Scénarios fonctionnels
|
|||
11. imprimer le document « certification de location » pour signature par son
|
||||
propriétaire en vu d'une demande de bourse ;
|
||||
|
||||
12. créer une demande de au format PDF qu'il transmettra au service du logement
|
||||
12. créer une demande de bourse au format PDF qu'il transmettra au service du logement
|
||||
du CROUS |---| s'il est doté de la propriété « accès aux logements à prix
|
||||
réduit » ;
|
||||
|
||||
|
@ -1207,12 +1178,16 @@ Scénarios fonctionnels
|
|||
|
||||
15. voir le détail d'une annonce ;
|
||||
|
||||
16. se déconnecter.
|
||||
16. se déconnecter ;
|
||||
|
||||
17. valider les conditions générales d'utilisation.
|
||||
|
||||
Le scénario 16 est accessible depuis toute les pages.
|
||||
|
||||
Le scénario 14 est pris en charge par la vue du socle ``/valider-email``.
|
||||
|
||||
Le scénario 17 est rempli par la vue du socle correspondante.
|
||||
|
||||
Arborescence du module
|
||||
======================
|
||||
|
||||
|
@ -1223,24 +1198,24 @@ Chemin Description
|
|||
d'accueil, moteur de recherche des
|
||||
annonces, liens vers gestion des alertes
|
||||
et liste des annonces sauvegardées.
|
||||
la recherche en cours peut-être transformée
|
||||
la recherche en cours peut être transformée
|
||||
en une alerte. [3, 4, 7, 10]
|
||||
``/recherche/connexion`` page de connexion CAS ou email et mot de passe [1]
|
||||
``/recherche/raccordement-a-mon-compte-ent`` Cette vue n'est visible qu'aux
|
||||
profils recherche temporaires,
|
||||
elle est accesible via un lien
|
||||
elle est accessible via un lien
|
||||
ajouté à la page d'accueil des
|
||||
profils recherche temporaires. [2]
|
||||
``/recherche/mes-alertes`` page de listing et de suppressions des alertes [8,9]
|
||||
``/recherche/mes-annonces`` page de listing et de suppressions des annonces
|
||||
sauvegardées [5,6]
|
||||
``/recherche/annonce/xxx`` vue d'une annonce pour le profil recherhce [15]
|
||||
``/recherche/annonce/xxx`` vue d'une annonce pour le profil recherche [15]
|
||||
``/recherche/annonce/xxx/contact`` formulaire de contact [10]
|
||||
``/recherche/annonce/xxx/certificat`` édition du certificat de location à imprimer [11]
|
||||
|
||||
``/recherche/mon-email`` formulaire de modification de l'email pour les
|
||||
comptes temporaires [13]
|
||||
``/recherche/demande-de-bourse`` édition du formulaire de demande bourse à imprimer [12]
|
||||
``/recherche/demande-de-bourse`` édition du formulaire de demande de bourse à imprimer [12]
|
||||
============================================ ======================================
|
||||
|
||||
Vues
|
||||
|
@ -1268,17 +1243,17 @@ Le formulaire de recherche suivra, il contiendra 3 champs:
|
|||
|
||||
dont la valeur par défaut sera « Paris ».
|
||||
|
||||
- quatres cases à cocher :
|
||||
- quatre cases à cocher :
|
||||
|
||||
- Nord-est
|
||||
- nord-est
|
||||
|
||||
- Nord-ouest
|
||||
- nord-ouest
|
||||
|
||||
- Sud-est
|
||||
- sud-est
|
||||
|
||||
- Sud-ouest
|
||||
- sud-ouest
|
||||
|
||||
qui seront toutes intitialement cochées.
|
||||
qui seront toutes inititialement cochées.
|
||||
|
||||
Le formulaire sera suivi:
|
||||
|
||||
|
@ -1290,38 +1265,33 @@ Le formulaire sera suivi:
|
|||
recherche présents dans le formulaire. L'utilisateur sera redirigé sur la
|
||||
vue ``/recherche/mes-alertes``.
|
||||
|
||||
Le listing des annonces correspondant aux critère du formulaire dernièrent
|
||||
soumi ou de ses valeurs par défaut, ainsi que du critère type d'offre associé
|
||||
au compte du profil recherche connecté est affiché à la suite. Ce listing est
|
||||
Le listing des annonces correspondant aux critère du formulaire dernièrement
|
||||
soumis ou de ses valeurs par défaut, ainsi que du critère type d'offre associé
|
||||
au compte du profil recherche connecté s'affiche à la suite. Ce listing est
|
||||
paginé par groupe de 20.
|
||||
|
||||
Une liste déroulange précèdera le listing. Elle permet de choisir un critère de
|
||||
tri pour le listing. Les critère possibles seront, le prix, la surface, le
|
||||
nombre de pièces, et le type d'offre.
|
||||
Une liste déroulante précédera le listing. Elle permet de choisir un critère de
|
||||
tri pour le listing. Les critère possibles sont le prix, la surface, le
|
||||
nombre de pièces et le type d'offre.
|
||||
|
||||
Le listing sera triable en fonction de ces colonnnes en cliquant sur leur
|
||||
titre. Quatres états se succèderont lorsque l'on cliquera sur un entête:
|
||||
Les annonces sont présentées sous la forme d'un élément rectangulaire.
|
||||
|
||||
- tri par ordre décroissant, indiqué par un pictogramme en forme de triangle
|
||||
sur sa pointe,
|
||||
|
||||
- tri par ordre croissant, indiqué par un pictogramme en forme de triangle sur
|
||||
sa base,
|
||||
|
||||
- pas de tri.
|
||||
|
||||
Les annonces seront présentées sous la forme d'un élément rectangulaires
|
||||
reprenant en entête les informations des colonnes triables, aligné avec les
|
||||
titres de ces colonnes.
|
||||
|
||||
Un thème différencié, typiquement via une couleur de fond, permettra de
|
||||
Un thème différencié, typiquement via une couleur de fond, permet de
|
||||
distinguer le type d'offre des annonces d'un seul coup d'oeuil.
|
||||
|
||||
Le reste du bloc annonce contiendra la position géographique de l'annonce,
|
||||
|---| ville et carte zoomable |---|, les prestations présentées de manière
|
||||
compacte puis la description libre.
|
||||
Le bloc annonce contient:
|
||||
|
||||
Deux liens termineront l'affichage d'une annonce:
|
||||
- le prix,
|
||||
|
||||
- la surface,
|
||||
|
||||
- la position géographique de l'annonce |---| ville et carte,
|
||||
|
||||
- les prestations présentées de manière compacte,
|
||||
|
||||
- enfin la description libre.
|
||||
|
||||
Deux liens sous forme de bouton terminent l'affichage d'une annonce:
|
||||
|
||||
- « Voir plus de détails » pour aller sur la page de détail de l'annonce,
|
||||
|
||||
|
@ -1333,7 +1303,7 @@ Vue ``/recherche/connexion``
|
|||
Cette page affichera un bouton permettant de se connecter à l'aide de son
|
||||
compte ENT ainsi qu'un formulaire pour les titulaires d'un compte temporaire.
|
||||
|
||||
L'appuie sur le bouton de connexion par le compte ENT déclenchera l'envoi d'une
|
||||
L'appui sur le bouton de connexion par le compte ENT déclenchera l'envoi d'une
|
||||
requête d'authentification vers le serveur CAS. La vue sera aussi capable de
|
||||
consommer un ticket CAS et de vérifier ses attributs dans l'annuaire LDAP de
|
||||
l'Université Paris Dauphine.
|
||||
|
@ -1348,7 +1318,7 @@ compte, comme par exemple contacter le service du logement de l'Université
|
|||
Paris Dauphine.
|
||||
|
||||
Pour les autres types d'utilisateur il sera indiqué que le service leur est
|
||||
inacessible.
|
||||
inaccessible.
|
||||
|
||||
Vue ``/recherche/raccorder-a-mon-compte-ent``
|
||||
---------------------------------------------
|
||||
|
@ -1719,6 +1689,8 @@ logement. Ces paramètres seront:
|
|||
- la durée de publication maximum d'une annonce avant dépublication
|
||||
automatique,
|
||||
|
||||
- la durée avant la suppression d'un profil offre non validé par son créateur,
|
||||
|
||||
- le modèle d'email pour les notifications de dépublication,
|
||||
|
||||
- le modèle d'email pour les notifications de validation ou invalidation
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Django==1.4
|
||||
django-picklefield
|
||||
south
|
||||
django-mailer
|
||||
django-crispy-forms
|
||||
git+https://github.com/aljosa/django-tinymce.git@9f0f3ffe5
|
||||
django-pagination
|
|
@ -1,83 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from django.db.models import (BooleanField, Model, DecimalField, CharField,
|
||||
TextField, ManyToManyField, ForeignKey, IntegerField, DateTimeField)
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
QUADRANTS = (
|
||||
('NE', 'Nord-est'),
|
||||
('NO', 'Nord-ouest'),
|
||||
('SE', 'Sud-est'),
|
||||
('SO', 'Sud-ouest'))
|
||||
|
||||
quadrants_map = dict(QUADRANTS)
|
||||
|
||||
class TypeDePrix(Model):
|
||||
nom = CharField(max_length=128)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class ProfilRecherche(User):
|
||||
ldap_dn = CharField(max_length=256, blank=True)
|
||||
validated = BooleanField(blank=True)
|
||||
saved_announces = ManyToManyField('Announce')
|
||||
reduced_price = BooleanField(blank=True)
|
||||
free_rent = BooleanField(blank=True)
|
||||
saved_searches = ManyToManyField('SavedSearch')
|
||||
housing_type_access = ManyToManyField(HousingType, blank=True)
|
||||
|
||||
class SavedSearch(Model):
|
||||
maximum_price = DecimalField(max_digits=6,decimal_places=2)
|
||||
intra_muros = BooleanField(blank=True)
|
||||
quadrant = CharField(max_length=2, choices=QUADRANTS)
|
||||
|
||||
class ProfilOffre(User):
|
||||
validated = BooleanField(blank=True)
|
||||
priviledged = BooleanField(blank=True)
|
||||
reduced_price_annouces = BooleanField(blank=True)
|
||||
want_rush_months_email = BooleanField(blank=True)
|
||||
|
||||
class ProfileAdmin(User):
|
||||
pass
|
||||
|
||||
|
||||
class HousingFeatureGroup(Model):
|
||||
name = CharField(max_length=32)
|
||||
order = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class HousingFeature(Model):
|
||||
feature_group = ForeignKey('HousingFeatureGroup', max_length=32)
|
||||
name = CharField(max_length=32)
|
||||
order = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Announce(Model):
|
||||
active = BooleanField(blank=True)
|
||||
valide = BooleanField(blank=True)
|
||||
proprietaire = ForeignKey(ProfilOffre)
|
||||
|
||||
prix = DecimalField(max_digits=6,decimal_places=2)
|
||||
surface = DecimalField(max_digits=4, decimal_places=2)
|
||||
intra_muros = BooleanField(blank=True)
|
||||
quadrant = CharField(max_length=2, choices=QUADRANTS)
|
||||
housing_type = ForeignKey(HousingType)
|
||||
surface = IntegerField()
|
||||
features = ManyToManyField(HousingFeature)
|
||||
description = TextField(blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
if self.intra_muros:
|
||||
desc = u'%s € Paris '
|
||||
else:
|
||||
desc = u'%s € Paris et alentours '
|
||||
desc = desc % self.price_by_month
|
||||
desc += quadrants_map[self.quadrant]
|
||||
desc += ' ' + ', '.join(map(unicode, self.features.all()))
|
||||
return desc
|
||||
|
||||
# Create your models here.
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
import models
|
||||
|
||||
class ProfilAdmin(UserAdmin):
|
||||
fieldsets = None
|
||||
|
||||
admin.site.register(models.HousingType)
|
||||
admin.site.register(models.ProfilRecherche, ProfilAdmin)
|
||||
admin.site.register(models.ProfilOffre, ProfilAdmin)
|
||||
admin.site.register(models.SavedSearch)
|
||||
admin.site.register(models.HousingFeatureGroup)
|
||||
admin.site.register(models.HousingFeature)
|
||||
admin.site.register(models.Announce)
|
|
@ -1,83 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from django.db.models import (BooleanField, Model, DecimalField, CharField,
|
||||
TextField, ManyToManyField, ForeignKey, IntegerField, DateTimeField)
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
QUADRANTS = (
|
||||
('NE', 'Nord-est'),
|
||||
('NO', 'Nord-ouest'),
|
||||
('SE', 'Sud-est'),
|
||||
('SO', 'Sud-ouest'))
|
||||
|
||||
quadrants_map = dict(QUADRANTS)
|
||||
|
||||
class TypeDePrix(Model):
|
||||
nom = CharField(max_length=128)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class ProfilRecherche(User):
|
||||
ldap_dn = CharField(max_length=256, blank=True)
|
||||
validated = BooleanField(blank=True)
|
||||
saved_announces = ManyToManyField('Announce')
|
||||
reduced_price = BooleanField(blank=True)
|
||||
free_rent = BooleanField(blank=True)
|
||||
saved_searches = ManyToManyField('SavedSearch')
|
||||
housing_type_access = ManyToManyField(HousingType, blank=True)
|
||||
|
||||
class SavedSearch(Model):
|
||||
maximum_price = DecimalField(max_digits=6,decimal_places=2)
|
||||
intra_muros = BooleanField(blank=True)
|
||||
quadrant = CharField(max_length=2, choices=QUADRANTS)
|
||||
|
||||
class ProfilOffre(User):
|
||||
validated = BooleanField(blank=True)
|
||||
priviledged = BooleanField(blank=True)
|
||||
reduced_price_annouces = BooleanField(blank=True)
|
||||
want_rush_months_email = BooleanField(blank=True)
|
||||
|
||||
class ProfileAdmin(User):
|
||||
pass
|
||||
|
||||
|
||||
class HousingFeatureGroup(Model):
|
||||
name = CharField(max_length=32)
|
||||
order = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class HousingFeature(Model):
|
||||
feature_group = ForeignKey('HousingFeatureGroup', max_length=32)
|
||||
name = CharField(max_length=32)
|
||||
order = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Announce(Model):
|
||||
active = BooleanField(blank=True)
|
||||
valide = BooleanField(blank=True)
|
||||
proprietaire = ForeignKey(ProfilOffre)
|
||||
|
||||
prix = DecimalField(max_digits=6,decimal_places=2)
|
||||
surface = DecimalField(max_digits=4, decimal_places=2)
|
||||
intra_muros = BooleanField(blank=True)
|
||||
quadrant = CharField(max_length=2, choices=QUADRANTS)
|
||||
housing_type = ForeignKey(HousingType)
|
||||
surface = IntegerField()
|
||||
features = ManyToManyField(HousingFeature)
|
||||
description = TextField(blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
if self.intra_muros:
|
||||
desc = u'%s € Paris '
|
||||
else:
|
||||
desc = u'%s € Paris et alentours '
|
||||
desc = desc % self.price_by_month
|
||||
desc += quadrants_map[self.quadrant]
|
||||
desc += ' ' + ', '.join(map(unicode, self.features.all()))
|
||||
return desc
|
||||
|
||||
# Create your models here.
|
|
@ -1,22 +0,0 @@
|
|||
{% load i18n %}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" href="{{ STATIC_URL }}css/style.css" />
|
||||
<title>{% trans "site_title" %} — {% block title %}{% endblock %}</title>
|
||||
{% block jquery_script %}
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="{{ STATIC_URL }}jquery-ui-1.8/css/ui-lightness/jquery-ui-1.8.18.custom.css"/>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}jquery-ui-1.8/js/jquery-1.7.1.min.js"></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}jquery-ui-1.8/js/jquery-ui-1.8.18.custom.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body {% block bodyargs %}{% endblock %} >
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,58 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="tabs">
|
||||
<ul id="menu">
|
||||
<li><h1><a href="#ma-recherche">Ma recherche</a></h1></li>
|
||||
<li><h1><a href="#mes-annonces">Mes annonces</a></h1></li>
|
||||
<li><h1><a href="#mes-alertes">Mes alertes</a></h1></li>
|
||||
</ul>
|
||||
<div id="ma-recherche">
|
||||
<form method="post">
|
||||
{{ form.as_p }}
|
||||
{% csrf_token %}
|
||||
<input type="submit" name="Rechercher"/>
|
||||
</form>
|
||||
{% if announces %}
|
||||
<div id="announces">
|
||||
{% for announce in announces.object_list %}
|
||||
<div id="announce-{{ announce.id }}">
|
||||
{{ announce }}
|
||||
<form method="post">
|
||||
<input type="hidden" name="announce-id" value="{{announce.id}}"/>
|
||||
<input type="submit" name="save-announce" value="Sauvegarder"/>
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if announces.has_previous %}
|
||||
<a href="?{{base_query}}page={{ announces.previous_page_number }}#ma-recherche">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ announces.number }} of {{ announces.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if announces.has_next %}
|
||||
<a href="?{{base_query}}page={{ announces.next_page_number }}#ma-recherche">next</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Aucune annonce trouvée.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="mes-annonces">
|
||||
</div>
|
||||
<div id="mes-alertes">
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$( "#tabs" ).tabs();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
|
@ -1,77 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Create your views here.
|
||||
|
||||
from django.forms import (Form, IntegerField, ChoiceField,
|
||||
MultipleChoiceField, ValidationError, CheckboxSelectMultiple, RadioSelect)
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, redirect
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
|
||||
from models import ProfilOffre, QUADRANTS, Announce
|
||||
|
||||
ZONE_GEOGRAPHIQUE = (
|
||||
('intra-muros', 'Paris'),
|
||||
('paris-et-banlieue', 'Paris et alentours'))
|
||||
|
||||
class SearchForm(Form):
|
||||
max_price = IntegerField(required=False)
|
||||
paris_ou_alentour = ChoiceField(choices=ZONE_GEOGRAPHIQUE, required=False,
|
||||
widget=RadioSelect)
|
||||
quadrant = MultipleChoiceField(choices=QUADRANTS, required=False,
|
||||
widget=CheckboxSelectMultiple)
|
||||
|
||||
class ProfilOffreAuthenticationForm(AuthenticationForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(ProfilOffreAuthenticationForm, self).clean()
|
||||
if not ProfilOffre.objects.filter(user_ptr=self.user_cache):
|
||||
self.user_cache = None
|
||||
raise ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
def recherche(request):
|
||||
if request.method == 'POST':
|
||||
if u'Rechercher' in request.POST:
|
||||
form = SearchForm(request.POST)
|
||||
if form.is_valid():
|
||||
query = '/recherche/?%s#ma-recherche' % urlencode(request.POST)
|
||||
return redirect(query)
|
||||
elif 'save-announce' in request.POST:
|
||||
try:
|
||||
announce = Announce.objects.get(id=request.POST.get('announce-id'))
|
||||
request.user.profiloffre.saved_announces.add(announce)
|
||||
return HttpResponseRedirect('')
|
||||
except Announce.DoesNotExist:
|
||||
return HttpResponseRedirect('')
|
||||
else:
|
||||
form = SearchForm(request.GET)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
announces = Announce.objects.all()
|
||||
if data.get('max_price'):
|
||||
announces = announces.filter(price_by_month__lte=data['max_price'])
|
||||
if data.get('paris_ou_alentour') == 'intra-muros':
|
||||
announces = announces.filter(intra_muros=True)
|
||||
if data.get('quadrant'):
|
||||
announces = announces.filter(quadrant__in=data['quadrant'])
|
||||
paginator = Paginator(announces, 10)
|
||||
page = request.GET.get('page', -1)
|
||||
try:
|
||||
announces = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
# If page is not an integer, deliver first page.
|
||||
announces = paginator.page(1)
|
||||
except EmptyPage:
|
||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
||||
announces = paginator.page(paginator.num_pages)
|
||||
params = request.GET.copy()
|
||||
params.pop('page', None)
|
||||
base_query = params.urlencode()
|
||||
if base_query:
|
||||
base_query = base_query + '&'
|
||||
ctx = { 'form': form, 'base_query': base_query, 'announces': announces }
|
||||
return render(request, 'dauphine/recherche.html', ctx)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
import models
|
||||
|
||||
class ProfilAdmin(UserAdmin):
|
||||
fieldsets = None
|
||||
|
||||
admin.site.register(models.HousingType)
|
||||
admin.site.register(models.ProfilRecherche, ProfilAdmin)
|
||||
admin.site.register(models.ProfilOffre, ProfilAdmin)
|
||||
admin.site.register(models.SavedSearch)
|
||||
admin.site.register(models.HousingFeatureGroup)
|
||||
admin.site.register(models.HousingFeature)
|
||||
admin.site.register(models.Announce)
|
|
@ -1,83 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from django.db.models import (BooleanField, Model, DecimalField, CharField,
|
||||
TextField, ManyToManyField, ForeignKey, IntegerField, DateTimeField)
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
QUADRANTS = (
|
||||
('NE', 'Nord-est'),
|
||||
('NO', 'Nord-ouest'),
|
||||
('SE', 'Sud-est'),
|
||||
('SO', 'Sud-ouest'))
|
||||
|
||||
quadrants_map = dict(QUADRANTS)
|
||||
|
||||
class TypeDePrix(Model):
|
||||
nom = CharField(max_length=128)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class ProfilRecherche(User):
|
||||
ldap_dn = CharField(max_length=256, blank=True)
|
||||
validated = BooleanField(blank=True)
|
||||
saved_announces = ManyToManyField('Announce')
|
||||
reduced_price = BooleanField(blank=True)
|
||||
free_rent = BooleanField(blank=True)
|
||||
saved_searches = ManyToManyField('SavedSearch')
|
||||
housing_type_access = ManyToManyField(HousingType, blank=True)
|
||||
|
||||
class SavedSearch(Model):
|
||||
maximum_price = DecimalField(max_digits=6,decimal_places=2)
|
||||
intra_muros = BooleanField(blank=True)
|
||||
quadrant = CharField(max_length=2, choices=QUADRANTS)
|
||||
|
||||
class ProfilOffre(User):
|
||||
validated = BooleanField(blank=True)
|
||||
priviledged = BooleanField(blank=True)
|
||||
reduced_price_annouces = BooleanField(blank=True)
|
||||
want_rush_months_email = BooleanField(blank=True)
|
||||
|
||||
class ProfileAdmin(User):
|
||||
pass
|
||||
|
||||
|
||||
class HousingFeatureGroup(Model):
|
||||
name = CharField(max_length=32)
|
||||
order = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class HousingFeature(Model):
|
||||
feature_group = ForeignKey('HousingFeatureGroup', max_length=32)
|
||||
name = CharField(max_length=32)
|
||||
order = IntegerField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class Announce(Model):
|
||||
active = BooleanField(blank=True)
|
||||
valide = BooleanField(blank=True)
|
||||
proprietaire = ForeignKey(ProfilOffre)
|
||||
|
||||
prix = DecimalField(max_digits=6,decimal_places=2)
|
||||
surface = DecimalField(max_digits=4, decimal_places=2)
|
||||
intra_muros = BooleanField(blank=True)
|
||||
quadrant = CharField(max_length=2, choices=QUADRANTS)
|
||||
housing_type = ForeignKey(HousingType)
|
||||
surface = IntegerField()
|
||||
features = ManyToManyField(HousingFeature)
|
||||
description = TextField(blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
if self.intra_muros:
|
||||
desc = u'%s € Paris '
|
||||
else:
|
||||
desc = u'%s € Paris et alentours '
|
||||
desc = desc % self.price_by_month
|
||||
desc += quadrants_map[self.quadrant]
|
||||
desc += ' ' + ', '.join(map(unicode, self.features.all()))
|
||||
return desc
|
||||
|
||||
# Create your models here.
|
|
@ -1,22 +0,0 @@
|
|||
{% load i18n %}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" href="{{ STATIC_URL }}css/style.css" />
|
||||
<title>{% trans "site_title" %} — {% block title %}{% endblock %}</title>
|
||||
{% block jquery_script %}
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="{{ STATIC_URL }}jquery-ui-1.8/css/ui-lightness/jquery-ui-1.8.18.custom.css"/>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}jquery-ui-1.8/js/jquery-1.7.1.min.js"></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}jquery-ui-1.8/js/jquery-ui-1.8.18.custom.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body {% block bodyargs %}{% endblock %} >
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,58 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="tabs">
|
||||
<ul id="menu">
|
||||
<li><h1><a href="#ma-recherche">Ma recherche</a></h1></li>
|
||||
<li><h1><a href="#mes-annonces">Mes annonces</a></h1></li>
|
||||
<li><h1><a href="#mes-alertes">Mes alertes</a></h1></li>
|
||||
</ul>
|
||||
<div id="ma-recherche">
|
||||
<form method="post">
|
||||
{{ form.as_p }}
|
||||
{% csrf_token %}
|
||||
<input type="submit" name="Rechercher"/>
|
||||
</form>
|
||||
{% if announces %}
|
||||
<div id="announces">
|
||||
{% for announce in announces.object_list %}
|
||||
<div id="announce-{{ announce.id }}">
|
||||
{{ announce }}
|
||||
<form method="post">
|
||||
<input type="hidden" name="announce-id" value="{{announce.id}}"/>
|
||||
<input type="submit" name="save-announce" value="Sauvegarder"/>
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if announces.has_previous %}
|
||||
<a href="?{{base_query}}page={{ announces.previous_page_number }}#ma-recherche">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ announces.number }} of {{ announces.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if announces.has_next %}
|
||||
<a href="?{{base_query}}page={{ announces.next_page_number }}#ma-recherche">next</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Aucune annonce trouvée.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="mes-annonces">
|
||||
</div>
|
||||
<div id="mes-alertes">
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$( "#tabs" ).tabs();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
|
@ -1,77 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Create your views here.
|
||||
|
||||
from django.forms import (Form, IntegerField, ChoiceField,
|
||||
MultipleChoiceField, ValidationError, CheckboxSelectMultiple, RadioSelect)
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, redirect
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
|
||||
from models import ProfilOffre, QUADRANTS, Announce
|
||||
|
||||
ZONE_GEOGRAPHIQUE = (
|
||||
('intra-muros', 'Paris'),
|
||||
('paris-et-banlieue', 'Paris et alentours'))
|
||||
|
||||
class SearchForm(Form):
|
||||
max_price = IntegerField(required=False)
|
||||
paris_ou_alentour = ChoiceField(choices=ZONE_GEOGRAPHIQUE, required=False,
|
||||
widget=RadioSelect)
|
||||
quadrant = MultipleChoiceField(choices=QUADRANTS, required=False,
|
||||
widget=CheckboxSelectMultiple)
|
||||
|
||||
class ProfilOffreAuthenticationForm(AuthenticationForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(ProfilOffreAuthenticationForm, self).clean()
|
||||
if not ProfilOffre.objects.filter(user_ptr=self.user_cache):
|
||||
self.user_cache = None
|
||||
raise ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
def recherche(request):
|
||||
if request.method == 'POST':
|
||||
if u'Rechercher' in request.POST:
|
||||
form = SearchForm(request.POST)
|
||||
if form.is_valid():
|
||||
query = '/recherche/?%s#ma-recherche' % urlencode(request.POST)
|
||||
return redirect(query)
|
||||
elif 'save-announce' in request.POST:
|
||||
try:
|
||||
announce = Announce.objects.get(id=request.POST.get('announce-id'))
|
||||
request.user.profiloffre.saved_announces.add(announce)
|
||||
return HttpResponseRedirect('')
|
||||
except Announce.DoesNotExist:
|
||||
return HttpResponseRedirect('')
|
||||
else:
|
||||
form = SearchForm(request.GET)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
announces = Announce.objects.all()
|
||||
if data.get('max_price'):
|
||||
announces = announces.filter(price_by_month__lte=data['max_price'])
|
||||
if data.get('paris_ou_alentour') == 'intra-muros':
|
||||
announces = announces.filter(intra_muros=True)
|
||||
if data.get('quadrant'):
|
||||
announces = announces.filter(quadrant__in=data['quadrant'])
|
||||
paginator = Paginator(announces, 10)
|
||||
page = request.GET.get('page', -1)
|
||||
try:
|
||||
announces = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
# If page is not an integer, deliver first page.
|
||||
announces = paginator.page(1)
|
||||
except EmptyPage:
|
||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
||||
announces = paginator.page(paginator.num_pages)
|
||||
params = request.GET.copy()
|
||||
params.pop('page', None)
|
||||
base_query = params.urlencode()
|
||||
if base_query:
|
||||
base_query = base_query + '&'
|
||||
ctx = { 'form': form, 'base_query': base_query, 'announces': announces }
|
||||
return render(request, 'dauphine/recherche.html', ctx)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
import models
|
||||
|
||||
class ProfilAdmin(UserAdmin):
|
||||
fieldsets = None
|
||||
|
||||
admin.site.register(models.HousingType)
|
||||
admin.site.register(models.ProfilRecherche, ProfilAdmin)
|
||||
admin.site.register(models.ProfilOffre, ProfilAdmin)
|
||||
admin.site.register(models.SavedSearch)
|
||||
admin.site.register(models.HousingFeatureGroup)
|
||||
admin.site.register(models.HousingFeature)
|
||||
admin.site.register(models.Announce)
|
|
@ -1,22 +0,0 @@
|
|||
{% load i18n %}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" href="{{ STATIC_URL }}css/style.css" />
|
||||
<title>{% trans "site_title" %} — {% block title %}{% endblock %}</title>
|
||||
{% block jquery_script %}
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="{{ STATIC_URL }}jquery-ui-1.8/css/ui-lightness/jquery-ui-1.8.18.custom.css"/>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}jquery-ui-1.8/js/jquery-1.7.1.min.js"></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}jquery-ui-1.8/js/jquery-ui-1.8.18.custom.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body {% block bodyargs %}{% endblock %} >
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,58 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="tabs">
|
||||
<ul id="menu">
|
||||
<li><h1><a href="#ma-recherche">Ma recherche</a></h1></li>
|
||||
<li><h1><a href="#mes-annonces">Mes annonces</a></h1></li>
|
||||
<li><h1><a href="#mes-alertes">Mes alertes</a></h1></li>
|
||||
</ul>
|
||||
<div id="ma-recherche">
|
||||
<form method="post">
|
||||
{{ form.as_p }}
|
||||
{% csrf_token %}
|
||||
<input type="submit" name="Rechercher"/>
|
||||
</form>
|
||||
{% if announces %}
|
||||
<div id="announces">
|
||||
{% for announce in announces.object_list %}
|
||||
<div id="announce-{{ announce.id }}">
|
||||
{{ announce }}
|
||||
<form method="post">
|
||||
<input type="hidden" name="announce-id" value="{{announce.id}}"/>
|
||||
<input type="submit" name="save-announce" value="Sauvegarder"/>
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if announces.has_previous %}
|
||||
<a href="?{{base_query}}page={{ announces.previous_page_number }}#ma-recherche">previous</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ announces.number }} of {{ announces.paginator.num_pages }}.
|
||||
</span>
|
||||
|
||||
{% if announces.has_next %}
|
||||
<a href="?{{base_query}}page={{ announces.next_page_number }}#ma-recherche">next</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Aucune annonce trouvée.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="mes-annonces">
|
||||
</div>
|
||||
<div id="mes-alertes">
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$( "#tabs" ).tabs();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,16 +0,0 @@
|
|||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
|
@ -1,77 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Create your views here.
|
||||
|
||||
from django.forms import (Form, IntegerField, ChoiceField,
|
||||
MultipleChoiceField, ValidationError, CheckboxSelectMultiple, RadioSelect)
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.http import urlencode
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, redirect
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
|
||||
from models import ProfilOffre, QUADRANTS, Announce
|
||||
|
||||
ZONE_GEOGRAPHIQUE = (
|
||||
('intra-muros', 'Paris'),
|
||||
('paris-et-banlieue', 'Paris et alentours'))
|
||||
|
||||
class SearchForm(Form):
|
||||
max_price = IntegerField(required=False)
|
||||
paris_ou_alentour = ChoiceField(choices=ZONE_GEOGRAPHIQUE, required=False,
|
||||
widget=RadioSelect)
|
||||
quadrant = MultipleChoiceField(choices=QUADRANTS, required=False,
|
||||
widget=CheckboxSelectMultiple)
|
||||
|
||||
class ProfilOffreAuthenticationForm(AuthenticationForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(ProfilOffreAuthenticationForm, self).clean()
|
||||
if not ProfilOffre.objects.filter(user_ptr=self.user_cache):
|
||||
self.user_cache = None
|
||||
raise ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive."))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
def recherche(request):
|
||||
if request.method == 'POST':
|
||||
if u'Rechercher' in request.POST:
|
||||
form = SearchForm(request.POST)
|
||||
if form.is_valid():
|
||||
query = '/recherche/?%s#ma-recherche' % urlencode(request.POST)
|
||||
return redirect(query)
|
||||
elif 'save-announce' in request.POST:
|
||||
try:
|
||||
announce = Announce.objects.get(id=request.POST.get('announce-id'))
|
||||
request.user.profiloffre.saved_announces.add(announce)
|
||||
return HttpResponseRedirect('')
|
||||
except Announce.DoesNotExist:
|
||||
return HttpResponseRedirect('')
|
||||
else:
|
||||
form = SearchForm(request.GET)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
announces = Announce.objects.all()
|
||||
if data.get('max_price'):
|
||||
announces = announces.filter(price_by_month__lte=data['max_price'])
|
||||
if data.get('paris_ou_alentour') == 'intra-muros':
|
||||
announces = announces.filter(intra_muros=True)
|
||||
if data.get('quadrant'):
|
||||
announces = announces.filter(quadrant__in=data['quadrant'])
|
||||
paginator = Paginator(announces, 10)
|
||||
page = request.GET.get('page', -1)
|
||||
try:
|
||||
announces = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
# If page is not an integer, deliver first page.
|
||||
announces = paginator.page(1)
|
||||
except EmptyPage:
|
||||
# If page is out of range (e.g. 9999), deliver last page of results.
|
||||
announces = paginator.page(paginator.num_pages)
|
||||
params = request.GET.copy()
|
||||
params.pop('page', None)
|
||||
base_query = params.urlencode()
|
||||
if base_query:
|
||||
base_query = base_query + '&'
|
||||
ctx = { 'form': form, 'base_query': base_query, 'announces': announces }
|
||||
return render(request, 'dauphine/recherche.html', ctx)
|
||||
|
Reference in New Issue