• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rero / sonar / 17425918180

03 Sep 2025 07:11AM UTC coverage: 95.796% (-0.6%) from 96.378%
17425918180

push

github

PascalRepond
translations: extract messages

Co-Authored-by: Pascal Repond <pascal.repond@rero.ch>

7816 of 8159 relevant lines covered (95.8%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.97
/sonar/theme/views.py
1
# Swiss Open Access Repository
2
# Copyright (C) 2021 RERO
3
#
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU Affero General Public License as published by
6
# the Free Software Foundation, version 3 of the License.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU Affero General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

16
"""Blueprint used for loading templates.
17

18
The sole purpose of this blueprint is to ensure that Invenio can find the
19
templates and static files located in the folders of the same names next to
20
this file.
21
"""
22

23
import contextlib
1✔
24
import re
1✔
25
from copy import deepcopy
1✔
26
from datetime import datetime
1✔
27
from urllib.parse import urlparse
1✔
28

29
import dateutil.parser
1✔
30
import pytz
1✔
31
from flask import (
1✔
32
    Blueprint,
33
    abort,
34
    current_app,
35
    jsonify,
36
    redirect,
37
    render_template,
38
    request,
39
    url_for,
40
)
41
from flask_login import current_user, login_required
1✔
42
from flask_menu import current_menu
1✔
43
from invenio_jsonschemas import current_jsonschemas
1✔
44
from invenio_jsonschemas.errors import JSONSchemaNotFound
1✔
45
from invenio_pidstore.models import PersistentIdentifier
1✔
46

47
from sonar.jsonschemas.factory import JSONSchemaFactory
1✔
48
from sonar.modules.collections.permissions import (
1✔
49
    RecordPermission as CollectionPermission,
50
)
51
from sonar.modules.deposits.permissions import DepositPermission
1✔
52
from sonar.modules.documents.api import DocumentRecord
1✔
53
from sonar.modules.documents.permissions import DocumentPermission
1✔
54
from sonar.modules.organisations.api import OrganisationSearch
1✔
55
from sonar.modules.organisations.permissions import OrganisationPermission
1✔
56
from sonar.modules.permissions import can_access_manage_view
1✔
57
from sonar.modules.subdivisions.permissions import (
1✔
58
    RecordPermission as SubdivisionPermission,
59
)
60
from sonar.modules.users.api import current_user_record
1✔
61
from sonar.modules.users.permissions import UserPermission
1✔
62
from sonar.resources.projects.permissions import RecordPermissionPolicy
1✔
63

64
blueprint = Blueprint("sonar", __name__, template_folder="templates", static_folder="static")
1✔
65

66

67
@blueprint.before_app_request
1✔
68
def init_view():
1✔
69
    """Do some stuff before rendering any view."""
70
    current_menu.submenu("settings").submenu("security").hide()
1✔
71
    current_menu.submenu("settings").submenu("admin").hide()
1✔
72

73

74
@blueprint.route("/robots.txt")
1✔
75
def robots_txt():
1✔
76
    """Generate dynamically robots.txt."""
77
    template = "sonar/robots.txt"
1✔
78
    if not current_app.config.get("SONAR_APP_PRODUCTION_STATE"):
1✔
79
        # If we are not in production status, we disable all robots
80
        return current_app.response_class(
1✔
81
            response=render_template(template, state=False),
82
            status=200,
83
            mimetype="text/plain",
84
        )
85
    url_data = urlparse(request.url)
1✔
86
    scheme = url_data.scheme
1✔
87
    server_name = url_data.netloc.split(":")[0]
1✔
88
    if org_pid := OrganisationSearch().get_organisation_pid_by_server_name(server_name):
1✔
89
        sitemap = f"{scheme}://{server_name}/{org_pid}/sitemap.xml"
×
90
    else:
91
        view = current_app.config.get("SONAR_APP_DEFAULT_ORGANISATION")
1✔
92
        sitemap = f"{scheme}://{url_data.netloc}/{view}/sitemap.xml"
1✔
93
    return current_app.response_class(
1✔
94
        response=render_template(
95
            template,
96
            state=current_app.config.get("SONAR_APP_PRODUCTION_STATE"),
97
            sitemap=sitemap,
98
        ),
99
        status=200,
100
        mimetype="text/plain",
101
    )
102

103

104
@blueprint.route("/users/profile")
1✔
105
@blueprint.route("/users/profile/<pid>")
1✔
106
@login_required
1✔
107
def profile(pid=None):
1✔
108
    """Logged user profile edition page.
109

110
    :param pid: Logged user PID.
111
    """
112
    if pid and pid != current_user_record["pid"]:
1✔
113
        abort(403)
1✔
114

115
    if not pid:
1✔
116
        return redirect(url_for("sonar.profile", pid=current_user_record["pid"]))
1✔
117

118
    return render_template("sonar/accounts/profile.html")
1✔
119

120

121
@blueprint.route("/error")
1✔
122
def error():
1✔
123
    """Error to generate exception for test purposes."""
124
    raise Exception("this is an error for test purposes")
1✔
125

126

127
@blueprint.route("/rerodoc/<pid>")
1✔
128
@blueprint.route("/rerodoc/<pid>/files/<filename>")
1✔
129
def rerodoc_redirection(pid, filename=None):
1✔
130
    """Redirection to document with identifier from RERODOC.
131

132
    :param pid: PID from RERODOC.
133
    :returns: A redirection to record's detail page or 404 if not found.
134
    """
135
    try:
1✔
136
        pid = PersistentIdentifier.get("rerod", pid)
1✔
137
    except Exception:
1✔
138
        abort(404)
1✔
139

140
    # Files URLs does not contains view
141
    if filename:
1✔
142
        return redirect(
1✔
143
            url_for(
144
                "invenio_records_ui.doc_files",
145
                pid_value=pid.get_redirect().pid_value,
146
                filename=filename,
147
            )
148
        )
149
    doc_pid = pid.get_redirect().pid_value
1✔
150
    doc = DocumentRecord.get_record_by_pid(doc_pid)
1✔
151
    if doc:
1✔
152
        doc = doc.resolve()
1✔
153
        orgs = doc.get("organisation", [])
1✔
154
        # In case of multiple organisations we redirect to the global view
155
        if len(orgs) == 1:
1✔
156
            org = orgs.pop()
1✔
157
            # Only for dedicated or shared
158
            if org.get("isDedicated") or org.get("isShared"):
1✔
159
                return redirect(
1✔
160
                    url_for(
161
                        "invenio_records_ui.doc",
162
                        view=org.get("code"),
163
                        pid_value=pid.get_redirect().pid_value,
164
                    )
165
                )
166
    global_view = current_app.config.get("SONAR_APP_DEFAULT_ORGANISATION")
1✔
167
    return redirect(
1✔
168
        url_for(
169
            "invenio_records_ui.doc",
170
            view=global_view,
171
            pid_value=pid.get_redirect().pid_value,
172
        )
173
    )
174

175

176
@blueprint.route("/manage/")
1✔
177
@blueprint.route("/manage/<path:path>")
1✔
178
@can_access_manage_view
1✔
179
def manage(path=None):
1✔
180
    """Admin access page integrating angular ui."""
181
    return render_template("sonar/manage.html")
1✔
182

183

184
@blueprint.route("/logged-user/", methods=["GET"])
1✔
185
def logged_user():
1✔
186
    """Current logged user informations in JSON."""
187
    data = {"settings": {"document_identifier_link": current_app.config.get("SONAR_APP_DOCUMENT_IDENTIFIER_LINK")}}
1✔
188

189
    if not current_user.is_anonymous:
1✔
190
        user = current_user_record
1✔
191
        if user and "resolve" in request.args:
1✔
192
            user = user.replace_refs()
1✔
193

194
        if user:
1✔
195
            data["metadata"] = user.dumps()
1✔
196
            data["metadata"]["is_superuser"] = user.is_superuser
1✔
197
            data["metadata"]["is_admin"] = user.is_admin
1✔
198
            data["metadata"]["is_moderator"] = user.is_moderator
1✔
199
            data["metadata"]["is_submitter"] = user.is_submitter
1✔
200
            data["metadata"]["is_user"] = user.is_user
1✔
201
            data["metadata"]["permissions"] = {
1✔
202
                "users": {
203
                    "add": UserPermission.create(user),
204
                    "list": UserPermission.list(user),
205
                },
206
                "documents": {
207
                    "add": DocumentPermission.create(user),
208
                    "list": DocumentPermission.list(user),
209
                },
210
                "organisations": {
211
                    "add": OrganisationPermission.create(user),
212
                    "list": OrganisationPermission.list(user),
213
                },
214
                "deposits": {
215
                    "add": DepositPermission.create(user),
216
                    "list": DepositPermission.list(user),
217
                },
218
                "projects": {
219
                    "add": RecordPermissionPolicy("create").can(),
220
                    "list": RecordPermissionPolicy("search").can(),
221
                },
222
                "collections": {
223
                    "add": CollectionPermission.create(user),
224
                    "list": CollectionPermission.list(user),
225
                },
226
                "subdivisions": {
227
                    "add": SubdivisionPermission.create(user),
228
                    "list": SubdivisionPermission.list(user),
229
                },
230
            }
231

232
    # TODO: If an organisation is associated to user and only when running
233
    # tests, organisation cannot not be encoded to JSON after call of
234
    # user.replace_refs() --> check why
235
    return jsonify(data)
1✔
236

237

238
def replace_ref_url(schema, new_host):
1✔
239
    """Replace all $refs with local $refs.
240

241
    :param: schema: Schema to replace the $refs
242
    :param: new_host: The host to replace the $ref with.
243
    :returns: modified schema.
244
    """
245
    jsonschema_host = current_app.config.get("JSONSCHEMAS_HOST")
×
246
    if default := schema.get("properties", {}).get("$schema", {}).get("default"):
×
247
        schema["properties"]["$schema"]["default"] = default.replace(jsonschema_host, new_host)
×
248
    for k, v in schema.items():
×
249
        if isinstance(v, dict):
×
250
            schema[k] = replace_ref_url(schema=schema[k], new_host=new_host)
×
251
    if "$ref" in schema and isinstance(schema["$ref"], str):
×
252
        schema["$ref"] = schema["$ref"].replace(jsonschema_host, new_host)
×
253
    # Todo: local://
254
    return schema
×
255

256

257
@blueprint.route("/schemas/<record_type>")
1✔
258
@blueprint.route("/schemas/<record_type>/<path:schema>")
1✔
259
def schemas(record_type, schema=None):
1✔
260
    """Return schema for the editor.
261

262
    :param record_type: Type of resource.
263
    :returns: JSONified schema or a 404 if not found.
264
    """
265
    resolved = request.args.get("resolved", current_app.config.get("JSONSCHEMAS_RESOLVE_SCHEMA"), type=int)
1✔
266
    new_host = urlparse(request.base_url).netloc
1✔
267
    try:
1✔
268
        schema = JSONSchemaFactory.create(record_type, with_refs=bool(resolved))
1✔
269
        schema = schema.process()
1✔
270
        if not resolved:
1✔
271
            schema = replace_ref_url(schema, new_host)
×
272
        return jsonify({"schema": schema})
1✔
273
    except JSONSchemaNotFound:
1✔
274
        schema_path = f"{record_type}/{schema}"
1✔
275
        with contextlib.suppress(JSONSchemaNotFound):
1✔
276
            if current_app.debug:
1✔
277
                current_jsonschemas.get_schema.cache_clear()
×
278
            schema = deepcopy(current_jsonschemas.get_schema(schema_path, with_refs=bool(resolved)))
1✔
279
        if not resolved:
1✔
280
            schema = replace_ref_url(schema, new_host)
×
281
            return jsonify({"schema": schema})
×
282
    abort(404)
1✔
283
    return None
×
284

285

286
@blueprint.app_template_filter()
1✔
287
def record_image_url(record, code, key=None):
1✔
288
    """Get image URL for a record.
289

290
    :param files: Liste of files of the record.
291
    :param key: The key of the file to be rendered, if no key, takes the first.
292
    :returns: Image url corresponding to key, or the first one.
293
    """
294
    if not (record.get("_files") and record.get("pid")):
1✔
295
        return None
1✔
296
    for file in record["_files"]:
1✔
297
        if re.match(r"^.*\.(jpe?g|png|gif|svg)$", file["key"], flags=re.IGNORECASE) and (not key or file["key"] == key):
1✔
298
            return url_for(
1✔
299
                f"invenio_records_ui.{code}_files",
300
                pid_value=record.get("pid"),
301
                filename=file["key"],
302
            )
303
    return None
×
304

305

306
@blueprint.app_template_filter()
1✔
307
def format_date(date, format="%d/%m/%Y"):  # noqa: A002
1✔
308
    """Format the given ISO format date string.
309

310
    :param date: Date string in ISO format.
311
    :param fmt: Output format.
312
    :returns: Formatted date string.
313
    """
314
    # Parse date
315
    if not isinstance(date, datetime):
1✔
316
        date = dateutil.parser.isoparse(date)
1✔
317

318
    # Add timezone info
319
    if not date.tzinfo:
1✔
320
        date = pytz.utc.localize(date)
1✔
321

322
    # Change date to the right timezone
323
    timezone = pytz.timezone(current_app.config.get("BABEL_DEFAULT_TIMEZONE"))
1✔
324
    date = date.astimezone(timezone)
1✔
325

326
    return date.strftime(format)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc