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(ClassicRecipe, ClassicRecipeAdmin)
admin.site.register(PythonEnvironment)
admin.site.register(SiteNotice)

View File

@ -1,11 +1,13 @@
# 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
from layerindex.models import Branch, LayerItem
from layerindex.models import Branch, LayerItem, SiteNotice
from django.contrib.sites.models import Site
from django.db.models import Q
from datetime import datetime
def layerindex_context(request):
import settings
@ -20,4 +22,5 @@ def layerindex_context(request):
'oe_classic': Branch.objects.filter(name='oe-classic'),
'site_name': site_name,
'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
#
# Copyright (C) 2013-2016 Intel Corporation
# Copyright (C) 2013-2018 Intel Corporation
#
# Licensed under the MIT license, see COPYING.MIT for details
@ -658,3 +658,26 @@ class RecipeChange(models.Model):
def reset_fields(self):
for fieldname in self.RECIPE_VARIABLE_MAP:
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 signal
import codecs
from bs4 import BeautifulSoup
def get_branch(branchname):
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)
sys.path.insert(0, os.path.join(core_layerdir, 'lib'))
def run_command_interruptible(cmd):
"""
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:
signal.signal(signal.SIGINT, signal.SIG_DFL)
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
anyjson==0.3.3
beautifulsoup4==4.6.0
billiard==3.5.0.3
celery==4.1.0
confusable-homoglyphs==3.0.0

View File

@ -89,6 +89,11 @@
{% 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 %}
{% for message in messages %}
<div{% if message.tags %} class="alert {{ message.tags }}"{% endif %}>{{ message }}</div>