sipa/blueprints/news.py

Summary

Maintainability
A
1 hr
Test Coverage
"""
Blueprint providing features regarding the news entries.
"""
import typing as t
from operator import attrgetter
from traceback import format_exception_only

from flask import (
    Blueprint,
    abort,
    current_app,
    render_template,
    request,
    render_template_string,
)
from flask_flatpages import Page

from sipa.flatpages import CategorizedFlatPages, Article

bp_news = Blueprint('news', __name__, url_prefix='/news')


@bp_news.route("/")
def show():
    """Get all markdown files from 'content/news/', parse them and put
    them in a list for the template.
    The formatting of these files is described in the readme.
    """
    start = request.args.get('start', None, int)
    end = request.args.get('end', None, int)
    cf_pages = current_app.cf_pages
    news = sorted(
        (article for article in cf_pages.get_articles_of_category('news')
         if hasattr(article, 'date')),
        key=attrgetter('date'),
        reverse=True,
    )
    if len(news) == 0:
        return render_template(
            "news.html", articles=None, previous_range=0, next_range=0
        )

    default_step = 10
    # calculating mod len() allows things like `end=-1` for the last
    # article(s).  this may lead to confusing behaviour because this
    # allows values out of the range (|val|≥len(latest)), but this
    # will only result in displaying articles instead of throwing an
    # error.  Apart from that, such values would just appear if edited
    # manually.
    if start is None:
        if end is None:
            start, end = 0, default_step
        else:
            end %= len(news)
            start = max(end - default_step + 1, 0)
    else:
        start %= len(news)
        if end is None:
            end = min(start + default_step - 1, len(news) - 1)
        else:
            end %= len(news)

    delta = end - start + 1
    prev_range, next_range = None, None

    if start > 0:
        prev_range = {'start': max(start - delta, 0), 'end': start - 1}
    if end < len(news) - 1:
        next_range = {'start': end + 1, 'end': min(end + delta, len(news) - 1)}

    return render_template(
        "news.html",
        articles=news[start : end + 1],
        previous_range=prev_range,
        next_range=next_range,
    )


@bp_news.route("/<filename>")
def show_news(filename):
    news = current_app.cf_pages.get_articles_of_category('news')

    for article in news:
        if article.file_basename == filename:
            return render_template("news.html", articles=[article])

    abort(404)


def try_get_content(cf_pages: CategorizedFlatPages, filename: str) -> str:
    """Reconstructs the content of a news article from the given filename."""
    news = cf_pages.get_articles_of_category("news")
    article = next((a for a in news if a.file_basename == filename), None)
    if not article:
        return ""
    assert isinstance(article, Article)
    p = article.localized_page
    # need to reconstruct actual content; only have access to parsed form
    return p._meta + "\n\n" + p.body


@bp_news.route("/edit")
@bp_news.route("/<filename>/edit")
def edit(filename: str | None = None):
    return render_template(
        "news_edit.html", content=try_get_content(current_app.cf_pages, filename)
    )


@bp_news.route("/preview", methods=["GET", "POST"])
def preview():
    article = request.form.get("article-content") or request.args.get("article-content")
    if article is None:
        abort(400)

    flatpages = t.cast(CategorizedFlatPages, current_app.cf_pages).flat_pages
    page = t.cast(Page, flatpages._parse(content=article, path="…", rel_path="…"))
    try:
        return render_template_string(
            '{% import "macros/article.html" as m %} {{ m.render_news(page) }}',
            page=page,
        )
    except Exception as e:
        return render_template_string(
            """
                <div class="alert alert-danger" role='alert'>
                    <h4 class="alert-heading">Error</h4>
                    <small>
                        <pre><code>{{ backtrace }}</code></pre>
                    </small>
                </div>
            """,
            backtrace=("\n".join(format_exception_only(e))),
        )