Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a dialog for selecting/unselecting sections in reports #298

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions comptages/comptages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import pytz
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
from functools import partial

from qgis.PyQt.QtGui import QIcon
Expand Down Expand Up @@ -29,6 +29,7 @@
from comptages.report.yearly_report_bike import YearlyReportBike
from comptages.ics.ics_importer import IcsImporter
from comptages.ui.resources import *
from comptages.ui.select_reports import SelectSectionsToReport
from comptages.core import definitions


Expand Down Expand Up @@ -515,34 +516,51 @@ def do_generate_report_action(self, count_id):
return

file_dialog = QFileDialog()
title = "Exporter un rapport"
path = self.settings.value("report_export_directory")
file_path = QFileDialog.getExistingDirectory(file_dialog, title, path)
mondays = list(report._mondays_of_count(count))
sections_ids = (
models.Section.objects.filter(lane__id_installation__count=count)
.distinct()
.values_list("id", flat=True)
)
report_selection_dialog = SelectSectionsToReport(
sections_ids=list(sections_ids), mondays=mondays
)

if not file_path:
if report_selection_dialog.exec_():
selected_sections_dates: dict[
str, list[date]
] = report_selection_dialog.get_inputs()
title = "Exporter un rapport"

path = self.settings.value("report_export_directory")
file_path = QFileDialog.getExistingDirectory(file_dialog, title, path)

if not file_path:
QgsMessageLog.logMessage(
"{} - Generate report action ended: No file_path given".format(
datetime.now()
),
"Comptages",
Qgis.Info,
)
return
QgsMessageLog.logMessage(
"{} - Generate report action ended: No file_path given".format(
datetime.now()
),
f"""
{datetime.now()} - Generate report action can really begin now for count {count.id} with file_path: {file_path}.
Selected sections and dates: {selected_sections_dates}
""",
"Comptages",
Qgis.Info,
)
return
QgsMessageLog.logMessage(
"{} - Generate report action can really begin now for count {} with file_path: {}".format(
datetime.now(), count.id, file_path
),
"Comptages",
Qgis.Info,
)

self.tm.allTasksFinished.connect(partial(self.all_tasks_finished, "report"))
self.tm.addTask(
report_task.ReportTask(
file_path=file_path,
count=count,
self.tm.allTasksFinished.connect(partial(self.all_tasks_finished, "report"))
self.tm.addTask(
report_task.ReportTask(
file_path=file_path,
count=count,
selected_sections_dates=selected_sections_dates,
)
)
)

def do_export_plan_action(self, count_id):
count = models.Count.objects.get(id=count_id)
Expand Down
43 changes: 32 additions & 11 deletions comptages/core/report.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os

from datetime import timedelta, datetime
from datetime import date, timedelta, datetime
from typing import Generator, Optional
from openpyxl import load_workbook, Workbook

from qgis.core import Qgis, QgsMessageLog

from comptages.datamodel import models
from comptages.core import statistics

Expand All @@ -13,18 +15,22 @@ def simple_print_callback(progress):

def prepare_reports(
file_path,
count=None,
count: Optional[models.Count] = None,
year=None,
template="default",
section_id=None,
sections_days: Optional[dict[str, list[date]]] = None,
callback_progress=simple_print_callback,
):
current_dir = os.path.dirname(os.path.abspath(__file__))

if template == "default":
template_name = "template.xlsx"
template_path = os.path.join(current_dir, os.pardir, "report", template_name)
_prepare_default_reports(file_path, count, template_path, callback_progress)
assert count
_prepare_default_reports(
file_path, count, template_path, callback_progress, sections_days
)
elif template == "yearly":
template_name = "template_yearly.xlsx"
template_path = os.path.join(current_dir, os.pardir, "report", template_name)
Expand All @@ -38,17 +44,32 @@ def prepare_reports(


def _prepare_default_reports(
file_path: str, count: models.Count, template_path: str, callback_progress
file_path: str,
count: models.Count,
template_path: str,
callback_progress,
sections_days: Optional[dict[str, list[date]]] = None,
):
# We do by section and not by count because of special cases.
sections = models.Section.objects.filter(
lane__id_installation__count=count
).distinct()
sections = models.Section.objects.filter(lane__id_installation__count=count)

mondays_qty = len(list(_mondays_of_count(count)))
mondays = _mondays_of_count(count)
# Filter out sections if the user narrowed down the section to include
# in report
if sections_days:
sections = sections.filter(id__in=list(sections_days.keys()))

mondays = list(_mondays_of_count(count))
mondays_qty = len(mondays)

QgsMessageLog.logMessage(
f"Reporting on {sections.distinct().count()} sections", "Report", Qgis.Info
)
for section in sections:
for i, monday in enumerate(mondays):
# Filter out date based on parameter
if sections_days and monday not in sections_days[section.id]:
continue
QgsMessageLog.logMessage("Adding to workbook", "Report", Qgis.Info)
progress = int(100 / mondays_qty * (i - 1))
callback_progress(progress)

Expand Down Expand Up @@ -89,7 +110,7 @@ def _prepare_yearly_report(
workbook.save(filename=output)


def _mondays_of_count(count: models.Count):
def _mondays_of_count(count: models.Count) -> Generator[date, None, None]:
"""Generator that return the Mondays of the count"""

start = count.start_process_date
Expand Down
13 changes: 11 additions & 2 deletions comptages/core/report_task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from datetime import datetime
from datetime import date, datetime
from typing import Optional

from qgis.core import QgsTask, Qgis, QgsMessageLog

Expand All @@ -8,7 +9,13 @@

class ReportTask(QgsTask):
def __init__(
self, file_path, count=None, year=None, template="default", section_id=None
self,
file_path,
count=None,
year=None,
template="default",
section_id=None,
selected_sections_dates: Optional[dict[str, list[date]]] = None,
):
self.basename = os.path.basename(file_path)
super().__init__("Génération du rapport: {}".format(self.basename))
Expand All @@ -18,6 +25,7 @@ def __init__(
self.template = template
self.year = year
self.section_id = section_id
self.only_sections_ids = selected_sections_dates

def run(self):
try:
Expand All @@ -27,6 +35,7 @@ def run(self):
self.year,
self.template,
self.section_id,
self.only_sections_ids,
callback_progress=self.setProgress,
)
return True
Expand Down
1 change: 1 addition & 0 deletions comptages/report/yearly_report_bike.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def values_by_direction(self):
.annotate(total=Sum("times"))
.values("weekday", "id_lane__direction", "total")
)
return result

def values_by_day_and_hour(self):
# Get all the count details for section and the year
Expand Down
150 changes: 150 additions & 0 deletions comptages/ui/select_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from datetime import date, datetime
from functools import partial
from qgis.PyQt.QtWidgets import (
QCheckBox,
QDialog,
QDialogButtonBox,
QHLine,
QLabel,
QRadioButton,
QVBoxLayout,
QScrollArea,
QWidget,
)


class SelectSectionsToReport(QDialog):
fmt = "%d-%m-%Y"

def __init__(self, *args, **kwargs):
sections_ids: list[str] = kwargs.pop("sections_ids")
mondays: list[datetime] = kwargs.pop("mondays")
mondays_as_datestr = [d.strftime(self.fmt) for d in mondays]

super().__init__(*args, **kwargs)

self.max_selected = len(sections_ids) * len(mondays)
self.setMinimumWidth(550)
self.setWindowTitle(
"Please select the sections and dates to include in the report..."
)

# Parent layout
self.layout = QVBoxLayout()

# Basic controls
QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)

# Create `all` and `none` selectors only if there are several sections
if len(sections_ids) > 1:
# Radio: all
self.all_selector = QRadioButton()
self.all_selector.setChecked(True)
self.all_selector.toggled.connect(partial(self.select_all_none, "all"))
self.all_selector_label = QLabel()
self.all_selector_label.setText("Select all")
self.all_selector_label.setBuddy(self.all_selector)
self.layout.addWidget(self.all_selector_label)
self.layout.addWidget(self.all_selector)

# Radio: none
self.none_selector = QRadioButton()
self.none_selector.toggled.connect(partial(self.select_all_none, "none"))
self.none_selector_label = QLabel()
self.none_selector_label.setText("Unselect all")
self.none_selector_label.setBuddy(self.none_selector)
self.layout.addWidget(self.none_selector_label)
self.layout.addWidget(self.none_selector)

# Checkboxes: containers
self.scrollarea = QScrollArea()
self.widget = QWidget()
self.vbox = QVBoxLayout()

# Checkboxes: items
self.items_check_boxes = {}
for item in sections_ids:
item_checkbox = QCheckBox(f"section {item}")
item_checkbox.setChecked(True)
item_checkbox.clicked.connect(partial(self.update_children_of, item))
label = QLabel()
label.setBuddy(item_checkbox)

self.vbox.addWidget(label)
self.vbox.addWidget(item_checkbox)
self.items_check_boxes[item] = {
"checkbox": item_checkbox,
"subcheckboxes": {},
}

# Checkboxes: subitems
for subitem in mondays_as_datestr:
subitem_checkbox = QCheckBox(subitem)
subitem_checkbox.setStyleSheet("QCheckBox { padding-left: 15px; }")
subitem_checkbox.setChecked(True)
subitem_checkbox.clicked.connect(self.update_selected_count)
label = QLabel()
label.setBuddy(subitem_checkbox)

self.vbox.addWidget(label)
self.vbox.addWidget(subitem_checkbox)
self.items_check_boxes[item]["subcheckboxes"][
subitem
] = subitem_checkbox

self.vbox.addWidget(QHLine())

# Checkbox: containers: populate layout
self.widget.setLayout(self.vbox)
self.scrollarea.setWidget(self.widget)
self.layout.addWidget(self.scrollarea)

# Selected
self.selected = QLabel()
selected_text = f"Selected: {self.count_selected()} out of {self.max_selected}"
self.selected.setText(selected_text)
self.layout.addWidget(self.selected)

# Buttons
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)

def update_selected_count(self):
self.selected.setText(
f"Selected: {self.count_selected()} out of {self.max_selected}"
)

def count_selected(self) -> int:
count = 0
for item in self.items_check_boxes.values():
for subcheckbox in item["subcheckboxes"].values():
if subcheckbox.isChecked():
count += 1
return count

def update_children_of(self, item: str):
new_state = self.items_check_boxes[item]["checkbox"].isChecked()
for subcheckbox in self.items_check_boxes[item]["subcheckboxes"].values():
subcheckbox.setChecked(new_state)
self.update_selected_count()

def select_all_none(self, desired: str):
new_state = desired == "all"
for item in self.items_check_boxes.values():
item["checkbox"].setChecked(new_state)
for subcheckbox in item["subcheckboxes"].values():
subcheckbox.setChecked(new_state)
self.update_selected_count()

def get_inputs(self) -> dict[str, list[date]]:
builder = {}
for section_id, item in self.items_check_boxes.items():
builder[section_id] = [
datetime.strptime(monday_datestr, self.fmt).date()
for monday_datestr, subcheckbox in item["subcheckboxes"].items()
if subcheckbox.isChecked()
]
return builder