settings: Add support for uploading logo for night mode.

This adds a new field named realm_night_logo which is used for
displaying the organization logo when the user is in night mode.

Fixes #11176.
This commit is contained in:
sahil839 2019-01-26 23:25:10 -08:00 committed by Tim Abbott
parent e67cf30dfd
commit 7157edf4af
27 changed files with 369 additions and 110 deletions

View File

@ -121,6 +121,7 @@
"reactions": false,
"realm_icon": false,
"realm_logo": false,
"realm_night_logo": false,
"recent_senders": false,
"reload": false,
"reload_state": false,

View File

@ -296,6 +296,16 @@ var event_fixtures = {
},
},
realm__update_dict__night_logo: {
type: 'realm',
op: 'update_dict',
property: 'night_logo',
data: {
night_logo_url: 'night_logo.png',
night_logo_source: 'U',
},
},
realm__deactivated: {
type: 'realm',
op: 'deactivated',
@ -940,6 +950,12 @@ with_overrides(function (override) {
assert_same(page_params.realm_logo_url, 'logo.png');
assert_same(page_params.realm_logo_source, 'U');
event = event_fixtures.realm__update_dict__night_logo;
override('realm_logo.rerender', noop);
dispatch(event);
assert_same(page_params.realm_night_logo_url, 'night_logo.png');
assert_same(page_params.realm_night_logo_source, 'U');
event = event_fixtures.realm__deactivated;
window.location = {};
dispatch(event);
@ -1336,6 +1352,7 @@ with_overrides(function (override) {
event = event_fixtures.update_display_settings__night_mode;
page_params.night_mode = false;
override('night_mode.enable', stub.f); // automatically checks if called
override('realm_logo.rerender', noop);
dispatch(event);
assert_same(page_params.night_mode, true);
});

View File

@ -57,6 +57,7 @@ const _ui_report = {
const _realm_logo = {
build_realm_logo_widget: noop,
build_realm_night_logo_widget: noop,
};
set_global('channel', _channel);

View File

@ -53,6 +53,8 @@ exports.build_page = function () {
realm_icon_url: page_params.realm_icon_url,
realm_logo_source: page_params.realm_logo_source,
realm_logo_url: page_params.realm_logo_url,
realm_night_logo_source: page_params.realm_night_logo_source,
realm_night_logo_url: page_params.realm_night_logo_url,
realm_mandatory_topics: page_params.realm_mandatory_topics,
realm_send_welcome_emails: page_params.realm_send_welcome_emails,
realm_message_content_allowed_in_email_notifications:

View File

@ -2,22 +2,22 @@
var realm_logo = (function () {
var exports = {};
exports.build_realm_logo_widget = function (upload_function) {
var get_file_input = function () {
return $('#realm_logo_file_input').expectOne();
};
if (page_params.realm_logo_source === 'D') {
$("#realm_logo_delete_button").hide();
} else {
$("#realm_logo_delete_button").show();
}
var data = {night: JSON.stringify(false)};
$("#realm_logo_delete_button").on('click', function (e) {
e.preventDefault();
e.stopPropagation();
channel.del({
url: '/json/realm/logo',
data: data,
});
});
@ -30,18 +30,61 @@ var realm_logo = (function () {
);
};
exports.build_realm_night_logo_widget = function (upload_function) {
var get_file_input = function () {
return $('#realm_night_logo_file_input').expectOne();
};
if (page_params.realm_night_logo_source === 'D') {
$("#realm_night_logo_delete_button").hide();
} else {
$("#realm_night_logo_delete_button").show();
}
var data = {night: JSON.stringify(true)};
$("#realm_night_logo_delete_button").on('click', function (e) {
e.preventDefault();
e.stopPropagation();
channel.del({
url: '/json/realm/logo',
data: data,
});
});
return upload_widget.build_direct_upload_widget(
get_file_input,
$("#realm_night_logo_file_input_error").expectOne(),
$("#realm_night_logo_upload_button").expectOne(),
upload_function,
page_params.max_logo_file_size
);
};
exports.rerender = function () {
var file_input = $("#realm_logo_file_input");
var night_file_input = $("#realm_night_logo_file_input");
$("#realm-settings-logo").attr("src", page_params.realm_logo_url);
$("#realm-logo").attr("src", page_params.realm_logo_url);
$("#realm-settings-night-logo").attr("src", page_params.realm_night_logo_url);
if (page_params.night_mode) {
$("#realm-logo").attr("src", page_params.realm_night_logo_url);
} else {
$("#realm-logo").attr("src", page_params.realm_logo_url);
}
if (page_params.realm_logo_source === 'U') {
$("#realm_logo_delete_button").show();
} else {
$("#realm_logo_delete_button").hide();
// Need to clear input because of a small edge case
// where you try to upload the same image you just deleted.
var file_input = $("#realm_logo_file_input");
file_input.val('');
}
if (page_params.realm_night_logo_source === 'U') {
$("#realm_night_logo_delete_button").show();
} else {
$("#realm_night_logo_delete_button").hide();
// Need to clear input because of a small edge case
// where you try to upload the same image you just deleted.
night_file_input.val('');
}
};
return exports;

View File

@ -165,6 +165,10 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
page_params.realm_logo_url = event.data.logo_url;
page_params.realm_logo_source = event.data.logo_source;
realm_logo.rerender();
} else if (event.op === 'update_dict' && event.property === 'night_logo') {
page_params.realm_night_logo_url = event.data.night_logo_url;
page_params.realm_night_logo_source = event.data.night_logo_source;
realm_logo.rerender();
} else if (event.op === 'deactivated') {
window.location.href = "/accounts/deactivated/";
}
@ -392,8 +396,10 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
setTimeout(function () {
if (event.setting === true) {
night_mode.enable();
realm_logo.rerender();
} else {
night_mode.disable();
realm_logo.rerender();
}
$("body").fadeIn(300);
}, 300);

View File

@ -1116,20 +1116,30 @@ exports.build_page = function () {
}
realm_icon.build_realm_icon_widget(upload_realm_icon);
function upload_realm_logo(file_input) {
function upload_realm_logo(file_input, night) {
var form_data = new FormData();
var spinner;
var error_field;
var button_text;
form_data.append('csrfmiddlewaretoken', csrf_token);
jQuery.each(file_input[0].files, function (i, file) {
form_data.append('file-' + i, file);
});
var error_field = $("#realm_logo_file_input_error");
if (night) {
error_field = $("#realm_night_logo_file_input_error");
spinner = $("#upload_night_logo_spinner");
button_text = $("#upload_night_logo_button_text");
} else {
error_field = $("#realm_logo_file_input_error");
spinner = $("#upload_logo_spinner");
button_text = $("#upload_logo_button_text");
}
spinner.expectOne();
error_field.hide();
var spinner = $("#upload_logo_spinner").expectOne();
button_text.expectOne().hide();
loading.make_indicator(spinner, {text: i18n.t("Uploading logo.")});
$("#upload_logo_button_text").expectOne().hide();
form_data.append('night', JSON.stringify(night));
channel.post({
url: '/json/realm/logo',
data: form_data,
@ -1137,17 +1147,18 @@ exports.build_page = function () {
processData: false,
contentType: false,
success: function () {
loading.destroy_indicator($("#upload_logo_spinner"));
$("#upload_logo_button_text").expectOne().show();
loading.destroy_indicator(spinner);
button_text.expectOne().show();
},
error: function (xhr) {
loading.destroy_indicator($("#upload_logo_spinner"));
$("#upload_logo_button_text").expectOne().show();
loading.destroy_indicator(spinner);
button_text.expectOne().show();
ui_report.error("", xhr, error_field);
},
});
}
realm_logo.build_realm_night_logo_widget(upload_realm_logo);
realm_logo.build_realm_logo_widget(upload_realm_logo);
$('#deactivate_realm_button').on('click', function (e) {

View File

@ -116,10 +116,15 @@ var upload_widget = (function () {
) {
// default value of max upladed file size
max_file_upload_size = max_file_upload_size || default_max_file_size;
function accept() {
input_error.hide();
upload_function(get_file_input());
if (upload_button[0].id === "realm_night_logo_upload_button") {
upload_function(get_file_input(), true);
} else if (upload_button[0].id === "realm_logo_upload_button") {
upload_function(get_file_input(), false);
} else {
upload_function(get_file_input());
}
}
function clear() {

View File

@ -970,7 +970,8 @@ input[type=checkbox].inline-block {
#upload_avatar_spinner,
#upload_logo_spinner,
#upload_icon_spinner {
#upload_icon_spinner,
#upload_night_logo_spinner {
font-size: 14px;
margin: auto;
}
@ -1113,7 +1114,8 @@ input[type=checkbox].inline-block {
height: 100px;
}
#realm-settings-logo {
#realm-settings-logo,
#realm-settings-night-logo {
border-radius: 5px;
box-shadow: 0px 0px 10px hsla(0, 0%, 0%, 0.2);
/* We allow actual images up to 800x100 in the main display, but the
@ -1690,7 +1692,8 @@ input[type=text]#settings_search {
}
@media (max-width: 1023px) {
#realm-settings-logo {
#realm-settings-logo,
#realm-settings-night-logo {
max-width: 600px;
height: 75px;
}
@ -1716,7 +1719,8 @@ input[type=text]#settings_search {
margin: 5px 0 0 0;
}
#realm-settings-logo {
#realm-settings-logo,
#realm-settings-night-logo {
max-width: 400px;
height: 50px;
}

View File

@ -66,6 +66,27 @@
</div>
</div>
<h3>{{t "Organization logo for night mode" }}</h3>
<p>{{t "Like Organization Logo, but for the night theme." }}</p>
<div class="realm-logo-section realm-night-logo-section">
<div class="block realm-logo-block">
<img id="realm-settings-night-logo" src="{{ realm_night_logo_url }}">
<input type="file" name="realm_night_logo_file_input" class="notvisible"
id="realm_night_logo_file_input" value="{{t 'Upload logo' }}"/>
</div>
<div class="block avatar-controls">
<div id="realm_night_logo_file_input_error" class="alert text-error"></div>
<button class="button rounded sea-green w-200 block input-size"
id="realm_night_logo_upload_button">
<span id="upload_night_logo_button_text">{{t 'Upload new logo' }}</span>
<span id="upload_night_logo_spinner"></span>
</button>
<button class="button rounded btn-danger w-200 m-t-10 block input-size"
id="realm_night_logo_delete_button">{{t 'Delete logo' }}</button>
</div>
</div>
<h3 class="light">{{t "Deactivate organization" }}</h3>
<div class="deactivate-realm-section">
<div class="input-group">

View File

@ -56,6 +56,9 @@ curl {{ api_url }}/v1/server_settings \
* `realm_logo`: the URI of the organization's logo as a horizontal
format image (displayed in the top-left corner of the logged-in
webapp).
* `realm_night_logo`: the URI of the organization's logo in the night mode as a
horizontal format image (dispalyed in the top-left corner of the logged-in
webapp).
* `realm_description`: HTML description of the organization, as configured by
the [organization profile](/help/create-your-organization-profile).

View File

@ -31,7 +31,7 @@
<nav class="header-main rightside-userlist" id="top_navbar">
<div class="column-left">
<a class="brand no-style" href="#">
<img id="realm-logo" src="{{ realm_logo }}" alt="" class="nav-logo no-drag">
<img id="realm-logo" src="{% if night_mode %} {{ realm_night_logo }}{% else %} {{ realm_logo }} {% endif %}" alt="" class="nav-logo no-drag"/>
</a>
</div>
<div class="column-middle" id="navbar-middle">

View File

@ -55,6 +55,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
realm_name = None
realm_icon = None
realm_logo = None
realm_night_logo = None
realm_description = None
realm_invite_required = False
realm_plan_type = 0
@ -62,7 +63,8 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
realm_uri = realm.uri
realm_name = realm.name
realm_icon = get_realm_icon_url(realm)
realm_logo = get_realm_logo_url(realm)
realm_logo = get_realm_logo_url(realm, night = False)
realm_night_logo = get_realm_logo_url(realm, night = True)
realm_description_raw = realm.description or "The coolest place in the universe."
realm_description = bugdown_convert(realm_description_raw, message_realm=realm)
realm_invite_required = realm.invite_required
@ -118,6 +120,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
'realm_name': realm_name,
'realm_icon': realm_icon,
'realm_logo': realm_logo,
'realm_night_logo': realm_night_logo,
'realm_description': realm_description,
'realm_plan_type': realm_plan_type,
'root_domain_uri': settings.ROOT_DOMAIN_URI,

View File

@ -3266,21 +3266,38 @@ def do_change_icon_source(realm: Realm, icon_source: str, log: bool=True) -> Non
icon_url=realm_icon_url(realm))),
active_user_ids(realm.id))
def do_change_logo_source(realm: Realm, logo_source: str) -> None:
realm.logo_source = logo_source
realm.logo_version += 1
realm.save(update_fields=["logo_source", "logo_version"])
def do_change_logo_source(realm: Realm, logo_source: str, night: bool) -> None:
if not night:
realm.logo_source = logo_source
realm.logo_version += 1
realm.save(update_fields=["logo_source", "logo_version"])
else:
realm.night_logo_source = logo_source
realm.night_logo_version += 1
realm.save(update_fields=["night_logo_source", "night_logo_version"])
RealmAuditLog.objects.create(event_type=RealmAuditLog.REALM_LOGO_CHANGED,
realm=realm, event_time=timezone_now())
send_event(realm,
dict(type='realm',
op='update_dict',
property="logo",
data=dict(logo_source=realm.logo_source,
logo_url=realm_logo_url(realm))),
active_user_ids(realm.id))
if not night:
send_event(realm,
dict(type='realm',
op='update_dict',
property="logo",
data=dict(logo_source=realm.logo_source,
logo_url=realm_logo_url(realm, night))),
active_user_ids(realm.id))
else:
send_event(realm,
dict(type='realm',
op='update_dict',
property="night_logo",
data=dict(night_logo_source=realm.night_logo_source,
night_logo_url=realm_logo_url(realm, night))),
active_user_ids(realm.id))
def do_change_plan_type(realm: Realm, plan_type: int) -> None:
old_value = realm.plan_type

View File

@ -180,8 +180,10 @@ def fetch_initial_state_data(user_profile: UserProfile,
state['realm_icon_url'] = realm_icon_url(realm)
state['realm_icon_source'] = realm.icon_source
state['max_icon_file_size'] = settings.MAX_ICON_FILE_SIZE
state['realm_logo_url'] = realm_logo_url(realm)
state['realm_logo_url'] = realm_logo_url(realm, night = False)
state['realm_logo_source'] = realm.logo_source
state['realm_night_logo_url'] = realm_logo_url(realm, night = True)
state['realm_night_logo_source'] = realm.night_logo_source
state['max_logo_file_size'] = settings.MAX_LOGO_FILE_SIZE
state['realm_bot_domain'] = realm.get_bot_domain()
state['realm_uri'] = realm.uri

View File

@ -3,11 +3,17 @@ from django.conf import settings
from zerver.lib.upload import upload_backend
from zerver.models import Realm
def realm_logo_url(realm: Realm) -> str:
return get_realm_logo_url(realm)
def realm_logo_url(realm: Realm, night: bool) -> str:
return get_realm_logo_url(realm, night)
def get_realm_logo_url(realm: Realm) -> str:
if realm.logo_source == 'U':
return upload_backend.get_realm_logo_url(realm.id, realm.logo_version)
def get_realm_logo_url(realm: Realm, night: bool) -> str:
if not night:
if realm.logo_source == 'U':
return upload_backend.get_realm_logo_url(realm.id, realm.logo_version, night)
else:
return settings.DEFAULT_LOGO_URI+'?version=0'
else:
return settings.DEFAULT_LOGO_URI+'?version=0'
if realm.night_logo_source == 'U':
return upload_backend.get_realm_logo_url(realm.id, realm.night_logo_version, night)
else:
return settings.DEFAULT_LOGO_URI+'?version=0'

View File

@ -225,10 +225,11 @@ class ZulipUploadBackend:
def get_realm_icon_url(self, realm_id: int, version: int) -> str:
raise NotImplementedError()
def upload_realm_logo_image(self, logo_file: File, user_profile: UserProfile) -> None:
def upload_realm_logo_image(self, logo_file: File, user_profile: UserProfile,
night: bool) -> None:
raise NotImplementedError()
def get_realm_logo_url(self, realm_id: int, version: int) -> str:
def get_realm_logo_url(self, realm_id: int, version: int, night: bool) -> str:
raise NotImplementedError()
def upload_emoji_image(self, emoji_file: File, emoji_file_name: str, user_profile: UserProfile) -> None:
@ -467,10 +468,15 @@ class S3UploadBackend(ZulipUploadBackend):
# ?x=x allows templates to append additional parameters with &s
return "https://%s.s3.amazonaws.com/%s/realm/icon.png?version=%s" % (bucket, realm_id, version)
def upload_realm_logo_image(self, logo_file: File, user_profile: UserProfile) -> None:
def upload_realm_logo_image(self, logo_file: File, user_profile: UserProfile,
night: bool) -> None:
content_type = guess_type(logo_file.name)[0]
bucket_name = settings.S3_AVATAR_BUCKET
s3_file_name = os.path.join(str(user_profile.realm.id), 'realm', 'logo')
if night:
basename = 'night_logo'
else:
basename = 'logo'
s3_file_name = os.path.join(str(user_profile.realm.id), 'realm', basename)
image_data = logo_file.read()
upload_image_to_s3(
@ -492,10 +498,14 @@ class S3UploadBackend(ZulipUploadBackend):
# See avatar_url in avatar.py for URL. (That code also handles the case
# that users use gravatar.)
def get_realm_logo_url(self, realm_id: int, version: int) -> str:
def get_realm_logo_url(self, realm_id: int, version: int, night: bool) -> str:
bucket = settings.S3_AVATAR_BUCKET
# ?x=x allows templates to append additional parameters with &s
return "https://%s.s3.amazonaws.com/%s/realm/logo.png?version=%s" % (bucket, realm_id, version)
if not night:
file_name = 'logo.png'
else:
file_name = 'night_logo.png'
return "https://%s.s3.amazonaws.com/%s/realm/%s?version=%s" % (bucket, realm_id, file_name, version)
def ensure_medium_avatar_image(self, user_profile: UserProfile) -> None:
file_path = user_avatar_path(user_profile)
@ -671,21 +681,31 @@ class LocalUploadBackend(ZulipUploadBackend):
# ?x=x allows templates to append additional parameters with &s
return "/user_avatars/%s/realm/icon.png?version=%s" % (realm_id, version)
def upload_realm_logo_image(self, logo_file: File, user_profile: UserProfile) -> None:
def upload_realm_logo_image(self, logo_file: File, user_profile: UserProfile,
night: bool) -> None:
upload_path = os.path.join('avatars', str(user_profile.realm.id), 'realm')
if night:
original_file = 'night_logo.original'
resized_file = 'night_logo.png'
else:
original_file = 'logo.original'
resized_file = 'logo.png'
image_data = logo_file.read()
write_local_file(
upload_path,
'logo.original',
original_file,
image_data)
resized_data = resize_logo(image_data)
write_local_file(upload_path, 'logo.png', resized_data)
write_local_file(upload_path, resized_file, resized_data)
def get_realm_logo_url(self, realm_id: int, version: int) -> str:
def get_realm_logo_url(self, realm_id: int, version: int, night: bool) -> str:
# ?x=x allows templates to append additional parameters with &s
return "/user_avatars/%s/realm/logo.png?version=%s" % (realm_id, version)
if night:
file_name = 'night_logo.png'
else:
file_name = 'logo.png'
return "/user_avatars/%s/realm/%s?version=%s" % (realm_id, file_name, version)
def ensure_medium_avatar_image(self, user_profile: UserProfile) -> None:
file_path = user_avatar_path(user_profile)
@ -757,8 +777,8 @@ def copy_avatar(source_profile: UserProfile, target_profile: UserProfile) -> Non
def upload_icon_image(user_file: File, user_profile: UserProfile) -> None:
upload_backend.upload_realm_icon_image(user_file, user_profile)
def upload_logo_image(user_file: File, user_profile: UserProfile) -> None:
upload_backend.upload_realm_logo_image(user_file, user_profile)
def upload_logo_image(user_file: File, user_profile: UserProfile, night: bool) -> None:
upload_backend.upload_realm_logo_image(user_file, user_profile, night)
def upload_emoji_image(emoji_file: File, emoji_file_name: str, user_profile: UserProfile) -> None:
upload_backend.upload_emoji_image(emoji_file, emoji_file_name, user_profile)

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-01-15 16:07
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('zerver', '0207_multiuseinvite_invited_as'),
]
operations = [
migrations.AddField(
model_name='realm',
name='night_logo_source',
field=models.CharField(choices=[('D', 'Default to Zulip'), ('U', 'Uploaded by administrator')], default='D', max_length=1),
),
migrations.AddField(
model_name='realm',
name='night_logo_version',
field=models.PositiveSmallIntegerField(default=1),
),
]

View File

@ -316,6 +316,10 @@ class Realm(models.Model):
max_length=1) # type: str
logo_version = models.PositiveSmallIntegerField(default=1) # type: int
night_logo_source = models.CharField(default=LOGO_DEFAULT, choices=LOGO_SOURCES,
max_length=1) # type: str
night_logo_version = models.PositiveSmallIntegerField(default=1) # type: int
BOT_CREATION_POLICY_TYPES = [
BOT_CREATION_EVERYONE,
BOT_CREATION_LIMIT_GENERIC_BOTS,

View File

@ -1759,6 +1759,10 @@ paths:
type: string
description: The URI of the organization's top-left navbar logo
(usually a wide rectangular version of the logo).
realm_night_logo:
type: string
description: The URI of the organization's top-left navbar logo in night_mode
(usually a wide rectangular version of the logo).
realm_description:
type: string
description: HTML description of the organization, as
@ -1771,6 +1775,7 @@ paths:
"msg": "",
"realm_icon": "https://secure.gravatar.com/avatar/62429d594b6ffc712f54aee976a18b44?d=identicon",
"realm_logo": "/static/images/logo/zulip-org-logo.png",
"realm_night_logo": "static/images/logo/zulip-org-logo.png",
"realm_description": "<p>The Zulip development environment default organization. It's great for testing!</p>",
"email_auth_enabled": true,
"zulip_version": "1.9.0-rc1+git",

View File

@ -1719,6 +1719,7 @@ class FetchAuthBackends(ZulipTestCase):
('realm_description', check_string),
('realm_icon', check_string),
('realm_logo', check_string),
('realm_night_logo', check_string),
])
def test_fetch_auth_backend_format(self) -> None:

View File

@ -157,6 +157,8 @@ class HomeTest(ZulipTestCase):
"realm_name",
"realm_name_changes_disabled",
"realm_name_in_notifications",
"realm_night_logo_source",
"realm_night_logo_url",
"realm_non_active_users",
"realm_notifications_stream_id",
"realm_password_auth_enabled",

View File

@ -40,6 +40,7 @@ from zerver.lib.users import get_api_key
from zerver.views.upload import upload_file_backend
import urllib
import ujson
from PIL import Image
from io import StringIO
@ -799,8 +800,10 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
"/user_avatars/hash-medium.png?x=x")
self.assertEqual(backend.get_realm_icon_url(15, 1),
"/user_avatars/15/realm/icon.png?version=1")
self.assertEqual(backend.get_realm_logo_url(15, 1),
self.assertEqual(backend.get_realm_logo_url(15, 1, False),
"/user_avatars/15/realm/logo.png?version=1")
self.assertEqual(backend.get_realm_logo_url(15, 1, True),
"/user_avatars/15/realm/night_logo.png?version=1")
with self.settings(S3_AVATAR_BUCKET="bucket"):
backend = S3UploadBackend()
@ -810,8 +813,10 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
"https://bucket.s3.amazonaws.com/hash-medium.png?x=x")
self.assertEqual(backend.get_realm_icon_url(15, 1),
"https://bucket.s3.amazonaws.com/15/realm/icon.png?version=1")
self.assertEqual(backend.get_realm_logo_url(15, 1),
self.assertEqual(backend.get_realm_logo_url(15, 1, False),
"https://bucket.s3.amazonaws.com/15/realm/logo.png?version=1")
self.assertEqual(backend.get_realm_logo_url(15, 1, True),
"https://bucket.s3.amazonaws.com/15/realm/night_logo.png?version=1")
def test_multiple_upload_failure(self) -> None:
"""
@ -1250,6 +1255,7 @@ class RealmIconTest(UploadSerializeMixin, ZulipTestCase):
destroy_uploads()
class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
night = False
def test_multiple_upload_failure(self) -> None:
"""
@ -1259,7 +1265,8 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
self.login(self.example_email("iago"))
with get_test_image_file('img.png') as fp1, \
get_test_image_file('img.png') as fp2:
result = self.client_post("/json/realm/logo", {'f1': fp1, 'f2': fp2})
result = self.client_post("/json/realm/logo", {'f1': fp1, 'f2': fp2,
'night': ujson.dumps(self.night)})
self.assert_json_error(result, "You must upload exactly one logo.")
def test_no_file_upload_failure(self) -> None:
@ -1268,7 +1275,7 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
"""
self.login(self.example_email("iago"))
result = self.client_post("/json/realm/logo")
result = self.client_post("/json/realm/logo", {'night': ujson.dumps(self.night)})
self.assert_json_error(result, "You must upload exactly one logo.")
correct_files = [
@ -1283,7 +1290,7 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
def test_no_admin_user_upload(self) -> None:
self.login(self.example_email("hamlet"))
with get_test_image_file(self.correct_files[0][0]) as fp:
result = self.client_post("/json/realm/logo", {'file': fp})
result = self.client_post("/json/realm/logo", {'file': fp, 'night': ujson.dumps(self.night)})
self.assert_json_error(result, 'Must be an organization administrator')
def test_upload_limited_plan_type(self) -> None:
@ -1291,45 +1298,53 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
do_change_plan_type(user_profile.realm, Realm.LIMITED)
self.login(user_profile.email)
with get_test_image_file(self.correct_files[0][0]) as fp:
result = self.client_post("/json/realm/logo", {'file': fp})
result = self.client_post("/json/realm/logo", {'file': fp, 'night': ujson.dumps(self.night)})
self.assert_json_error(result, 'Feature unavailable on your current plan.')
def test_get_default_logo(self) -> None:
self.login(self.example_email("hamlet"))
realm = get_realm('zulip')
realm.logo_source = Realm.LOGO_DEFAULT
realm.night_logo_source = Realm.LOGO_DEFAULT
realm.save()
response = self.client_get("/json/realm/logo?foo=bar")
response = self.client_get("/json/realm/logo", {'night': ujson.dumps(self.night)})
redirect_url = response['Location']
self.assertEqual(redirect_url, realm_logo_url(realm) + '&foo=bar')
self.assertEqual(redirect_url, realm_logo_url(realm, self.night) +
'&night=%s' % (str(self.night).lower()))
def test_get_realm_logo(self) -> None:
self.login(self.example_email("hamlet"))
realm = get_realm('zulip')
realm.logo_source = Realm.LOGO_UPLOADED
realm.night_logo_source = Realm.LOGO_UPLOADED
realm.save()
response = self.client_get("/json/realm/logo?foo=bar")
response = self.client_get("/json/realm/logo", {'night': ujson.dumps(self.night)})
redirect_url = response['Location']
self.assertTrue(redirect_url.endswith(realm_logo_url(realm) + '&foo=bar'))
self.assertTrue(redirect_url.endswith(realm_logo_url(realm, self.night) +
'&night=%s' % (str(self.night).lower())))
def test_valid_logos(self) -> None:
"""
A PUT request to /json/realm/logo with a valid file should return a url
and actually create an realm logo.
"""
if self.night:
field_name = 'night_logo_url'
file_name = 'night_logo.png'
else:
field_name = 'logo_url'
file_name = 'logo.png'
for fname, rfname in self.correct_files:
# TODO: use self.subTest once we're exclusively on python 3 by uncommenting the line below.
# with self.subTest(fname=fname):
self.login(self.example_email("iago"))
with get_test_image_file(fname) as fp:
result = self.client_post("/json/realm/logo", {'file': fp})
result = self.client_post("/json/realm/logo", {'file': fp, 'night': ujson.dumps(self.night)})
realm = get_realm('zulip')
self.assert_json_success(result)
self.assertIn("logo_url", result.json())
base = '/user_avatars/%s/realm/logo.png' % (realm.id,)
url = result.json()['logo_url']
self.assertIn(field_name, result.json())
base = '/user_avatars/%s/realm/%s' % (realm.id, file_name)
url = result.json()[field_name]
self.assertEqual(base, url[:len(base)])
if rfname is not None:
@ -1339,7 +1354,7 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
# while trying to fit in a 800 x 100 box without losing part of the image
self.assertEqual(Image.open(io.BytesIO(data)).size, (100, 100))
def test_invalid_logos(self) -> None:
def test_invalid_logo_upload(self) -> None:
"""
A PUT request to /json/realm/logo with an invalid file should fail.
"""
@ -1347,7 +1362,7 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
# with self.subTest(fname=fname):
self.login(self.example_email("iago"))
with get_test_image_file(fname) as fp:
result = self.client_post("/json/realm/logo", {'file': fp})
result = self.client_post("/json/realm/logo", {'file': fp, 'night': ujson.dumps(self.night)})
self.assert_json_error(result, "Could not decode image; did you upload an image file?")
@ -1355,39 +1370,57 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
"""
A DELETE request to /json/realm/logo should delete the realm logo and return gravatar URL
"""
if self.night:
field_name = 'night_logo_url'
else:
field_name = 'logo_url'
self.login(self.example_email("iago"))
realm = get_realm('zulip')
realm.logo_source = Realm.LOGO_UPLOADED
realm.night_logo_source = Realm.LOGO_UPLOADED
realm.save()
result = self.client_delete("/json/realm/logo")
result = self.client_delete("/json/realm/logo", {'night': ujson.dumps(self.night)})
self.assert_json_success(result)
self.assertIn("logo_url", result.json())
self.assertIn(field_name, result.json())
realm = get_realm('zulip')
self.assertEqual(result.json()["logo_url"], realm_logo_url(realm))
self.assertEqual(realm.logo_source, Realm.LOGO_DEFAULT)
self.assertEqual(result.json()[field_name], realm_logo_url(realm, self.night))
if self.night:
self.assertEqual(realm.night_logo_source, Realm.LOGO_DEFAULT)
else:
self.assertEqual(realm.logo_source, Realm.LOGO_DEFAULT)
def test_realm_logo_version(self) -> None:
def test_logo_version(self) -> None:
self.login(self.example_email("iago"))
realm = get_realm('zulip')
logo_version = realm.logo_version
self.assertEqual(logo_version, 1)
if self.night:
version = realm.night_logo_version
else:
version = realm.logo_version
self.assertEqual(version, 1)
with get_test_image_file(self.correct_files[0][0]) as fp:
self.client_post("/json/realm/logo", {'file': fp})
self.client_post("/json/realm/logo", {'file': fp, 'night': ujson.dumps(self.night)})
realm = get_realm('zulip')
self.assertEqual(realm.logo_version, logo_version + 1)
if self.night:
self.assertEqual(realm.night_logo_version, version + 1)
else:
self.assertEqual(realm.logo_version, version + 1)
def test_realm_logo_upload_file_size_error(self) -> None:
def test_logo_upload_file_size_error(self) -> None:
self.login(self.example_email("iago"))
with get_test_image_file(self.correct_files[0][0]) as fp:
with self.settings(MAX_LOGO_FILE_SIZE=0):
result = self.client_post("/json/realm/logo", {'file': fp})
result = self.client_post("/json/realm/logo", {'file': fp, 'night':
ujson.dumps(self.night)})
self.assert_json_error(result, "Uploaded file is larger than the allowed limit of 0 MB")
def tearDown(self) -> None:
destroy_uploads()
class RealmNightLogoTest(RealmLogoTest):
# Run the same tests as for RealmLogoTest, just with night mode enabled
night = True
class LocalStorageTest(UploadSerializeMixin, ZulipTestCase):
def test_file_upload_local(self) -> None:
@ -1662,23 +1695,29 @@ class S3Test(ZulipTestCase):
self.assertEqual(resized_image, (DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE))
@use_s3_backend
def test_upload_realm_logo_image(self) -> None:
def _test_upload_logo_image(self, night: bool, file_name: str) -> None:
bucket = create_s3_buckets(settings.S3_AVATAR_BUCKET)[0]
user_profile = self.example_user("hamlet")
image_file = get_test_image_file("img.png")
zerver.lib.upload.upload_backend.upload_realm_logo_image(image_file, user_profile)
zerver.lib.upload.upload_backend.upload_realm_logo_image(image_file, user_profile, night)
original_path_id = os.path.join(str(user_profile.realm.id), "realm", "logo.original")
original_path_id = os.path.join(str(user_profile.realm.id), "realm", "%s.original" % (file_name))
print(original_path_id)
original_key = bucket.get_key(original_path_id)
print(original_key)
image_file.seek(0)
self.assertEqual(image_file.read(), original_key.get_contents_as_string())
resized_path_id = os.path.join(str(user_profile.realm.id), "realm", "logo.png")
resized_path_id = os.path.join(str(user_profile.realm.id), "realm", "%s.png" % (file_name))
resized_data = bucket.get_key(resized_path_id).read()
resized_image = Image.open(io.BytesIO(resized_data)).size
self.assertEqual(resized_image, (DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE))
def test_upload_realm_logo_image(self) -> None:
self._test_upload_logo_image(night = False, file_name = 'logo')
self._test_upload_logo_image(night = True, file_name = 'night_logo')
@use_s3_backend
def test_upload_emoji_image(self) -> None:
bucket = create_s3_buckets(settings.S3_AVATAR_BUCKET)[0]
@ -1802,5 +1841,8 @@ class DecompressionBombTests(ZulipTestCase):
with get_test_image_file("bomb.png") as fp:
for url, error_string in self.test_urls.items():
fp.seek(0, 0)
result = self.client_post(url, {'f1': fp})
if (url == "/json/realm/logo"):
result = self.client_post(url, {'f1': fp, 'night': ujson.dumps(False)})
else:
result = self.client_post(url, {'f1': fp})
self.assert_json_error(result, error_string)

View File

@ -872,6 +872,7 @@ def api_get_server_settings(request: HttpRequest) -> HttpResponse:
"realm_name",
"realm_icon",
"realm_logo",
"realm_night_logo",
"realm_description"]:
if context[settings_item] is not None:
result[settings_item] = context[settings_item]

View File

@ -285,6 +285,7 @@ def home_real(request: HttpRequest) -> HttpResponse:
'show_plans': show_plans,
'is_admin': user_profile.is_realm_admin,
'is_guest': user_profile.is_guest,
'night_mode': user_profile.night_mode,
'show_webathena': user_profile.realm.webathena_enabled,
'enable_feedback': settings.ENABLE_FEEDBACK,
'embedded': narrow_stream is not None,

View File

@ -2,7 +2,11 @@ from django.conf import settings
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpRequest
from typing import Optional, Callable, Any
from zerver.lib.validator import check_string, check_int, check_list, check_dict, \
check_bool, check_variable_type, check_capped_string, check_color
from zerver.lib.request import REQ, has_request_variables
from zerver.decorator import require_realm_admin
from zerver.lib.actions import do_change_logo_source
from zerver.lib.realm_logo import realm_logo_url
@ -12,42 +16,54 @@ from zerver.models import Realm, UserProfile
@require_realm_admin
def upload_logo(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
@has_request_variables
def upload_logo(request: HttpRequest, user_profile: UserProfile,
night: bool=REQ(validator=check_bool)) -> HttpResponse:
if user_profile.realm.plan_type == Realm.LIMITED:
return json_error(_("Feature unavailable on your current plan."))
if len(request.FILES) != 1:
return json_error(_("You must upload exactly one logo."))
logo_file = list(request.FILES.values())[0]
if ((settings.MAX_LOGO_FILE_SIZE * 1024 * 1024) < logo_file.size):
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
settings.MAX_LOGO_FILE_SIZE))
upload_logo_image(logo_file, user_profile)
do_change_logo_source(user_profile.realm, user_profile.realm.LOGO_UPLOADED)
logo_url = realm_logo_url(user_profile.realm)
json_result = dict(
logo_url=logo_url
)
upload_logo_image(logo_file, user_profile, night)
do_change_logo_source(user_profile.realm, user_profile.realm.LOGO_UPLOADED, night)
logo_url = realm_logo_url(user_profile.realm, night)
if night:
json_result = dict(
night_logo_url=logo_url
)
else:
json_result = dict(
logo_url=logo_url
)
return json_success(json_result)
@require_realm_admin
def delete_logo_backend(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
@has_request_variables
def delete_logo_backend(request: HttpRequest, user_profile: UserProfile,
night: bool=REQ(validator=check_bool)) -> HttpResponse:
# We don't actually delete the logo because it might still
# be needed if the URL was cached and it is rewrited
# in any case after next update.
do_change_logo_source(user_profile.realm, user_profile.realm.LOGO_DEFAULT)
default_url = realm_logo_url(user_profile.realm)
json_result = dict(
logo_url=default_url
)
do_change_logo_source(user_profile.realm, user_profile.realm.LOGO_DEFAULT, night)
default_url = realm_logo_url(user_profile.realm, night)
if night:
json_result = dict(
night_logo_url=default_url
)
else:
json_result = dict(
logo_url=default_url
)
return json_success(json_result)
def get_logo_backend(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
url = realm_logo_url(user_profile.realm)
@has_request_variables
def get_logo_backend(request: HttpRequest, user_profile: UserProfile,
night: bool=REQ(validator=check_bool)) -> HttpResponse:
url = realm_logo_url(user_profile.realm, night)
# We can rely on the url already having query parameters. Because
# our templates depend on being able to use the ampersand to

View File

@ -98,7 +98,7 @@ v1_api_and_json_patterns = [
'DELETE': 'zerver.views.realm_icon.delete_icon_backend',
'GET': 'zerver.views.realm_icon.get_icon_backend'}),
# realm/logo -> zerver.views.realm_logo_
# realm/logo -> zerver.views.realm_logo
url(r'^realm/logo$', rest_dispatch,
{'POST': 'zerver.views.realm_logo.upload_logo',
'DELETE': 'zerver.views.realm_logo.delete_logo_backend',