Compare commits

...
5 Commits
Author SHA1 Message Date
Cian-H cdee2d929d chore: version bump 2026-08-13 17:35:46 +01:00
Cian-H fc920e1b04 Updated footer logos 2026-08-13 14:23:00 +01:00
Cian-H 9963c3e7fe Added user login and account creation to admin panels 2026-08-13 14:03:03 +01:00
Cian-H 9040da0172 Added admin panel access with Role Assignment 2026-08-13 13:30:53 +01:00
Cian-H 4ff4e83e05 Added translation compilation to wheel 2026-08-13 11:54:10 +01:00
18 changed files with 602 additions and 12 deletions
+2
View File
@@ -20,6 +20,8 @@ jobs:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
- name: Compile translations
run: uv run pybabel compile -d invenio_theme_iform/translations/
- name: Build package
run: uv build
- name: Publish to PyPI
+1 -1
View File
@@ -8,4 +8,4 @@
"""Metadata for this python module."""
__version__ = "2026.8.12"
__version__ = "2026.8.13"
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 I-Form.
#
# invenio-theme-iform is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
"""Administration views."""
+219
View File
@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 I-Form.
#
# invenio-theme-iform is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
"""I-Form administration views."""
import json
import os
from datetime import datetime
from flask import current_app, flash, redirect, request, url_for
from invenio_accounts.models import Role, User
from invenio_accounts.proxies import current_accounts
from invenio_administration.views.base import AdminView
from invenio_db import db
from invenio_i18n import lazy_gettext as _
def get_site_settings_path():
"""Get the path to site_settings.json."""
app_data_dir = os.path.join(current_app.instance_path, "app_data")
os.makedirs(app_data_dir, exist_ok=True)
return os.path.join(app_data_dir, "site_settings.json")
def get_site_settings():
"""Load site settings from JSON file."""
path = get_site_settings_path()
if not os.path.exists(path):
return {"registration_enabled": True}
try:
with open(path, "r") as f:
return json.load(f)
except Exception:
return {"registration_enabled": True}
def set_site_settings(settings):
"""Save site settings to JSON file."""
path = get_site_settings_path()
with open(path, "w") as f:
json.dump(settings, f, indent=2)
class RoleManagementView(AdminView):
"""Admin view for managing user roles."""
name = "roles"
category = "User management"
template = "invenio_theme_iform/administration/roles.html"
url = "/roles"
menu_label = _("Role Assignment")
icon = "users"
def get(self):
"""Render the role management template."""
roles = Role.query.order_by(Role.name).all()
selected_role_id = request.args.get("role_id")
users = []
selected_role = None
if selected_role_id:
selected_role = Role.query.get(selected_role_id)
if selected_role:
users = User.query.order_by(User.email).all()
return self.render(
roles=roles,
selected_role=selected_role,
users=users,
)
def post(self):
"""Handle role assignment/revocation."""
user_id = request.form.get("user_id")
role_id = request.form.get("role_id")
action = request.form.get("action")
if not (user_id and role_id and action):
flash(_("Missing required fields."), "error")
return redirect(url_for("administration.roles", role_id=role_id))
try:
user = User.query.get(int(user_id))
except ValueError:
user = None
role = Role.query.get(role_id)
if not user or not role:
flash(_("User or role not found."), "error")
return redirect(url_for("administration.roles", role_id=role_id))
if action == "enable":
if role not in user.roles:
current_accounts.datastore.add_role_to_user(user, role)
flash(
_("Role {role} enabled for {email}.").format(
role=role.name, email=user.email
),
"success",
)
elif action == "disable":
if role in user.roles:
current_accounts.datastore.remove_role_from_user(user, role)
flash(
_("Role {role} disabled for {email}.").format(
role=role.name, email=user.email
),
"success",
)
db.session.commit()
return redirect(url_for("administration.roles", role_id=role_id))
class RegistrationSettingsView(AdminView):
"""Admin view for controlling site registration settings."""
name = "registration-settings"
category = "Site Settings"
template = "invenio_theme_iform/administration/registration_settings.html"
url = "/registration-settings"
menu_label = _("Registration Settings")
icon = "setting"
def get(self):
"""Render registration settings page."""
settings = get_site_settings()
return self.render(
registration_enabled=settings.get("registration_enabled", True)
)
def post(self):
"""Handle registration settings update."""
enabled = request.form.get("registration_enabled") == "true"
settings = get_site_settings()
settings["registration_enabled"] = enabled
set_site_settings(settings)
status_str = _("enabled") if enabled else _("disabled")
flash(
_("Public user registration has been {status}.").format(
status=status_str
),
"success",
)
return redirect(url_for("administration.registration-settings"))
class UserCreateView(AdminView):
"""Admin view for creating users."""
name = "create-user"
category = "User management"
template = "invenio_theme_iform/administration/user_create.html"
url = "/create-user"
menu_label = _("Create User")
icon = "user plus"
def get(self):
"""Render user creation page."""
return self.render()
def post(self):
"""Handle manual user creation."""
from flask_security.utils import hash_password
from invenio_access.permissions import system_identity
from invenio_users_resources.proxies import current_users_service
email = request.form.get("email", "").strip()
password = request.form.get("password", "").strip()
confirm_email = request.form.get("confirm_email") == "true"
if not email or not password:
flash(_("Email and Password are required."), "error")
return redirect(url_for("administration.create-user"))
if User.query.filter_by(email=email).first():
flash(_("A user with this email already exists."), "error")
return redirect(url_for("administration.create-user"))
try:
user_kwargs = {
"email": email,
"password": hash_password(password),
"active": True,
}
if confirm_email:
user_kwargs["confirmed_at"] = datetime.utcnow()
user = current_accounts.datastore.create_user(**user_kwargs)
db.session.commit()
# Index the newly created user in search engine
try:
from invenio_search import current_search_client
current_users_service.reindex(system_identity, uids=[user.id])
current_users_service.indexer.process_bulk_queue()
current_search_client.indices.refresh(index="*")
except Exception as ie:
current_app.logger.warning("Failed to index user %s: %s", user.id, str(ie))
flash(
_("User {email} created successfully.").format(email=email),
"success",
)
except Exception as e:
db.session.rollback()
flash(
_("Failed to create user: {error}").format(error=str(e)),
"error",
)
return redirect(url_for("administration.create-user"))
@@ -39,8 +39,20 @@
}
}
div .logos a:hover {
background-color: transparent !important;
div .logos {
--invenio-logo-glass-color: @invenioLogoGlassColor;
--ri-logo-color: @riLogoColor;
a:hover {
background-color: transparent !important;
}
}
}
@media (prefers-color-scheme: dark) {
#footer div .logos {
--invenio-logo-glass-color: #ffffff;
--ri-logo-color: @riLogoColorDark;
}
}
@@ -59,6 +59,9 @@
// footer specific
@footerGrey: #5e5e5e;
@footerBottomBackground: #f2f2f2;
@invenioLogoGlassColor: #333333;
@riLogoColor: #19261b;
@riLogoColorDark: #decbe9;
// record specific
@recordVersionBackground: #f2f2f2;
+29 -1
View File
@@ -8,7 +8,7 @@
"""invenio module for I-Form theme."""
from flask import g, has_request_context
from flask import g, has_request_context, request
from flask_login import login_required
from . import config
@@ -32,8 +32,36 @@ class InvenioThemeIform(object):
app.register_error_handler(423, locked)
@app.before_request
def block_registration_if_disabled():
if request.endpoint == "security.register":
from flask import current_app, render_template
from .administration.views import get_site_settings
settings = get_site_settings()
if not settings.get("registration_enabled", True):
admin_email = (
current_app.config.get("APP_RDM_ADMIN_EMAIL_RECIPIENT")
or current_app.config.get("SUPPORT_EMAIL")
or current_app.config.get("THEME_SUPPORT_EMAIL")
or current_app.config.get("SECURITY_EMAIL_SENDER")
or "info@repo.i-form.ie"
)
return (
render_template(
"invenio_theme_iform/accounts/registration_disabled.html",
admin_email=admin_email,
),
403,
)
app.extensions["invenio-theme-iform"] = self
@app.context_processor
def inject_site_settings():
from .administration.views import get_site_settings
settings = get_site_settings()
return dict(registration_enabled=settings.get("registration_enabled", True))
def init_config(self, app):
"""Initialize configuration."""
for k in dir(config):
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 29.069883 10.420284">
<g transform="translate(1142.2809,279.38339)">
<a transform="matrix(0.26458333,0,0,0.26458333,-1279.7936,-339.14401)" id="a9715" xlink:href="/products/rdm/">
<g transform="translate(0.9420447)">
<g aria-label="RDM" transform="scale(0.96631971,1.0348542)" style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:37.37073898px;font-family:Oswald;fill:#fb8273;stroke:none">
<path d="m 628.93512,251.59732 0.52319,-30.2703 h 6.53987 l 3.73708,19.09645 3.99867,-19.09645 h 6.31565 l 0.52319,30.2703 h -4.48449 l -0.48582,-20.51654 -3.84918,20.51654 h -3.84919 l -4.03604,-20.66602 -0.41108,20.66602 z m -21.27388,0 v -30.2703 h 7.3994 q 3.84919,0 5.97932,1.00901 2.1675,0.97164 3.0644,3.10177 0.8969,2.09276 0.8969,5.38139 v 11.02437 q 0,3.36336 -0.8969,5.53087 -0.8969,2.1675 -3.02703,3.21388 -2.09276,1.00901 -5.8672,1.00901 z m 6.16617,-4.29763 h 1.30798 q 1.71905,0 2.42909,-0.52319 0.74742,-0.56057 0.93427,-1.60695 0.18686,-1.08375 0.18686,-2.65332 V 230.0344 q 0,-1.56957 -0.2616,-2.50384 -0.22422,-0.97163 -0.97164,-1.42008 -0.71004,-0.44845 -2.35436,-0.44845 h -1.2706 z m -27.84412,4.29763 v -30.2703 h 7.84785 q 3.06441,0 5.23191,0.71005 2.20487,0.67267 3.36336,2.42909 1.19587,1.75643 1.19587,4.8582 0,1.86854 -0.33634,3.326 -0.29896,1.42008 -1.12112,2.50384 -0.78479,1.04638 -2.24224,1.68168 l 4.18552,14.76144 h -6.31566 l -3.47548,-13.71506 h -2.1675 v 13.71506 z m 6.16617,-17.37739 h 1.7938 q 1.49483,0 2.35435,-0.48582 0.85953,-0.48582 1.19587,-1.45746 0.3737,-1.00901 0.3737,-2.46647 0,-2.09276 -0.78478,-3.17651 -0.74742,-1.12113 -2.84018,-1.12113 h -2.09276 z"/>
</g>
<g transform="matrix(0.43046721,0,0,0.43046721,198.64479,126.38329)">
<g transform="translate(474.21725,226.44114)">
<path d="m 328.162,4.665 c -18.767,0 -34.032,15.268 -34.032,34.034 0,7.28 2.306,14.028 6.214,19.568 l -30.078,26.432 c -1.881,1.878 0.067,6.867 1.944,8.749 1.879,1.876 6.87,3.822 8.748,1.943 l 26.265,-29.894 c 5.779,4.525 13.047,7.233 20.939,7.233 18.768,0 34.034,-15.267 34.034,-34.032 0,-18.765 -15.266,-34.033 -34.034,-34.033 z m 0,56.722 c -12.51,0 -22.689,-10.177 -22.689,-22.688 0,-12.511 10.18,-22.69 22.689,-22.69 12.513,0 22.688,10.18 22.688,22.69 0,12.51 -10.175,22.688 -22.688,22.688 z" style="fill:var(--invenio-logo-glass-color, #000000)"/>
</g>
<g transform="matrix(0.38742049,0,0,0.38742049,670.93796,251.12579)">
<path d="m 375.104,38.079 c 0.174,18.184 -8.123,32.126 -11.51,37.52 8.183,-5.347 19.898,-19.36 19.674,-37.661 0.174,-18.183 -11.431,-32.127 -19.631,-37.52 3.323,5.347 11.691,19.36 11.467,37.661 z" style="fill:var(--invenio-logo-glass-color, #000000)"/>
<path d="m 375.104,38.079 c 0.174,18.184 -8.123,32.126 -11.51,37.52 8.183,-5.347 19.898,-19.36 19.674,-37.661 0.174,-18.183 -11.431,-32.127 -19.631,-37.52 3.323,5.347 11.691,19.36 11.467,37.661 z" style="fill:none;stroke:var(--invenio-logo-glass-color, #000000)"/>
</g>
</g>
</g>
</a>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

@@ -2,7 +2,7 @@
{%- if not current_user.is_authenticated %}
{%- if config.SECURITY_REGISTERABLE %}
{%- if config.SECURITY_REGISTERABLE and registration_enabled %}
<div class="short-menu-right-button">
<a href="{{ url_for('security.register') }}" class="no-decoration">
{{ _ ("Sign up") }}
@@ -22,6 +22,13 @@
<i class="user icon"></i> {{ current_user.email }}
</a>
</div>
{%- for item in current_menu.submenu('profile-admin').children if item.visible %}
<div class="short-menu-right-button">
<a class="dropdown-item no-decoration" href="{{ item.url }}">
{{ item.text|safe }}
</a>
</div>
{%- endfor %}
<div class="short-menu-right-button">
<a class="dropdown-item no-decoration" href="{{ url_for_security('logout') }}"
>{{ _("Sign out") }}</a
@@ -0,0 +1,42 @@
{#
Copyright (C) 2024 I-Form.
invenio-theme-iform is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
#}
{% extends config.THEME_ERROR_TEMPLATE %}
{% block message %}
<div class="ui container center aligned rel-mt-5 rel-mb-5">
<h1 class="ui icon header">
<i class="user cancel icon red"></i>
<div class="content">
{{ _("Public Registration Disabled") }}
<div class="sub header rel-mt-2">
{{ _("Public user registration is currently closed on this repository.") }}
</div>
</div>
</h1>
<div class="ui message info left aligned" style="max-width: 600px; margin: 2em auto;">
<div class="header">
{{ _("Need an account?") }}
</div>
<p>
{{ _("If you require an account to upload or manage research data, please contact an administrator at:") }}
</p>
<p style="font-size: 1.1em; font-weight: bold; text-align: center;">
<a href="mailto:{{ admin_email }}"><i class="envelope icon"></i> {{ admin_email }}</a>
</p>
</div>
<div class="rel-mt-3">
<a href="{{ url_for('security.login') }}" class="ui button primary">
<i class="sign-in icon"></i> {{ _("Back to Log In") }}
</a>
<a href="{{ url_for('index') }}" class="ui button">
<i class="home icon"></i> {{ _("Home Page") }}
</a>
</div>
</div>
{% endblock message %}
@@ -0,0 +1,61 @@
{#
Copyright (C) 2024 I-Form.
invenio-theme-iform is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
#}
{% extends "invenio_administration/base.html" %}
{% block admin_page_content %}
<div class="ui container dashboard rel-mt-5">
<div class="ui grid">
<div class="sixteen wide column">
<h2 class="ui header">
<i class="setting icon"></i>
<div class="content">
{{ _("Registration Settings") }}
<div class="sub header">{{ _("Enable or disable public user registration on the website.") }}</div>
</div>
</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="ui {{ 'positive' if category == 'success' else 'negative' }} message">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="ui segment">
<h3 class="ui header">{{ _("Public Sign-up Status") }}</h3>
<p>
{% if registration_enabled %}
<span class="ui green large label"><i class="check icon"></i> {{ _("Public Sign-ups ENABLED") }}</span>
{% else %}
<span class="ui red large label"><i class="ban icon"></i> {{ _("Public Sign-ups DISABLED") }}</span>
{% endif %}
</p>
<div class="ui divider"></div>
<form method="POST" class="ui form">
{% if registration_enabled %}
<input type="hidden" name="registration_enabled" value="false">
<button type="submit" class="ui red button">
<i class="ban icon"></i> {{ _("Disable Public Sign-ups") }}
</button>
{% else %}
<input type="hidden" name="registration_enabled" value="true">
<button type="submit" class="ui green button">
<i class="check icon"></i> {{ _("Enable Public Sign-ups") }}
</button>
{% endif %}
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,120 @@
{#
Copyright (C) 2024 I-Form.
invenio-theme-iform is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
#}
{% extends "invenio_administration/base.html" %}
{% block admin_page_content %}
<div class="ui container dashboard rel-mt-5">
<div class="ui grid">
<div class="sixteen wide column">
<h2 class="ui header">
<i class="users icon"></i>
<div class="content">
{{ _("Role Assignment") }}
<div class="sub header">{{ _("Select a role and enable or disable it for users.") }}</div>
</div>
</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="ui {{ 'positive' if category == 'success' else 'negative' }} message">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="ui segment">
<form method="GET" class="ui form">
<div class="field">
<label>{{ _("Select a Role") }}</label>
<div class="ui action input">
<select name="role_id" class="ui dropdown">
<option value="">{{ _("-- Select a role --") }}</option>
{% for r in roles %}
<option value="{{ r.id }}" {% if selected_role and selected_role.id == r.id %}selected{% endif %}>
{{ r.name }}
</option>
{% endfor %}
</select>
<button class="ui button primary" type="submit">{{ _("Select") }}</button>
</div>
</div>
</form>
</div>
{% if selected_role %}
<div class="ui segment">
<h3 class="ui header">
{{ _("Users assigned to:") }} {{ selected_role.name }}
{% if selected_role.description %}
<div class="sub header" style="margin-top: 5px;">
<i class="info circle icon"></i> {{ selected_role.description }}
</div>
{% endif %}
</h3>
<table class="ui celled striped table">
<thead>
<tr>
<th>{{ _("Email") }}</th>
<th>{{ _("Active") }}</th>
<th class="center aligned">{{ _("Has Role?") }}</th>
<th class="center aligned">{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.email }}</td>
<td>
{% if user.active %}
<div class="ui green horizontal label">{{ _("Active") }}</div>
{% else %}
<div class="ui grey horizontal label">{{ _("Inactive") }}</div>
{% endif %}
</td>
{% set has_role = selected_role in user.roles %}
<td class="center aligned">
{% if has_role %}
<i class="green check large icon"></i>
{% else %}
<i class="red times large icon"></i>
{% endif %}
</td>
<td class="center aligned">
<form method="POST" style="display:inline;">
<input type="hidden" name="user_id" value="{{ user.id }}">
<input type="hidden" name="role_id" value="{{ selected_role.id }}">
{% if has_role %}
<input type="hidden" name="action" value="disable">
<button class="ui tiny red button" type="submit">
<i class="minus icon"></i> {{ _("Disable") }}
</button>
{% else %}
<input type="hidden" name="action" value="enable">
<button class="ui tiny green button" type="submit">
<i class="plus icon"></i> {{ _("Enable") }}
</button>
{% endif %}
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="center aligned">{{ _("No users found.") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,58 @@
{#
Copyright (C) 2024 I-Form.
invenio-theme-iform is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
#}
{% extends "invenio_administration/base.html" %}
{% block admin_page_content %}
<div class="ui container dashboard rel-mt-5">
<div class="ui grid">
<div class="sixteen wide column">
<h2 class="ui header">
<i class="user plus icon"></i>
<div class="content">
{{ _("Create User") }}
<div class="sub header">{{ _("Manually add a new user account.") }}</div>
</div>
</h2>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="ui {{ 'positive' if category == 'success' else 'negative' }} message">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="ui segment">
<form method="POST" class="ui form">
<div class="field required">
<label>{{ _("Email Address") }}</label>
<input type="email" name="email" placeholder="user@example.com" required>
</div>
<div class="field required">
<label>{{ _("Password") }}</label>
<input type="password" name="password" placeholder="Password" required>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="confirm_email" value="true" id="confirm_email" checked>
<label for="confirm_email">{{ _("Automatically confirm email address (allow immediate login)") }}</label>
</div>
</div>
<button type="submit" class="ui button primary">
<i class="user plus icon"></i> {{ _("Create User") }}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -108,12 +108,12 @@
<a
href="http://inveniosoftware.org/products/rdm"
target="_blank"
title="invenioRDM"
title="InvenioRDM"
>
<img
src="{{ url_for('static', filename='images/inveniordm-tail.svg') }}"
src="{{ url_for('static', filename='images/invenio-favicon.svg') }}"
alt="InvenioRDM logo"
style="display: block; height: 90px; margin-top: 8px;"
style="display: block; height: 50px; margin-top: 8px;"
/>
</a>
</div>
@@ -122,13 +122,13 @@
<div class="logos">
<strong>{{ _("Funded by") }}</strong>
<a
href="https://www.sfi.ie"
href="https://www.researchireland.ie"
target="_blank"
title="Science Foundation Ireland"
title="Research Ireland"
>
<img
src="{{ url_for('static', filename='images/SFI_logo.png') }}"
alt="Science Foundation Ireland"
src="{{ url_for('static', filename='images/research-ireland-logo.svg') }}"
alt="Research Ireland"
style="display: block; height: auto; margin-top: 15px; width: 230px;"
/>
</a>
+6
View File
@@ -55,6 +55,12 @@ invenio_theme_iform = "invenio_theme_iform.config"
[project.entry-points."invenio_base.finalize_app"]
invenio_theme_iform = "invenio_theme_iform.ext:finalize_app"
[project.entry-points."invenio_administration.views"]
invenio_theme_iform_roles = "invenio_theme_iform.administration.views:RoleManagementView"
invenio_theme_iform_registration_settings = "invenio_theme_iform.administration.views:RegistrationSettingsView"
invenio_theme_iform_user_create = "invenio_theme_iform.administration.views:UserCreateView"
[dependency-groups]
dev = [
"hatch>=1.14.1",