Backport new menu, new sidebar and CSS changes

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-04-21 23:50:35 -04:00
parent d410e1ef6c
commit 1e37ca8315
31 changed files with 550 additions and 165 deletions
+72 -38
View File
@@ -6,33 +6,37 @@ import logging
from furl import furl
from django.apps import apps
from django.conf import settings
from django.contrib.admin.utils import label_for_field
from django.core.exceptions import PermissionDenied
from django.shortcuts import resolve_url
from django.template import VariableDoesNotExist, Variable
from django.template.defaulttags import URLNode
from django.urls import Resolver404, resolve
from django.urls import resolve, reverse
from django.utils.encoding import force_str, force_text
from mayan.apps.common.settings import setting_home_view
from mayan.apps.common.utils import return_attrib
from mayan.apps.permissions import Permission
from .utils import get_current_view_name
logger = logging.getLogger(__name__)
class ResolvedLink(object):
def __init__(self, link, current_view):
self.current_view = current_view
def __init__(self, link, current_view_name):
self.context = None
self.current_view_name = current_view_name
self.disabled = False
self.link = link
self.url = '#'
self.context = None
self.request = None
self.url = '#'
def __repr__(self):
return '<ResolvedLink: {}>'.format(self.url)
@property
def active(self):
return self.link.view == self.current_view
return self.link.view == self.current_view_name
@property
def badge_text(self):
@@ -97,12 +101,15 @@ class Menu(object):
self.__class__._registry[name] = self
self.non_sorted_sources = non_sorted_sources or []
def __repr__(self):
return '<Menu: {}>'.format(self.name)
def _map_links_to_source(self, links, source, map_variable='bound_links', position=None):
source_links = getattr(self, map_variable).setdefault(source, [])
for link in links:
source_links.append(link)
self.link_positions[link] = position
self.link_positions[link] = position or 0
def add_unsorted_source(self, source):
self.non_sorted_sources.append(source)
@@ -159,6 +166,24 @@ class Menu(object):
)
return resolved_navigation_object_list
def get_result_position(self, item):
"""
Method to help sort results by position.
"""
if isinstance(item, ResolvedLink):
return self.link_positions.get(item.link, 0)
else:
return self.link_positions.get(item, 0) or 0
def get_result_label(self, item):
"""
Method to help sort results by label.
"""
if isinstance(item, ResolvedLink):
return item.link.text
else:
return item.label
def resolve(self, context, source=None, sort_results=False):
if not self.check_condition(context=context):
return []
@@ -178,15 +203,8 @@ class Menu(object):
logger.warning('No request variable, aborting menu resolution')
return ()
current_path = request.META['PATH_INFO']
# Get sources: view name, view objects
try:
current_view = resolve(current_path).view_name
except Resolver404:
# Can't figure out which view corresponds to this URL.
# Most likely it is an invalid URL.
logger.warning('Can\'t figure out which view corresponds to this URL: %s; aborting menu resolution.', current_path)
current_view_name = get_current_view_name(request=request)
if not current_view_name:
return ()
resolved_navigation_object_list = self.get_resolved_navigation_object_list(
@@ -227,18 +245,28 @@ class Menu(object):
pass
if resolved_links:
result.append(resolved_links)
result.append(
{
'object': resolved_navigation_object,
'links': resolved_links
}
)
resolved_links = []
# View links
for link in self.bound_links.get(current_view, []):
for link in self.bound_links.get(current_view_name, []):
resolved_link = link.resolve(context=context)
if resolved_link:
if resolved_link.link not in self.unbound_links.get(current_view, ()):
if resolved_link.link not in self.unbound_links.get(current_view_name, ()):
resolved_links.append(resolved_link)
if resolved_links:
result.append(resolved_links)
result.append(
{
'object': current_view_name,
'links': resolved_links
}
)
resolved_links = []
@@ -256,7 +284,12 @@ class Menu(object):
resolved_links.append(resolved_link)
if resolved_links:
result.append(resolved_links)
result.append(
{
'object': None,
'links': resolved_links
}
)
if result:
unsorted_source = False
@@ -267,16 +300,15 @@ class Menu(object):
break
if sort_results and not unsorted_source:
result[0] = sorted(
result[0], key=lambda item: (
item.link.text if isinstance(item, ResolvedLink) else item.label
for link_group in result:
link_group['links'] = sorted(
link_group['links'], key=self.get_result_label
)
)
else:
# Sort links by position value passed during bind
result[0] = sorted(
result[0], key=lambda item: (self.link_positions.get(item.link) or 0) if isinstance(item, ResolvedLink) else (self.link_positions.get(item) or 0)
)
for link_group in result:
link_group['links'] = sorted(
link_group['links'], key=self.get_result_position
)
return result
@@ -308,7 +340,7 @@ class Link(object):
def remove(cls, name):
del cls._registry[name]
def __init__(self, badge_text=None, text=None, view=None, args=None, condition=None,
def __init__(self, text=None, view=None, args=None, badge_text=None, condition=None,
conditional_disable=None, description=None, html_data=None,
html_extra_classes=None, icon=None, icon_class=None,
keep_query=False, kwargs=None, name=None, permissions=None,
@@ -351,7 +383,7 @@ class Link(object):
request = Variable('request').resolve(context)
current_path = request.META['PATH_INFO']
current_view = resolve(current_path).view_name
current_view_name = resolve(current_path).view_name
# ACL is tested agains the resolved_object or just {{ object }} if not
if not resolved_object:
@@ -386,7 +418,9 @@ class Link(object):
if not self.condition(context):
return None
resolved_link = ResolvedLink(current_view=current_view, link=self)
resolved_link = ResolvedLink(
current_view_name=current_view_name, link=self
)
if self.view:
view_name = Variable('"{}"'.format(self.view))
@@ -419,7 +453,7 @@ class Link(object):
try:
resolved_link.url = node.render(context)
except Exception as exception:
logger.debug(
logger.error(
'Error resolving link "%s" URL; %s', self.text, exception
)
elif self.url:
@@ -437,7 +471,7 @@ class Link(object):
parsed_url = furl(
force_str(
request.get_full_path() or request.META.get(
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
'HTTP_REFERER', reverse(setting_home_view.value)
)
)
)
@@ -467,7 +501,7 @@ class Separator(Link):
self.view = None
def resolve(self, *args, **kwargs):
result = ResolvedLink(current_view=None, link=self)
result = ResolvedLink(current_view_name=None, link=self)
result.separator = True
return result
@@ -543,7 +577,7 @@ class Text(Link):
self.view = None
def resolve(self, *args, **kwargs):
result = ResolvedLink(current_view=None, link=self)
result = ResolvedLink(current_view_name=None, link=self)
result.context = kwargs.get('context')
result.text_span = True
return result
@@ -1,5 +1,6 @@
{% load i18n %}
{% spaceless %}
{% if as_dropdown %}
<div class="dropdown text-center">
<button class="btn btn-default dropdown-toggle btn-danger btn-sm" type="button" id="dropdownMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@@ -23,3 +24,4 @@
{% include 'navigation/generic_subnavigation.html' %}
{% endfor %}
{% endif %}
{% endspaceless %}
@@ -12,13 +12,15 @@
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
{% get_menu_links link.name as menu_links %}
{% for linkset in menu_links %}
{% with '' as li_class_active %}
{% with linkset as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% navigation_resolve_menu name=link.name as sub_menus_results %}
{% for sub_menu_results in sub_menus_results %}
{% for link_group in sub_menu_results.link_groups %}
{% with '' as li_class_active %}
{% with link_group.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</ul>
</li>
@@ -39,8 +39,9 @@ def get_menus_links(context, names, source=None, sort_results=None):
def get_multi_item_links_form(context, object_list):
actions = []
for link_set in Menu.get('multi item').resolve(context=context, source=object_list[0], sort_results=True):
for link in link_set:
actions.append((link.url, link.text))
for link in link_set['links']:
if hasattr(link, 'url'):
actions.append((link.url, link.text))
form = MultiItemForm(actions=actions)
context.update({'multi_item_form': form, 'multi_item_actions': actions})
@@ -70,6 +71,35 @@ def get_source_columns(source):
return SourceColumn.get_for_source(source)
@register.simple_tag(takes_context=True)
def navigation_resolve_menu(context, name, source=None, sort_results=None):
result = []
menu = Menu.get(name)
link_groups = menu.resolve(
context=context, source=source, sort_results=sort_results
)
if link_groups:
result.append({'link_groups': link_groups, 'menu': menu})
return result
@register.simple_tag(takes_context=True)
def navigation_resolve_menus(context, names, source=None, sort_results=None):
result = []
for name in names.split(','):
menu = Menu.get(name=name)
link_groups = menu.resolve(context=context, sort_results=sort_results)
if link_groups:
result.append({'link_groups': link_groups, 'menu': menu})
return result
@register.simple_tag(takes_context=True)
def resolve_link(context, link):
# This can be used to resolve links or menus too
+21 -22
View File
@@ -29,7 +29,8 @@ class LinkClassTestCase(GenericViewTestCase):
self.add_test_view(test_object=self.test_object)
self.namespace = PermissionNamespace(
TEST_PERMISSION_NAMESPACE_NAME, TEST_PERMISSION_NAMESPACE_TEXT
label=TEST_PERMISSION_NAMESPACE_TEXT,
name=TEST_PERMISSION_NAMESPACE_NAME
)
self.test_permission = self.namespace.add_permission(
@@ -45,32 +46,30 @@ class LinkClassTestCase(GenericViewTestCase):
Permission.invalidate_cache()
def test_link_resolve(self):
response = self.get(TEST_VIEW_NAME)
response = self.get(viewname=TEST_VIEW_NAME)
context = Context({'request': response.wsgi_request})
resolved_link = self.link.resolve(context=context)
self.assertEqual(resolved_link.url, reverse(TEST_VIEW_NAME))
self.assertEqual(
resolved_link.url, reverse(viewname=TEST_VIEW_NAME)
)
def test_link_permission_resolve_no_permission(self):
self.login_user()
link = Link(
permissions=(self.test_permission,), text=TEST_LINK_TEXT,
view=TEST_VIEW_NAME
)
response = self.get(TEST_VIEW_NAME)
response = self.get(viewname=TEST_VIEW_NAME)
response.context.update({'request': response.wsgi_request})
context = Context(response.context)
resolved_link = link.resolve(context=context)
self.assertEqual(resolved_link, None)
def test_link_permission_resolve_with_permission(self):
self.login_user()
link = Link(
permissions=(self.test_permission,), text=TEST_LINK_TEXT,
view=TEST_VIEW_NAME
@@ -78,17 +77,16 @@ class LinkClassTestCase(GenericViewTestCase):
self.grant_access(obj=self.test_object, permission=self.test_permission)
response = self.get(TEST_VIEW_NAME)
response = self.get(viewname=TEST_VIEW_NAME)
response.context.update({'request': response.wsgi_request})
context = Context(response.context)
resolved_link = link.resolve(context=context)
self.assertEqual(resolved_link.url, reverse(TEST_VIEW_NAME))
self.assertEqual(resolved_link.url, reverse(viewname=TEST_VIEW_NAME))
def test_link_permission_resolve_with_acl(self):
# ACL is tested agains the resolved_object or just {{ object }} if not
link = Link(
permissions=(self.test_permission,), text=TEST_LINK_TEXT,
view=TEST_VIEW_NAME
@@ -96,14 +94,14 @@ class LinkClassTestCase(GenericViewTestCase):
self.grant_access(obj=self.test_object, permission=self.test_permission)
response = self.get(TEST_VIEW_NAME)
response = self.get(viewname=TEST_VIEW_NAME)
response.context.update({'request': response.wsgi_request})
context = Context(response.context)
resolved_link = link.resolve(context=context)
self.assertNotEqual(resolved_link, None)
self.assertEqual(resolved_link.url, reverse(TEST_VIEW_NAME))
self.assertEqual(resolved_link.url, reverse(viewname=TEST_VIEW_NAME))
def test_link_with_unicode_querystring_request(self):
url = furl(reverse(TEST_VIEW_NAME))
@@ -130,7 +128,6 @@ class LinkClassTestCase(GenericViewTestCase):
context = Context({'request': response.wsgi_request})
resolved_link = self.link.resolve(context=context)
self.assertEqual(
resolved_link.url,
'{}?{}'.format(TEST_URL, TEST_QUERYSTRING_TWO_KEYS)
@@ -184,10 +181,10 @@ class MenuClassTestCase(GenericViewTestCase):
def test_null_source_link_unbinding(self):
self.menu.bind_links(links=(self.link,))
response = self.get(TEST_VIEW_NAME)
response = self.get(viewname=TEST_VIEW_NAME)
context = Context({'request': response.wsgi_request})
self.assertEqual(
self.menu.resolve(context=context)[0][0].link, self.link
self.menu.resolve(context=context)[0]['links'][0].link, self.link
)
self.menu.unbind_links(links=(self.link,))
@@ -197,10 +194,12 @@ class MenuClassTestCase(GenericViewTestCase):
def test_null_source_submenu_unbinding(self):
self.menu.bind_links(links=(self.sub_menu,))
response = self.get(TEST_VIEW_NAME)
response = self.get(viewname=TEST_VIEW_NAME)
context = Context({'request': response.wsgi_request})
self.assertEqual(self.menu.resolve(context=context), [[self.sub_menu]])
self.assertEqual(
self.menu.resolve(context=context)[0]['links'], [self.sub_menu]
)
self.menu.unbind_links(links=(self.sub_menu,))
+22
View File
@@ -1,10 +1,15 @@
from __future__ import absolute_import, unicode_literals
import logging
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.urls import Resolver404, resolve
from mayan.apps.permissions import Permission
logger = logging.getLogger(__name__)
def get_cascade_condition(app_label, model_name, object_permission, view_permission=None):
"""
@@ -37,3 +42,20 @@ def get_cascade_condition(app_label, model_name, object_permission, view_permiss
return queryset.count() > 0
return condition
def get_current_view_name(request):
current_path = request.META['PATH_INFO']
# Get sources: view name, view objects
try:
current_view_name = resolve(current_path).view_name
except Resolver404:
# Can't figure out which view corresponds to this URL.
# Most likely it is an invalid URL.
logger.warning(
'Can\'t figure out which view corresponds to this '
'URL: %s; aborting menu resolution.', current_path
)
else:
return current_view_name