Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: before_app_first_request deprecation #82

Merged
merged 7 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#
# This file is part of Invenio.
# Copyright (C) 2016-2024 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Add has_custom_view to pages."""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "9fae3c5404d9"
down_revision = "b0f93ca4a147"
branch_labels = ()
depends_on = None


def upgrade():
"""Upgrade database."""
# Drop the column
op.drop_column("pages_page", "has_custom_view")
op.drop_column("pages_page_version", "has_custom_view")
# ### end Alembic commands ###


def downgrade():
"""Downgrade database."""
# Add the new column
op.add_column(
"pages_page",
sa.Column(
"has_custom_view",
sa.Boolean(),
nullable=False,
server_default=sa.sql.expression.literal(False),
default=False,
),
)
op.add_column(
"pages_page_version",
sa.Column(
"has_custom_view",
sa.Boolean(),
nullable=True,
server_default=sa.sql.expression.literal(False),
default=False,
),
)
# ### end Alembic commands ###
8 changes: 6 additions & 2 deletions invenio_pages/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@
#
# This file is part of Invenio.
# Copyright (C) 2015-2022 CERN.
# Copyright (C) 2023-2024 Graz University of Technology.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Static pages module for Invenio."""

from flask import url_for
import sqlalchemy
from flask import request, url_for
from invenio_db import db
from jinja2.sandbox import SandboxedEnvironment
from werkzeug.exceptions import NotFound

from . import config
from .records.models import PageModel as Page
from .resources import PageResource, PageResourceConfig
from .services import PageService, PageServiceConfig
from .views import handle_not_found
from .views import add_url_rule, handle_not_found, render_page


class InvenioPages(object):
Expand Down
6 changes: 1 addition & 5 deletions invenio_pages/records/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ class PageModel(db.Model, Timestamp):
template_name = db.Column(db.String(70), nullable=False)
"""Page template name."""

has_custom_view = db.Column(db.Boolean(), nullable=False, default=False)
"""Page custom view flag."""

@classmethod
def create(self, data):
"""Create a new page."""
Expand All @@ -59,7 +56,6 @@ def create(self, data):
content=data.get("content", ""),
description=data.get("description", ""),
template_name=data["template_name"],
has_custom_view=data.get("has_custom_view", False),
)
db.session.add(obj)

Expand Down Expand Up @@ -147,7 +143,7 @@ def __repr__(self):
Used on Page admin view in inline model.
:returns: unambiguous page representation.
"""
return f"URL: {self.url}, title: {self.title}, has_custom_view: {self.has_custom_view}"
return f"URL: {self.url}, title: {self.title}"


class PageList(db.Model):
Expand Down
1 change: 0 additions & 1 deletion invenio_pages/services/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,5 @@ class PageSchema(Schema):
content = DynamicSanitizedHTML()
description = fields.String()
template_name = fields.String()
has_custom_view = fields.Boolean()
created = TZDateTime(timezone=timezone.utc, format="iso", dump_only=True)
updated = TZDateTime(timezone=timezone.utc, format="iso", dump_only=True)
92 changes: 45 additions & 47 deletions invenio_pages/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,26 @@
#
# This file is part of Invenio.
# Copyright (C) 2015-2022 CERN.
# Copyright (C) 2023-2024 Graz University of Technology.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Views for Pages module."""


from flask import Blueprint, abort, current_app, g, render_template, request
from invenio_db import db
from sqlalchemy.orm.exc import NoResultFound
from werkzeug.exceptions import NotFound

from invenio_pages.proxies import current_pages_service

from .proxies import current_pages_service
from .records.models import PageModel as Page

blueprint = Blueprint(
"invenio_pages", __name__, url_prefix="/", template_folder="templates"
)


@blueprint.before_app_first_request
def register_pages():
"""Register URL rules for all static pages in the application.

This function iterates over all static pages stored in the database and registers
their URL rules with the application. Pages with custom views are skipped to allow
default handling for pages without customizations.
"""
# We need to set the function view, to be able to directly register the urls in the Flask.url_map
current_app.view_functions["invenio_pages.view"] = view

for page in Page.query.all():
if not page.has_custom_view: # Skip registration of pages with custom view
_add_url_rule(page.url)


@blueprint.app_template_filter("render_string")
def render_string(source):
"""Render a string in sandboxed environment.
Expand All @@ -49,32 +32,6 @@ def render_string(source):
return current_app.extensions["invenio-pages"].render_template(source)


def view():
"""Public interface to the page view.

Models: `pages.pages`.
Templates: Uses the template defined by the ``template_name`` field
or ``pages/default.html`` if template_name is not defined.
Context: page `pages.pages` object.
"""
return render_page(request.path) # pragma: no cover


def render_page(path):
"""Internal interface to the page view.

:param path: Page path.
:returns: The rendered template.
"""
try:
page = current_pages_service.read_by_url(g.identity, request.path).to_dict()
except NoResultFound:
abort(404)
return render_template(
[page["template_name"], current_app.config["PAGES_DEFAULT_TEMPLATE"]], page=page
)


def handle_not_found(exception, **extra):
"""Custom blueprint exception handler."""
assert isinstance(exception, NotFound)
Expand All @@ -83,7 +40,7 @@ def handle_not_found(exception, **extra):
db.or_(Page.url == request.path, Page.url == request.path + "/")
).first()
if page:
_add_url_rule(page.url)
add_url_rule(page.url)
return render_template(
[page.template_name, current_app.config["PAGES_DEFAULT_TEMPLATE"]],
page=page,
Expand All @@ -94,7 +51,7 @@ def handle_not_found(exception, **extra):
return exception


def _add_url_rule(url):
def add_url_rule(url, app=None):
"""Register URL rule to application URL map."""
rule = current_app.url_rule_class(
url,
Expand All @@ -110,3 +67,44 @@ def create_pages_api_bp(app):
"""Create the pages resource api blueprint."""
ext = app.extensions["invenio-pages"]
return ext.pages_resource.as_blueprint()


def render_page(path, **template_ctx):
"""Internal interface to the page view.

:param path: Page path.
:param template_ctx: Passed to the rendered template.
:returns: The rendered template.
"""
try:
page = current_pages_service.read_by_url(g.identity, request.path).to_dict()
except NoResultFound:
abort(404)
return render_template(
[page["template_name"], current_app.config["PAGES_DEFAULT_TEMPLATE"]],
page=page,
**template_ctx,
)


def create_page_view(page_path):
"""Create a page view.

.. code-block:: python

app = Flask(__name__)
app.add_url_rule("/about", view_func=create_page_view("/about"))

"""

def _view():
"""Public interface to the page view.

Models: `pages.pages`.
Templates: Uses the template defined by the ``template_name`` field
or ``pages/default.html`` if template_name is not defined.
Context: page `pages.pages` object.
"""
return render_page(page_path)

return _view
42 changes: 1 addition & 41 deletions tests/records/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@
from invenio_pages import PageModel as Page
from invenio_pages.records.errors import PageNotCreatedError, PageNotFoundError
from invenio_pages.services.config import PageServiceConfig
from invenio_pages.views import register_pages


def test_page_repr(module_scoped_pages_fixture, base_app):
dog_page = Page.get_by_url("/dogs/shiba")
assert (
dog_page.__repr__()
== "URL: /dogs/shiba, title: Page for doge!, has_custom_view: False"
)
assert dog_page.__repr__() == "URL: /dogs/shiba, title: Page for doge!"


def test_page_versions(module_scoped_pages_fixture, base_app, db):
Expand Down Expand Up @@ -147,39 +143,3 @@ def test_delete_all(module_scoped_pages_fixture, base_app):
Page.delete_all()
pages = Page.search(map_search_params(PageServiceConfig.search, {}), [])
assert len(pages.items) == 0


def test_register_pages_with_custom_view(module_scoped_pages_fixture, base_app):
"""Test that URL is not registered if has_custom_view is True."""
# Create a page with has_custom_view set to True
data_with_custom_view = {
"url": "/custom-page-1",
"title": "Custom Page 1",
"content": "Content for Custom Page 1",
"template_name": "invenio_pages/default.html",
"has_custom_view": True,
}

# Create a page with has_custom_view set to False
data_without_custom_view = {
"url": "/custom-page-2",
"title": "Custom Page 2",
"content": "Content for Custom Page 2",
"template_name": "invenio_pages/default.html",
"has_custom_view": False,
}

# Create the pages
Page.create(data_with_custom_view)
Page.create(data_without_custom_view)

# Register pages
register_pages()

# Verify that the URL is not registered for the page with custom view set to True
assert not any(
rule.rule == "/custom-page-1" for rule in base_app.url_map.iter_rules()
)

# Verify that the URL is registered for the page with custom view set to False
assert any(rule.rule == "/custom-page-2" for rule in base_app.url_map.iter_rules())
2 changes: 0 additions & 2 deletions tests/resources/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def test_page_content(module_scoped_pages_fixture, base_client):
"content": "Generic dog.",
"id": "1",
"template_name": "invenio_pages/default.html",
"has_custom_view": False,
"links": {"self": "https://127.0.0.1:5000/api/pages/1"},
}
assert json == expected_data
Expand All @@ -48,7 +47,6 @@ def test_html_content(module_scoped_pages_fixture, base_client):
"content": "<h1>HTML aware dog.</h1>.\n" '<p class="test">paragraph<br /></p>',
"id": "4",
"template_name": "invenio_pages/default.html",
"has_custom_view": False,
"links": {"self": "https://127.0.0.1:5000/api/pages/4"},
}
assert json == expected_data
Expand Down
2 changes: 0 additions & 2 deletions tests/services/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def test_page_read(module_scoped_pages_fixture, simple_user_identity):
"content": "Generic dog.",
"id": "1",
"template_name": "invenio_pages/default.html",
"has_custom_view": False,
"links": {"self": "https://127.0.0.1:5000/api/pages/1"},
}
assert page == expected_data
Expand All @@ -51,7 +50,6 @@ def test_page_read_by_url(module_scoped_pages_fixture, simple_user_identity):
"content": "Generic dog.",
"id": "1",
"template_name": "invenio_pages/default.html",
"has_custom_view": False,
"links": {"self": "https://127.0.0.1:5000/api/pages/1"},
}
assert page == expected_data
Expand Down
2 changes: 0 additions & 2 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

"""Test views for Pages module."""

from unittest import mock

import pytest
from invenio_db import db
from jinja2.exceptions import UndefinedError
Expand Down
Loading