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:
@@ -1,39 +1,93 @@
|
||||
from .mixins import *
|
||||
from .mixins import (WebsiteTemplateMixin, WebsiteAjaxTemplateMixin,
|
||||
SpecialUserRequiredMixin)
|
||||
from .navbar import ApplicationMenu
|
||||
|
||||
|
||||
|
||||
def _insert_bases(cls, bases):
|
||||
""" Insert new bases in given view class """
|
||||
old_bases = cls.__bases__
|
||||
new_bases = tuple(bases) + old_bases
|
||||
cls.__bases__ = new_bases
|
||||
|
||||
def app_config(**options):
|
||||
""" Insert per-application configuration options :
|
||||
-- name : name of the app to register under in navbar
|
||||
-- groups : user groups needed to access this application
|
||||
-- menu : user menu templates to be used
|
||||
-- admin_menu : admin menu templates, only appear for superuser
|
||||
-- ajax : view will return content_template for Ajax requests
|
||||
|
||||
|
||||
class Webpage:
|
||||
""" Webpage configurator. It is used as a decorator.
|
||||
|
||||
The constructor takes one positionnal argument:
|
||||
- app_name : name of the application where this view shall be categorized.
|
||||
and keyword arguments:
|
||||
- defaults : mapping of default options.
|
||||
- menu : does it register a menu ? default is True
|
||||
- icon : bootstrap name of menu header icon, ignored if 'menu' is False.
|
||||
|
||||
Options are :
|
||||
- title: tuple of (header, header_small), header_small is optionnal.
|
||||
- restricted: set of group to which access is restricted.
|
||||
- ajax: can this view be called as ajax ?
|
||||
|
||||
"""
|
||||
name = options.pop('name', None)
|
||||
groups = options.pop('groups', []) #Transition from app_users
|
||||
menu = options.pop('menu', [])
|
||||
admin_menu = options.pop('admin_menu', [])
|
||||
ajax = options.pop('ajax', False)
|
||||
|
||||
new_bases = []
|
||||
if ajax:
|
||||
new_bases.append(WebsiteAjaxTemplateMixin)
|
||||
else:
|
||||
new_bases.append(WebsiteTemplateMixin)
|
||||
options = [
|
||||
('title', ('Unset', 'small header')),
|
||||
('restricted', []),
|
||||
('ajax', False)
|
||||
]
|
||||
|
||||
if groups: #TODO: use group instaed of user class
|
||||
new_bases.append(SpecialUserRequiredMixin)
|
||||
def __init__(self, app_name, icon=None, defaults={}, menu=True):
|
||||
self.app_name = app_name
|
||||
|
||||
def class_decorator(cls):
|
||||
_insert_bases(cls, new_bases)
|
||||
cls._user_menu = menu
|
||||
cls._admin_menu = admin_menu
|
||||
cls.app_name = name
|
||||
cls.app_users = groups.copy()
|
||||
return cls
|
||||
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
|
||||
|
||||
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.apps import apps
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.template import Template, Context
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
||||
|
||||
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
|
||||
## Mixins ##
|
||||
|
||||
def special_user_required(authorized_users):
|
||||
|
||||
valid_cls = tuple(authorized_users)
|
||||
|
||||
def check_special_user(user):
|
||||
print('check user is instance of', valid_cls)
|
||||
if isinstance(user, valid_cls):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return user_passes_test(check_special_user)
|
||||
|
||||
|
||||
class SpecialUserRequiredMixin(object):
|
||||
""" Requires that the User is an instance of some class """
|
||||
app_users = []
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, **initkwargs):
|
||||
view = super().as_view(**initkwargs)
|
||||
return special_user_required(cls.app_users)(view)
|
||||
return cls.special_user_required(cls.app_users)(view)
|
||||
|
||||
@staticmethod
|
||||
def special_user_required(authorized_users):
|
||||
valid_cls = tuple(authorized_users)
|
||||
if not valid_cls: # No restriction usually means misconfiguration !
|
||||
raise ImproperlyConfigured(
|
||||
'A view was configured as "restricted" with no restricting parameters !')
|
||||
|
||||
|
||||
class TemplateFieldsMetaclass(type):
|
||||
""" Loads Template objects with given string for
|
||||
header, header_small, title, ...
|
||||
|
||||
Theses strings shall be found in cls.Template
|
||||
"""
|
||||
def __init__(cls, bases, Dict):
|
||||
pass
|
||||
def check_special_user(user):
|
||||
if isinstance(user, valid_cls):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return user_passes_test(check_special_user)
|
||||
|
||||
|
||||
def user_processor(request, context):
|
||||
context['user_group'] = request.user.__class__.__qualname__
|
||||
return context
|
||||
|
||||
|
||||
class NavbarMixin(object):
|
||||
|
||||
registered_apps = ['maraudes', 'suivi']
|
||||
app_name = None
|
||||
def header_processor(header, context):
|
||||
context['page_header'] = Template(header[0]).render(context)
|
||||
context['page_header_small'] = Template(header[1]).render(context) if len(header) == 2 else ''
|
||||
context['page_title'] = " - ".join((context['page_header'], context['page_header_small']))
|
||||
return context
|
||||
|
||||
|
||||
|
||||
def get_apps_config(self):
|
||||
""" Load additionnal config data on each app registered in navbar
|
||||
Add :
|
||||
- menu_icon : glyphicon in sidebar
|
||||
- disabled : show/hide in sidebar
|
||||
"""
|
||||
## Utils ##
|
||||
APP_ICONS = {
|
||||
'maraudes': 'road',
|
||||
'suivi': 'eye-open',
|
||||
}
|
||||
app_names = self.registered_apps
|
||||
self._apps = []
|
||||
for name in app_names:
|
||||
app_config = apps.get_app_config(name)
|
||||
app_config.menu_icon = APP_ICONS[name]
|
||||
#TODO: Seems unsafe (only need module perm)
|
||||
app_config.disabled = not self.request.user.has_module_perms(name)
|
||||
self._apps.append(app_config)
|
||||
return self._apps
|
||||
|
||||
@property
|
||||
def apps(self):
|
||||
if not hasattr(self, '_apps'):
|
||||
self._apps = self.get_apps_config()
|
||||
return self._apps
|
||||
|
||||
def get_active_app(self):
|
||||
if not self.app_name:
|
||||
self.app_name = self.__class__.__module__.split(".")[0]
|
||||
# If app is website, there is no "active" application
|
||||
if self.app_name == "website":
|
||||
return None
|
||||
|
||||
active_app = apps.get_app_config(self.app_name)
|
||||
if not active_app in self.apps: #TODO: how do we deal with this ?
|
||||
raise ValueError("%s must be registered in Configuration.navbar_apps" % active_app)
|
||||
return active_app
|
||||
|
||||
@property
|
||||
def active_app(self):
|
||||
if not hasattr(self, '_active_app'):
|
||||
self._active_app = self.get_active_app()
|
||||
return self._active_app
|
||||
|
||||
@property
|
||||
def menu(self):
|
||||
""" Renvoie la liste des templates utilisés comme menu pour l'application
|
||||
active
|
||||
"""
|
||||
if not self.request.user.is_superuser:
|
||||
return self._user_menu
|
||||
return self._user_menu + self._admin_menu
|
||||
|
||||
|
||||
|
||||
class WebsiteTemplateMixin(NavbarMixin, TemplateResponseMixin):
|
||||
class WebsiteTemplateMixin(TemplateResponseMixin):
|
||||
""" Mixin for easy integration of 'website' templates
|
||||
|
||||
Each child can specify:
|
||||
- title : title of the page
|
||||
- header : header of the page
|
||||
- header_small : sub-header of the page
|
||||
|
||||
If 'content_template' is not defined, value will fallback to template_name
|
||||
in child view.
|
||||
"""
|
||||
base_template = "base_site.html"
|
||||
content_template = None
|
||||
|
||||
_user_menu = []
|
||||
_admin_menu = []
|
||||
_groups = []
|
||||
|
||||
app_name = None
|
||||
|
||||
class Configuration:
|
||||
stylesheets = ['base.css']
|
||||
page_blocks = ['header', 'header_small', 'title']
|
||||
stylesheets = ['css/base.css']
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
self._page_blocks = []
|
||||
if not hasattr(self, "PageInfo"):
|
||||
raise ImproperlyConfigured("You must define a PageInfo on ", self)
|
||||
for attr, val in self.PageInfo.__dict__.items():
|
||||
if attr[0] is not "_" and type(val) is str:
|
||||
setattr(self, attr, Template(val))
|
||||
self._page_blocks.append(attr)
|
||||
|
||||
def get_template_names(self):
|
||||
""" Ensure same template for all children views. """
|
||||
@@ -159,30 +72,20 @@ class WebsiteTemplateMixin(NavbarMixin, TemplateResponseMixin):
|
||||
raise ImproperlyConfigured(self, "has no template defined !")
|
||||
return self.content_template
|
||||
|
||||
|
||||
def _update_context_with_rendered_blocks(self, context):
|
||||
""" Render text for existing PageInfo attributes.
|
||||
See Configuration.page_blocks for valid attribute names """
|
||||
render_context = Context(context)
|
||||
for attr in self._page_blocks:
|
||||
name = "page_%s" % attr
|
||||
context[name] = getattr(self, attr).render(render_context)
|
||||
return context
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
self._update_context_with_rendered_blocks(context)
|
||||
context = Context(super().get_context_data(**kwargs))
|
||||
#Website processor
|
||||
context['stylesheets'] = self.Configuration.stylesheets
|
||||
context['apps'] = self.apps
|
||||
context['active_app'] = self.active_app
|
||||
context['active_app'] = self.app_name # Set by Webpage decorator
|
||||
# User processor
|
||||
context = header_processor(self.header, context)
|
||||
context = user_processor(self.request, context)
|
||||
#Webpage
|
||||
context['content_template'] = self.get_content_template()
|
||||
context['app_menu'] = self.menu
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class WebsiteAjaxTemplateMixin(WebsiteTemplateMixin):
|
||||
""" Mixin that returns content_template instead of base_template when
|
||||
request is Ajax.
|
||||
|
||||
102
website/navbar.py
Normal file
102
website/navbar.py
Normal file
@@ -0,0 +1,102 @@
|
||||
""" Draft for navbar application menu
|
||||
"""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
registered = []
|
||||
|
||||
|
||||
class Link:
|
||||
""" Navbar link
|
||||
|
||||
Constructor takes one required argument :
|
||||
- text : text to display for the link
|
||||
|
||||
and two optional arguments :
|
||||
- target : str or tuple (str, dict)
|
||||
- icon : bootstrap icon name, no validation
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, text, target="#", icon=None):
|
||||
self.text = text
|
||||
self._target = target
|
||||
self.icon = icon
|
||||
|
||||
@property
|
||||
def href(self):
|
||||
""" Lazy creation of html 'href' value, using 'target' instance attribute """
|
||||
if not hasattr(self, '_href'):
|
||||
if self._target == "#": #Create void link
|
||||
self._href = "#"
|
||||
else:
|
||||
try:
|
||||
target, kwargs = self._target
|
||||
except ValueError:
|
||||
target = self._target
|
||||
kwargs = {}
|
||||
assert type(target) == str
|
||||
assert type(kwargs) == dict
|
||||
self._href = reverse(target, **kwargs)
|
||||
return self._href
|
||||
|
||||
|
||||
|
||||
class LinkManager:
|
||||
""" Per-class manager of links """
|
||||
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance: #Filtering done at each call, not optimized at all !
|
||||
if not instance.user.is_superuser:
|
||||
return [link for link in self.items if link.admin_link == False]
|
||||
else:
|
||||
return self.items.copy()
|
||||
return self
|
||||
|
||||
def add(self, link):
|
||||
self.items.append(link)
|
||||
|
||||
def __repr__(self):
|
||||
return '<LinkManager: [' + ', '.join((l.text for l in self.items)) + ']>'
|
||||
|
||||
|
||||
|
||||
class MenuRegistry(type):
|
||||
""" Metaclass that registers subclass into module level variable 'registered' """
|
||||
def __new__(metacls, name, bases, attrs):
|
||||
cls = type.__new__(metacls, name, bases, attrs)
|
||||
if name != "ApplicationMenu":
|
||||
print('registering menu', cls)
|
||||
registered.append(cls)
|
||||
# Create Link instance for header attributes
|
||||
try:
|
||||
header, target, icon = cls.header
|
||||
except ValueError:
|
||||
header = cls.header
|
||||
target = "#"
|
||||
icon = None
|
||||
cls.header = Link(header, target, icon)
|
||||
cls.links = LinkManager()
|
||||
return cls
|
||||
|
||||
|
||||
|
||||
class ApplicationMenu(metaclass=MenuRegistry):
|
||||
name = None
|
||||
header = None
|
||||
|
||||
def __init__(self, view, user):
|
||||
self.view = view
|
||||
self.user = user
|
||||
self.is_active = self.name == self.view.app_name
|
||||
|
||||
@classmethod
|
||||
def add_link(cls, link, admin=False):
|
||||
if not isinstance(link, Link):
|
||||
link = Link(*link)
|
||||
link.admin_link = admin
|
||||
cls.links.add(link)
|
||||
|
||||
14
website/static/bootstrap/css/bootstrap.min.css
vendored
14
website/static/bootstrap/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,23 +1,19 @@
|
||||
|
||||
#menu {
|
||||
border: none;
|
||||
border-right: 4px solid #980300;
|
||||
}
|
||||
|
||||
.dropdown-toggle{
|
||||
border-right: 4px solid #980300 !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.navbar-fixed-side .navbar-nav>li>a {
|
||||
border-bottom: none;
|
||||
font-variant: small-caps;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#menu {
|
||||
border: none;
|
||||
border-right: 4px solid #980300;
|
||||
background-color: #121212;
|
||||
}
|
||||
|
||||
@media (max-width:768px){
|
||||
#menu { border: none; }
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
background-color: #121212;
|
||||
@@ -25,10 +21,12 @@
|
||||
}
|
||||
|
||||
|
||||
@media (max-width:768px){
|
||||
#menu { border: none; }
|
||||
.active{
|
||||
border-right: 2px solid #980300 !important;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-color: #fefefe;
|
||||
.dropdown-menu {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
|
||||
11
website/static/css/bootstrap/css/bootstrap.min.css
vendored
Normal file
11
website/static/css/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
1
website/static/css/select2.min.css
vendored
Normal file
1
website/static/css/select2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,21 +0,0 @@
|
||||
|
||||
/* Maraudes application base stylesheet */
|
||||
|
||||
.rencontre {
|
||||
background-color: #efefef;
|
||||
width:100%;
|
||||
border: 5px dotted white;
|
||||
margin:2px;
|
||||
margin-bottom:8px;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
.header td {
|
||||
color: white;
|
||||
background-color: black;
|
||||
text-align:center;
|
||||
font-weight:bold;
|
||||
font-family: Arial;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
43
website/static/scripts/bootstrap-modal.js
vendored
Normal file
43
website/static/scripts/bootstrap-modal.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
/* Lier les boutons de création
|
||||
* Thanks to Derek Morgan, https://dmorgan.info/posts/django-views-bootstrap-modals/
|
||||
*/
|
||||
;(function($) {
|
||||
|
||||
var formAjaxSubmit = function(form, modal)
|
||||
{
|
||||
$(form).submit(function (e) {
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
type: $(this).attr('method'),
|
||||
url: $(this).attr('action'),
|
||||
data: $(this).serialize(),
|
||||
success: function (xhr, ajaxOptions, thrownError) {
|
||||
if ( $(xhr).find('.has-error').length > 0 || $(xhr).find('.alert-danger').length > 0) {
|
||||
$(modal).find('.modal-body').html(xhr);
|
||||
formAjaxSubmit(form, modal);
|
||||
} else {
|
||||
$(modal).modal('toggle');
|
||||
// Reload page ?
|
||||
location.reload()
|
||||
}
|
||||
},
|
||||
error: function (xhr, ajaxOptions, thrownError) {
|
||||
// handle response errors here
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$.fn.openModalEvent = function(id, href, title)
|
||||
{
|
||||
$('#'+id).click(function() {
|
||||
$('#form-modal-body').load(href, function()
|
||||
{
|
||||
$('.modal-title').text(title);
|
||||
$('#form-modal').modal('toggle');
|
||||
formAjaxSubmit("#form-modal-body form", "#form-modal");
|
||||
});
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
5
website/static/scripts/jquery.min.js
vendored
Normal file
5
website/static/scripts/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
website/static/scripts/select2.min.js
vendored
Normal file
2
website/static/scripts/select2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
{% load staticfiles %} {% load bootstrap3 %}
|
||||
{% load staticfiles %} {% load bootstrap3 %} {% load navbar %}
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<title>{% block title %}La maraude{% endblock %}</title>
|
||||
{% bootstrap_css %}{% bootstrap_javascript %}
|
||||
<!-- Side Navbar from http://www.samrayner.com/bootstrap-side-navbar/inverse.html -->
|
||||
<link href="/static/bootstrap/navbar-fixed-side.css" rel="stylesheet" />
|
||||
<link href="/static/css/bootstrap/navbar-fixed-side.css" rel="stylesheet" />
|
||||
{% if stylesheets %}{% for stylesheet in stylesheets %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static stylesheet %}" />{% endfor %}{% endif %}
|
||||
</head>
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-lg-2">
|
||||
{% include "navbar.html" %}
|
||||
{% navbar %}
|
||||
</div>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<h1 class="page-header">{% block page_header %}{% endblock %}</h1>
|
||||
|
||||
11
website/templates/navbar/app-menu.html
Normal file
11
website/templates/navbar/app-menu.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% load bootstrap3 %}
|
||||
<li {% if active %}class="active"{%endif%}>
|
||||
<a href="{{header.href}}">{% if header.icon %}{% bootstrap_icon header.icon %} · {% endif %}<strong>{{header.text}}</strong></a>
|
||||
</li>
|
||||
{% if active %}{% for link in links %}
|
||||
<li class="app-menu">
|
||||
<a href="{{link.href}}">{{ link.text }}
|
||||
{% if link.icon %}<span class="pull-right">{% bootstrap_icon link.icon %}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}{% endif %}
|
||||
52
website/templates/navbar/layout.html
Normal file
52
website/templates/navbar/layout.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% load bootstrap3 %}{% load staticfiles %}{% load navbar %}
|
||||
<nav class="navbar navbar-inverse navbar-fixed-side" role="navigation" id="menu">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{% url 'index' %}">La Maraude ALSA</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">{% for app in apps %}
|
||||
{% navbar_menu app %}
|
||||
{% endfor %}</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a id="UserMenu" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<strong style="color:#fff;">{% bootstrap_icon "user" %} · {{user}} </strong>· {{ user_group }}<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" aria-labelledby="UserMenu">
|
||||
{% if next %}
|
||||
<p class="well-sm text-center"><strong style="color:#980300;">Vous n'avez pas l'autorisation<br/> d'accéder à cette page.</strong></p>
|
||||
{%endif%}
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.is_superuser %}<li><a href="{% url 'admin:index' %}">{% bootstrap_icon "wrench" %} Administration</a></li>{% endif %}
|
||||
<li><a href="{% url 'logout' %}">{% bootstrap_icon "log-out" %} Déconnecter</a></li>
|
||||
{% else %}
|
||||
<li>
|
||||
<form class="navbar-form navbar-left" method="post" action="/login/">{% csrf_token %}
|
||||
{% if next %}<input name="next" value="{{next}}" hidden />{% endif %}
|
||||
<div class="form-group form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 sr-only control-label" for="id_username">Username</label>
|
||||
<div class="col-md-10">
|
||||
<input autofocus="" class="form-control" id="id_username" maxlength="254" name="username" placeholder="Username" title="" type="text" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 sr-only control-label" for="id_password">Password</label>
|
||||
<div class="col-md-10"><input class="form-control" id="id_password" name="password" placeholder="Password" title="" type="password" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center"><button class="btn btn-primary navbar-button" type="submit">Connexion</button></div>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
14
website/templatetags/boolean_icons.py
Normal file
14
website/templatetags/boolean_icons.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django import template
|
||||
from django.utils.html import format_html
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def as_icon(value):
|
||||
icons = {True: "ok",
|
||||
False: "remove",
|
||||
None: "asterisk"
|
||||
}
|
||||
if not value in icons:
|
||||
raise ValueError(value, 'is not a boolean or empty value !')
|
||||
else:
|
||||
return format_html('<span class="glyphicon glyphicon-{}"></span>', icons[value])
|
||||
51
website/templatetags/navbar.py
Normal file
51
website/templatetags/navbar.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#-*- coding:utf-8 -*-
|
||||
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class NavbarNode(template.Node):
|
||||
|
||||
_apps = None
|
||||
|
||||
def get_menus(self, view, user):
|
||||
if not self._apps:
|
||||
from website.navbar import registered
|
||||
if not registered:
|
||||
print('WARNING: No app registered into "navbar" module')
|
||||
self._apps = registered.copy()
|
||||
return [app_menu(view, user) for app_menu in self._apps]
|
||||
|
||||
def get_template(self):
|
||||
return template.loader.get_template('navbar/layout.html')
|
||||
|
||||
def render(self, context):
|
||||
request = context.get('request')
|
||||
user, view = context.get('user'), context.get('view')
|
||||
apps = self.get_menus(view, user)
|
||||
# Add user menu
|
||||
context = template.Context({
|
||||
'apps': apps,
|
||||
'user': user,
|
||||
'user_group': context.get('user_group', None),
|
||||
'next': context.get('next', None),
|
||||
|
||||
})
|
||||
return self.get_template().render(context, request)
|
||||
|
||||
@register.tag
|
||||
def navbar(parser, token):
|
||||
return NavbarNode()
|
||||
|
||||
@register.inclusion_tag("navbar/app-menu.html")
|
||||
def navbar_menu(app_menu):
|
||||
return {
|
||||
'active': app_menu.is_active,
|
||||
'header': app_menu.header,
|
||||
'links': app_menu.links,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from .views import Index, login_view
|
||||
from maraudes import urls as maraudes_urls
|
||||
from suivi import urls as suivi_urls
|
||||
from sujets import urls as sujets_urls
|
||||
from utilisateurs import urls as utilisateurs_urls
|
||||
|
||||
urlpatterns = [
|
||||
# Authentification
|
||||
@@ -19,4 +20,5 @@ urlpatterns = [
|
||||
url(r'^maraudes/', include(maraudes_urls, namespace="maraudes")),
|
||||
url(r'^suivi/', include(suivi_urls, namespace="suivi")),
|
||||
url(r'^sujets/', include(sujets_urls, namespace="sujets")),
|
||||
url(r'^utilisateurs/', include(utilisateurs_urls, namespace="utilisateurs")),
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@ class Index(WebsiteTemplateMixin, views.generic.TemplateView):
|
||||
|
||||
template_name = "main.html"
|
||||
app_menu = None
|
||||
|
||||
header = ('La Maraude ALSA', 'accueil')
|
||||
class PageInfo:
|
||||
title = "La maraude ALSA"
|
||||
header = "La Maraude ALSA"
|
||||
|
||||
Reference in New Issue
Block a user