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:
artus40
2017-06-11 17:16:17 +02:00
committed by GitHub
parent 0be59a61a7
commit be087464fc
155 changed files with 3568 additions and 1988 deletions

View File

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -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)

View File

@@ -0,0 +1,5 @@
from django.utils import timezone as tz
def website_processor(request):
return {'date': tz.now().date}

View File

@@ -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

View File

@@ -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)

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -432,4 +432,4 @@
"transition.js"
],
"customizerUrl": "http://getbootstrap.com/customize/?id=7f853f3d936c9ba68499a06009229bc9"
}
}

View File

@@ -22,7 +22,8 @@
}
},
error: function (xhr, ajaxOptions, thrownError) {
// handle response errors here
console.log("Error with ajax request : ");
console.log(thrownError);
}
});
});

File diff suppressed because one or more lines are too long

View File

@@ -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')
});
});

View File

@@ -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" %}&nbsp; Maraudes</a></li>
<li {% active namespace="notes" %}><a href="{% url "notes:index" %}">{% bootstrap_icon "pencil" %}&nbsp; Notes</a></li>
{% else %}
<li {% active namespace="statistiques" %}><a href="{% url "statistiques:index" %}">{% bootstrap_icon "stats" %}&nbsp; 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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -1 +0,0 @@
<h1>{{ title }}</h1>

View File

@@ -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 %} &middot; <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" %} &middot; {{user}} </strong>&middot; {{ user_group }}<span class="caret"></span></a>
<ul class="dropdown-menu" aria-labelledby="UserMenu">
{% if next %}
<p class="well-sm text-center"><strong style="color:#980300;">Vous n'avez pas l'autorisation<br/> d'accéder à cette page.</strong></p>
{%endif%}
{% if user.is_authenticated %}
{% if user.is_superuser %}<li><a href="{% url 'admin:index' %}">{% bootstrap_icon "wrench" %} Administration</a></li>{% endif %}
<li><a href="{% url 'logout' %}">{% bootstrap_icon "log-out" %} Déconnecter</a></li>
{% else %}
<li>
<form class="navbar-form navbar-left" method="post" action="/login/">{% csrf_token %}
{% if next %}
<input name="next" value="{{next}}" hidden />
{% endif %}
<div class="form-group form-horizontal">
<div class="form-group">
<label class="col-md-2 sr-only control-label" for="id_username">Username</label>
<div class="col-md-10">
<input autofocus="" class="form-control" id="id_username" maxlength="254" name="username" placeholder="Username" title="" type="text" required />
</div>
</div>
<div class="form-group">
<label class="col-md-2 sr-only control-label" for="id_password">Password</label>
<div class="col-md-10"><input class="form-control" id="id_password" name="password" placeholder="Password" title="" type="password" required />
</div>
</div>
</div>
<div class="text-center"><button class="btn btn-primary navbar-button" type="submit">Connexion</button></div>
</form>
</li>
{% endif %}
</ul>
</li>
</ul>
</div>
</div>
</nav>

View File

@@ -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>

View File

@@ -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 ""

View File

@@ -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
))
}

View File

@@ -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)

View File

@@ -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")),
]

View File

@@ -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('/')