We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Sources with a null value for last_updated will break the web UI
## Description

If a source is created without a `last_updated` filed, it will cause the following error on the journalist index page.

```
172.17.0.1 - - [10/Oct/2018 18:49:40] "GET / HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_app/main.py", line 79, in index
    starred=starred)
  File "/usr/local/lib/python2.7/dist-packages/flask/templating.py", line 135, in render_template
    context, ctx.app)
  File "/usr/local/lib/python2.7/dist-packages/flask/templating.py", line 117, in _render
    rv = template.render(context)
  File "/usr/local/lib/python2.7/dist-packages/jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "/usr/local/lib/python2.7/dist-packages/jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/index.html", line 1, in top-level template code
    {% extends "base.html" %}
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/base.html", line 50, in top-level template code
    {% block body %}{% endblock %}
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/index.html", line 25, in block "body"
    {% include '_source_row.html' %}
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/_source_row.html", line 4, in top-level template code
    <time class="date" title="{{ source.last_updated|rel_datetime_format }}" datetime="{{ source.last_updated|rel_datetime_format(fmt="%Y-%m-%d %H:%M:%S%Z") }}">{{ source.last_updated|rel_datetime_format(relative=True) }}</time>
  File "/home/heartsucker/code/freedomofpress/securedrop/securedrop/template_filters.py", line 12, in rel_datetime_format
    time = dates.format_timedelta(datetime.utcnow() - dt,
TypeError: unsupported operand type(s) for -: 'datetime.datetime' and 'NoneType'
```

The root cause of this is the schema in `sqlite` permits null values in the `last_updated` columns.

```sql
CREATE TABLE sources (
	id INTEGER NOT NULL, 
	uuid VARCHAR(36) NOT NULL, 
	filesystem_id VARCHAR(96), 
	journalist_designation VARCHAR(255) NOT NULL, 
	flagged BOOLEAN, 
	last_updated DATETIME,
        -- ^ allows null :(
	pending BOOLEAN, 
	interaction_count INTEGER NOT NULL, 
	PRIMARY KEY (id), 
	UNIQUE (uuid), 
	UNIQUE (filesystem_id), 
	CHECK (flagged IN (0, 1)), 
	CHECK (pending IN (0, 1))
);
```

## Steps to Reproduce

Manually create a source with `last_updated = null`.

## Expected Behavior

The app should recover gracefully or (better) disallow this entirely.

## Actual Behavior

The app blows up.

--- 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 
3 import os
4 
5 from datetime import datetime
6 from flask import (Blueprint, request, current_app, session, url_for, redirect,
7                    render_template, g, flash, abort)
8 from flask_babel import gettext
9 from sqlalchemy.sql.expression import false
10 
11 from db import db
12 from models import Source, SourceStar, Submission, Reply
13 from journalist_app.forms import ReplyForm
14 from journalist_app.utils import (validate_user, bulk_delete, download,
15                                   confirm_bulk_delete, get_source)
16 
17 
18 def make_blueprint(config):
19     view = Blueprint('main', __name__)
20 
21     @view.route('/login', methods=('GET', 'POST'))
22     def login():
23         if request.method == 'POST':
24             user = validate_user(request.form['username'],
25                                  request.form['password'],
26                                  request.form['token'])
27             if user:
28                 current_app.logger.info("'{}' logged in with the token {}"
29                                         .format(request.form['username'],
30                                                 request.form['token']))
31 
32                 # Update access metadata
33                 user.last_access = datetime.utcnow()
34                 db.session.add(user)
35                 db.session.commit()
36 
37                 session['uid'] = user.id
38                 return redirect(url_for('main.index'))
39 
40         return render_template("login.html")
41 
42     @view.route('/logout')
43     def logout():
44         session.pop('uid', None)
45         session.pop('expires', None)
46         return redirect(url_for('main.index'))
47 
48     @view.route('/org-logo')
49     def select_logo():
50         if os.path.exists(os.path.join(current_app.static_folder, 'i',
51                           'custom_logo.png')):
52             return redirect(url_for('static', filename='i/custom_logo.png'))
53         else:
54             return redirect(url_for('static', filename='i/logo.png'))
55 
56     @view.route('/')
57     def index():
58         unstarred = []
59         starred = []
60 
61         # Long SQLAlchemy statements look best when formatted according to
62         # the Pocoo style guide, IMHO:
63         # http://www.pocoo.org/internal/styleguide/
64         sources = Source.query.filter_by(pending=False) \
65                               .order_by(Source.last_updated.desc()) \
66                               .all()
67         for source in sources:
68             star = SourceStar.query.filter_by(source_id=source.id).first()
69             if star and star.starred:
70                 starred.append(source)
71             else:
72                 unstarred.append(source)
73             source.num_unread = len(
74                 Submission.query.filter_by(source_id=source.id,
75                                            downloaded=False).all())
76 
77         return render_template('index.html',
78                                unstarred=unstarred,
79                                starred=starred)
80 
81     @view.route('/reply', methods=('POST',))
82     def reply():
83         """Attempt to send a Reply from a Journalist to a Source. Empty
84         messages are rejected, and an informative error message is flashed
85         on the client. In the case of unexpected errors involving database
86         transactions (potentially caused by racing request threads that
87         modify the same the database object) logging is done in such a way
88         so as not to write potentially sensitive information to disk, and a
89         generic error message is flashed on the client.
90 
91         Returns:
92            flask.Response: The user is redirected to the same Source
93                collection view, regardless if the Reply is created
94                successfully.
95         """
96         form = ReplyForm()
97         if not form.validate_on_submit():
98             for error in form.message.errors:
99                 flash(error, "error")
100             return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
101 
102         g.source.interaction_count += 1
103         filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count,
104                                               g.source.journalist_filename)
105         current_app.crypto_util.encrypt(
106             form.message.data,
107             [current_app.crypto_util.getkey(g.filesystem_id),
108              config.JOURNALIST_KEY],
109             output=current_app.storage.path(g.filesystem_id, filename),
110         )
111         reply = Reply(g.user, g.source, filename)
112 
113         try:
114             db.session.add(reply)
115             db.session.commit()
116         except Exception as exc:
117             flash(gettext(
118                 "An unexpected error occurred! Please "
119                 "inform your administrator."), "error")
120             # We take a cautious approach to logging here because we're dealing
121             # with responses to sources. It's possible the exception message
122             # could contain information we don't want to write to disk.
123             current_app.logger.error(
124                 "Reply from '{}' (ID {}) failed: {}!".format(g.user.username,
125                                                              g.user.id,
126                                                              exc.__class__))
127         else:
128             flash(gettext("Thanks. Your reply has been stored."),
129                   "notification")
130         finally:
131             return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
132 
133     @view.route('/flag', methods=('POST',))
134     def flag():
135         g.source.flagged = True
136         db.session.commit()
137         return render_template('flag.html', filesystem_id=g.filesystem_id,
138                                codename=g.source.journalist_designation)
139 
140     @view.route('/bulk', methods=('POST',))
141     def bulk():
142         action = request.form['action']
143 
144         doc_names_selected = request.form.getlist('doc_names_selected')
145         selected_docs = [doc for doc in g.source.collection
146                          if doc.filename in doc_names_selected]
147         if selected_docs == []:
148             if action == 'download':
149                 flash(gettext("No collections selected for download."),
150                       "error")
151             elif action in ('delete', 'confirm_delete'):
152                 flash(gettext("No collections selected for deletion."),
153                       "error")
154             return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
155 
156         if action == 'download':
157             source = get_source(g.filesystem_id)
158             return download(source.journalist_filename, selected_docs)
159         elif action == 'delete':
160             return bulk_delete(g.filesystem_id, selected_docs)
161         elif action == 'confirm_delete':
162             return confirm_bulk_delete(g.filesystem_id, selected_docs)
163         else:
164             abort(400)
165 
166     @view.route('/regenerate-code', methods=('POST',))
167     def regenerate_code():
168         original_journalist_designation = g.source.journalist_designation
169         g.source.journalist_designation = current_app.crypto_util.display_id()
170 
171         for item in g.source.collection:
172             item.filename = current_app.storage.rename_submission(
173                 g.filesystem_id,
174                 item.filename,
175                 g.source.journalist_filename)
176         db.session.commit()
177 
178         flash(gettext(
179             "The source '{original_name}' has been renamed to '{new_name}'")
180               .format(original_name=original_journalist_designation,
181                       new_name=g.source.journalist_designation),
182               "notification")
183         return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
184 
185     @view.route('/download_unread/<filesystem_id>')
186     def download_unread_filesystem_id(filesystem_id):
187         id = Source.query.filter(Source.filesystem_id == filesystem_id) \
188             .one().id
189         submissions = Submission.query.filter(
190             Submission.source_id == id,
191             Submission.downloaded == false()).all()
192         if submissions == []:
193             flash(gettext("No unread submissions for this source."))
194             return redirect(url_for('col.col', filesystem_id=filesystem_id))
195         source = get_source(filesystem_id)
196         return download(source.journalist_filename, submissions)
197 
198     return view
199
```
--- 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
@@ -62,6 +62,7 @@
         # the Pocoo style guide, IMHO:
         # http://www.pocoo.org/internal/styleguide/
         sources = Source.query.filter_by(pending=False) \
+                              .filter(Source.last_updated.isnot(None)) \
                               .order_by(Source.last_updated.desc()) \
                               .all()
         for source in sources:

{"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@@ -62,6 +62,7 @@\n         # the Pocoo style guide, IMHO:\n         # http://www.pocoo.org/internal/styleguide/\n         sources = Source.query.filter_by(pending=False) \\\n+                              .filter(Source.last_updated.isnot(None)) \\\n                               .order_by(Source.last_updated.desc()) \\\n                               .all()\n         for source in sources:\n", "issue": "Sources with a null value for last_updated will break the web UI\n## Description\r\n\r\nIf a source is created without a `last_updated` filed, it will cause the following error on the journalist index page.\r\n\r\n```\r\n172.17.0.1 - - [10/Oct/2018 18:49:40] \"GET / HTTP/1.1\" 500 -\r\nTraceback (most recent call last):\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 2309, in __call__\r\n    return self.wsgi_app(environ, start_response)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 2295, in wsgi_app\r\n    response = self.handle_exception(e)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 1741, in handle_exception\r\n    reraise(exc_type, exc_value, tb)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 2292, in wsgi_app\r\n    response = self.full_dispatch_request()\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 1815, in full_dispatch_request\r\n    rv = self.handle_user_exception(e)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 1718, in handle_user_exception\r\n    reraise(exc_type, exc_value, tb)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 1813, in full_dispatch_request\r\n    rv = self.dispatch_request()\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/app.py\", line 1799, in dispatch_request\r\n    return self.view_functions[rule.endpoint](**req.view_args)\r\n  File \"/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_app/main.py\", line 79, in index\r\n    starred=starred)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/templating.py\", line 135, in render_template\r\n    context, ctx.app)\r\n  File \"/usr/local/lib/python2.7/dist-packages/flask/templating.py\", line 117, in _render\r\n    rv = template.render(context)\r\n  File \"/usr/local/lib/python2.7/dist-packages/jinja2/environment.py\", line 1008, in render\r\n    return self.environment.handle_exception(exc_info, True)\r\n  File \"/usr/local/lib/python2.7/dist-packages/jinja2/environment.py\", line 780, in handle_exception\r\n    reraise(exc_type, exc_value, tb)\r\n  File \"/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/index.html\", line 1, in top-level template code\r\n    {% extends \"base.html\" %}\r\n  File \"/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/base.html\", line 50, in top-level template code\r\n    {% block body %}{% endblock %}\r\n  File \"/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/index.html\", line 25, in block \"body\"\r\n    {% include '_source_row.html' %}\r\n  File \"/home/heartsucker/code/freedomofpress/securedrop/securedrop/journalist_templates/_source_row.html\", line 4, in top-level template code\r\n    <time class=\"date\" title=\"{{ source.last_updated|rel_datetime_format }}\" datetime=\"{{ source.last_updated|rel_datetime_format(fmt=\"%Y-%m-%d %H:%M:%S%Z\") }}\">{{ source.last_updated|rel_datetime_format(relative=True) }}</time>\r\n  File \"/home/heartsucker/code/freedomofpress/securedrop/securedrop/template_filters.py\", line 12, in rel_datetime_format\r\n    time = dates.format_timedelta(datetime.utcnow() - dt,\r\nTypeError: unsupported operand type(s) for -: 'datetime.datetime' and 'NoneType'\r\n```\r\n\r\nThe root cause of this is the schema in `sqlite` permits null values in the `last_updated` columns.\r\n\r\n```sql\r\nCREATE TABLE sources (\r\n\tid INTEGER NOT NULL, \r\n\tuuid VARCHAR(36) NOT NULL, \r\n\tfilesystem_id VARCHAR(96), \r\n\tjournalist_designation VARCHAR(255) NOT NULL, \r\n\tflagged BOOLEAN, \r\n\tlast_updated DATETIME,\r\n        -- ^ allows null :(\r\n\tpending BOOLEAN, \r\n\tinteraction_count INTEGER NOT NULL, \r\n\tPRIMARY KEY (id), \r\n\tUNIQUE (uuid), \r\n\tUNIQUE (filesystem_id), \r\n\tCHECK (flagged IN (0, 1)), \r\n\tCHECK (pending IN (0, 1))\r\n);\r\n```\r\n\r\n## Steps to Reproduce\r\n\r\nManually create a source with `last_updated = null`.\r\n\r\n## Expected Behavior\r\n\r\nThe app should recover gracefully or (better) disallow this entirely.\r\n\r\n## Actual Behavior\r\n\r\nThe app blows up.\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\nimport os\n\nfrom datetime import datetime\nfrom flask import (Blueprint, request, current_app, session, url_for, redirect,\n                   render_template, g, flash, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.sql.expression import false\n\nfrom db import db\nfrom models import 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)\n\n\ndef make_blueprint(config):\n    view = Blueprint('main', __name__)\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login():\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 token {}\"\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                return redirect(url_for('main.index'))\n\n        return render_template(\"login.html\")\n\n    @view.route('/logout')\n    def logout():\n        session.pop('uid', None)\n        session.pop('expires', None)\n        return redirect(url_for('main.index'))\n\n    @view.route('/org-logo')\n    def select_logo():\n        if os.path.exists(os.path.join(current_app.static_folder, 'i',\n                          'custom_logo.png')):\n            return redirect(url_for('static', filename='i/custom_logo.png'))\n        else:\n            return redirect(url_for('static', filename='i/logo.png'))\n\n    @view.route('/')\n    def index():\n        unstarred = []\n        starred = []\n\n        # Long SQLAlchemy statements look best when formatted according to\n        # the Pocoo style guide, IMHO:\n        # http://www.pocoo.org/internal/styleguide/\n        sources = Source.query.filter_by(pending=False) \\\n                              .order_by(Source.last_updated.desc()) \\\n                              .all()\n        for source in sources:\n            star = SourceStar.query.filter_by(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_by(source_id=source.id,\n                                           downloaded=False).all())\n\n        return render_template('index.html',\n                               unstarred=unstarred,\n                               starred=starred)\n\n    @view.route('/reply', methods=('POST',))\n    def reply():\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.getkey(g.filesystem_id),\n             config.JOURNALIST_KEY],\n            output=current_app.storage.path(g.filesystem_id, filename),\n        )\n        reply = Reply(g.user, g.source, filename)\n\n        try:\n            db.session.add(reply)\n            db.session.commit()\n        except Exception as exc:\n            flash(gettext(\n                \"An unexpected error occurred! Please \"\n                \"inform your administrator.\"), \"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            flash(gettext(\"Thanks. Your reply has been stored.\"),\n                  \"notification\")\n        finally:\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n    @view.route('/flag', methods=('POST',))\n    def flag():\n        g.source.flagged = True\n        db.session.commit()\n        return render_template('flag.html', filesystem_id=g.filesystem_id,\n                               codename=g.source.journalist_designation)\n\n    @view.route('/bulk', methods=('POST',))\n    def bulk():\n        action = request.form['action']\n\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(gettext(\"No collections selected for download.\"),\n                      \"error\")\n            elif action in ('delete', 'confirm_delete'):\n                flash(gettext(\"No collections selected for deletion.\"),\n                      \"error\")\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n        if action == 'download':\n            source = get_source(g.filesystem_id)\n            return download(source.journalist_filename, selected_docs)\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('/regenerate-code', methods=('POST',))\n    def regenerate_code():\n        original_journalist_designation = g.source.journalist_designation\n        g.source.journalist_designation = current_app.crypto_util.display_id()\n\n        for item in g.source.collection:\n            item.filename = current_app.storage.rename_submission(\n                g.filesystem_id,\n                item.filename,\n                g.source.journalist_filename)\n        db.session.commit()\n\n        flash(gettext(\n            \"The source '{original_name}' has been renamed to '{new_name}'\")\n              .format(original_name=original_journalist_designation,\n                      new_name=g.source.journalist_designation),\n              \"notification\")\n        return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n    @view.route('/download_unread/<filesystem_id>')\n    def download_unread_filesystem_id(filesystem_id):\n        id = Source.query.filter(Source.filesystem_id == filesystem_id) \\\n            .one().id\n        submissions = Submission.query.filter(\n            Submission.source_id == id,\n            Submission.downloaded == false()).all()\n        if submissions == []:\n            flash(gettext(\"No unread submissions for this source.\"))\n            return redirect(url_for('col.col', filesystem_id=filesystem_id))\n        source = get_source(filesystem_id)\n        return download(source.journalist_filename, submissions)\n\n    return view\n", "path": "securedrop/journalist_app/main.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\nimport os\n\nfrom datetime import datetime\nfrom flask import (Blueprint, request, current_app, session, url_for, redirect,\n                   render_template, g, flash, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.sql.expression import false\n\nfrom db import db\nfrom models import 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)\n\n\ndef make_blueprint(config):\n    view = Blueprint('main', __name__)\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login():\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 token {}\"\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                return redirect(url_for('main.index'))\n\n        return render_template(\"login.html\")\n\n    @view.route('/logout')\n    def logout():\n        session.pop('uid', None)\n        session.pop('expires', None)\n        return redirect(url_for('main.index'))\n\n    @view.route('/org-logo')\n    def select_logo():\n        if os.path.exists(os.path.join(current_app.static_folder, 'i',\n                          'custom_logo.png')):\n            return redirect(url_for('static', filename='i/custom_logo.png'))\n        else:\n            return redirect(url_for('static', filename='i/logo.png'))\n\n    @view.route('/')\n    def index():\n        unstarred = []\n        starred = []\n\n        # Long SQLAlchemy statements look best when formatted according to\n        # the Pocoo style guide, IMHO:\n        # http://www.pocoo.org/internal/styleguide/\n        sources = Source.query.filter_by(pending=False) \\\n                              .filter(Source.last_updated.isnot(None)) \\\n                              .order_by(Source.last_updated.desc()) \\\n                              .all()\n        for source in sources:\n            star = SourceStar.query.filter_by(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_by(source_id=source.id,\n                                           downloaded=False).all())\n\n        return render_template('index.html',\n                               unstarred=unstarred,\n                               starred=starred)\n\n    @view.route('/reply', methods=('POST',))\n    def reply():\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.getkey(g.filesystem_id),\n             config.JOURNALIST_KEY],\n            output=current_app.storage.path(g.filesystem_id, filename),\n        )\n        reply = Reply(g.user, g.source, filename)\n\n        try:\n            db.session.add(reply)\n            db.session.commit()\n        except Exception as exc:\n            flash(gettext(\n                \"An unexpected error occurred! Please \"\n                \"inform your administrator.\"), \"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            flash(gettext(\"Thanks. Your reply has been stored.\"),\n                  \"notification\")\n        finally:\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n    @view.route('/flag', methods=('POST',))\n    def flag():\n        g.source.flagged = True\n        db.session.commit()\n        return render_template('flag.html', filesystem_id=g.filesystem_id,\n                               codename=g.source.journalist_designation)\n\n    @view.route('/bulk', methods=('POST',))\n    def bulk():\n        action = request.form['action']\n\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(gettext(\"No collections selected for download.\"),\n                      \"error\")\n            elif action in ('delete', 'confirm_delete'):\n                flash(gettext(\"No collections selected for deletion.\"),\n                      \"error\")\n            return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n        if action == 'download':\n            source = get_source(g.filesystem_id)\n            return download(source.journalist_filename, selected_docs)\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('/regenerate-code', methods=('POST',))\n    def regenerate_code():\n        original_journalist_designation = g.source.journalist_designation\n        g.source.journalist_designation = current_app.crypto_util.display_id()\n\n        for item in g.source.collection:\n            item.filename = current_app.storage.rename_submission(\n                g.filesystem_id,\n                item.filename,\n                g.source.journalist_filename)\n        db.session.commit()\n\n        flash(gettext(\n            \"The source '{original_name}' has been renamed to '{new_name}'\")\n              .format(original_name=original_journalist_designation,\n                      new_name=g.source.journalist_designation),\n              \"notification\")\n        return redirect(url_for('col.col', filesystem_id=g.filesystem_id))\n\n    @view.route('/download_unread/<filesystem_id>')\n    def download_unread_filesystem_id(filesystem_id):\n        id = Source.query.filter(Source.filesystem_id == filesystem_id) \\\n            .one().id\n        submissions = Submission.query.filter(\n            Submission.source_id == id,\n            Submission.downloaded == false()).all()\n        if submissions == []:\n            flash(gettext(\"No unread submissions for this source.\"))\n            return redirect(url_for('col.col', filesystem_id=filesystem_id))\n        source = get_source(filesystem_id)\n        return download(source.journalist_filename, submissions)\n\n    return view\n", "path": "securedrop/journalist_app/main.py"}]}