Skip to content

Commit

Permalink
reset branch commit history
Browse files Browse the repository at this point in the history
  • Loading branch information
FuHsinyu committed Jul 8, 2024
1 parent 9b6d6b2 commit 5ea41c9
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 7 deletions.
92 changes: 89 additions & 3 deletions admin/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
#!/usr/bin/env python3

__copyright__ = "Copyright (c) 2024, Utrecht University"
__license__ = "GPLv3, see LICENSE"
__license__ = "GPLv3, see LICENSE"

from flask import abort, Blueprint, render_template, Response
import json
from functools import wraps
from os import path

from flask import (
abort, Blueprint, current_app as app, flash, g, redirect,
render_template, request, Response, url_for
)
from markupsafe import escape

import api

# Blueprint configuration
admin_bp = Blueprint("admin_bp", __name__,
template_folder="templates/admin",
static_folder="static/admin",
Expand All @@ -15,10 +24,87 @@

@admin_bp.route("/")
def index() -> Response:
"""Route to the admin page, if user has admin access"""
"""Route to the admin page, if user has admin access."""
has_admin_access = api.call("admin_has_access", data={})["data"]

if has_admin_access:
return render_template("admin.html")
else:
return abort(403)


def admin_required(f):
"""Decorator to check if the user has admin privileges."""
@wraps(f)
def decorated_function(*args, **kwargs):
if not getattr(g, 'admin', False):
flash('You do not have permission to perform this action.', 'danger')
return redirect(url_for('admin_bp.index'))
return f(*args, **kwargs)
return decorated_function


@admin_bp.route('/set_banner', methods=['POST'])
@admin_required
def set_banner():
"""Set the banner message and persist it to configuration files."""
banner_message = request.form.get('banner', '').strip()
banner_message = escape_html(banner_message) # Ensure safe text

# Message length check
error_message, is_valid = length_check(banner_message)
if not is_valid:
flash(error_message, "danger")
return redirect(url_for('admin_bp.index'))

# Update app config settings and save settings
settings = {
'banner_enabled': True,
'banner_importance': 'importance' in request.form,
'banner_message': banner_message
}
flash_msg = 'Set banner message successfully'
return save_settings(settings, flash_msg)


@admin_bp.route('/remove_banner', methods=['POST'])
@admin_required
def remove_banner():
"""Remove banner message and save settings to web server's config files."""
settings = {
'banner_enabled': False,
'banner_importance': False,
'banner_message': ''
}
flash_msg = 'Banner removed successfully'
return save_settings(settings, flash_msg)


def length_check(banner_message):
"""Validate the length and content of the banner message."""
max_length = 256
if not banner_message:
return "Empty banner message found.", False
elif len(banner_message) > max_length:
return "Banner message too long.", False
return None, True


def escape_html(text):
"""Escape HTML special characters in text."""
return escape(text)


def save_settings(settings, flash_msg):
"""Apply and save the given settings to the configuration file."""
config_file_path = path.join(app.config['APP_SHARED_FOLDER'], 'banner_settings.json')
app.config.update(settings)
try:
with open(config_file_path, 'w') as file:
json.dump(settings, file)
flash(flash_msg, 'success')
except IOError as e:
flash(f"Failed to save settings: {str(e)}", "danger")
return f"Failed to save settings: {e}", 500

return redirect(url_for('admin_bp.index'))
33 changes: 29 additions & 4 deletions admin/templates/admin/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,34 @@
{% block title %}{{ super() }} ‐ Administration{% endblock title %}

{% block content %}
<h1>Administration</h1>

<div class="card">
<p>This is a blank admin page, feature development is on the way ...</p>
<div class="container mt-4">
<header>
<h1>Administration</h1>
</header>
<main>
<section class="card my-3">
<div class="card-body">
<h2 class="card-title">Set maintenance banner</h2>
<div class="d-flex justify-content-start align-items-end" style="width: 100%;">
<form action="{{ url_for('admin_bp.set_banner') }}" method="POST" class="flex-fill me-2 needs-validation" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="banner">Banner Details:</label>
<textarea class="form-control" name="banner" id="banner" placeholder="Enter system maintenance details..." required></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="importance" name="importance">
<label class="form-check-label" for="importance">Mark as Important</label>
</div>
<button type="submit" class="btn btn-primary">Set Banner</button>
</form>
<form action="{{ url_for('admin_bp.remove_banner') }}" method="POST" class="flex-shrink-1">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-secondary">Remove Banner</button>
</form>
</div>
</div>
</section>
</main>
</div>
{% endblock content %}
28 changes: 28 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
__copyright__ = 'Copyright (c) 2021-2023, Utrecht University'
__license__ = 'GPLv3, see LICENSE'

import json
from os import path
from typing import Dict, Optional

Expand Down Expand Up @@ -43,6 +44,33 @@
])
app.jinja_loader = theme_loader


# Load banner configurations
def load_banner_config():
'''Load or initialize banner configurations.'''
config_file_path = path.join(app.config['APP_SHARED_FOLDER'], 'banner_settings.json')
default_config = {'banner_enabled': False}

try:
if not path.exists(config_file_path):
return default_config

with open(config_file_path, 'r') as file:
settings = json.load(file)
return {
'banner_enabled': settings.get('banner_enabled', False),
'banner_importance': settings.get('banner_importance', False),
'banner_message': settings.get('banner_message', '')
}
except json.JSONDecodeError:
return default_config
except Exception:
return default_config


app.config['APP_SHARED_FOLDER'] = '/tmp'
app.config.update(load_banner_config())

# Setup values for the navigation bar used in
# general/templates/general/base.html
app.config['modules'] = []
Expand Down
3 changes: 3 additions & 0 deletions general/templates/general/banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="non-production sticky-top text-center fw-bold lh-1 p-1 {{ 'text-bg-danger' if config.banner_importance else 'text-bg-primary' }}" role="alert">
{{ config.get('banner_message', 'Default banner message if none provided.') }}
</div>
1 change: 1 addition & 0 deletions general/templates/general/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

<body class="d-flex flex-column min-vh-100">
{% if config.get('YODA_ENVIRONMENT') != "production" %}{% include 'environment.html' %}{% endif %}
{% if config.get('banner_enabled') %}{% include 'banner.html' %}{% endif %}
<header class="py-3">
<div class="container">
<div class="row">
Expand Down

0 comments on commit 5ea41c9

Please sign in to comment.