From 2249ba3e014c332930603fca5b862a5a1d0f90a5 Mon Sep 17 00:00:00 2001 From: Vladislav Manchev Date: Fri, 12 Feb 2016 22:08:56 +0200 Subject: [PATCH] Add custom realm emoji feature to admin --- frontend_tests/casper_tests/11-admin.js | 23 ++++++ frontend_tests/node_tests/templates.js | 23 ++++++ static/js/admin.js | 87 ++++++++++++++++++++ static/styles/zulip.css | 14 ++++ static/templates/admin_emoji_list.handlebars | 15 ++++ static/templates/admin_tab.handlebars | 28 +++++++ zerver/lib/actions.py | 4 +- zerver/migrations/0010_auto_20160212_1454.py | 25 ++++++ zerver/models.py | 7 +- zerver/views/realm_emoji.py | 25 ++++++ zproject/urls.py | 7 +- 11 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 static/templates/admin_emoji_list.handlebars create mode 100644 zerver/migrations/0010_auto_20160212_1454.py create mode 100644 zerver/views/realm_emoji.py diff --git a/frontend_tests/casper_tests/11-admin.js b/frontend_tests/casper_tests/11-admin.js index 9ad95663b2aaa3..16c008dcef67e0 100644 --- a/frontend_tests/casper_tests/11-admin.js +++ b/frontend_tests/casper_tests/11-admin.js @@ -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://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.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://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.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(); diff --git a/frontend_tests/node_tests/templates.js b/frontend_tests/node_tests/templates.js index 3593a92f235740..c774065152d0c4 100644 --- a/frontend_tests/node_tests/templates.js +++ b/frontend_tests/node_tests/templates.js @@ -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 += ''; + html += render('admin_emoji_list', args); + html += ''; + + 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. diff --git a/static/js/admin.js b/static/js/admin.js index 8c70d67e644d14..8b31d613088bdf 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -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.emoji, 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,15 @@ exports.setup_page = function () { error: failed_listing_streams }); + // Populate emoji table + channel.get({ + url: '/json/realm/emoji', + idempotent: true, + timeout: 10 * 1000, + success: populate_emoji, + error: failed_listing_emoji + }); + // Setup click handlers $(".admin_user_table").on("click", ".deactivate", function (e) { e.preventDefault(); @@ -408,6 +434,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( + $("

").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; diff --git a/static/styles/zulip.css b/static/styles/zulip.css index 59873ebcaae80c..f16794d99cc51f 100644 --- a/static/styles/zulip.css +++ b/static/styles/zulip.css @@ -3351,6 +3351,7 @@ div.edit_bot { } #administration .settings-section .admin-realm-form, +#administration .settings-section .admin-emoji-form, #settings .settings-section .account-settings-form, #settings .settings-section .new-bot-form, #settings .settings-section .edit-bot-form-box { @@ -4184,3 +4185,16 @@ li.show-more-private-messages a { } } + +.admin_emoji_table { + margin: 20px auto; + width: 90%; +} + +.admin_emoji_table caption { + font-size: 18px; +} + +#admin-emoji-name-status, #admin-emoji-url-status { + margin: 20px 0 0 0; +} diff --git a/static/templates/admin_emoji_list.handlebars b/static/templates/admin_emoji_list.handlebars new file mode 100644 index 00000000000000..a73549d0d16b2f --- /dev/null +++ b/static/templates/admin_emoji_list.handlebars @@ -0,0 +1,15 @@ +{{#with emoji}} + + + {{name}} + + + {{name}} + + + + + +{{/with}} diff --git a/static/templates/admin_tab.handlebars b/static/templates/admin_tab.handlebars index f7790496e6bd3a..3b833bad44c8f5 100644 --- a/static/templates/admin_tab.handlebars +++ b/static/templates/admin_tab.handlebars @@ -59,6 +59,34 @@ + + + + + + + +
Custom realm emoji:
NameImageActions
+

+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+
diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index e1dd1424a9ee05..5c96e4ffa1c7c4 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -2883,7 +2883,9 @@ def notify_realm_emoji(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() + 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): diff --git a/zerver/migrations/0010_auto_20160212_1454.py b/zerver/migrations/0010_auto_20160212_1454.py new file mode 100644 index 00000000000000..8b51131d0fdc33 --- /dev/null +++ b/zerver/migrations/0010_auto_20160212_1454.py @@ -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', '0009_add_missing_migrations'), + ] + + 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_\\-\\.]*$')]), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 048962f0e08e9c..1c9731c3510a11 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -12,12 +12,13 @@ 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 from guardian.shortcuts import get_users_with_perms import zlib @@ -214,8 +215,8 @@ def remote_user_to_email(remote_user): class RealmEmoji(models.Model): realm = models.ForeignKey(Realm) - name = models.TextField() - img_url = models.TextField() + name = models.TextField(validators=[MinLengthValidator(1), RegexValidator(regex=r'^[0-9a-zA-Z_\-\.]*$')]) + img_url = models.URLField() class Meta(object): unique_together = ("realm", "name") diff --git a/zerver/views/realm_emoji.py b/zerver/views/realm_emoji.py new file mode 100644 index 00000000000000..d5161dc0a2a912 --- /dev/null +++ b/zerver/views/realm_emoji.py @@ -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 do_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: + do_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({}) diff --git a/zproject/urls.py b/zproject/urls.py index ec32d448c32848..6f3ec590d11680 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -191,7 +191,12 @@ # 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\w+)$', 'rest_dispatch', + {'DELETE': 'delete_emoji'}), ) + patterns('zerver.views.users', url(r'^users$', 'rest_dispatch', {'GET': 'get_members_backend',