mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
realm-emoji: Add realm emoji uploading instead url providing.
- Add file_name field to `RealmEmoji` model and migration. - Add emoji upload supporting to Upload backends. - Add uploaded file processing to emoji views. - Use emoji source url as based for display url. - Change emoji form for image uploading. - Fix back-end tests. - Fix front-end tests. - Add tests for emoji uploading. Fixes #1134
This commit is contained in:
@@ -71,7 +71,7 @@ casper.then(function () {
|
|||||||
casper.waitUntilVisible('.admin-emoji-form', function () {
|
casper.waitUntilVisible('.admin-emoji-form', function () {
|
||||||
casper.fill('form.admin-emoji-form', {
|
casper.fill('form.admin-emoji-form', {
|
||||||
name: 'MouseFace',
|
name: 'MouseFace',
|
||||||
url: 'http://zulipdev.com:9991/static/images/integrations/logos/jenkins.png',
|
emoji_file_input: 'static/images/logo/zulip-icon-128x128.png',
|
||||||
}, true);
|
}, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -85,7 +85,7 @@ casper.then(function () {
|
|||||||
casper.then(function () {
|
casper.then(function () {
|
||||||
casper.waitUntilVisible('.emoji_row', function () {
|
casper.waitUntilVisible('.emoji_row', function () {
|
||||||
casper.test.assertSelectorHasText('.emoji_row .emoji_name', 'MouseFace');
|
casper.test.assertSelectorHasText('.emoji_row .emoji_name', 'MouseFace');
|
||||||
casper.test.assertExists('.emoji_row img[src="http://zulipdev.com:9991/static/images/integrations/logos/jenkins.png"]');
|
casper.test.assertExists('.emoji_row img[src="/user_avatars/1/emoji/MouseFace.png"]');
|
||||||
casper.click('.emoji_row button.delete');
|
casper.click('.emoji_row button.delete');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ exports.update_emojis = function update_emojis(realm_emojis) {
|
|||||||
// Copy the default emoji list and add realm-specific emoji to it
|
// Copy the default emoji list and add realm-specific emoji to it
|
||||||
exports.emojis = default_emojis.slice(0);
|
exports.emojis = default_emojis.slice(0);
|
||||||
_.each(realm_emojis, function (data, name) {
|
_.each(realm_emojis, function (data, name) {
|
||||||
exports.emojis.push({emoji_name: name, emoji_url: data.display_url, is_realm_emoji: true});
|
exports.emojis.push({emoji_name: name, emoji_url: data.source_url, is_realm_emoji: true});
|
||||||
exports.realm_emojis[name] = {emoji_name: name, emoji_url: data.display_url};
|
exports.realm_emojis[name] = {emoji_name: name, emoji_url: data.source_url};
|
||||||
});
|
});
|
||||||
// Add the Zulip emoji to the realm emojis list
|
// Add the Zulip emoji to the realm emojis list
|
||||||
exports.emojis.push(zulip_emoji);
|
exports.emojis.push(zulip_emoji);
|
||||||
@@ -70,6 +70,26 @@ exports.initialize = function initialize() {
|
|||||||
|
|
||||||
exports.update_emojis(page_params.realm_emoji);
|
exports.update_emojis(page_params.realm_emoji);
|
||||||
|
|
||||||
|
exports.build_emoji_upload_widget = function () {
|
||||||
|
|
||||||
|
var get_file_input = function () {
|
||||||
|
return $('#emoji_file_input');
|
||||||
|
};
|
||||||
|
|
||||||
|
var file_name_field = $('#emoji-file-name');
|
||||||
|
var input_error = $('#emoji_file_input_error');
|
||||||
|
var clear_button = $('#emoji_image_clear_button');
|
||||||
|
var upload_button = $('#emoji_upload_button');
|
||||||
|
|
||||||
|
return upload_widget.build_widget(
|
||||||
|
get_file_input,
|
||||||
|
file_name_field,
|
||||||
|
input_error,
|
||||||
|
clear_button,
|
||||||
|
upload_button
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return exports;
|
return exports;
|
||||||
}());
|
}());
|
||||||
if (typeof module !== 'undefined') {
|
if (typeof module !== 'undefined') {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports.populate_emoji = function (emoji_data) {
|
|||||||
emoji_table.append(templates.render('admin_emoji_list', {
|
emoji_table.append(templates.render('admin_emoji_list', {
|
||||||
emoji: {
|
emoji: {
|
||||||
name: name, source_url: data.source_url,
|
name: name, source_url: data.source_url,
|
||||||
display_url: data.display_url,
|
display_url: data.source_url,
|
||||||
author: data.author,
|
author: data.author,
|
||||||
is_admin: page_params.is_admin,
|
is_admin: page_params.is_admin,
|
||||||
},
|
},
|
||||||
@@ -61,28 +61,38 @@ exports.set_up = function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var emoji_widget = emoji.build_emoji_upload_widget();
|
||||||
|
|
||||||
$(".organization").on("submit", "form.admin-emoji-form", function (e) {
|
$(".organization").on("submit", "form.admin-emoji-form", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var emoji_status = $('#admin-emoji-status');
|
var emoji_status = $('#admin-emoji-status');
|
||||||
var emoji = {};
|
var emoji = {};
|
||||||
|
var formData = new FormData();
|
||||||
_.each($(this).serializeArray(), function (obj) {
|
_.each($(this).serializeArray(), function (obj) {
|
||||||
emoji[obj.name] = obj.value;
|
emoji[obj.name] = obj.value;
|
||||||
});
|
});
|
||||||
|
$.each($('#emoji_file_input')[0].files, function (i, file) {
|
||||||
|
formData.append('file-' + i, file);
|
||||||
|
});
|
||||||
channel.put({
|
channel.put({
|
||||||
url: "/json/realm/emoji/" + encodeURIComponent(emoji.name),
|
url: "/json/realm/emoji/" + encodeURIComponent(emoji.name),
|
||||||
data: $(this).serialize(),
|
data: formData,
|
||||||
|
cache: false,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
success: function () {
|
success: function () {
|
||||||
$('#admin-emoji-status').hide();
|
$('#admin-emoji-status').hide();
|
||||||
ui_report.success(i18n.t("Custom emoji added!"), emoji_status);
|
ui_report.success(i18n.t("Custom emoji added!"), emoji_status);
|
||||||
$("form.admin-emoji-form input[type='text']").val("");
|
$("form.admin-emoji-form input[type='text']").val("");
|
||||||
|
emoji_widget.clear();
|
||||||
},
|
},
|
||||||
error: function (xhr) {
|
error: function (xhr) {
|
||||||
$('#admin-emoji-status').hide();
|
$('#admin-emoji-status').hide();
|
||||||
var errors = JSON.parse(xhr.responseText).msg;
|
var errors = JSON.parse(xhr.responseText).msg;
|
||||||
xhr.responseText = JSON.stringify({msg: errors});
|
xhr.responseText = JSON.stringify({msg: errors});
|
||||||
ui_report.error(i18n.t("Failed!"), xhr, emoji_status);
|
ui_report.error(i18n.t("Failed!"), xhr, emoji_status);
|
||||||
|
emoji_widget.clear();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<span class="emoji_name">{{name}}</span>
|
<span class="emoji_name">{{name}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="emoji_image"><a href="{{source_url}}"><img src="{{display_url}}" alt="{{name}}" /></a></span>
|
<span class="emoji_image"><a href="{{source_url}}"><img src="{{source_url}}" alt="{{name}}" /></a></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{#if author}}
|
{{#if author}}
|
||||||
|
|||||||
@@ -23,8 +23,12 @@
|
|||||||
<input type="text" name="name" id="emoji_name" placeholder="mouse_face" />
|
<input type="text" name="name" id="emoji_name" placeholder="mouse_face" />
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="emoji_url">{{t "Emoji URL" }}</label>
|
<p id="emoji-file-name"></p>
|
||||||
<input type="text" name="url" id="emoji_url" placeholder="http://emojipedia-us.s3.amazonaws.com/cache/46/7f/467fe69069c408e07517621f263ea9b5.png" />
|
<input type="file" name="emoji_file_input" class="notvisible"
|
||||||
|
id="emoji_file_input" value="{{t 'Upload emoji' }}"/>
|
||||||
|
<button class="btn btn-default display-none" id="emoji_image_clear_button">{{t "Clear emoji image" }}</button>
|
||||||
|
<button class="button white" id="emoji_upload_button">{{t "Upload emoji" }}</button>
|
||||||
|
<p id="emoji_file_input_error" class="text-error"></p>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="button white rounded sea-green">
|
<button type="submit" class="button white rounded sea-green">
|
||||||
{{t 'Add emoji' }}
|
{{t 'Add emoji' }}
|
||||||
|
|||||||
@@ -3095,9 +3095,9 @@ def notify_realm_emoji(realm):
|
|||||||
realm_emoji=realm.get_emoji())
|
realm_emoji=realm.get_emoji())
|
||||||
send_event(event, active_user_ids(realm))
|
send_event(event, active_user_ids(realm))
|
||||||
|
|
||||||
def check_add_realm_emoji(realm, name, img_url, author=None):
|
def check_add_realm_emoji(realm, name, file_name, author=None):
|
||||||
# type: (Realm, Text, Text, Optional[UserProfile]) -> None
|
# type: (Realm, Text, Text, Optional[UserProfile]) -> None
|
||||||
emoji = RealmEmoji(realm=realm, name=name, img_url=img_url, author=author)
|
emoji = RealmEmoji(realm=realm, name=name, file_name=file_name, author=author)
|
||||||
emoji.full_clean()
|
emoji.full_clean()
|
||||||
emoji.save()
|
emoji.save()
|
||||||
notify_realm_emoji(realm)
|
notify_realm_emoji(realm)
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ class Emoji(markdown.inlinepatterns.Pattern):
|
|||||||
realm_emoji = db_data['emoji']
|
realm_emoji = db_data['emoji']
|
||||||
|
|
||||||
if current_message and name in realm_emoji:
|
if current_message and name in realm_emoji:
|
||||||
return make_realm_emoji(realm_emoji[name]['display_url'], orig_syntax)
|
return make_realm_emoji(realm_emoji[name]['source_url'], orig_syntax)
|
||||||
elif name == 'zulip':
|
elif name == 'zulip':
|
||||||
return make_realm_emoji('/static/generated/emoji/images/emoji/unicode/zulip.png', orig_syntax)
|
return make_realm_emoji('/static/generated/emoji/images/emoji/unicode/zulip.png', orig_syntax)
|
||||||
elif name in name_to_codepoint:
|
elif name in name_to_codepoint:
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from typing import Text
|
from typing import Text
|
||||||
|
|
||||||
from zerver.lib.bugdown import name_to_codepoint
|
from zerver.lib.bugdown import name_to_codepoint
|
||||||
from zerver.lib.request import JsonableError
|
from zerver.lib.request import JsonableError
|
||||||
|
from zerver.lib.upload import upload_backend
|
||||||
from zerver.models import Realm, UserProfile
|
from zerver.models import Realm, UserProfile
|
||||||
|
|
||||||
def check_valid_emoji(realm, emoji_name):
|
def check_valid_emoji(realm, emoji_name):
|
||||||
@@ -29,3 +30,13 @@ def check_valid_emoji_name(emoji_name):
|
|||||||
if re.match('^[0-9a-zA-Z.\-_]+(?<![.\-_])$', emoji_name):
|
if re.match('^[0-9a-zA-Z.\-_]+(?<![.\-_])$', emoji_name):
|
||||||
return
|
return
|
||||||
raise JsonableError(_("Invalid characters in emoji name"))
|
raise JsonableError(_("Invalid characters in emoji name"))
|
||||||
|
|
||||||
|
def get_emoji_url(emoji_file_name, realm_id):
|
||||||
|
# type: (Text, int) -> Text
|
||||||
|
return upload_backend.get_emoji_url(emoji_file_name, realm_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_emoji_file_name(emoji_file_name, emoji_name):
|
||||||
|
# type: (Text, Text) -> Text
|
||||||
|
_, image_ext = os.path.splitext(emoji_file_name)
|
||||||
|
return ''.join((emoji_name, image_ext))
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ from boto.s3.connection import S3Connection
|
|||||||
from mimetypes import guess_type, guess_extension
|
from mimetypes import guess_type, guess_extension
|
||||||
|
|
||||||
from zerver.lib.str_utils import force_bytes, force_str
|
from zerver.lib.str_utils import force_bytes, force_str
|
||||||
from zerver.models import get_user_profile_by_email, get_user_profile_by_id
|
from zerver.models import get_user_profile_by_email, get_user_profile_by_id, RealmEmoji
|
||||||
from zerver.models import Attachment
|
from zerver.models import Attachment
|
||||||
from zerver.models import Realm, UserProfile, Message
|
from zerver.models import Realm, RealmEmoji, UserProfile, Message
|
||||||
|
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
import base64
|
import base64
|
||||||
@@ -36,6 +36,7 @@ import logging
|
|||||||
|
|
||||||
DEFAULT_AVATAR_SIZE = 100
|
DEFAULT_AVATAR_SIZE = 100
|
||||||
MEDIUM_AVATAR_SIZE = 500
|
MEDIUM_AVATAR_SIZE = 500
|
||||||
|
DEFAULT_EMOJI_SIZE = 64
|
||||||
|
|
||||||
# Performance Note:
|
# Performance Note:
|
||||||
#
|
#
|
||||||
@@ -99,6 +100,26 @@ def resize_avatar(image_data, size=DEFAULT_AVATAR_SIZE):
|
|||||||
im.save(out, format='png')
|
im.save(out, format='png')
|
||||||
return out.getvalue()
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def resize_emoji(image_data, size=DEFAULT_EMOJI_SIZE):
|
||||||
|
# type: (binary_type, int) -> binary_type
|
||||||
|
try:
|
||||||
|
im = Image.open(io.BytesIO(image_data))
|
||||||
|
image_format = im.format
|
||||||
|
if image_format == 'GIF' and im.is_animated:
|
||||||
|
if im.size[0] > size or im.size[1] > size:
|
||||||
|
raise JsonableError(
|
||||||
|
_("Animated emoji can't be larger than 64px in width or height."))
|
||||||
|
else:
|
||||||
|
return image_data
|
||||||
|
im = ImageOps.fit(im, (size, size), Image.ANTIALIAS)
|
||||||
|
except IOError:
|
||||||
|
raise BadImageError("Could not decode image; did you upload an image file?")
|
||||||
|
out = io.BytesIO()
|
||||||
|
im.save(out, format=image_format)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
### Common
|
### Common
|
||||||
|
|
||||||
class ZulipUploadBackend(object):
|
class ZulipUploadBackend(object):
|
||||||
@@ -131,6 +152,14 @@ class ZulipUploadBackend(object):
|
|||||||
# type: (int, int) -> Text
|
# type: (int, int) -> Text
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def upload_emoji_image(self, emoji_file, emoji_file_name, user_profile):
|
||||||
|
# type: (File, Text, UserProfile) -> None
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_emoji_url(self, emoji_file_name, realm_id):
|
||||||
|
# type: (Text, int) -> Text
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
### S3
|
### S3
|
||||||
|
|
||||||
@@ -221,7 +250,9 @@ def get_realm_for_filename(path):
|
|||||||
return None
|
return None
|
||||||
return get_user_profile_by_id(key.metadata["user_profile_id"]).realm_id
|
return get_user_profile_by_id(key.metadata["user_profile_id"]).realm_id
|
||||||
|
|
||||||
|
|
||||||
class S3UploadBackend(ZulipUploadBackend):
|
class S3UploadBackend(ZulipUploadBackend):
|
||||||
|
|
||||||
def upload_message_image(self, uploaded_file_name, uploaded_file_size,
|
def upload_message_image(self, uploaded_file_name, uploaded_file_size,
|
||||||
content_type, file_data, user_profile, target_realm=None):
|
content_type, file_data, user_profile, target_realm=None):
|
||||||
# type: (Text, int, Optional[Text], binary_type, UserProfile, Optional[Realm]) -> Text
|
# type: (Text, int, Optional[Text], binary_type, UserProfile, Optional[Realm]) -> Text
|
||||||
@@ -357,6 +388,40 @@ class S3UploadBackend(ZulipUploadBackend):
|
|||||||
resized_medium
|
resized_medium
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def upload_emoji_image(self, emoji_file, emoji_file_name, user_profile):
|
||||||
|
# type: (File, Text, UserProfile) -> None
|
||||||
|
content_type = guess_type(emoji_file.name)[0]
|
||||||
|
bucket_name = settings.S3_AVATAR_BUCKET
|
||||||
|
emoji_path = RealmEmoji.PATH_ID_TEMPLATE.format(
|
||||||
|
realm_id=user_profile.realm_id,
|
||||||
|
emoji_file_name=emoji_file_name
|
||||||
|
)
|
||||||
|
|
||||||
|
image_data = emoji_file.read()
|
||||||
|
resized_image_data = resize_emoji(image_data)
|
||||||
|
upload_image_to_s3(
|
||||||
|
bucket_name,
|
||||||
|
".".join((emoji_path, "original")),
|
||||||
|
content_type,
|
||||||
|
user_profile,
|
||||||
|
image_data,
|
||||||
|
)
|
||||||
|
upload_image_to_s3(
|
||||||
|
bucket_name,
|
||||||
|
emoji_path,
|
||||||
|
content_type,
|
||||||
|
user_profile,
|
||||||
|
resized_image_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_emoji_url(self, emoji_file_name, realm_id):
|
||||||
|
# type: (Text, int) -> Text
|
||||||
|
bucket = settings.S3_AVATAR_BUCKET
|
||||||
|
emoji_path = RealmEmoji.PATH_ID_TEMPLATE.format(realm_id=realm_id,
|
||||||
|
emoji_file_name=emoji_file_name)
|
||||||
|
return u"https://%s.s3.amazonaws.com/%s" % (bucket, emoji_path)
|
||||||
|
|
||||||
|
|
||||||
### Local
|
### Local
|
||||||
|
|
||||||
def mkdirs(path):
|
def mkdirs(path):
|
||||||
@@ -459,6 +524,30 @@ class LocalUploadBackend(ZulipUploadBackend):
|
|||||||
resized_medium = resize_avatar(image_data, MEDIUM_AVATAR_SIZE)
|
resized_medium = resize_avatar(image_data, MEDIUM_AVATAR_SIZE)
|
||||||
write_local_file('avatars', file_path + '-medium.png', resized_medium)
|
write_local_file('avatars', file_path + '-medium.png', resized_medium)
|
||||||
|
|
||||||
|
def upload_emoji_image(self, emoji_file, emoji_file_name, user_profile):
|
||||||
|
# type: (File, Text, UserProfile) -> None
|
||||||
|
emoji_path = RealmEmoji.PATH_ID_TEMPLATE.format(
|
||||||
|
realm_id= user_profile.realm_id,
|
||||||
|
emoji_file_name=emoji_file_name
|
||||||
|
)
|
||||||
|
|
||||||
|
image_data = emoji_file.read()
|
||||||
|
resized_image_data = resize_emoji(image_data)
|
||||||
|
write_local_file(
|
||||||
|
'avatars',
|
||||||
|
".".join((emoji_path, "original")),
|
||||||
|
image_data)
|
||||||
|
write_local_file(
|
||||||
|
'avatars',
|
||||||
|
emoji_path,
|
||||||
|
resized_image_data)
|
||||||
|
|
||||||
|
def get_emoji_url(self, emoji_file_name, realm_id):
|
||||||
|
# type: (Text, int) -> Text
|
||||||
|
return os.path.join(
|
||||||
|
u"/user_avatars",
|
||||||
|
RealmEmoji.PATH_ID_TEMPLATE.format(realm_id=realm_id, emoji_file_name=emoji_file_name))
|
||||||
|
|
||||||
# Common and wrappers
|
# Common and wrappers
|
||||||
if settings.LOCAL_UPLOADS_DIR is not None:
|
if settings.LOCAL_UPLOADS_DIR is not None:
|
||||||
upload_backend = LocalUploadBackend() # type: ZulipUploadBackend
|
upload_backend = LocalUploadBackend() # type: ZulipUploadBackend
|
||||||
@@ -477,6 +566,10 @@ def upload_icon_image(user_file, user_profile):
|
|||||||
# type: (File, UserProfile) -> None
|
# type: (File, UserProfile) -> None
|
||||||
upload_backend.upload_realm_icon_image(user_file, user_profile)
|
upload_backend.upload_realm_icon_image(user_file, user_profile)
|
||||||
|
|
||||||
|
def upload_emoji_image(emoji_file, emoji_file_name, user_profile):
|
||||||
|
# type: (File, Text, UserProfile) -> None
|
||||||
|
upload_backend.upload_emoji_image(emoji_file, emoji_file_name, user_profile)
|
||||||
|
|
||||||
def upload_message_image(uploaded_file_name, uploaded_file_size,
|
def upload_message_image(uploaded_file_name, uploaded_file_size,
|
||||||
content_type, file_data, user_profile, target_realm=None):
|
content_type, file_data, user_profile, target_realm=None):
|
||||||
# type: (Text, int, Optional[Text], binary_type, UserProfile, Optional[Realm]) -> Text
|
# type: (Text, int, Optional[Text], binary_type, UserProfile, Optional[Realm]) -> Text
|
||||||
|
|||||||
175
zerver/migrations/0077_add_file_name_field_to_realm_emoji.py
Normal file
175
zerver/migrations/0077_add_file_name_field_to_realm_emoji.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-03-09 05:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from mimetypes import guess_type
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from PIL import ImageOps
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
|
||||||
|
from django.db.migrations.state import StateApps
|
||||||
|
from django.conf import settings
|
||||||
|
from boto.s3.key import Key
|
||||||
|
from boto.s3.connection import S3Connection
|
||||||
|
from requests import ConnectionError, Response
|
||||||
|
from typing import Dict, Text, Tuple, Optional, Union
|
||||||
|
|
||||||
|
from six import binary_type
|
||||||
|
|
||||||
|
|
||||||
|
def force_str(s, encoding='utf-8'):
|
||||||
|
# type: (Union[Text, binary_type], Text) -> str
|
||||||
|
"""converts a string to a native string"""
|
||||||
|
if isinstance(s, str):
|
||||||
|
return s
|
||||||
|
elif isinstance(s, Text):
|
||||||
|
return s.encode(str(encoding))
|
||||||
|
elif isinstance(s, binary_type):
|
||||||
|
return s.decode(encoding)
|
||||||
|
else:
|
||||||
|
raise TypeError("force_str expects a string type")
|
||||||
|
|
||||||
|
|
||||||
|
class Uploader(object):
|
||||||
|
def __init__(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.path_template = "{realm_id}/emoji/{emoji_file_name}"
|
||||||
|
self.emoji_size = (64, 64)
|
||||||
|
|
||||||
|
def upload_files(self, response, resized_image, dst_path_id):
|
||||||
|
# type: (Response, binary_type, Text) -> None
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_dst_path_id(self, realm_id, url, emoji_name):
|
||||||
|
# type: (int, Text, Text) -> Tuple[Text,Text]
|
||||||
|
_, image_ext = os.path.splitext(url)
|
||||||
|
file_name = ''.join((emoji_name, image_ext))
|
||||||
|
return file_name, self.path_template.format(realm_id=realm_id, emoji_file_name=file_name)
|
||||||
|
|
||||||
|
def resize_emoji(self, image_data):
|
||||||
|
# type: (binary_type) -> Optional[binary_type]
|
||||||
|
im = Image.open(io.BytesIO(image_data))
|
||||||
|
format_ = im.format
|
||||||
|
if format_ == 'GIF' and im.is_animated:
|
||||||
|
return None
|
||||||
|
im = ImageOps.fit(im, self.emoji_size, Image.ANTIALIAS)
|
||||||
|
out = io.BytesIO()
|
||||||
|
im.save(out, format_)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
def upload_emoji(self, realm_id, image_url, emoji_name):
|
||||||
|
# type: (int, Text, Text) -> Optional[Text]
|
||||||
|
file_name, dst_path_id = self.get_dst_path_id(realm_id, image_url, emoji_name)
|
||||||
|
try:
|
||||||
|
response = requests.get(image_url, stream=True)
|
||||||
|
except ConnectionError:
|
||||||
|
return None
|
||||||
|
if response.status_code != 200:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
resized_image = self.resize_emoji(response.content)
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
self.upload_files(response, resized_image, dst_path_id)
|
||||||
|
return file_name
|
||||||
|
|
||||||
|
|
||||||
|
class LocalUploader(Uploader):
|
||||||
|
def __init__(self):
|
||||||
|
# type: () -> None
|
||||||
|
super(LocalUploader, self).__init__()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mkdirs(path):
|
||||||
|
# type: (Text) -> None
|
||||||
|
dirname = os.path.dirname(path)
|
||||||
|
if not os.path.isdir(dirname):
|
||||||
|
os.makedirs(dirname)
|
||||||
|
|
||||||
|
def write_local_file(self, path, file_data):
|
||||||
|
# type: (Text, binary_type) -> None
|
||||||
|
self.mkdirs(path)
|
||||||
|
with open(path, 'wb') as f: # type: ignore
|
||||||
|
f.write(file_data)
|
||||||
|
|
||||||
|
def upload_files(self, response, resized_image, dst_path_id):
|
||||||
|
# type: (Response, binary_type, Text) -> None
|
||||||
|
dst_file = os.path.join(settings.LOCAL_UPLOADS_DIR, 'avatars', dst_path_id)
|
||||||
|
if resized_image:
|
||||||
|
self.write_local_file(dst_file, resized_image)
|
||||||
|
else:
|
||||||
|
self.write_local_file(dst_file, response.content)
|
||||||
|
self.write_local_file('.'.join((dst_file, 'original')), response.content)
|
||||||
|
|
||||||
|
|
||||||
|
class S3Uploader(Uploader):
|
||||||
|
def __init__(self):
|
||||||
|
# type: () -> None
|
||||||
|
super(S3Uploader, self).__init__()
|
||||||
|
conn = S3Connection(settings.S3_KEY, settings.S3_SECRET_KEY)
|
||||||
|
bucket_name = settings.S3_AVATAR_BUCKET
|
||||||
|
self.bucket = conn.get_bucket(bucket_name, validate=False)
|
||||||
|
|
||||||
|
def upload_to_s3(self, path, file_data, headers):
|
||||||
|
# type: (Text, binary_type, Optional[Dict[Text, Text]]) -> None
|
||||||
|
key = Key(self.bucket)
|
||||||
|
key.key = path
|
||||||
|
key.set_contents_from_string(force_str(file_data), headers=headers)
|
||||||
|
|
||||||
|
def upload_files(self, response, resized_image, dst_path_id):
|
||||||
|
# type: (Response, binary_type, Text) -> None
|
||||||
|
headers = None # type: Optional[Dict[Text, Text]]
|
||||||
|
content_type = response.headers.get(str("Content-Type")) or guess_type(dst_path_id)[0]
|
||||||
|
if content_type:
|
||||||
|
headers = {u'Content-Type': content_type}
|
||||||
|
if resized_image:
|
||||||
|
self.upload_to_s3(dst_path_id, resized_image, headers)
|
||||||
|
else:
|
||||||
|
self.upload_to_s3(dst_path_id, response.content, headers)
|
||||||
|
self.upload_to_s3('.'.join((dst_path_id, 'original')), response.content, headers)
|
||||||
|
|
||||||
|
def get_uploader():
|
||||||
|
# type: () -> Uploader
|
||||||
|
if settings.LOCAL_UPLOADS_DIR is None:
|
||||||
|
return S3Uploader()
|
||||||
|
return LocalUploader()
|
||||||
|
|
||||||
|
|
||||||
|
def upload_emoji_to_storage(apps, schema_editor):
|
||||||
|
# type: (StateApps, DatabaseSchemaEditor) -> None
|
||||||
|
realm_emoji_model = apps.get_model('zerver', 'RealmEmoji')
|
||||||
|
uploader = get_uploader() # type: Uploader
|
||||||
|
for emoji in realm_emoji_model.objects.all():
|
||||||
|
file_name = uploader.upload_emoji(emoji.realm_id, emoji.img_url, emoji.name)
|
||||||
|
if file_name is None:
|
||||||
|
logging.warning("ERROR: Could not download emoji %s; please reupload manually" %
|
||||||
|
(emoji,))
|
||||||
|
emoji.file_name = file_name
|
||||||
|
emoji.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('zerver', '0076_userprofile_emojiset'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='realmemoji',
|
||||||
|
name='file_name',
|
||||||
|
field=models.TextField(db_index=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.RunPython(upload_emoji_to_storage),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='realmemoji',
|
||||||
|
name='img_url',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from typing import Any, DefaultDict, Dict, List, Set, Tuple, TypeVar, Text, \
|
from typing import Any, DefaultDict, Dict, List, Set, Tuple, TypeVar, Text, \
|
||||||
Union, Optional, Sequence, AbstractSet, Pattern, AnyStr, Callable
|
Union, Optional, Sequence, AbstractSet, Pattern, AnyStr, Callable, Iterable
|
||||||
from typing.re import Match
|
from typing.re import Match
|
||||||
from zerver.lib.str_utils import NonBinaryStr
|
from zerver.lib.str_utils import NonBinaryStr
|
||||||
|
|
||||||
@@ -25,7 +25,6 @@ from zerver.lib.cache import cache_with_key, flush_user_profile, flush_realm, \
|
|||||||
from zerver.lib.utils import make_safe_digest, generate_random_token
|
from zerver.lib.utils import make_safe_digest, generate_random_token
|
||||||
from zerver.lib.str_utils import ModelReprMixin
|
from zerver.lib.str_utils import ModelReprMixin
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from zerver.lib.camo import get_camo_url
|
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
@@ -197,7 +196,7 @@ class Realm(ModelReprMixin, models.Model):
|
|||||||
|
|
||||||
@cache_with_key(get_realm_emoji_cache_key, timeout=3600*24*7)
|
@cache_with_key(get_realm_emoji_cache_key, timeout=3600*24*7)
|
||||||
def get_emoji(self):
|
def get_emoji(self):
|
||||||
# type: () -> Dict[Text, Optional[Dict[str, Text]]]
|
# type: () -> Dict[Text, Optional[Dict[str, Iterable[Text]]]]
|
||||||
return get_realm_emoji_uncached(self)
|
return get_realm_emoji_uncached(self)
|
||||||
|
|
||||||
def get_admin_users(self):
|
def get_admin_users(self):
|
||||||
@@ -394,20 +393,21 @@ class RealmEmoji(ModelReprMixin, models.Model):
|
|||||||
name = models.TextField(validators=[MinLengthValidator(1),
|
name = models.TextField(validators=[MinLengthValidator(1),
|
||||||
RegexValidator(regex=r'^[0-9a-zA-Z.\-_]+(?<![.\-_])$',
|
RegexValidator(regex=r'^[0-9a-zA-Z.\-_]+(?<![.\-_])$',
|
||||||
message=_("Invalid characters in emoji name"))]) # type: Text
|
message=_("Invalid characters in emoji name"))]) # type: Text
|
||||||
# URLs start having browser compatibility problem below 2000
|
file_name = models.TextField(db_index=True, null=True) # type: Text
|
||||||
# characters, so 1000 seems like a safe limit.
|
|
||||||
img_url = models.URLField(max_length=1000) # type: Text
|
PATH_ID_TEMPLATE = "{realm_id}/emoji/{emoji_file_name}"
|
||||||
|
|
||||||
class Meta(object):
|
class Meta(object):
|
||||||
unique_together = ("realm", "name")
|
unique_together = ("realm", "name")
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
# type: () -> Text
|
# type: () -> Text
|
||||||
return u"<RealmEmoji(%s): %s %s>" % (self.realm.string_id, self.name, self.img_url)
|
return u"<RealmEmoji(%s): %s %s>" % (self.realm.string_id, self.name, self.file_name)
|
||||||
|
|
||||||
def get_realm_emoji_uncached(realm):
|
def get_realm_emoji_uncached(realm):
|
||||||
# type: (Realm) -> Dict[Text, Optional[Dict[str, Text]]]
|
# type: (Realm) -> Dict[Text, Optional[Dict[str, Iterable[Text]]]]
|
||||||
d = {}
|
d = {}
|
||||||
|
from zerver.lib.emoji import get_emoji_url
|
||||||
for row in RealmEmoji.objects.filter(realm=realm).select_related('author'):
|
for row in RealmEmoji.objects.filter(realm=realm).select_related('author'):
|
||||||
if row.author:
|
if row.author:
|
||||||
author = {
|
author = {
|
||||||
@@ -416,8 +416,7 @@ def get_realm_emoji_uncached(realm):
|
|||||||
'full_name': row.author.full_name}
|
'full_name': row.author.full_name}
|
||||||
else:
|
else:
|
||||||
author = None
|
author = None
|
||||||
d[row.name] = dict(source_url=row.img_url,
|
d[row.name] = dict(source_url=get_emoji_url(row.file_name, row.realm_id),
|
||||||
display_url=get_camo_url(row.img_url),
|
|
||||||
author=author)
|
author=author)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from zerver.lib.actions import (
|
|||||||
)
|
)
|
||||||
from zerver.lib.alert_words import alert_words_in_realm
|
from zerver.lib.alert_words import alert_words_in_realm
|
||||||
from zerver.lib.camo import get_camo_url
|
from zerver.lib.camo import get_camo_url
|
||||||
|
from zerver.lib.emoji import get_emoji_url
|
||||||
from zerver.lib.message import render_markdown
|
from zerver.lib.message import render_markdown
|
||||||
from zerver.lib.request import (
|
from zerver.lib.request import (
|
||||||
JsonableError,
|
JsonableError,
|
||||||
@@ -478,18 +479,18 @@ class BugdownTest(TestCase):
|
|||||||
|
|
||||||
def test_realm_emoji(self):
|
def test_realm_emoji(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
def emoji_img(name, url):
|
def emoji_img(name, file_name, realm_id):
|
||||||
# type: (Text, Text) -> Text
|
# type: (Text, Text, int) -> Text
|
||||||
return '<img alt="%s" class="emoji" src="%s" title="%s">' % (name, get_camo_url(url), name)
|
return '<img alt="%s" class="emoji" src="%s" title="%s">' % (
|
||||||
|
name, get_emoji_url(file_name, realm_id), name)
|
||||||
|
|
||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
url = "https://zulip.com/test_realm_emoji.png"
|
check_add_realm_emoji(realm, "test", 'test.png')
|
||||||
check_add_realm_emoji(realm, "test", url)
|
|
||||||
|
|
||||||
# Needs to mock an actual message because that's how bugdown obtains the realm
|
# 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"))
|
msg = Message(sender=get_user_profile_by_email("hamlet@zulip.com"))
|
||||||
converted = bugdown.convert(":test:", message_realm=realm, message=msg)
|
converted = bugdown.convert(":test:", message_realm=realm, message=msg)
|
||||||
self.assertEqual(converted, '<p>%s</p>' % (emoji_img(':test:', url)))
|
self.assertEqual(converted, '<p>%s</p>' % (emoji_img(':test:', 'test.png', realm.id)))
|
||||||
|
|
||||||
do_remove_realm_emoji(realm, 'test')
|
do_remove_realm_emoji(realm, 'test')
|
||||||
converted = bugdown.convert(":test:", message_realm=realm, message=msg)
|
converted = bugdown.convert(":test:", message_realm=realm, message=msg)
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import ujson
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
from zerver.lib.test_helpers import tornado_redirected_to_list, get_display_recipient
|
from zerver.lib.test_helpers import tornado_redirected_to_list, get_display_recipient, \
|
||||||
|
get_test_image_file
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.models import get_realm, get_user_profile_by_email, Recipient, UserMessage
|
from zerver.models import get_realm, get_user_profile_by_email, Recipient, UserMessage
|
||||||
|
|
||||||
@@ -97,9 +98,10 @@ class ReactionEmojiTest(ZulipTestCase):
|
|||||||
"""
|
"""
|
||||||
sender = 'hamlet@zulip.com'
|
sender = 'hamlet@zulip.com'
|
||||||
emoji_name = 'my_emoji'
|
emoji_name = 'my_emoji'
|
||||||
emoji_data = {'url': 'https://example.com/my_emoji'}
|
with get_test_image_file('img.png') as fp1:
|
||||||
result = self.client_put('/json/realm/emoji/my_emoji', info=emoji_data,
|
emoji_data = {'f1': fp1}
|
||||||
**self.api_auth(sender))
|
result = self.client_put_multipart('/json/realm/emoji/my_emoji', info=emoji_data,
|
||||||
|
**self.api_auth(sender))
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
self.assertEqual(200, result.status_code)
|
self.assertEqual(200, result.status_code)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import absolute_import
|
|||||||
|
|
||||||
from zerver.lib.actions import get_realm, check_add_realm_emoji
|
from zerver.lib.actions import get_realm, check_add_realm_emoji
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
|
from zerver.lib.test_helpers import get_test_image_file
|
||||||
from zerver.models import RealmEmoji
|
from zerver.models import RealmEmoji
|
||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
# type: () -> None
|
# type: () -> None
|
||||||
self.login("iago@zulip.com")
|
self.login("iago@zulip.com")
|
||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
check_add_realm_emoji(realm, "my_emoji", "https://example.com/my_emoji")
|
check_add_realm_emoji(realm, "my_emoji", "my_emoji")
|
||||||
result = self.client_get("/json/realm/emoji")
|
result = self.client_get("/json/realm/emoji")
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
self.assertEqual(200, result.status_code)
|
self.assertEqual(200, result.status_code)
|
||||||
@@ -23,7 +24,7 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
# type: () -> None
|
# type: () -> None
|
||||||
self.login("iago@zulip.com")
|
self.login("iago@zulip.com")
|
||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
RealmEmoji.objects.create(realm=realm, name='my_emojy', img_url='https://example.com/my_emoji')
|
RealmEmoji.objects.create(realm=realm, name='my_emojy', file_name='my_emojy')
|
||||||
result = self.client_get("/json/realm/emoji")
|
result = self.client_get("/json/realm/emoji")
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
content = ujson.loads(result.content)
|
content = ujson.loads(result.content)
|
||||||
@@ -36,19 +37,20 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
realm.add_emoji_by_admins_only = True
|
realm.add_emoji_by_admins_only = True
|
||||||
realm.save()
|
realm.save()
|
||||||
check_add_realm_emoji(realm, "my_emoji", "https://example.com/my_emoji")
|
check_add_realm_emoji(realm, 'my_emojy', 'my_emojy')
|
||||||
result = self.client_get("/json/realm/emoji")
|
result = self.client_get("/json/realm/emoji")
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
content = ujson.loads(result.content)
|
content = ujson.loads(result.content)
|
||||||
self.assertEqual(len(content["emoji"]), 1)
|
self.assertEqual(len(content["emoji"]), 1)
|
||||||
self.assertIsNone(content["emoji"]['my_emoji']['author'])
|
self.assertIsNone(content["emoji"]['my_emojy']['author'])
|
||||||
|
|
||||||
def test_upload(self):
|
def test_upload(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
email = "iago@zulip.com"
|
email = "iago@zulip.com"
|
||||||
self.login(email)
|
self.login(email)
|
||||||
data = {"url": "https://example.com/my_emoji"}
|
with get_test_image_file('img.png') as fp1:
|
||||||
result = self.client_put("/json/realm/emoji/my_emoji", data)
|
emoji_data = {'f1': fp1}
|
||||||
|
result = self.client_put_multipart('/json/realm/emoji/my_emoji', info=emoji_data)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
self.assertEqual(200, result.status_code)
|
self.assertEqual(200, result.status_code)
|
||||||
emoji = RealmEmoji.objects.get(name="my_emoji")
|
emoji = RealmEmoji.objects.get(name="my_emoji")
|
||||||
@@ -65,14 +67,15 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
realm_emoji = RealmEmoji.objects.get(realm=get_realm('zulip'))
|
realm_emoji = RealmEmoji.objects.get(realm=get_realm('zulip'))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(realm_emoji),
|
str(realm_emoji),
|
||||||
'<RealmEmoji(zulip): my_emoji https://example.com/my_emoji>'
|
'<RealmEmoji(zulip): my_emoji my_emoji.png>'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_upload_exception(self):
|
def test_upload_exception(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
self.login("iago@zulip.com")
|
self.login("iago@zulip.com")
|
||||||
data = {"url": "https://example.com/my_emoji"}
|
with get_test_image_file('img.png') as fp1:
|
||||||
result = self.client_put("/json/realm/emoji/my_em*oji", info=data)
|
emoji_data = {'f1': fp1}
|
||||||
|
result = self.client_put_multipart('/json/realm/emoji/my_em*oji', info=emoji_data)
|
||||||
self.assert_json_error(result, 'Invalid characters in emoji name')
|
self.assert_json_error(result, 'Invalid characters in emoji name')
|
||||||
|
|
||||||
def test_upload_admins_only(self):
|
def test_upload_admins_only(self):
|
||||||
@@ -81,15 +84,16 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
realm.add_emoji_by_admins_only = True
|
realm.add_emoji_by_admins_only = True
|
||||||
realm.save()
|
realm.save()
|
||||||
data = {"url": "https://example.com/my_emoji"}
|
with get_test_image_file('img.png') as fp1:
|
||||||
result = self.client_put("/json/realm/emoji/my_emoji", info=data)
|
emoji_data = {'f1': fp1}
|
||||||
|
result = self.client_put_multipart('/json/realm/emoji/my_emoji', info=emoji_data)
|
||||||
self.assert_json_error(result, 'Must be a realm administrator')
|
self.assert_json_error(result, 'Must be a realm administrator')
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
self.login("iago@zulip.com")
|
self.login("iago@zulip.com")
|
||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
check_add_realm_emoji(realm, "my_emoji", "https://example.com/my_emoji")
|
check_add_realm_emoji(realm, "my_emoji", "my_emoji.png")
|
||||||
result = self.client_delete("/json/realm/emoji/my_emoji")
|
result = self.client_delete("/json/realm/emoji/my_emoji")
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
@@ -104,7 +108,7 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
realm = get_realm('zulip')
|
realm = get_realm('zulip')
|
||||||
realm.add_emoji_by_admins_only = True
|
realm.add_emoji_by_admins_only = True
|
||||||
realm.save()
|
realm.save()
|
||||||
check_add_realm_emoji(realm, "my_emoji", "https://example.com/my_emoji")
|
check_add_realm_emoji(realm, "my_emoji", "my_emoji.png")
|
||||||
result = self.client_delete("/json/realm/emoji/my_emoji")
|
result = self.client_delete("/json/realm/emoji/my_emoji")
|
||||||
self.assert_json_error(result, 'Must be a realm administrator')
|
self.assert_json_error(result, 'Must be a realm administrator')
|
||||||
|
|
||||||
@@ -113,3 +117,29 @@ class RealmEmojiTest(ZulipTestCase):
|
|||||||
self.login("iago@zulip.com")
|
self.login("iago@zulip.com")
|
||||||
result = self.client_delete("/json/realm/emoji/invalid_emoji")
|
result = self.client_delete("/json/realm/emoji/invalid_emoji")
|
||||||
self.assert_json_error(result, "Emoji 'invalid_emoji' does not exist")
|
self.assert_json_error(result, "Emoji 'invalid_emoji' does not exist")
|
||||||
|
|
||||||
|
def test_multiple_upload(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.login("iago@zulip.com")
|
||||||
|
with get_test_image_file('img.png') as fp1, get_test_image_file('img.png') as fp2:
|
||||||
|
result = self.client_put_multipart('/json/realm/emoji/my_emoji', {'f1': fp1, 'f2': fp2})
|
||||||
|
self.assert_json_error(result, 'You must upload exactly one file.')
|
||||||
|
|
||||||
|
def test_emoji_upload_file_size_error(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.login("iago@zulip.com")
|
||||||
|
with get_test_image_file('img.png') as fp:
|
||||||
|
with self.settings(MAX_EMOJI_FILE_SIZE=0):
|
||||||
|
result = self.client_put_multipart('/json/realm/emoji/my_emoji', {'file': fp})
|
||||||
|
self.assert_json_error(result, 'Uploaded file is larger than the allowed limit of 0 MB')
|
||||||
|
|
||||||
|
def test_upload_already_existed_emoji(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.login("iago@zulip.com")
|
||||||
|
with get_test_image_file('img.png') as fp1:
|
||||||
|
emoji_data = {'f1': fp1}
|
||||||
|
self.client_put_multipart('/json/realm/emoji/my_emoji', info=emoji_data)
|
||||||
|
with get_test_image_file('img.png') as fp1:
|
||||||
|
emoji_data = {'f1': fp1}
|
||||||
|
result = self.client_put_multipart('/json/realm/emoji/my_emoji', info=emoji_data)
|
||||||
|
self.assert_json_error(result, 'Realm emoji with this Realm and Name already exists.')
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from typing import Text
|
from typing import Text
|
||||||
|
|
||||||
|
from zerver.lib.upload import upload_emoji_image
|
||||||
from zerver.models import UserProfile
|
from zerver.models import UserProfile
|
||||||
from zerver.lib.emoji import check_emoji_admin, check_valid_emoji_name, check_valid_emoji
|
from zerver.lib.emoji import check_emoji_admin, check_valid_emoji_name, check_valid_emoji, \
|
||||||
|
get_emoji_file_name
|
||||||
from zerver.lib.request import JsonableError, REQ, has_request_variables
|
from zerver.lib.request import JsonableError, REQ, has_request_variables
|
||||||
from zerver.lib.response import json_success
|
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.actions import check_add_realm_emoji, do_remove_realm_emoji
|
||||||
|
|
||||||
|
|
||||||
def list_emoji(request, user_profile):
|
def list_emoji(request, user_profile):
|
||||||
# type: (HttpRequest, UserProfile) -> HttpResponse
|
# type: (HttpRequest, UserProfile) -> HttpResponse
|
||||||
|
|
||||||
@@ -17,14 +22,27 @@ def list_emoji(request, user_profile):
|
|||||||
# emoji is public.
|
# emoji is public.
|
||||||
return json_success({'emoji': user_profile.realm.get_emoji()})
|
return json_success({'emoji': user_profile.realm.get_emoji()})
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def upload_emoji(request, user_profile, emoji_name, url=REQ()):
|
def upload_emoji(request, user_profile, emoji_name=REQ()):
|
||||||
# type: (HttpRequest, UserProfile, Text, Text) -> HttpResponse
|
# type: (HttpRequest, UserProfile, Text) -> HttpResponse
|
||||||
check_valid_emoji_name(emoji_name)
|
check_valid_emoji_name(emoji_name)
|
||||||
check_emoji_admin(user_profile)
|
check_emoji_admin(user_profile)
|
||||||
check_add_realm_emoji(user_profile.realm, emoji_name, url, author=user_profile)
|
if len(request.FILES) != 1:
|
||||||
|
return json_error(_("You must upload exactly one file."))
|
||||||
|
emoji_file = list(request.FILES.values())[0]
|
||||||
|
if (settings.MAX_EMOJI_FILE_SIZE * 1024 * 1024) < emoji_file.size:
|
||||||
|
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
|
||||||
|
settings.MAX_EMOJI_FILE_SIZE))
|
||||||
|
emoji_file_name = get_emoji_file_name(emoji_file.name, emoji_name)
|
||||||
|
upload_emoji_image(emoji_file, emoji_file_name, user_profile)
|
||||||
|
try:
|
||||||
|
check_add_realm_emoji(user_profile.realm, emoji_name, emoji_file_name, author=user_profile)
|
||||||
|
except ValidationError as e:
|
||||||
|
return json_error(e.messages[0])
|
||||||
return json_success()
|
return json_success()
|
||||||
|
|
||||||
|
|
||||||
def delete_emoji(request, user_profile, emoji_name):
|
def delete_emoji(request, user_profile, emoji_name):
|
||||||
# type: (HttpRequest, UserProfile, Text) -> HttpResponse
|
# type: (HttpRequest, UserProfile, Text) -> HttpResponse
|
||||||
check_emoji_admin(user_profile)
|
check_emoji_admin(user_profile)
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '',
|
|||||||
'MAX_FILE_UPLOAD_SIZE': 25,
|
'MAX_FILE_UPLOAD_SIZE': 25,
|
||||||
'MAX_AVATAR_FILE_SIZE': 5,
|
'MAX_AVATAR_FILE_SIZE': 5,
|
||||||
'MAX_ICON_FILE_SIZE': 5,
|
'MAX_ICON_FILE_SIZE': 5,
|
||||||
|
'MAX_EMOJI_FILE_SIZE': 5,
|
||||||
'ERROR_REPORTING': True,
|
'ERROR_REPORTING': True,
|
||||||
'BROWSER_ERROR_REPORTING': False,
|
'BROWSER_ERROR_REPORTING': False,
|
||||||
'STAGING_ERROR_NOTIFICATIONS': False,
|
'STAGING_ERROR_NOTIFICATIONS': False,
|
||||||
|
|||||||
Reference in New Issue
Block a user