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,
|
||||
"status_classes": false,
|
||||
"password_quality": false,
|
||||
"attachments_ui": false,
|
||||
"csrf_token": false,
|
||||
"typeahead_helper": false,
|
||||
"popovers": false,
|
||||
|
|
|
@ -42,6 +42,7 @@ function render(template_name, args) {
|
|||
'notification-settings',
|
||||
'bot-settings',
|
||||
'alert-word-settings',
|
||||
'attachments-settings',
|
||||
'ui-settings',
|
||||
]);
|
||||
}());
|
||||
|
@ -252,6 +253,22 @@ function render(template_name, args) {
|
|||
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() {
|
||||
var args = {
|
||||
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();
|
||||
|
||||
alert_words_ui.set_up_alert_words();
|
||||
attachments_ui.set_up_attachments();
|
||||
|
||||
$("#api_key_value").text("");
|
||||
$("#get_api_key_box").hide();
|
||||
|
|
|
@ -462,7 +462,7 @@ input[type=checkbox].inline-block {
|
|||
position: inherit;
|
||||
}
|
||||
|
||||
#alert_words_list {
|
||||
#alert_words_list, #attachments_list {
|
||||
list-style-type: none;
|
||||
margin: auto;
|
||||
}
|
||||
|
@ -472,7 +472,7 @@ input[type=checkbox].inline-block {
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#alert_words_list .edit-alert-word-buttons {
|
||||
#alert_words_list .edit-alert-word-buttons, #attachments_list .edit-attachment-buttons {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
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 "attachments-settings" }}
|
||||
|
||||
{{ partial "ui-settings" }}
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
<div class="icon icon-vector-book"></div>
|
||||
<div class="text">{{ _('Custom alert words') }}</div>
|
||||
</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">
|
||||
<i class="icon icon-vector-beaker"></i>
|
||||
<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)
|
||||
|
||||
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.request import JsonableError
|
||||
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'):
|
||||
state['alert_words'] = user_alert_words(user_profile)
|
||||
|
||||
if want('attachments'):
|
||||
state['attachments'] = user_attachments(user_profile)
|
||||
|
||||
if want('message'):
|
||||
# The client should use get_old_messages() to fetch messages
|
||||
# starting with the max_message_id. They will get messages
|
||||
|
|
|
@ -1140,6 +1140,19 @@ class Attachment(ModelReprMixin, models.Model):
|
|||
# type: () -> bool
|
||||
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):
|
||||
# type: (int) -> Sequence[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!!!
|
||||
expected_keys = [
|
||||
"alert_words",
|
||||
"attachments",
|
||||
"autoscroll_forever",
|
||||
"avatar_source",
|
||||
"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),
|
||||
save_stacktraces = settings.SAVE_FRONTEND_STACKTRACES,
|
||||
alert_words = register_ret['alert_words'],
|
||||
attachments = register_ret['attachments'],
|
||||
muted_topics = register_ret['muted_topics'],
|
||||
realm_filters = register_ret['realm_filters'],
|
||||
realm_default_streams = register_ret['realm_default_streams'],
|
||||
|
|
|
@ -852,6 +852,7 @@ JS_SPECS = {
|
|||
'js/message_flags.js',
|
||||
'js/alert_words.js',
|
||||
'js/alert_words_ui.js',
|
||||
'js/attachments_ui.js',
|
||||
'js/message_store.js',
|
||||
'js/server_events.js',
|
||||
'js/zulip.js',
|
||||
|
|
|
@ -228,6 +228,12 @@ v1_api_and_json_patterns = [
|
|||
{'PUT': 'zerver.views.reactions.add_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
|
||||
# POST sends a typing notification event to recipients
|
||||
url(r'^typing$', rest_dispatch,
|
||||
|
|
Loading…
Reference in New Issue