Navbar (#31)
* 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:
@@ -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)
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ class Maraude(models.Model):
|
||||
limit_choices_to={
|
||||
'is_superuser': False,
|
||||
'is_staff': True,
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
|
||||
2
utilisateurs/templates/utilisateurs/details.html
Normal file
2
utilisateurs/templates/utilisateurs/details.html
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
{{ user.first_name }}, {{ user.last_name }}
|
||||
7
utilisateurs/urls.py
Normal file
7
utilisateurs/urls.py
Normal 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
14
utilisateurs/views.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
102
website/navbar.py
Normal 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)
|
||||
|
||||
14
website/static/bootstrap/css/bootstrap.min.css
vendored
14
website/static/bootstrap/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
11
website/static/css/bootstrap/css/bootstrap.min.css
vendored
Normal file
11
website/static/css/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
1
website/static/css/select2.min.css
vendored
Normal file
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 |
@@ -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;
|
||||
}
|
||||
|
||||
43
website/static/scripts/bootstrap-modal.js
vendored
Normal file
43
website/static/scripts/bootstrap-modal.js
vendored
Normal 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
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
2
website/static/scripts/select2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
11
website/templates/navbar/app-menu.html
Normal file
11
website/templates/navbar/app-menu.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% load bootstrap3 %}
|
||||
<li {% if active %}class="active"{%endif%}>
|
||||
<a href="{{header.href}}">{% if header.icon %}{% bootstrap_icon header.icon %} · {% 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 %}
|
||||
52
website/templates/navbar/layout.html
Normal file
52
website/templates/navbar/layout.html
Normal 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" %} · {{user}} </strong>· {{ 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>
|
||||
14
website/templatetags/boolean_icons.py
Normal file
14
website/templatetags/boolean_icons.py
Normal 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])
|
||||
51
website/templatetags/navbar.py
Normal file
51
website/templatetags/navbar.py
Normal 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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user