Remaster (#38)
* setup new 'statistiques' module * added 'graphos' package and created first test graph * put graphos in requirements, deleted local folder * added "load_csv" management command ! * added update of premiere_rencontre field in 'load_csv' management command * added missing urls.py file * added 'merge' action and view * added 'info_completed' ratio * linked sujets:merge views inside suivi:details * added link to maraudes:details in notes table headers, if any * Major reorganisation, moved 'suivi' and 'sujets' to 'notes', cleanup in 'maraudes', dropping 'website' mixins (mostly useless) * small cleanup * worked on Maraude and Sujet lists * corrected missing line in notes.__init__ * restored 'details' view for maraudes and sujets insie 'notes' module * worked on 'notes': added navigation between maraude's compte-rendu, right content in details, header to list tables * changed queryset for CompteRenduDetailsView to all notes of same date, minor layout changes * added right content to 'details-sujet', created 'statistiques' view and update templates * restored 'statistiques' ajax view in 'details-sujet', fixed 'merge_two' util function * added auto-creation of FicheStatistique (plus some tests), pagination for notes in 'details-sujet' * added error-prone cases in paginator * fixed non-working modals, added titles * added UpdateStatistiques capacity in CompteRenduCreate view * fixed missing AjaxTemplateMixin for CreateSujetView, worked on compte-rendu creation scripts * fixed MaraudeManager.all_of() for common Maraudeurs, added color hints in planning * re-instated statistiques module link and first test page * added FinalizeView to send a mail before finalizing compte-rendu * Added PieChart view for FicheStatistique fields * small style updates, added 'age' and 'genre' fields from sujets in statistiques.PieChartView * worked on statistiques, fixed small issues in 'notes' list views * small theme change * removed some dead code * fixed notes.tests, fixed statistiques.info_completed display, added filter in SujetLisView * added some tests * added customised admin templates * added authenticate in CustomAuthenticatationBackend, more verbose login thanks to messages * added django-nose for test coverage * Corrected raising exception on first migration On first migration, qs.exists() would previously be called and raising an Exception, sot he migrations would fail. * Better try block * cleaned up custom settings.py, added some overrides of django base_settings * corrected bad dictionnary key
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from utilisateurs.models import Maraudeur
|
||||
|
||||
|
||||
def user_models():
|
||||
return (Maraudeur,)
|
||||
|
||||
class MyBackend(ModelBackend):
|
||||
|
||||
def get_user(self, user_id):
|
||||
""" Essaye de récupérer une classe enfant de User existante, telle que
|
||||
définie dans 'utilisateurs.models'. Fallback to default user.
|
||||
"""
|
||||
for user_model in user_models():
|
||||
try:
|
||||
return user_model.objects.get(pk=user_id)
|
||||
except user_model.DoesNotExist:
|
||||
print('Tried %s.' % user_model.__class__)
|
||||
return super().get_user(user_id)
|
||||
|
||||
def has_perm(self, *args, **kwargs):
|
||||
print('call has_perm', args, kwargs)
|
||||
return super().has_perm(*args, **kwargs)
|
||||
5
website/context_processors.py
Normal file
5
website/context_processors.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.utils import timezone as tz
|
||||
|
||||
def website_processor(request):
|
||||
return {'date': tz.now().date}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
from .mixins import (WebsiteTemplateMixin, WebsiteAjaxTemplateMixin,
|
||||
SpecialUserRequiredMixin)
|
||||
from .navbar import ApplicationMenu
|
||||
|
||||
|
||||
|
||||
def _insert_bases(cls, bases):
|
||||
""" Insert new bases in given view class """
|
||||
old_bases = cls.__bases__
|
||||
new_bases = tuple(bases) + old_bases
|
||||
cls.__bases__ = new_bases
|
||||
|
||||
|
||||
|
||||
class Webpage:
|
||||
""" Webpage configurator. It is used as a decorator.
|
||||
|
||||
The constructor takes one positionnal argument:
|
||||
- app_name : name of the application where this view shall be categorized.
|
||||
and keyword arguments:
|
||||
- defaults : mapping of default options.
|
||||
- menu : does it register a menu ? default is True
|
||||
- icon : bootstrap name of menu header icon, ignored if 'menu' is False.
|
||||
|
||||
Options are :
|
||||
- title: tuple of (header, header_small), header_small is optionnal.
|
||||
- restricted: set of group to which access is restricted.
|
||||
- ajax: can this view be called as ajax ?
|
||||
|
||||
"""
|
||||
|
||||
options = [
|
||||
('title', ('Unset', 'small header')),
|
||||
('restricted', []),
|
||||
('ajax', False)
|
||||
]
|
||||
|
||||
def __init__(self, app_name, icon=None, defaults={}, menu=True):
|
||||
self.app_name = app_name
|
||||
|
||||
if menu: # Build ApplicationMenu subclass
|
||||
app_menu = type(
|
||||
app_name.title() + "Menu",
|
||||
(ApplicationMenu,),
|
||||
{'name': app_name,
|
||||
'header': (app_name.title(), '%s:index' % app_name, icon),
|
||||
'_links': [],
|
||||
'_dropdowns': [],
|
||||
}
|
||||
)
|
||||
self.app_menu = app_menu
|
||||
else:
|
||||
self.app_menu = None
|
||||
|
||||
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,102 +1,13 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.template import Template, Context
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
|
||||
## Mixins ##
|
||||
|
||||
|
||||
class SpecialUserRequiredMixin(object):
|
||||
""" Requires that the User is an instance of some class """
|
||||
app_users = []
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
view = super().as_view(**initkwargs)
|
||||
return 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):
|
||||
if isinstance(user, valid_cls):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return user_passes_test(check_special_user)
|
||||
|
||||
|
||||
def user_processor(request, context):
|
||||
context['user_group'] = request.user.__class__.__qualname__
|
||||
return context
|
||||
|
||||
def header_processor(header, context):
|
||||
context['page_header'] = Template(header[0]).render(context)
|
||||
context['page_header_small'] = Template(header[1]).render(context) if len(header) == 2 else ''
|
||||
context['page_title'] = " - ".join((context['page_header'], context['page_header_small']))
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class WebsiteTemplateMixin(TemplateResponseMixin):
|
||||
""" Mixin for easy integration of 'website' templates
|
||||
|
||||
If 'content_template' is not defined, value will fallback to template_name
|
||||
in child view.
|
||||
"""
|
||||
base_template = "base_site.html"
|
||||
content_template = None
|
||||
app_name = None
|
||||
|
||||
class Configuration:
|
||||
stylesheets = ['css/base.css']
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
|
||||
def get_template_names(self):
|
||||
""" Ensure same template for all children views. """
|
||||
return [self.base_template]
|
||||
|
||||
def get_content_template(self):
|
||||
# Ensure easy integration with generic views
|
||||
if hasattr(self, 'template_name'):
|
||||
self.content_template = self.template_name
|
||||
else:
|
||||
raise ImproperlyConfigured(self, "has no template defined !")
|
||||
return self.content_template
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = Context(super().get_context_data(**kwargs))
|
||||
#Website processor
|
||||
context['stylesheets'] = self.Configuration.stylesheets
|
||||
context['active_app'] = self.app_name # Set by Webpage decorator
|
||||
# User processor
|
||||
context = header_processor(self.header, context)
|
||||
context = user_processor(self.request, context)
|
||||
#Webpage
|
||||
context['content_template'] = self.get_content_template()
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class WebsiteAjaxTemplateMixin(WebsiteTemplateMixin):
|
||||
class AjaxTemplateMixin:
|
||||
""" Mixin that returns content_template instead of base_template when
|
||||
request is Ajax.
|
||||
"""
|
||||
is_ajax = False
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not hasattr(self, 'content_template') or not self.content_template:
|
||||
self.content_template = self.get_content_template()
|
||||
if not hasattr(self, 'ajax_template'):
|
||||
self.ajax_template = '%s_inner.html' % self.content_template.split(".")[0]
|
||||
self.ajax_template = '%s_inner.html' % self.template_name.split(".")[0]
|
||||
if request.is_ajax():
|
||||
self.is_ajax = True
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,102 +0,0 @@
|
||||
""" 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)
|
||||
|
||||
@@ -1,32 +1,127 @@
|
||||
|
||||
.navbar-fixed-side .navbar-nav>li>a {
|
||||
border-bottom: none;
|
||||
font-variant: small-caps;
|
||||
color: #fff;
|
||||
body {
|
||||
padding: 0px 0 10px 0;
|
||||
}
|
||||
|
||||
#menu {
|
||||
border: none;
|
||||
border-right: 4px solid #980300;
|
||||
background-color: #121212;
|
||||
#page-header {
|
||||
font-size:1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width:768px){
|
||||
#menu { border: none; }
|
||||
.navbar-text.breadcrumb {
|
||||
padding: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
background-color: #121212;
|
||||
border: none;
|
||||
.navbar-text.breadcrumb > li {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.navbar-text.breadcrumb > li > a {
|
||||
color: #e2f2f2;
|
||||
}
|
||||
|
||||
.navbar-text.breadcrumb > li > a:hover {
|
||||
color: #d9230f;
|
||||
}
|
||||
|
||||
/* Admin overrides */
|
||||
|
||||
#content-related {
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
|
||||
|
||||
.active{
|
||||
border-right: 2px solid #980300 !important;
|
||||
/* Bootstrap Navbar custom */
|
||||
|
||||
.navbar-default {
|
||||
background-color: #2e2f2f;
|
||||
border-color: #a91b0c;
|
||||
}
|
||||
.navbar-default .navbar-brand {
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-brand:hover,
|
||||
.navbar-default .navbar-brand:focus {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-text {
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > a {
|
||||
color: #f6f6f6;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
min-height: 45px;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > a:hover,
|
||||
.navbar-default .navbar-nav > li > a:focus {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu {
|
||||
background-color: #2e2f2f;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > a {
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > a:hover,
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-nav > li > .dropdown-menu > li > .divider {
|
||||
background-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-nav > .active > a,
|
||||
.navbar-default .navbar-nav > .active > a:hover,
|
||||
.navbar-default .navbar-nav > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-nav > .open > a,
|
||||
.navbar-default .navbar-nav > .open > a:hover,
|
||||
.navbar-default .navbar-nav > .open > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-toggle {
|
||||
border-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-toggle:hover,
|
||||
.navbar-default .navbar-toggle:focus {
|
||||
background-color: #d2220f;
|
||||
}
|
||||
.navbar-default .navbar-toggle .icon-bar {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-collapse,
|
||||
.navbar-default .navbar-form {
|
||||
border-color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-link {
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-link:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-bottom: none !important;
|
||||
@media (max-width: 767px) {
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > li > a {
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
|
||||
color: #ffffff;
|
||||
}
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #d2220f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -432,4 +432,4 @@
|
||||
"transition.js"
|
||||
],
|
||||
"customizerUrl": "http://getbootstrap.com/customize/?id=7f853f3d936c9ba68499a06009229bc9"
|
||||
}
|
||||
}
|
||||
|
||||
3
website/static/scripts/bootstrap-modal.js
vendored
3
website/static/scripts/bootstrap-modal.js
vendored
@@ -22,7 +22,8 @@
|
||||
}
|
||||
},
|
||||
error: function (xhr, ajaxOptions, thrownError) {
|
||||
// handle response errors here
|
||||
console.log("Error with ajax request : ");
|
||||
console.log(thrownError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
8
website/static/scripts/jquery.flot.min.js
vendored
Normal file
8
website/static/scripts/jquery.flot.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,8 +1,7 @@
|
||||
$(function() {
|
||||
|
||||
var input = $('#id_heure_debut')
|
||||
var min_value = input.attr('value').split(":")
|
||||
var is_morning = (min_value[0] <= 12)
|
||||
|
||||
$.fn.editHeureValue = function(mod) {
|
||||
var value = input.attr('value').split(":");
|
||||
var new_hour = parseInt(value[0]);
|
||||
@@ -19,23 +18,19 @@
|
||||
} else if (new_hour < 0) {
|
||||
new_hour += 24
|
||||
};
|
||||
|
||||
value[0] = new_hour;
|
||||
value[1] = new_minutes;
|
||||
var test_value = value[0] * 10000 + value[1] * 100 + parseInt(value[2]);
|
||||
var test_min_value = min_value[0] * 10000 + min_value[1] *100 + parseInt(min_value[2]);
|
||||
console.log('test:', test_value, 'min:', test_min_value)
|
||||
if (test_value >= test_min_value || (!is_morning && test_value < 120000)) {
|
||||
input.attr('value', value.join(":"));
|
||||
console.log('updated!')
|
||||
};
|
||||
input.attr('value', value.join(":"));
|
||||
|
||||
};
|
||||
|
||||
$('#minus-5').click(function() {
|
||||
$.fn.editHeureValue(-5)
|
||||
console.log('minus 5')
|
||||
//console.log('minus 5')
|
||||
});
|
||||
$('#plus-5').click(function() {
|
||||
$.fn.editHeureValue(5)
|
||||
console.log('plus 5')
|
||||
//console.log('plus 5')
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,21 +4,74 @@
|
||||
<head>
|
||||
<title>{% block title %}La maraude{% endblock %}</title>
|
||||
{% bootstrap_css %}{% bootstrap_javascript %}
|
||||
<!-- Side Navbar from http://www.samrayner.com/bootstrap-side-navbar/inverse.html -->
|
||||
<link href="/static/css/bootstrap/navbar-fixed-side.css" rel="stylesheet" />
|
||||
{% if stylesheets %}{% for stylesheet in stylesheets %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static stylesheet %}" />{% endfor %}{% endif %}
|
||||
<link rel="stylesheet" type="text/css" href="/static/css/base.css" />
|
||||
{% block extrastyle %}{% endblock %}
|
||||
{% if stylesheets %}{% for stylesheet in stylesheets %}<link rel="stylesheet" type="text/css" href="{% static stylesheet %}" />{% endfor %}{% endif %}
|
||||
{% block extrahead %}{% endblock %}
|
||||
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body {% block extra_body_attrs %}{% endblock %}>
|
||||
<div class="container-fluid">
|
||||
<!-- START: Navigation Bar -->
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2">
|
||||
{% navbar %}
|
||||
</div>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<h1 class="page-header">{% block page_header %}{% endblock %}</h1>
|
||||
<nav class="navbar navbar-static-top navbar-default">
|
||||
<div class="container-fluid">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false">
|
||||
<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="/">Maraude ALSA</a>
|
||||
</div>
|
||||
<!-- Collect the nav links, forms, and other content for toggling -->
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<ul class="nav navbar-nav navbar-left">
|
||||
{% if user.is_authenticated %}
|
||||
<li {% active namespace="maraudes" %}><a href="{% url "maraudes:index" %}">{% bootstrap_icon "road" %} Maraudes</a></li>
|
||||
<li {% active namespace="notes" %}><a href="{% url "notes:index" %}">{% bootstrap_icon "pencil" %} Notes</a></li>
|
||||
{% else %}
|
||||
<li {% active namespace="statistiques" %}><a href="{% url "statistiques:index" %}">{% bootstrap_icon "stats" %} Statistiques</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ol class="breadcrumb navbar-text">
|
||||
{% block breadcrumbs %}
|
||||
<li>{{ page_header }}</li>
|
||||
{% if page_header_small %}<li>{{ page_header_small }}</li>{% endif %}
|
||||
{% endblock %}
|
||||
</ol>
|
||||
{% if user.is_authenticated %}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
Menu <span class="glyphicon glyphicon-menu-hamburger"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url "statistiques:index" %}"><span class="glyphicon glyphicon-stats"></span> Statistiques</a></li>
|
||||
{% if user.is_superuser %}
|
||||
<li><a href="/admin/"><span class="glyphicon glyphicon-wrench"></span> Administration</a></li>
|
||||
{% endif %}
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="{% url "utilisateurs:index" %}"><span class="glyphicon glyphicon-user"></span> {{ user }}</a></li>
|
||||
<li><a href="/logout/"><span class="glyphicon glyphicon-log-out"></span> Déconnexion</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div><!-- /.navbar-collapse -->
|
||||
</div><!-- /.container-fluid -->
|
||||
</nav>
|
||||
</div>
|
||||
<!-- END: Navigation Bar -->
|
||||
<div class="row">
|
||||
<div class="col-lg-10 col-lg-push-2 col-md-9 col-md-push-3">
|
||||
{% bootstrap_messages %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block page_content %}{% endblock %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-lg-pull-10 col-md-3 col-md-pull-9">
|
||||
{% block sidebar %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ page_title }}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<span style="color:#980300;">{{ page_header }}</span>
|
||||
<small>{{ page_header_small }}</small>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include content_template %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<div class="col-md-12 col-lg-6">
|
||||
{% extends "base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include "login.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="jumbotron">
|
||||
<h2>Objectifs</h2>
|
||||
<p>Description de la maraude à destination des visiteurs, partenaires, etc...</p>
|
||||
@@ -8,5 +15,5 @@
|
||||
et les <b>vendredis</b> en fin d'après-midi.
|
||||
</p>
|
||||
<p>Ils sont reconnaissables à leur vestes oranges, n'hésitez pas à les interpeller.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,41 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{# Tweak columns layout for login box %}
|
||||
{% block panels %}<div class="col-md-8 col-md-offset-2">{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading"><h3 class="panel-title">Connexion</h3></div>
|
||||
<div class="panel-body text-center">
|
||||
{% if user.is_authenticated %}
|
||||
<p>Bienvenue {{ user.first_name|default:user.username }} !</p>
|
||||
{% if next %}
|
||||
<div class="alert alert-warning"><p>Votre compte ne donne pas accès à cette page. Veuillez vous
|
||||
connecter avec un autre compte.</p></div>
|
||||
<a href="{% url 'logout' %}" class="btn btn-danger">Déconnexion</a>
|
||||
{% else %}
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'maraudes:index' %}" class="btn btn-primary">Entrer</a>
|
||||
{% if user.is_superuser %}
|
||||
<a href="admin/" class="btn btn-warning">Administration</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'logout' %}" class="btn btn-danger">Déconnexion</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if next %}
|
||||
<div class="alert alert-danger"><p>Veuillez vous connecter pour accéder à cette page.</p></div>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% bootstrap_button "Connexion" button_type="submit" button_class="btn-lg btn-primary" %}
|
||||
<input type="hidden" name="next" value="{{ next|default:'/maraudes/' }}" />
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Connexion</h3>
|
||||
</div>
|
||||
<div class="panel-body text-center">
|
||||
{% if user.is_authenticated %}
|
||||
<p>{{ user.first_name|default:user.username }}, vous êtes connecté !</p>
|
||||
{% if next %}
|
||||
<div class="alert alert-warning">
|
||||
<p>Votre compte ne donne pas accès à cette page. Veuillez vous connecter avec un autre compte.</p>
|
||||
</div>
|
||||
<a href="{% url 'logout' %}" class="btn btn-danger">Déconnexion</a>
|
||||
{% else %}
|
||||
<div class="btn-group-vertical">
|
||||
<a href="{% url 'maraudes:index' %}" class="btn btn-sm btn-primary">Entrer</a>
|
||||
{% if user.is_superuser %}
|
||||
<a href="admin/" class="btn btn-sm btn-default">Administration</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'logout' %}" class="btn btn-sm btn-default">Déconnexion</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<form class="form" method="post" action="{% url "login" %}">
|
||||
{% csrf_token %}
|
||||
{% if next %}
|
||||
<div class="alert alert-warning">
|
||||
<p>Vous devez vous connecter pour accéder à cette page.</p>
|
||||
</div>
|
||||
<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="input-group col-md-9">
|
||||
<span class="input-group-addon"><span class="glyphicon glyphicon-user"></span></span>
|
||||
<input autofocus="" class="form-control" id="id_username" maxlength="254" name="username" placeholder="Username" title="" type="text" required />
|
||||
</div>
|
||||
<div class="panel-footer"><p>version: 0.01</p></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 sr-only control-label" for="id_password">Password</label>
|
||||
<div class="input-group col-md-9">
|
||||
<span class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>
|
||||
<input class="form-control" id="id_password" name="password" placeholder="Password" title="" type="password" required />
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary navbar-button" type="submit">Connexion</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-footer"><p>version: 0.2beta</p></div>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<h1>{{ title }}</h1>
|
||||
@@ -1,60 +0,0 @@
|
||||
{% load bootstrap3 %}{% load staticfiles %}
|
||||
<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 %}{% if not app.disabled %}
|
||||
<li class="{% if app == active_app %}active{%endif%}">
|
||||
<a href="/{{app.label}}/">{% bootstrap_icon app.menu_icon %} · <strong>{{ app.name|title }}</strong></a>
|
||||
</li>
|
||||
{% if app == active_app %}{% for template in app_menu %}{% include template %}{% endfor %}{% endif %}
|
||||
{% endif %}{%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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,13 +1,10 @@
|
||||
<table class="table table-condensed">
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
{% for object in row %}
|
||||
<td>
|
||||
{% if object %}
|
||||
{% include cell_template with object=object %}
|
||||
{%endif%}
|
||||
</td>
|
||||
<table class="table table-condensed table-striped">
|
||||
{% if header %}<tr><th colspan="{{ cols_number }}" class="text-center">{{ header }}</th></tr>{% endif %}
|
||||
{% for row in rows %}<tr>
|
||||
{% for object in row %}<td>
|
||||
{% if object %}{% include cell_template with object=object %}{%endif%}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -48,4 +50,14 @@ def navbar_menu(app_menu):
|
||||
}
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def active(context, namespace=None, viewname=None):
|
||||
try:
|
||||
(cur_namespace, cur_viewname) = context.request.resolver_match.view_name.split(":")
|
||||
except:
|
||||
(cur_namespace, cur_viewname) = (None, context.request.resolver_match.view_name)
|
||||
|
||||
if namespace == cur_namespace:
|
||||
if not viewname or viewname == cur_viewname:
|
||||
return mark_safe("class=\"active\"")
|
||||
return ""
|
||||
|
||||
@@ -11,21 +11,14 @@ def get_columns(iterable, cols):
|
||||
yield iterable[i*cols_len:(i+1)*cols_len]
|
||||
|
||||
@register.inclusion_tag("tables/table.html")
|
||||
def table(object_list, cols=2, cell_template="tables/table_cell.html"):
|
||||
def table(object_list, cols=2, cell_template="tables/table_cell_default.html", header=None):
|
||||
""" Render object list in table of given columns number """
|
||||
return {
|
||||
'cell_template': cell_template,
|
||||
'cols_number': cols,
|
||||
'header': header,
|
||||
'rows': tuple(zip_longest( *get_columns(object_list, cols),
|
||||
fillvalue=None
|
||||
))
|
||||
}
|
||||
|
||||
@register.inclusion_tag("tables/header_table.html")
|
||||
def header_table(object_list, cols=2):
|
||||
""" Display object list in table of given columns number """
|
||||
return {
|
||||
'cols': cols,
|
||||
'rows': tuple(zip_longest( *get_columns(object_list, cols),
|
||||
fillvalue=None
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from utilisateurs.models import Maraudeur
|
||||
# Create your tests here.
|
||||
|
||||
class RestrictedAccessAnonymousUserTestCase(TestCase):
|
||||
|
||||
modules = ["maraudes", "notes", "utilisateurs"]
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_access_restricted_modules(self):
|
||||
for mod in self.modules:
|
||||
url = "/%s/" % mod
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/?next=%s" % url)
|
||||
|
||||
|
||||
class RestrictedAccessConnectedMaraudeurTestCase(TestCase):
|
||||
modules = ["maraudes", "notes/sujets"]
|
||||
def setUp(self):
|
||||
m = Maraudeur.objects.create(first_name="Astérix", last_name="LeGaulois")
|
||||
self.client = Client()
|
||||
self.client.force_login(m)
|
||||
|
||||
def test_access_restricted_modules(self):
|
||||
for mod in self.modules:
|
||||
url = "/%s/" % mod
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
class NonRestrictedAccessTestCase(TestCase):
|
||||
|
||||
urls = ["/statistiques/", "/"]
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_access(self):
|
||||
for url in self.urls:
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -4,21 +4,20 @@ from django.contrib.auth import views as auth_views
|
||||
|
||||
from .views import Index, login_view
|
||||
from maraudes import urls as maraudes_urls
|
||||
from suivi import urls as suivi_urls
|
||||
from sujets import urls as sujets_urls
|
||||
from notes import urls as notes_urls
|
||||
from utilisateurs import urls as utilisateurs_urls
|
||||
from statistiques import urls as stats_urls
|
||||
|
||||
urlpatterns = [
|
||||
# Authentification
|
||||
url(r'^$', Index.as_view(), name="index"),
|
||||
url(r'^login/$', login_view),
|
||||
url(r'^login/$', login_view, name="login"),
|
||||
url(r'^logout/$', auth_views.logout, {
|
||||
'template_name': 'logout.html',
|
||||
'next_page': 'index',
|
||||
}, name="logout"),
|
||||
# Applications
|
||||
url(r'^maraudes/', include(maraudes_urls, namespace="maraudes")),
|
||||
url(r'^suivi/', include(suivi_urls, namespace="suivi")),
|
||||
url(r'^sujets/', include(sujets_urls, namespace="sujets")),
|
||||
url(r'^notes/', include(notes_urls, namespace="notes")),
|
||||
url(r'^utilisateurs/', include(utilisateurs_urls, namespace="utilisateurs")),
|
||||
url(r'^statistiques/', include(stats_urls, namespace="statistiques")),
|
||||
]
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django import views
|
||||
from .mixins import WebsiteTemplateMixin
|
||||
|
||||
from django.contrib.auth import login, authenticate
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
||||
|
||||
class Index(WebsiteTemplateMixin, views.generic.TemplateView):
|
||||
class Index(views.generic.TemplateView):
|
||||
|
||||
template_name = "main.html"
|
||||
template_name = "index.html"
|
||||
app_menu = None
|
||||
header = ('La Maraude ALSA', 'accueil')
|
||||
class PageInfo:
|
||||
title = "La maraude ALSA"
|
||||
header = "La Maraude ALSA"
|
||||
header_small = "accueil"
|
||||
|
||||
http_method_names = ['get',]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -25,7 +22,9 @@ class Index(WebsiteTemplateMixin, views.generic.TemplateView):
|
||||
|
||||
def _get_entry_point(user):
|
||||
from utilisateurs.models import Maraudeur
|
||||
from utilisateurs.backends import CustomUserAuthentication
|
||||
|
||||
print("Entry point for ", user, user.__class__)
|
||||
if isinstance(user, Maraudeur):
|
||||
return reverse('maraudes:index')
|
||||
else:
|
||||
@@ -43,6 +42,9 @@ def login_view(request):
|
||||
next = request.POST.get('next', None)
|
||||
if not next:
|
||||
next = _get_entry_point(user)
|
||||
messages.success(request, "%s, vous êtes connecté !" % user)
|
||||
return HttpResponseRedirect(next)
|
||||
else:
|
||||
messages.error(request, "Le nom d'utilisateur et/ou le mot de passe sont incorrects !")
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user