Merge branch 'recherche' into 'master'

Recherche des stages

See merge request !2
This commit is contained in:
Robin Champenois 2017-06-20 19:19:13 +02:00
commit d7b611e1d4
21 changed files with 1590 additions and 452 deletions

View file

@ -39,6 +39,24 @@ Vous pouvez alors lancez le serveur de développement
python manage.py runserver python manage.py runserver
## Configuration de la recherche
Il faut installer elasticsearch 5.*. C'est compliqué. Mais en suivant https://www.elastic.co/guide/en/elasticsearch/reference/5.4/deb.html ça va.
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
sudo apt-get install apt-transport-https
echo "deb https://artifacts.elastic.co/packages/5.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-5.x.list
sudo apt-get update && sudo apt-get install elasticsearch
sudo systemctl daemon-reload
sudo systemctl enable elasticsearch.service
sudo systemctl start elasticsearch.service
Et puis, de retour dans le virtualenv python
python manage.py search_index --rebuild
Si des erreurs s'affichent, il y a une cachuète dans le beurre.
## Changer le CSS ## Changer le CSS

72
avisstage/documents.py Normal file
View file

@ -0,0 +1,72 @@
from django_elasticsearch_dsl import DocType, Index, fields
from elasticsearch_dsl import analyzer, token_filter, tokenizer
from .models import Stage, AvisStage, AvisLieu
from .statics import PAYS_OPTIONS
PAYS_DICT = dict(PAYS_OPTIONS)
stage = Index('stages')
stage.settings(
number_of_shards=1,
number_of_replicas=0
)
text_analyzer = analyzer(
'default',
tokenizer="standard",
filter=['lowercase', 'standard', 'asciifolding',
token_filter("frstop", type="stop", stopwords="_french_"),
token_filter("frsnow", type="snowball", language="French")])
stage.analyzer(text_analyzer)
@stage.doc_type
class StageDocument(DocType):
lieux = fields.ObjectField(properties={
'nom': fields.StringField(),
'ville': fields.StringField(),
'pays': fields.StringField(),
})
auteur = fields.ObjectField(properties={
'nom': fields.StringField(),
})
thematiques = fields.StringField()
matieres = fields.StringField()
class Meta:
model = Stage
fields = [
'sujet',
'encadrants',
'type_stage',
'niveau_scol',
'structure',
'date_debut',
'date_fin'
]
def prepare_thematiques(self, instance):
return ", ".join(instance.thematiques.all().values_list("name", flat=True))
def prepare_matieres(self, instance):
return ", ".join(instance.matieres.all().values_list("nom", flat=True))
def prepare_niveau_scol(self, instance):
return instance.get_niveau_scol_display()
def prepare_type_stage(self, instance):
return instance.type_stage_fancy
def prepare_date_fin(self, instance):
return instance.date_fin.year
def prepare_date_debut(self, instance):
return instance.date_debut.year
# Hook pour l'affichage des noms de pays
def prepare(self, instance):
data = super(StageDocument, self).prepare(instance)
for lieu in data['lieux']:
lieu['pays'] = PAYS_DICT[lieu['pays']]
return data

View file

@ -0,0 +1,202 @@
section.content.recherche {
form.recherche {
.generale {
display: inline-block;
text-align: right;
position: relative;
left: 50%;
transform: translateX(-50%);
width: 500px;
max-width: 100%;
white-space: nowrap;
span {
display:flex;
}
input[type="text"] {
max-width: 500px;
padding: 10px;
border: 1px solid $fond;
margin:0 5px;
}
input {
display: inline;
}
}
.avancee {
background: #fff;
display: none;
padding: 15px;
margin-bottom: 15px;
&.expanded {
display:block;
}
.help_text {
font-style: italic;
font-size: 0.9em;
}
ul {
margin: 0 -5px;
display: flexbox;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
li {
flex-grow: 1;
width: 22%;
min-width: 150px;
margin: 5px 0;
padding: 0 10px;
label {
font-weight: bold;
font-size: 0.9em;
}
input[type="text"], input[type='number'], select {
display: block;
min-width: 150px;
display: inline-block;
width: 100%;
font-size: 0.9em;
background-color: #f8f8f8;
}
&.btnsubmit {
text-align:right;
}
&.field__sujet, &.field__contexte {
width:45%;
min-width: 300px;
}
}
}
}
}
.recherche-carte, .recherche-liste {
position:relative;
}
.numresults {
font-size: 0.9em;
font-weight: bold;
}
&.vue-hybride #voir_hybride,
&.vue-carte #voir_carte,
&.vue-liste #voir_liste {
background: lighten($fond, 30%);
color: #000;
}
.vue-options {
text-align: center;
ul {
display: inline-block;
border-radius: 5px;
overflow: hidden;
li {
display: inline-block;
background: #fff;
padding: 10px;
a {
display: block;
padding: 10px;
margin: -10px;
}
}
}
}
.recherche-carte .vue-options {
position: absolute;
z-index: 3;
top: 15px;
left: 60px;
}
&.vue-hybride, &.vue-carte {
width: 100%;
min-width: unset;
max-width: unset;
min-height: unset;
max-height: unset;
height: 90vh;
height: calc(100vh - 30px);
padding: 0;
margin: 0;
.recherche-carte, .recherche-liste {
height: 100%;
}
.recherche-liste {
overflow-y: auto;
padding: 15px;
}
}
&.vue-liste .recherche-carte,
&.vue-carte .recherche-liste {
display: none;
}
&.vue-carte .recherche-carte,
&.vue-liste .recherche-liste {
display: block;
width: 100%;
}
&.vue-hybride {
display: flex;
.recherche-liste {
width: 40vw;
min-width: 500px;
max-width: 800px;
.dates {
display:none;
}
ul.infos li {
font-size: 0.8em;
font-weight: normal;
&.year {
display: inline-block;
}
}
}
.recherche-carte {
flex-shrink: 1;
width: 100%;
.vue-options {
display:none;
}
}
}
#carte {
width:100%;
height:100%;
}
.recherche-liste.recherche-details {
display: none !important;
}
&.vue-hybride.vue-details .recherche-liste {
display: none;
&.recherche-details {
display: block !important;
}
}
}

View file

@ -0,0 +1,275 @@
article.stage .avis, div.tinymce {
ul, ol {
list-style: unset;
padding-left: 20px;
}
}
article.stage {
font-weight: normal;
font-family: $paragraphfont;
h2 {
background: desaturate(lighten($jaune, 15%), 40%);
color: #000;
padding: 10px 20px;
margin: -20px;
margin-bottom: 5px;
text-shadow: -3px 3px 0 rgba(#fff, 0.3);
}
h3 {
//border-bottom: 2px solid desaturate($compl, 40%);
margin-top: 30px;
padding: 5px;
padding-left: 0px;
color: darken($vert, 20);
text-shadow: -3px 3px 0 rgba($vert, 0.1);
//margin-right: 25px;
}
section {
&.avis section {
max-width: 700px;
background: #fff;
padding: 14px;
margin: 30px auto;
}
&:first-child {
margin-top: 0;
h3 {
margin-top: 0;
}
}
#stage-map {
height: 300px;
width: 100%;
}
&.misc {
padding-top: 0;
margin-bottom: 30px;
.misc-content {
&.withmap {
display:table;
width: 100%;
direction: rtl;
& > div {
direction: ltr;
display:table-cell;
vertical-align: top;
}
.map {
min-width: 250px;
width: 30%;
min-height: 300px;
vertical-align: middle;
}
.desc {
padding: 5px;
padding-left: 15px;
}
}
}
}
.chapo, .avis-texte {
margin-bottom: 15px;
background: #fff;
padding: 0 20px;
}
.avis-texte {
font-size: $textfontsize - 1px;
}
.chapo {
font-size: 1.1em;
//font-family: $textfont;
font-weight: 700;
padding-left: 0px;
}
.avis-texte {
//border-left: 1px solid #ccc;
padding-left: 15px;
}
.plusmoins {
max-width: 600px;
margin: 15px auto;
margin-top: 40px;
& > div {
display: table;
width: 100%;
&:before {
content: "&nbsp";
width: 90px;
font-size: 1.8em;
font-weight: bold;
text-align: right;
padding-right: 12px;
}
& > *, &:before {
display:table-cell;
}
& > div {
padding: 15px;
color: #fff;
h4 {
font-weight: normal;
margin-left: -5px;
font-size: 0.9em;
opacity: 0.9;
}
p {
font-weight: bold;
margin: 2px;
}
}
}
.plus {
& > div {
background: darken($vert, 5%);
}
&:before {
content: "Les +";
vertical-align: bottom;
color: darken($vert, 10%);
}
}
.moins {
& > div {
background: darken($rouge, 5%);
}
&:before {
content: "Les ";
vertical-align: top;
color: darken($rouge, 10%);
}
}
}
}
// Sommaire sur le côté
.section-wrapper {
display: table;
margin-left: -15px;
width: 100%;
.toc-wrapper, & > section {
display: table-cell;
vertical-align: top;
}
.toc-wrapper {
max-width: 230px;
width: 25%;
padding: 5px;
padding-right: 25px;
}
.toc {
font-family: $textfont;
position: -webkit-sticky;
position: sticky;
top: 12px;
margin-left: -40px;
background: #fff;
padding: 5px;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2);
ul {
list-style: none;
padding: 0;
}
a {
display: block;
color: inherit;
font-weight: normal;
border-radius: 7px;
padding: 5px;
line-height: 1;
&:hover {
color: $fond;
}
}
.toc-h1 a {
font-weight: 900;
}
.toc-h2 {
margin-top: 10px;
font-weight: 400;
}
.toc-h3 a {
font-weight: 300;
}
.toc-active a {
color: darken($vert, 20);
}
}
}
}
.misc-hdr {
display:table;
width: 100%;
border-bottom: 1px solid #ccc;
& > * {
display: table-cell;
vertical-align: bottom;
}
h1, h3 {
width: 100%;
padding-right: 5px;
}
.dates {
width: 50px;
background-color: darken($rouge, 20);
color: #fff;
padding: 3px 10px;
border-radius: 5px 5px 0 0;
font-family: $textfont;
font-size: 0.8em;
text-align: center;
span {
display:block;
}
.year {
font-size: 1.8em;
}
}
}
// Bandeau pour passer en public ou éditer
.edit-box {
background: #eee;
margin: 10px;
padding: 10px 20px;
text-align: center;
&.public {
background: lighten($vert, 40%);
border: 1px solid $vert * 0.7;
}
&.prive {
background: lighten($rouge, 40%);
border: 1px solid $rouge * 0.7;
}
}

View file

@ -62,14 +62,14 @@ a {
#feedback-button { #feedback-button {
position:fixed; position:fixed;
left:0; right:0;
top:30%; top:30%;
color:#fff; color:#fff;
z-index:4; z-index:4;
background: #000; background: #000;
padding: 14px; padding: 14px;
transform: rotateZ(90deg); transform: rotateZ(-90deg);
transform-origin: bottom left; transform-origin: bottom right;
} }
// Cartes // Cartes
@ -190,15 +190,19 @@ header {
.stage-liste { .stage-liste {
li { li {
display: block; display: block;
position: relative;
&.date-maj { &.date-maj {
font-weight: 300; font-weight: 300;
font-size: 0.9em; font-size: 0.9em;
padding: 3px 0; padding: 3px 0;
font-style: italic;
} }
&.stage { &.stage {
padding: 10px; padding: 10px;
background: #fff; background: #fff;
margin: 10px; margin: 10px;
border-left: 5px solid $compl;
h3 { h3 {
font-size: 1.4em; font-size: 1.4em;
@ -217,9 +221,6 @@ header {
} }
} }
} }
ul.infos {
display:inline;
}
} }
.misc-hdr { .misc-hdr {
margin-bottom: 10px; margin-bottom: 10px;
@ -234,12 +235,26 @@ header {
} }
} }
} }
a.hoverlink {
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
} }
} }
ul.infos { ul.infos {
margin: 0 -3px; margin: 0 -3px;
padding: 0; padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100;
li { li {
display: inline-block; display: inline-block;
@ -248,19 +263,32 @@ ul.infos {
font-weight: bold; font-weight: bold;
font-size: 0.9em; font-size: 0.9em;
border-radius: 4px; border-radius: 4px;
flex-grow: 1;
text-align: center;
background-color: #ddd;
&.thematique { &.thematique {
color: darken($vert, 20); color: darken($vert, 25);
background-color: lighten($vert, 20); background-color: lighten($vert, 25);
} }
&.matiere { &.matiere {
color: darken($fond, 20); color: darken($fond, 30);
background-color: lighten($fond, 20); background-color: lighten($fond, 25);
} }
&.lieu { &.lieu {
color: darken($rouge, 20); color: darken($rouge, 25);
background-color: lighten($rouge, 20); background-color: lighten($rouge, 25);
} }
&.year {
background-color: darken($rouge, 20);
color: #fff;
display: none;
}
}
&:after {
content: "";
flex: 1000;
} }
} }
@ -268,279 +296,7 @@ ul.infos {
// //
// Détail d'un stage // Détail d'un stage
article.stage .avis, div.tinymce { @import "_stage_detail.scss";
ul, ol {
list-style: unset;
padding-left: 20px;
}
}
article.stage {
font-weight: normal;
font-family: $paragraphfont;
h2 {
background: desaturate(lighten($jaune, 15%), 40%);
color: #000;
padding: 10px 20px;
margin: -20px;
margin-bottom: 5px;
text-shadow: -3px 3px 0 rgba(#fff, 0.3);
}
h3 {
//border-bottom: 2px solid desaturate($compl, 40%);
margin-top: 30px;
padding: 5px;
padding-left: 0px;
color: darken($vert, 20);
text-shadow: -3px 3px 0 rgba($vert, 0.1);
//margin-right: 25px;
}
section {
&.avis section {
max-width: 700px;
background: #fff;
padding: 14px;
margin: 30px auto;
}
&:first-child {
margin-top: 0;
h3 {
margin-top: 0;
}
}
#stage-map {
height: 300px;
width: 100%;
}
&.misc {
padding-top: 0;
margin-bottom: 30px;
.misc-content {
&.withmap {
display:table;
width: 100%;
direction: rtl;
& > div {
direction: ltr;
display:table-cell;
vertical-align: top;
}
.map {
min-width: 250px;
width: 30%;
min-height: 300px;
vertical-align: middle;
}
.desc {
padding: 5px;
padding-left: 15px;
}
}
}
}
.chapo, .avis-texte {
margin-bottom: 15px;
background: #fff;
padding: 0 20px;
}
.avis-texte {
font-size: $textfontsize - 1px;
}
.chapo {
font-size: 1.1em;
//font-family: $textfont;
font-weight: 700;
padding-left: 0px;
}
.avis-texte {
//border-left: 1px solid #ccc;
padding-left: 15px;
}
.plusmoins {
max-width: 600px;
margin: 15px auto;
margin-top: 40px;
& > div {
display: table;
width: 100%;
&:before {
content: "&nbsp";
width: 90px;
font-size: 1.8em;
font-weight: bold;
text-align: right;
padding-right: 12px;
}
& > *, &:before {
display:table-cell;
}
& > div {
padding: 15px;
color: #fff;
h4 {
font-weight: normal;
margin-left: -5px;
font-size: 0.9em;
opacity: 0.9;
}
p {
font-weight: bold;
margin: 2px;
}
}
}
.plus {
& > div {
background: darken($vert, 5%);
}
&:before {
content: "Les +";
vertical-align: bottom;
color: darken($vert, 10%);
}
}
.moins {
& > div {
background: darken($rouge, 5%);
}
&:before {
content: "Les ";
vertical-align: top;
color: darken($rouge, 10%);
}
}
}
}
// Sommaire sur le côté
.section-wrapper {
display: table;
margin-left: -15px;
width: 100%;
.toc-wrapper, & > section {
display: table-cell;
vertical-align: top;
}
.toc-wrapper {
max-width: 230px;
width: 25%;
padding: 5px;
padding-right: 25px;
}
.toc {
font-family: $textfont;
position: -webkit-sticky;
position: sticky;
top: 12px;
margin-left: -40px;
background: #fff;
padding: 5px;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2);
ul {
list-style: none;
padding: 0;
}
a {
display: block;
color: inherit;
font-weight: normal;
border-radius: 7px;
padding: 5px;
line-height: 1;
&:hover {
color: $fond;
}
}
.toc-h1 a {
font-weight: 900;
}
.toc-h2 {
margin-top: 10px;
font-weight: 400;
}
.toc-h3 a {
font-weight: 300;
}
.toc-active a {
color: darken($vert, 20);
}
}
}
}
.misc-hdr {
display:table;
width: 100%;
border-bottom: 1px solid #ccc;
& > * {
display: table-cell;
vertical-align: bottom;
}
h1, h3 {
width: 100%;
padding-right: 5px;
}
.dates {
width: 50px;
background-color: darken($rouge, 20);
color: #fff;
padding: 3px 10px;
border-radius: 5px 5px 0 0;
font-family: $textfont;
font-size: 0.8em;
text-align: center;
span {
display:block;
}
.year {
font-size: 1.8em;
}
}
}
// Bandeau pour passer en public ou éditer
.edit-box {
background: #eee;
margin: 10px;
padding: 10px 20px;
text-align: center;
&.public {
background: lighten($vert, 40%);
border: 1px solid $vert * 0.7;
}
&.prive {
background: lighten($rouge, 40%);
border: 1px solid $rouge * 0.7;
}
}
// //
// //
@ -565,7 +321,7 @@ input, textarea, select, div.tinymce, option, optgroup:before {
input[type='text'], input[type='password'], input[type='text'], input[type='password'],
input[type='email'], textarea, select { input[type='email'], input[type='number'], textarea, select {
border:none; border:none;
border-bottom: 1px solid $fond; border-bottom: 1px solid $fond;
width: 100%; width: 100%;
@ -579,6 +335,12 @@ select {
width: auto; width: auto;
margin-right: 5px; margin-right: 5px;
cursor: pointer; cursor: pointer;
padding: 0;
padding-right: 30px;
background: url($staticurl + "images/choix.svg") no-repeat;
background-position: right center;
background-color: #fff;
background-size: contain;
option { option {
padding: 3px; padding: 3px;
@ -1034,4 +796,14 @@ article.promo {
} }
} }
//
//
// Recherche
@import "_recherche.scss";
//
//
// Responsive
@import "_responsive.scss"; @import "_responsive.scss";

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="40"
height="40"
viewBox="0 0 40.000001 40.000001"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="choix.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="-158.22667"
inkscape:cy="-53.805144"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1366"
inkscape:window-height="720"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1012.3622)">
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-opacity:1"
id="path3338"
sodipodi:sides="3"
sodipodi:cx="154.28571"
sodipodi:cy="312.36221"
sodipodi:r1="49.897854"
sodipodi:r2="24.948927"
sodipodi:arg1="-0.52359878"
sodipodi:arg2="0.52359878"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 197.49851,287.41329 -43.2128,74.84678 -43.21281,-74.84678 z"
inkscape:transform-center-y="1.3333278"
transform="matrix(0.18513029,0,0,0.10688502,-8.5629579,997.64203)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,191 @@
function InterfaceRecherche(STATIC_ROOT, API_LIEU, lieux) {
var interface_mode, main_container;
var lieux_map = {}, lieux_list = [], stages_map = {}, lieux_db = {};
var stages_data = {};
var marqueurs = L.markerClusterGroup();
var marqueurs_db = {};
var changevue;
var details_lock = false;
var map, lieux_survol;
// Initialisation globale de l'interface et du switch liste / hybride / carte
// TODO se souvenir des préférences d'affichage
function initInterface() {
main_container = $(".content.recherche");
if (main_container.hasClass("vue-liste")) {
interface_mode = "liste";
} else if (main_container.hasClass("vue-carte")) {
interface_mode = "carte";
} else {
interface_mode = "hybride";
}
changevue = $(".vue-options");
$.each(changevue.find('a'), function(i, item) {
$(item).on('click', btnChangeInterface);
});
$.each(lieux, function(i, item) {
var stage_id = item[0];
var lieu_id = item[1];
if (lieux_map[lieu_id] === undefined) {
lieux_map[lieu_id] = [];
lieux_list.push(lieu_id);
}
lieux_map[lieu_id].push(stage_id);
if (stages_map[stage_id] === undefined) {
stages_map[stage_id] = [];
}
stages_map[stage_id].push(lieu_id);
});
changeInterface(interface_mode);
}
// Changement d'affichage : mise à jour des classes et démarrage de la carte si nécessaire
function btnChangeInterface() {
changeInterface(this.id.split('_')[1]);
}
function changeInterface(mode) {
interface_mode = mode;
$(".content.recherche").removeClass("vue-carte vue-hybride vue-liste")
.addClass("vue-"+mode);
if (mode=="hybride" || mode=="carte") {
initCarte();
map.invalidateSize();
}
}
// Carte
function initCarte() {
if (map !== undefined) return;
map = L.map("carte").panTo([30, 15]).setZoom(1);
var layer = new L.TileLayer("https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}", {attribution: 'Map tiles by <a href="http://korona.geog.uni-heidelberg.de/">GIScience Heidelberg</a>'});
map.addLayer(layer);
$.getJSON(API_LIEU + "set/"+lieux_list.join(';')+"/?format=json", onLoadLieux);
}
function makeIcon(couleur, scale){
if (scale===undefined) scale = 1;
return L.icon({
iconUrl: STATIC_ROOT + 'images/marker-'+couleur+'.png',
iconSize: [36 * scale, 46 * scale],
iconAnchor: [18 * scale, 45 * scale],
popupAnchor: [0, -48]
})
}
var greenIcon = makeIcon('red');
var blueIcon = makeIcon('blue', 1.2);
// Chargeùent des infos
function onLoadLieux(data){
console.log(data);
var lieux = data.objects;
$.each(lieux, function(i, item) {
var marqueur = L.marker(item.coord, {icon: greenIcon});
var txt = item.num_stages > 1 ? item.num_stages+" stages ici": "1 stage ici";
txt = "<h3>"+item.nom+"</h3>"+
"<p>"+txt+"</p>";
marqueur.bindPopup(txt + "<p>Chargement...</p>");
marqueurs.addLayer(marqueur);
marqueur.on("popupopen", showDetailLieu)
.on("popupclose", unlockDetailsListe)
.on("mouseover", showDetailsListeListener)
.on("mouseout", hideDetailsListeListener);
marqueur._lieu_data = item;
marqueur._popup_header = txt;
marqueur._lieu_data_loading = false;
marqueurs_db[item.id] = marqueur;
lieux_db[item.id] = item;
});
map.addLayer(marqueurs);
lieux_survol = new L.layerGroup();
map.addLayer(lieux_survol);
$("#liste-resultats .stage")
.on("mouseover", showLieuxFromStage)
.on("mouseout", hideLieuxSurvol);
}
//
// Actions sur la carte -> Affichage réduit de la liste des stages
//
function showDetailLieu () {
// Affichage du popup détaillé
var data = this._lieu_data;
details_lock = data;
var marqueur = marqueurs_db[data.id];
var html = $("<div>").html(marqueur._popup_header);
var stageliste = $("<ul>");
$.each(lieux_map[data.id], function(i, item) {
var stage_el = $("#resultat-stage-"+item);
var url = stage_el.find('a.stage-sujet').attr('href');
var sujet = stage_el.find('a.stage-sujet').text();
var auteur = stage_el.find('.auteur').text();
var stage = $("<li>")
.append($("<a>", {href: url}).text(sujet))
.append($("<span>").text(" par "+auteur));
stageliste.append(stage);
});
html.append(stageliste);
marqueur.setPopupContent(html[0]);
}
function showDetailsListeListener () {
showDetailsListe(this._lieu_data);
}
function showDetailsListe (data) {
main_container.addClass("vue-details");
var liste_el = $("#resultats-details");
$.each(liste_el.children(), function(i, item){$(item).remove();});
$.each(lieux_map[data.id], function(i, item) {
var stage_el = $("#resultat-stage-"+item);
var new_el = $("<li>", {class:"stage"}).html(stage_el.html());
liste_el.append(new_el);
});
}
function unlockDetailsListe () {
details_lock = false;
hideDetailsListeListener();
}
function hideDetailsListeListener () {
if (details_lock === false)
main_container.removeClass("vue-details");
else
showDetailsListe(details_lock);
}
//
// Survol dans la liste -> Affichage des lieux
//
function showLieuxFromStage () {
if (stages_map === undefined) return;
var stageid = this.id.split("-")[2];
var lieuxids = stages_map[stageid];
$.each(lieuxids, function(i, lieu_id) {
var data = lieux_db[lieu_id];
var marqueur = new L.marker(data.coord, {icon:blueIcon});
lieux_survol.addLayer(marqueur);
});
}
function hideLieuxSurvol () {
lieux_survol.clearLayers();
}
// __init__
initInterface();
}

View file

@ -46,7 +46,7 @@
{% feedback_widget %} {% feedback_widget %}
{% endif %} {% endif %}
<section class="content"> <section class="content {% block extra_content_class %}{% endblock %}">
{% if messages %} {% if messages %}
<ul class="messages"> <ul class="messages">
{% for message in messages %} {% for message in messages %}

View file

@ -26,7 +26,7 @@
<img src="{% static 'images/home2.jpg' %}"/> <img src="{% static 'images/home2.jpg' %}"/>
</div> </div>
<div> <div>
<p>Ne partez plus en stage en terre inconnue : nourrissez-vous de l'expérience des séjours effectués par la communauté normalienne, repérez les bons plans, et ne faites pas les mêmes erreurs&nbsp;!</p> <p>Ne partez plus en stage en terre inconnue : nourrissez-vous des {{ num_stages }} expériences de séjours effectués par la communauté normalienne, repérez les bons plans, et ne faites pas les mêmes erreurs&nbsp;!</p>
{% if user.is_authenticated %}<p><a href="{% url 'avisstage:recherche' %}" class="btn">Rechercher des stages</a></p>{% endif %} {% if user.is_authenticated %}<p><a href="{% url 'avisstage:recherche' %}" class="btn">Rechercher des stages</a></p>{% endif %}
</div> </div>
</div> </div>

View file

@ -0,0 +1,27 @@
<form class="recherche" method="GET" action="{% url 'avisstage:recherche_resultats' %}">
<div>
<p class="generale">
<span>
{{ form.generique }}
<input type="submit" action="submit" value="Chercher un stage"/>
</span>
<a class="toggle_avancee" href="#" onclick="$('#recherche_avancee').toggleClass('expanded'); return false;">Recherche avancée</a>
</p>
</div>
<div class="avancee" id="recherche_avancee">
<p class="help_text">Le champ principal (ci-dessus) est aussi utilisé dans la recherche, ces champs ne servent qu'à filtrer plus précisément</p>
<ul>
{% for field in form %}
{% if field != form.generique %}
<li class="field__{{ field.name }}">
{% if field.label %}{{ field.label_tag }}{% endif %}
{{ field }}
</li>
{% endif %}
{% endfor %}
<li class="btnsubmit">
<input type="submit" action="submit" value="Chercher un stage"/>
</li>
</ul>
</div>
</form>

View file

@ -0,0 +1,14 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Chercher un stage - ExperiENS{% endblock %}
{% block extra_content_class %}recherche{% endblock %}
{% block content %}
<h1>Chercher un stage</h1>
{% include "avisstage/recherche/formulaire.html" with form=form %}
<h2>Ou alors</h2>
<p><a href="{% url 'avisstage:recherche_resultats' %}?vue=carte">Afficher la carte de tous les stages</a></p>
<p><a href="{% url 'avisstage:recherche_resultats' %}?vue=liste&tri=-date_maj">Afficher les dernières mises à jour</a></p>
{% endblock %}

View file

@ -0,0 +1,91 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Chercher un stage - ExperiENS{% endblock %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'js/leaflet.js' %}"></script>
<script type="text/javascript" src="{% static 'js/leaflet.markercluster.js' %}"></script>
<script type="text/javascript" src="{% static 'js/recherche.js' %}"></script>
<link rel="stylesheet" type="text/css" href="{% static 'css/leaflet.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/MarkerCluster.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/MarkerCluster.Default.css' %}" />
{% endblock %}
{% block extra_content_class %}recherche {{ vue }}{% endblock %}
{% block content %}
<section class="recherche-liste" id="recherche-liste">
{% include "avisstage/recherche/formulaire.html" with form=form %}
<article class="resultslist" id="liste-resultats">
<h2>Résultats de la recherche</h2>
{% if stages %}
<div id="vue-options" class="vue-options">
<ul>
<li>Affichage :</li>
<li><a href="javascript:void(0);" id="voir_liste">Liste</a></li>
<li><a href="javascript:void(0);" id="voir_hybride">Hybride</a></li>
<li><a href="javascript:void(0);" id="voir_carte">Carte</a></li>
</ul>
</div>
<p class="numresults">{{ stages|length }} expérience{{ stages|length|pluralize }} trouvée{{ stages|length|pluralize }}</p>
{% endif %}
<ul class="stage-liste" id="resultats">
{% for stage in stages %}
{% if tri == '-date_maj' %}
{% ifchanged stage.date_maj.date %}<li class="date-maj">Mis à jour le {{ stage.date_maj.date }}</li>{% endifchanged %}
{% endif %}
<li class="stage" id="resultat-stage-{{ stage.id }}">
<div class="misc-hdr">
<h3><a href="{% url "avisstage:stage" stage.id %}" class="stage-sujet">{{ stage.sujet }}</a><span class="auteur"> par <span class="stage-auteur">{{ stage.auteur.nom }}</span></span></h3>
<p class="dates"><span class="detail"><span class="debut">{{ stage.date_debut|date:"d/m" }}</span><span class="fin">{{ stage.date_fin|date:"d/m" }}</span></span><span class="year">{{ stage.date_debut|date:"Y" }}</span></p>
</div>
<div>
<ul class="infos">
<li class="type">{{ stage.get_type_stage_display }}</li>
<li class="structure">{{ stage.structure }}</li>
{% for lieu in stage.lieux.all %}<li class="lieu">{{ lieu.nom }}</li>{% endfor %}
{% for matiere in stage.matieres.all %}
<li class="matiere">{{ matiere.nom }}</li>
{% endfor %}
{% for thematique in stage.thematiques.all %}
<li class="thematique">{{ thematique.name }}</li>
{% endfor %}
<li class="year">{{ stage.date_debut|date:"Y" }}</li>
</ul>
</div>
<a href="{% url "avisstage:stage" stage.id %}" class="hoverlink">&nbsp;</a>
</li>
{% empty %}
<li class="stage">Aucun stage ne correspond à votre recherche et vos critères</li>
{% endfor %}
</ul>
</article>
</section>
<section class="recherche-liste recherche-details">
<article class="resultstlist fakeresults">
<ul class="stage-liste" id="resultats-details">
</ul>
</article>
</section>
{% if lieux %}
<section class="recherche-carte" id="recherche-carte">
<div id="carte"></div>
<div id="vue-options2" class="vue-options">
<ul>
<li><a href="javascript:void(0);" id="voir_hybride">Afficher la liste</a></li>
</ul>
</div>
<script type="text/javascript">
var lieux = [{{ lieux|join:',' }}];
var interfaceRecherche = new InterfaceRecherche("{{ STATIC_URL|escapejs }}", "{% url 'avisstage:api_dispatch_list' resource_name="lieu" api_name="v1" %}", lieux);
</script>
</section>
{% endif %}
{% endblock %}

View file

@ -22,6 +22,7 @@ urlpatterns = [
name='profil'), name='profil'),
url(r'^profil/edit/$', views.ProfilEdit.as_view(), name='profil_edit'), url(r'^profil/edit/$', views.ProfilEdit.as_view(), name='profil_edit'),
url(r'^recherche/$', views.recherche, name='recherche'), url(r'^recherche/$', views.recherche, name='recherche'),
url(r'^recherche/resultats/$', views.recherche_resultats, name='recherche_resultats'),
url(r'^feedback/$', views.feedback, name='feedback'), url(r'^feedback/$', views.feedback, name='feedback'),
url(r'^api/', include(v1_api.urls)), url(r'^api/', include(v1_api.urls)),
] ]

View file

@ -14,6 +14,7 @@ from django.db.models import Q
from avisstage.models import Normalien, Stage, Lieu, AvisLieu, AvisStage from avisstage.models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from avisstage.forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm from avisstage.forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm
from avisstage.views_search import *
import random, math import random, math
@ -23,7 +24,9 @@ import random, math
# Page d'accueil # Page d'accueil
def index(request): def index(request):
return render(request, 'avisstage/index.html') num_stages = Stage.objects.filter(public=True).count()
return render(request, 'avisstage/index.html',
{"num_stages": num_stages})
# Espace personnel # Espace personnel
@login_required @login_required
@ -55,16 +58,11 @@ class StageView(LoginRequiredMixin, DetailView):
#login_required #login_required
class StageListe(LoginRequiredMixin, ListView): class StageListe(LoginRequiredMixin, ListView):
model = Stage model = Stage
template_name = 'avisstage/liste/stage.html' template_name = 'avisstage/recherche/stage.html'
def get_queryset(self): def get_queryset(self):
return Stage.objects.filter(public=True).order_by('-date_maj') return Stage.objects.filter(public=True).order_by('-date_maj')
# Recherche
@login_required
def recherche(request):
return render(request, 'avisstage/recherche.html')
# FAQ # FAQ
def faq(request): def faq(request):
return render(request, 'avisstage/faq.html') return render(request, 'avisstage/faq.html')
@ -140,7 +138,7 @@ def manage_stage(request, pk=None):
{'form': form, 'avis_stage_form': avis_stage_form, {'form': form, 'avis_stage_form': avis_stage_form,
'avis_lieu_formset': avis_lieu_formset, 'avis_lieu_formset': avis_lieu_formset,
'creation': pk is None}) 'creation': pk is None})
# Ajout d'un lieu de stage # Ajout d'un lieu de stage
#login_required #login_required

148
avisstage/views_search.py Normal file
View file

@ -0,0 +1,148 @@
# coding: utf-8
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django import forms
from django.db.models import Q
from .documents import StageDocument
from .models import Stage
from .statics import TYPE_LIEU_OPTIONS, TYPE_STAGE_OPTIONS, NIVEAU_SCOL_OPTIONS
from datetime import date
# Recherche
class SearchForm(forms.Form):
generique = forms.CharField(required=False)
sujet = forms.CharField(label=u'À propos de', required=False)
contexte = forms.CharField(label=u'Contexte (lieu, encadrant⋅e⋅s, structure)',
required=False)
apres_annee = forms.IntegerField(label=u'Après cette année', required=False)
avant_annee = forms.IntegerField(label=u'Avant cette année', required=False)
type_stage = forms.ChoiceField(label="Type de stage", choices=([('', u'')]
+ list(TYPE_STAGE_OPTIONS)),
required=False)
niveau_scol = forms.ChoiceField(label="Année d'étude", choices=([('', u'')]
+ list(NIVEAU_SCOL_OPTIONS)),
required=False)
type_lieu = forms.ChoiceField(label=u"Type de lieu d'accueil",
choices=([('', u'')]
+ list(TYPE_LIEU_OPTIONS)),
required=False)
tri = forms.ChoiceField(label=u'Tri par',
choices=[('pertinence', u'Pertinence'),
('-date_maj',u'Dernière mise à jour')],
required=False, initial='pertinence')
def cherche(**kwargs):
filtres = Q(public=True)
dsl = StageDocument.search()
use_dsl = False
def field_relevant(field, test_string=True):
return field in kwargs and \
kwargs[field] is not None and \
((not test_string) or kwargs[field].strip() != '')
#
# Recherche libre
#
# Champ générique : recherche dans tous les champs
if field_relevant("generique"):
#print "Filtre generique", kwargs['generique']
dsl = dsl.filter(
"match",
_all={"query": kwargs["generique"],
"fuzziness": "auto"})
use_dsl = True
# Sujet -> Recherche dan les noms de sujets et les thématiques
if field_relevant("sujet"):
dsl = dsl.filter("multi_match",
query = kwargs["sujet"],
fields = ['sujet^2', 'thematiques'],
fuzziness = "auto")
use_dsl = True
# Contexte -> Encadrants, structure, lieu
if field_relevant("contexte"):
dsl = dsl.filter("multi_match",
query = kwargs["contexte"],
fields = ['encadrants', 'structure^2',
'lieux.nom', 'lieux.pays', 'lieux.ville'],
fuzziness = "auto")
use_dsl = True
#
# Filtres directs db
#
# Dates
if field_relevant('avant_annee', False):
dte = date(kwargs['avant_annee']+1, 1, 1)
filtres &= Q(date_fin__lt=dte)
if field_relevant('apres_annee', False):
dte = date(kwargs['apres_annee'], 1, 1)
filtres &= Q(date_debut__gte=dte)
# Type de stage
if field_relevant('type_stage'):
filtres &= Q(type_stage=kwargs["type_stage"])
if field_relevant('niveau_scol'):
filtres &= Q(niveau_scol=kwargs["niveau_scol"])
# Type de lieu
if field_relevant('type_lieu'):
filtres &= Q(lieux__type_lieu=kwargs["type_lieu"])
# Application
if use_dsl:
filtres &= Q(id__in=[s.meta.id for s in dsl])
#print filtres
resultat = Stage.objects.filter(filtres)
tri = 'pertinence'
if not use_dsl:
kwargs['tri'] = '-date_maj'
if field_relevant('tri') and kwargs['tri'] != 'pertinence':
tri = kwargs['tri']
resultat = resultat.order_by(kwargs['tri'])
return resultat, tri
@login_required
def recherche(request):
form = SearchForm()
return render(request, 'avisstage/recherche/recherche.html',
{"form": form})
@login_required
def recherche_resultats(request):
stages = []
tri = ''
vue = 'vue-liste'
lieux = []
if request.method == "GET":
form = SearchForm(request.GET)
if form.is_valid():
stages, tri = cherche(**form.cleaned_data)
lieux = map(list, stages.values_list('id', 'lieux'))
else:
form = SearchForm()
if stages:
vue = 'vue-hybride'
return render(request, 'avisstage/recherche/resultats.html',
{"form": form, "stages":stages,
"tri": tri, "vue": vue, "lieux": lieux})

View file

@ -30,6 +30,8 @@ INSTALLED_APPS = (
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.gis', 'django.contrib.gis',
'django_elasticsearch_dsl',
'tastypie', 'tastypie',
'django_cas_ng', 'django_cas_ng',
'braces', 'braces',

View file

@ -26,3 +26,9 @@ STATIC_ROOT = "/home/evarin/Bureau/experiENS/static/"
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
STATIC_URL = "/experiens/static/" STATIC_URL = "/experiens/static/"
ELASTICSEARCH_DSL = {
'default': {
'hosts': 'localhost:9200'
},
}

View file

@ -43,3 +43,10 @@ MEDIA_URL = ROOT_URL + 'media/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/') STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
EMAIL_HOST = "nef.ens.fr" EMAIL_HOST = "nef.ens.fr"
ELASTICSEARCH_DSL = {
'default': {
'hosts': '127.0.0.1:9200'
},
}

View file

@ -8,3 +8,4 @@ django-taggit-autosuggest
pytz pytz
django-tastypie django-tastypie
lxml lxml
git+https://github.com/sabricot/django-elasticsearch-dsl