Add site-wide notice support

Add the ability to show a notice at the top of every page; this provides
the ability for admins to display a message to visitors in the case of
infrastructure or index data issues. Notices can have an expiry date and
can be disabled and re-enabled if needed. A subset of HTML can be used
for formatting the text, URLs will be made into clickable links, and
four "levels" are supported (info, success, warning and error).

Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
Paul Eggleton 2018-01-05 10:12:52 +13:00
parent 2f02ddf16d
commit 49981aebf6
7 changed files with 74 additions and 3 deletions

View File

@ -198,3 +198,4 @@ admin.site.register(Patch)
admin.site.register(RecipeChangeset, RecipeChangesetAdmin) admin.site.register(RecipeChangeset, RecipeChangesetAdmin)
admin.site.register(ClassicRecipe, ClassicRecipeAdmin) admin.site.register(ClassicRecipe, ClassicRecipeAdmin)
admin.site.register(PythonEnvironment) admin.site.register(PythonEnvironment)
admin.site.register(SiteNotice)

View File

@ -1,11 +1,13 @@
# layerindex-web - custom context processor # layerindex-web - custom context processor
# #
# Copyright (C) 2013 Intel Corporation # Copyright (C) 2013, 2018 Intel Corporation
# #
# Licensed under the MIT license, see COPYING.MIT for details # Licensed under the MIT license, see COPYING.MIT for details
from layerindex.models import Branch, LayerItem from layerindex.models import Branch, LayerItem, SiteNotice
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db.models import Q
from datetime import datetime
def layerindex_context(request): def layerindex_context(request):
import settings import settings
@ -20,4 +22,5 @@ def layerindex_context(request):
'oe_classic': Branch.objects.filter(name='oe-classic'), 'oe_classic': Branch.objects.filter(name='oe-classic'),
'site_name': site_name, 'site_name': site_name,
'rrs_enabled': 'rrs' in settings.INSTALLED_APPS, 'rrs_enabled': 'rrs' in settings.INSTALLED_APPS,
'notices': SiteNotice.objects.filter(disabled=False).filter(Q(expires__isnull=True) | Q(expires__gte=datetime.now())),
} }

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('layerindex', '0013_patch'),
]
operations = [
migrations.CreateModel(
name='SiteNotice',
fields=[
('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)),
('text', models.TextField(help_text='Text to show in the notice. A limited subset of HTML is supported for formatting.')),
('level', models.CharField(choices=[('I', 'Info'), ('S', 'Success'), ('W', 'Warning'), ('E', 'Error')], help_text='Level of notice to display', default='I', max_length=1)),
('disabled', models.BooleanField(verbose_name='Disabled', help_text='Use to temporarily disable this notice', default=False)),
('expires', models.DateTimeField(blank=True, help_text='Optional date/time when this notice will stop showing', null=True)),
],
),
]

View File

@ -1,6 +1,6 @@
# layerindex-web - model definitions # layerindex-web - model definitions
# #
# Copyright (C) 2013-2016 Intel Corporation # Copyright (C) 2013-2018 Intel Corporation
# #
# Licensed under the MIT license, see COPYING.MIT for details # Licensed under the MIT license, see COPYING.MIT for details
@ -658,3 +658,26 @@ class RecipeChange(models.Model):
def reset_fields(self): def reset_fields(self):
for fieldname in self.RECIPE_VARIABLE_MAP: for fieldname in self.RECIPE_VARIABLE_MAP:
setattr(self, fieldname, getattr(self.recipe, fieldname)) setattr(self, fieldname, getattr(self.recipe, fieldname))
class SiteNotice(models.Model):
NOTICE_LEVEL_CHOICES = [
('I', 'Info'),
('S', 'Success'),
('W', 'Warning'),
('E', 'Error'),
]
text = models.TextField(help_text='Text to show in the notice. A limited subset of HTML is supported for formatting.')
level = models.CharField(max_length=1, choices=NOTICE_LEVEL_CHOICES, default='I', help_text='Level of notice to display')
disabled = models.BooleanField('Disabled', default=False, help_text='Use to temporarily disable this notice')
expires = models.DateTimeField(blank=True, null=True, help_text='Optional date/time when this notice will stop showing')
def __str__(self):
prefix = ''
if self.expires and datetime.now() >= self.expires:
prefix = '[expired] '
elif self.disabled:
prefix = '[disabled] '
return '%s%s' % (prefix, self.text)
def text_sanitised(self):
return utils.sanitise_html(self.text)

View File

@ -14,6 +14,7 @@ import time
import fcntl import fcntl
import signal import signal
import codecs import codecs
from bs4 import BeautifulSoup
def get_branch(branchname): def get_branch(branchname):
from layerindex.models import Branch from layerindex.models import Branch
@ -388,6 +389,7 @@ def setup_core_layer_sys_path(settings, branchname):
core_layerdir = os.path.join(core_repodir, core_layerbranch.vcs_subdir) core_layerdir = os.path.join(core_repodir, core_layerbranch.vcs_subdir)
sys.path.insert(0, os.path.join(core_layerdir, 'lib')) sys.path.insert(0, os.path.join(core_layerdir, 'lib'))
def run_command_interruptible(cmd): def run_command_interruptible(cmd):
""" """
Run a command with output displayed on the console, but ensure any Ctrl+C is Run a command with output displayed on the console, but ensure any Ctrl+C is
@ -416,3 +418,15 @@ def run_command_interruptible(cmd):
finally: finally:
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
return process.returncode, buf return process.returncode, buf
def sanitise_html(html):
soup = BeautifulSoup(html, "html.parser")
for tag in soup.findAll(True):
if tag.name not in ['strong', 'em', 'b', 'i', 'p', 'ul', 'ol', 'li', 'br', 'p']:
tag.hidden = True
elif tag.attrs:
tag.attrs = []
return soup.renderContents()

View File

@ -1,5 +1,6 @@
amqp==2.2.2 amqp==2.2.2
anyjson==0.3.3 anyjson==0.3.3
beautifulsoup4==4.6.0
billiard==3.5.0.3 billiard==3.5.0.3
celery==4.1.0 celery==4.1.0
confusable-homoglyphs==3.0.0 confusable-homoglyphs==3.0.0

View File

@ -89,6 +89,11 @@
{% endblock %} {% endblock %}
{% block contenttag %}<div id="content" class="container top-padded">{% endblock %} {% block contenttag %}<div id="content" class="container top-padded">{% endblock %}
{% if notices %}
{% for notice in notices %}
<div class="alert {% if notice.level == 'I' %}alert-info{% elif notice.level == 'S' %}alert-success{% elif notice.level == 'W' %}{% elif notice.level == 'E' %}alert-error{% endif %}">{{ notice.text_sanitised|safe|urlize }}</div>
{% endfor %}
{% endif %}
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div{% if message.tags %} class="alert {{ message.tags }}"{% endif %}>{{ message }}</div> <div{% if message.tags %} class="alert {{ message.tags }}"{% endif %}>{{ message }}</div>