# Buggy code
def make_blueprint(config):
    view = Blueprint("main", __name__)

    @view.route("/login", methods=("GET", "POST"))
    def login():
        if request.method == "POST":
            user = validate_user(
                request.form["username"],
                request.form["password"],
                request.form["token"],
            )
            if user:
                current_app.logger.info(
                    "'{}' logged in with the token {}".format(
                        request.form["username"], request.form["token"]
                    )
                )

                # Update access metadata
                user.last_access = datetime.utcnow()
                db.session.add(user)
                db.session.commit()

                session["uid"] = user.id
                return redirect(url_for("main.index"))

        return render_template("login.html")

    @view.route("/logout")
    def logout():
        session.pop("uid", None)
        session.pop("expires", None)
        return redirect(url_for("main.index"))

    @view.route("/org-logo")
    def select_logo():
        if os.path.exists(
            os.path.join(current_app.static_folder, "i", "custom_logo.png")
        ):
            return redirect(url_for("static", filename="i/custom_logo.png"))
        else:
            return redirect(url_for("static", filename="i/logo.png"))

    @view.route("/")
    def index():
        unstarred = []
        starred = []

        # Long SQLAlchemy statements look best when formatted according to
        # the Pocoo style guide, IMHO:
        # http://www.pocoo.org/internal/styleguide/
        sources = (
            Source.query.filter_by(pending=False)
            .order_by(Source.last_updated.desc())
            .all()
        )
        for source in sources:
            star = SourceStar.query.filter_by(source_id=source.id).first()
            if star and star.starred:
                starred.append(source)
            else:
                unstarred.append(source)
            source.num_unread = len(
                Submission.query.filter_by(source_id=source.id, downloaded=False).all()
            )

        return render_template("index.html", unstarred=unstarred, starred=starred)

    @view.route("/reply", methods=("POST",))
    def reply():
        """Attempt to send a Reply from a Journalist to a Source. Empty
        messages are rejected, and an informative error message is flashed
        on the client. In the case of unexpected errors involving database
        transactions (potentially caused by racing request threads that
        modify the same the database object) logging is done in such a way
        so as not to write potentially sensitive information to disk, and a
        generic error message is flashed on the client.

        Returns:
           flask.Response: The user is redirected to the same Source
               collection view, regardless if the Reply is created
               successfully.
        """
        form = ReplyForm()
        if not form.validate_on_submit():
            for error in form.message.errors:
                flash(error, "error")
            return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

        g.source.interaction_count += 1
        filename = "{0}-{1}-reply.gpg".format(
            g.source.interaction_count, g.source.journalist_filename
        )
        current_app.crypto_util.encrypt(
            form.message.data,
            [current_app.crypto_util.getkey(g.filesystem_id), config.JOURNALIST_KEY],
            output=current_app.storage.path(g.filesystem_id, filename),
        )
        reply = Reply(g.user, g.source, filename)

        try:
            db.session.add(reply)
            db.session.commit()
        except Exception as exc:
            flash(
                gettext(
                    "An unexpected error occurred! Please inform your administrator."
                ),
                "error",
            )
            # We take a cautious approach to logging here because we're dealing
            # with responses to sources. It's possible the exception message
            # could contain information we don't want to write to disk.
            current_app.logger.error(
                "Reply from '{}' (ID {}) failed: {}!".format(
                    g.user.username, g.user.id, exc.__class__
                )
            )
        else:
            flash(gettext("Thanks. Your reply has been stored."), "notification")
        finally:
            return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

    @view.route("/flag", methods=("POST",))
    def flag():
        g.source.flagged = True
        db.session.commit()
        return render_template(
            "flag.html",
            filesystem_id=g.filesystem_id,
            codename=g.source.journalist_designation,
        )

    @view.route("/bulk", methods=("POST",))
    def bulk():
        action = request.form["action"]

        doc_names_selected = request.form.getlist("doc_names_selected")
        selected_docs = [
            doc for doc in g.source.collection if doc.filename in doc_names_selected
        ]
        if selected_docs == []:
            if action == "download":
                flash(gettext("No collections selected for download."), "error")
            elif action in ("delete", "confirm_delete"):
                flash(gettext("No collections selected for deletion."), "error")
            return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

        if action == "download":
            source = get_source(g.filesystem_id)
            return download(source.journalist_filename, selected_docs)
        elif action == "delete":
            return bulk_delete(g.filesystem_id, selected_docs)
        elif action == "confirm_delete":
            return confirm_bulk_delete(g.filesystem_id, selected_docs)
        else:
            abort(400)

    @view.route("/regenerate-code", methods=("POST",))
    def regenerate_code():
        original_journalist_designation = g.source.journalist_designation
        g.source.journalist_designation = current_app.crypto_util.display_id()

        for item in g.source.collection:
            item.filename = current_app.storage.rename_submission(
                g.filesystem_id, item.filename, g.source.journalist_filename
            )
        db.session.commit()

        flash(
            gettext(
                "The source '{original_name}' has been renamed to '{new_name}'"
            ).format(
                original_name=original_journalist_designation,
                new_name=g.source.journalist_designation,
            ),
            "notification",
        )
        return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

    @view.route("/download_unread/<filesystem_id>")
    def download_unread_filesystem_id(filesystem_id):
        id = Source.query.filter(Source.filesystem_id == filesystem_id).one().id
        submissions = Submission.query.filter(
            Submission.source_id == id, Submission.downloaded == false()
        ).all()
        if submissions == []:
            flash(gettext("No unread submissions for this source."))
            return redirect(url_for("col.col", filesystem_id=filesystem_id))
        source = get_source(filesystem_id)
        return download(source.journalist_filename, submissions)

    return view


# Traceback (TypeError)
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'

# Fixed code
def make_blueprint(config):
    view = Blueprint("main", __name__)

    @view.route("/login", methods=("GET", "POST"))
    def login():
        if request.method == "POST":
            user = validate_user(
                request.form["username"],
                request.form["password"],
                request.form["token"],
            )
            if user:
                current_app.logger.info(
                    "'{}' logged in with the token {}".format(
                        request.form["username"], request.form["token"]
                    )
                )

                # Update access metadata
                user.last_access = datetime.utcnow()
                db.session.add(user)
                db.session.commit()

                session["uid"] = user.id
                return redirect(url_for("main.index"))

        return render_template("login.html")

    @view.route("/logout")
    def logout():
        session.pop("uid", None)
        session.pop("expires", None)
        return redirect(url_for("main.index"))

    @view.route("/org-logo")
    def select_logo():
        if os.path.exists(
            os.path.join(current_app.static_folder, "i", "custom_logo.png")
        ):
            return redirect(url_for("static", filename="i/custom_logo.png"))
        else:
            return redirect(url_for("static", filename="i/logo.png"))

    @view.route("/")
    def index():
        unstarred = []
        starred = []

        # Long SQLAlchemy statements look best when formatted according to
        # 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:
            star = SourceStar.query.filter_by(source_id=source.id).first()
            if star and star.starred:
                starred.append(source)
            else:
                unstarred.append(source)
            source.num_unread = len(
                Submission.query.filter_by(source_id=source.id, downloaded=False).all()
            )

        return render_template("index.html", unstarred=unstarred, starred=starred)

    @view.route("/reply", methods=("POST",))
    def reply():
        """Attempt to send a Reply from a Journalist to a Source. Empty
        messages are rejected, and an informative error message is flashed
        on the client. In the case of unexpected errors involving database
        transactions (potentially caused by racing request threads that
        modify the same the database object) logging is done in such a way
        so as not to write potentially sensitive information to disk, and a
        generic error message is flashed on the client.

        Returns:
           flask.Response: The user is redirected to the same Source
               collection view, regardless if the Reply is created
               successfully.
        """
        form = ReplyForm()
        if not form.validate_on_submit():
            for error in form.message.errors:
                flash(error, "error")
            return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

        g.source.interaction_count += 1
        filename = "{0}-{1}-reply.gpg".format(
            g.source.interaction_count, g.source.journalist_filename
        )
        current_app.crypto_util.encrypt(
            form.message.data,
            [current_app.crypto_util.getkey(g.filesystem_id), config.JOURNALIST_KEY],
            output=current_app.storage.path(g.filesystem_id, filename),
        )
        reply = Reply(g.user, g.source, filename)

        try:
            db.session.add(reply)
            db.session.commit()
        except Exception as exc:
            flash(
                gettext(
                    "An unexpected error occurred! Please inform your administrator."
                ),
                "error",
            )
            # We take a cautious approach to logging here because we're dealing
            # with responses to sources. It's possible the exception message
            # could contain information we don't want to write to disk.
            current_app.logger.error(
                "Reply from '{}' (ID {}) failed: {}!".format(
                    g.user.username, g.user.id, exc.__class__
                )
            )
        else:
            flash(gettext("Thanks. Your reply has been stored."), "notification")
        finally:
            return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

    @view.route("/flag", methods=("POST",))
    def flag():
        g.source.flagged = True
        db.session.commit()
        return render_template(
            "flag.html",
            filesystem_id=g.filesystem_id,
            codename=g.source.journalist_designation,
        )

    @view.route("/bulk", methods=("POST",))
    def bulk():
        action = request.form["action"]

        doc_names_selected = request.form.getlist("doc_names_selected")
        selected_docs = [
            doc for doc in g.source.collection if doc.filename in doc_names_selected
        ]
        if selected_docs == []:
            if action == "download":
                flash(gettext("No collections selected for download."), "error")
            elif action in ("delete", "confirm_delete"):
                flash(gettext("No collections selected for deletion."), "error")
            return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

        if action == "download":
            source = get_source(g.filesystem_id)
            return download(source.journalist_filename, selected_docs)
        elif action == "delete":
            return bulk_delete(g.filesystem_id, selected_docs)
        elif action == "confirm_delete":
            return confirm_bulk_delete(g.filesystem_id, selected_docs)
        else:
            abort(400)

    @view.route("/regenerate-code", methods=("POST",))
    def regenerate_code():
        original_journalist_designation = g.source.journalist_designation
        g.source.journalist_designation = current_app.crypto_util.display_id()

        for item in g.source.collection:
            item.filename = current_app.storage.rename_submission(
                g.filesystem_id, item.filename, g.source.journalist_filename
            )
        db.session.commit()

        flash(
            gettext(
                "The source '{original_name}' has been renamed to '{new_name}'"
            ).format(
                original_name=original_journalist_designation,
                new_name=g.source.journalist_designation,
            ),
            "notification",
        )
        return redirect(url_for("col.col", filesystem_id=g.filesystem_id))

    @view.route("/download_unread/<filesystem_id>")
    def download_unread_filesystem_id(filesystem_id):
        id = Source.query.filter(Source.filesystem_id == filesystem_id).one().id
        submissions = Submission.query.filter(
            Submission.source_id == id, Submission.downloaded == false()
        ).all()
        if submissions == []:
            flash(gettext("No unread submissions for this source."))
            return redirect(url_for("col.col", filesystem_id=filesystem_id))
        source = get_source(filesystem_id)
        return download(source.journalist_filename, submissions)

    return view