mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-03-11 09:04:54 +00:00
Refactored 'users' module
- allows editing users (currently the groups and username) - allows any logged-in user to change the passwords of any other users - improved url highlighting of subsubmenu
This commit is contained in:
parent
733a4fd139
commit
4b3b3c666a
@ -19,9 +19,16 @@
|
||||
Plinth module to manage users
|
||||
"""
|
||||
|
||||
from . import users
|
||||
from .users import init
|
||||
from gettext import gettext as _
|
||||
from plinth import cfg
|
||||
|
||||
__all__ = ['users', 'init']
|
||||
|
||||
def init():
|
||||
"""Intialize the user module"""
|
||||
menu = cfg.main_menu.get('system:index')
|
||||
menu.add_urlname(_('Users and Groups'), 'glyphicon-user', 'users:index',
|
||||
15)
|
||||
|
||||
__all__ = ['init']
|
||||
|
||||
depends = ['plinth.modules.system']
|
||||
|
||||
30
plinth/modules/users/forms.py
Normal file
30
plinth/modules/users/forms.py
Normal file
@ -0,0 +1,30 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
"""Form to change one user"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'groups')
|
||||
widgets = {
|
||||
'username': forms.widgets.TextInput(attrs={'style': 'width: 50%'}),
|
||||
'groups': forms.widgets.CheckboxSelectMultiple(),
|
||||
}
|
||||
46
plinth/modules/users/templates/users_change_password.html
Normal file
46
plinth/modules/users/templates/users_change_password.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Change password of {{ form.user.username }}</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary" value="Save Password"/>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_js %}
|
||||
<script>
|
||||
$('#id_password1').focus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -22,14 +22,18 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary" value="Add User"/>
|
||||
<input type="submit" class="btn btn-primary" value="Create User"/>
|
||||
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -37,7 +41,7 @@
|
||||
|
||||
<div class="well sidebar">
|
||||
|
||||
<h3>Add User</h3>
|
||||
<h3>Create User</h3>
|
||||
|
||||
<p>Adding a user via this administrative
|
||||
interface <strong>might</strong> create a system user. For
|
||||
@ -48,3 +52,9 @@
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_js %}
|
||||
<script>
|
||||
$('#id_username').focus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -22,17 +22,19 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Delete Users</h3>
|
||||
|
||||
<p>Deleting users is permanent!</p>
|
||||
<h3>Delete User {{ object.username }}</h3>
|
||||
<p>
|
||||
Deleting is permanent. Are you sure?
|
||||
</p>
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary" value="Delete User"/>
|
||||
|
||||
<input type="submit" class="btn btn-md btn-primary"
|
||||
value="Delete {{ object.username }}"/>
|
||||
<a href="{% url 'users:index' %}" role="button"
|
||||
class="btn btn-md btn-primary">
|
||||
Cancel
|
||||
</a>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
52
plinth/modules/users/templates/users_list.html
Normal file
52
plinth/modules/users/templates/users_list.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Users</h3>
|
||||
<p>
|
||||
You can edit or delete users here.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<div class="list-group">
|
||||
{% for user in object_list %}
|
||||
<div class="list-group-item">
|
||||
<a href="{% url 'users:edit' user.username %}"
|
||||
style="display:inline-block; width:75%;"
|
||||
title="Edit user {{ user.username }}">
|
||||
{{ user.username }}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'users:delete' user.username %}"
|
||||
class="btn btn-default btn-xs pull-right"
|
||||
role="button" title="Delete user {{ user.username }}">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
58
plinth/modules/users/templates/users_update.html
Normal file
58
plinth/modules/users/templates/users_update.html
Normal file
@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block page_head %}
|
||||
<style type="text/css">
|
||||
.multiple-checkbox li {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Edit {{ object.username }}</h3>
|
||||
|
||||
<p style='font-size:larger'>
|
||||
Use the
|
||||
<a href='{% url 'users:change_password' object.username %}'>
|
||||
Change password form
|
||||
</a> to change the password of {{ object.username }}.
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
|
||||
<input type="submit" class="btn btn-primary" value="Save Changes"/>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_js %}
|
||||
<script>
|
||||
$('#id_username').focus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -20,15 +20,17 @@ URLs for the Users module
|
||||
"""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
from django.views.generic import RedirectView
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = patterns( # pylint: disable-msg=C0103
|
||||
'plinth.modules.users.users',
|
||||
# create an index page (that only forwards) to have correct highlighting
|
||||
# of submenu items
|
||||
url(r'^sys/users/$', RedirectView.as_view(pattern_name='users:add'),
|
||||
name='index'),
|
||||
url(r'^sys/users/add/$', 'add', name='add'),
|
||||
url(r'^sys/users/edit/$', 'edit', name='edit'),
|
||||
urlpatterns = patterns(
|
||||
'plinth.modules.users.views',
|
||||
url(r'^sys/users/$', views.UserList.as_view(), name='index'),
|
||||
url(r'^sys/users/create/$', views.UserCreate.as_view(), name='create'),
|
||||
url(r'^sys/users/edit/(?P<slug>[\w.@+-]+)$', views.UserUpdate.as_view(),
|
||||
name='edit'),
|
||||
url(r'^sys/users/delete/(?P<slug>[\w.@+-]+)$', views.UserDelete.as_view(),
|
||||
name='delete'),
|
||||
url(r'^sys/users/change_password/(?P<slug>[\w.@+-]+)$',
|
||||
views.UserChangePassword.as_view(), name='change_password'),
|
||||
)
|
||||
|
||||
@ -1,160 +0,0 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import validators
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.template.response import TemplateResponse
|
||||
from gettext import gettext as _
|
||||
import logging
|
||||
|
||||
from plinth import cfg
|
||||
from plinth.modules.lib.auth import add_user
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
subsubmenu = {'title': _('Users and Groups'),
|
||||
'items': [{'url': reverse_lazy('users:add'),
|
||||
'text': _('Add User')},
|
||||
{'url': reverse_lazy('users:edit'),
|
||||
'text': _('Delete Users')}]}
|
||||
|
||||
|
||||
def init():
|
||||
"""Intialize the module"""
|
||||
menu = cfg.main_menu.get('system:index')
|
||||
menu.add_urlname(_('Users and Groups'), 'glyphicon-user', 'users:index',
|
||||
15)
|
||||
|
||||
|
||||
class UserAddForm(forms.Form): # pylint: disable-msg=W0232
|
||||
"""Form to add a new user"""
|
||||
|
||||
username = forms.CharField(
|
||||
label=_('Username'),
|
||||
help_text=_('Must be lower case alphanumeric and start with \
|
||||
and alphabet'),
|
||||
validators=[
|
||||
validators.RegexValidator(r'^[a-z][a-z0-9]*$',
|
||||
_('Invalid username'))])
|
||||
|
||||
password = forms.CharField(label=_('Password'),
|
||||
widget=forms.PasswordInput())
|
||||
full_name = forms.CharField(label=_('Full name'), required=False)
|
||||
email = forms.EmailField(label=_('Email'), required=False)
|
||||
|
||||
|
||||
@login_required
|
||||
def add(request):
|
||||
"""Serve the form"""
|
||||
form = None
|
||||
|
||||
if request.method == 'POST':
|
||||
form = UserAddForm(request.POST, prefix='user')
|
||||
# pylint: disable-msg=E1101
|
||||
if form.is_valid():
|
||||
_add_user(request, form.cleaned_data)
|
||||
form = UserAddForm(prefix='user')
|
||||
else:
|
||||
form = UserAddForm(prefix='user')
|
||||
|
||||
return TemplateResponse(request, 'users_add.html',
|
||||
{'title': _('Add User'),
|
||||
'form': form,
|
||||
'subsubmenu': subsubmenu})
|
||||
|
||||
|
||||
def _add_user(request, data):
|
||||
"""Add a user"""
|
||||
if User.objects.filter(username=data['username']).exists():
|
||||
messages.error(request, _('User "{username}" already exists').format(
|
||||
username=data['username']))
|
||||
return
|
||||
|
||||
add_user(data['username'], data['password'], data['full_name'],
|
||||
data['email'], False)
|
||||
messages.success(request, _('User "{username}" added').format(
|
||||
username=data['username']))
|
||||
|
||||
|
||||
class UserEditForm(forms.Form): # pylint: disable-msg=W0232
|
||||
"""Form to edit/delete a user"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# pylint: disable-msg=E1002
|
||||
super(forms.Form, self).__init__(*args, **kwargs)
|
||||
|
||||
for user in User.objects.all():
|
||||
label = '%s (%s)' % (user.first_name, user.username)
|
||||
field = forms.BooleanField(label=label, required=False)
|
||||
# pylint: disable-msg=E1101
|
||||
self.fields['delete_user_' + user.username] = field
|
||||
|
||||
|
||||
@login_required
|
||||
def edit(request):
|
||||
"""Serve the edit form"""
|
||||
form = None
|
||||
|
||||
if request.method == 'POST':
|
||||
form = UserEditForm(request.POST, prefix='user')
|
||||
# pylint: disable-msg=E1101
|
||||
if form.is_valid():
|
||||
_apply_edit_changes(request, form.cleaned_data)
|
||||
form = UserEditForm(prefix='user')
|
||||
else:
|
||||
form = UserEditForm(prefix='user')
|
||||
|
||||
return TemplateResponse(request, 'users_edit.html',
|
||||
{'title': _('Delete Users'),
|
||||
'form': form,
|
||||
'subsubmenu': subsubmenu})
|
||||
|
||||
|
||||
def _apply_edit_changes(request, data):
|
||||
"""Apply form changes"""
|
||||
for field, value in data.items():
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if not field.startswith('delete_user_'):
|
||||
continue
|
||||
|
||||
username = field.split('delete_user_')[1]
|
||||
|
||||
requesting_user = request.user.username
|
||||
LOGGER.info('%s asked to delete %s', requesting_user, username)
|
||||
|
||||
if username == requesting_user:
|
||||
messages.error(
|
||||
request, _('Can not delete current account - "%s"') % username)
|
||||
continue
|
||||
|
||||
if not User.objects.filter(username=username).exists():
|
||||
messages.error(request, _('User "%s" does not exist') % username)
|
||||
continue
|
||||
|
||||
try:
|
||||
User.objects.filter(username=username).delete()
|
||||
messages.success(request, _('User "%s" deleted') % username)
|
||||
except IOError as exception:
|
||||
messages.error(request, _('Error deleting "%s" - %s') %
|
||||
(username, exception))
|
||||
128
plinth/modules/users/views.py
Normal file
128
plinth/modules/users/views.py
Normal file
@ -0,0 +1,128 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.forms import UserCreationForm, AdminPasswordChangeForm
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
from django.views.generic.edit import (CreateView, DeleteView, UpdateView,
|
||||
FormView)
|
||||
from django.views.generic import ListView
|
||||
from gettext import gettext as _
|
||||
from .forms import UserForm
|
||||
|
||||
|
||||
# TODO: we do not use the title anymore, and 'items' is also a python keyword.
|
||||
# For all subsubmenus: let's remove title and just use the items list directly.
|
||||
# Make sure to update the tests too.
|
||||
subsubmenu = {'title': _('Users and Groups'),
|
||||
'items': [{'url': reverse_lazy('users:index'),
|
||||
'text': _('Users')},
|
||||
{'url': reverse_lazy('users:create'),
|
||||
'text': _('Create User')}]}
|
||||
|
||||
|
||||
class PlinthContextMixin():
|
||||
"""Add 'subsubmenu' and 'title' to the context of class-based views"""
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Use self.title and the module-level subsubmenu"""
|
||||
context = super(PlinthContextMixin, self).get_context_data(**kwargs)
|
||||
context['subsubmenu'] = subsubmenu
|
||||
context['title'] = getattr(self, 'title', '')
|
||||
return context
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UserCreate(PlinthContextMixin, SuccessMessageMixin, CreateView):
|
||||
form_class = UserCreationForm
|
||||
template_name = 'users_create.html'
|
||||
model = User
|
||||
success_message = "%(username)s was created successfully"
|
||||
success_url = reverse_lazy('users:create')
|
||||
title = _('Create User')
|
||||
|
||||
|
||||
class UserList(PlinthContextMixin, ListView):
|
||||
model = User
|
||||
template_name = 'users_list.html'
|
||||
title = _('Edit or Delete User')
|
||||
|
||||
|
||||
class UserUpdate(PlinthContextMixin, SuccessMessageMixin, UpdateView):
|
||||
template_name = 'users_update.html'
|
||||
form_class = UserForm
|
||||
model = User
|
||||
slug_field = "username"
|
||||
success_message = "User %(username)s was changed successfully"
|
||||
fields = ['username', 'password']
|
||||
exclude = ('last_login', 'email', 'first_name', 'last_name')
|
||||
title = _('Edit User')
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('users:edit', kwargs={'slug': self.object.username})
|
||||
|
||||
|
||||
class UserDelete(PlinthContextMixin, DeleteView):
|
||||
"""Handle deleting users, showing a confirmation dialog first
|
||||
|
||||
On GET, display a confirmation page
|
||||
on POST, delete the user
|
||||
"""
|
||||
template_name = 'users_delete.html'
|
||||
model = User
|
||||
slug_field = "username"
|
||||
success_url = reverse_lazy('users:index')
|
||||
title = _('Delete User')
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Set the success message of deleting the user.
|
||||
|
||||
The SuccessMessageMixin doesn't work with the DeleteView on Django1.7,
|
||||
so set the success message manually here.
|
||||
"""
|
||||
message = _("User %s was deleted" % self.kwargs['slug'])
|
||||
output = super(UserDelete, self).delete(*args, **kwargs)
|
||||
messages.success(self.request, message)
|
||||
return output
|
||||
|
||||
|
||||
class UserChangePassword(PlinthContextMixin, SuccessMessageMixin, FormView):
|
||||
template_name = 'users_change_password.html'
|
||||
form_class = AdminPasswordChangeForm
|
||||
slug_field = "username"
|
||||
model = User
|
||||
title = _('Create User')
|
||||
success_message = _("The password was changed successfully")
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Make the user object available to the form"""
|
||||
kwargs = super(UserChangePassword, self).get_form_kwargs()
|
||||
kwargs['user'] = User.objects.get(username=self.kwargs['slug'])
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('users:edit', kwargs={'slug': self.kwargs['slug']})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.user == self.request.user:
|
||||
update_session_auth_hash(self.request, form.user)
|
||||
return super(UserChangePassword, self).form_valid(form)
|
||||
@ -1,4 +1,6 @@
|
||||
{% load static %}
|
||||
{% load plinth_extras %}
|
||||
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
@ -145,14 +147,7 @@
|
||||
|
||||
{% block subsubmenu %}
|
||||
{% if subsubmenu %}
|
||||
<ul class="nav nav-tabs">
|
||||
{% for urlitem in subsubmenu.items %}
|
||||
<li {% if urlitem.url == request.path %} class="active"{% endif %}
|
||||
role="presentation">
|
||||
<a href="{{ urlitem.url }}">{{ urlitem.text }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% show_subsubmenu subsubmenu %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
27
plinth/templates/subsubmenu.html
Normal file
27
plinth/templates/subsubmenu.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% comment %}
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
{% endcomment %}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{% for urlitem in subsubmenu.items %}
|
||||
<li {% if urlitem.active %} class="active"{% endif %}
|
||||
role="presentation">
|
||||
<a href="{{ urlitem.url }}">{{ urlitem.text }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
0
plinth/templatetags/__init__.py
Normal file
0
plinth/templatetags/__init__.py
Normal file
64
plinth/templatetags/plinth_extras.py
Normal file
64
plinth/templatetags/plinth_extras.py
Normal file
@ -0,0 +1,64 @@
|
||||
#
|
||||
# This file is part of Plinth.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import os
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
def mark_active_menuitem(menu, path):
|
||||
"""Mark the best-matching menu item with 'active'
|
||||
|
||||
Input: a menu dict in the form of:
|
||||
{'title': 'x',
|
||||
'items': [{'url': 'a/b', 'text': 'myUrl'}, {'url': ...}]
|
||||
}
|
||||
|
||||
Output: The same dictionary; the best-matching URL dict gets the value
|
||||
'active': True. All other URL dicts get the value 'active': False.
|
||||
|
||||
Note: this sets the 'active' values on the menu itself, not on a copy.
|
||||
"""
|
||||
best_match = ''
|
||||
best_match_item = None
|
||||
|
||||
for urlitem in menu['items']:
|
||||
urlitem['active'] = False
|
||||
# TODO: use a more suitable function instead of os.path.commonprefix
|
||||
match = os.path.commonprefix([urlitem['url'], path])
|
||||
# In case of 'xx/create' and 'xx/change' we'd have 'xx/c' as prefix.
|
||||
# That's a wrong match, skip it.
|
||||
match_clean = match.rpartition('/')[0]
|
||||
if (len(match_clean) + 1) < len(match):
|
||||
continue
|
||||
|
||||
if len(match_clean) > len(best_match):
|
||||
best_match = match
|
||||
best_match_item = urlitem
|
||||
|
||||
if best_match_item:
|
||||
best_match_item['active'] = True
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
@register.inclusion_tag('subsubmenu.html', takes_context=True)
|
||||
def show_subsubmenu(context, menudata):
|
||||
"""Mark the active menu item and display the subsubmenu"""
|
||||
menudata = mark_active_menuitem(menudata, context['request'].path)
|
||||
return {'subsubmenu': menudata}
|
||||
Loading…
x
Reference in New Issue
Block a user