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.

Files changed (41) hide show
  1. pub_analyzer/css/body.tcss +48 -35
  2. pub_analyzer/css/buttons.tcss +0 -4
  3. pub_analyzer/css/main.tcss +18 -12
  4. pub_analyzer/css/summary.tcss +75 -0
  5. pub_analyzer/internal/identifier.py +26 -0
  6. pub_analyzer/internal/report.py +73 -31
  7. pub_analyzer/internal/templates/author/author_summary.typ +112 -0
  8. pub_analyzer/internal/templates/author/report.typ +16 -3
  9. pub_analyzer/internal/templates/author/sources.typ +7 -5
  10. pub_analyzer/internal/templates/author/works.typ +119 -32
  11. pub_analyzer/internal/templates/author/works_extended.typ +5 -6
  12. pub_analyzer/main.py +8 -3
  13. pub_analyzer/models/author.py +9 -25
  14. pub_analyzer/models/concept.py +19 -0
  15. pub_analyzer/models/institution.py +11 -1
  16. pub_analyzer/models/report.py +14 -14
  17. pub_analyzer/models/source.py +59 -3
  18. pub_analyzer/models/topic.py +59 -0
  19. pub_analyzer/models/work.py +23 -0
  20. pub_analyzer/widgets/author/cards.py +6 -5
  21. pub_analyzer/widgets/author/core.py +11 -10
  22. pub_analyzer/widgets/common/summary.py +7 -0
  23. pub_analyzer/widgets/institution/core.py +10 -9
  24. pub_analyzer/widgets/report/cards.py +10 -11
  25. pub_analyzer/widgets/report/concept.py +47 -0
  26. pub_analyzer/widgets/report/core.py +14 -0
  27. pub_analyzer/widgets/report/grants.py +46 -0
  28. pub_analyzer/widgets/report/source.py +11 -4
  29. pub_analyzer/widgets/report/topic.py +55 -0
  30. pub_analyzer/widgets/report/work.py +45 -9
  31. pub_analyzer/widgets/search/results.py +8 -8
  32. pub_analyzer/widgets/sidebar.py +11 -2
  33. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/METADATA +8 -7
  34. pub_analyzer-0.4.0.dist-info/RECORD +69 -0
  35. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/WHEEL +1 -1
  36. pub_analyzer/css/author.tcss +0 -82
  37. pub_analyzer/css/institution.tcss +0 -82
  38. pub_analyzer/internal/templates/author/author_resume.typ +0 -96
  39. pub_analyzer-0.2.0.dist-info/RECORD +0 -64
  40. {pub_analyzer-0.2.0.dist-info → pub_analyzer-0.4.0.dist-info}/LICENSE +0 -0
  41. {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, VerticalScroll
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 _AuthorResumeWidget(Static):
21
- """Author info resume."""
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="block-container button-container"):
66
+ with Vertical(classes="button-container"):
66
67
  yield Button("Make Report", variant="primary", id="make-report-button")
67
68
 
68
69
 
69
- class AuthorResumeWidget(VerticalScroll):
70
- """Author info resume container."""
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(_AuthorResumeWidget(author=self.author))
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("_AuthorResumeWidget Filter").results(Filter) if not filter.filter_disabled]
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("_AuthorResumeWidget #make-report-button", Button).disabled = not all_filters_valid
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("AuthorResumeWidget").remove()
122
+ await self.app.query_one("AuthorSummaryWidget").remove()
@@ -0,0 +1,7 @@
1
+ """Common Summary format Widget."""
2
+
3
+ from textual.containers import VerticalScroll
4
+
5
+
6
+ class SummaryWidget(VerticalScroll):
7
+ """Common format summary container."""
@@ -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, VerticalScroll
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 _InstitutionResumeWidget(Static):
21
- """Institution info resume."""
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 InstitutionResumeWidget(VerticalScroll):
70
- """Institution info resume container."""
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(_InstitutionResumeWidget(institution=self.institution))
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("_InstitutionResumeWidget Filter").results(Filter) if not filter.filter_disabled]
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("_InstitutionResumeWidget #make-report-button", Button).disabled = not all_filters_valid
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("InstitutionResumeWidget").remove()
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.citation_resume.type_a_count
30
- type_b_count = self.report.citation_resume.type_b_count
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 WorksTypeResumeCard(Card):
39
- """Works Type Counters Resume Card."""
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.works_type_resume:
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 OpenAccessResumeCard(Card):
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.open_access_resume.model_dump().items():
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.citation_resume.type_a_count
124
- type_b_count = self.work_report.citation_resume.type_b_count
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 DehydratedSource
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[DehydratedSource]) -> None:
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("Is Open Access")
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.sources_resume.sources)
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
- OpenAccessResumeCard,
20
+ OpenAccessSummaryCard,
21
21
  ReportCitationMetricsCard,
22
- WorksTypeResumeCard,
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
- # Abtract if exists
124
- if self.work_report.work.abstract:
125
- with TabPane("Abstract"):
126
- yield Label(self.work_report.work.abstract, classes="abstract")
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 WorksTypeResumeCard(report=self.report)
212
- yield OpenAccessResumeCard(report=self.report)
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 AuthorResumeWidget
12
- from pub_analyzer.widgets.institution.core import InstitutionResumeWidget
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 resume page."""
42
+ """Go to the Author summary page."""
43
43
  from pub_analyzer.widgets.body import MainContent
44
44
 
45
- author_resume_widget = AuthorResumeWidget(author_result=self.author_result)
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(author_resume_widget)
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 resume page."""
77
+ """Go to the Institution summary page."""
78
78
  from pub_analyzer.widgets.body import MainContent
79
79
 
80
- institution_resume_widget = InstitutionResumeWidget(institution_result=self.institution_result)
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(institution_resume_widget)
84
+ await main_content.mount(institution_summary_widget)
85
85
 
86
86
  await self.app.query_one("FinderWidget").remove()
@@ -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
- yield Button(SideBarOptionsName.SEARCH.value, variant="primary", id="search-sidebar-button", classes="sidebar-option")
32
- yield Button(SideBarOptionsName.LOAD_REPORT.value, variant="primary", id="load-sidebar-button", classes="sidebar-option")
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: