pub-analyzer 0.2.0__py3-none-any.whl → 0.4.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 -4
- pub_analyzer/css/main.tcss +18 -12
- pub_analyzer/css/summary.tcss +75 -0
- pub_analyzer/internal/identifier.py +26 -0
- pub_analyzer/internal/report.py +73 -31
- pub_analyzer/internal/templates/author/author_summary.typ +112 -0
- pub_analyzer/internal/templates/author/report.typ +16 -3
- pub_analyzer/internal/templates/author/sources.typ +7 -5
- pub_analyzer/internal/templates/author/works.typ +119 -32
- pub_analyzer/internal/templates/author/works_extended.typ +5 -6
- pub_analyzer/main.py +8 -3
- pub_analyzer/models/author.py +9 -25
- pub_analyzer/models/concept.py +19 -0
- pub_analyzer/models/institution.py +11 -1
- 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 +23 -0
- pub_analyzer/widgets/author/cards.py +6 -5
- pub_analyzer/widgets/author/core.py +11 -10
- pub_analyzer/widgets/common/summary.py +7 -0
- pub_analyzer/widgets/institution/core.py +10 -9
- pub_analyzer/widgets/report/cards.py +10 -11
- pub_analyzer/widgets/report/concept.py +47 -0
- pub_analyzer/widgets/report/core.py +14 -0
- pub_analyzer/widgets/report/grants.py +46 -0
- pub_analyzer/widgets/report/source.py +11 -4
- pub_analyzer/widgets/report/topic.py +55 -0
- pub_analyzer/widgets/report/work.py +45 -9
- pub_analyzer/widgets/search/results.py +8 -8
- pub_analyzer/widgets/sidebar.py +11 -2
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/METADATA +8 -7
- pub_analyzer-0.4.0.dist-info/RECORD +69 -0
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/WHEEL +1 -1
- pub_analyzer/css/author.tcss +0 -82
- pub_analyzer/css/institution.tcss +0 -82
- pub_analyzer/internal/templates/author/author_resume.typ +0 -96
- pub_analyzer-0.2.0.dist-info/RECORD +0 -64
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/LICENSE +0 -0
- {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -5,20 +5,21 @@ from typing import Any
|
|
|
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
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
9
9
|
from textual.widgets import Button, Collapsible, Label, Static
|
|
10
10
|
|
|
11
11
|
from pub_analyzer.internal.identifier import get_author_id
|
|
12
12
|
from pub_analyzer.models.author import Author, AuthorResult
|
|
13
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 CreateAuthorReportWidget
|
|
15
16
|
|
|
16
17
|
from .cards import CitationMetricsCard, IdentifiersCard, LastInstitutionCard
|
|
17
18
|
from .tables import AuthorWorksByYearTable
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class
|
|
21
|
-
"""Author info
|
|
21
|
+
class _AuthorSummaryWidget(Static):
|
|
22
|
+
"""Author info summary."""
|
|
22
23
|
|
|
23
24
|
def __init__(self, author: Author) -> None:
|
|
24
25
|
self.author = author
|
|
@@ -62,12 +63,12 @@ class _AuthorResumeWidget(Static):
|
|
|
62
63
|
yield DateRangeFilter(checkbox_label="Cited date range:", id="cited-date-range-filter")
|
|
63
64
|
|
|
64
65
|
# Button
|
|
65
|
-
with Vertical(classes="
|
|
66
|
+
with Vertical(classes="button-container"):
|
|
66
67
|
yield Button("Make Report", variant="primary", id="make-report-button")
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
class
|
|
70
|
-
"""Author info
|
|
70
|
+
class AuthorSummaryWidget(SummaryWidget):
|
|
71
|
+
"""Author info summary container."""
|
|
71
72
|
|
|
72
73
|
def __init__(self, author_result: AuthorResult) -> None:
|
|
73
74
|
self.author_result = author_result
|
|
@@ -91,17 +92,17 @@ class AuthorResumeWidget(VerticalScroll):
|
|
|
91
92
|
async def load_data(self) -> None:
|
|
92
93
|
"""Query OpenAlex API and composing the widget."""
|
|
93
94
|
await self._get_info()
|
|
94
|
-
await self.mount(
|
|
95
|
+
await self.mount(_AuthorSummaryWidget(author=self.author))
|
|
95
96
|
|
|
96
97
|
self.loading = False
|
|
97
98
|
|
|
98
99
|
@on(Filter.Changed)
|
|
99
100
|
def filter_change(self) -> None:
|
|
100
101
|
"""Handle filter changes."""
|
|
101
|
-
filters = [filter for filter in self.query("
|
|
102
|
+
filters = [filter for filter in self.query("_AuthorSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
|
|
102
103
|
all_filters_valid = all(filter.validation_state for filter in filters)
|
|
103
104
|
|
|
104
|
-
self.query_one("
|
|
105
|
+
self.query_one("_AuthorSummaryWidget #make-report-button", Button).disabled = not all_filters_valid
|
|
105
106
|
|
|
106
107
|
@on(Button.Pressed, "#make-report-button")
|
|
107
108
|
async def make_report(self) -> None:
|
|
@@ -118,4 +119,4 @@ class AuthorResumeWidget(VerticalScroll):
|
|
|
118
119
|
|
|
119
120
|
report_widget = CreateAuthorReportWidget(author=self.author, **filters)
|
|
120
121
|
await self.app.query_one("MainContent").mount(report_widget)
|
|
121
|
-
await self.app.query_one("
|
|
122
|
+
await self.app.query_one("AuthorSummaryWidget").remove()
|
|
@@ -5,20 +5,21 @@ from typing import Any
|
|
|
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
|
|
8
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
9
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
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
24
|
def __init__(self, institution: Institution) -> None:
|
|
24
25
|
self.institution = institution
|
|
@@ -66,8 +67,8 @@ class _InstitutionResumeWidget(Static):
|
|
|
66
67
|
yield Button("Make Report", variant="primary", id="make-report-button")
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
class
|
|
70
|
-
"""Institution info
|
|
70
|
+
class InstitutionSummaryWidget(SummaryWidget):
|
|
71
|
+
"""Institution info summary container."""
|
|
71
72
|
|
|
72
73
|
def __init__(self, institution_result: InstitutionResult) -> None:
|
|
73
74
|
self.institution_result = institution_result
|
|
@@ -91,17 +92,17 @@ class InstitutionResumeWidget(VerticalScroll):
|
|
|
91
92
|
async def load_data(self) -> None:
|
|
92
93
|
"""Query OpenAlex API and composing the widget."""
|
|
93
94
|
await self._get_info()
|
|
94
|
-
await self.mount(
|
|
95
|
+
await self.mount(_InstitutionSummaryWidget(institution=self.institution))
|
|
95
96
|
|
|
96
97
|
self.loading = False
|
|
97
98
|
|
|
98
99
|
@on(Filter.Changed)
|
|
99
100
|
def filter_change(self) -> None:
|
|
100
101
|
"""Handle filter changes."""
|
|
101
|
-
filters = [filter for filter in self.query("
|
|
102
|
+
filters = [filter for filter in self.query("_InstitutionSummaryWidget Filter").results(Filter) if not filter.filter_disabled]
|
|
102
103
|
all_filters_valid = all(filter.validation_state for filter in filters)
|
|
103
104
|
|
|
104
|
-
self.query_one("
|
|
105
|
+
self.query_one("_InstitutionSummaryWidget #make-report-button", Button).disabled = not all_filters_valid
|
|
105
106
|
|
|
106
107
|
@on(Button.Pressed, "#make-report-button")
|
|
107
108
|
async def make_report(self) -> None:
|
|
@@ -118,4 +119,4 @@ class InstitutionResumeWidget(VerticalScroll):
|
|
|
118
119
|
|
|
119
120
|
report_widget = CreateInstitutionReportWidget(institution=self.institution, **filters)
|
|
120
121
|
await self.app.query_one("MainContent").mount(report_widget)
|
|
121
|
-
await self.app.query_one("
|
|
122
|
+
await self.app.query_one("InstitutionSummaryWidget").remove()
|
|
@@ -11,9 +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
|
-
|
|
16
14
|
|
|
15
|
+
# Works pane cards.
|
|
17
16
|
class ReportCitationMetricsCard(Card):
|
|
18
17
|
"""Citation metrics for this report."""
|
|
19
18
|
|
|
@@ -26,8 +25,8 @@ class ReportCitationMetricsCard(Card):
|
|
|
26
25
|
yield Label("[italic]Citation metrics:[/italic]", classes="card-title")
|
|
27
26
|
|
|
28
27
|
with Vertical(classes="card-container"):
|
|
29
|
-
type_a_count = self.report.
|
|
30
|
-
type_b_count = self.report.
|
|
28
|
+
type_a_count = self.report.citation_summary.type_a_count
|
|
29
|
+
type_b_count = self.report.citation_summary.type_b_count
|
|
31
30
|
cited_by_count = type_a_count + type_b_count
|
|
32
31
|
|
|
33
32
|
yield Label(f"[bold]Count:[/bold] {cited_by_count}")
|
|
@@ -35,8 +34,8 @@ class ReportCitationMetricsCard(Card):
|
|
|
35
34
|
yield Label(f"[bold]Type B:[/bold] {type_b_count}")
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
class
|
|
39
|
-
"""Works Type Counters
|
|
37
|
+
class WorksTypeSummaryCard(Card):
|
|
38
|
+
"""Works Type Counters Summary Card."""
|
|
40
39
|
|
|
41
40
|
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
42
41
|
self.report = report
|
|
@@ -47,11 +46,11 @@ class WorksTypeResumeCard(Card):
|
|
|
47
46
|
yield Label("[italic]Work Type[/italic]", classes="card-title")
|
|
48
47
|
|
|
49
48
|
with VerticalScroll(classes="card-container"):
|
|
50
|
-
for work_type_counter in self.report.
|
|
49
|
+
for work_type_counter in self.report.works_type_summary:
|
|
51
50
|
yield Label(f"[bold]{work_type_counter.type_name}:[/bold] {work_type_counter.count}")
|
|
52
51
|
|
|
53
52
|
|
|
54
|
-
class
|
|
53
|
+
class OpenAccessSummaryCard(Card):
|
|
55
54
|
"""Open Access counts for this report."""
|
|
56
55
|
|
|
57
56
|
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
@@ -63,7 +62,7 @@ class OpenAccessResumeCard(Card):
|
|
|
63
62
|
yield Label("[italic]Open Access[/italic]", classes="card-title")
|
|
64
63
|
|
|
65
64
|
with VerticalScroll(classes="card-container"):
|
|
66
|
-
for status, count in self.report.
|
|
65
|
+
for status, count in self.report.open_access_summary.model_dump().items():
|
|
67
66
|
yield Label(f"[bold]{status}:[/bold] {count}")
|
|
68
67
|
|
|
69
68
|
|
|
@@ -120,8 +119,8 @@ class CitationMetricsCard(Card):
|
|
|
120
119
|
|
|
121
120
|
def compose(self) -> ComposeResult:
|
|
122
121
|
"""Compose card."""
|
|
123
|
-
type_a_count = self.work_report.
|
|
124
|
-
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
|
|
125
124
|
cited_by_count = type_a_count + type_b_count
|
|
126
125
|
|
|
127
126
|
yield Label("[italic]Citation[/italic]", classes="card-title")
|
|
@@ -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,12 +3,15 @@
|
|
|
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
|
|
|
@@ -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."""
|
|
@@ -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")
|
|
@@ -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,7 +22,7 @@ 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
|
|
|
@@ -36,7 +36,9 @@ class SourcesTable(Static):
|
|
|
36
36
|
sources_table.add_column("Publisher or institution", ratio=2)
|
|
37
37
|
sources_table.add_column("Type")
|
|
38
38
|
sources_table.add_column("ISSN-L")
|
|
39
|
-
sources_table.add_column("
|
|
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:
|
|
@@ -49,6 +51,9 @@ class SourcesTable(Static):
|
|
|
49
51
|
title = f"""[@click=app.open_link('{quote(str(source.id))}')][u]{source.display_name}[/u][/]"""
|
|
50
52
|
type_source = source.type
|
|
51
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
|
+
|
|
52
57
|
is_open_access = "[#909d63]True[/]" if source.is_oa else "[#bc5653]False[/]"
|
|
53
58
|
|
|
54
59
|
sources_table.add_row(
|
|
@@ -57,6 +62,8 @@ class SourcesTable(Static):
|
|
|
57
62
|
Text.from_markup(host_organization),
|
|
58
63
|
Text.from_markup(type_source),
|
|
59
64
|
Text.from_markup(issn_l),
|
|
65
|
+
Text.from_markup(impact_factor),
|
|
66
|
+
Text.from_markup(h_index),
|
|
60
67
|
Text.from_markup(is_open_access),
|
|
61
68
|
)
|
|
62
69
|
|
|
@@ -80,4 +87,4 @@ class SourcesReportPane(VerticalScroll):
|
|
|
80
87
|
|
|
81
88
|
def compose(self) -> ComposeResult:
|
|
82
89
|
"""Compose content pane."""
|
|
83
|
-
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")
|
|
@@ -17,12 +17,15 @@ from pub_analyzer.widgets.report.cards import (
|
|
|
17
17
|
AuthorshipCard,
|
|
18
18
|
CitationMetricsCard,
|
|
19
19
|
OpenAccessCard,
|
|
20
|
-
|
|
20
|
+
OpenAccessSummaryCard,
|
|
21
21
|
ReportCitationMetricsCard,
|
|
22
|
-
|
|
22
|
+
WorksTypeSummaryCard,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
+
from .concept import ConceptsTable
|
|
26
|
+
from .grants import GrantsTable
|
|
25
27
|
from .locations import LocationsTable
|
|
28
|
+
from .topic import TopicsTable
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class CitedByTable(Static):
|
|
@@ -108,22 +111,40 @@ class WorkModal(Modal[None]):
|
|
|
108
111
|
yield CitationMetricsCard(work_report=self.work_report)
|
|
109
112
|
|
|
110
113
|
with TabbedContent(id="tables-container"):
|
|
114
|
+
# Abtract if exists
|
|
115
|
+
if self.work_report.work.abstract:
|
|
116
|
+
with TabPane("Abstract"):
|
|
117
|
+
yield Label(self.work_report.work.abstract, classes="abstract")
|
|
111
118
|
# Citations Table
|
|
112
119
|
with TabPane("Cited By Works"):
|
|
113
120
|
if len(self.work_report.cited_by):
|
|
114
121
|
yield CitedByTable(citations_list=self.work_report.cited_by)
|
|
115
122
|
else:
|
|
116
123
|
yield Label("No works found.")
|
|
124
|
+
# Concepts Table
|
|
125
|
+
with TabPane("Concepts"):
|
|
126
|
+
if len(self.work_report.work.concepts):
|
|
127
|
+
yield ConceptsTable(self.work_report.work.concepts)
|
|
128
|
+
else:
|
|
129
|
+
yield Label("No Concepts found.")
|
|
130
|
+
# Grants Table
|
|
131
|
+
with TabPane("Grants"):
|
|
132
|
+
if len(self.work_report.work.grants):
|
|
133
|
+
yield GrantsTable(self.work_report.work.grants)
|
|
134
|
+
else:
|
|
135
|
+
yield Label("No Grants found.")
|
|
117
136
|
# Locations Table
|
|
118
137
|
with TabPane("Locations"):
|
|
119
138
|
if len(self.work_report.work.locations):
|
|
120
139
|
yield LocationsTable(self.work_report.work.locations)
|
|
121
140
|
else:
|
|
122
141
|
yield Label("No sources found.")
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
yield
|
|
142
|
+
# Topics Table
|
|
143
|
+
with TabPane("Topics"):
|
|
144
|
+
if len(self.work_report.work.topics):
|
|
145
|
+
yield TopicsTable(self.work_report.work.topics)
|
|
146
|
+
else:
|
|
147
|
+
yield Label("No Topics found.")
|
|
127
148
|
|
|
128
149
|
|
|
129
150
|
class WorksTable(Static):
|
|
@@ -136,8 +157,9 @@ class WorksTable(Static):
|
|
|
136
157
|
}
|
|
137
158
|
"""
|
|
138
159
|
|
|
139
|
-
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
160
|
+
def __init__(self, report: AuthorReport | InstitutionReport, show_empty_works: bool = True) -> None:
|
|
140
161
|
self.report = report
|
|
162
|
+
self.show_empty_works = show_empty_works
|
|
141
163
|
super().__init__()
|
|
142
164
|
|
|
143
165
|
class _WorksTableRenderer(Static):
|
|
@@ -174,6 +196,9 @@ class WorksTable(Static):
|
|
|
174
196
|
|
|
175
197
|
for idx, work_report in enumerate(self.report.works):
|
|
176
198
|
work = work_report.work
|
|
199
|
+
if not self.show_empty_works and len(work_report.cited_by) < 1:
|
|
200
|
+
continue
|
|
201
|
+
|
|
177
202
|
doi = work.ids.doi
|
|
178
203
|
doi_url = f"""[@click=app.open_link("{quote(str(doi))}")]DOI[/]""" if doi else "-"
|
|
179
204
|
|
|
@@ -204,12 +229,23 @@ class WorkReportPane(VerticalScroll):
|
|
|
204
229
|
self.report = report
|
|
205
230
|
super().__init__()
|
|
206
231
|
|
|
232
|
+
async def toggle_empty_works(self) -> None:
|
|
233
|
+
"""Hide/show works if cites are cero."""
|
|
234
|
+
report_works_status: bool = self.app.query_one("ReportWidget").show_empty_works # type: ignore
|
|
235
|
+
table_works_status = self.query_one(WorksTable).show_empty_works
|
|
236
|
+
|
|
237
|
+
if self.report.works and (report_works_status != table_works_status):
|
|
238
|
+
self.loading = True
|
|
239
|
+
await self.query_one(WorksTable).remove()
|
|
240
|
+
await self.mount(WorksTable(report=self.report, show_empty_works=report_works_status))
|
|
241
|
+
self.loading = False
|
|
242
|
+
|
|
207
243
|
def compose(self) -> ComposeResult:
|
|
208
244
|
"""Compose content pane."""
|
|
209
245
|
with Horizontal(classes="cards-container"):
|
|
210
246
|
yield ReportCitationMetricsCard(report=self.report)
|
|
211
|
-
yield
|
|
212
|
-
yield
|
|
247
|
+
yield WorksTypeSummaryCard(report=self.report)
|
|
248
|
+
yield OpenAccessSummaryCard(report=self.report)
|
|
213
249
|
|
|
214
250
|
if self.report.works:
|
|
215
251
|
yield WorksTable(report=self.report)
|
|
@@ -8,8 +8,8 @@ from textual.widgets import Button, Label, Static
|
|
|
8
8
|
|
|
9
9
|
from pub_analyzer.models.author import AuthorResult
|
|
10
10
|
from pub_analyzer.models.institution import InstitutionResult
|
|
11
|
-
from pub_analyzer.widgets.author.core import
|
|
12
|
-
from pub_analyzer.widgets.institution.core import
|
|
11
|
+
from pub_analyzer.widgets.author.core import AuthorSummaryWidget
|
|
12
|
+
from pub_analyzer.widgets.institution.core import InstitutionSummaryWidget
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ResultWidget(Static):
|
|
@@ -39,14 +39,14 @@ class AuthorResultWidget(ResultWidget):
|
|
|
39
39
|
yield Label(self.author_result.hint or "", classes="text-hint")
|
|
40
40
|
|
|
41
41
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
42
|
-
"""Go to the Author
|
|
42
|
+
"""Go to the Author summary page."""
|
|
43
43
|
from pub_analyzer.widgets.body import MainContent
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
author_summary_widget = AuthorSummaryWidget(author_result=self.author_result)
|
|
46
46
|
|
|
47
47
|
main_content = self.app.query_one(MainContent)
|
|
48
48
|
main_content.update_title(title=self.author_result.display_name)
|
|
49
|
-
await main_content.mount(
|
|
49
|
+
await main_content.mount(author_summary_widget)
|
|
50
50
|
|
|
51
51
|
await self.app.query_one("FinderWidget").remove()
|
|
52
52
|
|
|
@@ -74,13 +74,13 @@ class InstitutionResultWidget(ResultWidget):
|
|
|
74
74
|
yield Label(self.institution_result.hint or "", classes="text-hint")
|
|
75
75
|
|
|
76
76
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
77
|
-
"""Go to the Institution
|
|
77
|
+
"""Go to the Institution summary page."""
|
|
78
78
|
from pub_analyzer.widgets.body import MainContent
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
institution_summary_widget = InstitutionSummaryWidget(institution_result=self.institution_result)
|
|
81
81
|
|
|
82
82
|
main_content = self.app.query_one(MainContent)
|
|
83
83
|
main_content.update_title(title=self.institution_result.display_name)
|
|
84
|
-
await main_content.mount(
|
|
84
|
+
await main_content.mount(institution_summary_widget)
|
|
85
85
|
|
|
86
86
|
await self.app.query_one("FinderWidget").remove()
|
pub_analyzer/widgets/sidebar.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Sidebar components and options."""
|
|
2
|
+
|
|
2
3
|
from enum import Enum
|
|
4
|
+
from importlib.metadata import version
|
|
3
5
|
|
|
4
6
|
from textual import on
|
|
5
7
|
from textual.app import ComposeResult
|
|
@@ -25,19 +27,26 @@ class SideBar(Static):
|
|
|
25
27
|
|
|
26
28
|
def compose(self) -> ComposeResult:
|
|
27
29
|
"""Compose dynamically the sidebar options."""
|
|
30
|
+
pub_analyzer_version = version("pub-analyzer")
|
|
31
|
+
|
|
28
32
|
with Vertical(classes="sidebar-options-column"):
|
|
29
33
|
yield Label("Menu", id="sidebar-title")
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
with Vertical(classes="sidebar-buttons-column"):
|
|
36
|
+
yield Button(SideBarOptionsName.SEARCH.value, variant="primary", id="search-sidebar-button", classes="sidebar-option")
|
|
37
|
+
yield Button(SideBarOptionsName.LOAD_REPORT.value, variant="primary", id="load-sidebar-button", classes="sidebar-option")
|
|
38
|
+
|
|
39
|
+
yield Label(f"v{pub_analyzer_version}", id="module-version-label")
|
|
33
40
|
|
|
34
41
|
def toggle(self) -> None:
|
|
35
42
|
"""Show/Hide Sidebar."""
|
|
36
43
|
if self.has_class("-hidden"):
|
|
37
44
|
self.remove_class("-hidden")
|
|
45
|
+
self.styles.animate("width", value=20, duration=0.5)
|
|
38
46
|
else:
|
|
39
47
|
if self.query("*:focus"):
|
|
40
48
|
self.screen.set_focus(None)
|
|
49
|
+
self.styles.animate("width", value=0, duration=0.5)
|
|
41
50
|
self.add_class("-hidden")
|
|
42
51
|
|
|
43
52
|
async def _replace_main_content(self, new_title: str, new_widget: Widget) -> None:
|