diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index b6be665..ab3c8e4 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -282,6 +282,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', 'dashboard.context_processors.notifications', 'dashboard.context_processors.extract_settings', + 'dashboard.context_processors.broadcast_messages', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs diff --git a/circle/dashboard/admin.py b/circle/dashboard/admin.py index ac798ca..f2baf13 100644 --- a/circle/dashboard/admin.py +++ b/circle/dashboard/admin.py @@ -21,7 +21,7 @@ from django import contrib from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.models import User, Group -from dashboard.models import Profile, GroupProfile, ConnectCommand +from dashboard.models import Profile, GroupProfile, ConnectCommand, Message class ProfileInline(contrib.admin.TabularInline): @@ -43,3 +43,5 @@ contrib.admin.site.unregister(User) contrib.admin.site.register(User, UserAdmin) contrib.admin.site.unregister(Group) contrib.admin.site.register(Group, GroupAdmin) + +contrib.admin.site.register(Message) diff --git a/circle/dashboard/context_processors.py b/circle/dashboard/context_processors.py index 3777694..dff324a 100644 --- a/circle/dashboard/context_processors.py +++ b/circle/dashboard/context_processors.py @@ -16,6 +16,10 @@ # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. from django.conf import settings +from django.db.models import Q +from django.utils import timezone + +from .models import Message def notifications(request): @@ -31,3 +35,10 @@ def extract_settings(request): 'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None), 'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False), } + + +def broadcast_messages(request): + now = timezone.now() + messages = Message.objects.filter(enabled=True).exclude( + Q(starts_at__gt=now) | Q(ends_at__lt=now)) + return {'broadcast_messages': messages} diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index db5a462..43d619e 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -57,7 +57,7 @@ from vm.models import ( from storage.models import DataStore, Disk from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Permission -from .models import Profile, GroupProfile +from .models import Profile, GroupProfile, Message from circle.settings.base import LANGUAGES, MAX_NODE_RAM from django.utils.translation import string_concat @@ -1624,3 +1624,15 @@ class DiskForm(ModelForm): model = Disk fields = ("name", "filename", "datastore", "type", "bus", "size", "base", "dev_num", "destroyed", "is_ready", ) + + +class MessageForm(ModelForm): + class Meta: + model = Message + fields = ("message", "enabled", "effect", "starts_at", "ends_at") + + @property + def helper(self): + helper = FormHelper() + helper.add_input(Submit("submit", _("Save"))) + return helper diff --git a/circle/dashboard/migrations/0003_message.py b/circle/dashboard/migrations/0003_message.py new file mode 100644 index 0000000..4c8aa47 --- /dev/null +++ b/circle/dashboard/migrations/0003_message.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0002_auto_20150318_1317'), + ] + + operations = [ + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('message', models.CharField(max_length=500, verbose_name='message')), + ('starts_at', models.DateTimeField(null=True, verbose_name='starts at', blank=True)), + ('ends_at', models.DateTimeField(null=True, verbose_name='ends at', blank=True)), + ('effect', models.CharField(default=b'info', max_length=10, verbose_name='effect', choices=[(b'success', 'success'), (b'info', 'info'), (b'warning', 'warning'), (b'danger', 'danger')])), + ('enabled', models.BooleanField(default=False, verbose_name='enabled')), + ], + options={ + 'ordering': ['-ends_at'], + }, + bases=(models.Model,), + ), + ] diff --git a/circle/dashboard/models.py b/circle/dashboard/models.py index 2a5ac3f..5690704 100644 --- a/circle/dashboard/models.py +++ b/circle/dashboard/models.py @@ -59,6 +59,31 @@ def pwgen(): return User.objects.make_random_password() +class Message(TimeStampedModel): + message = CharField(max_length=500, verbose_name=_('message')) + starts_at = DateTimeField( + null=True, blank=True, verbose_name=_('starts at')) + ends_at = DateTimeField( + null=True, blank=True, verbose_name=_('ends at')) + effect = CharField( + default='info', max_length=10, verbose_name=_('effect'), + choices=(('success', _('success')), ('info', _('info')), + ('warning', _('warning')), ('danger', _('danger')))) + enabled = BooleanField(default=False, verbose_name=_('enabled')) + + class Meta: + ordering = ["id"] + verbose_name = _('message') + verbose_name_plural = _('messages') + + def __unicode__(self): + return self.message + + @permalink + def get_absolute_url(self): + return ('dashboard.views.message-detail', None, {'pk': self.pk}) + + class Favourite(Model): instance = ForeignKey("vm.Instance") user = ForeignKey(User) diff --git a/circle/dashboard/static/dashboard/dashboard.js b/circle/dashboard/static/dashboard/dashboard.js index e466f70..9d16f6f 100644 --- a/circle/dashboard/static/dashboard/dashboard.js +++ b/circle/dashboard/static/dashboard/dashboard.js @@ -527,3 +527,22 @@ function replaceTag(tag) { function safe_tags_replace(str) { return str.replace(/[&<>]/g, replaceTag); } + +$(function () { + var closed = JSON.parse(getCookie('broadcast-messages')); + $('.broadcast-message').each(function() { + var id = $(this).data('id'); + if (closed && closed.indexOf(id) != -1) { + $(this).remove() + } + }); + + $('.broadcast-message').on('closed.bs.alert', function () { + var closed = JSON.parse(getCookie('broadcast-messages')); + if (!closed) { + closed = []; + } + closed.push($(this).data('id')); + setCookie('broadcast-messages', JSON.stringify(closed), 7 * 24 * 60 * 60 * 1000, "/"); + }); +}); diff --git a/circle/dashboard/static/dashboard/dashboard.less b/circle/dashboard/static/dashboard/dashboard.less index e191027..923ee29 100644 --- a/circle/dashboard/static/dashboard/dashboard.less +++ b/circle/dashboard/static/dashboard/dashboard.less @@ -1315,3 +1315,9 @@ textarea[name="new_members"] { .little-margin-bottom { margin-bottom: 5px; } + +.broadcast-message { + margin-bottom: 5px; + padding-top: 5px; + padding-bottom: 5px; +} diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py index b5059cd..b36aaf0 100644 --- a/circle/dashboard/tables.py +++ b/circle/dashboard/tables.py @@ -29,7 +29,7 @@ from django_sshkey.models import UserKey from storage.models import Disk from vm.models import Node, InstanceTemplate, Lease -from dashboard.models import ConnectCommand +from dashboard.models import ConnectCommand, Message class FileSizeColumn(Column): @@ -354,3 +354,18 @@ class DiskListTable(Table): order_by = ("-pk", ) per_page = 15 empty_text = _("No disk found.") + + +class MessageListTable(Table): + message = LinkColumn( + 'dashboard.views.message-detail', + args=[A('pk')], + attrs={'th': {'data-sort': "string"}} + ) + + class Meta: + model = Message + attrs = {'class': "table table-bordered table-striped table-hover", + 'id': "disk-list-table"} + order_by = ("-pk", ) + fields = ('pk', 'message', 'enabled', 'effect') diff --git a/circle/dashboard/templates/base.html b/circle/dashboard/templates/base.html index b3bf340..30c500e 100644 --- a/circle/dashboard/templates/base.html +++ b/circle/dashboard/templates/base.html @@ -1,5 +1,6 @@ {% load i18n %} {% load staticfiles %} +{% load cache %} {% load compressed %} <!DOCTYPE html> <html lang="{{lang}}"> @@ -40,6 +41,22 @@ </div><!-- navbar navbar-inverse navbar-fixed-top --> <div class="container"> + {% block broadcast_messages %} + {% cache 1 broadcast_messages %} + <div id="broadcast-messages"> + {% for message in broadcast_messages %} + <div data-id={{ message.id }} class="alert alert-{{ message.effect }} + text-center broadcast-message"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + {{ message.message|safe }} + </div> + {% endfor %} + </div> + {% endcache %} + {% endblock broadcast_messages %} + {% block messages %} <div class="messagelist"> {% if messages %} diff --git a/circle/dashboard/templates/dashboard/base.html b/circle/dashboard/templates/dashboard/base.html index 855265e..7466c6e 100644 --- a/circle/dashboard/templates/dashboard/base.html +++ b/circle/dashboard/templates/dashboard/base.html @@ -27,6 +27,12 @@ <span class="hidden-sm">{% trans "Admin" %}</span> </a> </li> + <li> + <a href="{% url "dashboard.views.message-list" %}"> + <i class="fa fa-bullhorn"></i> + <span class="hidden-sm">{% trans "Messages" %}</span> + </a> + </li> {% endif %} <li> <a href="{% url "dashboard.views.storage" %}"> diff --git a/circle/dashboard/templates/dashboard/message-create.html b/circle/dashboard/templates/dashboard/message-create.html new file mode 100644 index 0000000..4a3f199 --- /dev/null +++ b/circle/dashboard/templates/dashboard/message-create.html @@ -0,0 +1,27 @@ +{% extends "dashboard/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title-page %}{% trans "Broadcast Messages" %}{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <a href="{% url "dashboard.views.message-list" %}" class="btn btn-default btn-xs pull-right"> + {% trans "Back" %} + </a> + <h3 class="no-margin"> + <i class="fa fa-bullhorn"></i> + {% trans "New message" %} + </h3> + </div> + <div class="panel-body"> + {% crispy form %} + </div><!-- .panel-body --> + </div> + </div> +</div> +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/message-edit.html b/circle/dashboard/templates/dashboard/message-edit.html new file mode 100644 index 0000000..ea6b2a7 --- /dev/null +++ b/circle/dashboard/templates/dashboard/message-edit.html @@ -0,0 +1,34 @@ +{% extends "dashboard/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title-page %}{% trans "Broadcast Messages" %}{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right"> + <a href="{% url "dashboard.views.message-list" %}" + class="btn btn-default btn-xs"> + {% trans "Back" %} + </a> + <a href="{% url "dashboard.views.message-delete" pk=object.pk %}" + class="btn btn-danger btn-xs"> + {% trans "Delete" %} + </a> + </div> + <h3 class="no-margin"> + <i class="fa fa-bullhorn"></i> + {% trans "Edit message" %} + </h3> + </div> + <div class="panel-body"> + {% crispy form %} + </div><!-- .panel-body --> + </div> + </div> +</div> +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/message-list.html b/circle/dashboard/templates/dashboard/message-list.html new file mode 100644 index 0000000..a3834a5 --- /dev/null +++ b/circle/dashboard/templates/dashboard/message-list.html @@ -0,0 +1,28 @@ +{% extends "dashboard/base.html" %} +{% load staticfiles %} +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block title-page %}{% trans "Broadcast Messages" %}{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-md-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <a href="{% url "dashboard.views.message-create" %}" class="pull-right btn btn-success btn-xs"> + <i class="fa fa-plus"></i> {% trans "new message" %} + </a> + <h3 class="no-margin"><i class="fa fa-bullhorn"></i> {% trans "Broadcast Messages" %}</h3> + </div> + <div class="panel-body"> + <div class="table-responsive"> + {% render_table table %} + </div> + </div> + </div> + </div> +</div> + +{% endblock %} diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index edefaeb..11d2f11 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -54,6 +54,7 @@ from .views import ( NodeActivityView, UserList, StorageDetail, DiskDetail, + MessageList, MessageDetail, MessageCreate, MessageDelete, ) from .views.vm import vm_ops, vm_mass_ops from .views.node import node_ops @@ -232,6 +233,15 @@ urlpatterns = patterns( name="dashboard.views.storage"), url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(), name="dashboard.views.disk-detail"), + + url(r'^message/list/$', MessageList.as_view(), + name="dashboard.views.message-list"), + url(r'^message/(?P<pk>\d+)/$', MessageDetail.as_view(), + name="dashboard.views.message-detail"), + url(r'^message/create/$', MessageCreate.as_view(), + name="dashboard.views.message-create"), + url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(), + name="dashboard.views.message-delete"), ) urlpatterns += patterns( diff --git a/circle/dashboard/views/__init__.py b/circle/dashboard/views/__init__.py index 64cc7d4..f799d8d 100644 --- a/circle/dashboard/views/__init__.py +++ b/circle/dashboard/views/__init__.py @@ -14,3 +14,4 @@ from vm import * from graph import * from storage import * from request import * +from message import * diff --git a/circle/dashboard/views/message.py b/circle/dashboard/views/message.py new file mode 100644 index 0000000..8310eac --- /dev/null +++ b/circle/dashboard/views/message.py @@ -0,0 +1,41 @@ +from django.contrib.messages.views import SuccessMessageMixin +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from django.views.generic import CreateView, DeleteView, UpdateView + +from braces.views import SuperuserRequiredMixin, LoginRequiredMixin +from django_tables2 import SingleTableView + +from ..forms import MessageForm +from ..models import Message +from ..tables import MessageListTable + + +class MessageList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView): + template_name = "dashboard/message-list.html" + model = Message + table_class = MessageListTable + + +class MessageDetail(LoginRequiredMixin, SuperuserRequiredMixin, + SuccessMessageMixin, UpdateView): + model = Message + template_name = "dashboard/message-edit.html" + form_class = MessageForm + success_message = _("Broadcast message successfully updated.") + + +class MessageCreate(LoginRequiredMixin, SuperuserRequiredMixin, + SuccessMessageMixin, CreateView): + model = Message + template_name = "dashboard/message-create.html" + form_class = MessageForm + success_message = _("New broadcast message successfully created.") + + +class MessageDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): + model = Message + template_name = "dashboard/confirm/base-delete.html" + + def get_success_url(self): + return reverse("dashboard.views.message-list")