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.

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