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>
|
||||
</div>
|
||||
</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 'maraudes:liste' %}">Pas de filtre</a></li>
|
||||
<li><a href="?filter=month-only">Ce mois-ci</a></li>
|
||||
</ul>
|
||||
</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"]
|
||||
|
||||
Reference in New Issue
Block a user