mirror of https://github.com/zulip/zulip.git
Add support for managing and deleting attachments.
Modified substantially by tabbott to fix tons of issues. Fixes #454.
This commit is contained in:
parent
f528af2be0
commit
9a5179c460
|
@ -19,6 +19,7 @@
|
||||||
"page_params": false,
|
"page_params": false,
|
||||||
"status_classes": false,
|
"status_classes": false,
|
||||||
"password_quality": false,
|
"password_quality": false,
|
||||||
|
"attachments_ui": false,
|
||||||
"csrf_token": false,
|
"csrf_token": false,
|
||||||
"typeahead_helper": false,
|
"typeahead_helper": false,
|
||||||
"popovers": false,
|
"popovers": false,
|
||||||
|
|
|
@ -42,6 +42,7 @@ function render(template_name, args) {
|
||||||
'notification-settings',
|
'notification-settings',
|
||||||
'bot-settings',
|
'bot-settings',
|
||||||
'alert-word-settings',
|
'alert-word-settings',
|
||||||
|
'attachments-settings',
|
||||||
'ui-settings',
|
'ui-settings',
|
||||||
]);
|
]);
|
||||||
}());
|
}());
|
||||||
|
@ -252,6 +253,22 @@ function render(template_name, args) {
|
||||||
global.write_handlebars_output("announce_stream_docs", html);
|
global.write_handlebars_output("announce_stream_docs", html);
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
(function attachment_settings_item() {
|
||||||
|
var html = '<ul id="attachments">';
|
||||||
|
var attachments = [
|
||||||
|
{messages: [], id: 42, name: "foo.txt"},
|
||||||
|
{messages: [], id: 43, name: "bar.txt"},
|
||||||
|
];
|
||||||
|
_.each(attachments, function (attachment) {
|
||||||
|
var args = {attachment: attachment};
|
||||||
|
html += render('attachment-item', args);
|
||||||
|
});
|
||||||
|
html += "</ul>";
|
||||||
|
global.write_handlebars_output("attachment-item", html);
|
||||||
|
var li = $(html).find("li.attachment-item:first");
|
||||||
|
assert.equal(li.attr('data-attachment'), 42);
|
||||||
|
}());
|
||||||
|
|
||||||
(function bankruptcy_modal() {
|
(function bankruptcy_modal() {
|
||||||
var args = {
|
var args = {
|
||||||
unread_count: 99,
|
unread_count: 99,
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
var attachments_ui = (function () {
|
||||||
|
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
function delete_attachments(attachment) {
|
||||||
|
channel.del({url: '/json/attachments/' + attachment, idempotent: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.set_up_attachments = function () {
|
||||||
|
// The settings page must be rendered before this function gets called.
|
||||||
|
|
||||||
|
var attachment_list = $('#attachments_list');
|
||||||
|
_.each(page_params.attachments, function (attachment) {
|
||||||
|
var li = templates.render('attachment-item', {attachment: attachment});
|
||||||
|
attachment_list.append(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#attachments_list').on('click', '.remove-attachment', function (event) {
|
||||||
|
var li = $(event.currentTarget).parents('li');
|
||||||
|
li.remove();
|
||||||
|
delete_attachments($(this).data('attachment'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return exports;
|
||||||
|
}());
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
module.exports = attachments_ui;
|
||||||
|
}
|
|
@ -199,6 +199,7 @@ function _setup_page() {
|
||||||
$("#ui-settings-status").hide();
|
$("#ui-settings-status").hide();
|
||||||
|
|
||||||
alert_words_ui.set_up_alert_words();
|
alert_words_ui.set_up_alert_words();
|
||||||
|
attachments_ui.set_up_attachments();
|
||||||
|
|
||||||
$("#api_key_value").text("");
|
$("#api_key_value").text("");
|
||||||
$("#get_api_key_box").hide();
|
$("#get_api_key_box").hide();
|
||||||
|
|
|
@ -462,7 +462,7 @@ input[type=checkbox].inline-block {
|
||||||
position: inherit;
|
position: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#alert_words_list {
|
#alert_words_list, #attachments_list {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
@ -472,7 +472,7 @@ input[type=checkbox].inline-block {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#alert_words_list .edit-alert-word-buttons {
|
#alert_words_list .edit-alert-word-buttons, #attachments_list .edit-attachment-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
|
||||||
|
<li class="attachment-item" data-attachment='{{attachment.id}}'>
|
||||||
|
<div class="attachment-information-box list-container">
|
||||||
|
<div class="attachment_listing">
|
||||||
|
<strong>{{attachment.name}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="edit-attachment-buttons">
|
||||||
|
<button type="submit"
|
||||||
|
class="button small btn-small btn-danger remove-attachment"
|
||||||
|
title="{{t 'Delete file' }}" data-attachment="{{attachment.id}}">
|
||||||
|
<i class="icon-vector-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{#if attachment.messages }}
|
||||||
|
<ul class="attachment-messages">
|
||||||
|
{{#each attachment.messages}}
|
||||||
|
<li>
|
||||||
|
<a href="/#narrow/id/{{ this.id }}">{{ this.name }}</a>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</li>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<div id="attachments-settings" class="settings-section" data-name="uploaded-files">
|
||||||
|
<div class="settings-section-title">
|
||||||
|
<i class="icon-vector-paper-clip settings-section-icon"></i>
|
||||||
|
{{t "Uploaded files" }}
|
||||||
|
</div>
|
||||||
|
<div class="side-padded-container">
|
||||||
|
<p class="alert-word-settings-note">
|
||||||
|
{{t 'For each file, we list any messages that link to it.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul id="attachments_list"></ul>
|
||||||
|
</div>
|
|
@ -11,5 +11,7 @@
|
||||||
|
|
||||||
{{ partial "alert-word-settings" }}
|
{{ partial "alert-word-settings" }}
|
||||||
|
|
||||||
|
{{ partial "attachments-settings" }}
|
||||||
|
|
||||||
{{ partial "ui-settings" }}
|
{{ partial "ui-settings" }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,6 +28,10 @@
|
||||||
<div class="icon icon-vector-book"></div>
|
<div class="icon icon-vector-book"></div>
|
||||||
<div class="text">{{ _('Custom alert words') }}</div>
|
<div class="text">{{ _('Custom alert words') }}</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li tabindex="1" data-section="uploaded-files">
|
||||||
|
<div class="icon icon-vector-paper-clip"></div>
|
||||||
|
<div class="text">{{ _('Uploaded files') }}</div>
|
||||||
|
</li>
|
||||||
<li tabindex="1" data-section="zulip-labs">
|
<li tabindex="1" data-section="zulip-labs">
|
||||||
<i class="icon icon-vector-beaker"></i>
|
<i class="icon icon-vector-beaker"></i>
|
||||||
<div class="text">{{ _('Zulip labs') }}</div>
|
<div class="text">{{ _('Zulip labs') }}</div>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from zerver.lib.request import JsonableError
|
||||||
|
from zerver.lib.upload import delete_message_image
|
||||||
|
from zerver.models import Attachment, UserProfile
|
||||||
|
|
||||||
|
def user_attachments(user_profile):
|
||||||
|
# type: (UserProfile) -> List[Dict[str, Any]]
|
||||||
|
attachments = Attachment.objects.filter(owner=user_profile).prefetch_related('messages')
|
||||||
|
return [a.to_dict() for a in attachments]
|
||||||
|
|
||||||
|
def access_attachment_by_id(user_profile, attachment_id, needs_owner=False):
|
||||||
|
# type: (UserProfile, int, bool) -> Attachment
|
||||||
|
query = Attachment.objects.filter(id=attachment_id)
|
||||||
|
if needs_owner:
|
||||||
|
query = query.filter(owner=user_profile)
|
||||||
|
|
||||||
|
attachment = query.first()
|
||||||
|
if attachment is None:
|
||||||
|
raise JsonableError(_("Invalid attachment"))
|
||||||
|
return attachment
|
||||||
|
|
||||||
|
def remove_attachment(user_profile, attachment):
|
||||||
|
# type: (UserProfile, Attachment) -> None
|
||||||
|
delete_message_image(attachment.path_id)
|
||||||
|
attachment.delete()
|
|
@ -18,6 +18,7 @@ from typing import (
|
||||||
session_engine = import_module(settings.SESSION_ENGINE)
|
session_engine = import_module(settings.SESSION_ENGINE)
|
||||||
|
|
||||||
from zerver.lib.alert_words import user_alert_words
|
from zerver.lib.alert_words import user_alert_words
|
||||||
|
from zerver.lib.attachments import user_attachments
|
||||||
from zerver.lib.narrow import check_supported_events_narrow_filter
|
from zerver.lib.narrow import check_supported_events_narrow_filter
|
||||||
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, \
|
||||||
|
@ -54,6 +55,9 @@ def fetch_initial_state_data(user_profile, event_types, queue_id):
|
||||||
if want('alert_words'):
|
if want('alert_words'):
|
||||||
state['alert_words'] = user_alert_words(user_profile)
|
state['alert_words'] = user_alert_words(user_profile)
|
||||||
|
|
||||||
|
if want('attachments'):
|
||||||
|
state['attachments'] = user_attachments(user_profile)
|
||||||
|
|
||||||
if want('message'):
|
if want('message'):
|
||||||
# The client should use get_old_messages() to fetch messages
|
# The client should use get_old_messages() to fetch messages
|
||||||
# starting with the max_message_id. They will get messages
|
# starting with the max_message_id. They will get messages
|
||||||
|
|
|
@ -1140,6 +1140,19 @@ class Attachment(ModelReprMixin, models.Model):
|
||||||
# type: () -> bool
|
# type: () -> bool
|
||||||
return self.messages.count() > 0
|
return self.messages.count() > 0
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
# type: () -> Dict[str, Any]
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.file_name,
|
||||||
|
'path_id': self.path_id,
|
||||||
|
'messages': [{
|
||||||
|
'id': m.id,
|
||||||
|
'name': '{m.pub_date:%Y-%m-%d %H:%M} {recipient}/{m.subject}'.format(
|
||||||
|
recipient=get_display_recipient(m.recipient), m=m)
|
||||||
|
} for m in self.messages.all()]
|
||||||
|
}
|
||||||
|
|
||||||
def get_old_unclaimed_attachments(weeks_ago):
|
def get_old_unclaimed_attachments(weeks_ago):
|
||||||
# type: (int) -> Sequence[Attachment]
|
# type: (int) -> Sequence[Attachment]
|
||||||
# TODO: Change return type to QuerySet[Attachment]
|
# TODO: Change return type to QuerySet[Attachment]
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import ujson
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from zerver.lib.attachments import user_attachments
|
||||||
|
from zerver.lib.test_helpers import get_user_profile_by_email
|
||||||
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
|
from zerver.models import Attachment
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentsTests(ZulipTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# type: () -> None
|
||||||
|
user = get_user_profile_by_email("cordelia@zulip.com")
|
||||||
|
self.attachment = Attachment.objects.create(
|
||||||
|
file_name='test.txt', path_id='foo/bar/test.txt', owner=user)
|
||||||
|
|
||||||
|
def test_list_by_user(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.login("cordelia@zulip.com")
|
||||||
|
result = self.client_get('/json/attachments')
|
||||||
|
self.assert_json_success(result)
|
||||||
|
user = get_user_profile_by_email("cordelia@zulip.com")
|
||||||
|
attachments = user_attachments(user)
|
||||||
|
data = ujson.loads(result.content)
|
||||||
|
self.assertEqual(data['attachments'], attachments)
|
||||||
|
|
||||||
|
@mock.patch('zerver.lib.attachments.delete_message_image')
|
||||||
|
def test_remove_attachment(self, ignored):
|
||||||
|
# type: (Any) -> None
|
||||||
|
self.login("cordelia@zulip.com")
|
||||||
|
result = self.client_delete('/json/attachments/{pk}'.format(pk=self.attachment.pk))
|
||||||
|
self.assert_json_success(result)
|
||||||
|
user = get_user_profile_by_email("cordelia@zulip.com")
|
||||||
|
attachments = user_attachments(user)
|
||||||
|
self.assertEqual(attachments, [])
|
||||||
|
|
||||||
|
def test_list_another_user(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.login("iago@zulip.com")
|
||||||
|
result = self.client_get('/json/attachments')
|
||||||
|
self.assert_json_success(result)
|
||||||
|
data = ujson.loads(result.content)
|
||||||
|
self.assertEqual(data['attachments'], [])
|
||||||
|
|
||||||
|
def test_remove_another_user(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.login("iago@zulip.com")
|
||||||
|
result = self.client_delete('/json/attachments/{pk}'.format(pk=self.attachment.pk))
|
||||||
|
self.assert_json_error(result, 'Invalid attachment')
|
||||||
|
user = get_user_profile_by_email("cordelia@zulip.com")
|
||||||
|
attachments = user_attachments(user)
|
||||||
|
self.assertEqual(attachments, [self.attachment.to_dict()])
|
||||||
|
|
||||||
|
def test_list_unauthenticated(self):
|
||||||
|
# type: () -> None
|
||||||
|
result = self.client_get('/json/attachments')
|
||||||
|
self.assert_json_error(result, 'Not logged in: API authentication or user session required', status_code=401)
|
||||||
|
|
||||||
|
def test_delete_unauthenticated(self):
|
||||||
|
# type: () -> None
|
||||||
|
result = self.client_delete('/json/attachments/{pk}'.format(pk=self.attachment.pk))
|
||||||
|
self.assert_json_error(result, 'Not logged in: API authentication or user session required', status_code=401)
|
|
@ -1856,6 +1856,7 @@ class HomeTest(ZulipTestCase):
|
||||||
# Keep this list sorted!!!
|
# Keep this list sorted!!!
|
||||||
expected_keys = [
|
expected_keys = [
|
||||||
"alert_words",
|
"alert_words",
|
||||||
|
"attachments",
|
||||||
"autoscroll_forever",
|
"autoscroll_forever",
|
||||||
"avatar_source",
|
"avatar_source",
|
||||||
"avatar_url",
|
"avatar_url",
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
|
from zerver.decorator import REQ
|
||||||
|
from zerver.models import UserProfile
|
||||||
|
from zerver.lib.validator import check_int
|
||||||
|
from zerver.lib.response import json_success
|
||||||
|
from zerver.lib.attachments import user_attachments, remove_attachment, \
|
||||||
|
access_attachment_by_id
|
||||||
|
|
||||||
|
|
||||||
|
def list_by_user(request, user_profile):
|
||||||
|
# type: (HttpRequest, UserProfile) -> HttpResponse
|
||||||
|
return json_success({"attachments": user_attachments(user_profile)})
|
||||||
|
|
||||||
|
|
||||||
|
def remove(request, user_profile, attachment_id=REQ(validator=check_int)):
|
||||||
|
# type: (HttpRequest, UserProfile, int) -> HttpResponse
|
||||||
|
attachment = access_attachment_by_id(user_profile, attachment_id,
|
||||||
|
needs_owner=True)
|
||||||
|
remove_attachment(user_profile, attachment)
|
||||||
|
return json_success()
|
|
@ -264,6 +264,7 @@ def home_real(request):
|
||||||
furthest_read_time = sent_time_in_epoch_seconds(latest_read),
|
furthest_read_time = sent_time_in_epoch_seconds(latest_read),
|
||||||
save_stacktraces = settings.SAVE_FRONTEND_STACKTRACES,
|
save_stacktraces = settings.SAVE_FRONTEND_STACKTRACES,
|
||||||
alert_words = register_ret['alert_words'],
|
alert_words = register_ret['alert_words'],
|
||||||
|
attachments = register_ret['attachments'],
|
||||||
muted_topics = register_ret['muted_topics'],
|
muted_topics = register_ret['muted_topics'],
|
||||||
realm_filters = register_ret['realm_filters'],
|
realm_filters = register_ret['realm_filters'],
|
||||||
realm_default_streams = register_ret['realm_default_streams'],
|
realm_default_streams = register_ret['realm_default_streams'],
|
||||||
|
|
|
@ -852,6 +852,7 @@ JS_SPECS = {
|
||||||
'js/message_flags.js',
|
'js/message_flags.js',
|
||||||
'js/alert_words.js',
|
'js/alert_words.js',
|
||||||
'js/alert_words_ui.js',
|
'js/alert_words_ui.js',
|
||||||
|
'js/attachments_ui.js',
|
||||||
'js/message_store.js',
|
'js/message_store.js',
|
||||||
'js/server_events.js',
|
'js/server_events.js',
|
||||||
'js/zulip.js',
|
'js/zulip.js',
|
||||||
|
|
|
@ -228,6 +228,12 @@ v1_api_and_json_patterns = [
|
||||||
{'PUT': 'zerver.views.reactions.add_reaction_backend',
|
{'PUT': 'zerver.views.reactions.add_reaction_backend',
|
||||||
'DELETE': 'zerver.views.reactions.remove_reaction_backend'}),
|
'DELETE': 'zerver.views.reactions.remove_reaction_backend'}),
|
||||||
|
|
||||||
|
# attachments -> zerver.views.attachments
|
||||||
|
url(r'^attachments$', rest_dispatch,
|
||||||
|
{'GET': 'zerver.views.attachments.list_by_user'}),
|
||||||
|
url(r'^attachments/(?P<attachment_id>[0-9]+)$', rest_dispatch,
|
||||||
|
{'DELETE': 'zerver.views.attachments.remove'}),
|
||||||
|
|
||||||
# typing -> zerver.views.typing
|
# typing -> zerver.views.typing
|
||||||
# POST sends a typing notification event to recipients
|
# POST sends a typing notification event to recipients
|
||||||
url(r'^typing$', rest_dispatch,
|
url(r'^typing$', rest_dispatch,
|
||||||
|
|
Loading…
Reference in New Issue