Skip to content

Commit 0db5518

Browse files
committed
feat: store metrics in local tables and refresh via scim actions
1 parent a6320d0 commit 0db5518

19 files changed

Lines changed: 155 additions & 191 deletions

ckanext/scientometrics/cli.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
from ckanext.scientometrics import config
77

88
__all__ = [
9-
"scientometrics",
9+
"scim",
1010
]
1111

1212

1313
@click.group()
14-
def scientometrics():
14+
def scim():
1515
pass
1616

1717

18-
@scientometrics.command()
18+
@scim.command()
1919
@click.option(
2020
"--user-ids",
2121
type=str,
@@ -43,8 +43,8 @@ def update_user_metrics(user_ids: tuple, requested_sources: tuple):
4343
requested_sources = config.enabled_metrics()
4444
with click.progressbar(user_ids, label="Updating user metrics") as bar:
4545
for user_id in bar:
46-
tk.get_action("scientometrics_update_user_metrics")(
47-
{}, {"user_id": user_id, "requested_sources": requested_sources}
46+
tk.get_action("scim_update_user_metrics")(
47+
{"ignore_auth": True}, {"user_id": user_id, "requested_sources": requested_sources}
4848
)
4949

5050
click.echo("Metrics update complete!")

ckanext/scientometrics/const.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

ckanext/scientometrics/helpers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
from ckanext.scientometrics import config
88

99

10-
def scientometrics_get_user_metrics(user_id: str) -> dict[str, Any]:
10+
def scim_get_user_metrics(user_id: str) -> dict[str, Any]:
1111
"""Retrieve the metrics for a user."""
12-
return tk.get_action("scientometrics_get_user_metrics")({}, {"user_id": user_id})
12+
return tk.get_action("scim_get_user_metrics")({}, {"user_id": user_id})
1313

1414

15-
def scientometrics_get_enabled_metrics() -> list[str]:
15+
def scim_get_enabled_metrics() -> list[str]:
1616
"""List of enabled metrics."""
1717
return config.enabled_metrics()
1818

1919

20-
def scientometrics_show_metrics_on_user_page() -> bool:
20+
def scim_show_metrics_on_user_page() -> bool:
2121
"""Show metrics on user page in the info section."""
2222
return config.show_metrics_on_user_page()

ckanext/scientometrics/interfaces.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
class IScientometrics(Interface):
9-
def get_metrics_extractors(self) -> dict[str, AuthorMetricsExtractor]:
9+
def get_metrics_extractors(self) -> dict[str, type[AuthorMetricsExtractor]]:
1010
"""Allows to redefine the default metrics extractors.
1111
1212
Default:
Lines changed: 93 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
from __future__ import annotations
22

33
import copy
4+
import logging
45
from typing import Any
56

67
import ckan.plugins.toolkit as tk
7-
from ckan import types
8+
from ckan import model, types
89
from ckan.logic import validate
910

10-
from ckanext.scientometrics import utils
11+
from ckanext.scientometrics import config, utils
1112
from ckanext.scientometrics.logic import schema
13+
from ckanext.scientometrics.model import UserMetric
1214

13-
14-
@tk.chained_action
15-
def user_create(next_: Any, context: types.Context, data_dict: dict[str, Any]):
16-
"""Attach scientometrics extras to a user when it is created."""
17-
user = next_(context, data_dict)
18-
_attach_extras(context, data_dict, user["id"])
19-
return user
15+
log = logging.getLogger(__name__)
2016

2117

2218
@tk.chained_action
@@ -36,10 +32,8 @@ def user_show(next_: Any, context: types.Context, data_dict: dict[str, Any]):
3632
userobj = context["model"].User.get(user_data["id"])
3733
if not userobj:
3834
raise tk.ObjectNotFound("user")
39-
if userobj.plugin_extras and "scientometrics" in userobj.plugin_extras:
40-
user_data["scientometrics"] = copy.deepcopy(
41-
userobj.plugin_extras.get("scientometrics", {})
42-
)
35+
if userobj.plugin_extras and "scim" in userobj.plugin_extras:
36+
user_data["scim"] = copy.deepcopy(userobj.plugin_extras.get("scim", {}))
4337
return user_data
4438

4539

@@ -52,73 +46,117 @@ def _attach_extras(context: types.Context, data_dict: dict[str, Any], user_id: s
5246
raise tk.ObjectNotFound("user")
5347
extras = copy.deepcopy(userobj.plugin_extras or {})
5448

55-
if "scientometrics" not in extras:
56-
extras["scientometrics"] = {}
57-
extras["scientometrics"].update(sm_details)
49+
scim = extras.setdefault("scim", {})
50+
for source in config.enabled_metrics():
51+
key = f"{source}_author_id"
52+
if key in data_dict and not data_dict.get(key):
53+
scim.pop(key, None)
54+
scim.update(sm_details)
5855
userobj.plugin_extras = extras
5956
userobj.save()
6057

6158

6259
@tk.side_effect_free
63-
@validate(schema.scientometrics_update_user_metrics)
64-
def scientometrics_update_user_metrics(
65-
context: types.Context, data_dict: dict[str, Any]
66-
) -> dict[str, Any]:
67-
"""Update a user's scientometrics metrics.
60+
@validate(schema.scim_update_user_metrics)
61+
def scim_get_user_metrics(context: types.Context, data_dict: dict[str, Any]) -> dict[str, Any]:
62+
"""Retrieve user scientometrics metrics.
63+
64+
Args:
65+
context (Dict[str, Any]): The CKAN action context.
66+
data_dict (Dict[str, Any]): A dictionary containing:
67+
- "user_id": The ID of the user whose metrics we want to retrieve.
68+
69+
Returns:
70+
Dict[str, Any]: The user's scientometrics metrics keyed by source.
71+
"""
72+
user_id_or_name = data_dict["user_id"]
73+
74+
user_dict = tk.get_action("user_show")({"ignore_auth": True}, {"id": user_id_or_name})
75+
records = UserMetric.by_user_id(user_dict["id"])
76+
return {record.source: record.dictize({}) for record in records}
77+
78+
79+
@validate(schema.scim_update_user_metrics)
80+
def scim_update_user_metrics(context: types.Context, data_dict: dict[str, Any]) -> dict[str, Any]:
81+
"""Create/update a user's scientometrics metrics using author ids.
6882
6983
Args:
7084
context (Context): The CKAN action context.
7185
data_dict (dict[str, Any]): A dictionary containing:
7286
- "user_id": The ID of the user to update.
73-
- "requested_sources": A list or dict indicating which metrics to update.
87+
- "requested_sources": A list or dict indicating which metrics to update (subset of extras).
7488
7589
Returns:
7690
Dict[str, Any]: A dictionary containing the updated metrics.
7791
"""
92+
tk.check_access("scim_update_user_metrics", context, data_dict)
7893
user_id_or_name = data_dict["user_id"]
79-
user_dict = tk.get_action("user_show")(
80-
{"ignore_auth": True}, {"id": user_id_or_name}
81-
)
94+
user_dict = tk.get_action("user_show")({"ignore_auth": True}, {"id": user_id_or_name})
8295
user_id = user_dict["id"]
8396

84-
requested_sources = data_dict["requested_sources"]
85-
scientometrics_ids = user_dict.get("scientometrics", {})
86-
updated_metrics = {}
97+
requested_sources = set(data_dict["requested_sources"] or [])
98+
records = UserMetric.by_user_id(user_id)
99+
existing = {record.source: record for record in records}
100+
authors = _collect_authors(user_id, existing)
101+
sources = requested_sources & set(authors.keys()) if requested_sources else set(authors.keys())
102+
updated_metrics: dict[str, dict[str, Any]] = {}
87103

88-
for metric_id_key, author_id in scientometrics_ids.items():
89-
source = metric_id_key.replace("_author_id", "")
90-
if not source or source not in requested_sources:
104+
for source in sources:
105+
author_id = authors.get(source)
106+
if not author_id:
91107
continue
92108

93-
extracted_metrics = utils.fetch_author_metrics(source + "_author", author_id)
109+
try:
110+
extracted_metrics = utils.fetch_author_metrics(source + "_author", author_id)
111+
except tk.ValidationError as exc:
112+
log.warning("Failed to fetch metrics for user %s source %s: %s", user_id, source, exc, exc_info=True)
113+
extracted_metrics = {"error": str(exc)}
94114
if not extracted_metrics:
95115
continue
96116

97-
utils.save_metrics_in_flake(user_id, source, author_id, extracted_metrics)
117+
payload = dict(extracted_metrics)
118+
payload["author_id"] = author_id
119+
existing_record = existing.get(source)
120+
external_id = payload.get("external_id") or (existing_record.external_id if existing_record else str(author_id))
121+
external_url = (
122+
payload.get("external_url")
123+
or payload.get("url")
124+
or (existing_record.external_url if existing_record else None)
125+
)
126+
status = existing_record.status if existing_record else "pending"
127+
external = {"id": external_id, "url": external_url}
128+
129+
UserMetric.upsert(
130+
user_id=user_id,
131+
source=source,
132+
metrics=payload,
133+
external=external,
134+
status=status,
135+
)
98136
updated_metrics[source] = extracted_metrics
99137

100-
return updated_metrics
101-
138+
model.Session.commit()
102139

103-
@tk.side_effect_free
104-
@validate(schema.scientometrics_update_user_metrics)
105-
def scientometrics_get_user_metrics(
106-
context: types.Context, data_dict: dict[str, Any]
107-
) -> dict[str, Any]:
108-
"""Retrieve user scientometrics metrics.
109-
110-
Args:
111-
context (Dict[str, Any]): The CKAN action context.
112-
data_dict (Dict[str, Any]): A dictionary containing:
113-
- "user_id": The ID of the user whose metrics we want to retrieve.
114-
115-
Returns:
116-
Dict[str, Any]: The user's scientometrics metrics from the flake store.
117-
"""
118-
user_id_or_name = data_dict["user_id"]
140+
return updated_metrics
119141

120-
user_dict = tk.get_action("user_show")(
121-
{"ignore_auth": True}, {"id": user_id_or_name}
122-
)
123142

124-
return utils.get_metrics_from_flake(user_dict["id"])
143+
@validate(schema.scim_delete_user_metrics)
144+
def scim_delete_user_metrics(context: types.Context, data_dict: dict[str, Any]) -> int:
145+
"""Delete all scientometrics metrics for a user."""
146+
tk.check_access("scim_delete_user_metrics", context, data_dict)
147+
user_dict = tk.get_action("user_show")({"ignore_auth": True}, {"id": data_dict["user_id"]})
148+
deleted = UserMetric.delete_by_user_id(user_dict["id"])
149+
model.Session.commit()
150+
return deleted
151+
152+
153+
def _collect_authors(user_id: str, existing: dict[str, UserMetric]) -> dict[str, str]:
154+
"""Collect authors strictly from user extras (scim or legacy scientometrics)."""
155+
authors: dict[str, str] = {}
156+
user_obj = model.User.get(user_id)
157+
extras = (user_obj.plugin_extras or {}) if user_obj else {}
158+
scim_extras = extras.get("scim") or extras.get("scientometrics") or {}
159+
for key, val in scim_extras.items():
160+
if key.endswith("_author_id") and val:
161+
authors[key.removesuffix("_author_id")] = val
162+
return authors

ckanext/scientometrics/logic/auth.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22

33
from typing import Any
44

5-
from ckan import authz, types
5+
from ckan import types
66

77

8-
def scientometrics_update_user_metrics(
9-
context: types.Context, data_dict: dict[str, Any]
10-
):
11-
return authz.is_authorized("sysadmin", context, data_dict)
8+
def scim_update_user_metrics(context: types.Context, data_dict: dict[str, Any]):
9+
return {"success": False}
10+
11+
12+
def scim_get_user_metrics(context: types.Context, data_dict: dict[str, Any]):
13+
return {"success": True}
14+
15+
16+
def scim_delete_user_metrics(context: types.Context, data_dict: dict[str, Any]):
17+
return {"success": False}
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
1-
from typing import Any
2-
31
from ckan import types
42
from ckan.logic.schema import validator_args
53

64
from ckanext.scientometrics import config
75

8-
Schema = dict[str, Any]
9-
106

117
@validator_args
128
def user_extras(ignore_empty: types.Validator) -> types.Schema:
13-
return {
14-
source + "_author_id": [ignore_empty] for source in config.enabled_metrics()
15-
}
9+
return {source + "_author_id": [ignore_empty] for source in config.enabled_metrics()}
1610

1711

1812
@validator_args
19-
def scientometrics_update_user_metrics(
13+
def scim_update_user_metrics(
2014
not_empty: types.Validator,
2115
default: types.Validator,
2216
convert_to_list_if_string: types.Validator,
@@ -28,3 +22,12 @@ def scientometrics_update_user_metrics(
2822
convert_to_list_if_string,
2923
],
3024
}
25+
26+
27+
@validator_args
28+
def scim_delete_user_metrics(
29+
not_empty: types.Validator,
30+
) -> types.Schema:
31+
return {
32+
"user_id": [not_empty],
33+
}

ckanext/scientometrics/migration/scientometrics/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def include_object(
3737
compare_to: Any,
3838
):
3939
if type_ == "table":
40-
return object_name.startswith(name)
40+
return object_name.startswith("scim_")
4141
return True
4242

4343

ckanext/scientometrics/plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ckan.plugins.toolkit as tk
22
from ckan import plugins as p
3+
from ckan.common import CKANConfig
34

45

56
@tk.blanket.helpers
@@ -13,7 +14,7 @@ class ScientometricsPlugin(p.SingletonPlugin):
1314

1415
# IConfigurer
1516

16-
def update_config(self, config_):
17+
def update_config(self, config_: CKANConfig):
1718
tk.add_template_directory(config_, "templates")
1819
tk.add_public_directory(config_, "public")
1920
tk.add_resource("assets", "scientometrics")

ckanext/scientometrics/templates/user/edit_user_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{% ckan_extends %}
22

33
{% block extra_fields %}
4-
{% set enabled_metrics = h.scientometrics_get_enabled_metrics() %}
4+
{% set enabled_metrics = h.scim_get_enabled_metrics() %}
55
{% for metric in enabled_metrics %}
66
{% snippet 'user/snippets/' + metric + '_field.html', form=form, data=data, errors=errors %}
77
{% endfor %}

0 commit comments

Comments
 (0)