diff --git a/.eslintrc.json b/.eslintrc.json
index d7a9640e07..d844701080 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -58,6 +58,7 @@
"viewport": false,
"upload_widget": false,
"avatar": false,
+ "realm_icon": false,
"feature_flags": false,
"search_suggestion": false,
"referral": false,
diff --git a/static/js/admin.js b/static/js/admin.js
index 85154985bd..6613aa56d9 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -323,6 +323,8 @@ function _setup_page() {
realm_default_language: page_params.realm_default_language,
realm_waiting_period_threshold: page_params.realm_waiting_period_threshold,
is_admin: page_params.is_admin,
+ realm_icon_source: page_params.realm_icon_source,
+ realm_icon: page_params.realm_icon,
};
var admin_tab = templates.render('admin_tab', options);
@@ -1050,6 +1052,31 @@ function _setup_page() {
});
});
+ function upload_realm_icon(file_input) {
+ var form_data = new FormData();
+
+ form_data.append('csrfmiddlewaretoken', csrf_token);
+ jQuery.each(file_input[0].files, function (i, file) {
+ form_data.append('file-'+i, file);
+ });
+
+ var spinner = $("#upload_icon_spinner").expectOne();
+ loading.make_indicator(spinner, {text: i18n.t("Uploading icon.")});
+
+ channel.put({
+ url: '/json/realm/icon',
+ data: form_data,
+ cache: false,
+ processData: false,
+ contentType: false,
+ success: function () {
+ loading.destroy_indicator($("#upload_icon_spinner"));
+ },
+ });
+
+ }
+ realm_icon.build_realm_icon_widget(upload_realm_icon);
+
}
exports.launch_page = function (tab) {
diff --git a/static/js/realm_icon.js b/static/js/realm_icon.js
new file mode 100644
index 0000000000..4d581ac89f
--- /dev/null
+++ b/static/js/realm_icon.js
@@ -0,0 +1,34 @@
+var realm_icon = (function () {
+
+ var exports = {};
+
+ exports.build_realm_icon_widget = function (upload_function) {
+ var get_file_input = function () {
+ return $('#realm_icon_file_input').expectOne();
+ };
+
+ if (page_params.realm_icon_source === 'G') {
+ $("#realm_icon_delete_button").hide();
+ }
+ $("#realm_icon_delete_button").on('click', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ channel.del({
+ url: '/json/realm/icon',
+ });
+ });
+
+ return upload_widget.build_direct_upload_widget(
+ get_file_input,
+ $("#realm_icon_file_input_error").expectOne(),
+ $("#realm_icon_upload_button").expectOne(),
+ upload_function
+ );
+ };
+ return exports;
+
+}());
+
+if (typeof module !== 'undefined') {
+ module.exports = realm_icon;
+}
diff --git a/static/js/server_events.js b/static/js/server_events.js
index 848866ceaa..5366bf1a40 100644
--- a/static/js/server_events.js
+++ b/static/js/server_events.js
@@ -245,6 +245,19 @@ function dispatch_normal_event(event) {
break;
}
break;
+ case 'realm_change_icon':
+ $("#realm-settings-icon").attr("src", event.url);
+ if (event.source === 'U') {
+ $("#realm_icon_delete_button").show();
+ } else {
+ $("#realm_icon_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_icon_file_input");
+ file_input.val('');
+ }
+ page_params.realm_icon = event.url;
+ page_params.icon_source = event.source;
}
}
diff --git a/static/styles/settings.css b/static/styles/settings.css
index 78fad5238b..058841900e 100644
--- a/static/styles/settings.css
+++ b/static/styles/settings.css
@@ -31,6 +31,10 @@ label {
margin-top: 20px;
}
+.new-style .m-t-10 {
+ margin-top: 10px;
+}
+
.new-style .grid label {
width: 200px;
}
@@ -58,14 +62,14 @@ label {
width: 214px;
}
-.user-avatar-section {
+.user-avatar-section, .realm-icon-section {
float: right;
background-color: #fff;
margin: 20px 0px;
padding: 20px;
}
-.user-avatar-section .inline-block {
+.user-avatar-section .inline-block, .realm-icon-section .inline-block {
display: block;
}
@@ -213,7 +217,7 @@ input[type=checkbox] + .inline-block {
margin-left: 10px;
}
-#user-settings-avatar {
+#user-settings-avatar, #realm-icon-section {
border-radius: 5px;
box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
}
@@ -530,7 +534,7 @@ input[type=checkbox].inline-block {
margin-right: 20px;
}
-#upload_avatar_spinner {
+#upload_avatar_spinner, #upload_icon_spinner {
font-size: 14px;
margin: auto;
}
@@ -590,6 +594,13 @@ input[type=checkbox].inline-block {
height: 200px;
}
+#realm-settings-icon {
+ border-radius: 5px;
+ box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
+ width: 100px;
+ height: 100px;
+ margin-left: 25%;
+}
/* -- new settings overlay -- */
#settings_overlay_container {
pointer-events: none;
@@ -832,20 +843,27 @@ input[type=text]#settings_search {
/* -- end new settings overlay -- */
@media (max-width: 1215px) {
- .user-avatar-section {
+ .user-avatar-section, .realm-icon-section {
float: none;
display: inline-block;
}
- .user-avatar-section .inline-block {
+ .user-avatar-section .inline-block, .realm-icon-section .inline-block {
display: inline-block;
vertical-align: top;
+ }
+
+ .user-avatar-section .inline-block {
margin: 0px 10px;
}
+
+ .realm-icon-section .inline-block {
+ margin: 0px 25px;
+ }
}
@media (max-width: 856px) {
- .user-avatar-section .inline-block {
+ .user-avatar-section .inline-block, .realm-icon-section .inline-block {
display: block;
margin: 0;
}
diff --git a/static/templates/settings/organization-settings-admin.handlebars b/static/templates/settings/organization-settings-admin.handlebars
index 21b25f5aac..196b8b484d 100644
--- a/static/templates/settings/organization-settings-admin.handlebars
+++ b/static/templates/settings/organization-settings-admin.handlebars
@@ -12,6 +12,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/test-backend b/tools/test-backend
index af6542c477..8377650c1c 100755
--- a/tools/test-backend
+++ b/tools/test-backend
@@ -30,6 +30,7 @@ target_fully_covered = {path for target in [
'zerver/lib/mention.py',
'zerver/lib/message.py',
'zerver/lib/name_restrictions.py',
+ 'zerver/lib/realm_icon.py',
'zerver/lib/retention.py',
'zerver/lib/streams.py',
'zerver/lib/users.py',
diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py
index ea497ab609..270c66350b 100644
--- a/zerver/lib/actions.py
+++ b/zerver/lib/actions.py
@@ -25,6 +25,7 @@ from zerver.lib.message import (
message_to_dict,
render_markdown,
)
+from zerver.lib.realm_icon import realm_icon_url
from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \
Subscription, Recipient, Message, Attachment, UserMessage, \
Client, DefaultStream, UserPresence, Referral, PushDeviceToken, MAX_SUBJECT_LENGTH, \
@@ -1952,6 +1953,23 @@ def do_change_avatar_fields(user_profile, avatar_source, log=True):
person=payload),
active_user_ids(user_profile.realm))
+
+def do_change_icon_source(realm, icon_source, log=True):
+ # type: (Realm, Text, bool) -> None
+ realm.icon_source = icon_source
+ realm.icon_version += 1
+ realm.save(update_fields=["icon_source", "icon_version"])
+
+ if log:
+ log_event({'type': 'realm_change_icon',
+ 'realm': realm.domain,
+ 'icon_source': icon_source})
+
+ send_event(dict(type='realm_change_icon',
+ source=realm.icon_source,
+ url=realm_icon_url(realm)),
+ active_user_ids(realm))
+
def _default_stream_permision_check(user_profile, stream):
# type: (UserProfile, Optional[Stream]) -> None
# Any user can have a None default stream
diff --git a/zerver/lib/events.py b/zerver/lib/events.py
index a3b3cd7079..dc78e5c6c6 100644
--- a/zerver/lib/events.py
+++ b/zerver/lib/events.py
@@ -21,6 +21,7 @@ from zerver.lib.alert_words import user_alert_words
from zerver.lib.attachments import user_attachments
from zerver.lib.avatar import get_avatar_url
from zerver.lib.narrow import check_supported_events_narrow_filter
+from zerver.lib.realm_icon import realm_icon_url
from zerver.lib.request import JsonableError
from zerver.lib.actions import validate_user_access_to_subscribers_helper, \
do_get_streams, get_default_streams_for_realm, \
@@ -156,6 +157,10 @@ def fetch_initial_state_data(user_profile, event_types, queue_id,
state['enable_online_push_notifications'] = user_profile.enable_online_push_notifications
state['enable_digest_emails'] = user_profile.enable_digest_emails
+ if want('realm_change_icon'):
+ state['realm_icon'] = realm_icon_url(user_profile.realm)
+ state['realm_icon_source'] = user_profile.realm.icon_source
+
return state
def apply_events(state, events, user_profile, include_subscribers=True):
@@ -377,6 +382,9 @@ def apply_event(state, event, user_profile, include_subscribers):
state['enable_online_push_notifications'] = event['setting']
elif event['notification_name'] == "enable_digest_emails":
state['enable_digest_emails'] = event['setting']
+ elif event['type'] == "realm_change_icon":
+ state['realm_icon'] = event['url']
+ state['realm_icon_source'] = event['source']
else:
raise ValueError("Unexpected event type %s" % (event['type'],))
diff --git a/zerver/lib/realm_icon.py b/zerver/lib/realm_icon.py
new file mode 100644
index 0000000000..7fa1e7fd05
--- /dev/null
+++ b/zerver/lib/realm_icon.py
@@ -0,0 +1,22 @@
+from __future__ import absolute_import
+from django.conf import settings
+
+from typing import Text
+
+from zerver.lib.avatar_hash import gravatar_hash, user_avatar_hash
+from zerver.lib.upload import upload_backend
+from zerver.models import Realm
+
+def realm_icon_url(realm):
+ # type: (Realm) -> Text
+ return get_realm_icon_url(realm)
+
+def get_realm_icon_url(realm):
+ # type: (Realm) -> Text
+ if realm.icon_source == u'U':
+ return upload_backend.get_realm_icon_url(realm.id, realm.icon_version)
+ elif settings.ENABLE_GRAVATAR:
+ hash_key = gravatar_hash(realm.domain)
+ return u"https://secure.gravatar.com/avatar/%s?d=identicon" % (hash_key,)
+ else:
+ return settings.DEFAULT_AVATAR_URI+'?version=0'
diff --git a/zerver/lib/upload.py b/zerver/lib/upload.py
index 3eeddcc0b6..45220df20e 100644
--- a/zerver/lib/upload.py
+++ b/zerver/lib/upload.py
@@ -118,6 +118,15 @@ class ZulipUploadBackend(object):
# type: (Text) -> None
raise NotImplementedError()
+ def upload_realm_icon_image(self, icon_file, user_profile):
+ # type: (File, UserProfile) -> None
+ raise NotImplementedError()
+
+ def get_realm_icon_url(self, realm_id, version):
+ # type: (int, int) -> Text
+ raise NotImplementedError()
+
+
### S3
def get_bucket(conn, bucket_name):
@@ -269,6 +278,38 @@ class S3UploadBackend(ZulipUploadBackend):
# ?x=x allows templates to append additional parameters with &s
return u"https://%s.s3.amazonaws.com/%s%s?x=x" % (bucket, medium_suffix, hash_key)
+ def upload_realm_icon_image(self, icon_file, user_profile):
+ # type: (File, UserProfile) -> None
+ content_type = guess_type(icon_file.name)[0]
+ bucket_name = settings.S3_AVATAR_BUCKET
+ s3_file_name = os.path.join(str(user_profile.realm.id), 'realm', 'icon')
+
+ image_data = icon_file.read()
+ upload_image_to_s3(
+ bucket_name,
+ s3_file_name + ".original",
+ content_type,
+ user_profile,
+ image_data,
+ )
+
+ resized_data = resize_avatar(image_data)
+ upload_image_to_s3(
+ bucket_name,
+ s3_file_name,
+ 'image/png',
+ user_profile,
+ resized_data,
+ )
+ # See avatar_url in avatar.py for URL. (That code also handles the case
+ # that users use gravatar.)
+
+ def get_realm_icon_url(self, realm_id, version):
+ # type: (int, int) -> Text
+ bucket = settings.S3_AVATAR_BUCKET
+ # ?x=x allows templates to append additional parameters with &s
+ return u"https://%s.s3.amazonaws.com/%s/realm/icon.png?version=%s" % (bucket, realm_id, version)
+
def ensure_medium_avatar_image(self, email):
# type: (Text) -> None
user_profile = get_user_profile_by_email(email)
@@ -359,6 +400,24 @@ class LocalUploadBackend(ZulipUploadBackend):
medium_suffix = "-medium" if medium else ""
return u"/user_avatars/%s%s.png?x=x" % (hash_key, medium_suffix)
+ def upload_realm_icon_image(self, icon_file, user_profile):
+ # type: (File, UserProfile) -> None
+ upload_path = os.path.join('avatars', str(user_profile.realm.id), 'realm')
+
+ image_data = icon_file.read()
+ write_local_file(
+ upload_path,
+ 'icon.original',
+ image_data)
+
+ resized_data = resize_avatar(image_data)
+ write_local_file(upload_path, 'icon.png', resized_data)
+
+ def get_realm_icon_url(self, realm_id, version):
+ # type: (int, int) -> Text
+ # ?x=x allows templates to append additional parameters with &s
+ return u"/user_avatars/%s/realm/icon.png?version=%s" % (realm_id, version)
+
def ensure_medium_avatar_image(self, email):
# type: (Text) -> None
email_hash = user_avatar_hash(email)
@@ -386,6 +445,10 @@ def upload_avatar_image(user_file, user_profile, email):
# type: (File, UserProfile, Text) -> None
upload_backend.upload_avatar_image(user_file, user_profile, email)
+def upload_icon_image(user_file, user_profile):
+ # type: (File, UserProfile) -> None
+ upload_backend.upload_realm_icon_image(user_file, user_profile)
+
def upload_message_image(uploaded_file_name, content_type, file_data, user_profile, target_realm=None):
# type: (Text, Optional[Text], binary_type, UserProfile, Optional[Realm]) -> Text
return upload_backend.upload_message_image(uploaded_file_name, content_type, file_data,
diff --git a/zerver/migrations/0054_realm_icon.py b/zerver/migrations/0054_realm_icon.py
new file mode 100644
index 0000000000..ce3d215188
--- /dev/null
+++ b/zerver/migrations/0054_realm_icon.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-15 06:18
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('zerver', '0053_emailchangestatus'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='realm',
+ name='icon_source',
+ field=models.CharField(
+ choices=[('G', 'Hosted by Gravatar'), ('U', 'Uploaded by administrator')],
+ default='G', max_length=1),
+ ),
+ migrations.AddField(
+ model_name='realm',
+ name='icon_version',
+ field=models.PositiveSmallIntegerField(default=1),
+ ),
+ ]
diff --git a/zerver/models.py b/zerver/models.py
index 573e70b43e..6c9e5ecf7d 100644
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -140,6 +140,16 @@ class Realm(ModelReprMixin, models.Model):
default=2**31 - 1) # type: BitHandler
waiting_period_threshold = models.PositiveIntegerField(default=0) # type: int
+ ICON_FROM_GRAVATAR = u'G'
+ ICON_UPLOADED = u'U'
+ ICON_SOURCES = (
+ (ICON_FROM_GRAVATAR, 'Hosted by Gravatar'),
+ (ICON_UPLOADED, 'Uploaded by administrator'),
+ )
+ icon_source = models.CharField(default=ICON_FROM_GRAVATAR, choices=ICON_SOURCES,
+ max_length=1) # type: Text
+ icon_version = models.PositiveSmallIntegerField(default=1) # type: int
+
DEFAULT_NOTIFICATION_STREAM_NAME = u'announce'
def authentication_methods_dict(self):
diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py
index 63c80a391b..abcfd0717b 100644
--- a/zerver/tests/test_events.py
+++ b/zerver/tests/test_events.py
@@ -62,7 +62,7 @@ from zerver.lib.actions import (
do_add_realm_alias,
do_change_realm_alias,
do_remove_realm_alias,
-)
+ do_change_icon_source)
from zerver.lib.events import (
apply_events,
fetch_initial_state_data,
@@ -959,6 +959,19 @@ class EventsRegisterTest(ZulipTestCase):
error = self.realm_bot_schema('avatar_url', check_string)('events[0]', events[0])
self.assert_on_error(error)
+ def test_change_realm_icon_source(self):
+ # type: () -> None
+ realm = get_realm('zulip')
+ action = lambda: do_change_icon_source(realm, realm.ICON_FROM_GRAVATAR)
+ events = self.do_test(action, state_change_expected=False)
+ schema_checker = check_dict([
+ ('type', equals('realm_change_icon')),
+ ('source', check_string),
+ ('url', check_string)
+ ])
+ error = schema_checker('events[0]', events[0])
+ self.assert_on_error(error)
+
def test_change_bot_default_all_public_streams(self):
# type: () -> None
bot = self.create_bot('test-bot@zulip.com')
diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py
index 638624c890..eab723f4e0 100644
--- a/zerver/tests/test_upload.py
+++ b/zerver/tests/test_upload.py
@@ -6,6 +6,7 @@ from unittest import skip
from zerver.lib.avatar import avatar_url
from zerver.lib.bugdown import url_filename
+from zerver.lib.realm_icon import realm_icon_url
from zerver.lib.test_classes import ZulipTestCase, UploadSerializeMixin
from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file
from zerver.lib.test_runner import slow
@@ -13,7 +14,7 @@ from zerver.lib.upload import sanitize_name, S3UploadBackend, \
upload_message_image, delete_message_image, LocalUploadBackend
import zerver.lib.upload
from zerver.models import Attachment, Recipient, get_user_profile_by_email, \
- get_old_unclaimed_attachments, Message, UserProfile
+ get_old_unclaimed_attachments, Message, UserProfile, Realm, get_realm
from zerver.lib.actions import do_delete_old_unclaimed_attachments
import ujson
@@ -473,6 +474,145 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
# type: () -> None
destroy_uploads()
+class RealmIconTest(UploadSerializeMixin, ZulipTestCase):
+
+ def test_multiple_upload_failure(self):
+ # type: () -> None
+ """
+ Attempting to upload two files should fail.
+ """
+ # Log in as admin
+ 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/icon", {'f1': fp1, 'f2': fp2})
+ self.assert_json_error(result, "You must upload exactly one icon.")
+
+ def test_no_file_upload_failure(self):
+ # type: () -> None
+ """
+ Calling this endpoint with no files should fail.
+ """
+ self.login("iago@zulip.com")
+
+ result = self.client_put_multipart("/json/realm/icon")
+ self.assert_json_error(result, "You must upload exactly one icon.")
+
+ correct_files = [
+ ('img.png', 'png_resized.png'),
+ ('img.jpg', None), # jpeg resizing is platform-dependent
+ ('img.gif', 'gif_resized.png'),
+ ('img.tif', 'tif_resized.png')
+ ]
+ corrupt_files = ['text.txt', 'corrupt.png', 'corrupt.gif']
+
+ def test_no_admin_user_upload(self):
+ # type: () -> None
+ self.login("hamlet@zulip.com")
+ with get_test_image_file(self.correct_files[0][0]) as fp:
+ result = self.client_put_multipart("/json/realm/icon", {'file': fp})
+ self.assert_json_error(result, 'Must be a realm administrator')
+
+ def test_get_gravatar_icon(self):
+ # type: () -> None
+ self.login("hamlet@zulip.com")
+ realm = get_realm('zulip')
+ realm.icon_source = Realm.ICON_FROM_GRAVATAR
+ realm.save()
+ with self.settings(ENABLE_GRAVATAR=True):
+ response = self.client_get("/json/realm/icon?foo=bar")
+ redirect_url = response['Location']
+ self.assertEqual(redirect_url, realm_icon_url(realm) + '&foo=bar')
+
+ with self.settings(ENABLE_GRAVATAR=False):
+ response = self.client_get("/json/realm/icon?foo=bar")
+ redirect_url = response['Location']
+ self.assertTrue(redirect_url.endswith(realm_icon_url(realm) + '&foo=bar'))
+
+ def test_get_realm_icon(self):
+ # type: () -> None
+ self.login("hamlet@zulip.com")
+
+ realm = get_realm('zulip')
+ realm.icon_source = Realm.ICON_UPLOADED
+ realm.save()
+ response = self.client_get("/json/realm/icon?foo=bar")
+ redirect_url = response['Location']
+ self.assertTrue(redirect_url.endswith(realm_icon_url(realm) + '&foo=bar'))
+
+ def test_valid_icons(self):
+ # type: () -> None
+ """
+ A PUT request to /json/realm/icon with a valid file should return a url
+ and actually create an realm icon.
+ """
+ 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("iago@zulip.com")
+ with get_test_image_file(fname) as fp:
+ result = self.client_put_multipart("/json/realm/icon", {'file': fp})
+ realm = get_realm('zulip')
+ self.assert_json_success(result)
+ json = ujson.loads(result.content)
+ self.assertIn("icon_url", json)
+ url = json["icon_url"]
+ base = '/user_avatars/%s/realm/icon.png' % (realm.id,)
+ self.assertEqual(base, url[:len(base)])
+
+ if rfname is not None:
+ response = self.client_get(url)
+ data = b"".join(response.streaming_content)
+ self.assertEqual(Image.open(io.BytesIO(data)).size, (100, 100))
+
+ def test_invalid_icons(self):
+ # type: () -> None
+ """
+ A PUT request to /json/realm/icon with an invalid file should fail.
+ """
+ for fname in self.corrupt_files:
+ # with self.subTest(fname=fname):
+ self.login("iago@zulip.com")
+ with get_test_image_file(fname) as fp:
+ result = self.client_put_multipart("/json/realm/icon", {'file': fp})
+
+ self.assert_json_error(result, "Could not decode image; did you upload an image file?")
+
+ def test_delete_icon(self):
+ # type: () -> None
+ """
+ A DELETE request to /json/realm/icon should delete the realm icon and return gravatar URL
+ """
+ self.login("iago@zulip.com")
+ realm = get_realm('zulip')
+ realm.icon_source = Realm.ICON_UPLOADED
+ realm.save()
+
+ result = self.client_delete("/json/realm/icon")
+
+ self.assert_json_success(result)
+ json = ujson.loads(result.content)
+ self.assertIn("icon_url", json)
+ realm = get_realm('zulip')
+ self.assertEqual(json["icon_url"], realm_icon_url(realm))
+ self.assertEqual(realm.icon_source, Realm.ICON_FROM_GRAVATAR)
+
+ def test_realm_icon_version(self):
+ # type: () -> None
+
+ self.login("iago@zulip.com")
+ realm = get_realm('zulip')
+ icon_version = realm.icon_version
+ self.assertEqual(icon_version, 1)
+ with get_test_image_file(self.correct_files[0][0]) as fp:
+ self.client_put_multipart("/json/realm/icon", {'file': fp})
+ realm = get_realm('zulip')
+ self.assertEqual(realm.icon_version, icon_version + 1)
+
+ def tearDown(self):
+ # type: () -> None
+ destroy_uploads()
+
class LocalStorageTest(UploadSerializeMixin, ZulipTestCase):
def test_file_upload_local(self):
diff --git a/zerver/tests/tests.py b/zerver/tests/tests.py
index 65d0844d31..4d3d9315c3 100644
--- a/zerver/tests/tests.py
+++ b/zerver/tests/tests.py
@@ -1947,6 +1947,8 @@ class HomeTest(ZulipTestCase):
"realm_default_streams",
"realm_emoji",
"realm_filters",
+ "realm_icon",
+ "realm_icon_source",
"realm_invite_by_admins_only",
"realm_invite_required",
"realm_message_content_edit_limit_seconds",
@@ -1991,6 +1993,7 @@ class HomeTest(ZulipTestCase):
page_params = self._get_page_params(result)
actual_keys = sorted([str(k) for k in page_params.keys()])
+
self.assertEqual(actual_keys, expected_keys)
# TODO: Inspect the page_params data further.
diff --git a/zerver/views/home.py b/zerver/views/home.py
index cfb57d2f36..c946abd1ac 100644
--- a/zerver/views/home.py
+++ b/zerver/views/home.py
@@ -12,6 +12,7 @@ from six.moves import zip_longest, zip, range
from version import ZULIP_VERSION
from zerver.decorator import zulip_login_required, process_client
from zerver.forms import ToSForm
+from zerver.lib.realm_icon import realm_icon_url
from zerver.models import Message, UserProfile, Stream, Subscription, Huddle, \
Recipient, Realm, UserMessage, DefaultStream, RealmEmoji, RealmAlias, \
RealmFilter, PreregistrationUser, UserActivity, \
@@ -225,6 +226,8 @@ def home_real(request):
realm_restricted_to_domain = register_ret['realm_restricted_to_domain'],
realm_default_language = register_ret['realm_default_language'],
realm_waiting_period_threshold = register_ret['realm_waiting_period_threshold'],
+ realm_icon = realm_icon_url(user_profile.realm),
+ realm_icon_source = user_profile.realm.icon_source,
enter_sends = user_profile.enter_sends,
user_id = user_profile.id,
left_side_userlist = register_ret['left_side_userlist'],
diff --git a/zerver/views/realm_icon.py b/zerver/views/realm_icon.py
new file mode 100644
index 0000000000..4abb75e61d
--- /dev/null
+++ b/zerver/views/realm_icon.py
@@ -0,0 +1,55 @@
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from django.http import HttpResponse, HttpRequest
+
+from zerver.decorator import require_realm_admin
+from zerver.lib.actions import do_change_icon_source
+from zerver.lib.realm_icon import realm_icon_url
+from zerver.lib.response import json_error, json_success
+from zerver.lib.upload import upload_icon_image
+from zerver.models import UserProfile
+
+
+@require_realm_admin
+def upload_icon(request, user_profile):
+ # type: (HttpRequest, UserProfile) -> HttpResponse
+
+ if len(request.FILES) != 1:
+ return json_error(_("You must upload exactly one icon."))
+
+ icon_file = list(request.FILES.values())[0]
+ upload_icon_image(icon_file, user_profile)
+ do_change_icon_source(user_profile.realm, user_profile.realm.ICON_UPLOADED)
+ icon_url = realm_icon_url(user_profile.realm)
+
+ json_result = dict(
+ icon_url=icon_url
+ )
+ return json_success(json_result)
+
+
+@require_realm_admin
+def delete_icon_backend(request, user_profile):
+ # type: (HttpRequest, UserProfile) -> HttpResponse
+ # We don't actually delete the icon because it might still
+ # be needed if the URL was cached and it is rewrited
+ # in any case after next update.
+ do_change_icon_source(user_profile.realm, user_profile.realm.ICON_FROM_GRAVATAR)
+ gravatar_url = realm_icon_url(user_profile.realm)
+ json_result = dict(
+ icon_url=gravatar_url
+ )
+ return json_success(json_result)
+
+
+def get_icon_backend(request, user_profile):
+ # type: (HttpRequest, UserProfile) -> HttpResponse
+ url = realm_icon_url(user_profile.realm)
+
+ # We can rely on the url already having query parameters. Because
+ # our templates depend on being able to use the ampersand to
+ # add query parameters to our url, get_icon_url does '?version=version_number'
+ # hacks to prevent us from having to jump through decode/encode hoops.
+ assert '?' in url
+ url += '&' + request.META['QUERY_STRING']
+ return redirect(url)
diff --git a/zproject/settings.py b/zproject/settings.py
index f870bab368..ff70200552 100644
--- a/zproject/settings.py
+++ b/zproject/settings.py
@@ -859,6 +859,7 @@ JS_SPECS = {
'js/templates.js',
'js/upload_widget.js',
'js/avatar.js',
+ 'js/realm_icon.js',
'js/settings.js',
'js/admin.js',
'js/tab_bar.js',
diff --git a/zproject/urls.py b/zproject/urls.py
index 4d4a22ba61..780739731f 100644
--- a/zproject/urls.py
+++ b/zproject/urls.py
@@ -185,6 +185,12 @@ v1_api_and_json_patterns = [
{'PUT': 'zerver.views.realm_emoji.upload_emoji',
'DELETE': 'zerver.views.realm_emoji.delete_emoji'}),
+ # realm/icon -> zerver.views.realm_icon
+ url(r'^realm/icon$', rest_dispatch,
+ {'PUT': 'zerver.views.realm_icon.upload_icon',
+ 'DELETE': 'zerver.views.realm_icon.delete_icon_backend',
+ 'GET': 'zerver.views.realm_icon.get_icon_backend'}),
+
# realm/filters -> zerver.views.realm_filters
url(r'^realm/filters$', rest_dispatch,
{'GET': 'zerver.views.realm_filters.list_filters',