définition de l'application socle, modèle de donnée et page d'administration pour les développeurs

This commit is contained in:
Benjamin Dauvergne 2012-04-10 10:22:24 +02:00
parent 20c932e355
commit 5c97d6c0d0
75 changed files with 1134 additions and 1123 deletions

17
appli_project/admin.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url admin:index %}">{% trans 'Home' %}</a>
&rsaquo; Paramètres
</div>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="Sauver"/>
</form>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "appli_socle/base.html" %}
{% block body %}
{{ form.as_p }}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "appli_socle/base.html" %}
{% block body %}
{{ form.as_p }}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "appli_socle/base.html" %}
{% block body %}
{{ form.as_p }}
{% end block %}

View File

@ -0,0 +1,4 @@
<html>
<head></head>
<body>{% block body %}{% endblock %}</body>
</html>

View File

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

View File

@ -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();
}
}

View File

@ -0,0 +1,2 @@
{% extends "gis/admin/openlayers.html" %}
{% block openlayers %}{% include "gis/admin/osm.js" %}{% endblock %}

View File

@ -0,0 +1,2 @@
{% extends "gis/admin/openlayers.js" %}
{% block base_layer %}new OpenLayers.Layer.OSM("OpenStreetMap (Mapnik)");{% endblock %}

View File

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

View File

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

View File

@ -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 %} &rsaquo; {{ 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>

View File

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

View File

@ -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>&nbsp;</td>
{% endif %}
{% if model.admin_url %}
<td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
{% else %}
<td>&nbsp;</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 %}

View File

@ -0,0 +1,10 @@
<html>
<head>
</head>
<body>
{% cms_toolbar %}
{% placeholder base_content %}
{% block base_content%}{% endblock %}
{% render_block "js" %}
</body>
</html>

View File

@ -0,0 +1,11 @@
<html>
<head>
<title>{{ flatpage.title }}</title>
</head>
<body>
<h1>{{ flatpage.h1 }}</h1>
<div>
{{ flatpage.content }}
</div>
</body>
</html>

2
documentation/Makefile Normal file
View File

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

View File

@ -0,0 +1,5 @@
[latex2e writer]
documentclass: article
documentoptions: 10pt,a4paper,dvipdfmx,times
output-encoding: utf-8
stylesheet: docutils.tex

View File

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

3
documentation/map.ini Normal file
View File

@ -0,0 +1,3 @@
[Formats]
blockquote-bulletlist: List 1
bulletlist: List 1

View File

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

7
requirements Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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