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
|
@@ -11,8 +11,8 @@ from pub_analyzer.models.report import AuthorReport, InstitutionReport, WorkRepo
|
|
|
11
11
|
from pub_analyzer.models.work import Work
|
|
12
12
|
from pub_analyzer.widgets.common import Card
|
|
13
13
|
|
|
14
|
-
# Works pane cards.
|
|
15
14
|
|
|
15
|
+
# Works pane cards.
|
|
16
16
|
class ReportCitationMetricsCard(Card):
|
|
17
17
|
"""Citation metrics for this report."""
|
|
18
18
|
|
|
@@ -22,20 +22,20 @@ class ReportCitationMetricsCard(Card):
|
|
|
22
22
|
|
|
23
23
|
def compose(self) -> ComposeResult:
|
|
24
24
|
"""Compose card."""
|
|
25
|
-
yield Label(
|
|
25
|
+
yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
|
|
26
26
|
|
|
27
|
-
with Vertical(classes=
|
|
28
|
-
type_a_count = self.report.
|
|
29
|
-
type_b_count = self.report.
|
|
27
|
+
with Vertical(classes="card-container"):
|
|
28
|
+
type_a_count = self.report.citation_summary.type_a_count
|
|
29
|
+
type_b_count = self.report.citation_summary.type_b_count
|
|
30
30
|
cited_by_count = type_a_count + type_b_count
|
|
31
31
|
|
|
32
|
-
yield Label(f
|
|
33
|
-
yield Label(f
|
|
34
|
-
yield Label(f
|
|
32
|
+
yield Label(f"[bold]Count:[/bold] {cited_by_count}")
|
|
33
|
+
yield Label(f"[bold]Type A:[/bold] {type_a_count}")
|
|
34
|
+
yield Label(f"[bold]Type B:[/bold] {type_b_count}")
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
class
|
|
38
|
-
"""Works Type Counters
|
|
37
|
+
class WorksTypeSummaryCard(Card):
|
|
38
|
+
"""Works Type Counters Summary Card."""
|
|
39
39
|
|
|
40
40
|
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
41
41
|
self.report = report
|
|
@@ -43,14 +43,14 @@ class WorksTypeResumeCard(Card):
|
|
|
43
43
|
|
|
44
44
|
def compose(self) -> ComposeResult:
|
|
45
45
|
"""Compose card."""
|
|
46
|
-
yield Label(
|
|
46
|
+
yield Label("[italic]Work Type[/italic]", classes="card-title")
|
|
47
47
|
|
|
48
|
-
with VerticalScroll(classes=
|
|
49
|
-
for work_type_counter in self.report.
|
|
50
|
-
yield Label(f
|
|
48
|
+
with VerticalScroll(classes="card-container"):
|
|
49
|
+
for work_type_counter in self.report.works_type_summary:
|
|
50
|
+
yield Label(f"[bold]{work_type_counter.type_name}:[/bold] {work_type_counter.count}")
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
class
|
|
53
|
+
class OpenAccessSummaryCard(Card):
|
|
54
54
|
"""Open Access counts for this report."""
|
|
55
55
|
|
|
56
56
|
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
@@ -59,11 +59,11 @@ class OpenAccessResumeCard(Card):
|
|
|
59
59
|
|
|
60
60
|
def compose(self) -> ComposeResult:
|
|
61
61
|
"""Compose card."""
|
|
62
|
-
yield Label(
|
|
62
|
+
yield Label("[italic]Open Access[/italic]", classes="card-title")
|
|
63
63
|
|
|
64
|
-
with VerticalScroll(classes=
|
|
65
|
-
for status, count in self.report.
|
|
66
|
-
yield Label(f
|
|
64
|
+
with VerticalScroll(classes="card-container"):
|
|
65
|
+
for status, count in self.report.open_access_summary.model_dump().items():
|
|
66
|
+
yield Label(f"[bold]{status}:[/bold] {count}")
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
# Work Info cards.
|
|
@@ -77,18 +77,20 @@ class AuthorshipCard(Card):
|
|
|
77
77
|
|
|
78
78
|
def compose(self) -> ComposeResult:
|
|
79
79
|
"""Compose card."""
|
|
80
|
-
yield Label(
|
|
80
|
+
yield Label("[italic]Authorships[/italic]", classes="card-title")
|
|
81
81
|
|
|
82
|
-
with VerticalScroll(classes=
|
|
82
|
+
with VerticalScroll(classes="card-container"):
|
|
83
83
|
for authorship in self.work.authorships:
|
|
84
84
|
# If the author was provided, highlight
|
|
85
85
|
if self.author and authorship.author.display_name == self.author.display_name:
|
|
86
|
-
author_name_formated = f
|
|
86
|
+
author_name_formated = f"[b #909d63]{authorship.author.display_name}[/]"
|
|
87
87
|
else:
|
|
88
88
|
author_name_formated = str(authorship.author.display_name)
|
|
89
89
|
|
|
90
90
|
external_id = authorship.author.orcid or authorship.author.id
|
|
91
|
-
yield Label(
|
|
91
|
+
yield Label(
|
|
92
|
+
f"""- [b]{authorship.author_position}:[/b] [@click=app.open_link('{quote(str(external_id))}')]{author_name_formated}[/]""" # noqa: E501
|
|
93
|
+
)
|
|
92
94
|
|
|
93
95
|
|
|
94
96
|
class OpenAccessCard(Card):
|
|
@@ -102,8 +104,8 @@ class OpenAccessCard(Card):
|
|
|
102
104
|
"""Compose card."""
|
|
103
105
|
work_url = self.work.open_access.oa_url
|
|
104
106
|
|
|
105
|
-
yield Label(
|
|
106
|
-
yield Label(f
|
|
107
|
+
yield Label("[italic]Open Access[/italic]", classes="card-title")
|
|
108
|
+
yield Label(f"[bold]Status:[/bold] {self.work.open_access.oa_status.value}")
|
|
107
109
|
if work_url:
|
|
108
110
|
yield Label(f"""[bold]URL:[/bold] [@click=app.open_link('{quote(str(work_url))}')]{work_url}[/]""")
|
|
109
111
|
|
|
@@ -117,12 +119,12 @@ class CitationMetricsCard(Card):
|
|
|
117
119
|
|
|
118
120
|
def compose(self) -> ComposeResult:
|
|
119
121
|
"""Compose card."""
|
|
120
|
-
type_a_count = self.work_report.
|
|
121
|
-
type_b_count = self.work_report.
|
|
122
|
+
type_a_count = self.work_report.citation_summary.type_a_count
|
|
123
|
+
type_b_count = self.work_report.citation_summary.type_b_count
|
|
122
124
|
cited_by_count = type_a_count + type_b_count
|
|
123
125
|
|
|
124
|
-
yield Label(
|
|
126
|
+
yield Label("[italic]Citation[/italic]", classes="card-title")
|
|
125
127
|
|
|
126
|
-
yield Label(f
|
|
127
|
-
yield Label(f
|
|
128
|
-
yield Label(f
|
|
128
|
+
yield Label(f"[bold]Count:[/bold] {cited_by_count}")
|
|
129
|
+
yield Label(f"[bold]Type A:[/bold] {type_a_count}")
|
|
130
|
+
yield Label(f"[bold]Type B:[/bold] {type_b_count}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Concepts Widgets."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
from pub_analyzer.models.concept import DehydratedConcept
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConceptsTable(Static):
|
|
14
|
+
"""All Concepts from a work in a table."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
ConceptsTable .concepts-table {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 1 2 0 2;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, concepts_list: list[DehydratedConcept]) -> None:
|
|
24
|
+
self.concepts_list = concepts_list
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
"""Compose Table."""
|
|
29
|
+
concepts_table = Table(title="Concepts", expand=True, show_lines=True)
|
|
30
|
+
|
|
31
|
+
# Define Columns
|
|
32
|
+
concepts_table.add_column("", justify="center", vertical="middle")
|
|
33
|
+
concepts_table.add_column("Name", ratio=5)
|
|
34
|
+
concepts_table.add_column("Score", ratio=1)
|
|
35
|
+
concepts_table.add_column("Level", ratio=1)
|
|
36
|
+
|
|
37
|
+
for idx, concept in enumerate(self.concepts_list):
|
|
38
|
+
name = f"""[@click=app.open_link('{quote(str(concept.wikidata))}')][u]{concept.display_name}[/u][/]"""
|
|
39
|
+
|
|
40
|
+
concepts_table.add_row(
|
|
41
|
+
str(idx),
|
|
42
|
+
Text.from_markup(name, overflow="ellipsis"),
|
|
43
|
+
Text.from_markup(f"{concept.score:.2f}"),
|
|
44
|
+
Text.from_markup(f"{concept.level:.1f}"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
yield Static(concepts_table, classes="concepts-table")
|
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
import datetime
|
|
4
4
|
import pathlib
|
|
5
5
|
from enum import Enum
|
|
6
|
+
from typing import ClassVar
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
from pydantic import TypeAdapter, ValidationError
|
|
9
10
|
from textual import on
|
|
10
11
|
from textual.app import ComposeResult
|
|
12
|
+
from textual.binding import Binding, BindingType
|
|
11
13
|
from textual.containers import Container, Horizontal
|
|
14
|
+
from textual.reactive import reactive
|
|
12
15
|
from textual.widget import Widget
|
|
13
16
|
from textual.widgets import Button, LoadingIndicator, Static, TabbedContent, TabPane
|
|
14
17
|
|
|
15
|
-
from pub_analyzer.internal.report import make_author_report, make_institution_report
|
|
18
|
+
from pub_analyzer.internal.report import FromDate, ToDate, make_author_report, make_institution_report
|
|
16
19
|
from pub_analyzer.models.author import Author
|
|
17
20
|
from pub_analyzer.models.institution import Institution
|
|
18
21
|
from pub_analyzer.models.report import AuthorReport, InstitutionReport
|
|
@@ -28,6 +31,17 @@ from .work import WorkReportPane
|
|
|
28
31
|
class ReportWidget(Static):
|
|
29
32
|
"""Base report widget."""
|
|
30
33
|
|
|
34
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
35
|
+
Binding(key="ctrl+y", action="toggle_works", description="Toggle empty works"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
show_empty_works: reactive[bool] = reactive(True)
|
|
39
|
+
|
|
40
|
+
async def action_toggle_works(self) -> None:
|
|
41
|
+
"""Toggle show empty works attribute."""
|
|
42
|
+
self.show_empty_works = not self.show_empty_works
|
|
43
|
+
await self.query_one(WorkReportPane).toggle_empty_works()
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
class AuthorReportWidget(ReportWidget):
|
|
33
47
|
"""Author report generator view."""
|
|
@@ -96,11 +110,11 @@ class CreateReportWidget(Static):
|
|
|
96
110
|
self.query_one(LoadingIndicator).display = False
|
|
97
111
|
status_error = f"HTTP Exception for url: {exc.request.url}. Status code: {exc.response.status_code}"
|
|
98
112
|
self.app.notify(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
title="Error making report!",
|
|
114
|
+
message=f"The report could not be generated due to a problem with the OpenAlex API. {status_error}",
|
|
115
|
+
severity="error",
|
|
116
|
+
timeout=20.0,
|
|
117
|
+
)
|
|
104
118
|
return None
|
|
105
119
|
|
|
106
120
|
container = self.query_one(Container)
|
|
@@ -114,32 +128,82 @@ class CreateReportWidget(Static):
|
|
|
114
128
|
class CreateAuthorReportWidget(CreateReportWidget):
|
|
115
129
|
"""Widget Author report wrapper to load data from API."""
|
|
116
130
|
|
|
117
|
-
def __init__(
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
author: Author,
|
|
134
|
+
pub_from_date: datetime.datetime | None = None,
|
|
135
|
+
pub_to_date: datetime.datetime | None = None,
|
|
136
|
+
cited_from_date: datetime.datetime | None = None,
|
|
137
|
+
cited_to_date: datetime.datetime | None = None,
|
|
138
|
+
) -> None:
|
|
118
139
|
self.author = author
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
|
|
141
|
+
# Author publication date range
|
|
142
|
+
self.pub_from_date = pub_from_date
|
|
143
|
+
self.pub_to_date = pub_to_date
|
|
144
|
+
|
|
145
|
+
# Cited date range
|
|
146
|
+
self.cited_from_date = cited_from_date
|
|
147
|
+
self.cited_to_date = cited_to_date
|
|
121
148
|
|
|
122
149
|
super().__init__()
|
|
123
150
|
|
|
124
151
|
async def make_report(self) -> AuthorReportWidget:
|
|
125
152
|
"""Make report and create the widget."""
|
|
126
|
-
|
|
153
|
+
pub_from_date = FromDate(self.pub_from_date) if self.pub_from_date else None
|
|
154
|
+
pub_to_date = ToDate(self.pub_to_date) if self.pub_to_date else None
|
|
155
|
+
|
|
156
|
+
cited_from_date = FromDate(self.cited_from_date) if self.cited_from_date else None
|
|
157
|
+
cited_to_date = ToDate(self.cited_to_date) if self.cited_to_date else None
|
|
158
|
+
|
|
159
|
+
report = await make_author_report(
|
|
160
|
+
author=self.author,
|
|
161
|
+
pub_from_date=pub_from_date,
|
|
162
|
+
pub_to_date=pub_to_date,
|
|
163
|
+
cited_from_date=cited_from_date,
|
|
164
|
+
cited_to_date=cited_to_date,
|
|
165
|
+
)
|
|
127
166
|
return AuthorReportWidget(report=report)
|
|
128
167
|
|
|
129
168
|
|
|
130
169
|
class CreateInstitutionReportWidget(CreateReportWidget):
|
|
131
170
|
"""Widget Institution report wrapper to load data from API."""
|
|
132
171
|
|
|
133
|
-
def __init__(
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
institution: Institution,
|
|
175
|
+
pub_from_date: datetime.datetime | None = None,
|
|
176
|
+
pub_to_date: datetime.datetime | None = None,
|
|
177
|
+
cited_from_date: datetime.datetime | None = None,
|
|
178
|
+
cited_to_date: datetime.datetime | None = None,
|
|
179
|
+
) -> None:
|
|
134
180
|
self.institution = institution
|
|
135
|
-
|
|
136
|
-
|
|
181
|
+
|
|
182
|
+
# Institution publication date range
|
|
183
|
+
self.pub_from_date = pub_from_date
|
|
184
|
+
self.pub_to_date = pub_to_date
|
|
185
|
+
|
|
186
|
+
# Cited date range
|
|
187
|
+
self.cited_from_date = cited_from_date
|
|
188
|
+
self.cited_to_date = cited_to_date
|
|
137
189
|
|
|
138
190
|
super().__init__()
|
|
139
191
|
|
|
140
192
|
async def make_report(self) -> InstitutionReportWidget:
|
|
141
193
|
"""Make report and create the widget."""
|
|
142
|
-
|
|
194
|
+
pub_from_date = FromDate(self.pub_from_date) if self.pub_from_date else None
|
|
195
|
+
pub_to_date = ToDate(self.pub_to_date) if self.pub_to_date else None
|
|
196
|
+
|
|
197
|
+
cited_from_date = FromDate(self.cited_from_date) if self.cited_from_date else None
|
|
198
|
+
cited_to_date = ToDate(self.cited_to_date) if self.cited_to_date else None
|
|
199
|
+
|
|
200
|
+
report = await make_institution_report(
|
|
201
|
+
institution=self.institution,
|
|
202
|
+
pub_from_date=pub_from_date,
|
|
203
|
+
pub_to_date=pub_to_date,
|
|
204
|
+
cited_from_date=cited_from_date,
|
|
205
|
+
cited_to_date=cited_to_date,
|
|
206
|
+
)
|
|
143
207
|
return InstitutionReportWidget(report=report)
|
|
144
208
|
|
|
145
209
|
|
|
@@ -199,11 +263,11 @@ class LoadReportWidget(Static):
|
|
|
199
263
|
main_content.update_title(title=institution_report.institution.display_name)
|
|
200
264
|
except ValidationError:
|
|
201
265
|
self.app.notify(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
266
|
+
title="Error loading report!",
|
|
267
|
+
message="The report does not have the correct structure. This may be because it is an old version or because it is not of the specified type.", # noqa: E501
|
|
268
|
+
severity="error",
|
|
269
|
+
timeout=10.0,
|
|
270
|
+
)
|
|
207
271
|
|
|
208
272
|
@on(Select.Changed)
|
|
209
273
|
async def on_select_entity(self, event: Select.Changed) -> None:
|
|
@@ -221,7 +285,13 @@ class LoadReportWidget(Static):
|
|
|
221
285
|
with Horizontal(classes="filesystem-selector-container"):
|
|
222
286
|
entity_options = [(name.title(), endpoint) for name, endpoint in self.EntityType.__members__.items()]
|
|
223
287
|
|
|
224
|
-
yield FileSystemSelector(
|
|
288
|
+
yield FileSystemSelector(
|
|
289
|
+
path=pathlib.Path.home(),
|
|
290
|
+
only_dir=False,
|
|
291
|
+
extension=[
|
|
292
|
+
".json",
|
|
293
|
+
],
|
|
294
|
+
)
|
|
225
295
|
yield self.EntityTypeSelector(options=entity_options, value=self.entity_handler, allow_blank=False)
|
|
226
296
|
|
|
227
297
|
with Horizontal(classes="button-container"):
|
|
@@ -75,7 +75,7 @@ class ExportReportPane(VerticalScroll):
|
|
|
75
75
|
self.app.notify,
|
|
76
76
|
title="Report exported successfully!",
|
|
77
77
|
message=f"The report was exported correctly. You can go see it at [i]{file_path}[/]",
|
|
78
|
-
timeout=20.0
|
|
78
|
+
timeout=20.0,
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
@on(Button.Pressed, "#export-report-button")
|
|
@@ -98,7 +98,7 @@ class ExportReportPane(VerticalScroll):
|
|
|
98
98
|
with Vertical(classes="export-form-input-container"):
|
|
99
99
|
yield Label("[b]Name File:[/]", classes="export-form-label")
|
|
100
100
|
with Horizontal(classes="file-selector-container"):
|
|
101
|
-
type_options =
|
|
101
|
+
type_options = list(self.ExportFileType.__members__.items())
|
|
102
102
|
selector_disabled = isinstance(self.report, InstitutionReport)
|
|
103
103
|
|
|
104
104
|
yield Input(value=suggest_file_name, placeholder="report.json", classes="export-form-input")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Grants Widgets."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
from pub_analyzer.models.work import Grant
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GrantsTable(Static):
|
|
14
|
+
"""All Grants from a work in a table."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
GrantsTable .grants-table {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 1 2 0 2;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, grants_list: list[Grant]) -> None:
|
|
24
|
+
self.grants_list = grants_list
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
"""Compose Table."""
|
|
29
|
+
grants_table = Table(title="Grants", expand=True, show_lines=True)
|
|
30
|
+
|
|
31
|
+
# Define Columns
|
|
32
|
+
grants_table.add_column("", justify="center", vertical="middle")
|
|
33
|
+
grants_table.add_column("Name", ratio=3)
|
|
34
|
+
grants_table.add_column("Award ID", ratio=2)
|
|
35
|
+
|
|
36
|
+
for idx, grant in enumerate(self.grants_list):
|
|
37
|
+
name = f"""[@click=app.open_link('{quote(str(grant.funder))}')][u]{grant.funder_display_name}[/u][/]"""
|
|
38
|
+
award_id = grant.award_id or "-"
|
|
39
|
+
|
|
40
|
+
grants_table.add_row(
|
|
41
|
+
str(idx),
|
|
42
|
+
Text.from_markup(name, overflow="ellipsis"),
|
|
43
|
+
Text.from_markup(award_id),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
yield Static(grants_table, classes="grants-table")
|
|
@@ -26,17 +26,17 @@ class LocationsTable(Static):
|
|
|
26
26
|
|
|
27
27
|
def compose(self) -> ComposeResult:
|
|
28
28
|
"""Compose Table."""
|
|
29
|
-
locations_table = Table(title=
|
|
29
|
+
locations_table = Table(title="Locations", expand=True, show_lines=True)
|
|
30
30
|
|
|
31
31
|
# Define Columns
|
|
32
|
-
locations_table.add_column(
|
|
33
|
-
locations_table.add_column(
|
|
34
|
-
locations_table.add_column(
|
|
35
|
-
locations_table.add_column(
|
|
36
|
-
locations_table.add_column(
|
|
37
|
-
locations_table.add_column(
|
|
38
|
-
locations_table.add_column(
|
|
39
|
-
locations_table.add_column(
|
|
32
|
+
locations_table.add_column("", justify="center", vertical="middle")
|
|
33
|
+
locations_table.add_column("Name", ratio=3)
|
|
34
|
+
locations_table.add_column("Publisher or institution", ratio=2)
|
|
35
|
+
locations_table.add_column("Type")
|
|
36
|
+
locations_table.add_column("ISSN-L")
|
|
37
|
+
locations_table.add_column("Is OA")
|
|
38
|
+
locations_table.add_column("License")
|
|
39
|
+
locations_table.add_column("version")
|
|
40
40
|
|
|
41
41
|
for idx, location in enumerate(self.locations_list):
|
|
42
42
|
if location.source:
|
|
@@ -46,7 +46,9 @@ class LocationsTable(Static):
|
|
|
46
46
|
issn_l = source.issn_l if source.issn_l else "-"
|
|
47
47
|
|
|
48
48
|
if source.host_organization_name and source.host_organization:
|
|
49
|
-
publisher =
|
|
49
|
+
publisher = (
|
|
50
|
+
f"""[@click=app.open_link('{quote(str(source.host_organization))}')][u]{source.host_organization_name}[/u][/]"""
|
|
51
|
+
)
|
|
50
52
|
else:
|
|
51
53
|
publisher = source.host_organization_name if source.host_organization_name else "-"
|
|
52
54
|
else:
|
|
@@ -61,7 +63,7 @@ class LocationsTable(Static):
|
|
|
61
63
|
|
|
62
64
|
locations_table.add_row(
|
|
63
65
|
str(idx),
|
|
64
|
-
Text.from_markup(title, overflow=
|
|
66
|
+
Text.from_markup(title, overflow="ellipsis"),
|
|
65
67
|
Text.from_markup(publisher),
|
|
66
68
|
Text.from_markup(type),
|
|
67
69
|
Text.from_markup(issn_l),
|
|
@@ -70,4 +72,4 @@ class LocationsTable(Static):
|
|
|
70
72
|
Text.from_markup(version.capitalize()),
|
|
71
73
|
)
|
|
72
74
|
|
|
73
|
-
yield Static(locations_table, classes=
|
|
75
|
+
yield Static(locations_table, classes="locations-table")
|
|
@@ -9,7 +9,7 @@ from textual.containers import VerticalScroll
|
|
|
9
9
|
from textual.widgets import Static
|
|
10
10
|
|
|
11
11
|
from pub_analyzer.models.report import AuthorReport, InstitutionReport
|
|
12
|
-
from pub_analyzer.models.source import
|
|
12
|
+
from pub_analyzer.models.source import Source
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class SourcesTable(Static):
|
|
@@ -22,44 +22,52 @@ class SourcesTable(Static):
|
|
|
22
22
|
}
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
def __init__(self, sources_list: list[
|
|
25
|
+
def __init__(self, sources_list: list[Source]) -> None:
|
|
26
26
|
self.sources_list = sources_list
|
|
27
27
|
super().__init__()
|
|
28
28
|
|
|
29
29
|
def compose(self) -> ComposeResult:
|
|
30
30
|
"""Compose Table."""
|
|
31
|
-
sources_table = Table(title=
|
|
31
|
+
sources_table = Table(title="Sources", expand=True, show_lines=True)
|
|
32
32
|
|
|
33
33
|
# Define Columns
|
|
34
|
-
sources_table.add_column(
|
|
35
|
-
sources_table.add_column(
|
|
36
|
-
sources_table.add_column(
|
|
37
|
-
sources_table.add_column(
|
|
38
|
-
sources_table.add_column(
|
|
39
|
-
sources_table.add_column(
|
|
34
|
+
sources_table.add_column("", justify="center", vertical="middle")
|
|
35
|
+
sources_table.add_column("Name", ratio=3)
|
|
36
|
+
sources_table.add_column("Publisher or institution", ratio=2)
|
|
37
|
+
sources_table.add_column("Type")
|
|
38
|
+
sources_table.add_column("ISSN-L")
|
|
39
|
+
sources_table.add_column("Impact factor")
|
|
40
|
+
sources_table.add_column("h-index")
|
|
41
|
+
sources_table.add_column("Is OA")
|
|
40
42
|
|
|
41
43
|
for idx, source in enumerate(self.sources_list):
|
|
42
44
|
if source.host_organization_name:
|
|
43
|
-
host_organization =
|
|
45
|
+
host_organization = (
|
|
46
|
+
f"""[@click=app.open_link('{quote(str(source.host_organization))}')][u]{source.host_organization_name}[/u][/]"""
|
|
47
|
+
)
|
|
44
48
|
else:
|
|
45
49
|
host_organization = "-"
|
|
46
50
|
|
|
47
51
|
title = f"""[@click=app.open_link('{quote(str(source.id))}')][u]{source.display_name}[/u][/]"""
|
|
48
52
|
type_source = source.type
|
|
49
53
|
issn_l = source.issn_l if source.issn_l else "-"
|
|
54
|
+
impact_factor = f"{source.summary_stats.two_yr_mean_citedness:.3f}"
|
|
55
|
+
h_index = f"{source.summary_stats.h_index}"
|
|
56
|
+
|
|
50
57
|
is_open_access = "[#909d63]True[/]" if source.is_oa else "[#bc5653]False[/]"
|
|
51
58
|
|
|
52
59
|
sources_table.add_row(
|
|
53
60
|
str(idx),
|
|
54
|
-
Text.from_markup(title, overflow=
|
|
61
|
+
Text.from_markup(title, overflow="ellipsis"),
|
|
55
62
|
Text.from_markup(host_organization),
|
|
56
63
|
Text.from_markup(type_source),
|
|
57
64
|
Text.from_markup(issn_l),
|
|
65
|
+
Text.from_markup(impact_factor),
|
|
66
|
+
Text.from_markup(h_index),
|
|
58
67
|
Text.from_markup(is_open_access),
|
|
59
68
|
)
|
|
60
69
|
|
|
61
|
-
yield Static(sources_table, classes=
|
|
62
|
-
|
|
70
|
+
yield Static(sources_table, classes="sources-table")
|
|
63
71
|
|
|
64
72
|
|
|
65
73
|
class SourcesReportPane(VerticalScroll):
|
|
@@ -79,4 +87,4 @@ class SourcesReportPane(VerticalScroll):
|
|
|
79
87
|
|
|
80
88
|
def compose(self) -> ComposeResult:
|
|
81
89
|
"""Compose content pane."""
|
|
82
|
-
yield SourcesTable(sources_list=self.report.
|
|
90
|
+
yield SourcesTable(sources_list=self.report.sources_summary.sources)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Topics Widgets."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
from pub_analyzer.models.topic import DehydratedTopic
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TopicsTable(Static):
|
|
14
|
+
"""All Topics from a work in a table."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
TopicsTable .topics-table {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 1 2 0 2;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, topics_list: list[DehydratedTopic]) -> None:
|
|
24
|
+
self.topics_list = topics_list
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
"""Compose Table."""
|
|
29
|
+
topics_table = Table(title="Topics", expand=True, show_lines=True)
|
|
30
|
+
|
|
31
|
+
# Define Columns
|
|
32
|
+
topics_table.add_column("", justify="center", vertical="middle")
|
|
33
|
+
topics_table.add_column("Name", ratio=3)
|
|
34
|
+
topics_table.add_column("Score", ratio=1)
|
|
35
|
+
topics_table.add_column("Domain", ratio=1)
|
|
36
|
+
topics_table.add_column("Field", ratio=1)
|
|
37
|
+
topics_table.add_column("SubField", ratio=1)
|
|
38
|
+
|
|
39
|
+
for idx, topic in enumerate(self.topics_list):
|
|
40
|
+
name = f"""[@click=app.open_link('{quote(str(topic.id))}')][u]{topic.display_name}[/u][/]"""
|
|
41
|
+
|
|
42
|
+
domain = f"""[@click=app.open_link('{quote(str(topic.domain.id))}')][u]{topic.domain.display_name}[/u][/]"""
|
|
43
|
+
field = f"""[@click=app.open_link('{quote(str(topic.field.id))}')][u]{topic.field.display_name}[/u][/]"""
|
|
44
|
+
subfield = f"""[@click=app.open_link('{quote(str(topic.subfield.id))}')][u]{topic.subfield.display_name}[/u][/]"""
|
|
45
|
+
|
|
46
|
+
topics_table.add_row(
|
|
47
|
+
str(idx),
|
|
48
|
+
Text.from_markup(name, overflow="ellipsis"),
|
|
49
|
+
Text.from_markup(f"{topic.score:.2f}"),
|
|
50
|
+
Text.from_markup(domain),
|
|
51
|
+
Text.from_markup(field),
|
|
52
|
+
Text.from_markup(subfield),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
yield Static(topics_table, classes="topics-table")
|