Add custom realm emoji UI to administration page.

This commit is contained in:
Vladislav Manchev
2016-02-12 22:08:56 +02:00
committed by Tim Abbott
parent f5fe2d4bf7
commit f5e6176aea
14 changed files with 294 additions and 18 deletions

View File

@@ -68,6 +68,29 @@ casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"]:not(.deactiv
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Deactivate');
});
// Test custom realm emoji
casper.waitForSelector('.admin-emoji-form', function () {
casper.fill('form.admin-emoji-form', {
'name': 'MouseFace',
'url': 'http://localhost:9991/static/images/integrations/logos/jenkins.png'
});
casper.click('form.admin-emoji-form input.btn');
});
casper.waitUntilVisible('div#admin-emoji-status', function () {
casper.test.assertSelectorHasText('div#admin-emoji-status', 'Custom emoji added!');
});
casper.waitForSelector('.emoji_row', function () {
casper.test.assertSelectorHasText('.emoji_row .emoji_name', 'MouseFace');
casper.test.assertExists('.emoji_row img[src="http://localhost:9991/static/images/integrations/logos/jenkins.png"]');
casper.click('.emoji_row button.delete');
});
casper.waitWhileSelector('.emoji_row', function () {
casper.test.assertDoesntExist('.emoji_row');
});
// TODO: Test stream deletion
common.then_log_out();

View File

@@ -728,6 +728,29 @@ function render(template_name, args) {
}());
(function admin_emoji_list() {
global.use_template('admin_emoji_list');
var args = {
emoji: {
"name": "MouseFace",
"url": "http://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.png"
}
};
var html = '';
html += '<tbody id="admin_emoji_table">';
html += render('admin_emoji_list', args);
html += '</tbody>';
global.write_test_output('admin_emoji_list.handlebars', html);
var emoji_name = $(html).find('tr.emoji_row:first span.emoji_name');
var emoji_url = $(html).find('tr.emoji_row:first span.emoji_image img');
assert.equal(emoji_name.text(), 'MouseFace');
assert.equal(emoji_url.attr('src'), 'http://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.png');
}());
// By the end of this test, we should have compiled all our templates. Ideally,
// we will also have exercised them to some degree, but that's a little trickier
// to enforce.

View File

@@ -20,6 +20,10 @@ function failed_listing_streams(xhr, error) {
ui.report_error("Error listing streams", xhr, $("#administration-status"));
}
function failed_listing_emoji(xhr, error) {
ui.report_error("Error listing emoji", xhr, $("#administration-status"));
}
function populate_users (realm_people_data) {
var users_table = $("#admin_users_table");
var deactivated_users_table = $("#admin_deactivated_users_table");
@@ -70,6 +74,15 @@ function populate_streams (streams_data) {
loading.destroy_indicator($('#admin_page_streams_loading_indicator'));
}
function populate_emoji(emoji_data) {
var emoji_table = $('#admin_emoji_table').expectOne();
emoji_table.find('tr.emoji_row').remove();
_.each(emoji_data, function (url, name) {
emoji_table.append(templates.render('admin_emoji_list', {emoji: {name: name, url: url}}));
});
loading.destroy_indicator($('#admin_page_emoji_loading_indicator'));
}
exports.setup_page = function () {
var options = {
realm_name: page_params.realm_name,
@@ -85,12 +98,16 @@ exports.setup_page = function () {
$("#admin-realm-restricted-to-domain-status").expectOne().hide();
$("#admin-realm-invite-required-status").expectOne().hide();
$("#admin-realm-invite-by-admins-only-status").expectOne().hide();
$("#admin-emoji-status").expectOne().hide();
$("#admin-emoji-name-status").expectOne().hide();
$("#admin-emoji-url-status").expectOne().hide();
// create loading indicators
loading.make_indicator($('#admin_page_users_loading_indicator'));
loading.make_indicator($('#admin_page_bots_loading_indicator'));
loading.make_indicator($('#admin_page_streams_loading_indicator'));
loading.make_indicator($('#admin_page_deactivated_users_loading_indicator'));
loading.make_indicator($('#admin_page_emoji_loading_indicator'));
// Populate users and bots tables
channel.get({
@@ -110,6 +127,9 @@ exports.setup_page = function () {
error: failed_listing_streams
});
// Populate emoji table
populate_emoji(page_params.realm_emoji);
// Setup click handlers
$(".admin_user_table").on("click", ".deactivate", function (e) {
e.preventDefault();
@@ -408,6 +428,67 @@ exports.setup_page = function () {
}
});
});
$('.admin_emoji_table').on('click', '.delete', function (e) {
e.preventDefault();
e.stopPropagation();
var btn = $(this);
channel.del({
url: '/json/realm/emoji/' + encodeURIComponent(btn.attr('data-emoji-name')),
error: function (xhr, error_type) {
if (xhr.status.toString().charAt(0) === "4") {
btn.closest("td").html(
$("<p>").addClass("text-error").text($.parseJSON(xhr.responseText).msg)
);
} else {
btn.text("Failed!");
}
},
success: function () {
var row = btn.parents('tr');
row.remove();
}
});
});
$(".administration").on("submit", "form.admin-emoji-form", function (e) {
e.preventDefault();
e.stopPropagation();
var emoji_status = $('#admin-emoji-status');
var emoji_name_status = $('#admin-emoji-name-status');
var emoji_url_status = $('#admin-emoji-url-status');
var emoji_table = $('.admin_emoji_table');
var emoji = {};
$(this).serializeArray().map(function (x){emoji[x.name] = x.value;});
channel.put({
url: "/json/realm/emoji",
data: $(this).serialize(),
success: function () {
$('#admin-emoji-status, #admin-emoji-name-status, #admin-emoji-url-status').hide();
emoji_table.append(templates.render("admin_emoji_list", {emoji: emoji}));
ui.report_success("Custom emoji added!", emoji_status);
},
error: function (xhr, error) {
$('#admin-emoji-status, #admin-emoji-name-status, #admin-emoji-url-status').hide();
var errors = $.parseJSON(xhr.responseText).msg;
if (errors.name !== undefined) {
xhr.responseText = JSON.stringify({msg: errors.name});
ui.report_error("Failed!", xhr, emoji_name_status);
}
if (errors.img_url !== undefined) {
xhr.responseText = JSON.stringify({msg: errors.img_url});
ui.report_error("Failed!", xhr, emoji_url_status);
}
if (errors.__all__ !== undefined) {
xhr.responseText = JSON.stringify({msg: errors.__all__});
ui.report_error("Failed!", xhr, emoji_status);
}
}
});
});
};
return exports;

View File

@@ -3220,7 +3220,8 @@ div.edit_bot {
}
.edit_bot_form .control-label,
#create_bot_form .control-label {
#create_bot_form .control-label,
.admin-emoji-form .control-label {
width: 10em;
text-align: right;
margin-right: 20px;
@@ -3380,11 +3381,13 @@ div.edit_bot {
#administration .settings-section .admin-realm-form,
#settings .settings-section .new-bot-form,
#emoji-settings .new-emoji-form,
#settings .settings-section .edit-bot-form-box {
margin-top: 35px;
}
#settings .settings-section .new-bot-section-title {
#settings .settings-section .new-bot-section-title,
#emoji-settings .new-emoji-section-title {
top: 20px;
left: 20px;
}
@@ -3397,12 +3400,14 @@ div.edit_bot {
#settings .settings-section .account-settings-form .control-label,
#settings .settings-section .new-bot-form .control-label,
#emoji-settings .new-emoji-form .control-label,
#settings .settings-section .edit-bot-form-box .control-label {
width: 120px;
}
#settings .settings-section .account-settings-form .controls,
#settings .settings-section .new-bot-form .controls,
#emoji-settings .new-emoji-form .controls,
#settings .settings-section .edit-bot-form-box .controls {
margin-left: 140px;
}
@@ -3470,7 +3475,8 @@ div.edit_bot {
}
#settings .bot-information-box,
#settings .add-new-bot-box {
#settings .add-new-bot-box,
#emoji-settings .add-new-emoji-box {
background: #e3e3e3;
padding: 10px;
margin-left: 38px;
@@ -3481,7 +3487,8 @@ div.edit_bot {
font-size: 14px;
}
#settings .add-new-bot-box {
#settings .add-new-bot-box,
#emoji-settings .add-new-emoji-box {
background: #cbe3cb;
}
@@ -3531,6 +3538,7 @@ div.edit_bot {
#settings .settings-section .new-bot-form .control-label,
#emoji-settings .new-emoji-form .control-label,
#settings .settings-section .edit-bot-form-box .control-label {
float: left;
width: 120px;
@@ -3538,7 +3546,8 @@ div.edit_bot {
text-align: right;
}
#settings .settings-section .new-bot-form .controls {
#settings .settings-section .new-bot-form .controls,
#emoji-settings .new-emoji-form .controls {
margin-left: 110px;
}
@@ -3553,6 +3562,7 @@ div.edit_bot {
#administration .settings-section .organization-settings .admin-realm-form,
#settings .settings-section .account-settings-form,
#settings .settings-section .new-bot-form,
#emoji-settings .new-emoji-form,
#settings .settings-section .notification-settings-form,
#settings .settings-section .display-settings-form,
#settings .settings-section .edit-bot-form-box {
@@ -3562,6 +3572,7 @@ div.edit_bot {
#administration .settings-section .admin-realm-form .control-label,
#settings .settings-section .account-settings-form .control-label,
#settings .settings-section .new-bot-form .control-label,
#emoji-settings .new-emoji-form .control-label,
#settings .settings-section .edit-bot-form-box .control-label {
display: block;
width: 120px;
@@ -3575,6 +3586,7 @@ div.edit_bot {
#administration .settings-section .admin-realm-form .controls,
#settings .settings-section .account-settings-form .controls,
#settings .settings-section .new-bot-form .controls,
#emoji-settings .new-emoji-form .controls,
#settings .settings-section .edit-bot-form-box .controls {
margin: auto;
text-align: center;
@@ -3585,13 +3597,16 @@ div.edit_bot {
#settings .settings-section .account-settings-form .controls button,
#settings .settings-section .account-settings-form .controls input,
#settings .settings-section .new-bot-form .controls button,
#emoji-settings .new-emoji-form .controls button,
#settings .settings-section .edit-bot-form-box .controls button,
#settings .settings-section .new-bot-form .controls input,
#emoji-settings .new-emoji-form .controls input,
#settings .settings-section .edit-bot-form-box .controls input {
margin: auto;
}
#settings .settings-section .new-bot-form {
#settings .settings-section .new-bot-form,
#emoji-settings .new-emoji-form {
padding: 0px;
width: 100%;
text-align: center;
@@ -4217,3 +4232,26 @@ li.show-more-private-messages a {
}
}
.admin_emoji_table {
margin: 20px auto;
}
.emoji_image {
width: 50px; display: block;
}
.emoji_image img {
max-width: 100%;
}
#admin-emoji-name-status, #admin-emoji-url-status {
margin: 20px 0 0 0;
}
.admin-table-wrapper {
margin: 0 38px;
}
#emoji-settings .new-emoji-form #emoji_url {
width: 60%;
}

View File

@@ -0,0 +1,15 @@
{{#with emoji}}
<tr class="emoji_row" id="emoji_{{name}}">
<td>
<span class="emoji_name">{{name}}</span>
</td>
<td>
<span class="emoji_image"><img src="{{url}}" alt="{{name}}" /></span>
</td>
<td>
<button class="btn delete btn-danger" data-emoji-name="{{name}}">
Delete
</button>
</td>
</tr>
{{/with}}

View File

@@ -60,6 +60,42 @@
</div>
</form>
</div>
<div id="emoji-settings" class="settings-section">
<div class="settings-section-title"><i class="icon-vector-smile settings-section-icon"></i>
Custom realm emoji</div>
<div class="admin-table-wrapper">
<table class="table table-condensed table-striped admin_emoji_table">
<tbody id="admin_emoji_table">
<th>Name</th>
<th class="image">Image</th>
<th class="actions">Actions</th>
</tbody>
</table>
</div>
<form class="form-horizontal admin-emoji-form">
<div class="add-new-emoji-box">
<div class="new-emoji-form">
<div class="settings-section-title new-emoji-section-title">Add a New Emoji</div>
<div class="alert" id="admin-emoji-status"></div>
<div class="control-group">
<label for="emoji_name" class="control-label">Emoji name</label>
<input type="text" name="name" id="emoji_name" placeholder="mouse_face" />
<div class="alert" id="admin-emoji-name-status"></div>
</div>
<div class="control-group">
<label for="emoji_url" class="control-label">Emoji URL</label>
<input type="text" name="url" id="emoji_url" placeholder="http://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.png" />
<div class="alert" id="admin-emoji-url-status"></div>
</div>
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-big btn-primary" value="Add emoji" />
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="users">
<div id="admin-user-list" class="settings-section">

View File

@@ -2890,8 +2890,10 @@ def notify_realm_emoji(realm):
user_ids = [userdict['id'] for userdict in get_active_user_dicts_in_realm(realm)]
send_event(event, user_ids)
def do_add_realm_emoji(realm, name, img_url):
RealmEmoji(realm=realm, name=name, img_url=img_url).save()
def check_add_realm_emoji(realm, name, img_url):
emoji = RealmEmoji(realm=realm, name=name, img_url=img_url)
emoji.full_clean()
emoji.save()
notify_realm_emoji(realm)
def do_remove_realm_emoji(realm, name):

View File

@@ -3,7 +3,7 @@ from __future__ import print_function
from django.core.management.base import BaseCommand
from zerver.models import Realm, get_realm
from zerver.lib.actions import do_add_realm_emoji, do_remove_realm_emoji
from zerver.lib.actions import check_add_realm_emoji, do_remove_realm_emoji
import sys
import six
@@ -48,7 +48,7 @@ Example: python2.7 manage.py realm_emoji --realm=zulip.com --op=show
if img_url is None:
self.print_help("python2.7 manage.py", "realm_emoji")
sys.exit(1)
do_add_realm_emoji(realm, name, img_url)
check_add_realm_emoji(realm, name, img_url)
sys.exit(0)
elif options["op"] == "remove":
do_remove_realm_emoji(realm, name)

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('zerver', '0012_remove_appledevicetoken'),
]
operations = [
migrations.AlterField(
model_name='realmemoji',
name='img_url',
field=models.URLField(),
),
migrations.AlterField(
model_name='realmemoji',
name='name',
field=models.TextField(validators=[django.core.validators.MinLengthValidator(1), django.core.validators.RegexValidator(regex=b'^[0-9a-zA-Z.\\-_]+(?<![.\\-_])$')]),
),
]

View File

@@ -13,12 +13,13 @@ from zerver.lib.cache import cache_with_key, flush_user_profile, flush_realm, \
get_stream_cache_key, active_user_dicts_in_realm_cache_key, \
active_bot_dicts_in_realm_cache_key
from zerver.lib.utils import make_safe_digest, generate_random_token
from django.db import transaction, IntegrityError
from django.db import transaction
from zerver.lib.avatar import gravatar_hash, get_avatar_url
from django.utils import timezone
from django.contrib.sessions.models import Session
from zerver.lib.timestamp import datetime_to_timestamp
from django.db.models.signals import pre_save, post_save, post_delete
from django.core.validators import MinLengthValidator, RegexValidator
import zlib
from bitfield import BitField
@@ -214,8 +215,10 @@ def remote_user_to_email(remote_user):
class RealmEmoji(models.Model):
realm = models.ForeignKey(Realm)
name = models.TextField()
img_url = models.TextField()
# Second part of the regex (negative lookbehind) disallows names ending with one of the punctuation characters
name = models.TextField(validators=[MinLengthValidator(1),
RegexValidator(regex=r'^[0-9a-zA-Z.\-_]+(?<![.\-_])$')])
img_url = models.URLField()
class Meta(object):
unique_together = ("realm", "name")

View File

@@ -6,7 +6,7 @@ from django.test import TestCase
from zerver.lib import bugdown
from zerver.lib.actions import (
do_add_realm_emoji,
check_add_realm_emoji,
do_remove_realm_emoji,
get_realm,
)
@@ -307,7 +307,7 @@ class BugdownTest(TestCase):
zulip_realm = get_realm('zulip.com')
url = "https://zulip.com/test_realm_emoji.png"
do_add_realm_emoji(zulip_realm, "test", url)
check_add_realm_emoji(zulip_realm, "test", url)
# Needs to mock an actual message because that's how bugdown obtains the realm
msg = Message(sender=get_user_profile_by_email("hamlet@zulip.com"))

View File

@@ -11,7 +11,7 @@ from zerver.lib.actions import (
apply_events,
create_stream_if_needed,
do_add_alert_words,
do_add_realm_emoji,
check_add_realm_emoji,
do_add_realm_filter,
do_change_avatar_source,
do_change_default_all_public_streams,
@@ -479,7 +479,7 @@ class EventsRegisterTest(AuthedTestCase):
('op', equals('update')),
('realm_emoji', check_dict([])),
])
events = self.do_test(lambda: do_add_realm_emoji(get_realm("zulip.com"), "my_emoji",
events = self.do_test(lambda: check_add_realm_emoji(get_realm("zulip.com"), "my_emoji",
"https://realm.com/my_emoji"))
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)

View File

@@ -0,0 +1,25 @@
from django.core.exceptions import ValidationError
from django.views.decorators.csrf import csrf_exempt
from zerver.lib.response import json_success, json_error
from zerver.lib.actions import check_add_realm_emoji, do_remove_realm_emoji
from zerver.lib.rest import rest_dispatch as _rest_dispatch
rest_dispatch = csrf_exempt((lambda request, *args, **kwargs: _rest_dispatch(request, globals(), *args, **kwargs)))
def list_emoji(request, user_profile):
return json_success({'emoji': user_profile.realm.get_emoji()})
def upload_emoji(request, user_profile):
emoji_name = request.POST.get('name', None)
emoji_url = request.POST.get('url', None)
try:
check_add_realm_emoji(user_profile.realm, emoji_name, emoji_url)
except ValidationError as e:
return json_error(e.message_dict)
return json_success()
def delete_emoji(request, user_profile, emoji_name):
do_remove_realm_emoji(user_profile.realm, emoji_name)
return json_success({})

View File

@@ -188,7 +188,12 @@ v1_api_and_json_patterns = patterns('zerver.views',
# Returns a 204, used by desktop app to verify connectivity status
url(r'generate_204$', 'generate_204'),
) + patterns('zerver.views.realm_emoji',
url(r'^realm/emoji$', 'rest_dispatch',
{'GET': 'list_emoji',
'PUT': 'upload_emoji'}),
url(r'^realm/emoji/(?P<emoji_name>[0-9a-zA-Z.\-_]+(?<![.\-_]))$', 'rest_dispatch',
{'DELETE': 'delete_emoji'}),
) + patterns('zerver.views.users',
url(r'^users$', 'rest_dispatch',
{'GET': 'get_members_backend',