Skip to content
Snippets Groups Projects
Commit 7469a3d1 authored by Julian's avatar Julian
Browse files

Implemented services overview page

parent 028dad61
No related branches found
No related tags found
No related merge requests found
import datetime
import unittest
from flask import url_for
# These imports are required, because otherwise we get circular imports?!
from uffd import ldap, user
from utils import dump, UffdTestCase
class TestServices(UffdTestCase):
def setUpApp(self):
self.app.config['SERVICES'] = [
{
'title': 'Service Title',
'subtitle': 'Service Subtitle',
'description': 'Short description of the service as plain text',
'url': 'https://example.com/',
'logo_url': '/static/fairy-dust-color.png',
'required_group': 'users',
'permission_levels': [
{'name': 'Moderator', 'required_group': 'moderators'},
{'name': 'Admin', 'required_group': 'uffd_admin'},
],
'confidential': True,
'groups': [
{'name': 'Group "crew_crew"', 'required_group': 'users'},
{'name': 'Group "crew_logistik"', 'required_group': 'uffd_admin'},
],
'infos': [
{'title': 'Documentation', 'html': '<p>Some information about the service as html</p>', 'required_group': 'users'},
],
'links': [
{'title': 'Link to an external site', 'url': '#', 'required_group': 'users'},
],
},
{
'title': 'Minimal Service Title',
}
]
self.app.config['SERVICES_PUBLIC'] = True
def login(self):
self.client.post(path=url_for('session.login'),
data={'loginname': 'testuser', 'password': 'userpassword'}, follow_redirects=True)
def test_index(self):
r = self.client.get(path=url_for('services.index'))
dump('services_index_public', r)
self.assertEqual(r.status_code, 200)
self.assertNotIn(b'https://example.com/', r.data)
self.login()
r = self.client.get(path=url_for('services.index'))
dump('services_index', r)
self.assertEqual(r.status_code, 200)
self.assertIn(b'https://example.com/', r.data)
......@@ -39,10 +39,10 @@ def create_app(test_config=None):
db.init_app(app)
# pylint: disable=C0415
from uffd import user, selfservice, role, mail, session, csrf, ldap, mfa, oauth2
from uffd import user, selfservice, role, mail, session, csrf, ldap, mfa, oauth2, services
# pylint: enable=C0415
for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + ldap.bp + mfa.bp + oauth2.bp:
for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + ldap.bp + mfa.bp + oauth2.bp + services.bp:
app.register_blueprint(i)
@app.route("/")
......
......@@ -41,6 +41,48 @@ OAUTH2_CLIENTS={
# ... 'required_group': ['groupa', ['groupb', 'groupc']] ... allows users with group "groupa" as well as users with both "groupb" and "groupc" access
}
# Service overview page (disabled if empty)
SERVICES=[
# # Title is mandatory, all other fields are optional.
# # For permission_levels/groups/infos/links all fields are mandatory aside from required_group.
# {
# 'title': 'Service Title',
# 'subtitle': 'Service Subtitle',
# 'description': 'Short description of the service as plain text',
# 'url': 'https://example.com/',
# 'logo_url': 'https://example.com/logo.png',
# # Basic access group name, service is accessible to everyone if empty
# 'required_group': 'users',
# # Non-basic permission levels, the last matching entry is selected.
# # Users with a matching permission level are considered to have
# # access to the service (as if they have the basic access group).
# 'permission_levels': [
# {'name': 'Moderator', 'required_group': 'moderators'},
# {'name': 'Admin', 'required_group': 'uffd_admin'},
# ],
# # Per default all services are listed publicly (but grayed out for
# # guests/users without access). Confidential services are only visible
# # to users with access rights to the service.
# 'confidential': True,
# # In-service groups, all matching items are visible
# 'groups': [
# {'name': 'Group "crew_crew"', 'required_group': 'users'},
# {'name': 'Group "crew_logistik"', 'required_group': 'uffd_admin'},
# ],
# # Infos are small/medium amounts of information displayed in a modal
# # dialog. All matching items are visible.
# 'infos': [
# {'title': 'Documentation', 'html': '<p>Some information about the service as html</p>', 'required_group': 'users'},
# ],
# # Links to external sites, all matching items are visible
# 'links': [
# {'title': 'Link to an external site', 'url': '#', 'required_group': 'users'},
# ]
# },
]
# Enable the service overview page for users who are not logged in
SERVICES_PUBLIC=True
# do NOT set in production
#TEMPLATES_AUTO_RELOAD=True
......
from .views import bp as _bp
bp = [_bp]
{% extends 'base.html' %}
{% block body %}
{% set iconstyle = 'style="width: 1.8em;"'|safe %}
{% if not user %}
<div class="alert alert-warning" role="alert">Some services may not be publicly listed! Log in to see all services you have access to.</div>
{% endif %}
{% macro service_card(service) %}
<div class="col mb-4">
<div class="card h-100 {{ 'text-muted' if not service.has_access }}">
<div class="card-body">
{% if service.logo_url %}
{% if service.url and service.has_access %}<a href="{{ service.url }}" class="text-reset">{% endif %}
<img alt="Logo for {{ service.title }}" src="{{ service.logo_url }}" style="width: 100%; height: 10em; {{ 'filter: grayscale(100%);' if not service.has_access }}">
{% if service.url and service.has_access %}</a>{% endif %}
{% endif %}
<h5 class="card-title">
{% if service.url and service.has_access %}
<a href="{{ service.url }}" class="text-reset">{{ service.title }}</a>
{% else %}
{{ service.title }}
{% endif %}
</h5>
{% if service.subtitle %}
<h6 class="card-subtitle mb-2 text-muted">{{ service.subtitle }}</h6>
{% endif %}
{% if service.description %}
<p class="card-text">{{ service.description }}</p>
{% endif %}
</div>
<div class="list-group list-group-flush">
{% if not service.has_access %}
<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> No access</div>
{% elif service.permission %}
<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> {{ service.permission }}</div>
{% endif %}
{% for group in service.groups %}
<div class="list-group-item"><i class="fas fa-users" {{ iconstyle }}></i> {{ group.name }}</div>
{% endfor %}
{% for info in service.infos %}
<a href="#" class="list-group-item list-group-item-action" data-toggle="modal" data-target="#info-modal-{{ info.id }}"><i class="fas fa-info-circle" {{ iconstyle }}></i> {{ info.title }}</a>
{% endfor %}
{% for link in service.links %}
<a href="{{ link.url }}" class="list-group-item list-group-item-action"><i class="fas fa-external-link-alt" {{ iconstyle }}></i> {{ link.title }}</a>
{% endfor %}
</div>
</div>
</div>
{% endmacro %}
<div class="row row-cols-2 row-cols-md-3 mt-2">
{% for service in services if service.has_access %}
{{ service_card(service) }}
{% endfor %}
{% for service in services if not service.has_access %}
{{ service_card(service) }}
{% endfor %}
</div>
{% for service in services %}
{% for info in service.infos %}
<div class="modal" tabindex="-1" id="info-modal-{{ info.id }}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ info.title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ info.html|safe }}
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
{% endblock %}
from flask import Blueprint, render_template, current_app, abort
from uffd.navbar import register_navbar
from uffd.session import is_valid_session, get_current_user
bp = Blueprint("services", __name__, template_folder='templates', url_prefix='/services')
# pylint: disable=too-many-branches
def get_services(user=None):
if not user and not current_app.config['SERVICES_PUBLIC']:
return []
services = []
for service_data in current_app.config['SERVICES']:
if not service_data.get('title'):
continue
service = {
'title': service_data['title'],
'subtitle': service_data.get('subtitle', ''),
'description': service_data.get('description', ''),
'url': service_data.get('url', ''),
'logo_url': service_data.get('logo_url', ''),
'has_access': True,
'permission': '',
'groups': [],
'infos': [],
'links': [],
}
if service_data.get('required_group'):
if not user or not user.is_in_group(service_data['required_group']):
service['has_access'] = False
for permission_data in service_data.get('permission_levels', []):
if permission_data.get('required_group'):
if not user or not user.is_in_group(permission_data['required_group']):
continue
if not permission_data.get('name'):
continue
service['has_access'] = True
service['permission'] = permission_data['name']
if service_data.get('confidential', False) and not service['has_access']:
continue
for group_data in service_data.get('groups', []):
if group_data.get('required_group'):
if not user or not user.is_in_group(group_data['required_group']):
continue
if not group_data.get('name'):
continue
service['groups'].append(group_data)
for info_data in service_data.get('infos', []):
if info_data.get('required_group'):
if not user or not user.is_in_group(info_data['required_group']):
continue
if not info_data.get('title') or not info_data.get('html'):
continue
info = {
'title': info_data['title'],
'html': info_data['html'],
'id': '%d-%d'%(len(services), len(service['infos'])),
}
service['infos'].append(info)
for link_data in service_data.get('links', []):
if link_data.get('required_group'):
if not user or not user.is_in_group(link_data['required_group']):
continue
if not link_data.get('url') or not link_data.get('title'):
continue
service['links'].append(link_data)
services.append(service)
return services
def services_visible():
user = None
if is_valid_session():
user = get_current_user()
return len(get_services(user)) > 0
@bp.route("/")
@register_navbar('Services', icon='sitemap', blueprint=bp, visible=services_visible)
def index():
user = None
if is_valid_session():
user = get_current_user()
services = get_services(user)
if not current_app.config['SERVICES']:
abort(404)
return render_template('overview.html', user=user, services=services)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment