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"
|
menu_icon = "road"
|
||||||
def get_index_url(self):
|
def get_index_url(self):
|
||||||
return "/maraudes/"
|
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={
|
limit_choices_to={
|
||||||
'is_superuser': False,
|
'is_superuser': False,
|
||||||
'is_staff': True,
|
'is_staff': True,
|
||||||
|
'is_active': True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{% load notes %}
|
{% load notes %}
|
||||||
<h3> Compte-rendu </h3>
|
<div class="col-lg-6 col-md-12">
|
||||||
|
<table class="table table-bordered">
|
||||||
<table class="table table-bordered">
|
|
||||||
{% for note in notes %}
|
{% for note in notes %}
|
||||||
{% inline_table note header="sujet" %}
|
{% inline_table note header="sujet" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<div class="well bg-info">
|
<div class="col-lg-6 col-md-12">
|
||||||
<p><strong>Informations</strong></p>
|
<div class="well bg-info">
|
||||||
<p>Rencontres : {{ maraude.observation_count}}</p>
|
<p><strong>Informations</strong></p>
|
||||||
|
<p>Rencontres : {{ maraude.rencontre_count}}</p>
|
||||||
|
<p>Personnes rencontrées : {{ maraude.observation_count}}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% load bootstrap3 %}{% load staticfiles %}
|
{% 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">
|
<script type="text/javascript">
|
||||||
/* Dynamic Formsets */
|
/* Dynamic Formsets */
|
||||||
$(function() {
|
$(function() {
|
||||||
@@ -38,6 +38,14 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12">
|
<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">
|
<form method="post" action="{% url 'maraudes:create' maraude.pk %}?finalize=False">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="panel panel-primary panel-collapse">
|
<div class="panel panel-primary panel-collapse">
|
||||||
@@ -71,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 fade" id="form-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -85,3 +93,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% if maraude.est_terminee %}
|
||||||
{% include "compte_rendu/compterendu.html" %}
|
{% include "compte_rendu/compterendu.html" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if perms.maraudes.can_add_compterendu %}<a class="btn btn-primary" href="{% url 'maraudes:create' maraude.pk %}">Écrire le compte-rendu</a>
|
{% 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 %}
|
{% else %} <p class="alert alert-info">Le compte-rendu n'a pas encore été écrit</p>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,13 @@
|
|||||||
avec {% if user.is_superuser %}{{prochaine_maraude.binome}}{%else%}{{prochaine_maraude.referent}}{%endif%}.
|
avec {% if user.is_superuser %}{{prochaine_maraude.binome}}{%else%}{{prochaine_maraude.referent}}{%endif%}.
|
||||||
</strong></p>
|
</strong></p>
|
||||||
<hr />
|
<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 %}
|
{% else %}<p>Aucune maraude prévue.</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
{% if object.est_terminee %}
|
{% if object.est_terminee %}<a href="{% url 'maraudes:details' object.id %}" class="btn btn-link">
|
||||||
<a href="{% url 'maraudes:details' object.id %}" class="btn btn-link">
|
{% else %}<a href="#" class="btn btn-link disabled">{% endif %}
|
||||||
{% else %}
|
{{object.date}}</a>
|
||||||
<a href="#" class="btn btn-link disabled">
|
<div class="pull-right">
|
||||||
{% endif %}
|
|
||||||
{{object.date}}
|
|
||||||
</a>
|
|
||||||
<span class="label label-info">{{ object.binome }} & {{ object.referent }}</span>
|
<span class="label label-info">{{ object.binome }} & {{ object.referent }}</span>
|
||||||
{% if object.est_terminee %}
|
{% if object.est_terminee %}
|
||||||
<span class="label label-success">{{object.rencontres.count}} rencontres</span>
|
<span class="label label-success">{{object.rencontres.count}} rencontres</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load tables %}
|
{% load tables %}
|
||||||
<div class="col-md-12 col-lg-6">
|
<div class="col-lg-10">
|
||||||
<div class="panel panel-primary">
|
<div class="panel panel-primary">
|
||||||
<!-- Default panel contents -->
|
<!-- Default panel contents -->
|
||||||
<div class="panel-heading text-center">
|
<div class="panel-heading text-center">
|
||||||
<h3 class="panel-title">Maraudes passées</h3>
|
<h3 class="panel-title">Maraudes passées</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table -->
|
<!-- 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 %}
|
{% if is_paginated %}
|
||||||
<div class="panel-footer text-center">
|
<div class="panel-footer text-center">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
@@ -20,15 +19,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 col-md-3">
|
<div class="pull-left">
|
||||||
<div class="panel panel-default">
|
<div class="btn-group">
|
||||||
<!-- Default panel contents -->
|
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<div class="panel-heading text-center">
|
Filtrer <span class="caret"></span>
|
||||||
<h3 class="panel-title">Filtres</h3>
|
</button>
|
||||||
</div>
|
<ul class="dropdown-menu">
|
||||||
<div class="list-group">
|
<li><a href="{% url 'maraudes:liste' %}">Pas de filtre</a></li>
|
||||||
<a href="#" class="list-group-item disabled">Vos maraudes seulement</a></li>
|
<li><a href="?filter=month-only">Ce mois-ci</a></li>
|
||||||
<a href="#" class="list-group-item disabled">Ce mois-ci</a></li>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</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 datetime
|
||||||
import calendar
|
import calendar
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
# Views
|
# Views
|
||||||
@@ -22,56 +21,14 @@ from .forms import ( RencontreForm, RencontreInlineFormSet,
|
|||||||
ObservationInlineFormSet, ObservationInlineFormSetNoExtra,
|
ObservationInlineFormSet, ObservationInlineFormSetNoExtra,
|
||||||
MaraudeAutoDateForm, MonthSelectForm, )
|
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
|
from django.core.mail import send_mail
|
||||||
|
|
||||||
class DerniereMaraudeMixin(object):
|
from .apps import maraudes
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
@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"
|
template_name = "maraudes/index.html"
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
@@ -101,48 +58,44 @@ class IndexView(DerniereMaraudeMixin, generic.TemplateView):
|
|||||||
return Maraude.objects.next
|
return Maraude.objects.next
|
||||||
|
|
||||||
## MARAUDES
|
## MARAUDES
|
||||||
@maraudes
|
@maraudes.using(title=('{{maraude.date}}', 'compte-rendu'))
|
||||||
class MaraudeDetailsView(DerniereMaraudeMixin, generic.DetailView):
|
class MaraudeDetailsView(generic.DetailView):
|
||||||
""" Vue détaillé d'un compte-rendu de maraude """
|
""" Vue détaillé d'un compte-rendu de maraude """
|
||||||
|
|
||||||
model = CompteRendu
|
model = CompteRendu
|
||||||
context_object_name = "maraude"
|
context_object_name = "maraude"
|
||||||
template_name = "maraudes/details.html"
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['notes'] = self.object.get_observations()
|
context['notes'] = self.object.get_observations()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@maraudes.using(title=('Liste des maraudes',))
|
||||||
@maraudes
|
class MaraudeListView(generic.ListView):
|
||||||
class MaraudeListView(DerniereMaraudeMixin, generic.ListView):
|
|
||||||
""" Vue de la liste des compte-rendus de maraude """
|
""" Vue de la liste des compte-rendus de maraude """
|
||||||
|
|
||||||
model = CompteRendu
|
model = CompteRendu
|
||||||
template_name = "maraudes/liste.html"
|
template_name = "maraudes/liste.html"
|
||||||
paginate_by = 30
|
paginate_by = 30
|
||||||
|
|
||||||
class PageInfo:
|
|
||||||
title = "Maraude - Liste des maraudes"
|
|
||||||
header = "Liste des maraudes"
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
today = datetime.date.today()
|
current_date = timezone.localtime(timezone.now()).date()
|
||||||
return super().get_queryset().filter(
|
qs = super().get_queryset().filter(
|
||||||
date__lte=timezone.localtime(timezone.now()).date()
|
date__lte=current_date
|
||||||
).order_by('-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 DE MARAUDE
|
||||||
@compte_rendu
|
@maraudes.using(title=('{{maraude.date}}', 'rédaction'))
|
||||||
class CompteRenduCreateView(generic.DetailView):
|
class CompteRenduCreateView(generic.DetailView):
|
||||||
""" Vue pour la création d'un compte-rendu de maraude """
|
""" Vue pour la création d'un compte-rendu de maraude """
|
||||||
|
|
||||||
@@ -153,11 +106,6 @@ class CompteRenduCreateView(generic.DetailView):
|
|||||||
form = None
|
form = None
|
||||||
inline_formset = None
|
inline_formset = None
|
||||||
|
|
||||||
class PageInfo:
|
|
||||||
title = "{{maraude}} - Compte-rendu"
|
|
||||||
header = "{{maraude.date}}"
|
|
||||||
header_small = "écriture du compte-rendu"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
#WARNING: Overrides app_menu and replace it
|
#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...
|
# Add text to some mails ? Transmission, message à un référent, etc...
|
||||||
# Send mail to Maraudeurs
|
# Send mail to Maraudeurs
|
||||||
_from = maraude.referent.email
|
_from = maraude.referent.email
|
||||||
exclude = (maraude.referent, maraude.binome)
|
# Shall select only Maraudeur where 'is_active' is True !
|
||||||
recipients = []
|
recipients = [m for m in Maraudeur.objects.all() if m not in (maraude.referent, maraude.binome)]
|
||||||
for m in Maraudeur.objects.all():
|
|
||||||
if not m in exclude:
|
|
||||||
recipients.append(m.email)
|
|
||||||
objet = "Compte-rendu de maraude : %s" % maraude.date
|
objet = "Compte-rendu de maraude : %s" % maraude.date
|
||||||
message = "Sujets rencontrés : ..." #TODO: Mail content
|
message = "Sujets rencontrés : ..." #TODO: Mail content
|
||||||
send_mail(objet, message, _from, recipients)
|
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):
|
class CompteRenduUpdateView(generic.DetailView):
|
||||||
""" Vue pour mettre à jour le compte-rendu de la maraude """
|
""" Vue pour mettre à jour le compte-rendu de la maraude """
|
||||||
|
|
||||||
@@ -253,11 +198,6 @@ class CompteRenduUpdateView(generic.DetailView):
|
|||||||
context_object_name = "maraude"
|
context_object_name = "maraude"
|
||||||
template_name = "compte_rendu/compterendu_update.html"
|
template_name = "compte_rendu/compterendu_update.html"
|
||||||
|
|
||||||
class PageInfo:
|
|
||||||
title = "{{maraude}} - Compte-rendu"
|
|
||||||
header = "{{maraude.date}}"
|
|
||||||
header_small = "compte-rendu"
|
|
||||||
|
|
||||||
base_formset = None
|
base_formset = None
|
||||||
inline_formsets = []
|
inline_formsets = []
|
||||||
rencontres_queryset = None
|
rencontres_queryset = None
|
||||||
@@ -311,17 +251,12 @@ class CompteRenduUpdateView(generic.DetailView):
|
|||||||
|
|
||||||
|
|
||||||
## PLANNING
|
## PLANNING
|
||||||
@maraudes
|
@maraudes.using(title=('Planning',))
|
||||||
class PlanningView(generic.TemplateView):
|
class PlanningView(generic.TemplateView):
|
||||||
""" Display and edit the planning of next Maraudes """
|
""" Display and edit the planning of next Maraudes """
|
||||||
|
|
||||||
template_name = "planning/planning.html"
|
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):
|
def _parse_request(self):
|
||||||
self.current_date = datetime.date.today()
|
self.current_date = datetime.date.today()
|
||||||
try: self.month = int(self.request.GET['month'])
|
try: self.month = int(self.request.GET['month'])
|
||||||
@@ -381,18 +316,13 @@ class PlanningView(generic.TemplateView):
|
|||||||
|
|
||||||
## LIEU
|
## LIEU
|
||||||
|
|
||||||
@maraudes_ajax
|
@maraudes.using(ajax=True)
|
||||||
class LieuCreateView(generic.edit.CreateView):
|
class LieuCreateView(generic.edit.CreateView):
|
||||||
model = Lieu
|
model = Lieu
|
||||||
template_name = "maraudes/lieu_create.html"
|
template_name = "maraudes/lieu_create.html"
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
success_url = "/maraudes/"
|
success_url = "/maraudes/"
|
||||||
|
|
||||||
class PageInfo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
permissions = ['maraudes.add_lieu']
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if 'next' in self.request.POST:
|
if 'next' in self.request.POST:
|
||||||
self.success_url = self.request.POST["next"]
|
self.success_url = self.request.POST["next"]
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ LOGIN_URL = 'index'
|
|||||||
|
|
||||||
BOOTSTRAP3 = {
|
BOOTSTRAP3 = {
|
||||||
# The URL to the jQuery JavaScript file
|
# The URL to the jQuery JavaScript file
|
||||||
'base_url': os.path.join(STATIC_URL, 'bootstrap/'),
|
'base_url': os.path.join(STATIC_URL, 'css', 'bootstrap/'),
|
||||||
'jquery_url': '//code.jquery.com/jquery.min.js',
|
'jquery_url': os.path.join(STATIC_URL, 'scripts', 'jquery.min.js'),
|
||||||
'include_jquery': True,
|
'include_jquery': True,
|
||||||
# Label class to use in horizontal forms
|
# Label class to use in horizontal forms
|
||||||
'horizontal_label_class': 'col-md-2',
|
'horizontal_label_class': 'col-md-2',
|
||||||
@@ -32,6 +32,10 @@ BOOTSTRAP3 = {
|
|||||||
'horizontal_field_class': 'col-md-10',
|
'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'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
'website.backends.MyBackend'
|
'website.backends.MyBackend'
|
||||||
|
|||||||
@@ -3,3 +3,14 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
class SuiviConfig(AppConfig):
|
class SuiviConfig(AppConfig):
|
||||||
name = 'suivi'
|
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 %}
|
{% load bootstrap3 %}
|
||||||
{{ form.media.js }}{{ form.media.css }}
|
|
||||||
<form action="" method="POST">{% csrf_token %}
|
<form action="" method="POST">{% csrf_token %}
|
||||||
{% with "inline" as layout %}
|
{% with "inline" as layout %}
|
||||||
<div class="form-{{layout}} well col-md-10 col-md-offset-2">
|
<div class="form-{{layout}} well col-md-10 col-md-offset-2">
|
||||||
@@ -14,3 +13,4 @@
|
|||||||
</div> {% endwith %}
|
</div> {% endwith %}
|
||||||
<div class="pull-right">{% bootstrap_button "Enregistrer l'appel" button_type="submit" %}</div>
|
<div class="pull-right">{% bootstrap_button "Enregistrer l'appel" button_type="submit" %}</div>
|
||||||
</form>
|
</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.mixins import NoteFormMixin
|
||||||
from notes.forms import AutoNoteForm
|
from notes.forms import AutoNoteForm
|
||||||
# Create your views here.
|
# 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
|
from maraudes.compte_rendu import CompteRendu
|
||||||
|
|
||||||
@@ -30,12 +22,10 @@ def derniers_sujets_rencontres():
|
|||||||
sujets.add(obs.sujet)
|
sujets.add(obs.sujet)
|
||||||
return sujets
|
return sujets
|
||||||
|
|
||||||
@suivi
|
from .apps import suivi
|
||||||
|
|
||||||
|
@suivi.using(title=("Suivi", "Tableau de bord"))
|
||||||
class IndexView(NoteFormMixin, generic.TemplateView):
|
class IndexView(NoteFormMixin, generic.TemplateView):
|
||||||
class PageInfo:
|
|
||||||
title = "Suivi des bénéficiaires"
|
|
||||||
header = "Suivi"
|
|
||||||
header_small = "Tableau de bord"
|
|
||||||
#NoteFormMixin
|
#NoteFormMixin
|
||||||
forms = {
|
forms = {
|
||||||
'appel': AppelForm,
|
'appel': AppelForm,
|
||||||
@@ -54,11 +44,10 @@ class IndexView(NoteFormMixin, generic.TemplateView):
|
|||||||
context['derniers_sujets'] = ", ".join(map(str, derniers_sujets_rencontres()))
|
context['derniers_sujets'] = ", ".join(map(str, derniers_sujets_rencontres()))
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@suivi
|
|
||||||
|
|
||||||
|
@suivi.using(title=('Liste des sujets',))
|
||||||
class SujetListView(generic.ListView):
|
class SujetListView(generic.ListView):
|
||||||
class PageInfo:
|
|
||||||
title = "Sujet - Liste des sujets"
|
|
||||||
header = "Liste des sujets"
|
|
||||||
#ListView
|
#ListView
|
||||||
model = Sujet
|
model = Sujet
|
||||||
template_name = "sujets/sujet_liste.html"
|
template_name = "sujets/sujet_liste.html"
|
||||||
@@ -77,15 +66,10 @@ class SujetListView(generic.ListView):
|
|||||||
context['query_text'] = self.request.POST.get('q', None)
|
context['query_text'] = self.request.POST.get('q', None)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
# Import app_config from 'sujets' application, using
|
|
||||||
# its admin_menu option
|
|
||||||
from sujets.views import sujets
|
@suivi.using(title=('{{sujet}}', 'suivi'))
|
||||||
@sujets
|
|
||||||
class SuiviSujetView(NoteFormMixin, generic.DetailView):
|
class SuiviSujetView(NoteFormMixin, generic.DetailView):
|
||||||
class PageInfo:
|
|
||||||
title = "Sujet - {{sujet}}"
|
|
||||||
header = "{{sujet}}"
|
|
||||||
header_small = "suivi"
|
|
||||||
#NoteFormMixin
|
#NoteFormMixin
|
||||||
forms = {
|
forms = {
|
||||||
'note': AutoNoteForm,
|
'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">
|
<table class="table">
|
||||||
<tr><th colspan="4" class="active">Informations générales</th></tr>
|
<tr>
|
||||||
<tr><th>Première rencontre :</th><td> {{ sujet.premiere_rencontre }}</td></tr>
|
<th colspan="4" class="active">Informations générales</th>
|
||||||
<tr><th colspan="4" class="active">État-civil</th></tr>
|
</tr>
|
||||||
<tr><th>Nom</th><td>{{ sujet.nom|default:'?' }}</td><th>Prénom</th><td>{{ sujet.prenom|default:'?' }}</td></tr>
|
<tr>
|
||||||
<tr><th>Sexe</th><td>{{ sujet.genre }}</td><th>Âge</th><td>{{ sujet.age|default:'?' }}</td></tr>
|
<th>Première rencontre :</th>
|
||||||
<tr><th colspan="4" class="active">Problématiques</th></tr>
|
<td> {{ sujet.premiere_rencontre }}</td>
|
||||||
<tr><th>Psychiatrique</th><td>{{ sujet.prob_psychiatrie }}</td><th>Addiction</th><td>{{ sujet.prob_addiction }}</td></tr>
|
</tr>
|
||||||
<tr><th>Administratif</th><td>{{ sujet.prob_administratif }}</td><th>Somatique</th><td>{{ sujet.prob_somatique }}</td></tr>
|
<tr>
|
||||||
<tr><th colspan="4" class="active">Habitation</th></tr></td></tr>
|
<th colspan="4" class="active">État-civil</th>
|
||||||
<tr><th>Type</th><td>{{ sujet.habitation }}</td><th>Connu du SIAO</th><td>{{ sujet.connu_siao }}</td></tr>
|
</tr>
|
||||||
<tr><th colspan="4" class="active">Ressources</th></tr></td></tr>
|
<tr>
|
||||||
<tr><td colspan="4">{{ sujet.ressources }}</td></tr>
|
<th>Nom</th>
|
||||||
<tr><th colspan="4" class="active">Parcours de vie</th></tr></td></tr>
|
<td>{{ sujet.nom|default:'?' }}</td>
|
||||||
<tr><td colspan="2">{{ sujet.parcours_de_vie }}</td><th>Lien familial</th><td>{{ sujet.lien_familial }}</td></tr>
|
<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>
|
</table>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{% load tables %}
|
{% load tables %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
<div class="col-md-12 col-lg-6">
|
<div class="col-lg-10">
|
||||||
<div class="panel panel-primary">
|
<div class="panel panel-primary">
|
||||||
<!-- Default panel contents -->
|
<!-- Default panel contents -->
|
||||||
<div class="panel-heading text-center">
|
<div class="panel-heading text-center">
|
||||||
<h3 class="panel-title">Sujets rencontrés</h3>
|
<h3 class="panel-title">Sujets rencontrés
|
||||||
|
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form action="" method="POST" class="form form-group">{% csrf_token %}
|
<form action="" method="POST" class="form form-group">{% csrf_token %}
|
||||||
@@ -41,19 +43,17 @@
|
|||||||
{% endif %}{% endif %}
|
{% endif %}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pull-left">
|
||||||
<div class="col-lg-3 col-md-3">
|
<div class="btn-group">
|
||||||
<div class="panel panel-default">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<!-- Default panel contents -->
|
Filtrer <span class="caret"></span>
|
||||||
<div class="panel-heading text-center">
|
</button>
|
||||||
<h3 class="panel-title">Filtres</h3>
|
<ul class="dropdown-menu">
|
||||||
</div>
|
<li><a href="{% url 'suivi:liste' %}">Pas de filtre</a></li>
|
||||||
<div class="list-group">
|
<li role="separator" class="divider"></li>
|
||||||
<a href="#" class="list-group-item disabled">Rencontrés ce mois-ci</a></li>
|
<li><a href="?filter=month-only">Rencontrés ce mois-ci</a></li>
|
||||||
<a href="#" class="list-group-item disabled"><strong>Mes accompagnements</strong></a></li>
|
<li><a href="?filter=">Hébérgé en urgence</a></li>
|
||||||
<a href="#" class="list-group-item disabled">Hébérgé en urgence</a></li>
|
<li><a href="?filter=">Sans abri</a></li>
|
||||||
<a href="#" class="list-group-item disabled">Sans logement</a></li>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,49 +6,31 @@ from .forms import SujetCreateForm
|
|||||||
|
|
||||||
### Webpage config
|
### Webpage config
|
||||||
from utilisateurs.models import Maraudeur
|
from utilisateurs.models import Maraudeur
|
||||||
from website import decorators as website
|
from website.decorators import Webpage
|
||||||
sujets = website.app_config(
|
sujets = Webpage( "suivi", menu=False, defaults={
|
||||||
name="suivi",
|
'restricted': [Maraudeur],
|
||||||
groups=[Maraudeur],
|
'ajax': True,
|
||||||
menu=["suivi/menu/sujets.html"],
|
}
|
||||||
admin_menu=["sujets/menu/admin_sujet.html"],
|
)
|
||||||
ajax=True,
|
|
||||||
)
|
|
||||||
### Views
|
### Views
|
||||||
|
|
||||||
@sujets
|
@sujets.using(title=('{{object}}', 'details'))
|
||||||
class SujetDetailsView(generic.DetailView):
|
class SujetDetailsView(generic.DetailView):
|
||||||
class PageInfo:
|
|
||||||
title = "Sujet - {{ sujet }}"
|
|
||||||
header = "{{ sujet }}"
|
|
||||||
header_small = "informations"
|
|
||||||
#DetailView
|
#DetailView
|
||||||
template_name = "sujets/sujet_details.html"
|
template_name = "sujets/sujet_details.html"
|
||||||
model = Sujet
|
model = Sujet
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@sujets
|
@sujets
|
||||||
class SujetUpdateView(generic.edit.UpdateView):
|
class SujetUpdateView(generic.edit.UpdateView):
|
||||||
class PageInfo:
|
|
||||||
title = "Mise à jour - {{sujet}}"
|
|
||||||
header = "{{sujet}}"
|
|
||||||
header_small = "mise à jour"
|
|
||||||
#UpdateView
|
#UpdateView
|
||||||
template_name = "sujets/sujet_update.html"
|
template_name = "sujets/sujet_update.html"
|
||||||
model = Sujet
|
model = Sujet
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@sujets
|
@sujets
|
||||||
class SujetCreateView(generic.edit.CreateView):
|
class SujetCreateView(generic.edit.CreateView):
|
||||||
class PageInfo:
|
|
||||||
title = "Nouveau sujet"
|
|
||||||
header = "Nouveau sujet"
|
|
||||||
#CreateView
|
#CreateView
|
||||||
template_name = "sujets/sujet_create.html"
|
template_name = "sujets/sujet_create.html"
|
||||||
form_class = SujetCreateForm
|
form_class = SujetCreateForm
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class MaraudeurAdmin(admin.ModelAdmin):
|
|||||||
('Informations', {'fields': [('first_name', 'last_name')]}),
|
('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 django.apps import AppConfig
|
||||||
|
|
||||||
|
from website.decorators import Webpage
|
||||||
|
from .models import Professionnel
|
||||||
|
|
||||||
class UtilisateursConfig(AppConfig):
|
class UtilisateursConfig(AppConfig):
|
||||||
name = 'utilisateurs'
|
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):
|
def _insert_bases(cls, bases):
|
||||||
|
""" Insert new bases in given view class """
|
||||||
old_bases = cls.__bases__
|
old_bases = cls.__bases__
|
||||||
new_bases = tuple(bases) + old_bases
|
new_bases = tuple(bases) + old_bases
|
||||||
cls.__bases__ = new_bases
|
cls.__bases__ = new_bases
|
||||||
|
|
||||||
def app_config(**options):
|
|
||||||
""" Insert per-application configuration options :
|
|
||||||
-- name : name of the app to register under in navbar
|
class Webpage:
|
||||||
-- groups : user groups needed to access this application
|
""" Webpage configurator. It is used as a decorator.
|
||||||
-- menu : user menu templates to be used
|
|
||||||
-- admin_menu : admin menu templates, only appear for superuser
|
The constructor takes one positionnal argument:
|
||||||
-- ajax : view will return content_template for Ajax requests
|
- 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 = []
|
options = [
|
||||||
if ajax:
|
('title', ('Unset', 'small header')),
|
||||||
new_bases.append(WebsiteAjaxTemplateMixin)
|
('restricted', []),
|
||||||
else:
|
('ajax', False)
|
||||||
new_bases.append(WebsiteTemplateMixin)
|
]
|
||||||
|
|
||||||
if groups: #TODO: use group instaed of user class
|
def __init__(self, app_name, icon=None, defaults={}, menu=True):
|
||||||
new_bases.append(SpecialUserRequiredMixin)
|
self.app_name = app_name
|
||||||
|
|
||||||
def class_decorator(cls):
|
if menu: # Build ApplicationMenu subclass
|
||||||
_insert_bases(cls, new_bases)
|
app_menu = type(
|
||||||
cls._user_menu = menu
|
app_name.title() + "Menu",
|
||||||
cls._admin_menu = admin_menu
|
(ApplicationMenu,),
|
||||||
cls.app_name = name
|
{'name': app_name,
|
||||||
cls.app_users = groups.copy()
|
'header': (app_name.title(), '%s:index' % app_name, icon),
|
||||||
return cls
|
'_links': [],
|
||||||
|
'_dropdowns': [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.app_menu = app_menu
|
||||||
|
else:
|
||||||
|
self.app_menu = None
|
||||||
|
|
||||||
return class_decorator
|
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 __getattr__(self, attr):
|
||||||
|
""" Return the overriden value if any, default overwise """
|
||||||
|
return self._updated.get(attr, self._defaults[attr])
|
||||||
|
|
||||||
|
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.core.exceptions import ImproperlyConfigured
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
from django.contrib.auth.decorators import user_passes_test
|
||||||
from django.template import Template, Context
|
from django.template import Template, Context
|
||||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
from django.views.generic.base import TemplateResponseMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Mixins ##
|
## 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):
|
class SpecialUserRequiredMixin(object):
|
||||||
|
""" Requires that the User is an instance of some class """
|
||||||
app_users = []
|
app_users = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
view = super().as_view(**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 !')
|
||||||
|
|
||||||
|
def check_special_user(user):
|
||||||
class TemplateFieldsMetaclass(type):
|
if isinstance(user, valid_cls):
|
||||||
""" Loads Template objects with given string for
|
return True
|
||||||
header, header_small, title, ...
|
else:
|
||||||
|
return False
|
||||||
Theses strings shall be found in cls.Template
|
return user_passes_test(check_special_user)
|
||||||
"""
|
|
||||||
def __init__(cls, bases, Dict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def user_processor(request, context):
|
def user_processor(request, context):
|
||||||
context['user_group'] = request.user.__class__.__qualname__
|
context['user_group'] = request.user.__class__.__qualname__
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def header_processor(header, context):
|
||||||
class NavbarMixin(object):
|
context['page_header'] = Template(header[0]).render(context)
|
||||||
|
context['page_header_small'] = Template(header[1]).render(context) if len(header) == 2 else ''
|
||||||
registered_apps = ['maraudes', 'suivi']
|
context['page_title'] = " - ".join((context['page_header'], context['page_header_small']))
|
||||||
app_name = None
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_apps_config(self):
|
class WebsiteTemplateMixin(TemplateResponseMixin):
|
||||||
""" 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):
|
|
||||||
""" Mixin for easy integration of 'website' templates
|
""" 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
|
If 'content_template' is not defined, value will fallback to template_name
|
||||||
in child view.
|
in child view.
|
||||||
"""
|
"""
|
||||||
base_template = "base_site.html"
|
base_template = "base_site.html"
|
||||||
content_template = None
|
content_template = None
|
||||||
|
app_name = None
|
||||||
_user_menu = []
|
|
||||||
_admin_menu = []
|
|
||||||
_groups = []
|
|
||||||
|
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
stylesheets = ['base.css']
|
stylesheets = ['css/base.css']
|
||||||
page_blocks = ['header', 'header_small', 'title']
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.user = None
|
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):
|
def get_template_names(self):
|
||||||
""" Ensure same template for all children views. """
|
""" Ensure same template for all children views. """
|
||||||
@@ -159,30 +72,20 @@ class WebsiteTemplateMixin(NavbarMixin, TemplateResponseMixin):
|
|||||||
raise ImproperlyConfigured(self, "has no template defined !")
|
raise ImproperlyConfigured(self, "has no template defined !")
|
||||||
return self.content_template
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = Context(super().get_context_data(**kwargs))
|
||||||
self._update_context_with_rendered_blocks(context)
|
|
||||||
#Website processor
|
#Website processor
|
||||||
context['stylesheets'] = self.Configuration.stylesheets
|
context['stylesheets'] = self.Configuration.stylesheets
|
||||||
context['apps'] = self.apps
|
context['active_app'] = self.app_name # Set by Webpage decorator
|
||||||
context['active_app'] = self.active_app
|
|
||||||
# User processor
|
# User processor
|
||||||
|
context = header_processor(self.header, context)
|
||||||
context = user_processor(self.request, context)
|
context = user_processor(self.request, context)
|
||||||
#Webpage
|
#Webpage
|
||||||
context['content_template'] = self.get_content_template()
|
context['content_template'] = self.get_content_template()
|
||||||
context['app_menu'] = self.menu
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteAjaxTemplateMixin(WebsiteTemplateMixin):
|
class WebsiteAjaxTemplateMixin(WebsiteTemplateMixin):
|
||||||
""" Mixin that returns content_template instead of base_template when
|
""" Mixin that returns content_template instead of base_template when
|
||||||
request is Ajax.
|
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 {
|
.navbar-fixed-side .navbar-nav>li>a {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
font-variant: small-caps;
|
font-variant: small-caps;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
border: none;
|
||||||
|
border-right: 4px solid #980300;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:768px){
|
||||||
|
#menu { border: none; }
|
||||||
|
}
|
||||||
|
|
||||||
.app-menu {
|
.app-menu {
|
||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
@@ -25,10 +21,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width:768px){
|
.active{
|
||||||
#menu { border: none; }
|
border-right: 2px solid #980300 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron {
|
.dropdown-menu {
|
||||||
background-color: #fefefe;
|
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>
|
<!DOCTYPE html>
|
||||||
{% load staticfiles %} {% load bootstrap3 %}
|
{% load staticfiles %} {% load bootstrap3 %} {% load navbar %}
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}La maraude{% endblock %}</title>
|
<title>{% block title %}La maraude{% endblock %}</title>
|
||||||
{% bootstrap_css %}{% bootstrap_javascript %}
|
{% bootstrap_css %}{% bootstrap_javascript %}
|
||||||
<!-- Side Navbar from http://www.samrayner.com/bootstrap-side-navbar/inverse.html -->
|
<!-- 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 %}
|
{% if stylesheets %}{% for stylesheet in stylesheets %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static stylesheet %}" />{% endfor %}{% endif %}
|
<link rel="stylesheet" type="text/css" href="{% static stylesheet %}" />{% endfor %}{% endif %}
|
||||||
</head>
|
</head>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3 col-lg-2">
|
<div class="col-md-3 col-lg-2">
|
||||||
{% include "navbar.html" %}
|
{% navbar %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9 col-lg-10">
|
<div class="col-md-9 col-lg-10">
|
||||||
<h1 class="page-header">{% block page_header %}{% endblock %}</h1>
|
<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 maraudes import urls as maraudes_urls
|
||||||
from suivi import urls as suivi_urls
|
from suivi import urls as suivi_urls
|
||||||
from sujets import urls as sujets_urls
|
from sujets import urls as sujets_urls
|
||||||
|
from utilisateurs import urls as utilisateurs_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Authentification
|
# Authentification
|
||||||
@@ -19,4 +20,5 @@ urlpatterns = [
|
|||||||
url(r'^maraudes/', include(maraudes_urls, namespace="maraudes")),
|
url(r'^maraudes/', include(maraudes_urls, namespace="maraudes")),
|
||||||
url(r'^suivi/', include(suivi_urls, namespace="suivi")),
|
url(r'^suivi/', include(suivi_urls, namespace="suivi")),
|
||||||
url(r'^sujets/', include(sujets_urls, namespace="sujets")),
|
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"
|
template_name = "main.html"
|
||||||
app_menu = None
|
app_menu = None
|
||||||
|
header = ('La Maraude ALSA', 'accueil')
|
||||||
class PageInfo:
|
class PageInfo:
|
||||||
title = "La maraude ALSA"
|
title = "La maraude ALSA"
|
||||||
header = "La Maraude ALSA"
|
header = "La Maraude ALSA"
|
||||||
|
|||||||
Reference in New Issue
Block a user