We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
1.8.0 translation feedback: say replies are saved, not stored
## Description

From [AO](https://weblate.securedrop.org/translate/securedrop/securedrop/en/?checksum=a6f950dfa57047f3): "Your reply has been saved." might be better than "Your reply has been stored."


--- 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_app/main.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 from datetime import datetime
3 from typing import Union
4 
5 import werkzeug
6 from flask import (Blueprint, request, current_app, session, url_for, redirect,
7                    render_template, g, flash, abort, Markup, escape)
8 from flask_babel import gettext
9 from sqlalchemy.orm import joinedload
10 from sqlalchemy.sql import func
11 
12 import store
13 
14 from db import db
15 from models import SeenReply, Source, SourceStar, Submission, Reply
16 from journalist_app.forms import ReplyForm
17 from journalist_app.utils import (validate_user, bulk_delete, download,
18                                   confirm_bulk_delete, get_source)
19 from sdconfig import SDConfig
20 
21 
22 def make_blueprint(config: SDConfig) -> Blueprint:
23     view = Blueprint('main', __name__)
24 
25     @view.route('/login', methods=('GET', 'POST'))
26     def login() -> Union[str, werkzeug.Response]:
27         if request.method == 'POST':
28             user = validate_user(request.form['username'],
29                                  request.form['password'],
30                                  request.form['token'])
31             if user:
32                 current_app.logger.info("'{}' logged in with the two-factor code {}"
33                                         .format(request.form['username'],
34                                                 request.form['token']))
35 
36                 # Update access metadata
37                 user.last_access = datetime.utcnow()
38                 db.session.add(user)
39                 db.session.commit()
40 
41                 session['uid'] = user.id
42                 session['nonce'] = user.session_nonce
43                 return redirect(url_for('main.index'))
44 
45         return render_template("login.html")
46 
47     @view.route('/logout')
48     def logout() -> werkzeug.Response:
49         session.pop('uid', None)
50         session.pop('expires', None)
51         session.pop('nonce', None)
52         return redirect(url_for('main.index'))
53 
54     @view.route("/")
55     def index() -> str:
56         # Gather the count of unread submissions for each source
57         # ID. This query will be joined in the queries for starred and
58         # unstarred sources below, and the unread counts added to
59         # their result sets as an extra column.
60         unread_stmt = (
61             db.session.query(Submission.source_id, func.count("*").label("num_unread"))
62             .filter_by(seen_files=None, seen_messages=None)
63             .group_by(Submission.source_id)
64             .subquery()
65         )
66 
67         # Query for starred sources, along with their unread
68         # submission counts.
69         starred = (
70             db.session.query(Source, unread_stmt.c.num_unread)
71             .filter_by(pending=False, deleted_at=None)
72             .filter(Source.last_updated.isnot(None))
73             .filter(SourceStar.starred.is_(True))
74             .outerjoin(SourceStar)
75             .options(joinedload(Source.submissions))
76             .options(joinedload(Source.star))
77             .outerjoin(unread_stmt, Source.id == unread_stmt.c.source_id)
78             .order_by(Source.last_updated.desc())
79             .all()
80         )
81 
82         # Now, add "num_unread" attributes to the source entities.
83         for source, num_unread in starred:
84             source.num_unread = num_unread or 0
85         starred = [source for source, num_unread in starred]
86 
87         # Query for sources without stars, along with their unread
88         # submission counts.
89         unstarred = (
90             db.session.query(Source, unread_stmt.c.num_unread)
91             .filter_by(pending=False, deleted_at=None)
92             .filter(Source.last_updated.isnot(None))
93             .filter(~Source.star.has(SourceStar.starred.is_(True)))
94             .options(joinedload(Source.submissions))
95             .options(joinedload(Source.star))
96             .outerjoin(unread_stmt, Source.id == unread_stmt.c.source_id)
97             .order_by(Source.last_updated.desc())
98             .all()
99         )
100 
101         # Again, add "num_unread" attributes to the source entities.
102         for source, num_unread in unstarred:
103             source.num_unread = num_unread or 0
104         unstarred = [source for source, num_unread in unstarred]
105 
106         response = render_template("index.html", unstarred=unstarred, starred=starred)
107         return response
108 
109     @view.route('/reply', methods=('POST',))
110     def reply() -> werkzeug.Response:
111         """Attempt to send a Reply from a Journalist to a Source. Empty
112         messages are rejected, and an informative error message is flashed
113         on the client. In the case of unexpected errors involving database
114         transactions (potentially caused by racing request threads that
115         modify the same the database object) logging is done in such a way
116         so as not to write potentially sensitive information to disk, and a
117         generic error message is flashed on the client.
118 
119         Returns:
120            flask.Response: The user is redirected to the same Source
121                collection view, regardless if the Reply is created
122                successfully.
123         """
124         form = ReplyForm()
125         if not form.validate_on_submit():
126             for error in form.message.errors:
127                 flash(error, "error")
128             return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
129 
130         g.source.interaction_count += 1
131         filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count,
132                                               g.source.journalist_filename)
133         current_app.crypto_util.encrypt(
134             form.message.data,
135             [current_app.crypto_util.get_fingerprint(g.filesystem_id),
136              config.JOURNALIST_KEY],
137             output=current_app.storage.path(g.filesystem_id, filename),
138         )
139 
140         try:
141             reply = Reply(g.user, g.source, filename)
142             db.session.add(reply)
143             db.session.flush()
144             seen_reply = SeenReply(reply_id=reply.id, journalist_id=g.user.id)
145             db.session.add(seen_reply)
146             db.session.commit()
147             store.async_add_checksum_for_file(reply)
148         except Exception as exc:
149             flash(gettext(
150                 "An unexpected error occurred! Please "
151                 "inform your admin."), "error")
152             # We take a cautious approach to logging here because we're dealing
153             # with responses to sources. It's possible the exception message
154             # could contain information we don't want to write to disk.
155             current_app.logger.error(
156                 "Reply from '{}' (ID {}) failed: {}!".format(g.user.username,
157                                                              g.user.id,
158                                                              exc.__class__))
159         else:
160 
161             flash(
162                 Markup(
163                     "<b>{}</b> {}".format(
164                         # Translators: Precedes a message confirming the success of an operation.
165                         escape(gettext("Success!")),
166                         escape(gettext("Your reply has been stored."))
167                     )
168                 ), 'success')
169         finally:
170             return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
171 
172     @view.route('/bulk', methods=('POST',))
173     def bulk() -> Union[str, werkzeug.Response]:
174         action = request.form['action']
175         error_redirect = url_for('col.col', filesystem_id=g.filesystem_id)
176         doc_names_selected = request.form.getlist('doc_names_selected')
177         selected_docs = [doc for doc in g.source.collection
178                          if doc.filename in doc_names_selected]
179         if selected_docs == []:
180             if action == 'download':
181                 flash(
182                     Markup(
183                         "<b>{}</b> {}".format(
184                             # Translators: Error shown when a user has not selected items to act on.
185                             escape(gettext("Nothing Selected")),
186                             escape(gettext("You must select one or more items for download"))
187                         )
188                     ), 'error')
189             elif action in ('delete', 'confirm_delete'):
190                 flash(
191                     Markup(
192                         "<b>{}</b> {}".format(
193                             # Translators: Error shown when a user has not selected items to act on.
194                             escape(gettext("Nothing Selected")),
195                             escape(gettext("You must select one or more items for deletion"))
196                         )
197                     ), 'error')
198 
199             return redirect(error_redirect)
200 
201         if action == 'download':
202             source = get_source(g.filesystem_id)
203             return download(
204                 source.journalist_filename, selected_docs, on_error_redirect=error_redirect
205             )
206         elif action == 'delete':
207             return bulk_delete(g.filesystem_id, selected_docs)
208         elif action == 'confirm_delete':
209             return confirm_bulk_delete(g.filesystem_id, selected_docs)
210         else:
211             abort(400)
212 
213     @view.route('/download_unread/<filesystem_id>')
214     def download_unread_filesystem_id(filesystem_id: str) -> werkzeug.Response:
215         unseen_submissions = (
216             Submission.query.join(Source)
217             .filter(
218                 Source.deleted_at.is_(None),
219                 Source.filesystem_id == filesystem_id
220             )
221             .filter(~Submission.seen_files.any(), ~Submission.seen_messages.any())
222             .all()
223         )
224         if len(unseen_submissions) == 0:
225             flash(gettext("No unread submissions for this source."), "error")
226             return redirect(url_for('col.col', filesystem_id=filesystem_id))
227         source = get_source(filesystem_id)
228         return download(source.journalist_filename, unseen_submissions)
229 
230     return view
231
```
--- 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_app/main.py b/securedrop/journalist_app/main.py
--- a/securedrop/journalist_app/main.py
+++ b/securedrop/journalist_app/main.py
@@ -163,7 +163,8 @@
                     "<b>{}</b> {}".format(
                         # Translators: Precedes a message confirming the success of an operation.
                         escape(gettext("Success!")),
-                        escape(gettext("Your reply has been stored."))
+                        escape(gettext("The source will receive your reply "
+                                       "next time they log in."))
                     )
                 ), 'success')
         finally:

{"golden_diff": "diff --git a/securedrop/journalist_app/main.py b/securedrop/journalist_app/main.py\n--- a/securedrop/journalist_app/main.py\n+++ b/securedrop/journalist_app/main.py\n@@ -163,7 +163,8 @@\n                     \"<b>{}</b> {}\".format(\n                         # Translators: Precedes a message confirming the success of an operation.\n                         escape(gettext(\"Success!\")),\n-                        escape(gettext(\"Your reply has been stored.\"))\n+                        escape(gettext(\"The source will receive your reply \"\n+                                       \"next time they log in.\"))\n                     )\n                 ), 'success')\n         finally:\n", "issue": "1.8.0 translation feedback: say replies are saved, not stored\n## Description\r\n\r\nFrom [AO](https://weblate.securedrop.org/translate/securedrop/securedrop/en/?checksum=a6f950dfa57047f3): \"Your reply has been saved.\" might be better than \"Your reply has been stored.\"\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\nfrom typing import Union\n\nimport werkzeug\nfrom flask import (Blueprint, request, current_app, session, url_for, redirect,\n                   render_template, g, flash, abort, Markup, escape)\nfrom flask_babel import gettext\nfrom sqlalchemy.orm import joinedload\nfrom sqlalchemy.sql import func\n\nimport store\n\nfrom db import db\nfrom models import SeenReply, Source, SourceStar, Submission, Reply\nfrom journalist_app.forms import ReplyForm\nfrom journalist_app.utils import (validate_user, bulk_delete, download,\n                                  confirm_bulk_delete, get_source)\nfrom sdconfig import SDConfig\n\n\ndef make_blueprint(config: SDConfig) -> Blueprint:\n    view = Blueprint('main', __name__)\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login() -> Union[str, werkzeug.Response]:\n        if request.method == 'POST':\n            user = validate_user(request.form['username'],\n                                 request.form['password'],\n                                 request.form['token'])\n            if user:\n                current_app.logger.info(\"'{}' logged in with the two-factor code {}\"\n                                        .format(request.form['username'],\n                                                request.form['token']))\n\n                # Update access metadata\n                user.last_access = datetime.utcnow()\n                db.session.add(user)\n                db.session.commit()\n\n                session['uid'] = user.id\n                session['nonce'] = user.session_nonce\n                return redirect(url_for('main.index'))\n\n        return render_template(\"login.html\")\n\n    @view.route('/logout')\n    def logout() -> werkzeug.Response:\n        session.pop('uid', None)\n        session.pop('expires', None)\n        session.pop('nonce', None)\n        return redirect(url_for('main.index'))\n\n    @view.route(\"/\")\n    def index() -> str:\n        # Gather the count of unread submissions for each source\n        # ID. This query will be joined in the queries for starred and\n        # unstarred sources below, and the unread counts added to\n        # their result sets as an extra column.\n        unread_stmt = (\n            db.session.query(Submission.source_id, func.count(\"*\").label(\"num_unread\"))\n            .filter_by(seen_files=None, seen_messages=None)\n            .group_by(Submission.source_id)\n            .subquery()\n        )\n\n        # Query for starred sources, along with their unread\n        # submission counts.\n        starred = (\n            db.session.query(Source, unread_stmt.c.num_unread)\n            .filter_by(pending=False, deleted_at=None)\n            .filter(Source.last_updated.isnot(None))\n            .filter(SourceStar.starred.is_(True))\n            .outerjoin(SourceStar)\n            .options(joinedload(Source.submissions))\n            .options(joinedload(Source.star))\n            .outerjoin(unread_stmt, Source.id == unread_stmt.c.source_id)\n            .order_by(Source.last_updated.desc())\n            .all()\n        )\n\n        # Now, add \"num_unread\" attributes to the source entities.\n        for source, num_unread in starred:\n            source.num_unread = num_unread or 0\n        starred = [source for source, num_unread in starred]\n\n        # Query for sources without stars, along with their unread\n        # submission counts.\n        unstarred = (\n            db.session.query(Source, unread_stmt.c.num_unread)\n            .filter_by(pending=False, deleted_at=None)\n            .filter(Source.last_updated.isnot(None))\n            .filter(~Source.star.has(SourceStar.starred.is_(True)))\n            .options(joinedload(Source.submissions))\n            .options(joinedload(Source.star))\n            .outerjoin(unread_stmt, Source.id == unread_stmt.c.source_id)\n            .order_by(Source.last_updated.desc())\n            .all()\n        )\n\n        # Again, add \"num_unread\" attributes to the source entities.\n        for source, num_unread in unstarred:\n            source.num_unread = num_unread or 0\n        unstarred = [source for source, num_unread in unstarred]\n\n        response = render_template(\"index.html\", unstarred=unstarred, starred=starred)\n        return response\n\n    @view.route('/reply', methods=('POST',))\n    def reply() -> werkzeug.Response:\n        \"\"\"Attempt to send a Reply from a Journalist to a Source. Empty\n        messages are rejected, and an informative error message is flashed\n        on the client. In the case of unexpected errors involving database\n        transactions (potentially caused by racing request threads that\n        modify the same the database object) logging is done in such a way\n        so as not to write potentially sensitive information to disk, and a\n        generic error message is flashed on the client.\n\n        Returns:\n           flask.Response: The user is redirected to the same Source\n               collection view, regardless if the Reply is created\n               successfully.\n        \"\"\"\n        form = ReplyForm()\n        if not form.validate_on_submit():\n            for error in form.message.errors:\n                flash(error, \"error\")\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n        g.source.interaction_count += 1\n        filename = \"{0}-{1}-reply.gpg\".format(g.source.interaction_count,\n                                              g.source.journalist_filename)\n        current_app.crypto_util.encrypt(\n            form.message.data,\n            [current_app.crypto_util.get_fingerprint(g.filesystem_id),\n             config.JOURNALIST_KEY],\n            output=current_app.storage.path(g.filesystem_id, filename),\n        )\n\n        try:\n            reply = Reply(g.user, g.source, filename)\n            db.session.add(reply)\n            db.session.flush()\n            seen_reply = SeenReply(reply_id=reply.id, journalist_id=g.user.id)\n            db.session.add(seen_reply)\n            db.session.commit()\n            store.async_add_checksum_for_file(reply)\n        except Exception as exc:\n            flash(gettext(\n                \"An unexpected error occurred! Please \"\n                \"inform your admin.\"), \"error\")\n            # We take a cautious approach to logging here because we're dealing\n            # with responses to sources. It's possible the exception message\n            # could contain information we don't want to write to disk.\n            current_app.logger.error(\n                \"Reply from '{}' (ID {}) failed: {}!\".format(g.user.username,\n                                                             g.user.id,\n                                                             exc.__class__))\n        else:\n\n            flash(\n                Markup(\n                    \"<b>{}</b> {}\".format(\n                        # Translators: Precedes a message confirming the success of an operation.\n                        escape(gettext(\"Success!\")),\n                        escape(gettext(\"Your reply has been stored.\"))\n                    )\n                ), 'success')\n        finally:\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n    @view.route('/bulk', methods=('POST',))\n    def bulk() -> Union[str, werkzeug.Response]:\n        action = request.form['action']\n        error_redirect = url_for('col.col', filesystem_id=g.filesystem_id)\n        doc_names_selected = request.form.getlist('doc_names_selected')\n        selected_docs = [doc for doc in g.source.collection\n                         if doc.filename in doc_names_selected]\n        if selected_docs == []:\n            if action == 'download':\n                flash(\n                    Markup(\n                        \"<b>{}</b> {}\".format(\n                            # Translators: Error shown when a user has not selected items to act on.\n                            escape(gettext(\"Nothing Selected\")),\n                            escape(gettext(\"You must select one or more items for download\"))\n                        )\n                    ), 'error')\n            elif action in ('delete', 'confirm_delete'):\n                flash(\n                    Markup(\n                        \"<b>{}</b> {}\".format(\n                            # Translators: Error shown when a user has not selected items to act on.\n                            escape(gettext(\"Nothing Selected\")),\n                            escape(gettext(\"You must select one or more items for deletion\"))\n                        )\n                    ), 'error')\n\n            return redirect(error_redirect)\n\n        if action == 'download':\n            source = get_source(g.filesystem_id)\n            return download(\n                source.journalist_filename, selected_docs, on_error_redirect=error_redirect\n            )\n        elif action == 'delete':\n            return bulk_delete(g.filesystem_id, selected_docs)\n        elif action == 'confirm_delete':\n            return confirm_bulk_delete(g.filesystem_id, selected_docs)\n        else:\n            abort(400)\n\n    @view.route('/download_unread/<filesystem_id>')\n    def download_unread_filesystem_id(filesystem_id: str) -> werkzeug.Response:\n        unseen_submissions = (\n            Submission.query.join(Source)\n            .filter(\n                Source.deleted_at.is_(None),\n                Source.filesystem_id == filesystem_id\n            )\n            .filter(~Submission.seen_files.any(), ~Submission.seen_messages.any())\n            .all()\n        )\n        if len(unseen_submissions) == 0:\n            flash(gettext(\"No unread submissions for this source.\"), \"error\")\n            return redirect(url_for('col.col', filesystem_id=filesystem_id))\n        source = get_source(filesystem_id)\n        return download(source.journalist_filename, unseen_submissions)\n\n    return view\n", "path": "securedrop/journalist_app/main.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nfrom datetime import datetime\nfrom typing import Union\n\nimport werkzeug\nfrom flask import (Blueprint, request, current_app, session, url_for, redirect,\n                   render_template, g, flash, abort, Markup, escape)\nfrom flask_babel import gettext\nfrom sqlalchemy.orm import joinedload\nfrom sqlalchemy.sql import func\n\nimport store\n\nfrom db import db\nfrom models import SeenReply, Source, SourceStar, Submission, Reply\nfrom journalist_app.forms import ReplyForm\nfrom journalist_app.utils import (validate_user, bulk_delete, download,\n                                  confirm_bulk_delete, get_source)\nfrom sdconfig import SDConfig\n\n\ndef make_blueprint(config: SDConfig) -> Blueprint:\n    view = Blueprint('main', __name__)\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login() -> Union[str, werkzeug.Response]:\n        if request.method == 'POST':\n            user = validate_user(request.form['username'],\n                                 request.form['password'],\n                                 request.form['token'])\n            if user:\n                current_app.logger.info(\"'{}' logged in with the two-factor code {}\"\n                                        .format(request.form['username'],\n                                                request.form['token']))\n\n                # Update access metadata\n                user.last_access = datetime.utcnow()\n                db.session.add(user)\n                db.session.commit()\n\n                session['uid'] = user.id\n                session['nonce'] = user.session_nonce\n                return redirect(url_for('main.index'))\n\n        return render_template(\"login.html\")\n\n    @view.route('/logout')\n    def logout() -> werkzeug.Response:\n        session.pop('uid', None)\n        session.pop('expires', None)\n        session.pop('nonce', None)\n        return redirect(url_for('main.index'))\n\n    @view.route(\"/\")\n    def index() -> str:\n        # Gather the count of unread submissions for each source\n        # ID. This query will be joined in the queries for starred and\n        # unstarred sources below, and the unread counts added to\n        # their result sets as an extra column.\n        unread_stmt = (\n            db.session.query(Submission.source_id, func.count(\"*\").label(\"num_unread\"))\n            .filter_by(seen_files=None, seen_messages=None)\n            .group_by(Submission.source_id)\n            .subquery()\n        )\n\n        # Query for starred sources, along with their unread\n        # submission counts.\n        starred = (\n            db.session.query(Source, unread_stmt.c.num_unread)\n            .filter_by(pending=False, deleted_at=None)\n            .filter(Source.last_updated.isnot(None))\n            .filter(SourceStar.starred.is_(True))\n            .outerjoin(SourceStar)\n            .options(joinedload(Source.submissions))\n            .options(joinedload(Source.star))\n            .outerjoin(unread_stmt, Source.id == unread_stmt.c.source_id)\n            .order_by(Source.last_updated.desc())\n            .all()\n        )\n\n        # Now, add \"num_unread\" attributes to the source entities.\n        for source, num_unread in starred:\n            source.num_unread = num_unread or 0\n        starred = [source for source, num_unread in starred]\n\n        # Query for sources without stars, along with their unread\n        # submission counts.\n        unstarred = (\n            db.session.query(Source, unread_stmt.c.num_unread)\n            .filter_by(pending=False, deleted_at=None)\n            .filter(Source.last_updated.isnot(None))\n            .filter(~Source.star.has(SourceStar.starred.is_(True)))\n            .options(joinedload(Source.submissions))\n            .options(joinedload(Source.star))\n            .outerjoin(unread_stmt, Source.id == unread_stmt.c.source_id)\n            .order_by(Source.last_updated.desc())\n            .all()\n        )\n\n        # Again, add \"num_unread\" attributes to the source entities.\n        for source, num_unread in unstarred:\n            source.num_unread = num_unread or 0\n        unstarred = [source for source, num_unread in unstarred]\n\n        response = render_template(\"index.html\", unstarred=unstarred, starred=starred)\n        return response\n\n    @view.route('/reply', methods=('POST',))\n    def reply() -> werkzeug.Response:\n        \"\"\"Attempt to send a Reply from a Journalist to a Source. Empty\n        messages are rejected, and an informative error message is flashed\n        on the client. In the case of unexpected errors involving database\n        transactions (potentially caused by racing request threads that\n        modify the same the database object) logging is done in such a way\n        so as not to write potentially sensitive information to disk, and a\n        generic error message is flashed on the client.\n\n        Returns:\n           flask.Response: The user is redirected to the same Source\n               collection view, regardless if the Reply is created\n               successfully.\n        \"\"\"\n        form = ReplyForm()\n        if not form.validate_on_submit():\n            for error in form.message.errors:\n                flash(error, \"error\")\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n        g.source.interaction_count += 1\n        filename = \"{0}-{1}-reply.gpg\".format(g.source.interaction_count,\n                                              g.source.journalist_filename)\n        current_app.crypto_util.encrypt(\n            form.message.data,\n            [current_app.crypto_util.get_fingerprint(g.filesystem_id),\n             config.JOURNALIST_KEY],\n            output=current_app.storage.path(g.filesystem_id, filename),\n        )\n\n        try:\n            reply = Reply(g.user, g.source, filename)\n            db.session.add(reply)\n            db.session.flush()\n            seen_reply = SeenReply(reply_id=reply.id, journalist_id=g.user.id)\n            db.session.add(seen_reply)\n            db.session.commit()\n            store.async_add_checksum_for_file(reply)\n        except Exception as exc:\n            flash(gettext(\n                \"An unexpected error occurred! Please \"\n                \"inform your admin.\"), \"error\")\n            # We take a cautious approach to logging here because we're dealing\n            # with responses to sources. It's possible the exception message\n            # could contain information we don't want to write to disk.\n            current_app.logger.error(\n                \"Reply from '{}' (ID {}) failed: {}!\".format(g.user.username,\n                                                             g.user.id,\n                                                             exc.__class__))\n        else:\n\n            flash(\n                Markup(\n                    \"<b>{}</b> {}\".format(\n                        # Translators: Precedes a message confirming the success of an operation.\n                        escape(gettext(\"Success!\")),\n                        escape(gettext(\"The source will receive your reply \"\n                                       \"next time they log in.\"))\n                    )\n                ), 'success')\n        finally:\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n    @view.route('/bulk', methods=('POST',))\n    def bulk() -> Union[str, werkzeug.Response]:\n        action = request.form['action']\n        error_redirect = url_for('col.col', filesystem_id=g.filesystem_id)\n        doc_names_selected = request.form.getlist('doc_names_selected')\n        selected_docs = [doc for doc in g.source.collection\n                         if doc.filename in doc_names_selected]\n        if selected_docs == []:\n            if action == 'download':\n                flash(\n                    Markup(\n                        \"<b>{}</b> {}\".format(\n                            # Translators: Error shown when a user has not selected items to act on.\n                            escape(gettext(\"Nothing Selected\")),\n                            escape(gettext(\"You must select one or more items for download\"))\n                        )\n                    ), 'error')\n            elif action in ('delete', 'confirm_delete'):\n                flash(\n                    Markup(\n                        \"<b>{}</b> {}\".format(\n                            # Translators: Error shown when a user has not selected items to act on.\n                            escape(gettext(\"Nothing Selected\")),\n                            escape(gettext(\"You must select one or more items for deletion\"))\n                        )\n                    ), 'error')\n\n            return redirect(error_redirect)\n\n        if action == 'download':\n            source = get_source(g.filesystem_id)\n            return download(\n                source.journalist_filename, selected_docs, on_error_redirect=error_redirect\n            )\n        elif action == 'delete':\n            return bulk_delete(g.filesystem_id, selected_docs)\n        elif action == 'confirm_delete':\n            return confirm_bulk_delete(g.filesystem_id, selected_docs)\n        else:\n            abort(400)\n\n    @view.route('/download_unread/<filesystem_id>')\n    def download_unread_filesystem_id(filesystem_id: str) -> werkzeug.Response:\n        unseen_submissions = (\n            Submission.query.join(Source)\n            .filter(\n                Source.deleted_at.is_(None),\n                Source.filesystem_id == filesystem_id\n            )\n            .filter(~Submission.seen_files.any(), ~Submission.seen_messages.any())\n            .all()\n        )\n        if len(unseen_submissions) == 0:\n            flash(gettext(\"No unread submissions for this source.\"), \"error\")\n            return redirect(url_for('col.col', filesystem_id=filesystem_id))\n        source = get_source(filesystem_id)\n        return download(source.journalist_filename, unseen_submissions)\n\n    return view\n", "path": "securedrop/journalist_app/main.py"}]}