pub-analyzer 0.1.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pub-analyzer might be problematic. Click here for more details.
- pub_analyzer/css/body.tcss +48 -35
- pub_analyzer/css/buttons.tcss +0 -1
- pub_analyzer/css/collapsible.tcss +31 -0
- pub_analyzer/css/main.tcss +4 -0
- pub_analyzer/css/summary.tcss +75 -0
- pub_analyzer/internal/identifier.py +36 -10
- pub_analyzer/internal/render.py +1 -1
- pub_analyzer/internal/report.py +177 -53
- pub_analyzer/internal/templates/author/{author_resume.typ → author_summary.typ} +4 -3
- pub_analyzer/internal/templates/author/report.typ +4 -3
- pub_analyzer/internal/templates/author/sources.typ +7 -5
- pub_analyzer/internal/templates/author/works.typ +12 -12
- pub_analyzer/internal/templates/author/works_extended.typ +4 -4
- pub_analyzer/main.py +6 -7
- pub_analyzer/models/author.py +20 -28
- pub_analyzer/models/concept.py +19 -0
- pub_analyzer/models/institution.py +22 -5
- pub_analyzer/models/report.py +14 -14
- pub_analyzer/models/source.py +59 -3
- pub_analyzer/models/topic.py +59 -0
- pub_analyzer/models/work.py +30 -7
- pub_analyzer/widgets/author/cards.py +15 -14
- pub_analyzer/widgets/author/core.py +80 -115
- pub_analyzer/widgets/author/tables.py +1 -1
- pub_analyzer/widgets/common/__init__.py +6 -6
- pub_analyzer/widgets/common/filesystem.py +16 -13
- pub_analyzer/widgets/common/filters.py +111 -0
- pub_analyzer/widgets/common/input.py +14 -5
- pub_analyzer/widgets/common/selector.py +1 -1
- pub_analyzer/widgets/common/summary.py +7 -0
- pub_analyzer/widgets/institution/cards.py +13 -15
- pub_analyzer/widgets/institution/core.py +81 -115
- pub_analyzer/widgets/institution/tables.py +1 -1
- pub_analyzer/widgets/report/cards.py +33 -31
- pub_analyzer/widgets/report/concept.py +47 -0
- pub_analyzer/widgets/report/core.py +90 -20
- pub_analyzer/widgets/report/export.py +2 -2
- pub_analyzer/widgets/report/grants.py +46 -0
- pub_analyzer/widgets/report/locations.py +14 -12
- pub_analyzer/widgets/report/source.py +22 -14
- pub_analyzer/widgets/report/topic.py +55 -0
- pub_analyzer/widgets/report/work.py +70 -34
- pub_analyzer/widgets/search/__init__.py +4 -4
- pub_analyzer/widgets/search/results.py +15 -16
- pub_analyzer/widgets/sidebar.py +11 -9
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/METADATA +31 -7
- pub_analyzer-0.3.0.dist-info/RECORD +69 -0
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/WHEEL +1 -1
- pub_analyzer/css/author.tcss +0 -78
- pub_analyzer/css/institution.tcss +0 -78
- pub_analyzer-0.1.2.dist-info/RECORD +0 -62
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/LICENSE +0 -0
- {pub_analyzer-0.1.2.dist-info → pub_analyzer-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -7,10 +7,10 @@ from .modal import Modal
|
|
|
7
7
|
from .selector import Select
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
"Card",
|
|
11
|
+
"DateInput",
|
|
12
|
+
"FileSystemSelector",
|
|
13
|
+
"Input",
|
|
14
|
+
"Modal",
|
|
15
|
+
"Select",
|
|
16
16
|
]
|
|
@@ -28,7 +28,9 @@ class PathTypeSelector(Enum):
|
|
|
28
28
|
class FilteredDirectoryTree(DirectoryTree):
|
|
29
29
|
"""Directory Tree filtered."""
|
|
30
30
|
|
|
31
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self, path: str | Path, *, show_hidden_paths: bool = False, only_dir: bool = False, extension: list[str] | None = None
|
|
33
|
+
) -> None:
|
|
32
34
|
self.show_hidden_paths = show_hidden_paths
|
|
33
35
|
self.only_dir = only_dir
|
|
34
36
|
self.extension = extension
|
|
@@ -78,7 +80,9 @@ class PathSelectorModal(Modal[Path | None]):
|
|
|
78
80
|
}
|
|
79
81
|
"""
|
|
80
82
|
|
|
81
|
-
def __init__(
|
|
83
|
+
def __init__(
|
|
84
|
+
self, path: str | Path, show_hidden_paths: bool = False, only_dir: bool = False, extension: list[str] | None = None
|
|
85
|
+
) -> None:
|
|
82
86
|
self.path = path
|
|
83
87
|
self.show_hidden_paths = show_hidden_paths
|
|
84
88
|
self.only_dir = only_dir
|
|
@@ -90,7 +94,7 @@ class PathSelectorModal(Modal[Path | None]):
|
|
|
90
94
|
@on(events.Key)
|
|
91
95
|
def exit_modal(self, message: events.Key) -> None:
|
|
92
96
|
"""Exit from the modal with esc KEY."""
|
|
93
|
-
if message.key ==
|
|
97
|
+
if message.key == "escape":
|
|
94
98
|
self.app.pop_screen()
|
|
95
99
|
|
|
96
100
|
@on(Button.Pressed, "#done-button")
|
|
@@ -114,11 +118,10 @@ class PathSelectorModal(Modal[Path | None]):
|
|
|
114
118
|
|
|
115
119
|
def compose(self) -> ComposeResult:
|
|
116
120
|
"""Compose Modal."""
|
|
117
|
-
with VerticalScroll(id=
|
|
118
|
-
yield Label("Export Path", classes=
|
|
121
|
+
with VerticalScroll(id="dialog"):
|
|
122
|
+
yield Label("Export Path", classes="dialog-title")
|
|
119
123
|
yield FilteredDirectoryTree(
|
|
120
|
-
path=self.path, show_hidden_paths=self.show_hidden_paths, only_dir=self.only_dir,
|
|
121
|
-
extension=self.extension
|
|
124
|
+
path=self.path, show_hidden_paths=self.show_hidden_paths, only_dir=self.only_dir, extension=self.extension
|
|
122
125
|
)
|
|
123
126
|
|
|
124
127
|
with Horizontal(classes="button-container"):
|
|
@@ -165,7 +168,9 @@ class FileSystemSelector(Static):
|
|
|
165
168
|
self.file_selected = file_selected
|
|
166
169
|
super().__init__()
|
|
167
170
|
|
|
168
|
-
def __init__(
|
|
171
|
+
def __init__(
|
|
172
|
+
self, path: str | Path, show_hidden_paths: bool = False, only_dir: bool = False, extension: list[str] | None = None
|
|
173
|
+
) -> None:
|
|
169
174
|
self.path = path
|
|
170
175
|
self.show_hidden_paths = show_hidden_paths
|
|
171
176
|
self.only_dir = only_dir
|
|
@@ -177,6 +182,7 @@ class FileSystemSelector(Static):
|
|
|
177
182
|
@on(PathSelectedBox.Selected)
|
|
178
183
|
async def show_export_report_modal(self) -> None:
|
|
179
184
|
"""Show export Modal."""
|
|
185
|
+
|
|
180
186
|
def update_file_selected(path: Path | None) -> None:
|
|
181
187
|
"""Call when modal is closed."""
|
|
182
188
|
self.path_selected = path
|
|
@@ -188,11 +194,8 @@ class FileSystemSelector(Static):
|
|
|
188
194
|
self.post_message(self.FileSelected(file_selected=path))
|
|
189
195
|
|
|
190
196
|
await self.app.push_screen(
|
|
191
|
-
PathSelectorModal(
|
|
192
|
-
|
|
193
|
-
extension=self.extension
|
|
194
|
-
),
|
|
195
|
-
callback=update_file_selected
|
|
197
|
+
PathSelectorModal(path=self.path, show_hidden_paths=self.show_hidden_paths, only_dir=self.only_dir, extension=self.extension),
|
|
198
|
+
callback=update_file_selected,
|
|
196
199
|
)
|
|
197
200
|
|
|
198
201
|
def compose(self) -> ComposeResult:
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Filters selectors for OpenAlex API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from rich.console import RenderableType
|
|
6
|
+
from textual import on
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Horizontal
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.reactive import reactive, var
|
|
11
|
+
from textual.widgets import Checkbox, Static
|
|
12
|
+
|
|
13
|
+
from .input import DateInput
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Filter(Static):
|
|
17
|
+
"""Base filter."""
|
|
18
|
+
|
|
19
|
+
class Changed(Message):
|
|
20
|
+
"""Posted when any input in the filter changes."""
|
|
21
|
+
|
|
22
|
+
filter_disabled: reactive[bool] = reactive(True)
|
|
23
|
+
"""Is filter inputs disabled?"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def validation_state(self) -> bool:
|
|
27
|
+
"""Return true if all valitadtion passes."""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DateRangeFilter(Filter):
|
|
32
|
+
"""Date range selector."""
|
|
33
|
+
|
|
34
|
+
DEFAULT_CSS = """
|
|
35
|
+
DateRangeFilter {
|
|
36
|
+
height: auto;
|
|
37
|
+
layout: horizontal;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
DateRangeFilter Checkbox {
|
|
41
|
+
width: 1fr;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
DateRangeFilter .filter-inputs {
|
|
45
|
+
height: auto;
|
|
46
|
+
width: 3fr;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
DateRangeFilter DateInput {
|
|
50
|
+
width: 1fr;
|
|
51
|
+
}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from_date: var[datetime | None] = var(None)
|
|
55
|
+
to_date: var[datetime | None] = var(None)
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
checkbox_label: str = "Date Range",
|
|
60
|
+
renderable: RenderableType = "",
|
|
61
|
+
*,
|
|
62
|
+
expand: bool = False,
|
|
63
|
+
shrink: bool = False,
|
|
64
|
+
markup: bool = True,
|
|
65
|
+
name: str | None = None,
|
|
66
|
+
id: str | None = None,
|
|
67
|
+
classes: str | None = None,
|
|
68
|
+
disabled: bool = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.checkbox_label = checkbox_label
|
|
71
|
+
super().__init__(renderable, expand=expand, shrink=shrink, markup=markup, name=name, id=id, classes=classes, disabled=disabled)
|
|
72
|
+
|
|
73
|
+
def compose(self) -> ComposeResult:
|
|
74
|
+
"""Compose Date range selector."""
|
|
75
|
+
yield Checkbox(self.checkbox_label, value=False, id="filter-checkbox")
|
|
76
|
+
with Horizontal(classes="filter-inputs", disabled=True):
|
|
77
|
+
yield DateInput(placeholder="From yyyy-mm-dd", id="from-date")
|
|
78
|
+
yield DateInput(placeholder="To yyyy-mm-dd", id="to-date")
|
|
79
|
+
|
|
80
|
+
def watch_filter_disabled(self, is_filter_disabled: bool) -> None:
|
|
81
|
+
"""Toggle filter disable status with the reactive attribute."""
|
|
82
|
+
self.query_one(".filter-inputs", Horizontal).disabled = is_filter_disabled
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def validation_state(self) -> bool:
|
|
86
|
+
"""Return true if all datetime inputs are correctly formatted."""
|
|
87
|
+
return all([self.from_date, self.to_date])
|
|
88
|
+
|
|
89
|
+
@on(Checkbox.Changed)
|
|
90
|
+
def toggle_filter_disabled(self, event: Checkbox.Changed) -> None:
|
|
91
|
+
"""Toggle filter enabled status."""
|
|
92
|
+
event.stop()
|
|
93
|
+
self.post_message(self.Changed())
|
|
94
|
+
self.filter_disabled = not event.value
|
|
95
|
+
|
|
96
|
+
@on(DateInput.Changed)
|
|
97
|
+
def date_input_handler(self, event: DateInput.Changed) -> None:
|
|
98
|
+
"""Handle date input change."""
|
|
99
|
+
event.stop()
|
|
100
|
+
self.post_message(self.Changed())
|
|
101
|
+
date_input = event.input
|
|
102
|
+
|
|
103
|
+
if event.validation_result:
|
|
104
|
+
new_value = datetime.strptime(event.value, "%Y-%m-%d") if event.validation_result.is_valid else None
|
|
105
|
+
else:
|
|
106
|
+
new_value = None
|
|
107
|
+
|
|
108
|
+
if date_input.id == "from-date":
|
|
109
|
+
self.from_date = new_value
|
|
110
|
+
elif date_input.id == "to-date":
|
|
111
|
+
self.to_date = new_value
|
|
@@ -38,7 +38,7 @@ class Input(TextualInput):
|
|
|
38
38
|
@on(Key)
|
|
39
39
|
def exit_focus(self, event: Key) -> None:
|
|
40
40
|
"""Unfocus from the input with esc KEY."""
|
|
41
|
-
if event.key ==
|
|
41
|
+
if event.key == "escape":
|
|
42
42
|
self.screen.set_focus(None)
|
|
43
43
|
|
|
44
44
|
|
|
@@ -57,6 +57,7 @@ class DateSuggester(Suggester):
|
|
|
57
57
|
return f"{year}-{month}-{day}"
|
|
58
58
|
return None
|
|
59
59
|
|
|
60
|
+
|
|
60
61
|
class DateInput(Input):
|
|
61
62
|
"""Input with Date validation."""
|
|
62
63
|
|
|
@@ -76,13 +77,21 @@ class DateInput(Input):
|
|
|
76
77
|
suggester = DateSuggester()
|
|
77
78
|
|
|
78
79
|
super().__init__(
|
|
79
|
-
value,
|
|
80
|
-
|
|
80
|
+
value,
|
|
81
|
+
placeholder,
|
|
82
|
+
highlighter,
|
|
83
|
+
password,
|
|
84
|
+
suggester=suggester,
|
|
85
|
+
validators=validators,
|
|
86
|
+
name=name,
|
|
87
|
+
id=id,
|
|
88
|
+
classes=classes,
|
|
89
|
+
disabled=disabled,
|
|
81
90
|
)
|
|
82
91
|
|
|
83
92
|
self.validators.append(
|
|
84
93
|
Regex(
|
|
85
|
-
regex=r"^(
|
|
86
|
-
failure_description="Input must be formatted as `yyyy-mm-dd`"
|
|
94
|
+
regex=r"^(?P<year>\d{4})-((?P<longMonth>(0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|(?P<shortMonth>(0[469]|11)-(0[1-9]|[12][0-9]|30))|(?P<februaryMonth>(02)-([01][1-9]|[2][0-8])))$",
|
|
95
|
+
failure_description="Input must be formatted as `yyyy-mm-dd`",
|
|
87
96
|
)
|
|
88
97
|
)
|
|
@@ -19,12 +19,12 @@ class CitationMetricsCard(Card):
|
|
|
19
19
|
|
|
20
20
|
def compose(self) -> ComposeResult:
|
|
21
21
|
"""Compose card."""
|
|
22
|
-
yield Label(
|
|
22
|
+
yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
|
|
23
23
|
|
|
24
|
-
with Vertical(classes=
|
|
25
|
-
yield Label(f
|
|
26
|
-
yield Label(f
|
|
27
|
-
yield Label(f
|
|
24
|
+
with Vertical(classes="card-container"):
|
|
25
|
+
yield Label(f"[bold]2-year mean:[/bold] {self.institution.summary_stats.two_yr_mean_citedness:.5f}")
|
|
26
|
+
yield Label(f"[bold]h-index:[/bold] {self.institution.summary_stats.h_index}")
|
|
27
|
+
yield Label(f"[bold]i10 index:[/bold] {self.institution.summary_stats.i10_index}")
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class IdentifiersCard(Card):
|
|
@@ -36,7 +36,7 @@ class IdentifiersCard(Card):
|
|
|
36
36
|
|
|
37
37
|
def compose(self) -> ComposeResult:
|
|
38
38
|
"""Compose card."""
|
|
39
|
-
yield Label(
|
|
39
|
+
yield Label("[italic]Identifiers:[/italic]", classes="card-title")
|
|
40
40
|
|
|
41
41
|
for platform, platform_url in self.institution.ids.model_dump().items():
|
|
42
42
|
if platform_url:
|
|
@@ -55,11 +55,11 @@ class GeoCard(Card):
|
|
|
55
55
|
|
|
56
56
|
def compose(self) -> ComposeResult:
|
|
57
57
|
"""Compose card."""
|
|
58
|
-
yield Label(
|
|
58
|
+
yield Label("[italic]Geo:[/italic]", classes="card-title")
|
|
59
59
|
|
|
60
|
-
with Vertical(classes=
|
|
61
|
-
yield Label(f
|
|
62
|
-
yield Label(f
|
|
60
|
+
with Vertical(classes="card-container"):
|
|
61
|
+
yield Label(f"[bold]City:[/bold] {self.institution.geo.city}")
|
|
62
|
+
yield Label(f"[bold]Country:[/bold] {self.institution.geo.country}")
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
class RolesCard(Card):
|
|
@@ -71,10 +71,8 @@ class RolesCard(Card):
|
|
|
71
71
|
|
|
72
72
|
def compose(self) -> ComposeResult:
|
|
73
73
|
"""Compose card."""
|
|
74
|
-
yield Label(
|
|
74
|
+
yield Label("[italic]Works by roles:[/italic]", classes="card-title")
|
|
75
75
|
|
|
76
|
-
with Vertical(classes=
|
|
76
|
+
with Vertical(classes="card-container"):
|
|
77
77
|
for role in self.institution.roles:
|
|
78
|
-
yield Label(
|
|
79
|
-
f"""[@click=app.open_link('{quote(str(role.id))}')]{role.role.value.title()}[/]: {role.works_count}"""
|
|
80
|
-
)
|
|
78
|
+
yield Label(f"""[@click=app.open_link('{quote(str(role.id))}')]{role.role.value.title()}[/]: {role.works_count}""")
|
|
@@ -1,82 +1,84 @@
|
|
|
1
1
|
"""Module with Widgets that allows to display the complete information of Institution using OpenAlex."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
from textual import on
|
|
7
7
|
from textual.app import ComposeResult
|
|
8
|
-
from textual.containers import Container, Horizontal, Vertical
|
|
9
|
-
from textual.widgets import Button,
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
9
|
+
from textual.widgets import Button, Collapsible, Label, Static
|
|
10
10
|
|
|
11
11
|
from pub_analyzer.internal.identifier import get_institution_id
|
|
12
12
|
from pub_analyzer.models.institution import Institution, InstitutionResult
|
|
13
|
-
from pub_analyzer.widgets.common import
|
|
13
|
+
from pub_analyzer.widgets.common.filters import DateRangeFilter, Filter
|
|
14
|
+
from pub_analyzer.widgets.common.summary import SummaryWidget
|
|
14
15
|
from pub_analyzer.widgets.report.core import CreateInstitutionReportWidget
|
|
15
16
|
|
|
16
17
|
from .cards import CitationMetricsCard, IdentifiersCard, RolesCard
|
|
17
18
|
from .tables import InstitutionWorksByYearTable
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class
|
|
21
|
-
"""Institution info
|
|
21
|
+
class _InstitutionSummaryWidget(Static):
|
|
22
|
+
"""Institution info summary."""
|
|
22
23
|
|
|
23
|
-
def __init__(self,
|
|
24
|
-
self.
|
|
25
|
-
self.institution: Institution
|
|
24
|
+
def __init__(self, institution: Institution) -> None:
|
|
25
|
+
self.institution = institution
|
|
26
26
|
super().__init__()
|
|
27
27
|
|
|
28
28
|
def compose(self) -> ComposeResult:
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
yield VerticalScroll(id="main-container")
|
|
29
|
+
"""Compose institution info."""
|
|
30
|
+
is_report_not_available = self.institution.works_count < 1
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
self.run_worker(self.load_data(), exclusive=True)
|
|
32
|
+
# Compose Cards
|
|
33
|
+
with Vertical(classes="block-container"):
|
|
34
|
+
yield Label("[bold]Institution info:[/bold]", classes="block-title")
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for date_input in self.query(DateInput).results(DateInput):
|
|
43
|
-
date_input.disabled = False
|
|
44
|
-
date_input.value = ""
|
|
45
|
-
else:
|
|
46
|
-
for date_input in self.query(DateInput).results(DateInput):
|
|
47
|
-
date_input.disabled = True
|
|
48
|
-
date_input.value = ""
|
|
49
|
-
self.query_one("#make-report-button", Button).disabled = False
|
|
50
|
-
|
|
51
|
-
@on(DateInput.Changed)
|
|
52
|
-
async def enable_make_report(self, event: DateInput.Changed) -> None:
|
|
53
|
-
"""Enable make report button."""
|
|
54
|
-
checkbox = self.query_one("#filters-checkbox", Checkbox)
|
|
55
|
-
|
|
56
|
-
if event.validation_result:
|
|
57
|
-
if not event.validation_result.is_valid and checkbox.value:
|
|
58
|
-
self.query_one("#make-report-button", Button).disabled = True
|
|
59
|
-
else:
|
|
60
|
-
self.query_one("#make-report-button", Button).disabled = False
|
|
36
|
+
with Horizontal(classes="cards-container"):
|
|
37
|
+
yield RolesCard(institution=self.institution)
|
|
38
|
+
yield IdentifiersCard(institution=self.institution)
|
|
39
|
+
yield CitationMetricsCard(institution=self.institution)
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
checkbox = self.query_one("#filters-checkbox", Checkbox)
|
|
66
|
-
from_input = self.query_one("#from-date", DateInput)
|
|
67
|
-
to_input = self.query_one("#to-date", DateInput)
|
|
41
|
+
# Work realeted info
|
|
42
|
+
with Vertical(classes="block-container"):
|
|
43
|
+
yield Label("[bold]Work Info:[/bold]", classes="block-title")
|
|
68
44
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
to_date = datetime.datetime.strptime(to_input.value, date_format)
|
|
45
|
+
with Horizontal(classes="info-container"):
|
|
46
|
+
yield Label(f"[bold]Cited by count:[/bold] {self.institution.cited_by_count}")
|
|
47
|
+
yield Label(f"[bold]Works count:[/bold] {self.institution.works_count}")
|
|
73
48
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
49
|
+
# Count by year table section
|
|
50
|
+
with Container(classes="table-container"):
|
|
51
|
+
yield InstitutionWorksByYearTable(institution=self.institution)
|
|
77
52
|
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
# Make report section
|
|
54
|
+
with Vertical(classes="block-container", disabled=is_report_not_available):
|
|
55
|
+
yield Label("[bold]Make report:[/bold]", classes="block-title")
|
|
56
|
+
|
|
57
|
+
# Filters
|
|
58
|
+
with Collapsible(title="Report filters.", classes="filter-collapsible"):
|
|
59
|
+
# Institution publication Date Range
|
|
60
|
+
yield DateRangeFilter(checkbox_label="Publication date range:", id="institution-date-range-filter")
|
|
61
|
+
|
|
62
|
+
# Cite Date Range
|
|
63
|
+
yield DateRangeFilter(checkbox_label="Cited date range:", id="cited-date-range-filter")
|
|
64
|
+
|
|
65
|
+
# Button
|
|
66
|
+
with Vertical(classes="block-container button-container"):
|
|
67
|
+
yield Button("Make Report", variant="primary", id="make-report-button")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class InstitutionSummaryWidget(SummaryWidget):
|
|
71
|
+
"""Institution info summary container."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, institution_result: InstitutionResult) -> None:
|
|
74
|
+
self.institution_result = institution_result
|
|
75
|
+
self.institution: Institution
|
|
76
|
+
super().__init__()
|
|
77
|
+
|
|
78
|
+
def on_mount(self) -> None:
|
|
79
|
+
"""Hide the empty container and call data in the background."""
|
|
80
|
+
self.loading = True
|
|
81
|
+
self.run_worker(self.load_data(), exclusive=True)
|
|
80
82
|
|
|
81
83
|
async def _get_info(self) -> None:
|
|
82
84
|
"""Query OpenAlex API."""
|
|
@@ -90,67 +92,31 @@ class InstitutionResumeWidget(Static):
|
|
|
90
92
|
async def load_data(self) -> None:
|
|
91
93
|
"""Query OpenAlex API and composing the widget."""
|
|
92
94
|
await self._get_info()
|
|
93
|
-
|
|
94
|
-
is_report_not_available = self.institution.works_count < 1
|
|
95
|
+
await self.mount(_InstitutionSummaryWidget(institution=self.institution))
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
await container.mount(
|
|
98
|
-
Vertical(
|
|
99
|
-
Label('[bold]Institution info:[/bold]', classes="block-title"),
|
|
100
|
-
Horizontal(
|
|
101
|
-
RolesCard(institution=self.institution),
|
|
102
|
-
IdentifiersCard(institution=self.institution),
|
|
103
|
-
CitationMetricsCard(institution=self.institution),
|
|
104
|
-
classes="cards-container"
|
|
105
|
-
),
|
|
106
|
-
classes="block-container"
|
|
107
|
-
)
|
|
108
|
-
)
|
|
97
|
+
self.loading = False
|
|
109
98
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Label(f'[bold]Cited by count:[/bold] {self.institution.cited_by_count}'),
|
|
116
|
-
Label(f'[bold]Works count:[/bold] {self.institution.works_count}'),
|
|
117
|
-
classes="info-container"
|
|
118
|
-
),
|
|
119
|
-
classes="block-container"
|
|
120
|
-
)
|
|
121
|
-
)
|
|
99
|
+
@on(Filter.Changed)
|
|
100
|
+
def filter_change(self) -> None:
|
|
101
|
+
"""Handle filter changes."""
|
|
102
|
+
filters = [filter for filter in self.query("_InstitutionSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
|
|
103
|
+
all_filters_valid = all(filter.validation_state for filter in filters)
|
|
122
104
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
classes="info-container filter-container",
|
|
142
|
-
),
|
|
143
|
-
|
|
144
|
-
# Button
|
|
145
|
-
Vertical(
|
|
146
|
-
Button("Make Report", variant="primary", id="make-report-button"),
|
|
147
|
-
classes="block-container button-container"
|
|
148
|
-
),
|
|
149
|
-
classes="block-container",
|
|
150
|
-
disabled=is_report_not_available
|
|
151
|
-
)
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
# Show results
|
|
155
|
-
self.query_one(LoadingIndicator).display = False
|
|
156
|
-
container.display = True
|
|
105
|
+
self.query_one("_InstitutionSummaryWidget #make-report-button", Button).disabled = not all_filters_valid
|
|
106
|
+
|
|
107
|
+
@on(Button.Pressed, "#make-report-button")
|
|
108
|
+
async def make_report(self) -> None:
|
|
109
|
+
"""Make the author report."""
|
|
110
|
+
filters: dict[str, Any] = {}
|
|
111
|
+
pub_date_range = self.query_one("#institution-date-range-filter", DateRangeFilter)
|
|
112
|
+
cited_date_range = self.query_one("#cited-date-range-filter", DateRangeFilter)
|
|
113
|
+
|
|
114
|
+
if not pub_date_range.filter_disabled:
|
|
115
|
+
filters.update({"pub_from_date": pub_date_range.from_date, "pub_to_date": pub_date_range.to_date})
|
|
116
|
+
|
|
117
|
+
if not cited_date_range.filter_disabled:
|
|
118
|
+
filters.update({"cited_from_date": cited_date_range.from_date, "cited_to_date": cited_date_range.to_date})
|
|
119
|
+
|
|
120
|
+
report_widget = CreateInstitutionReportWidget(institution=self.institution, **filters)
|
|
121
|
+
await self.app.query_one("MainContent").mount(report_widget)
|
|
122
|
+
await self.app.query_one("InstitutionSummaryWidget").remove()
|
|
@@ -16,7 +16,7 @@ class InstitutionWorksByYearTable(Static):
|
|
|
16
16
|
|
|
17
17
|
def compose(self) -> ComposeResult:
|
|
18
18
|
"""Compose Table."""
|
|
19
|
-
table = Table(
|
|
19
|
+
table = Table("Year", "Works Count", "Cited by Count", title="Counts by Year", expand=True)
|
|
20
20
|
for row in self.institution.counts_by_year:
|
|
21
21
|
year, works_count, cited_by_count = row.model_dump().values()
|
|
22
22
|
table.add_row(str(year), str(works_count), str(cited_by_count))
|