Commit 151c90c6 by Bach Dániel

Merge branch 'feature-broadcast-messages' into 'master'

Broadcast msg 

See merge request !326
parents ff91b0ff 257106ae
...@@ -282,6 +282,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -282,6 +282,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
'dashboard.context_processors.notifications', 'dashboard.context_processors.notifications',
'dashboard.context_processors.extract_settings', 'dashboard.context_processors.extract_settings',
'dashboard.context_processors.broadcast_messages',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
......
...@@ -21,7 +21,7 @@ from django import contrib ...@@ -21,7 +21,7 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group 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): class ProfileInline(contrib.admin.TabularInline):
...@@ -43,3 +43,5 @@ contrib.admin.site.unregister(User) ...@@ -43,3 +43,5 @@ contrib.admin.site.unregister(User)
contrib.admin.site.register(User, UserAdmin) contrib.admin.site.register(User, UserAdmin)
contrib.admin.site.unregister(Group) contrib.admin.site.unregister(Group)
contrib.admin.site.register(Group, GroupAdmin) contrib.admin.site.register(Group, GroupAdmin)
contrib.admin.site.register(Message)
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
from django.conf import settings from django.conf import settings
from .models import Message
def notifications(request): def notifications(request):
count = (request.user.notification_set.filter(status="new").count() count = (request.user.notification_set.filter(status="new").count()
...@@ -31,3 +33,7 @@ def extract_settings(request): ...@@ -31,3 +33,7 @@ def extract_settings(request):
'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None), 'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None),
'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False), 'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False),
} }
def broadcast_messages(request):
return {'broadcast_messages': Message.timeframed.filter(enabled=True)}
...@@ -57,7 +57,7 @@ from vm.models import ( ...@@ -57,7 +57,7 @@ from vm.models import (
from storage.models import DataStore, Disk from storage.models import DataStore, Disk
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission 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 circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat from django.utils.translation import string_concat
...@@ -1624,3 +1624,15 @@ class DiskForm(ModelForm): ...@@ -1624,3 +1624,15 @@ class DiskForm(ModelForm):
model = Disk model = Disk
fields = ("name", "filename", "datastore", "type", "bus", "size", fields = ("name", "filename", "datastore", "type", "bus", "size",
"base", "dev_num", "destroyed", "is_ready", ) "base", "dev_num", "destroyed", "is_ready", )
class MessageForm(ModelForm):
class Meta:
model = Message
fields = ("message", "enabled", "effect", "start", "end")
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
# -*- 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)),
('start', models.DateTimeField(null=True, verbose_name='start', blank=True)),
('end', models.DateTimeField(null=True, verbose_name='end', blank=True)),
('message', models.CharField(max_length=500, verbose_name='message')),
('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': ['id'],
'verbose_name': 'message',
'verbose_name_plural': 'messages',
},
bases=(models.Model,),
),
]
...@@ -27,7 +27,7 @@ from django.contrib.auth.signals import user_logged_in ...@@ -27,7 +27,7 @@ from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, BooleanField DateTimeField, BooleanField
) )
from django.db.models.signals import post_save, pre_delete, post_delete from django.db.models.signals import post_save, pre_delete, post_delete
from django.templatetags.static import static from django.templatetags.static import static
...@@ -39,7 +39,7 @@ from django.core.exceptions import ObjectDoesNotExist ...@@ -39,7 +39,7 @@ from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from jsonfield import JSONField from jsonfield import JSONField
from model_utils.models import TimeStampedModel from model_utils.models import TimeFramedModel, TimeStampedModel
from model_utils.fields import StatusField from model_utils.fields import StatusField
from model_utils import Choices from model_utils import Choices
...@@ -59,6 +59,27 @@ def pwgen(): ...@@ -59,6 +59,27 @@ def pwgen():
return User.objects.make_random_password() return User.objects.make_random_password()
class Message(TimeStampedModel, TimeFramedModel):
message = CharField(max_length=500, verbose_name=_('message'))
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
def get_absolute_url(self):
return reverse('dashboard.views.message-detail',
kwargs={'pk': self.pk})
class Favourite(Model): class Favourite(Model):
instance = ForeignKey("vm.Instance") instance = ForeignKey("vm.Instance")
user = ForeignKey(User) user = ForeignKey(User)
...@@ -270,10 +291,9 @@ class GroupProfile(AclBase): ...@@ -270,10 +291,9 @@ class GroupProfile(AclBase):
except cls.DoesNotExist: except cls.DoesNotExist:
return Group.objects.get(name=name) return Group.objects.get(name=name)
@permalink
def get_absolute_url(self): def get_absolute_url(self):
return ('dashboard.views.group-detail', None, return reverse('dashboard.views.group-detail',
{'pk': self.group.pk}) kwargs={'pk': self.group.pk})
def get_or_create_profile(self): def get_or_create_profile(self):
......
...@@ -527,3 +527,32 @@ function replaceTag(tag) { ...@@ -527,3 +527,32 @@ function replaceTag(tag) {
function safe_tags_replace(str) { function safe_tags_replace(str) {
return str.replace(/[&<>]/g, replaceTag); 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, "/");
});
$("#id_message").on('input', function() {
$('.broadcast-message').html($(this).val());
});
$("#id_effect").on('input', function() {
$('.broadcast-message').removeClass(
'alert-info alert-warning alert-success alert-danger').addClass(
"alert-" + $(this).val());
});
});
...@@ -1315,3 +1315,9 @@ textarea[name="new_members"] { ...@@ -1315,3 +1315,9 @@ textarea[name="new_members"] {
.little-margin-bottom { .little-margin-bottom {
margin-bottom: 5px; margin-bottom: 5px;
} }
.broadcast-message {
margin-bottom: 5px;
padding-top: 5px;
padding-bottom: 5px;
}
...@@ -29,7 +29,7 @@ from django_sshkey.models import UserKey ...@@ -29,7 +29,7 @@ from django_sshkey.models import UserKey
from storage.models import Disk from storage.models import Disk
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from dashboard.models import ConnectCommand from dashboard.models import ConnectCommand, Message
class FileSizeColumn(Column): class FileSizeColumn(Column):
...@@ -354,3 +354,19 @@ class DiskListTable(Table): ...@@ -354,3 +354,19 @@ class DiskListTable(Table):
order_by = ("-pk", ) order_by = ("-pk", )
per_page = 15 per_page = 15
empty_text = _("No disk found.") empty_text = _("No disk found.")
class MessageListTable(Table):
message = LinkColumn(
'dashboard.views.message-detail',
args=[A('pk')],
attrs={'th': {'data-sort': "string"}}
)
class Meta:
template = "django_tables2/with_pagination.html"
model = Message
attrs = {'class': "table table-bordered table-striped table-hover",
'id': "disk-list-table"}
order_by = ("-pk", )
fields = ('pk', 'message', 'enabled', 'effect')
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
{% load cache %}
{% load compressed %} {% load compressed %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{lang}}"> <html lang="{{lang}}">
...@@ -40,6 +41,22 @@ ...@@ -40,6 +41,22 @@
</div><!-- navbar navbar-inverse navbar-fixed-top --> </div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container"> <div class="container">
{% block broadcast_messages %}
{% cache 30 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">&times;</span>
</button>
{{ message.message|safe }}
</div>
{% endfor %}
</div>
{% endcache %}
{% endblock broadcast_messages %}
{% block messages %} {% block messages %}
<div class="messagelist"> <div class="messagelist">
{% if messages %} {% if messages %}
......
...@@ -27,6 +27,12 @@ ...@@ -27,6 +27,12 @@
<span class="hidden-sm">{% trans "Admin" %}</span> <span class="hidden-sm">{% trans "Admin" %}</span>
</a> </a>
</li> </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 %} {% endif %}
<li> <li>
<a href="{% url "dashboard.views.storage" %}"> <a href="{% url "dashboard.views.storage" %}">
......
{% 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 %}
{% 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>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-eye"></i>
{% trans "Preview" %}
</h3>
</div>
<div class="panel-body">
<div class="alert alert-{{ message.effect }} text-center broadcast-message">
{{ message.message|safe }}
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% block broadcast_messages %}
{% endblock %}
{% 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 %}
...@@ -54,6 +54,7 @@ from .views import ( ...@@ -54,6 +54,7 @@ from .views import (
NodeActivityView, NodeActivityView,
UserList, UserList,
StorageDetail, DiskDetail, StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -232,6 +233,15 @@ urlpatterns = patterns( ...@@ -232,6 +233,15 @@ urlpatterns = patterns(
name="dashboard.views.storage"), name="dashboard.views.storage"),
url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(), url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(),
name="dashboard.views.disk-detail"), 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( urlpatterns += patterns(
......
...@@ -14,3 +14,4 @@ from vm import * ...@@ -14,3 +14,4 @@ from vm import *
from graph import * from graph import *
from storage import * from storage import *
from request import * from request import *
from message import *
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
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 InvalidateMessageCacheMixin(object):
def post(self, *args, **kwargs):
key = make_template_fragment_key('broadcast_messages')
cache.delete(key)
return super(InvalidateMessageCacheMixin, self).post(*args, **kwargs)
class MessageList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
template_name = "dashboard/message-list.html"
model = Message
table_class = MessageListTable
class MessageDetail(InvalidateMessageCacheMixin, LoginRequiredMixin,
SuperuserRequiredMixin, SuccessMessageMixin, UpdateView):
model = Message
template_name = "dashboard/message-edit.html"
form_class = MessageForm
success_message = _("Broadcast message successfully updated.")
class MessageCreate(InvalidateMessageCacheMixin, LoginRequiredMixin,
SuperuserRequiredMixin, SuccessMessageMixin, CreateView):
model = Message
template_name = "dashboard/message-create.html"
form_class = MessageForm
success_message = _("New broadcast message successfully created.")
class MessageDelete(InvalidateMessageCacheMixin, LoginRequiredMixin,
SuperuserRequiredMixin, DeleteView):
model = Message
template_name = "dashboard/confirm/base-delete.html"
def get_success_url(self):
return reverse("dashboard.views.message-list")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment