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 = (
'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
......
......@@ -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)
......@@ -17,6 +17,8 @@
from django.conf import settings
from .models import Message
def notifications(request):
count = (request.user.notification_set.filter(status="new").count()
......@@ -31,3 +33,7 @@ def extract_settings(request):
'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None),
'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 (
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", "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
from django.core.urlresolvers import reverse
from django.db.models import (
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.templatetags.static import static
......@@ -39,7 +39,7 @@ from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField
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 import Choices
......@@ -59,6 +59,27 @@ def pwgen():
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):
instance = ForeignKey("vm.Instance")
user = ForeignKey(User)
......@@ -270,10 +291,9 @@ class GroupProfile(AclBase):
except cls.DoesNotExist:
return Group.objects.get(name=name)
@permalink
def get_absolute_url(self):
return ('dashboard.views.group-detail', None,
{'pk': self.group.pk})
return reverse('dashboard.views.group-detail',
kwargs={'pk': self.group.pk})
def get_or_create_profile(self):
......
......@@ -527,3 +527,32 @@ 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, "/");
});
$("#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"] {
.little-margin-bottom {
margin-bottom: 5px;
}
.broadcast-message {
margin-bottom: 5px;
padding-top: 5px;
padding-bottom: 5px;
}
......@@ -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,19 @@ 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:
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 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 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 %}
<div class="messagelist">
{% if messages %}
......
......@@ -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" %}">
......
{% 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 (
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(
......
......@@ -14,3 +14,4 @@ from vm import *
from graph import *
from storage 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