diff --git a/securedrop/journalist.py b/securedrop/journalist.py index e78f035793..826e428289 100644 --- a/securedrop/journalist.py +++ b/securedrop/journalist.py @@ -578,23 +578,37 @@ def download_unread(sid): return bulk_download(sid, docs) +@app.route('/download_all_unread') +@login_required +def download_all_unread(): + docs = Submission.query.filter(Submission.downloaded == False).all() + if docs == []: + flash("No unread documents in collections!", "error") + return redirect(url_for('index')) + return bulk_dl('all_unread', docs) + + @app.route('/bulk', methods=('POST',)) @login_required def bulk(): action = request.form['action'] doc_names_selected = request.form.getlist('doc_names_selected') - selected_docs = [doc for doc in g.source.collection - if doc.filename in doc_names_selected] - + if action == 'download_all': + selected_docs = g.source.collection + else: + selected_docs = [doc for doc in g.source.collection + if doc.filename in doc_names_selected] if selected_docs == []: if action == 'download': flash("No collections selected to download!", "error") + elif action == 'download_all': + flash("No collections available to download!", "error") elif action == 'delete' or action == 'confirm_delete': flash("No collections selected to delete!", "error") return redirect(url_for('col', sid=g.sid)) - if action == 'download': + if action in ('download', 'download_all'): return bulk_download(g.sid, selected_docs) elif action == 'delete': return bulk_delete(g.sid, selected_docs) @@ -627,7 +641,12 @@ def bulk_delete(sid, items_selected): def bulk_download(sid, items_selected): source = get_source(sid) - filenames = [store.path(sid, item.filename) for item in items_selected] + return bulk_dl(source.journalist_filename, items_selected) + + +def bulk_dl(zip_directory, items_selected): + filenames = [store.path(item.source.filesystem_id, item.filename) + for item in items_selected] # Mark the submissions that are about to be downloaded as such for item in items_selected: @@ -635,12 +654,10 @@ def bulk_download(sid, items_selected): item.downloaded = True db_session.commit() - zf = store.get_bulk_archive( - filenames, - zip_directory=source.journalist_filename) + zf = store.get_bulk_archive(filenames, + zip_directory=zip_directory) attachment_filename = "{}--{}.zip".format( - source.journalist_filename, - datetime.utcnow().strftime("%Y-%m-%d--%H-%M-%S")) + zip_directory, datetime.utcnow().strftime("%Y-%m-%d--%H-%M-%S")) return send_file(zf.name, mimetype="application/zip", attachment_filename=attachment_filename, as_attachment=True) diff --git a/securedrop/journalist_templates/col.html b/securedrop/journalist_templates/col.html index d4338d82c2..4f9a5b5b41 100644 --- a/securedrop/journalist_templates/col.html +++ b/securedrop/journalist_templates/col.html @@ -23,6 +23,7 @@
+
diff --git a/securedrop/journalist_templates/index.html b/securedrop/journalist_templates/index.html index 0730a66ba4..6338cb8d00 100644 --- a/securedrop/journalist_templates/index.html +++ b/securedrop/journalist_templates/index.html @@ -8,6 +8,7 @@

Sources

select all select none + Download all unread diff --git a/securedrop/tests/test_unit_journalist.py b/securedrop/tests/test_unit_journalist.py index 807fc449e9..21cfd7de2f 100644 --- a/securedrop/tests/test_unit_journalist.py +++ b/securedrop/tests/test_unit_journalist.py @@ -7,7 +7,7 @@ import mock from flask_testing import TestCase -from flask import url_for, escape +from flask import url_for, escape, g import crypto_util import journalist @@ -415,27 +415,149 @@ def test_edit_hotp(self): # should redirect to verification page self.assertRedirects(res, url_for('account_new_two_factor')) - def test_bulk_download(self): + def test_selected_bulk_download(self): sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' source = Source(sid, crypto_util.display_id()) db_session.add(source) db_session.commit() - files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg'] - filenames = common.setup_test_docs(sid, files) + files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg', '3-abc3-msg.gpg', '4-abc4-msg.gpg'] + selected_files = files[:2] + unselected_files = files[2:] + common.setup_test_docs(sid, files) self._login_user() rv = self.client.post('/bulk', data=dict( action='download', sid=sid, - doc_names_selected=files + doc_names_selected=selected_files )) self.assertEqual(rv.status_code, 200) self.assertEqual(rv.content_type, 'application/zip') self.assertTrue(zipfile.is_zipfile(StringIO(rv.data))) - self.assertTrue(zipfile.ZipFile(StringIO(rv.data)).getinfo( - os.path.join(source.journalist_filename, files[0]) + + for file in selected_files: + self.assertTrue(zipfile.ZipFile(StringIO(rv.data)).getinfo( + os.path.join(source.journalist_filename, file) + )) + + for file in unselected_files: + try: + zipfile.ZipFile(StringIO(rv.data)).getinfo( + os.path.join(source.journalist_filename, file)) + except KeyError: + pass + else: + self.assertTrue(False) + + def test_download_all_bulk_download(self): + sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' + source = Source(sid, crypto_util.display_id()) + db_session.add(source) + db_session.commit() + files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg', '3-abc3-msg.gpg', '4-abc4-msg.gpg'] + common.setup_test_docs(sid, files) + + self._login_user() + rv = self.client.post('/bulk', data=dict( + action='download_all', + sid=sid + )) + + self.assertEqual(rv.status_code, 200) + self.assertEqual(rv.content_type, 'application/zip') + self.assertTrue(zipfile.is_zipfile(StringIO(rv.data))) + for file in files: + self.assertTrue(zipfile.ZipFile(StringIO(rv.data)).getinfo( + os.path.join(source.journalist_filename, file) + )) + + def test_download_bulk_download_nothing_selected(self): + sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' + source = Source(sid, crypto_util.display_id()) + db_session.add(source) + db_session.commit() + files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg', '3-abc3-msg.gpg', '4-abc4-msg.gpg'] + common.setup_test_docs(sid, files) + + self._login_user() + rv = self.client.post('/bulk', data=dict( + action='download', + sid=sid, + doc_names_selected = [] + )) + self.assertRedirects(rv, url_for('col', sid=sid)) + + def test_download_bulk_download_all_no_submissions(self): + sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' + source = Source(sid, crypto_util.display_id()) + db_session.add(source) + db_session.commit() + + self._login_user() + rv = self.client.post('/bulk', data=dict( + action='download_all', + sid=sid, + )) + self.assertRedirects(rv, url_for('col', sid=sid)) + + def test_delete_bulk_delete_nothing_selected(self): + sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' + source = Source(sid, crypto_util.display_id()) + db_session.add(source) + db_session.commit() + files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg', '3-abc3-msg.gpg', '4-abc4-msg.gpg'] + common.setup_test_docs(sid, files) + + self._login_user() + rv = self.client.post('/bulk', data=dict( + action='delete', + sid=sid, + doc_names_selected = [] + )) + self.assertRedirects(rv, url_for('col', sid=sid)) + + def test_download_all(self): + sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' + source = Source(sid, crypto_util.display_id()) + db_session.add(source) + db_session.commit() + files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg', '3-abc3-msg.gpg', '4-abc4-msg.gpg'] + selected_files = files[::2] + unselected_files = files[1::2] + common.setup_test_docs(sid, files) + + self._login_user() + rv = self.client.post('/bulk', data=dict( + action='download', + sid=sid, + doc_names_selected=selected_files + )) + rv = self.client.get('/download_all_unread') + self.assertEqual(rv.status_code, 200) + self.assertEqual(rv.content_type, 'application/zip') + self.assertTrue(len(zipfile.ZipFile(StringIO(rv.data)).namelist()) == \ + len(unselected_files)) + for file in unselected_files: + self.assertTrue(zipfile.ZipFile(StringIO(rv.data)).getinfo( + os.path.join('all_unread', file + ))) + + def test_download_all_with_no_unread_submissions(self): + sid = 'EQZGCJBRGISGOTC2NZVWG6LILJBHEV3CINNEWSCLLFTUWZJPKJFECLS2NZ4G4U3QOZCFKTTPNZMVIWDCJBBHMUDBGFHXCQ3R' + source = Source(sid, crypto_util.display_id()) + db_session.add(source) + db_session.commit() + files = ['1-abc1-msg.gpg', '2-abc2-msg.gpg', '3-abc3-msg.gpg', '4-abc4-msg.gpg'] + common.setup_test_docs(sid, files) + + self._login_user() + rv = self.client.post('/bulk', data=dict( + action='download_all', + sid=sid, )) + rv = self.client.get('/download_all_unread') + self.assertRedirects(rv, url_for('index')) def test_max_password_length(self): """Creating a Journalist with a password that is greater than the