Skip to content

Commit

Permalink
Add support for managing and deleting attachments.
Browse files Browse the repository at this point in the history
Modified substantially by tabbott to fix tons of issues.

Fixes #454.
  • Loading branch information
paxapy authored and timabbott committed Feb 17, 2017
1 parent f528af2 commit 9a5179c
Show file tree
Hide file tree
Showing 18 changed files with 238 additions and 2 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Expand Up @@ -19,6 +19,7 @@
"page_params": false,
"status_classes": false,
"password_quality": false,
"attachments_ui": false,
"csrf_token": false,
"typeahead_helper": false,
"popovers": false,
Expand Down
17 changes: 17 additions & 0 deletions frontend_tests/node_tests/templates.js
Expand Up @@ -42,6 +42,7 @@ function render(template_name, args) {
'notification-settings',
'bot-settings',
'alert-word-settings',
'attachments-settings',
'ui-settings',
]);
}());
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions static/js/attachments_ui.js
@@ -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;
}
1 change: 1 addition & 0 deletions static/js/settings.js
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions static/styles/settings.css
Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions static/templates/settings/attachment-item.handlebars
@@ -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>
12 changes: 12 additions & 0 deletions static/templates/settings/attachments-settings.handlebars
@@ -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>
2 changes: 2 additions & 0 deletions static/templates/settings_tab.handlebars
Expand Up @@ -11,5 +11,7 @@

{{ partial "alert-word-settings" }}

{{ partial "attachments-settings" }}

{{ partial "ui-settings" }}
</div>
4 changes: 4 additions & 0 deletions templates/zerver/settings_overlay.html
Expand Up @@ -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>
Expand Down
29 changes: 29 additions & 0 deletions zerver/lib/attachments.py
@@ -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()
4 changes: 4 additions & 0 deletions zerver/lib/events.py
Expand Up @@ -18,6 +18,7 @@
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, \
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions zerver/models.py
Expand Up @@ -1140,6 +1140,19 @@ def is_claimed(self):
# 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]
Expand Down
68 changes: 68 additions & 0 deletions zerver/tests/test_attachments.py
@@ -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)
1 change: 1 addition & 0 deletions zerver/tests/tests.py
Expand Up @@ -1856,6 +1856,7 @@ def test_home(self):
# Keep this list sorted!!!
expected_keys = [
"alert_words",
"attachments",
"autoscroll_forever",
"avatar_source",
"avatar_url",
Expand Down
22 changes: 22 additions & 0 deletions zerver/views/attachments.py
@@ -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()
1 change: 1 addition & 0 deletions zerver/views/home.py
Expand Up @@ -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'],
Expand Down
1 change: 1 addition & 0 deletions zproject/settings.py
Expand Up @@ -852,6 +852,7 @@ def get_secret(key):
'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',
Expand Down
6 changes: 6 additions & 0 deletions zproject/urls.py
Expand Up @@ -228,6 +228,12 @@
{'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,
Expand Down

0 comments on commit 9a5179c

Please sign in to comment.