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 .
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 %}
@@ -40,6 +41,22 @@
+ {% block broadcast_messages %}
+ {% cache 1 broadcast_messages %}
+
+ {% for message in broadcast_messages %}
+
+
+ {{ message.message|safe }}
+
+ {% endfor %}
+
+ {% endcache %}
+ {% endblock broadcast_messages %}
+
{% block messages %}
{% 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 @@
{% trans "Admin" %}
+
+
+
+ {% trans "Messages" %}
+
+
{% endif %}
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 %}
+
+
+
+
+
+
+ {% crispy form %}
+
+
+
+
+{% 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 %}
+
+
+
+
+
+
+
+
+ {% trans "Edit message" %}
+
+
+
+ {% crispy form %}
+
+
+
+
+{% 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 %}
+
+
+
+
+
+
+
+ {% render_table table %}
+
+
+
+
+
+
+{% 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\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\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\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")