mirror of https://github.com/zulip/zulip.git
realm-icon: Add realm icon feature.
- Add realm icon fields to realm model. - Add migration for new realm model's field. - Add views for icon uploading and deleting. - Add routes for realm icons views. - Add JS widget for realm icon upload setting. - Add realm icon upload to administration organization setting. - Add tests for realm icons. Fixes #3660.
This commit is contained in:
parent
20b655016d
commit
257bb40698
|
@ -58,6 +58,7 @@
|
||||||
"viewport": false,
|
"viewport": false,
|
||||||
"upload_widget": false,
|
"upload_widget": false,
|
||||||
"avatar": false,
|
"avatar": false,
|
||||||
|
"realm_icon": false,
|
||||||
"feature_flags": false,
|
"feature_flags": false,
|
||||||
"search_suggestion": false,
|
"search_suggestion": false,
|
||||||
"referral": false,
|
"referral": false,
|
||||||
|
|
|
@ -323,6 +323,8 @@ function _setup_page() {
|
||||||
realm_default_language: page_params.realm_default_language,
|
realm_default_language: page_params.realm_default_language,
|
||||||
realm_waiting_period_threshold: page_params.realm_waiting_period_threshold,
|
realm_waiting_period_threshold: page_params.realm_waiting_period_threshold,
|
||||||
is_admin: page_params.is_admin,
|
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);
|
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) {
|
exports.launch_page = function (tab) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -245,6 +245,19 @@ function dispatch_normal_event(event) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,10 @@ label {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-style .m-t-10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.new-style .grid label {
|
.new-style .grid label {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
@ -58,14 +62,14 @@ label {
|
||||||
width: 214px;
|
width: 214px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar-section {
|
.user-avatar-section, .realm-icon-section {
|
||||||
float: right;
|
float: right;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin: 20px 0px;
|
margin: 20px 0px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar-section .inline-block {
|
.user-avatar-section .inline-block, .realm-icon-section .inline-block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +217,7 @@ input[type=checkbox] + .inline-block {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user-settings-avatar {
|
#user-settings-avatar, #realm-icon-section {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
|
box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
@ -530,7 +534,7 @@ input[type=checkbox].inline-block {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#upload_avatar_spinner {
|
#upload_avatar_spinner, #upload_icon_spinner {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
@ -590,6 +594,13 @@ input[type=checkbox].inline-block {
|
||||||
height: 200px;
|
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 -- */
|
/* -- new settings overlay -- */
|
||||||
#settings_overlay_container {
|
#settings_overlay_container {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -832,20 +843,27 @@ input[type=text]#settings_search {
|
||||||
/* -- end new settings overlay -- */
|
/* -- end new settings overlay -- */
|
||||||
|
|
||||||
@media (max-width: 1215px) {
|
@media (max-width: 1215px) {
|
||||||
.user-avatar-section {
|
.user-avatar-section, .realm-icon-section {
|
||||||
float: none;
|
float: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-avatar-section .inline-block {
|
.user-avatar-section .inline-block, .realm-icon-section .inline-block {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-section .inline-block {
|
||||||
margin: 0px 10px;
|
margin: 0px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.realm-icon-section .inline-block {
|
||||||
|
margin: 0px 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 856px) {
|
@media (max-width: 856px) {
|
||||||
.user-avatar-section .inline-block {
|
.user-avatar-section .inline-block, .realm-icon-section .inline-block {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<div class="alert" id="admin-realm-default-language-status"></div>
|
<div class="alert" id="admin-realm-default-language-status"></div>
|
||||||
<div class="alert" id="admin-realm-waiting_period_threshold_status"></div>
|
<div class="alert" id="admin-realm-waiting_period_threshold_status"></div>
|
||||||
|
|
||||||
|
<div class="m-10 inline-block grid organization-settings-parent">
|
||||||
<div class="input-group admin-realm">
|
<div class="input-group admin-realm">
|
||||||
<label for="realm_name">{{t "Your organization's name" }}</label>
|
<label for="realm_name">{{t "Your organization's name" }}</label>
|
||||||
<input type="text" id="id_realm_name" name="realm_name" class="admin-realm-name"
|
<input type="text" id="id_realm_name" name="realm_name" class="admin-realm-name"
|
||||||
|
@ -108,5 +109,21 @@
|
||||||
<div class="input-group organization-submission">
|
<div class="input-group organization-submission">
|
||||||
<input type="submit" class="button" value="{{t 'Save changes' }}" />
|
<input type="submit" class="button" value="{{t 'Save changes' }}" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="realm-icon-section box-shadow border-radius">
|
||||||
|
<div class="inline-block">
|
||||||
|
<img id="realm-settings-icon" src="{{ realm_icon }}"/>
|
||||||
|
<div id="realm_icon_file_input_error" class="text-error"></div>
|
||||||
|
<input type="file" name="realm_icon_file_input" class="notvisible"
|
||||||
|
id="realm_icon_file_input" value="{{t 'Upload icon' }}"/>
|
||||||
|
<div id="upload_icon_spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-block">
|
||||||
|
<button class="button sea-green w-200 m-t-10 block input-size"
|
||||||
|
id="realm_icon_upload_button">{{t 'Upload new ccon' }}</button>
|
||||||
|
<button class="button btn-danger w-200 m-t-10 block input-size"
|
||||||
|
id="realm_icon_delete_button">{{t 'Delete icon' }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,6 +30,7 @@ target_fully_covered = {path for target in [
|
||||||
'zerver/lib/mention.py',
|
'zerver/lib/mention.py',
|
||||||
'zerver/lib/message.py',
|
'zerver/lib/message.py',
|
||||||
'zerver/lib/name_restrictions.py',
|
'zerver/lib/name_restrictions.py',
|
||||||
|
'zerver/lib/realm_icon.py',
|
||||||
'zerver/lib/retention.py',
|
'zerver/lib/retention.py',
|
||||||
'zerver/lib/streams.py',
|
'zerver/lib/streams.py',
|
||||||
'zerver/lib/users.py',
|
'zerver/lib/users.py',
|
||||||
|
|
|
@ -25,6 +25,7 @@ from zerver.lib.message import (
|
||||||
message_to_dict,
|
message_to_dict,
|
||||||
render_markdown,
|
render_markdown,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.realm_icon import realm_icon_url
|
||||||
from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \
|
from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \
|
||||||
Subscription, Recipient, Message, Attachment, UserMessage, \
|
Subscription, Recipient, Message, Attachment, UserMessage, \
|
||||||
Client, DefaultStream, UserPresence, Referral, PushDeviceToken, MAX_SUBJECT_LENGTH, \
|
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),
|
person=payload),
|
||||||
active_user_ids(user_profile.realm))
|
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):
|
def _default_stream_permision_check(user_profile, stream):
|
||||||
# type: (UserProfile, Optional[Stream]) -> None
|
# type: (UserProfile, Optional[Stream]) -> None
|
||||||
# Any user can have a None default stream
|
# Any user can have a None default stream
|
||||||
|
|
|
@ -21,6 +21,7 @@ from zerver.lib.alert_words import user_alert_words
|
||||||
from zerver.lib.attachments import user_attachments
|
from zerver.lib.attachments import user_attachments
|
||||||
from zerver.lib.avatar import get_avatar_url
|
from zerver.lib.avatar import get_avatar_url
|
||||||
from zerver.lib.narrow import check_supported_events_narrow_filter
|
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.request import JsonableError
|
||||||
from zerver.lib.actions import validate_user_access_to_subscribers_helper, \
|
from zerver.lib.actions import validate_user_access_to_subscribers_helper, \
|
||||||
do_get_streams, get_default_streams_for_realm, \
|
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_online_push_notifications'] = user_profile.enable_online_push_notifications
|
||||||
state['enable_digest_emails'] = user_profile.enable_digest_emails
|
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
|
return state
|
||||||
|
|
||||||
def apply_events(state, events, user_profile, include_subscribers=True):
|
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']
|
state['enable_online_push_notifications'] = event['setting']
|
||||||
elif event['notification_name'] == "enable_digest_emails":
|
elif event['notification_name'] == "enable_digest_emails":
|
||||||
state['enable_digest_emails'] = event['setting']
|
state['enable_digest_emails'] = event['setting']
|
||||||
|
elif event['type'] == "realm_change_icon":
|
||||||
|
state['realm_icon'] = event['url']
|
||||||
|
state['realm_icon_source'] = event['source']
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unexpected event type %s" % (event['type'],))
|
raise ValueError("Unexpected event type %s" % (event['type'],))
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
@ -118,6 +118,15 @@ class ZulipUploadBackend(object):
|
||||||
# type: (Text) -> None
|
# type: (Text) -> None
|
||||||
raise NotImplementedError()
|
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
|
### S3
|
||||||
|
|
||||||
def get_bucket(conn, bucket_name):
|
def get_bucket(conn, bucket_name):
|
||||||
|
@ -269,6 +278,38 @@ class S3UploadBackend(ZulipUploadBackend):
|
||||||
# ?x=x allows templates to append additional parameters with &s
|
# ?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)
|
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):
|
def ensure_medium_avatar_image(self, email):
|
||||||
# type: (Text) -> None
|
# type: (Text) -> None
|
||||||
user_profile = get_user_profile_by_email(email)
|
user_profile = get_user_profile_by_email(email)
|
||||||
|
@ -359,6 +400,24 @@ class LocalUploadBackend(ZulipUploadBackend):
|
||||||
medium_suffix = "-medium" if medium else ""
|
medium_suffix = "-medium" if medium else ""
|
||||||
return u"/user_avatars/%s%s.png?x=x" % (hash_key, medium_suffix)
|
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):
|
def ensure_medium_avatar_image(self, email):
|
||||||
# type: (Text) -> None
|
# type: (Text) -> None
|
||||||
email_hash = user_avatar_hash(email)
|
email_hash = user_avatar_hash(email)
|
||||||
|
@ -386,6 +445,10 @@ def upload_avatar_image(user_file, user_profile, email):
|
||||||
# type: (File, UserProfile, Text) -> None
|
# type: (File, UserProfile, Text) -> None
|
||||||
upload_backend.upload_avatar_image(user_file, user_profile, email)
|
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):
|
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
|
# type: (Text, Optional[Text], binary_type, UserProfile, Optional[Realm]) -> Text
|
||||||
return upload_backend.upload_message_image(uploaded_file_name, content_type, file_data,
|
return upload_backend.upload_message_image(uploaded_file_name, content_type, file_data,
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -140,6 +140,16 @@ class Realm(ModelReprMixin, models.Model):
|
||||||
default=2**31 - 1) # type: BitHandler
|
default=2**31 - 1) # type: BitHandler
|
||||||
waiting_period_threshold = models.PositiveIntegerField(default=0) # type: int
|
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'
|
DEFAULT_NOTIFICATION_STREAM_NAME = u'announce'
|
||||||
|
|
||||||
def authentication_methods_dict(self):
|
def authentication_methods_dict(self):
|
||||||
|
|
|
@ -62,7 +62,7 @@ from zerver.lib.actions import (
|
||||||
do_add_realm_alias,
|
do_add_realm_alias,
|
||||||
do_change_realm_alias,
|
do_change_realm_alias,
|
||||||
do_remove_realm_alias,
|
do_remove_realm_alias,
|
||||||
)
|
do_change_icon_source)
|
||||||
from zerver.lib.events import (
|
from zerver.lib.events import (
|
||||||
apply_events,
|
apply_events,
|
||||||
fetch_initial_state_data,
|
fetch_initial_state_data,
|
||||||
|
@ -959,6 +959,19 @@ class EventsRegisterTest(ZulipTestCase):
|
||||||
error = self.realm_bot_schema('avatar_url', check_string)('events[0]', events[0])
|
error = self.realm_bot_schema('avatar_url', check_string)('events[0]', events[0])
|
||||||
self.assert_on_error(error)
|
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):
|
def test_change_bot_default_all_public_streams(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
bot = self.create_bot('test-bot@zulip.com')
|
bot = self.create_bot('test-bot@zulip.com')
|
||||||
|
|
|
@ -6,6 +6,7 @@ from unittest import skip
|
||||||
|
|
||||||
from zerver.lib.avatar import avatar_url
|
from zerver.lib.avatar import avatar_url
|
||||||
from zerver.lib.bugdown import url_filename
|
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_classes import ZulipTestCase, UploadSerializeMixin
|
||||||
from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file
|
from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file
|
||||||
from zerver.lib.test_runner import slow
|
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
|
upload_message_image, delete_message_image, LocalUploadBackend
|
||||||
import zerver.lib.upload
|
import zerver.lib.upload
|
||||||
from zerver.models import Attachment, Recipient, get_user_profile_by_email, \
|
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
|
from zerver.lib.actions import do_delete_old_unclaimed_attachments
|
||||||
|
|
||||||
import ujson
|
import ujson
|
||||||
|
@ -473,6 +474,145 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
destroy_uploads()
|
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):
|
class LocalStorageTest(UploadSerializeMixin, ZulipTestCase):
|
||||||
|
|
||||||
def test_file_upload_local(self):
|
def test_file_upload_local(self):
|
||||||
|
|
|
@ -1947,6 +1947,8 @@ class HomeTest(ZulipTestCase):
|
||||||
"realm_default_streams",
|
"realm_default_streams",
|
||||||
"realm_emoji",
|
"realm_emoji",
|
||||||
"realm_filters",
|
"realm_filters",
|
||||||
|
"realm_icon",
|
||||||
|
"realm_icon_source",
|
||||||
"realm_invite_by_admins_only",
|
"realm_invite_by_admins_only",
|
||||||
"realm_invite_required",
|
"realm_invite_required",
|
||||||
"realm_message_content_edit_limit_seconds",
|
"realm_message_content_edit_limit_seconds",
|
||||||
|
@ -1991,6 +1993,7 @@ class HomeTest(ZulipTestCase):
|
||||||
page_params = self._get_page_params(result)
|
page_params = self._get_page_params(result)
|
||||||
|
|
||||||
actual_keys = sorted([str(k) for k in page_params.keys()])
|
actual_keys = sorted([str(k) for k in page_params.keys()])
|
||||||
|
|
||||||
self.assertEqual(actual_keys, expected_keys)
|
self.assertEqual(actual_keys, expected_keys)
|
||||||
|
|
||||||
# TODO: Inspect the page_params data further.
|
# TODO: Inspect the page_params data further.
|
||||||
|
|
|
@ -12,6 +12,7 @@ from six.moves import zip_longest, zip, range
|
||||||
from version import ZULIP_VERSION
|
from version import ZULIP_VERSION
|
||||||
from zerver.decorator import zulip_login_required, process_client
|
from zerver.decorator import zulip_login_required, process_client
|
||||||
from zerver.forms import ToSForm
|
from zerver.forms import ToSForm
|
||||||
|
from zerver.lib.realm_icon import realm_icon_url
|
||||||
from zerver.models import Message, UserProfile, Stream, Subscription, Huddle, \
|
from zerver.models import Message, UserProfile, Stream, Subscription, Huddle, \
|
||||||
Recipient, Realm, UserMessage, DefaultStream, RealmEmoji, RealmAlias, \
|
Recipient, Realm, UserMessage, DefaultStream, RealmEmoji, RealmAlias, \
|
||||||
RealmFilter, PreregistrationUser, UserActivity, \
|
RealmFilter, PreregistrationUser, UserActivity, \
|
||||||
|
@ -225,6 +226,8 @@ def home_real(request):
|
||||||
realm_restricted_to_domain = register_ret['realm_restricted_to_domain'],
|
realm_restricted_to_domain = register_ret['realm_restricted_to_domain'],
|
||||||
realm_default_language = register_ret['realm_default_language'],
|
realm_default_language = register_ret['realm_default_language'],
|
||||||
realm_waiting_period_threshold = register_ret['realm_waiting_period_threshold'],
|
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,
|
enter_sends = user_profile.enter_sends,
|
||||||
user_id = user_profile.id,
|
user_id = user_profile.id,
|
||||||
left_side_userlist = register_ret['left_side_userlist'],
|
left_side_userlist = register_ret['left_side_userlist'],
|
||||||
|
|
|
@ -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)
|
|
@ -859,6 +859,7 @@ JS_SPECS = {
|
||||||
'js/templates.js',
|
'js/templates.js',
|
||||||
'js/upload_widget.js',
|
'js/upload_widget.js',
|
||||||
'js/avatar.js',
|
'js/avatar.js',
|
||||||
|
'js/realm_icon.js',
|
||||||
'js/settings.js',
|
'js/settings.js',
|
||||||
'js/admin.js',
|
'js/admin.js',
|
||||||
'js/tab_bar.js',
|
'js/tab_bar.js',
|
||||||
|
|
|
@ -185,6 +185,12 @@ v1_api_and_json_patterns = [
|
||||||
{'PUT': 'zerver.views.realm_emoji.upload_emoji',
|
{'PUT': 'zerver.views.realm_emoji.upload_emoji',
|
||||||
'DELETE': 'zerver.views.realm_emoji.delete_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
|
# realm/filters -> zerver.views.realm_filters
|
||||||
url(r'^realm/filters$', rest_dispatch,
|
url(r'^realm/filters$', rest_dispatch,
|
||||||
{'GET': 'zerver.views.realm_filters.list_filters',
|
{'GET': 'zerver.views.realm_filters.list_filters',
|
||||||
|
|
Loading…
Reference in New Issue