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

current user refresh #115

Merged
95 changes: 92 additions & 3 deletions oarepo_ui/resources/catalog.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import re
from collections import namedtuple
from functools import cached_property
from itertools import chain
from pathlib import Path
from typing import Dict, Tuple

import flask
import jinja2
from flask import current_app
from flask.globals import request
from jinjax import Catalog
from jinjax.exceptions import ComponentNotFound
from jinjax.jinjax import JinjaX

DEFAULT_URL_ROOT = "/static/components/"
ALLOWED_EXTENSIONS = (".css", ".js")
Expand All @@ -27,9 +31,94 @@
class OarepoCatalog(Catalog):
singleton_check = None

def __init__(self):
super().__init__()
self.jinja_env.undefined = jinja2.Undefined
def __init__(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to overwrite the constructor? It seems that the only line modified is 51 and that is done to pass the current_app.

The app is used on 101:
if name in self.jinja_env.app.template_context_processors

can it be changed to if name in current_app.template_context_processors ?

Also, we need tests for this functionality (register test blueprint with context processor and make sure it gets called)

self,
*,
globals: "dict[str, t.Any] | None" = None,
filters: "dict[str, t.Any] | None" = None,
tests: "dict[str, t.Any] | None" = None,
extensions: "list | None" = None,
jinja_env: "jinja2.Environment | None" = None,
root_url: str = DEFAULT_URL_ROOT,
file_ext: "TFileExt" = DEFAULT_EXTENSION,
use_cache: bool = True,
auto_reload: bool = True,
) -> None:
self.prefixes: "dict[str, jinja2.FileSystemLoader]" = {}
self.collected_css: "list[str]" = []
self.collected_js: "list[str]" = []
self.file_ext = file_ext
self.use_cache = use_cache
self.auto_reload = auto_reload

root_url = root_url.strip().rstrip(SLASH)
self.root_url = f"{root_url}{SLASH}"

env = flask.templating.Environment(undefined=jinja2.Undefined, app=current_app)
extensions = [*(extensions or []), "jinja2.ext.do", JinjaX]
globals = globals or {}
filters = filters or {}
tests = tests or {}

if jinja_env:
env.extensions.update(jinja_env.extensions)
globals.update(jinja_env.globals)
filters.update(jinja_env.filters)
tests.update(jinja_env.tests)
jinja_env.globals["catalog"] = self
jinja_env.filters["catalog"] = self

globals["catalog"] = self
filters["catalog"] = self

for ext in extensions:
env.add_extension(ext)
env.globals.update(globals)
env.filters.update(filters)
env.tests.update(tests)
env.extend(catalog=self)

self.jinja_env = env

self._cache: "dict[str, dict]" = {}

def update_template_context(self, context: dict) -> None:
"""Update the template context with some commonly used variables.
This injects request, session, config and g into the template
context as well as everything template context processors want
to inject. Note that the as of Flask 0.6, the original values
in the context will not be overridden if a context processor
decides to return a value with the same key.

:param context: the context as a dictionary that is updated in place
to add extra variables.
"""
names: t.Iterable[t.Optional[str]] = (None,)

# A template may be rendered outside a request context.
if request:
names = chain(names, reversed(request.blueprints))

# The values passed to render_template take precedence. Keep a
# copy to re-apply after all context functions.

for name in names:
if name in self.jinja_env.app.template_context_processors:
for func in self.jinja_env.app.template_context_processors[name]:
context.update(func())

def render(
self,
__name: str,
*,
caller: "t.Callable | None" = None,
**kw,
) -> str:
self.collected_css = []
self.collected_js = []
if "context" in kw:
self.update_template_context(kw["context"])
return self.irender(__name, caller=caller, **kw)

def get_source(self, cname: str, file_ext: "TFileExt" = "") -> str:
prefix, name = self._split_name(cname)
Expand Down
4 changes: 2 additions & 2 deletions oarepo_ui/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ def search_available_sort_options(self, api_config, identity):

def search_active_facets(self, api_config, identity):
"""Return list of active facets that will be displayed by search app.
By default, all facets are active but a repository can, for performance reasons,
display only a subset of facets.
By default, all facets are active but a repository can, for performance reasons,
display only a subset of facets.
"""
return list(self.search_available_facets(api_config, identity).keys())

Expand Down
19 changes: 14 additions & 5 deletions oarepo_ui/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@
# Resource
#
from ..proxies import current_oarepo_ui
from .config import RecordsUIResourceConfig, UIResourceConfig, TemplatePageUIResourceConfig
from .config import (
RecordsUIResourceConfig,
TemplatePageUIResourceConfig,
UIResourceConfig,
)

request_export_args = request_parser(
from_conf("request_export_args"), location="view_args"
Expand Down Expand Up @@ -174,6 +178,7 @@ def detail(self):
api_record=api_record,
extra_context=extra_context,
ui_links=ui_links,
context=current_oarepo_ui.catalog.jinja_env.globals,
)

def make_links_absolute(self, links, api_prefix):
Expand Down Expand Up @@ -253,6 +258,7 @@ def search(self):
ui_resource=self,
ui_links=ui_links,
extra_context=extra_context,
context=current_oarepo_ui.catalog.jinja_env.globals,
)

@request_read_args
Expand Down Expand Up @@ -348,6 +354,7 @@ def edit(self):
extra_context=extra_context,
ui_links=ui_links,
data=data,
context=current_oarepo_ui.catalog.jinja_env.globals,
)

@login_required
Expand Down Expand Up @@ -403,6 +410,7 @@ def create(self):
extra_context=extra_context,
ui_links=ui_links,
data=empty_record,
context=current_oarepo_ui.catalog.jinja_env.globals,
)

@property
Expand Down Expand Up @@ -430,18 +438,19 @@ def expand_search_links(self, identity, pagination, args):


class TemplatePageUIResource(UIResource):

def create_url_rules(self):
"""Create the URL rules for the record resource."""
self.config: TemplatePageUIResourceConfig

pages_config = self.config.pages
routes = []
for page_url_path, page_template_name in pages_config.items():
handler = getattr(self, f"render_{page_template_name}", None) or partial(self.render, page=page_template_name)
if not hasattr(handler, '__name__'):
handler = getattr(self, f"render_{page_template_name}", None) or partial(
self.render, page=page_template_name
)
if not hasattr(handler, "__name__"):
handler.__name__ = self.render.__name__
if not hasattr(handler, '__self__'):
if not hasattr(handler, "__self__"):
handler.__self__ = self

routes.append(
Expand Down
5 changes: 0 additions & 5 deletions oarepo_ui/theme/webpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,14 @@
devDependencies={},
aliases={
**aliases,

"@translations/oarepo_ui": "translations/oarepo_ui",

# search and edit
"@less/oarepo_ui": "less/oarepo_ui",
"@js/oarepo_ui": "js/oarepo_ui",

# hack for communities being dependent on RDM
"@translations/invenio_app_rdm/i18next": "translations/oarepo_ui/i18next.js",

# hack for vocabularies being dependent on RDM
"@translations/invenio_rdm_records/i18next": "translations/oarepo_ui/i18next.js",

# another hack for communities
"@templates/custom_fields": "js/custom_fields",
},
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = oarepo-ui
version = 5.0.97
version = 5.0.98
description = UI module for invenio 3.5+
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
48 changes: 45 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
from flask_security import login_user
from flask_security.utils import hash_password
from invenio_access import ActionUsers, current_access
from invenio_access.permissions import system_identity
from invenio_access.models import ActionRoles
from invenio_access.permissions import superuser_access, system_identity
from invenio_accounts.models import Role
from invenio_accounts.testutils import login_user_via_session
from invenio_app.factory import create_app as _create_app

from tests.model import ModelUIResource, ModelUIResourceConfig, TitlePageUIResource, TitlePageUIResourceConfig
from tests.model import (
ModelUIResource,
ModelUIResourceConfig,
TitlePageUIResource,
TitlePageUIResourceConfig,
)


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -72,7 +79,9 @@ def record_ui_resource(app, record_ui_resource_config, record_service):


@pytest.fixture(scope="module")
def titlepage_ui_resource(app, ):
def titlepage_ui_resource(
app,
):
ui_resource = TitlePageUIResource(TitlePageUIResourceConfig())
app.register_blueprint(
ui_resource.as_blueprint(template_folder=Path(__file__).parent / "templates")
Expand All @@ -91,6 +100,39 @@ def fake_manifest(app):
)


@pytest.fixture(scope="module")
def users(app):
"""Create example users."""
# This is a convenient way to get a handle on db that, as opposed to the
# fixture, won't cause a DB rollback after the test is run in order
# to help with test performance (creating users is a module -if not higher-
# concern)
from invenio_db import db

with db.session.begin_nested():
datastore = app.extensions["security"].datastore

su_role = Role(name="superuser-access")
db.session.add(su_role)

su_action_role = ActionRoles.create(action=superuser_access, role=su_role)
db.session.add(su_action_role)

user1 = datastore.create_user(
email="[email protected]", password=hash_password("password"), active=True
Dismissed Show dismissed Hide dismissed
)
user2 = datastore.create_user(
email="[email protected]", password=hash_password("password"), active=True
Dismissed Show dismissed Hide dismissed
)
admin = datastore.create_user(
email="[email protected]", password=hash_password("password"), active=True
Dismissed Show dismissed Hide dismissed
)
admin.roles.append(su_role)

db.session.commit()
return [user1, user2, admin]


@pytest.fixture
def simple_record(app, db, search_clear, record_service):
from .model import ModelRecord
Expand Down
10 changes: 4 additions & 6 deletions tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,10 @@ def _get_record(self, resource_requestctx, allow_draft=False):


class TitlePageUIResourceConfig(TemplatePageUIResourceConfig):
blueprint_name = 'titlepage'
url_prefix = '/'
pages = {
'': 'TitlePage'
}
blueprint_name = "titlepage"
url_prefix = "/"
pages = {"": "TitlePage"}


class TitlePageUIResource(TemplatePageUIResource):
pass
pass
4 changes: 3 additions & 1 deletion tests/templates/100-TestDetail.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
{
"ui_links": {{ record.links.ui_links|tojson|safe }},
"permissions": {{ extra_context.permissions|tojson|safe }},
"dummy_filter": {{ 1|dummy|tojson|safe }}
"dummy_filter": {{ 1|dummy|tojson|safe }},
"current_user": "{{current_user}}"

}


6 changes: 2 additions & 4 deletions tests/test_template_page.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import json


def test_template_page(
app, titlepage_ui_resource, client, fake_manifest
):
def test_template_page(app, titlepage_ui_resource, client, fake_manifest):
with client.get("/") as c:
assert c.status_code == 200
data = json.loads(c.text)
assert 'ok' in data
assert "ok" in data
18 changes: 17 additions & 1 deletion tests/test_ui_resource.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

from invenio_access.permissions import system_identity
from invenio_accounts.testutils import login_user_via_session


def test_ui_resource_create_new(app, record_ui_resource, record_service):
Expand All @@ -13,7 +14,7 @@ def test_ui_resource_form_config(app, record_ui_resource):


def test_permissions_on_detail(
app, record_ui_resource, simple_record, client, fake_manifest
app, record_ui_resource, simple_record, client, fake_manifest, users
):
with client.get(f"/simple-model/{simple_record.id}") as c:
assert c.status_code == 200
Expand All @@ -39,6 +40,21 @@ def test_permissions_on_detail(
}


def test_current_user(
app, record_ui_resource, simple_record, client, fake_manifest, users
):
with client.get(f"/simple-model/{simple_record.id}") as c:
print(c.text)
data = json.loads(c.text)
assert "<flask_security.core.AnonymousUser" in data["current_user"]

login_user_via_session(client, email=users[0].email)

with client.get(f"/simple-model/{simple_record.id}") as c:
data = json.loads(c.text)
assert "User <id=1, [email protected]>" in data["current_user"]


def test_filter_on_detail(
app, record_ui_resource, simple_record, client, fake_manifest
):
Expand Down