pub-analyzer 0.5.6__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.
- pub_analyzer/__init__.py +1 -0
- pub_analyzer/__main__.py +7 -0
- pub_analyzer/css/body.tcss +87 -0
- pub_analyzer/css/buttons.tcss +24 -0
- pub_analyzer/css/checkbox.tcss +29 -0
- pub_analyzer/css/collapsible.tcss +31 -0
- pub_analyzer/css/datatable.tcss +50 -0
- pub_analyzer/css/editor.tcss +60 -0
- pub_analyzer/css/main.tcss +50 -0
- pub_analyzer/css/report.tcss +131 -0
- pub_analyzer/css/search.tcss +81 -0
- pub_analyzer/css/summary.tcss +75 -0
- pub_analyzer/css/tabs.tcss +18 -0
- pub_analyzer/css/tree.tcss +44 -0
- pub_analyzer/internal/__init__.py +1 -0
- pub_analyzer/internal/identifier.py +106 -0
- pub_analyzer/internal/limiter.py +34 -0
- pub_analyzer/internal/render.py +41 -0
- pub_analyzer/internal/report.py +497 -0
- pub_analyzer/internal/templates/author_report.typ +591 -0
- pub_analyzer/main.py +81 -0
- pub_analyzer/models/__init__.py +1 -0
- pub_analyzer/models/author.py +87 -0
- pub_analyzer/models/concept.py +19 -0
- pub_analyzer/models/institution.py +138 -0
- pub_analyzer/models/report.py +111 -0
- pub_analyzer/models/source.py +77 -0
- pub_analyzer/models/topic.py +59 -0
- pub_analyzer/models/work.py +158 -0
- pub_analyzer/widgets/__init__.py +1 -0
- pub_analyzer/widgets/author/__init__.py +1 -0
- pub_analyzer/widgets/author/cards.py +65 -0
- pub_analyzer/widgets/author/core.py +122 -0
- pub_analyzer/widgets/author/tables.py +50 -0
- pub_analyzer/widgets/body.py +55 -0
- pub_analyzer/widgets/common/__init__.py +18 -0
- pub_analyzer/widgets/common/card.py +29 -0
- pub_analyzer/widgets/common/filesystem.py +203 -0
- pub_analyzer/widgets/common/filters.py +111 -0
- pub_analyzer/widgets/common/input.py +97 -0
- pub_analyzer/widgets/common/label.py +36 -0
- pub_analyzer/widgets/common/modal.py +43 -0
- pub_analyzer/widgets/common/selector.py +66 -0
- pub_analyzer/widgets/common/summary.py +7 -0
- pub_analyzer/widgets/institution/__init__.py +1 -0
- pub_analyzer/widgets/institution/cards.py +78 -0
- pub_analyzer/widgets/institution/core.py +122 -0
- pub_analyzer/widgets/institution/tables.py +24 -0
- pub_analyzer/widgets/report/__init__.py +1 -0
- pub_analyzer/widgets/report/author.py +43 -0
- pub_analyzer/widgets/report/cards.py +130 -0
- pub_analyzer/widgets/report/concept.py +47 -0
- pub_analyzer/widgets/report/core.py +308 -0
- pub_analyzer/widgets/report/editor.py +80 -0
- pub_analyzer/widgets/report/export.py +112 -0
- pub_analyzer/widgets/report/grants.py +85 -0
- pub_analyzer/widgets/report/institution.py +39 -0
- pub_analyzer/widgets/report/locations.py +75 -0
- pub_analyzer/widgets/report/source.py +90 -0
- pub_analyzer/widgets/report/topic.py +55 -0
- pub_analyzer/widgets/report/work.py +391 -0
- pub_analyzer/widgets/search/__init__.py +11 -0
- pub_analyzer/widgets/search/core.py +96 -0
- pub_analyzer/widgets/search/results.py +82 -0
- pub_analyzer/widgets/sidebar.py +70 -0
- pub_analyzer-0.5.6.dist-info/METADATA +102 -0
- pub_analyzer-0.5.6.dist-info/RECORD +70 -0
- pub_analyzer-0.5.6.dist-info/WHEEL +4 -0
- pub_analyzer-0.5.6.dist-info/entry_points.txt +3 -0
- pub_analyzer-0.5.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Locations 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 Location
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocationsTable(Static):
|
|
14
|
+
"""All Locations from a work in a table."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
LocationsTable .locations-table {
|
|
18
|
+
height: auto;
|
|
19
|
+
padding: 1 2 0 2;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, locations_list: list[Location]) -> None:
|
|
24
|
+
self.locations_list = locations_list
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
"""Compose Table."""
|
|
29
|
+
locations_table = Table(title="Locations", expand=True, show_lines=True)
|
|
30
|
+
|
|
31
|
+
# Define Columns
|
|
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
|
+
|
|
41
|
+
for idx, location in enumerate(self.locations_list):
|
|
42
|
+
if location.source:
|
|
43
|
+
source = location.source
|
|
44
|
+
title = f"""[@click=app.open_link('{quote(str(location.landing_page_url))}')][u]{source.display_name}[/u][/]"""
|
|
45
|
+
type = source.type or "-"
|
|
46
|
+
issn_l = source.issn_l if source.issn_l else "-"
|
|
47
|
+
|
|
48
|
+
if source.host_organization_name and source.host_organization:
|
|
49
|
+
publisher = (
|
|
50
|
+
f"""[@click=app.open_link('{quote(str(source.host_organization))}')][u]{source.host_organization_name}[/u][/]"""
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
publisher = source.host_organization_name if source.host_organization_name else "-"
|
|
54
|
+
else:
|
|
55
|
+
title = f"""[@click=app.open_link('{quote(str(location.landing_page_url))}')][u]{location.landing_page_url}[/u][/]"""
|
|
56
|
+
publisher = "-"
|
|
57
|
+
type = "-"
|
|
58
|
+
issn_l = "-"
|
|
59
|
+
|
|
60
|
+
is_open_access = "[#909d63]True[/]" if location.is_oa else "[#bc5653]False[/]"
|
|
61
|
+
version = location.version.name if location.version else "-"
|
|
62
|
+
license = location.license if location.license else "-"
|
|
63
|
+
|
|
64
|
+
locations_table.add_row(
|
|
65
|
+
str(idx),
|
|
66
|
+
Text.from_markup(title, overflow="ellipsis"),
|
|
67
|
+
Text.from_markup(publisher),
|
|
68
|
+
Text.from_markup(type),
|
|
69
|
+
Text.from_markup(issn_l),
|
|
70
|
+
Text.from_markup(is_open_access),
|
|
71
|
+
Text.from_markup(license),
|
|
72
|
+
Text.from_markup(version.capitalize()),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
yield Static(locations_table, classes="locations-table")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Sources Report 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.containers import VerticalScroll
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
from pub_analyzer.models.report import AuthorReport, InstitutionReport
|
|
12
|
+
from pub_analyzer.models.source import Source
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SourcesTable(Static):
|
|
16
|
+
"""All Sources from an author in a table."""
|
|
17
|
+
|
|
18
|
+
DEFAULT_CSS = """
|
|
19
|
+
SourcesTable .sources-table {
|
|
20
|
+
height: auto;
|
|
21
|
+
margin: 1 0 0 0;
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, sources_list: list[Source]) -> None:
|
|
26
|
+
self.sources_list = sources_list
|
|
27
|
+
super().__init__()
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
"""Compose Table."""
|
|
31
|
+
sources_table = Table(title="Sources", expand=True, show_lines=True)
|
|
32
|
+
|
|
33
|
+
# Define Columns
|
|
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")
|
|
42
|
+
|
|
43
|
+
for idx, source in enumerate(self.sources_list):
|
|
44
|
+
if source.host_organization_name:
|
|
45
|
+
host_organization = (
|
|
46
|
+
f"""[@click=app.open_link('{quote(str(source.host_organization))}')][u]{source.host_organization_name}[/u][/]"""
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
host_organization = "-"
|
|
50
|
+
|
|
51
|
+
title = f"""[@click=app.open_link('{quote(str(source.id))}')][u]{source.display_name}[/u][/]"""
|
|
52
|
+
type_source = source.type or "-"
|
|
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
|
+
|
|
57
|
+
is_open_access = "[#909d63]True[/]" if source.is_oa else "[#bc5653]False[/]"
|
|
58
|
+
|
|
59
|
+
sources_table.add_row(
|
|
60
|
+
str(idx),
|
|
61
|
+
Text.from_markup(title, overflow="ellipsis"),
|
|
62
|
+
Text.from_markup(host_organization),
|
|
63
|
+
Text.from_markup(type_source),
|
|
64
|
+
Text.from_markup(issn_l),
|
|
65
|
+
Text.from_markup(impact_factor),
|
|
66
|
+
Text.from_markup(h_index),
|
|
67
|
+
Text.from_markup(is_open_access),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
yield Static(sources_table, classes="sources-table")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SourcesReportPane(VerticalScroll):
|
|
74
|
+
"""Sources report Pane Widget."""
|
|
75
|
+
|
|
76
|
+
DEFAULT_CSS = """
|
|
77
|
+
SourcesReportPane {
|
|
78
|
+
layout: vertical;
|
|
79
|
+
overflow-x: hidden;
|
|
80
|
+
overflow-y: auto;
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
85
|
+
self.report = report
|
|
86
|
+
super().__init__()
|
|
87
|
+
|
|
88
|
+
def compose(self) -> ComposeResult:
|
|
89
|
+
"""Compose content pane."""
|
|
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")
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Works Report Widgets."""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
import re
|
|
5
|
+
from urllib.parse import quote, urlparse
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from rich.console import RenderableType
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from textual import events, on, work
|
|
12
|
+
from textual.app import ComposeResult
|
|
13
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
14
|
+
from textual.widgets import Button, Label, Static, TabbedContent, TabPane
|
|
15
|
+
|
|
16
|
+
from pub_analyzer.models.author import Author
|
|
17
|
+
from pub_analyzer.models.report import AuthorReport, CitationReport, CitationType, InstitutionReport, WorkReport
|
|
18
|
+
from pub_analyzer.models.work import Location
|
|
19
|
+
from pub_analyzer.widgets.common import FileSystemSelector, Input, Modal, ReactiveLabel, Select
|
|
20
|
+
from pub_analyzer.widgets.report.cards import (
|
|
21
|
+
AuthorshipCard,
|
|
22
|
+
CitationMetricsCard,
|
|
23
|
+
OpenAccessCard,
|
|
24
|
+
OpenAccessSummaryCard,
|
|
25
|
+
ReportCitationMetricsCard,
|
|
26
|
+
WorksTypeSummaryCard,
|
|
27
|
+
)
|
|
28
|
+
from pub_analyzer.widgets.report.editor import EditWidget
|
|
29
|
+
from pub_analyzer.widgets.report.grants import AwardsTable
|
|
30
|
+
|
|
31
|
+
from .concept import ConceptsTable
|
|
32
|
+
from .locations import LocationsTable
|
|
33
|
+
from .topic import TopicsTable
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CitedByTable(Static):
|
|
37
|
+
"""Table with the summary of the works that cite a work."""
|
|
38
|
+
|
|
39
|
+
DEFAULT_CSS = """
|
|
40
|
+
CitedByTable .citations-table {
|
|
41
|
+
height: auto;
|
|
42
|
+
margin: 1 0 0 0;
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, citations_list: list[CitationReport]) -> None:
|
|
47
|
+
self.citations_list = citations_list
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
def compose(self) -> ComposeResult:
|
|
51
|
+
"""Compose Table."""
|
|
52
|
+
citations_table = Table(title="Cited By", expand=True, show_lines=True)
|
|
53
|
+
|
|
54
|
+
# Define Columns
|
|
55
|
+
citations_table.add_column("", justify="center", vertical="middle")
|
|
56
|
+
citations_table.add_column("Title", ratio=3)
|
|
57
|
+
citations_table.add_column("Type", ratio=2)
|
|
58
|
+
citations_table.add_column("DOI")
|
|
59
|
+
citations_table.add_column("Cite Type", justify="center")
|
|
60
|
+
citations_table.add_column("Publication Date")
|
|
61
|
+
citations_table.add_column("Cited by count")
|
|
62
|
+
|
|
63
|
+
# Yield Rows
|
|
64
|
+
for idx, cited_by_work in enumerate(self.citations_list):
|
|
65
|
+
work = cited_by_work.work
|
|
66
|
+
|
|
67
|
+
work_pdf_url = work.primary_location.pdf_url if work.primary_location else None
|
|
68
|
+
title = f"""[@click=app.open_link('{quote(str(work_pdf_url))}')][u]{work.title}[/u][/]""" if work_pdf_url else work.title
|
|
69
|
+
ct_value = cited_by_work.citation_type
|
|
70
|
+
citation_type = f"[#909d63]{ct_value.name}[/]" if ct_value is CitationType.TypeA else f"[#bc5653]{ct_value.name}[/]"
|
|
71
|
+
|
|
72
|
+
doi = work.ids.doi
|
|
73
|
+
doi_url = f"""[@click=app.open_link('{quote(str(doi))}')]DOI[/]""" if doi else "-"
|
|
74
|
+
|
|
75
|
+
citations_table.add_row(
|
|
76
|
+
str(idx),
|
|
77
|
+
Text.from_markup(title, overflow="ellipsis"),
|
|
78
|
+
work.type,
|
|
79
|
+
Text.from_markup(doi_url, overflow="ellipsis"),
|
|
80
|
+
citation_type,
|
|
81
|
+
work.publication_date,
|
|
82
|
+
str(work.cited_by_count),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
yield Static(citations_table, classes="citations-table")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DownloadPane(VerticalScroll):
|
|
89
|
+
"""Download Work pane widget."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, work_report: WorkReport, locations: list[Location]) -> None:
|
|
92
|
+
self.work_report = work_report
|
|
93
|
+
self.locations = locations
|
|
94
|
+
super().__init__()
|
|
95
|
+
|
|
96
|
+
@on(FileSystemSelector.FileSelected)
|
|
97
|
+
def enable_button(self, event: FileSystemSelector.FileSelected) -> None:
|
|
98
|
+
"""Enable button on file select."""
|
|
99
|
+
if event.file_selected:
|
|
100
|
+
self.query_one(Button).disabled = False
|
|
101
|
+
else:
|
|
102
|
+
self.query_one(Button).disabled = True
|
|
103
|
+
|
|
104
|
+
@on(Button.Pressed, "#export-report-button")
|
|
105
|
+
async def export_report(self) -> None:
|
|
106
|
+
"""Handle export report button."""
|
|
107
|
+
export_path = self.query_one(FileSystemSelector).path_selected
|
|
108
|
+
file_name = self.query_one(Input).value
|
|
109
|
+
pdf_url = self.query_one(Select).value
|
|
110
|
+
|
|
111
|
+
if export_path and file_name:
|
|
112
|
+
file_path = export_path.joinpath(file_name)
|
|
113
|
+
self.download_work(file_path=file_path, pdf_url=pdf_url)
|
|
114
|
+
self.query_one(Button).disabled = True
|
|
115
|
+
|
|
116
|
+
@work(exclusive=True)
|
|
117
|
+
async def download_work(self, file_path: pathlib.Path, pdf_url: str) -> None:
|
|
118
|
+
"""Download PDF."""
|
|
119
|
+
async with httpx.AsyncClient() as client:
|
|
120
|
+
try:
|
|
121
|
+
self.log.info(f"Starting downloading: {pdf_url}")
|
|
122
|
+
response = await client.get(url=pdf_url, timeout=300, follow_redirects=True)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
with open(file_path, mode="wb") as f:
|
|
125
|
+
f.write(response.content)
|
|
126
|
+
|
|
127
|
+
self.app.notify(
|
|
128
|
+
title="PDF downloaded successfully!",
|
|
129
|
+
message=f"The file was downloaded successfully. You can go see it at [i]{file_path}[/]",
|
|
130
|
+
timeout=20.0,
|
|
131
|
+
)
|
|
132
|
+
except httpx.RequestError:
|
|
133
|
+
self.app.notify(
|
|
134
|
+
title="Network problems!",
|
|
135
|
+
message="An error occurred while requesting. Please check your connection and try again.",
|
|
136
|
+
severity="error",
|
|
137
|
+
timeout=20.0,
|
|
138
|
+
)
|
|
139
|
+
except httpx.HTTPStatusError as exec:
|
|
140
|
+
status_code = exec.response.status_code
|
|
141
|
+
title = f"HTTP Error! Status {status_code} ({httpx.codes.get_reason_phrase(status_code)})."
|
|
142
|
+
|
|
143
|
+
if status_code == httpx.codes.FORBIDDEN:
|
|
144
|
+
msg = (
|
|
145
|
+
"Sometimes servers forbid robots from accessing their websites."
|
|
146
|
+
+ "Try to download it from your browser using the following link: "
|
|
147
|
+
+ f"""[@click=app.open_link('{quote(pdf_url)}')][u]{pdf_url}[/u][/]"""
|
|
148
|
+
)
|
|
149
|
+
self.app.notify(
|
|
150
|
+
title=title,
|
|
151
|
+
message=msg,
|
|
152
|
+
severity="error",
|
|
153
|
+
timeout=30.0,
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
msg = (
|
|
157
|
+
"Try to download it from your browser using the following link: "
|
|
158
|
+
+ f"""[@click=app.open_link('{quote(pdf_url)}')][u]{pdf_url}[/u][/]"""
|
|
159
|
+
)
|
|
160
|
+
self.app.notify(
|
|
161
|
+
title=title,
|
|
162
|
+
message=msg,
|
|
163
|
+
severity="error",
|
|
164
|
+
timeout=30.0,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def safe_filename(self, title: str) -> str:
|
|
168
|
+
"""Create a safe filename."""
|
|
169
|
+
no_tags = re.sub(r"<[^>]+>", "", title)
|
|
170
|
+
hyphenated = re.sub(r"\s+", "-", no_tags.strip())
|
|
171
|
+
safe = re.sub(r"[^a-zA-Z0-9\-_]", "", hyphenated)
|
|
172
|
+
return safe[:20]
|
|
173
|
+
|
|
174
|
+
def compose(self) -> ComposeResult:
|
|
175
|
+
"""Compose content pane."""
|
|
176
|
+
filename = self.safe_filename(self.work_report.work.title)
|
|
177
|
+
suggest_file_name = f"{filename}.pdf"
|
|
178
|
+
with Vertical(id="export-form"):
|
|
179
|
+
with Vertical(classes="export-form-input-container"):
|
|
180
|
+
yield Label("[b]Name File:[/]", classes="export-form-label")
|
|
181
|
+
|
|
182
|
+
with Horizontal(classes="file-selector-container"):
|
|
183
|
+
options = []
|
|
184
|
+
for location in self.locations:
|
|
185
|
+
if location.source:
|
|
186
|
+
options.append((location.source.display_name, location.pdf_url))
|
|
187
|
+
else:
|
|
188
|
+
hostname = str(urlparse(location.pdf_url).hostname)
|
|
189
|
+
options.append((hostname, location.pdf_url))
|
|
190
|
+
|
|
191
|
+
yield Input(value=suggest_file_name, placeholder="work.pdf", classes="export-form-input")
|
|
192
|
+
yield Select(
|
|
193
|
+
options=options,
|
|
194
|
+
allow_blank=False,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
with Vertical(classes="export-form-input-container"):
|
|
198
|
+
yield Label("[b]Export Directory:[/]", classes="export-form-label")
|
|
199
|
+
yield FileSystemSelector(path=pathlib.Path.home(), only_dir=True)
|
|
200
|
+
|
|
201
|
+
with Horizontal(classes="export-form-buttons"):
|
|
202
|
+
yield Button("Download", variant="primary", disabled=True, id="export-report-button")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class WorkModal(Modal[None]):
|
|
206
|
+
"""Summary of the statistics of a work."""
|
|
207
|
+
|
|
208
|
+
def __init__(self, work_report: WorkReport, author: Author | None) -> None:
|
|
209
|
+
self.work_report = work_report
|
|
210
|
+
self.author = author
|
|
211
|
+
|
|
212
|
+
locations = self.work_report.work.locations
|
|
213
|
+
self.locations_with_pdf_available = [location for location in locations if location.pdf_url]
|
|
214
|
+
|
|
215
|
+
super().__init__()
|
|
216
|
+
|
|
217
|
+
@on(events.Key)
|
|
218
|
+
def exit_modal(self, message: events.Key) -> None:
|
|
219
|
+
"""Exit from the modal with esc KEY."""
|
|
220
|
+
if message.key == "escape":
|
|
221
|
+
self.app.pop_screen()
|
|
222
|
+
|
|
223
|
+
def compose(self) -> ComposeResult:
|
|
224
|
+
"""Compose metrics and Cited by Table."""
|
|
225
|
+
with VerticalScroll(id="dialog"):
|
|
226
|
+
yield Label(self.work_report.work.title, classes="dialog-title")
|
|
227
|
+
|
|
228
|
+
# Cards
|
|
229
|
+
with Horizontal(classes="cards-container"):
|
|
230
|
+
# Authorship's
|
|
231
|
+
yield AuthorshipCard(work=self.work_report.work, author=self.author)
|
|
232
|
+
|
|
233
|
+
# OpenAccess Info
|
|
234
|
+
yield OpenAccessCard(work=self.work_report.work)
|
|
235
|
+
|
|
236
|
+
# Citation Metrics
|
|
237
|
+
yield CitationMetricsCard(work_report=self.work_report)
|
|
238
|
+
|
|
239
|
+
with TabbedContent(id="tables-container"):
|
|
240
|
+
# Abstract if exists
|
|
241
|
+
if self.work_report.work.abstract:
|
|
242
|
+
with TabPane("Abstract"):
|
|
243
|
+
label = ReactiveLabel(self.work_report.work.abstract, classes="abstract")
|
|
244
|
+
yield label
|
|
245
|
+
yield EditWidget(
|
|
246
|
+
display_name="abstract",
|
|
247
|
+
field_name="abstract",
|
|
248
|
+
model=self.work_report.work,
|
|
249
|
+
widget=label,
|
|
250
|
+
widget_field="renderable",
|
|
251
|
+
)
|
|
252
|
+
# Citations Table
|
|
253
|
+
with TabPane("Cited By Works"):
|
|
254
|
+
if len(self.work_report.cited_by):
|
|
255
|
+
yield CitedByTable(citations_list=self.work_report.cited_by)
|
|
256
|
+
else:
|
|
257
|
+
yield Label("No works found.")
|
|
258
|
+
# Concepts Table
|
|
259
|
+
with TabPane("Concepts"):
|
|
260
|
+
if len(self.work_report.work.concepts):
|
|
261
|
+
yield ConceptsTable(self.work_report.work.concepts)
|
|
262
|
+
else:
|
|
263
|
+
yield Label("No Concepts found.")
|
|
264
|
+
# Awards Table
|
|
265
|
+
with TabPane("Awards"):
|
|
266
|
+
if len(self.work_report.work.awards):
|
|
267
|
+
yield AwardsTable(self.work_report.work.awards)
|
|
268
|
+
else:
|
|
269
|
+
yield Label("No Awards found.")
|
|
270
|
+
# Locations Table
|
|
271
|
+
with TabPane("Locations"):
|
|
272
|
+
if len(self.work_report.work.locations):
|
|
273
|
+
yield LocationsTable(self.work_report.work.locations)
|
|
274
|
+
else:
|
|
275
|
+
yield Label("No sources found.")
|
|
276
|
+
# Topics Table
|
|
277
|
+
with TabPane("Topics"):
|
|
278
|
+
if len(self.work_report.work.topics):
|
|
279
|
+
yield TopicsTable(self.work_report.work.topics)
|
|
280
|
+
else:
|
|
281
|
+
yield Label("No Topics found.")
|
|
282
|
+
# Download
|
|
283
|
+
location = self.work_report.work.best_oa_location
|
|
284
|
+
if location and location.pdf_url:
|
|
285
|
+
with TabPane("Download"):
|
|
286
|
+
yield DownloadPane(work_report=self.work_report, locations=self.locations_with_pdf_available)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class WorksTable(Static):
|
|
290
|
+
"""Table with all works produced by an author."""
|
|
291
|
+
|
|
292
|
+
DEFAULT_CSS = """
|
|
293
|
+
WorksTable {
|
|
294
|
+
height: auto;
|
|
295
|
+
margin: 1 0 0 0;
|
|
296
|
+
}
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, report: AuthorReport | InstitutionReport, show_empty_works: bool = True) -> None:
|
|
300
|
+
self.report = report
|
|
301
|
+
self.show_empty_works = show_empty_works
|
|
302
|
+
super().__init__()
|
|
303
|
+
|
|
304
|
+
class _WorksTableRenderer(Static):
|
|
305
|
+
"""Virtual Static Widget to handle table actions calls."""
|
|
306
|
+
|
|
307
|
+
def __init__(self, renderable: RenderableType, report: AuthorReport | InstitutionReport) -> None:
|
|
308
|
+
self.report = report
|
|
309
|
+
super().__init__(renderable)
|
|
310
|
+
|
|
311
|
+
def action_open_work_details(self, idx: int) -> None:
|
|
312
|
+
"""Open Modal."""
|
|
313
|
+
match self.report:
|
|
314
|
+
case AuthorReport():
|
|
315
|
+
self.app.push_screen(WorkModal(work_report=self.report.works[idx], author=self.report.author))
|
|
316
|
+
case InstitutionReport():
|
|
317
|
+
self.app.push_screen(WorkModal(work_report=self.report.works[idx], author=None))
|
|
318
|
+
|
|
319
|
+
def compose(self) -> ComposeResult:
|
|
320
|
+
"""Generate Table."""
|
|
321
|
+
if self.report.works:
|
|
322
|
+
first_pub_year = next((w.work.publication_year for w in self.report.works if w.work.publication_year is not None), "-")
|
|
323
|
+
last_pub_year = next((w.work.publication_year for w in reversed(self.report.works) if w.work.publication_year is not None), "-")
|
|
324
|
+
title = f"Works from {first_pub_year} to {last_pub_year}"
|
|
325
|
+
else:
|
|
326
|
+
title = "Works"
|
|
327
|
+
|
|
328
|
+
work_table = Table(title=title, expand=True, show_lines=True)
|
|
329
|
+
work_table.add_column("", justify="center", vertical="middle")
|
|
330
|
+
work_table.add_column("Title", ratio=3)
|
|
331
|
+
work_table.add_column("Type", ratio=2)
|
|
332
|
+
work_table.add_column("DOI")
|
|
333
|
+
work_table.add_column("Publication Date")
|
|
334
|
+
work_table.add_column("Cited by count")
|
|
335
|
+
|
|
336
|
+
for idx, work_report in enumerate(self.report.works):
|
|
337
|
+
work = work_report.work
|
|
338
|
+
if not self.show_empty_works and len(work_report.cited_by) < 1:
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
doi = work.ids.doi
|
|
342
|
+
doi_url = f"""[@click=app.open_link("{quote(str(doi))}")]DOI[/]""" if doi else "-"
|
|
343
|
+
publication_date = work.publication_date if work.publication_date else "-"
|
|
344
|
+
|
|
345
|
+
work_table.add_row(
|
|
346
|
+
str(f"""[@click=open_work_details({idx})]{idx}[/]"""),
|
|
347
|
+
Text(work.title, overflow="ellipsis"),
|
|
348
|
+
Text(work.type),
|
|
349
|
+
Text.from_markup(doi_url, overflow="ellipsis"),
|
|
350
|
+
Text(publication_date),
|
|
351
|
+
str(len(work_report.cited_by)),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
yield self._WorksTableRenderer(work_table, report=self.report)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class WorkReportPane(VerticalScroll):
|
|
358
|
+
"""Work report Pane Widget."""
|
|
359
|
+
|
|
360
|
+
DEFAULT_CSS = """
|
|
361
|
+
WorkReportPane {
|
|
362
|
+
layout: vertical;
|
|
363
|
+
overflow-x: hidden;
|
|
364
|
+
overflow-y: auto;
|
|
365
|
+
}
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(self, report: AuthorReport | InstitutionReport) -> None:
|
|
369
|
+
self.report = report
|
|
370
|
+
super().__init__()
|
|
371
|
+
|
|
372
|
+
async def toggle_empty_works(self) -> None:
|
|
373
|
+
"""Hide/show works if cites are cero."""
|
|
374
|
+
report_works_status: bool = self.app.query_one("ReportWidget").show_empty_works # type: ignore
|
|
375
|
+
table_works_status = self.query_one(WorksTable).show_empty_works
|
|
376
|
+
|
|
377
|
+
if self.report.works and (report_works_status != table_works_status):
|
|
378
|
+
self.loading = True
|
|
379
|
+
await self.query_one(WorksTable).remove()
|
|
380
|
+
await self.mount(WorksTable(report=self.report, show_empty_works=report_works_status))
|
|
381
|
+
self.loading = False
|
|
382
|
+
|
|
383
|
+
def compose(self) -> ComposeResult:
|
|
384
|
+
"""Compose content pane."""
|
|
385
|
+
with Horizontal(classes="cards-container"):
|
|
386
|
+
yield ReportCitationMetricsCard(report=self.report)
|
|
387
|
+
yield WorksTypeSummaryCard(report=self.report)
|
|
388
|
+
yield OpenAccessSummaryCard(report=self.report)
|
|
389
|
+
|
|
390
|
+
if self.report.works:
|
|
391
|
+
yield WorksTable(report=self.report)
|