* started workin on 'navbar' module

* changed bootstrap theme to bootswatch/Simplex

* big work on navbar logic

* starting creating menus using navbar

* converted app views to new Wepage decorator, updated navbar

* reimplemented DernieresMaraudes as a dropdown instead of ContextMixin

* reorganised static files, minor code cleanups

* turned Link.href into lazy-evaluated property

* collapsed 'navbar' module into 'website', dynamic building of ApplicationMenu subclasses

* minor cleanup

* blah blah blah

* added way to add admin/non-admin links

* minor style change : red border for active page instead of all dropdowns

* deleted file

* prepare adding removing menu templates files, being replaced by code

* essayé de généraliser le code pour les modaux bootstrap, non testé git status

* more preparation and thinking on navbar app_menus logic...

* added LinkManager and DropdownManager, getting closer...

* small fix in DropdownManager.__get__

* boosted up work: keep it simple so it can be merged fast, major layout changes

* added month filter on maraudes:liste

* added 'as_icon' filter to display boolean/null values as bootstrap icons

* remove inactive user from planning selection

* removed all unused 'menu' templates

* set up django_select2 to use static files

* small fix after review
This commit is contained in:
artus40
2017-02-11 18:20:13 +01:00
committed by GitHub
parent 288ca2cc20
commit 0be59a61a7
61 changed files with 665 additions and 525 deletions

View File

@@ -8,3 +8,19 @@ class Config(AppConfig):
menu_icon = "road"
def get_index_url(self):
return "/maraudes/"
from utilisateurs.models import Maraudeur
from website.decorators import Webpage
maraudes = Webpage('maraudes',
icon="road",
defaults={
'users': [Maraudeur],
'ajax': False,
'title': ('Maraudes','app'),
})
# Setting up some links
maraudes.app_menu.add_link(('Liste des maraudes', 'maraudes:liste', "list"))
maraudes.app_menu.add_link(('Planning', 'maraudes:planning', "calendar"), admin=True)

View File

@@ -87,6 +87,7 @@ class Maraude(models.Model):
limit_choices_to={
'is_superuser': False,
'is_staff': True,
'is_active': True,
}
)

View File

@@ -1,15 +1,15 @@
{% load notes %}
<h3> Compte-rendu </h3>
<table class="table table-bordered">
<div class="col-lg-6 col-md-12">
<table class="table table-bordered">
{% for note in notes %}
{% inline_table note header="sujet" %}
{% endfor %}
</table>
<div class="well bg-info">
<p><strong>Informations</strong></p>
<p>Rencontres : {{ maraude.observation_count}}</p>
</table>
</div>
<div class="col-lg-6 col-md-12">
<div class="well bg-info">
<p><strong>Informations</strong></p>
<p>Rencontres : {{ maraude.rencontre_count}}</p>
<p>Personnes rencontrées : {{ maraude.observation_count}}</p>
</div>
</div>

View File

@@ -1,6 +1,6 @@
{% load bootstrap3 %}{% load staticfiles %}
<script type="text/javascript" src="{% static "jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "scripts/jquery.formset.js" %}"></script>
<script type="text/javascript">
/* Dynamic Formsets */
$(function() {
@@ -38,6 +38,14 @@
<div class="row">
<div class="col-lg-6 col-md-12">
<!-- Modal buttons -->
<div class="well well-sm text-right"><strong>Créer un nouvel objet :</strong>
<div class="btn-group" role="group" aria-label="...">
<button id="new-sujet" class="btn btn-sm btn-primary">{% bootstrap_icon "user" %} Sujet</button>
<button id="new-lieu" class="btn btn-sm btn-primary">{% bootstrap_icon "globe" %} Lieu</button>
</div>
</div>
<form method="post" action="{% url 'maraudes:create' maraude.pk %}?finalize=False">
{% csrf_token %}
<div class="panel panel-primary panel-collapse">
@@ -71,7 +79,7 @@
</div>
</div>
</div>
<!-- Modal and button linking -->
<div class="modal fade" id="form-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
@@ -85,3 +93,12 @@
</div>
</div>
</div>
<script type="text/javascript" src="{% static 'scripts/bootstrap-modal.js' %}"></script>
<script type="text/javascript">
$.fn.openModalEvent('new-sujet',
'{% url "sujets:create" %}?next={% url "maraudes:create" pk=maraude.pk %}',
'Nouveau sujet');
$.fn.openModalEvent('new-lieu',
'{% url "maraudes:lieu-create" %}?next={% url "maraudes:create" pk=maraude.pk %}',
'Nouveau lieu');
</script>

View File

@@ -1,54 +0,0 @@
{% load bootstrap3 %}
<li class="dropdown app-menu"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Création<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#" id="new-sujet">{% bootstrap_icon "user" %} Nouveau sujet</a></li>
<li><a href="#" id="new-lieu">{% bootstrap_icon "globe" %} Nouveau lieu</a></li>
</ul>
<script type="text/javascript">
/* Lier les boutons de création
* Thanks to Derek Morgan, https://dmorgan.info/posts/django-views-bootstrap-modals/
*/
$(function() {
var formAjaxSubmit = function(form, modal) {
$(form).submit(function (e) {
e.preventDefault();
$.ajax({
type: $(this).attr('method'),
url: $(this).attr('action'),
data: $(this).serialize(),
success: function (xhr, ajaxOptions, thrownError) {
if ( $(xhr).find('.has-error').length > 0 || $(xhr).find('.alert-danger').length > 0) {
$(modal).find('.modal-body').html(xhr);
formAjaxSubmit(form, modal);
} else {
$(modal).modal('toggle');
// Reload page ?
location.reload(true)
}
},
error: function (xhr, ajaxOptions, thrownError) {
// handle response errors here
}
});
});
};
/* TODO: Use formAjaxSubmit above, but reload page on form success */
$('#new-sujet').click(function() {
$('#form-modal-body').load('{% url "sujets:create" %}?next={% url "maraudes:create" pk=maraude.id %}', function () {
$('.modal-title').text("Nouveau sujet");
$('#form-modal').modal('toggle');
formAjaxSubmit("#form-modal-body form", "#form-modal");
});
});
$('#new-lieu').click(function() {
$('#form-modal-body').load('{% url "maraudes:lieu-create" %}?next={% url "maraudes:create" pk=maraude.id %}', function () {
$('.modal-title').text("Nouveau lieu");
$('#form-modal').modal('toggle');
formAjaxSubmit("#form-modal-body form", "#form-modal");
});
});
});
</script>

View File

@@ -1,9 +1,7 @@
<div class="col-md-12 col-lg-6">
{% if maraude.est_terminee %}
{% include "compte_rendu/compterendu.html" %}
{% else %}
{% if perms.maraudes.can_add_compterendu %}<a class="btn btn-primary" href="{% url 'maraudes:create' maraude.pk %}">Écrire le compte-rendu</a>
{% else %} <p class="alert alert-info">Le compte-rendu n'a pas encore été écrit</p>{% endif %}
{% endif %}
</div>

View File

@@ -11,7 +11,13 @@
avec {% if user.is_superuser %}{{prochaine_maraude.binome}}{%else%}{{prochaine_maraude.referent}}{%endif%}.
</strong></p>
<hr />
<p><mark>Informations, notes, rendez-vous ?</mark></p>
{% if prochaine_maraude.est_terminee %}
<a class="btn btn-sm btn-primary" href="{% url 'maraudes:details' pk=prochaine_maraude.pk %}">
Voir le compte-rendu
</a>{%else%}
<a class="btn btn-sm btn-primary" href="{% url 'maraudes:create' pk=prochaine_maraude.pk %}">
Rédiger le compte-rendu
</a>{% endif %}
{% else %}<p>Aucune maraude prévue.</p>{% endif %}
</div>
</div>

View File

@@ -1,12 +1,9 @@
{% if object.est_terminee %}
<a href="{% url 'maraudes:details' object.id %}" class="btn btn-link">
{% else %}
<a href="#" class="btn btn-link disabled">
{% endif %}
{{object.date}}
</a>
{% if object.est_terminee %}<a href="{% url 'maraudes:details' object.id %}" class="btn btn-link">
{% else %}<a href="#" class="btn btn-link disabled">{% endif %}
{{object.date}}</a>
<div class="pull-right">
<span class="label label-info">{{ object.binome }} & {{ object.referent }}</span>
{% if object.est_terminee %}
<span class="label label-success">{{object.rencontres.count}} rencontres</span>
{% endif %}
</div>

View File

@@ -1,14 +1,13 @@
{% load bootstrap3 %}
{% load tables %}
<div class="col-md-12 col-lg-6">
<div class="col-lg-10">
<div class="panel panel-primary">
<!-- Default panel contents -->
<div class="panel-heading text-center">
<h3 class="panel-title">Maraudes passées</h3>
</div>
<!-- Table -->
{% table object_list cols=2 cell_template="maraudes/list_table_cell.html" %}
{% table object_list cols=3 cell_template="maraudes/list_table_cell.html" %}
{% if is_paginated %}
<div class="panel-footer text-center">
<ul class="pagination">
@@ -20,15 +19,14 @@
{% endif %}
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="panel panel-default">
<!-- Default panel contents -->
<div class="panel-heading text-center">
<h3 class="panel-title">Filtres</h3>
</div>
<div class="list-group">
<a href="#" class="list-group-item disabled">Vos maraudes seulement</a></li>
<a href="#" class="list-group-item disabled">Ce mois-ci</a></li>
<div class="pull-left">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Filtrer <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="{% url 'maraudes:liste' %}">Pas de filtre</a></li>
<li><a href="?filter=month-only">Ce mois-ci</a></li>
</ul>
</div>
</div>
</div>

View File

@@ -1,7 +0,0 @@
{% if user.is_superuser %}{% load bootstrap3 %}
<li class="dropdown app-menu"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Administration<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'maraudes:planning' %}">{% bootstrap_icon "calendar" %} Planning</a></li>
<li><a href="{% url 'admin:app_list' app_label=active_app.label %}">{% bootstrap_icon "wrench" %} Gérer les maraudes</a></li>
</ul>
{% endif %}

View File

@@ -1,15 +0,0 @@
{% load bootstrap3 %}
<li class="app-menu">
<a href="{% url 'maraudes:liste' %}">Liste des maraudes
<span class="pull-right">{% bootstrap_icon "list" %}</span></a>
</li>
{% if dernieres_maraudes %}
<li class="dropdown app-menu"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Dernières maraudes <b class="caret"></b></a>
<ul class="dropdown-menu">{% for m in dernieres_maraudes %}
<li><a href="{% url 'maraudes:details' m.pk %}" {% if maraude == m %}style="color:#fff;"{% endif %}>
<strong>{{ m }}</strong> <small>{{m.binome}} & {{m.referent}}</small></a>
</li>{% endfor %}
</ul>
</li>
{% endif %}

View File

@@ -1,7 +1,6 @@
import datetime
import calendar
from django.utils import timezone
from django.utils.functional import cached_property
from django.contrib import messages
from django.shortcuts import render, redirect
# Views
@@ -22,56 +21,14 @@ from .forms import ( RencontreForm, RencontreInlineFormSet,
ObservationInlineFormSet, ObservationInlineFormSetNoExtra,
MaraudeAutoDateForm, MonthSelectForm, )
from utilisateurs.models import Maraudeur
from website import decorators as website
maraudes = website.app_config(
name="maraudes",
groups=[Maraudeur],
menu=["maraudes/menu/dernieres_maraudes.html"],
admin_menu=["maraudes/menu/admin_menu.html"],
ajax=False,
)
compte_rendu = website.app_config(
name="maraudes",
groups=[Maraudeur],
menu=["compte_rendu/menu/creation.html"],
ajax=False,
)
maraudes_ajax = website.app_config(
name="maraudes",
groups=[Maraudeur],
ajax=True,
)
from django.core.mail import send_mail
class DerniereMaraudeMixin(object):
count = 5
@cached_property
def dernieres_maraudes(self):
""" Renvoie la liste des 'Maraude' passées et terminées """
return Maraude.objects.get_past().filter(
heure_fin__isnull=False
).order_by(
'-date'
)[:self.count]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['dernieres_maraudes'] = self.dernieres_maraudes
return context
from .apps import maraudes
@maraudes.using(title=('La Maraude', 'Tableau de bord'))
class IndexView(generic.TemplateView):
@maraudes
class IndexView(DerniereMaraudeMixin, generic.TemplateView):
class PageInfo:
title = "Maraude - Tableau de bord"
header = "La Maraude"
header_small = "Tableau de bord"
# TemplateView
template_name = "maraudes/index.html"
def get_context_data(self, *args, **kwargs):
@@ -101,48 +58,44 @@ class IndexView(DerniereMaraudeMixin, generic.TemplateView):
return Maraude.objects.next
## MARAUDES
@maraudes
class MaraudeDetailsView(DerniereMaraudeMixin, generic.DetailView):
@maraudes.using(title=('{{maraude.date}}', 'compte-rendu'))
class MaraudeDetailsView(generic.DetailView):
""" Vue détaillé d'un compte-rendu de maraude """
model = CompteRendu
context_object_name = "maraude"
template_name = "maraudes/details.html"
# Template
class PageInfo:
title = "Maraude - {{maraude.date}}"
header = "{{maraude.date}}"
header_small = "{{maraude.referent}} & {{maraude.binome}}"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['notes'] = self.object.get_observations()
return context
@maraudes
class MaraudeListView(DerniereMaraudeMixin, generic.ListView):
@maraudes.using(title=('Liste des maraudes',))
class MaraudeListView(generic.ListView):
""" Vue de la liste des compte-rendus de maraude """
model = CompteRendu
template_name = "maraudes/liste.html"
paginate_by = 30
class PageInfo:
title = "Maraude - Liste des maraudes"
header = "Liste des maraudes"
def get_queryset(self):
today = datetime.date.today()
return super().get_queryset().filter(
date__lte=timezone.localtime(timezone.now()).date()
current_date = timezone.localtime(timezone.now()).date()
qs = super().get_queryset().filter(
date__lte=current_date
).order_by('-date')
filtre = self.request.GET.get('filter', None)
if filtre == "month-only":
return qs.filter(date__month=current_date.month)
#Other cases...
else:
return qs
## COMPTE-RENDU DE MARAUDE
@compte_rendu
@maraudes.using(title=('{{maraude.date}}', 'rédaction'))
class CompteRenduCreateView(generic.DetailView):
""" Vue pour la création d'un compte-rendu de maraude """
@@ -153,11 +106,6 @@ class CompteRenduCreateView(generic.DetailView):
form = None
inline_formset = None
class PageInfo:
title = "{{maraude}} - Compte-rendu"
header = "{{maraude.date}}"
header_small = "écriture du compte-rendu"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
#WARNING: Overrides app_menu and replace it
@@ -180,11 +128,8 @@ class CompteRenduCreateView(generic.DetailView):
# Add text to some mails ? Transmission, message à un référent, etc...
# Send mail to Maraudeurs
_from = maraude.referent.email
exclude = (maraude.referent, maraude.binome)
recipients = []
for m in Maraudeur.objects.all():
if not m in exclude:
recipients.append(m.email)
# Shall select only Maraudeur where 'is_active' is True !
recipients = [m for m in Maraudeur.objects.all() if m not in (maraude.referent, maraude.binome)]
objet = "Compte-rendu de maraude : %s" % maraude.date
message = "Sujets rencontrés : ..." #TODO: Mail content
send_mail(objet, message, _from, recipients)
@@ -245,7 +190,7 @@ class CompteRenduCreateView(generic.DetailView):
@compte_rendu
@maraudes.using(title=('{{maraude.date}}', 'mise à jour'))
class CompteRenduUpdateView(generic.DetailView):
""" Vue pour mettre à jour le compte-rendu de la maraude """
@@ -253,11 +198,6 @@ class CompteRenduUpdateView(generic.DetailView):
context_object_name = "maraude"
template_name = "compte_rendu/compterendu_update.html"
class PageInfo:
title = "{{maraude}} - Compte-rendu"
header = "{{maraude.date}}"
header_small = "compte-rendu"
base_formset = None
inline_formsets = []
rencontres_queryset = None
@@ -311,17 +251,12 @@ class CompteRenduUpdateView(generic.DetailView):
## PLANNING
@maraudes
@maraudes.using(title=('Planning',))
class PlanningView(generic.TemplateView):
""" Display and edit the planning of next Maraudes """
template_name = "planning/planning.html"
class PageInfo:
title = "Planning"
header = "Planning"
header_small = "{{month}} {{year}}" #TODO: does not parse extra context
def _parse_request(self):
self.current_date = datetime.date.today()
try: self.month = int(self.request.GET['month'])
@@ -381,18 +316,13 @@ class PlanningView(generic.TemplateView):
## LIEU
@maraudes_ajax
@maraudes.using(ajax=True)
class LieuCreateView(generic.edit.CreateView):
model = Lieu
template_name = "maraudes/lieu_create.html"
fields = "__all__"
success_url = "/maraudes/"
class PageInfo:
pass
permissions = ['maraudes.add_lieu']
def post(self, request, *args, **kwargs):
if 'next' in self.request.POST:
self.success_url = self.request.POST["next"]

View File

@@ -23,8 +23,8 @@ LOGIN_URL = 'index'
BOOTSTRAP3 = {
# The URL to the jQuery JavaScript file
'base_url': os.path.join(STATIC_URL, 'bootstrap/'),
'jquery_url': '//code.jquery.com/jquery.min.js',
'base_url': os.path.join(STATIC_URL, 'css', 'bootstrap/'),
'jquery_url': os.path.join(STATIC_URL, 'scripts', 'jquery.min.js'),
'include_jquery': True,
# Label class to use in horizontal forms
'horizontal_label_class': 'col-md-2',
@@ -32,6 +32,10 @@ BOOTSTRAP3 = {
'horizontal_field_class': 'col-md-10',
}
# Django-select2 Configuration
SELECT2_JS = 'scripts/select2.min.js'
SELECT2_CSS = 'css/select2.min.css'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
AUTHENTICATION_BACKENDS = [
'website.backends.MyBackend'

View File

@@ -3,3 +3,14 @@ from django.apps import AppConfig
class SuiviConfig(AppConfig):
name = 'suivi'
from utilisateurs.models import Maraudeur
from website.decorators import Webpage
suivi = Webpage("suivi", icon="eye-open", defaults={
'restricted': [Maraudeur],
'ajax': False,
}
)
suivi.app_menu.add_link(('Liste des sujets', 'suivi:liste', 'list'))

View File

@@ -1,5 +1,4 @@
{% load bootstrap3 %}
{{ form.media.js }}{{ form.media.css }}
<form action="" method="POST">{% csrf_token %}
{% with "inline" as layout %}
<div class="form-{{layout}} well col-md-10 col-md-offset-2">
@@ -14,3 +13,4 @@
</div> {% endwith %}
<div class="pull-right">{% bootstrap_button "Enregistrer l'appel" button_type="submit" %}</div>
</form>
{{ form.media.js }}{{ form.media.css }}

View File

@@ -1,10 +0,0 @@
{% load bootstrap3 %}
<li class="dropdown app-menu">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
Administration <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="{% url 'sujets:create' %}">{% bootstrap_icon "plus" %} Nouveau sujet</a></li>
<li><a href="{% url 'admin:app_list' app_label='sujets' %}">
{% bootstrap_icon "wrench" %} Gérer les sujets</a></li>
</ul>

View File

@@ -1,6 +0,0 @@
{% load bootstrap3 %}
<li class="app-menu">
<a href="{% url 'suivi:liste' %}">Liste des sujets
<span class="pull-right">{% bootstrap_icon "list" %}</span>
</a>
</li>

View File

@@ -7,15 +7,7 @@ from .forms import *
from notes.mixins import NoteFormMixin
from notes.forms import AutoNoteForm
# Create your views here.
from utilisateurs.models import Maraudeur
from website import decorators as website
suivi = website.app_config(
name="suivi",
groups=[Maraudeur],
menu=["suivi/menu/sujets.html"],
admin_menu=["suivi/menu/admin_sujets.html"],
ajax=False,
)
from maraudes.compte_rendu import CompteRendu
@@ -30,12 +22,10 @@ def derniers_sujets_rencontres():
sujets.add(obs.sujet)
return sujets
@suivi
from .apps import suivi
@suivi.using(title=("Suivi", "Tableau de bord"))
class IndexView(NoteFormMixin, generic.TemplateView):
class PageInfo:
title = "Suivi des bénéficiaires"
header = "Suivi"
header_small = "Tableau de bord"
#NoteFormMixin
forms = {
'appel': AppelForm,
@@ -54,11 +44,10 @@ class IndexView(NoteFormMixin, generic.TemplateView):
context['derniers_sujets'] = ", ".join(map(str, derniers_sujets_rencontres()))
return context
@suivi
@suivi.using(title=('Liste des sujets',))
class SujetListView(generic.ListView):
class PageInfo:
title = "Sujet - Liste des sujets"
header = "Liste des sujets"
#ListView
model = Sujet
template_name = "sujets/sujet_liste.html"
@@ -77,15 +66,10 @@ class SujetListView(generic.ListView):
context['query_text'] = self.request.POST.get('q', None)
return context
# Import app_config from 'sujets' application, using
# its admin_menu option
from sujets.views import sujets
@sujets
@suivi.using(title=('{{sujet}}', 'suivi'))
class SuiviSujetView(NoteFormMixin, generic.DetailView):
class PageInfo:
title = "Sujet - {{sujet}}"
header = "{{sujet}}"
header_small = "suivi"
#NoteFormMixin
forms = {
'note': AutoNoteForm,

View File

@@ -1,7 +0,0 @@
{% if user.is_superuser %}
{% load bootstrap3 %}
<li class="dropdown app-menu"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Sujet<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'admin:notes_note_changelist' %}?sujet__personne_ptr__exact={{sujet.pk}}">{% bootstrap_icon "wrench" %} Éditer les notes</a></li>
</ul>
{% endif %}

View File

@@ -1,16 +1,63 @@
{% load boolean_icons %}
<table class="table">
<tr><th colspan="4" class="active">Informations générales</th></tr>
<tr><th>Première rencontre :</th><td> {{ sujet.premiere_rencontre }}</td></tr>
<tr><th colspan="4" class="active">État-civil</th></tr>
<tr><th>Nom</th><td>{{ sujet.nom|default:'?' }}</td><th>Prénom</th><td>{{ sujet.prenom|default:'?' }}</td></tr>
<tr><th>Sexe</th><td>{{ sujet.genre }}</td><th>Âge</th><td>{{ sujet.age|default:'?' }}</td></tr>
<tr><th colspan="4" class="active">Problématiques</th></tr>
<tr><th>Psychiatrique</th><td>{{ sujet.prob_psychiatrie }}</td><th>Addiction</th><td>{{ sujet.prob_addiction }}</td></tr>
<tr><th>Administratif</th><td>{{ sujet.prob_administratif }}</td><th>Somatique</th><td>{{ sujet.prob_somatique }}</td></tr>
<tr><th colspan="4" class="active">Habitation</th></tr></td></tr>
<tr><th>Type</th><td>{{ sujet.habitation }}</td><th>Connu du SIAO</th><td>{{ sujet.connu_siao }}</td></tr>
<tr><th colspan="4" class="active">Ressources</th></tr></td></tr>
<tr><td colspan="4">{{ sujet.ressources }}</td></tr>
<tr><th colspan="4" class="active">Parcours de vie</th></tr></td></tr>
<tr><td colspan="2">{{ sujet.parcours_de_vie }}</td><th>Lien familial</th><td>{{ sujet.lien_familial }}</td></tr>
<tr>
<th colspan="4" class="active">Informations générales</th>
</tr>
<tr>
<th>Première rencontre :</th>
<td> {{ sujet.premiere_rencontre }}</td>
</tr>
<tr>
<th colspan="4" class="active">État-civil</th>
</tr>
<tr>
<th>Nom</th>
<td>{{ sujet.nom|default:'?' }}</td>
<th>Prénom</th>
<td>{{ sujet.prenom|default:'?' }}</td>
</tr>
<tr>
<th>Sexe</th>
<td>{{ sujet.genre }}</td>
<th>Âge</th>
<td>{{ sujet.age|default:'?' }}</td>
</tr>
<tr>
<th colspan="4" class="active">Problématiques</th>
</tr>
<tr>
<th>Psychiatrique</th>
<td>{{ sujet.prob_psychiatrie|as_icon }}</td>
<th>Addiction</th>
<td>{{ sujet.prob_addiction|as_icon }}</td>
</tr>
<tr>
<th>Administratif</th>
<td>{{ sujet.prob_administratif|as_icon }}</td>
<th>Somatique</th>
<td>{{ sujet.prob_somatique|as_icon }}</td>
</tr>
<tr>
<th colspan="4" class="active">Habitation</th>
</tr>
<tr>
<th>Type</th>
<td>{{ sujet.habitation }}</td>
<th>Connu du SIAO</th>
<td>{{ sujet.connu_siao|as_icon }}</td>
</tr>
<tr>
<th colspan="4" class="active">Ressources</th>
</tr>
<tr>
<td colspan="4">{{ sujet.ressources }}</td>
</tr>
<tr>
<th colspan="4" class="active">Parcours de vie</th>
</tr>
<tr>
<td colspan="2">{{ sujet.parcours_de_vie }}</td>
<th>Lien familial</th>
<td>{{ sujet.lien_familial|as_icon }}</td>
</tr>
</table>

View File

@@ -1,10 +1,12 @@
{% load tables %}
{% load bootstrap3 %}
<div class="col-md-12 col-lg-6">
<div class="col-lg-10">
<div class="panel panel-primary">
<!-- Default panel contents -->
<div class="panel-heading text-center">
<h3 class="panel-title">Sujets rencontrés</h3>
<h3 class="panel-title">Sujets rencontrés
</h3>
</div>
<div class="panel-body">
<form action="" method="POST" class="form form-group">{% csrf_token %}
@@ -41,19 +43,17 @@
{% endif %}{% endif %}
</div>
</div>
<div class="col-lg-3 col-md-3">
<div class="panel panel-default">
<!-- Default panel contents -->
<div class="panel-heading text-center">
<h3 class="panel-title">Filtres</h3>
</div>
<div class="list-group">
<a href="#" class="list-group-item disabled">Rencontrés ce mois-ci</a></li>
<a href="#" class="list-group-item disabled"><strong>Mes accompagnements</strong></a></li>
<a href="#" class="list-group-item disabled">Hébérgé en urgence</a></li>
<a href="#" class="list-group-item disabled">Sans logement</a></li>
</div>
<div class="pull-left">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Filtrer <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="{% url 'suivi:liste' %}">Pas de filtre</a></li>
<li role="separator" class="divider"></li>
<li><a href="?filter=month-only">Rencontrés ce mois-ci</a></li>
<li><a href="?filter=">Hébérgé en urgence</a></li>
<li><a href="?filter=">Sans abri</a></li>
</ul>
</div>
</div>

View File

@@ -6,49 +6,31 @@ from .forms import SujetCreateForm
### Webpage config
from utilisateurs.models import Maraudeur
from website import decorators as website
sujets = website.app_config(
name="suivi",
groups=[Maraudeur],
menu=["suivi/menu/sujets.html"],
admin_menu=["sujets/menu/admin_sujet.html"],
ajax=True,
from website.decorators import Webpage
sujets = Webpage( "suivi", menu=False, defaults={
'restricted': [Maraudeur],
'ajax': True,
}
)
### Views
@sujets
@sujets.using(title=('{{object}}', 'details'))
class SujetDetailsView(generic.DetailView):
class PageInfo:
title = "Sujet - {{ sujet }}"
header = "{{ sujet }}"
header_small = "informations"
#DetailView
template_name = "sujets/sujet_details.html"
model = Sujet
@sujets
class SujetUpdateView(generic.edit.UpdateView):
class PageInfo:
title = "Mise à jour - {{sujet}}"
header = "{{sujet}}"
header_small = "mise à jour"
#UpdateView
template_name = "sujets/sujet_update.html"
model = Sujet
fields = '__all__'
@sujets
class SujetCreateView(generic.edit.CreateView):
class PageInfo:
title = "Nouveau sujet"
header = "Nouveau sujet"
#CreateView
template_name = "sujets/sujet_create.html"
form_class = SujetCreateForm

View File

@@ -12,7 +12,7 @@ class MaraudeurAdmin(admin.ModelAdmin):
('Informations', {'fields': [('first_name', 'last_name')]}),
]
list_display = ('first_name', 'last_name', 'is_active')
list_display = ('username', 'is_active')

View File

@@ -1,5 +1,16 @@
from django.apps import AppConfig
from website.decorators import Webpage
from .models import Professionnel
class UtilisateursConfig(AppConfig):
name = 'utilisateurs'
utilisateurs = Webpage('utilisateurs',
icon="user",
defaults={
'users': [Professionnel],
'ajax': False,
'title': ('Utilisateurs','app'),
})

View File

@@ -0,0 +1,2 @@
{{ user.first_name }}, {{ user.last_name }}

7
utilisateurs/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.UtilisateurView.as_view(), name="index"),
]

14
utilisateurs/views.py Normal file
View File

@@ -0,0 +1,14 @@
from django.views import generic
from .apps import utilisateurs
from .models import Professionnel
@utilisateurs
class UtilisateurView(generic.DetailView):
template_name = "utilisateurs/details.html"
model = Professionnel
def get_object(self):
qs = self.get_queryset()
return qs.filter(pk=self.request.user.pk)

View File

@@ -1,39 +1,93 @@
from .mixins import *
from .mixins import (WebsiteTemplateMixin, WebsiteAjaxTemplateMixin,
SpecialUserRequiredMixin)
from .navbar import ApplicationMenu
def _insert_bases(cls, bases):
""" Insert new bases in given view class """
old_bases = cls.__bases__
new_bases = tuple(bases) + old_bases
cls.__bases__ = new_bases
def app_config(**options):
""" Insert per-application configuration options :
-- name : name of the app to register under in navbar
-- groups : user groups needed to access this application
-- menu : user menu templates to be used
-- admin_menu : admin menu templates, only appear for superuser
-- ajax : view will return content_template for Ajax requests
class Webpage:
""" Webpage configurator. It is used as a decorator.
The constructor takes one positionnal argument:
- app_name : name of the application where this view shall be categorized.
and keyword arguments:
- defaults : mapping of default options.
- menu : does it register a menu ? default is True
- icon : bootstrap name of menu header icon, ignored if 'menu' is False.
Options are :
- title: tuple of (header, header_small), header_small is optionnal.
- restricted: set of group to which access is restricted.
- ajax: can this view be called as ajax ?
"""
name = options.pop('name', None)
groups = options.pop('groups', []) #Transition from app_users
menu = options.pop('menu', [])
admin_menu = options.pop('admin_menu', [])
ajax = options.pop('ajax', False)
new_bases = []
if ajax:
new_bases.append(WebsiteAjaxTemplateMixin)
options = [
('title', ('Unset', 'small header')),
('restricted', []),
('ajax', False)
]
def __init__(self, app_name, icon=None, defaults={}, menu=True):
self.app_name = app_name
if menu: # Build ApplicationMenu subclass
app_menu = type(
app_name.title() + "Menu",
(ApplicationMenu,),
{'name': app_name,
'header': (app_name.title(), '%s:index' % app_name, icon),
'_links': [],
'_dropdowns': [],
}
)
self.app_menu = app_menu
else:
new_bases.append(WebsiteTemplateMixin)
self.app_menu = None
if groups: #TODO: use group instaed of user class
new_bases.append(SpecialUserRequiredMixin)
self._defaults = {}
self._updated = {} # Store updated options
# Set all default options
for opt_name, opt_default in self.options:
self._set_option(opt_name, defaults.get(opt_name, opt_default))
def class_decorator(cls):
_insert_bases(cls, new_bases)
cls._user_menu = menu
cls._admin_menu = admin_menu
cls.app_name = name
cls.app_users = groups.copy()
return cls
def __getattr__(self, attr):
""" Return the overriden value if any, default overwise """
return self._updated.get(attr, self._defaults[attr])
return class_decorator
def _set_option(self, attr, value):
""" Set the default value if there is none already, updated overwise """
if not attr in self._defaults:
self._defaults[attr] = value
else:
if attr in self._updated:
raise RuntimeError(attr, 'has already been updated !')
self._updated[attr] = value
def __call__(self, view_cls):
""" Setup the view and return it """
bases_to_add = []
if self.ajax: bases_to_add.append(WebsiteAjaxTemplateMixin)
else: bases_to_add.append(WebsiteTemplateMixin)
if self.restricted: bases_to_add.append(SpecialUserRequiredMixin)
_insert_bases(view_cls, bases_to_add)
# Setup configuration. ISSUE: defaults values will be overriden !
view_cls.app_name = self.app_name
view_cls.header = self.title
view_cls.app_users = self.restricted
self._updated = {} # Reset updated attributes to avoid misbehavior
return view_cls
def using(self, **kwargs):
""" Overrides defaults options with the values given """
for opt_name, _ in self.options:
if opt_name in kwargs:
self._set_option(opt_name, kwargs[opt_name])
return self

View File

@@ -1,151 +1,64 @@
import datetime
from django.utils import timezone
from django.core.exceptions import ImproperlyConfigured
from django.apps import apps
from django.contrib.auth.decorators import user_passes_test
from django.template import Template, Context
from django.views.generic.base import ContextMixin, TemplateResponseMixin
from django.views.generic.base import TemplateResponseMixin
## Mixins ##
def special_user_required(authorized_users):
valid_cls = tuple(authorized_users)
def check_special_user(user):
print('check user is instance of', valid_cls)
if isinstance(user, valid_cls):
return True
else:
return False
return user_passes_test(check_special_user)
class SpecialUserRequiredMixin(object):
""" Requires that the User is an instance of some class """
app_users = []
@classmethod
def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs)
return special_user_required(cls.app_users)(view)
return cls.special_user_required(cls.app_users)(view)
@staticmethod
def special_user_required(authorized_users):
valid_cls = tuple(authorized_users)
if not valid_cls: # No restriction usually means misconfiguration !
raise ImproperlyConfigured(
'A view was configured as "restricted" with no restricting parameters !')
class TemplateFieldsMetaclass(type):
""" Loads Template objects with given string for
header, header_small, title, ...
Theses strings shall be found in cls.Template
"""
def __init__(cls, bases, Dict):
pass
def check_special_user(user):
if isinstance(user, valid_cls):
return True
else:
return False
return user_passes_test(check_special_user)
def user_processor(request, context):
context['user_group'] = request.user.__class__.__qualname__
return context
class NavbarMixin(object):
registered_apps = ['maraudes', 'suivi']
app_name = None
def header_processor(header, context):
context['page_header'] = Template(header[0]).render(context)
context['page_header_small'] = Template(header[1]).render(context) if len(header) == 2 else ''
context['page_title'] = " - ".join((context['page_header'], context['page_header_small']))
return context
def get_apps_config(self):
""" Load additionnal config data on each app registered in navbar
Add :
- menu_icon : glyphicon in sidebar
- disabled : show/hide in sidebar
"""
## Utils ##
APP_ICONS = {
'maraudes': 'road',
'suivi': 'eye-open',
}
app_names = self.registered_apps
self._apps = []
for name in app_names:
app_config = apps.get_app_config(name)
app_config.menu_icon = APP_ICONS[name]
#TODO: Seems unsafe (only need module perm)
app_config.disabled = not self.request.user.has_module_perms(name)
self._apps.append(app_config)
return self._apps
@property
def apps(self):
if not hasattr(self, '_apps'):
self._apps = self.get_apps_config()
return self._apps
def get_active_app(self):
if not self.app_name:
self.app_name = self.__class__.__module__.split(".")[0]
# If app is website, there is no "active" application
if self.app_name == "website":
return None
active_app = apps.get_app_config(self.app_name)
if not active_app in self.apps: #TODO: how do we deal with this ?
raise ValueError("%s must be registered in Configuration.navbar_apps" % active_app)
return active_app
@property
def active_app(self):
if not hasattr(self, '_active_app'):
self._active_app = self.get_active_app()
return self._active_app
@property
def menu(self):
""" Renvoie la liste des templates utilisés comme menu pour l'application
active
"""
if not self.request.user.is_superuser:
return self._user_menu
return self._user_menu + self._admin_menu
class WebsiteTemplateMixin(NavbarMixin, TemplateResponseMixin):
class WebsiteTemplateMixin(TemplateResponseMixin):
""" Mixin for easy integration of 'website' templates
Each child can specify:
- title : title of the page
- header : header of the page
- header_small : sub-header of the page
If 'content_template' is not defined, value will fallback to template_name
in child view.
"""
base_template = "base_site.html"
content_template = None
_user_menu = []
_admin_menu = []
_groups = []
app_name = None
class Configuration:
stylesheets = ['base.css']
page_blocks = ['header', 'header_small', 'title']
stylesheets = ['css/base.css']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
self._page_blocks = []
if not hasattr(self, "PageInfo"):
raise ImproperlyConfigured("You must define a PageInfo on ", self)
for attr, val in self.PageInfo.__dict__.items():
if attr[0] is not "_" and type(val) is str:
setattr(self, attr, Template(val))
self._page_blocks.append(attr)
def get_template_names(self):
""" Ensure same template for all children views. """
@@ -159,30 +72,20 @@ class WebsiteTemplateMixin(NavbarMixin, TemplateResponseMixin):
raise ImproperlyConfigured(self, "has no template defined !")
return self.content_template
def _update_context_with_rendered_blocks(self, context):
""" Render text for existing PageInfo attributes.
See Configuration.page_blocks for valid attribute names """
render_context = Context(context)
for attr in self._page_blocks:
name = "page_%s" % attr
context[name] = getattr(self, attr).render(render_context)
return context
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
self._update_context_with_rendered_blocks(context)
context = Context(super().get_context_data(**kwargs))
#Website processor
context['stylesheets'] = self.Configuration.stylesheets
context['apps'] = self.apps
context['active_app'] = self.active_app
context['active_app'] = self.app_name # Set by Webpage decorator
# User processor
context = header_processor(self.header, context)
context = user_processor(self.request, context)
#Webpage
context['content_template'] = self.get_content_template()
context['app_menu'] = self.menu
return context
class WebsiteAjaxTemplateMixin(WebsiteTemplateMixin):
""" Mixin that returns content_template instead of base_template when
request is Ajax.

102
website/navbar.py Normal file
View File

@@ -0,0 +1,102 @@
""" Draft for navbar application menu
"""
from django.urls import reverse
registered = []
class Link:
""" Navbar link
Constructor takes one required argument :
- text : text to display for the link
and two optional arguments :
- target : str or tuple (str, dict)
- icon : bootstrap icon name, no validation
"""
def __init__(self, text, target="#", icon=None):
self.text = text
self._target = target
self.icon = icon
@property
def href(self):
""" Lazy creation of html 'href' value, using 'target' instance attribute """
if not hasattr(self, '_href'):
if self._target == "#": #Create void link
self._href = "#"
else:
try:
target, kwargs = self._target
except ValueError:
target = self._target
kwargs = {}
assert type(target) == str
assert type(kwargs) == dict
self._href = reverse(target, **kwargs)
return self._href
class LinkManager:
""" Per-class manager of links """
def __init__(self):
self.items = []
def __get__(self, instance, owner):
if instance: #Filtering done at each call, not optimized at all !
if not instance.user.is_superuser:
return [link for link in self.items if link.admin_link == False]
else:
return self.items.copy()
return self
def add(self, link):
self.items.append(link)
def __repr__(self):
return '<LinkManager: [' + ', '.join((l.text for l in self.items)) + ']>'
class MenuRegistry(type):
""" Metaclass that registers subclass into module level variable 'registered' """
def __new__(metacls, name, bases, attrs):
cls = type.__new__(metacls, name, bases, attrs)
if name != "ApplicationMenu":
print('registering menu', cls)
registered.append(cls)
# Create Link instance for header attributes
try:
header, target, icon = cls.header
except ValueError:
header = cls.header
target = "#"
icon = None
cls.header = Link(header, target, icon)
cls.links = LinkManager()
return cls
class ApplicationMenu(metaclass=MenuRegistry):
name = None
header = None
def __init__(self, view, user):
self.view = view
self.user = user
self.is_active = self.name == self.view.app_name
@classmethod
def add_link(cls, link, admin=False):
if not isinstance(link, Link):
link = Link(*link)
link.admin_link = admin
cls.links.add(link)

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +1,19 @@
#menu {
border: none;
border-right: 4px solid #980300;
}
.dropdown-toggle{
border-right: 4px solid #980300 !important;
}
.dropdown-menu {
border-bottom: none !important;
}
.navbar-fixed-side .navbar-nav>li>a {
border-bottom: none;
font-variant: small-caps;
color: #fff;
}
#menu {
border: none;
border-right: 4px solid #980300;
background-color: #121212;
}
@media (max-width:768px){
#menu { border: none; }
}
.app-menu {
background-color: #121212;
@@ -25,10 +21,12 @@
}
@media (max-width:768px){
#menu { border: none; }
.active{
border-right: 2px solid #980300 !important;
}
.jumbotron {
background-color: #fefefe;
.dropdown-menu {
border-bottom: none !important;
}

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

1
website/static/css/select2.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,21 +0,0 @@
/* Maraudes application base stylesheet */
.rencontre {
background-color: #efefef;
width:100%;
border: 5px dotted white;
margin:2px;
margin-bottom:8px;
font-family: Arial;
}
.header td {
color: white;
background-color: black;
text-align:center;
font-weight:bold;
font-family: Arial;
padding: 2px;
}

View File

@@ -0,0 +1,43 @@
/* Lier les boutons de création
* Thanks to Derek Morgan, https://dmorgan.info/posts/django-views-bootstrap-modals/
*/
;(function($) {
var formAjaxSubmit = function(form, modal)
{
$(form).submit(function (e) {
e.preventDefault();
$.ajax({
type: $(this).attr('method'),
url: $(this).attr('action'),
data: $(this).serialize(),
success: function (xhr, ajaxOptions, thrownError) {
if ( $(xhr).find('.has-error').length > 0 || $(xhr).find('.alert-danger').length > 0) {
$(modal).find('.modal-body').html(xhr);
formAjaxSubmit(form, modal);
} else {
$(modal).modal('toggle');
// Reload page ?
location.reload()
}
},
error: function (xhr, ajaxOptions, thrownError) {
// handle response errors here
}
});
});
};
$.fn.openModalEvent = function(id, href, title)
{
$('#'+id).click(function() {
$('#form-modal-body').load(href, function()
{
$('.modal-title').text(title);
$('#form-modal').modal('toggle');
formAjaxSubmit("#form-modal-body form", "#form-modal");
});
});
};
})(jQuery);

5
website/static/scripts/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
website/static/scripts/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html>
{% load staticfiles %} {% load bootstrap3 %}
{% load staticfiles %} {% load bootstrap3 %} {% load navbar %}
<html lang="fr">
<head>
<title>{% block title %}La maraude{% endblock %}</title>
{% bootstrap_css %}{% bootstrap_javascript %}
<!-- Side Navbar from http://www.samrayner.com/bootstrap-side-navbar/inverse.html -->
<link href="/static/bootstrap/navbar-fixed-side.css" rel="stylesheet" />
<link href="/static/css/bootstrap/navbar-fixed-side.css" rel="stylesheet" />
{% if stylesheets %}{% for stylesheet in stylesheets %}
<link rel="stylesheet" type="text/css" href="{% static stylesheet %}" />{% endfor %}{% endif %}
</head>
@@ -13,7 +13,7 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-3 col-lg-2">
{% include "navbar.html" %}
{% navbar %}
</div>
<div class="col-md-9 col-lg-10">
<h1 class="page-header">{% block page_header %}{% endblock %}</h1>

View File

@@ -0,0 +1,11 @@
{% load bootstrap3 %}
<li {% if active %}class="active"{%endif%}>
<a href="{{header.href}}">{% if header.icon %}{% bootstrap_icon header.icon %} &middot; {% endif %}<strong>{{header.text}}</strong></a>
</li>
{% if active %}{% for link in links %}
<li class="app-menu">
<a href="{{link.href}}">{{ link.text }}
{% if link.icon %}<span class="pull-right">{% bootstrap_icon link.icon %}</span>{% endif %}
</a>
</li>
{% endfor %}{% endif %}

View File

@@ -0,0 +1,52 @@
{% load bootstrap3 %}{% load staticfiles %}{% load navbar %}
<nav class="navbar navbar-inverse navbar-fixed-side" role="navigation" id="menu">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{% url 'index' %}">La Maraude ALSA</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">{% for app in apps %}
{% navbar_menu app %}
{% endfor %}</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a id="UserMenu" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<strong style="color:#fff;">{% bootstrap_icon "user" %} &middot; {{user}} </strong>&middot; {{ user_group }}<span class="caret"></span></a>
<ul class="dropdown-menu" aria-labelledby="UserMenu">
{% if next %}
<p class="well-sm text-center"><strong style="color:#980300;">Vous n'avez pas l'autorisation<br/> d'accéder à cette page.</strong></p>
{%endif%}
{% if user.is_authenticated %}
{% if user.is_superuser %}<li><a href="{% url 'admin:index' %}">{% bootstrap_icon "wrench" %} Administration</a></li>{% endif %}
<li><a href="{% url 'logout' %}">{% bootstrap_icon "log-out" %} Déconnecter</a></li>
{% else %}
<li>
<form class="navbar-form navbar-left" method="post" action="/login/">{% csrf_token %}
{% if next %}<input name="next" value="{{next}}" hidden />{% endif %}
<div class="form-group form-horizontal">
<div class="form-group">
<label class="col-md-2 sr-only control-label" for="id_username">Username</label>
<div class="col-md-10">
<input autofocus="" class="form-control" id="id_username" maxlength="254" name="username" placeholder="Username" title="" type="text" required />
</div>
</div>
<div class="form-group">
<label class="col-md-2 sr-only control-label" for="id_password">Password</label>
<div class="col-md-10"><input class="form-control" id="id_password" name="password" placeholder="Password" title="" type="password" required />
</div>
</div>
</div>
<div class="text-center"><button class="btn btn-primary navbar-button" type="submit">Connexion</button></div>
</form>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>

View File

@@ -0,0 +1,14 @@
from django import template
from django.utils.html import format_html
register = template.Library()
@register.filter
def as_icon(value):
icons = {True: "ok",
False: "remove",
None: "asterisk"
}
if not value in icons:
raise ValueError(value, 'is not a boolean or empty value !')
else:
return format_html('<span class="glyphicon glyphicon-{}"></span>', icons[value])

View File

@@ -0,0 +1,51 @@
#-*- coding:utf-8 -*-
from django import template
from django.urls import reverse
register = template.Library()
class NavbarNode(template.Node):
_apps = None
def get_menus(self, view, user):
if not self._apps:
from website.navbar import registered
if not registered:
print('WARNING: No app registered into "navbar" module')
self._apps = registered.copy()
return [app_menu(view, user) for app_menu in self._apps]
def get_template(self):
return template.loader.get_template('navbar/layout.html')
def render(self, context):
request = context.get('request')
user, view = context.get('user'), context.get('view')
apps = self.get_menus(view, user)
# Add user menu
context = template.Context({
'apps': apps,
'user': user,
'user_group': context.get('user_group', None),
'next': context.get('next', None),
})
return self.get_template().render(context, request)
@register.tag
def navbar(parser, token):
return NavbarNode()
@register.inclusion_tag("navbar/app-menu.html")
def navbar_menu(app_menu):
return {
'active': app_menu.is_active,
'header': app_menu.header,
'links': app_menu.links,
}

View File

@@ -6,6 +6,7 @@ from .views import Index, login_view
from maraudes import urls as maraudes_urls
from suivi import urls as suivi_urls
from sujets import urls as sujets_urls
from utilisateurs import urls as utilisateurs_urls
urlpatterns = [
# Authentification
@@ -19,4 +20,5 @@ urlpatterns = [
url(r'^maraudes/', include(maraudes_urls, namespace="maraudes")),
url(r'^suivi/', include(suivi_urls, namespace="suivi")),
url(r'^sujets/', include(sujets_urls, namespace="sujets")),
url(r'^utilisateurs/', include(utilisateurs_urls, namespace="utilisateurs")),
]

View File

@@ -10,7 +10,7 @@ class Index(WebsiteTemplateMixin, views.generic.TemplateView):
template_name = "main.html"
app_menu = None
header = ('La Maraude ALSA', 'accueil')
class PageInfo:
title = "La maraude ALSA"
header = "La Maraude ALSA"