Skip to content

Commit

Permalink
Add custom realm emoji feature to admin
Browse files Browse the repository at this point in the history
  • Loading branch information
blablacio committed Mar 22, 2016
1 parent 5c28b03 commit 2249ba3
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 5 deletions.
23 changes: 23 additions & 0 deletions frontend_tests/casper_tests/11-admin.js
Expand Up @@ -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();
Expand Down
23 changes: 23 additions & 0 deletions frontend_tests/node_tests/templates.js
Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions static/js/admin.js
Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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();
Expand Down Expand Up @@ -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(
$("<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;
Expand Down
14 changes: 14 additions & 0 deletions static/styles/zulip.css
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
15 changes: 15 additions & 0 deletions static/templates/admin_emoji_list.handlebars
@@ -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}}
28 changes: 28 additions & 0 deletions static/templates/admin_tab.handlebars
Expand Up @@ -59,6 +59,34 @@
<input type="submit" class="btn btn-big btn-primary" value="Save changes" />
</div>
</form>
<table class="table table-condensed table-striped admin_emoji_table">
<caption>Custom realm emoji:</caption>
<tbody id="admin_emoji_table">
<th>Name</th>
<th>Image</th>
<th>Actions</th>
</tbody>
</table>
<form class="form-horizontal admin-emoji-form">
<div class="control-group">
<div class="alert" id="admin-emoji-status"></div>
<label for="emoji_name" class="control-label">Emoji name</label>
<div class="controls">
<input type="text" name="name" id="emoji_name" placeholder="MouseFace" />
</div>
<div class="alert" id="admin-emoji-name-status"></div>
</div>
<div class="control-group">
<label for="emoji_url" class="control-label">Emoji URL</label>
<div class="controls">
<input type="text" name="url" id="emoji_url" placeholder="http://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.png" />
</div>
<div class="alert" id="admin-emoji-url-status"></div>
</div>
<div class="controls">
<input type="submit" class="btn btn-big btn-primary" value="Add emoji" />
</div>
</form>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="users">
Expand Down
4 changes: 3 additions & 1 deletion zerver/lib/actions.py
Expand Up @@ -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):
Expand Down
25 changes: 25 additions & 0 deletions 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_\\-\\.]*$')]),
),
]
7 changes: 4 additions & 3 deletions zerver/models.py
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
25 changes: 25 additions & 0 deletions 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({})
7 changes: 6 additions & 1 deletion zproject/urls.py
Expand Up @@ -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<emoji_name>\w+)$', 'rest_dispatch',
{'DELETE': 'delete_emoji'}),
) + patterns('zerver.views.users',
url(r'^users$', 'rest_dispatch',
{'GET': 'get_members_backend',
Expand Down

0 comments on commit 2249ba3

Please sign in to comment.