We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Database error when trying to delete replies in the journalist interface
An error is thrown deleting replies in the journalist interface. An attempt is made to remove a record for the reply from the database but replies are only recorded on the filesystem.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/journalist.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import config
3 import version
4 import crypto_util
5 import store
6 import template_filters
7 from db import db_session, Source, Submission, SourceStar, get_one_or_else
8 
9 import os
10 from datetime import datetime
11 from flask import (Flask, request, render_template, send_file, redirect, flash, url_for, g, abort)
12 from flask_wtf.csrf import CsrfProtect
13 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
14 
15 import background
16 
17 app = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR)
18 app.config.from_object(config.JournalistInterfaceFlaskConfig)
19 CsrfProtect(app)
20 
21 app.jinja_env.globals['version'] = version.__version__
22 if getattr(config, 'CUSTOM_HEADER_IMAGE', None):
23     app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
24     app.jinja_env.globals['use_custom_header_image'] = True
25 else:
26     app.jinja_env.globals['header_image'] = 'logo.png'
27     app.jinja_env.globals['use_custom_header_image'] = False
28 
29 app.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat
30 
31 
32 @app.teardown_appcontext
33 def shutdown_session(exception=None):
34     """Automatically remove database sessions at the end of the request, or
35     when the application shuts down"""
36     db_session.remove()
37 
38 
39 def get_source(sid):
40     """Return a Source object, representing the database row, for the source
41     with id `sid`"""
42     source = None
43     query = Source.query.filter(Source.filesystem_id == sid)
44     source = get_one_or_else(query, app.logger, abort)
45 
46     return source
47 
48 
49 @app.before_request
50 def setup_g():
51     """Store commonly used values in Flask's special g object"""
52     if request.method == 'POST':
53         sid = request.form.get('sid')
54         if sid:
55             g.sid = sid
56             g.source = get_source(sid)
57 
58 
59 def get_docs(sid):
60     """Get docs associated with source id `sid`, sorted by submission date"""
61     docs = []
62     for filename in os.listdir(store.path(sid)):
63         os_stat = os.stat(store.path(sid, filename))
64         docs.append(dict(
65             name=filename,
66             date=datetime.fromtimestamp(os_stat.st_mtime),
67             size=os_stat.st_size,
68         ))
69     # sort in chronological order
70     docs.sort(key=lambda x: int(x['name'].split('-')[0]))
71     return docs
72 
73 
74 def make_star_true(sid):
75     source = get_source(sid)
76     if source.star:
77         source.star.starred = True
78     else:
79         source_star = SourceStar(source)
80         db_session.add(source_star)
81 
82 
83 def make_star_false(sid):
84     source = get_source(sid)
85     source.star.starred = False
86 
87 
88 @app.route('/col/add_star/<sid>', methods=('POST',))
89 def add_star(sid):
90     make_star_true(sid)
91     db_session.commit()
92     return redirect(url_for('index'))
93 
94 
95 @app.route("/col/remove_star/<sid>", methods=('POST',))
96 def remove_star(sid):
97     make_star_false(sid)
98     db_session.commit()
99     return redirect(url_for('index'))
100 
101 
102 @app.route('/')
103 def index():
104     unstarred = []
105     starred = []
106     for source in Source.query.filter_by(pending=False).order_by(Source.last_updated.desc()).all():
107         star = SourceStar.query.filter(SourceStar.source_id == source.id).first()
108         if star and star.starred:
109             starred.append(source)
110         else:
111             unstarred.append(source)
112         source.num_unread = len(
113             Submission.query.filter(Submission.source_id == source.id, Submission.downloaded == False).all())
114 
115     return render_template('index.html', unstarred=unstarred, starred=starred)
116 
117 
118 @app.route('/col/<sid>')
119 def col(sid):
120     source = get_source(sid)
121     docs = get_docs(sid)
122     submissions = [submission.filename for submission in Submission.query.filter(Submission.source_id == source.id).all()]
123     # Only include documents loaded from the filesystem which are replies or which are also listed in the
124     # submissions table to avoid displaying partially uploaded files (#561).
125     docs = [doc for doc in docs if doc['name'] in submissions or doc['name'].endswith('reply.gpg')]
126 
127     haskey = crypto_util.getkey(sid)
128     return render_template("col.html", sid=sid,
129                            codename=source.journalist_designation, docs=docs, haskey=haskey,
130                            flagged=source.flagged)
131 
132 
133 def delete_collection(source_id):
134     # Delete the source's collection of submissions
135     store.delete_source_directory(source_id)
136 
137     # Delete the source's reply keypair
138     crypto_util.delete_reply_keypair(source_id)
139 
140     # Delete their entry in the db
141     source = get_source(source_id)
142     db_session.delete(source)
143     db_session.commit()
144 
145 
146 @app.route('/col/process', methods=('POST',))
147 def col_process():
148     actions = {'delete': col_delete, 'star': col_star, 'un-star': col_un_star}
149     if 'cols_selected' not in request.form:
150         return redirect(url_for('index'))
151 
152     cols_selected = request.form.getlist('cols_selected') # getlist is cgi.FieldStorage.getlist
153     action = request.form['action']
154 
155     if action not in actions:
156         return abort(500)
157 
158     method = actions[action]
159     return method(cols_selected)
160 
161 
162 def col_star(cols_selected):
163     for sid in cols_selected:
164         make_star_true(sid)
165 
166     db_session.commit()
167     return redirect(url_for('index'))
168 
169 
170 def col_un_star(cols_selected):
171     for source_id in cols_selected:
172         make_star_false(source_id)
173 
174     db_session.commit()
175     return redirect(url_for('index'))
176 
177 
178 @app.route('/col/delete/<sid>', methods=('POST',))
179 def col_delete_single(sid):
180     """deleting a single collection from its /col page"""
181     source = get_source(sid)
182     delete_collection(sid)
183     flash("%s's collection deleted" % (source.journalist_designation,), "notification")
184     return redirect(url_for('index'))
185 
186 
187 def col_delete(cols_selected):
188     """deleting multiple collections from the index"""
189     if len(cols_selected) < 1:
190         flash("No collections selected to delete!", "error")
191     else:
192         for source_id in cols_selected:
193             delete_collection(source_id)
194         flash("%s %s deleted" % (
195             len(cols_selected),
196             "collection" if len(cols_selected) == 1 else "collections"
197         ), "notification")
198 
199     return redirect(url_for('index'))
200 
201 
202 @app.route('/col/<sid>/<fn>')
203 def doc(sid, fn):
204     if '..' in fn or fn.startswith('/'):
205         abort(404)
206     try:
207         Submission.query.filter(Submission.filename == fn).one().downloaded = True
208     except NoResultFound as e:
209         app.logger.error("Could not mark " + fn + " as downloaded: %s" % (e,))
210     db_session.commit()
211     return send_file(store.path(sid, fn), mimetype="application/pgp-encrypted")
212 
213 
214 @app.route('/reply', methods=('POST',))
215 def reply():
216     msg = request.form['msg']
217     g.source.interaction_count += 1
218     filename = "{0}-reply.gpg".format(g.source.interaction_count)
219 
220     crypto_util.encrypt(crypto_util.getkey(g.sid), msg, output=
221     store.path(g.sid, filename))
222 
223     db_session.commit()
224     return render_template('reply.html', sid=g.sid,
225                            codename=g.source.journalist_designation)
226 
227 
228 @app.route('/regenerate-code', methods=('POST',))
229 def generate_code():
230     original_journalist_designation = g.source.journalist_designation
231     g.source.journalist_designation = crypto_util.display_id()
232     
233     for doc in Submission.query.filter(Submission.source_id == g.source.id).all():
234         doc.filename = store.rename_submission(g.sid, doc.filename, g.source.journalist_filename())
235     db_session.commit()
236 
237     flash("The source '%s' has been renamed to '%s'" % (original_journalist_designation, g.source.journalist_designation), "notification")
238     return redirect('/col/' + g.sid)
239 
240 
241 @app.route('/download_unread/<sid>')
242 def download_unread(sid):
243     id = Source.query.filter(Source.filesystem_id == sid).one().id
244     docs = [doc.filename for doc in
245             Submission.query.filter(Submission.source_id == id, Submission.downloaded == False).all()]
246     return bulk_download(sid, docs)
247 
248 
249 @app.route('/bulk', methods=('POST',))
250 def bulk():
251     action = request.form['action']
252 
253     doc_names_selected = request.form.getlist('doc_names_selected')
254     docs_selected = [
255         doc for doc in get_docs(g.sid) if doc['name'] in doc_names_selected]
256     filenames_selected = [
257         doc['name'] for doc in docs_selected]
258 
259     if not docs_selected:
260         if action == 'download':
261             flash("No collections selected to download!", "error")
262         elif action == 'delete':
263             flash("No collections selected to delete!", "error")
264         return redirect(url_for('col', sid=g.sid))
265 
266     if action == 'download':
267         return bulk_download(g.sid, filenames_selected)
268     elif action == 'delete':
269         return bulk_delete(g.sid, docs_selected)
270     else:
271         abort(400)
272 
273 
274 def bulk_delete(sid, docs_selected):
275     source = get_source(sid)
276     confirm_delete = bool(request.form.get('confirm_delete', False))
277     if confirm_delete:
278         for doc in docs_selected:
279             db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())
280             fn = store.path(sid, doc['name'])
281             store.secure_unlink(fn)
282         db_session.commit()
283     return render_template('delete.html', sid=sid,
284                            codename=source.journalist_designation,
285                            docs_selected=docs_selected, confirm_delete=confirm_delete)
286 
287 
288 def bulk_download(sid, docs_selected):
289     source = get_source(sid)
290     filenames = []
291     for doc in docs_selected:
292         filenames.append(store.path(sid, doc))
293         try:
294             Submission.query.filter(Submission.filename == doc).one().downloaded = True
295         except NoResultFound as e:
296             app.logger.error("Could not mark " + doc + " as downloaded: %s" % (e,))
297     db_session.commit()
298     zip = store.get_bulk_archive(filenames, zip_directory=source.journalist_filename())
299     return send_file(zip.name, mimetype="application/zip",
300                      attachment_filename=source.journalist_filename() + ".zip",
301                      as_attachment=True)
302 
303 
304 @app.route('/flag', methods=('POST',))
305 def flag():
306     g.source.flagged = True
307     db_session.commit()
308     return render_template('flag.html', sid=g.sid,
309                            codename=g.source.journalist_designation)
310 def write_pidfile():
311     pid = str(os.getpid())
312     with open(config.JOURNALIST_PIDFILE, 'w') as fp:
313         fp.write(pid)
314 
315 if __name__ == "__main__":
316     write_pidfile()
317     # TODO make sure debug=False in production
318     app.run(debug=True, host='0.0.0.0', port=8081)
319
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/journalist.py b/securedrop/journalist.py
--- a/securedrop/journalist.py
+++ b/securedrop/journalist.py
@@ -276,7 +276,8 @@
     confirm_delete = bool(request.form.get('confirm_delete', False))
     if confirm_delete:
         for doc in docs_selected:
-            db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())
+            if not doc['name'].endswith('reply.gpg'):
+                db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())
             fn = store.path(sid, doc['name'])
             store.secure_unlink(fn)
         db_session.commit()

{"golden_diff": "diff --git a/securedrop/journalist.py b/securedrop/journalist.py\n--- a/securedrop/journalist.py\n+++ b/securedrop/journalist.py\n@@ -276,7 +276,8 @@\n     confirm_delete = bool(request.form.get('confirm_delete', False))\n     if confirm_delete:\n         for doc in docs_selected:\n-            db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())\n+            if not doc['name'].endswith('reply.gpg'):\n+                db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())\n             fn = store.path(sid, doc['name'])\n             store.secure_unlink(fn)\n         db_session.commit()\n", "issue": "Database error when trying to delete replies in the journalist interface\nAn error is thrown deleting replies in the journalist interface. An attempt is made to remove a record for the reply from the database but replies are only recorded on the filesystem.\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport config\nimport version\nimport crypto_util\nimport store\nimport template_filters\nfrom db import db_session, Source, Submission, SourceStar, get_one_or_else\n\nimport os\nfrom datetime import datetime\nfrom flask import (Flask, request, render_template, send_file, redirect, flash, url_for, g, abort)\nfrom flask_wtf.csrf import CsrfProtect\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\n\nimport background\n\napp = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR)\napp.config.from_object(config.JournalistInterfaceFlaskConfig)\nCsrfProtect(app)\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\n\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef get_source(sid):\n    \"\"\"Return a Source object, representing the database row, for the source\n    with id `sid`\"\"\"\n    source = None\n    query = Source.query.filter(Source.filesystem_id == sid)\n    source = get_one_or_else(query, app.logger, abort)\n\n    return source\n\n\n@app.before_request\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    if request.method == 'POST':\n        sid = request.form.get('sid')\n        if sid:\n            g.sid = sid\n            g.source = get_source(sid)\n\n\ndef get_docs(sid):\n    \"\"\"Get docs associated with source id `sid`, sorted by submission date\"\"\"\n    docs = []\n    for filename in os.listdir(store.path(sid)):\n        os_stat = os.stat(store.path(sid, filename))\n        docs.append(dict(\n            name=filename,\n            date=datetime.fromtimestamp(os_stat.st_mtime),\n            size=os_stat.st_size,\n        ))\n    # sort in chronological order\n    docs.sort(key=lambda x: int(x['name'].split('-')[0]))\n    return docs\n\n\ndef make_star_true(sid):\n    source = get_source(sid)\n    if source.star:\n        source.star.starred = True\n    else:\n        source_star = SourceStar(source)\n        db_session.add(source_star)\n\n\ndef make_star_false(sid):\n    source = get_source(sid)\n    source.star.starred = False\n\n\n@app.route('/col/add_star/<sid>', methods=('POST',))\ndef add_star(sid):\n    make_star_true(sid)\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\n@app.route(\"/col/remove_star/<sid>\", methods=('POST',))\ndef remove_star(sid):\n    make_star_false(sid)\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\n@app.route('/')\ndef index():\n    unstarred = []\n    starred = []\n    for source in Source.query.filter_by(pending=False).order_by(Source.last_updated.desc()).all():\n        star = SourceStar.query.filter(SourceStar.source_id == source.id).first()\n        if star and star.starred:\n            starred.append(source)\n        else:\n            unstarred.append(source)\n        source.num_unread = len(\n            Submission.query.filter(Submission.source_id == source.id, Submission.downloaded == False).all())\n\n    return render_template('index.html', unstarred=unstarred, starred=starred)\n\n\n@app.route('/col/<sid>')\ndef col(sid):\n    source = get_source(sid)\n    docs = get_docs(sid)\n    submissions = [submission.filename for submission in Submission.query.filter(Submission.source_id == source.id).all()]\n    # Only include documents loaded from the filesystem which are replies or which are also listed in the\n    # submissions table to avoid displaying partially uploaded files (#561).\n    docs = [doc for doc in docs if doc['name'] in submissions or doc['name'].endswith('reply.gpg')]\n\n    haskey = crypto_util.getkey(sid)\n    return render_template(\"col.html\", sid=sid,\n                           codename=source.journalist_designation, docs=docs, haskey=haskey,\n                           flagged=source.flagged)\n\n\ndef delete_collection(source_id):\n    # Delete the source's collection of submissions\n    store.delete_source_directory(source_id)\n\n    # Delete the source's reply keypair\n    crypto_util.delete_reply_keypair(source_id)\n\n    # Delete their entry in the db\n    source = get_source(source_id)\n    db_session.delete(source)\n    db_session.commit()\n\n\n@app.route('/col/process', methods=('POST',))\ndef col_process():\n    actions = {'delete': col_delete, 'star': col_star, 'un-star': col_un_star}\n    if 'cols_selected' not in request.form:\n        return redirect(url_for('index'))\n\n    cols_selected = request.form.getlist('cols_selected') # getlist is cgi.FieldStorage.getlist\n    action = request.form['action']\n\n    if action not in actions:\n        return abort(500)\n\n    method = actions[action]\n    return method(cols_selected)\n\n\ndef col_star(cols_selected):\n    for sid in cols_selected:\n        make_star_true(sid)\n\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\ndef col_un_star(cols_selected):\n    for source_id in cols_selected:\n        make_star_false(source_id)\n\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\n@app.route('/col/delete/<sid>', methods=('POST',))\ndef col_delete_single(sid):\n    \"\"\"deleting a single collection from its /col page\"\"\"\n    source = get_source(sid)\n    delete_collection(sid)\n    flash(\"%s's collection deleted\" % (source.journalist_designation,), \"notification\")\n    return redirect(url_for('index'))\n\n\ndef col_delete(cols_selected):\n    \"\"\"deleting multiple collections from the index\"\"\"\n    if len(cols_selected) < 1:\n        flash(\"No collections selected to delete!\", \"error\")\n    else:\n        for source_id in cols_selected:\n            delete_collection(source_id)\n        flash(\"%s %s deleted\" % (\n            len(cols_selected),\n            \"collection\" if len(cols_selected) == 1 else \"collections\"\n        ), \"notification\")\n\n    return redirect(url_for('index'))\n\n\n@app.route('/col/<sid>/<fn>')\ndef doc(sid, fn):\n    if '..' in fn or fn.startswith('/'):\n        abort(404)\n    try:\n        Submission.query.filter(Submission.filename == fn).one().downloaded = True\n    except NoResultFound as e:\n        app.logger.error(\"Could not mark \" + fn + \" as downloaded: %s\" % (e,))\n    db_session.commit()\n    return send_file(store.path(sid, fn), mimetype=\"application/pgp-encrypted\")\n\n\n@app.route('/reply', methods=('POST',))\ndef reply():\n    msg = request.form['msg']\n    g.source.interaction_count += 1\n    filename = \"{0}-reply.gpg\".format(g.source.interaction_count)\n\n    crypto_util.encrypt(crypto_util.getkey(g.sid), msg, output=\n    store.path(g.sid, filename))\n\n    db_session.commit()\n    return render_template('reply.html', sid=g.sid,\n                           codename=g.source.journalist_designation)\n\n\n@app.route('/regenerate-code', methods=('POST',))\ndef generate_code():\n    original_journalist_designation = g.source.journalist_designation\n    g.source.journalist_designation = crypto_util.display_id()\n    \n    for doc in Submission.query.filter(Submission.source_id == g.source.id).all():\n        doc.filename = store.rename_submission(g.sid, doc.filename, g.source.journalist_filename())\n    db_session.commit()\n\n    flash(\"The source '%s' has been renamed to '%s'\" % (original_journalist_designation, g.source.journalist_designation), \"notification\")\n    return redirect('/col/' + g.sid)\n\n\n@app.route('/download_unread/<sid>')\ndef download_unread(sid):\n    id = Source.query.filter(Source.filesystem_id == sid).one().id\n    docs = [doc.filename for doc in\n            Submission.query.filter(Submission.source_id == id, Submission.downloaded == False).all()]\n    return bulk_download(sid, docs)\n\n\n@app.route('/bulk', methods=('POST',))\ndef bulk():\n    action = request.form['action']\n\n    doc_names_selected = request.form.getlist('doc_names_selected')\n    docs_selected = [\n        doc for doc in get_docs(g.sid) if doc['name'] in doc_names_selected]\n    filenames_selected = [\n        doc['name'] for doc in docs_selected]\n\n    if not docs_selected:\n        if action == 'download':\n            flash(\"No collections selected to download!\", \"error\")\n        elif action == 'delete':\n            flash(\"No collections selected to delete!\", \"error\")\n        return redirect(url_for('col', sid=g.sid))\n\n    if action == 'download':\n        return bulk_download(g.sid, filenames_selected)\n    elif action == 'delete':\n        return bulk_delete(g.sid, docs_selected)\n    else:\n        abort(400)\n\n\ndef bulk_delete(sid, docs_selected):\n    source = get_source(sid)\n    confirm_delete = bool(request.form.get('confirm_delete', False))\n    if confirm_delete:\n        for doc in docs_selected:\n            db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())\n            fn = store.path(sid, doc['name'])\n            store.secure_unlink(fn)\n        db_session.commit()\n    return render_template('delete.html', sid=sid,\n                           codename=source.journalist_designation,\n                           docs_selected=docs_selected, confirm_delete=confirm_delete)\n\n\ndef bulk_download(sid, docs_selected):\n    source = get_source(sid)\n    filenames = []\n    for doc in docs_selected:\n        filenames.append(store.path(sid, doc))\n        try:\n            Submission.query.filter(Submission.filename == doc).one().downloaded = True\n        except NoResultFound as e:\n            app.logger.error(\"Could not mark \" + doc + \" as downloaded: %s\" % (e,))\n    db_session.commit()\n    zip = store.get_bulk_archive(filenames, zip_directory=source.journalist_filename())\n    return send_file(zip.name, mimetype=\"application/zip\",\n                     attachment_filename=source.journalist_filename() + \".zip\",\n                     as_attachment=True)\n\n\n@app.route('/flag', methods=('POST',))\ndef flag():\n    g.source.flagged = True\n    db_session.commit()\n    return render_template('flag.html', sid=g.sid,\n                           codename=g.source.journalist_designation)\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.JOURNALIST_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug=False in production\n    app.run(debug=True, host='0.0.0.0', port=8081)\n", "path": "securedrop/journalist.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport config\nimport version\nimport crypto_util\nimport store\nimport template_filters\nfrom db import db_session, Source, Submission, SourceStar, get_one_or_else\n\nimport os\nfrom datetime import datetime\nfrom flask import (Flask, request, render_template, send_file, redirect, flash, url_for, g, abort)\nfrom flask_wtf.csrf import CsrfProtect\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\n\nimport background\n\napp = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR)\napp.config.from_object(config.JournalistInterfaceFlaskConfig)\nCsrfProtect(app)\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\n\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef get_source(sid):\n    \"\"\"Return a Source object, representing the database row, for the source\n    with id `sid`\"\"\"\n    source = None\n    query = Source.query.filter(Source.filesystem_id == sid)\n    source = get_one_or_else(query, app.logger, abort)\n\n    return source\n\n\n@app.before_request\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    if request.method == 'POST':\n        sid = request.form.get('sid')\n        if sid:\n            g.sid = sid\n            g.source = get_source(sid)\n\n\ndef get_docs(sid):\n    \"\"\"Get docs associated with source id `sid`, sorted by submission date\"\"\"\n    docs = []\n    for filename in os.listdir(store.path(sid)):\n        os_stat = os.stat(store.path(sid, filename))\n        docs.append(dict(\n            name=filename,\n            date=datetime.fromtimestamp(os_stat.st_mtime),\n            size=os_stat.st_size,\n        ))\n    # sort in chronological order\n    docs.sort(key=lambda x: int(x['name'].split('-')[0]))\n    return docs\n\n\ndef make_star_true(sid):\n    source = get_source(sid)\n    if source.star:\n        source.star.starred = True\n    else:\n        source_star = SourceStar(source)\n        db_session.add(source_star)\n\n\ndef make_star_false(sid):\n    source = get_source(sid)\n    source.star.starred = False\n\n\n@app.route('/col/add_star/<sid>', methods=('POST',))\ndef add_star(sid):\n    make_star_true(sid)\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\n@app.route(\"/col/remove_star/<sid>\", methods=('POST',))\ndef remove_star(sid):\n    make_star_false(sid)\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\n@app.route('/')\ndef index():\n    unstarred = []\n    starred = []\n    for source in Source.query.filter_by(pending=False).order_by(Source.last_updated.desc()).all():\n        star = SourceStar.query.filter(SourceStar.source_id == source.id).first()\n        if star and star.starred:\n            starred.append(source)\n        else:\n            unstarred.append(source)\n        source.num_unread = len(\n            Submission.query.filter(Submission.source_id == source.id, Submission.downloaded == False).all())\n\n    return render_template('index.html', unstarred=unstarred, starred=starred)\n\n\n@app.route('/col/<sid>')\ndef col(sid):\n    source = get_source(sid)\n    docs = get_docs(sid)\n    submissions = [submission.filename for submission in Submission.query.filter(Submission.source_id == source.id).all()]\n    # Only include documents loaded from the filesystem which are replies or which are also listed in the\n    # submissions table to avoid displaying partially uploaded files (#561).\n    docs = [doc for doc in docs if doc['name'] in submissions or doc['name'].endswith('reply.gpg')]\n\n    haskey = crypto_util.getkey(sid)\n    return render_template(\"col.html\", sid=sid,\n                           codename=source.journalist_designation, docs=docs, haskey=haskey,\n                           flagged=source.flagged)\n\n\ndef delete_collection(source_id):\n    # Delete the source's collection of submissions\n    store.delete_source_directory(source_id)\n\n    # Delete the source's reply keypair\n    crypto_util.delete_reply_keypair(source_id)\n\n    # Delete their entry in the db\n    source = get_source(source_id)\n    db_session.delete(source)\n    db_session.commit()\n\n\n@app.route('/col/process', methods=('POST',))\ndef col_process():\n    actions = {'delete': col_delete, 'star': col_star, 'un-star': col_un_star}\n    if 'cols_selected' not in request.form:\n        return redirect(url_for('index'))\n\n    cols_selected = request.form.getlist('cols_selected') # getlist is cgi.FieldStorage.getlist\n    action = request.form['action']\n\n    if action not in actions:\n        return abort(500)\n\n    method = actions[action]\n    return method(cols_selected)\n\n\ndef col_star(cols_selected):\n    for sid in cols_selected:\n        make_star_true(sid)\n\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\ndef col_un_star(cols_selected):\n    for source_id in cols_selected:\n        make_star_false(source_id)\n\n    db_session.commit()\n    return redirect(url_for('index'))\n\n\n@app.route('/col/delete/<sid>', methods=('POST',))\ndef col_delete_single(sid):\n    \"\"\"deleting a single collection from its /col page\"\"\"\n    source = get_source(sid)\n    delete_collection(sid)\n    flash(\"%s's collection deleted\" % (source.journalist_designation,), \"notification\")\n    return redirect(url_for('index'))\n\n\ndef col_delete(cols_selected):\n    \"\"\"deleting multiple collections from the index\"\"\"\n    if len(cols_selected) < 1:\n        flash(\"No collections selected to delete!\", \"error\")\n    else:\n        for source_id in cols_selected:\n            delete_collection(source_id)\n        flash(\"%s %s deleted\" % (\n            len(cols_selected),\n            \"collection\" if len(cols_selected) == 1 else \"collections\"\n        ), \"notification\")\n\n    return redirect(url_for('index'))\n\n\n@app.route('/col/<sid>/<fn>')\ndef doc(sid, fn):\n    if '..' in fn or fn.startswith('/'):\n        abort(404)\n    try:\n        Submission.query.filter(Submission.filename == fn).one().downloaded = True\n    except NoResultFound as e:\n        app.logger.error(\"Could not mark \" + fn + \" as downloaded: %s\" % (e,))\n    db_session.commit()\n    return send_file(store.path(sid, fn), mimetype=\"application/pgp-encrypted\")\n\n\n@app.route('/reply', methods=('POST',))\ndef reply():\n    msg = request.form['msg']\n    g.source.interaction_count += 1\n    filename = \"{0}-reply.gpg\".format(g.source.interaction_count)\n\n    crypto_util.encrypt(crypto_util.getkey(g.sid), msg, output=\n    store.path(g.sid, filename))\n\n    db_session.commit()\n    return render_template('reply.html', sid=g.sid,\n                           codename=g.source.journalist_designation)\n\n\n@app.route('/regenerate-code', methods=('POST',))\ndef generate_code():\n    original_journalist_designation = g.source.journalist_designation\n    g.source.journalist_designation = crypto_util.display_id()\n    \n    for doc in Submission.query.filter(Submission.source_id == g.source.id).all():\n        doc.filename = store.rename_submission(g.sid, doc.filename, g.source.journalist_filename())\n    db_session.commit()\n\n    flash(\"The source '%s' has been renamed to '%s'\" % (original_journalist_designation, g.source.journalist_designation), \"notification\")\n    return redirect('/col/' + g.sid)\n\n\n@app.route('/download_unread/<sid>')\ndef download_unread(sid):\n    id = Source.query.filter(Source.filesystem_id == sid).one().id\n    docs = [doc.filename for doc in\n            Submission.query.filter(Submission.source_id == id, Submission.downloaded == False).all()]\n    return bulk_download(sid, docs)\n\n\n@app.route('/bulk', methods=('POST',))\ndef bulk():\n    action = request.form['action']\n\n    doc_names_selected = request.form.getlist('doc_names_selected')\n    docs_selected = [\n        doc for doc in get_docs(g.sid) if doc['name'] in doc_names_selected]\n    filenames_selected = [\n        doc['name'] for doc in docs_selected]\n\n    if not docs_selected:\n        if action == 'download':\n            flash(\"No collections selected to download!\", \"error\")\n        elif action == 'delete':\n            flash(\"No collections selected to delete!\", \"error\")\n        return redirect(url_for('col', sid=g.sid))\n\n    if action == 'download':\n        return bulk_download(g.sid, filenames_selected)\n    elif action == 'delete':\n        return bulk_delete(g.sid, docs_selected)\n    else:\n        abort(400)\n\n\ndef bulk_delete(sid, docs_selected):\n    source = get_source(sid)\n    confirm_delete = bool(request.form.get('confirm_delete', False))\n    if confirm_delete:\n        for doc in docs_selected:\n            if not doc['name'].endswith('reply.gpg'):\n                db_session.delete(Submission.query.filter(Submission.filename == doc['name']).one())\n            fn = store.path(sid, doc['name'])\n            store.secure_unlink(fn)\n        db_session.commit()\n    return render_template('delete.html', sid=sid,\n                           codename=source.journalist_designation,\n                           docs_selected=docs_selected, confirm_delete=confirm_delete)\n\n\ndef bulk_download(sid, docs_selected):\n    source = get_source(sid)\n    filenames = []\n    for doc in docs_selected:\n        filenames.append(store.path(sid, doc))\n        try:\n            Submission.query.filter(Submission.filename == doc).one().downloaded = True\n        except NoResultFound as e:\n            app.logger.error(\"Could not mark \" + doc + \" as downloaded: %s\" % (e,))\n    db_session.commit()\n    zip = store.get_bulk_archive(filenames, zip_directory=source.journalist_filename())\n    return send_file(zip.name, mimetype=\"application/zip\",\n                     attachment_filename=source.journalist_filename() + \".zip\",\n                     as_attachment=True)\n\n\n@app.route('/flag', methods=('POST',))\ndef flag():\n    g.source.flagged = True\n    db_session.commit()\n    return render_template('flag.html', sid=g.sid,\n                           codename=g.source.journalist_designation)\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.JOURNALIST_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug=False in production\n    app.run(debug=True, host='0.0.0.0', port=8081)\n", "path": "securedrop/journalist.py"}]}