diff --git a/.vscode/settings.json b/.vscode/settings.json index 212fc29..7ce7dc4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,4 @@ "editor.rulers": [ 120 ], - "explorer.excludeGitIgnore": true } \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index f2623a8..cb75b04 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # pip-compile --extra=dev --output-file=requirements/dev.txt pyproject.toml # -asgiref==3.10.0 +asgiref==3.11.0 # via django astroid==4.0.2 # via pylint @@ -14,7 +14,7 @@ charset-normalizer==3.4.4 # via # reportlab # requests -coverage==7.11.3 +coverage==7.12.0 # via ida (pyproject.toml) dill==0.4.0 # via pylint @@ -22,7 +22,7 @@ django==5.2.8 # via # django-telegram-app # ida (pyproject.toml) -django-telegram-app==0.3.0 +django-telegram-app==1.0.0 # via ida (pyproject.toml) django-types==0.22.0 # via ida (pyproject.toml) @@ -50,11 +50,11 @@ pyright==1.1.407 # via ida (pyproject.toml) python-dotenv==1.2.1 # via ida (pyproject.toml) -reportlab==4.4.4 +reportlab==4.4.5 # via ida (pyproject.toml) requests==2.32.5 # via django-telegram-app -ruff==0.14.5 +ruff==0.14.6 # via ida (pyproject.toml) sqlparse==0.5.3 # via django diff --git a/requirements/main.txt b/requirements/main.txt index 18f32ac..36f3f73 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=requirements/main.txt pyproject.toml # -asgiref==3.10.0 +asgiref==3.11.0 # via django certifi==2025.11.12 # via requests @@ -16,7 +16,7 @@ django==5.2.8 # via # django-telegram-app # ida (pyproject.toml) -django-telegram-app==0.3.0 +django-telegram-app==1.0.0 # via ida (pyproject.toml) envyronment==0.4.0 # via ida (pyproject.toml) @@ -28,7 +28,7 @@ packaging==25.0 # via gunicorn pillow==12.0.0 # via reportlab -reportlab==4.4.4 +reportlab==4.4.5 # via ida (pyproject.toml) requests==2.32.5 # via django-telegram-app diff --git a/src/apps/telegram/__init__.py b/src/apps/telegram/__init__.py new file mode 100644 index 0000000..20d1383 --- /dev/null +++ b/src/apps/telegram/__init__.py @@ -0,0 +1 @@ +"""Telegram app.""" diff --git a/src/apps/telegram/apps.py b/src/apps/telegram/apps.py new file mode 100644 index 0000000..e5e7efb --- /dev/null +++ b/src/apps/telegram/apps.py @@ -0,0 +1,10 @@ +"""Configuration for the telegram app.""" + +from django.apps import AppConfig + + +class TelegramConfig(AppConfig): + """Configuration for the telegram app.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "apps.telegram" diff --git a/src/apps/telegram/management/__init__.py b/src/apps/telegram/management/__init__.py new file mode 100644 index 0000000..5256935 --- /dev/null +++ b/src/apps/telegram/management/__init__.py @@ -0,0 +1 @@ +"""Management package for the telegram app.""" diff --git a/src/apps/telegram/management/base.py b/src/apps/telegram/management/base.py new file mode 100644 index 0000000..9c3eb57 --- /dev/null +++ b/src/apps/telegram/management/base.py @@ -0,0 +1,32 @@ +"""Base command for telegram management commands.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from django.utils.translation import override +from django_telegram_app.bot.bot import handle_update +from django_telegram_app.management.base import BaseManagementCommand + +from apps.telegram.models import TelegramSettings +from apps.users.models import IdaUser + +if TYPE_CHECKING: + from django_telegram_app.models import AbstractTelegramSettings + + +class ManagementCommand(BaseManagementCommand): + """Base command for telegram management commands.""" + + def get_telegram_settings_filter(self): + """Filter to get only active users.""" + return {"user__is_active": True} + + def handle_command(self, telegram_settings: AbstractTelegramSettings, command_text: str): + """Handle the update in the current user's language.""" + assert isinstance(telegram_settings, TelegramSettings) + assert isinstance(telegram_settings.user, IdaUser) + + update = {"message": {"chat": {"id": telegram_settings.chat_id}, "text": command_text}} + with override(telegram_settings.user.language): + handle_update(update, telegram_settings) diff --git a/src/apps/telegram/migrations/0001_initial.py b/src/apps/telegram/migrations/0001_initial.py new file mode 100644 index 0000000..b0909dc --- /dev/null +++ b/src/apps/telegram/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.8 on 2025-11-18 23:38 + +import django.core.serializers.json +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='TelegramSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('chat_id', models.IntegerField(unique=True, verbose_name='chat id')), + ('data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder, verbose_name='data')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/apps/telegram/migrations/__init__.py b/src/apps/telegram/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/telegram/models.py b/src/apps/telegram/models.py new file mode 100644 index 0000000..74a86ac --- /dev/null +++ b/src/apps/telegram/models.py @@ -0,0 +1,12 @@ +"""Models for the telegram app.""" + +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_telegram_app.models import AbstractTelegramSettings + + +class TelegramSettings(AbstractTelegramSettings): + """Custom Telegram settings model.""" + + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_("user")) diff --git a/src/apps/telegram/telegrambot/__init__.py b/src/apps/telegram/telegrambot/__init__.py new file mode 100644 index 0000000..4a28eb4 --- /dev/null +++ b/src/apps/telegram/telegrambot/__init__.py @@ -0,0 +1 @@ +"""Telegrambot package for the telegram app.""" diff --git a/src/apps/telegram/telegrambot/base.py b/src/apps/telegram/telegrambot/base.py new file mode 100644 index 0000000..681dfdf --- /dev/null +++ b/src/apps/telegram/telegrambot/base.py @@ -0,0 +1,43 @@ +"""Base telegram settings.""" + +from abc import ABC + +from django_telegram_app.bot.base import BaseBotCommand, Step + +from apps.telegram.models import TelegramSettings + + +class TelegramCommand(BaseBotCommand, ABC): + """Project specific base class for telegram commands.""" + + settings: TelegramSettings + + +class TelegramStep(Step, ABC): + """Project specific base class for telegram command steps.""" + + command: TelegramCommand + + def __init__(self, command: TelegramCommand, unique_id: str | None = None, steps_back: int = 0): + """Initialize the telegram step. + + Steps back is configured on the step level since we re-use steps in multiple commands, so the amount of + steps to go back might differ depending on the command. + """ + self.steps_back = steps_back + super().__init__(command, unique_id) + + def maybe_add_previous_button(self, keyboard: list[list[dict]], data: dict, **kwargs): + """Add a previous button if steps_back is set.""" + if self.steps_back <= 0: + return + keyboard.append( + [ + { + "text": "⬅️ Previous step", + "callback_data": self.previous_step_callback( + steps_back=self.steps_back, original_data=data, **kwargs + ), + } + ] + ) diff --git a/src/apps/telegram/tests.py b/src/apps/telegram/tests.py new file mode 100644 index 0000000..420de55 --- /dev/null +++ b/src/apps/telegram/tests.py @@ -0,0 +1 @@ +"""Tests for the Telegram app.""" diff --git a/src/apps/timesheets/management/commands/startcompletetimesheet.py b/src/apps/timesheets/management/commands/startcompletetimesheet.py index 85435ec..8cd9564 100644 --- a/src/apps/timesheets/management/commands/startcompletetimesheet.py +++ b/src/apps/timesheets/management/commands/startcompletetimesheet.py @@ -1,12 +1,12 @@ """Django command to start the CompleteTimesheet command for active users with a chat_id.""" from django.utils import timezone -from django_telegram_app.management.base import BaseTelegramCommand +from apps.telegram.management.base import ManagementCommand from apps.timesheets.telegrambot.commands.completetimesheet import Command as CompleteTimesheetCommand -class Command(BaseTelegramCommand): +class Command(ManagementCommand): """Start the CompleteTimesheet command.""" help = "Start the CompleteTimesheet command to let users complete their timesheets." diff --git a/src/apps/timesheets/management/commands/startregisterwork.py b/src/apps/timesheets/management/commands/startregisterwork.py index 389453c..b7f4691 100644 --- a/src/apps/timesheets/management/commands/startregisterwork.py +++ b/src/apps/timesheets/management/commands/startregisterwork.py @@ -1,12 +1,12 @@ """Django command to start a RegisterWork command for active users with a chat_id.""" from django.utils import timezone -from django_telegram_app.management.base import BaseTelegramCommand +from apps.telegram.management.base import ManagementCommand from apps.timesheets.telegrambot.commands.registerwork import Command as RegisterWorkCommand -class Command(BaseTelegramCommand): +class Command(ManagementCommand): """Start a RegisterWork command.""" help = "Start a RegisterWork command to let users register their work hours." diff --git a/src/apps/timesheets/telegrambot/commands/completetimesheet.py b/src/apps/timesheets/telegrambot/commands/completetimesheet.py index 1257126..6f36888 100644 --- a/src/apps/timesheets/telegrambot/commands/completetimesheet.py +++ b/src/apps/timesheets/telegrambot/commands/completetimesheet.py @@ -1,16 +1,15 @@ """Complete timesheet command for the Telegram bot.""" -from django_telegram_app.bot.base import BaseCommand, Step - +from apps.telegram.telegrambot.base import TelegramCommand from apps.timesheets.telegrambot.steps import Confirm, MarkTimesheetAsCompleted, SelectTimesheet -class Command(BaseCommand): +class Command(TelegramCommand): """Represent the complete timesheet command.""" description = "Mark a timesheet as completed" @property - def steps(self) -> list[Step]: + def steps(self): """Return the steps of the command.""" return [SelectTimesheet(self), Confirm(self, steps_back=1), MarkTimesheetAsCompleted(self)] diff --git a/src/apps/timesheets/telegrambot/commands/editwork.py b/src/apps/timesheets/telegrambot/commands/editwork.py index a36db9f..91de93e 100644 --- a/src/apps/timesheets/telegrambot/commands/editwork.py +++ b/src/apps/timesheets/telegrambot/commands/editwork.py @@ -1,16 +1,15 @@ """Edit work command for the Telegram bot.""" -from django_telegram_app.bot.base import BaseCommand, Step - +from apps.telegram.telegrambot.base import TelegramCommand from apps.timesheets.telegrambot.steps import EditWorkedHours, SelectExistingDay, SelectWorkedHours -class Command(BaseCommand): +class Command(TelegramCommand): """Represent the edit work command.""" description = "Edit previously registered working hours" @property - def steps(self) -> list[Step]: + def steps(self): """Return the steps of the command.""" return [SelectExistingDay(self), SelectWorkedHours(self, steps_back=1), EditWorkedHours(self)] diff --git a/src/apps/timesheets/telegrambot/commands/registerovertime.py b/src/apps/timesheets/telegrambot/commands/registerovertime.py index 3019454..1230c0f 100644 --- a/src/apps/timesheets/telegrambot/commands/registerovertime.py +++ b/src/apps/timesheets/telegrambot/commands/registerovertime.py @@ -1,7 +1,6 @@ """Register overtime command for the Telegram bot.""" -from django_telegram_app.bot.base import BaseCommand, Step - +from apps.telegram.telegrambot.base import TelegramCommand from apps.timesheets.telegrambot.steps import ( CombineDateTime, Confirm, @@ -14,13 +13,13 @@ ) -class Command(BaseCommand): +class Command(TelegramCommand): """Represent the register overtime command.""" description = "Register overtime for a specific day on a specific project." @property - def steps(self) -> list[Step]: + def steps(self): """Return the steps of the command.""" return [ SelectProject(self), diff --git a/src/apps/timesheets/telegrambot/commands/registerwork.py b/src/apps/timesheets/telegrambot/commands/registerwork.py index 74911a2..8353ff2 100644 --- a/src/apps/timesheets/telegrambot/commands/registerwork.py +++ b/src/apps/timesheets/telegrambot/commands/registerwork.py @@ -1,16 +1,15 @@ """Register work command for the Telegram bot.""" -from django_telegram_app.bot.base import BaseCommand, Step - +from apps.telegram.telegrambot.base import TelegramCommand from apps.timesheets.telegrambot.steps import RegisterWorkedHours, SelectMissingDay, SelectWorkedHours -class Command(BaseCommand): +class Command(TelegramCommand): """Represent the register work command.""" description = "Register working hours for a specific day on a specific project." @property - def steps(self) -> list[Step]: + def steps(self): """Return the steps of the command.""" return [SelectMissingDay(self), SelectWorkedHours(self, steps_back=1), RegisterWorkedHours(self)] diff --git a/src/apps/timesheets/telegrambot/commands/requestoverview.py b/src/apps/timesheets/telegrambot/commands/requestoverview.py index 191e397..a1baef7 100644 --- a/src/apps/timesheets/telegrambot/commands/requestoverview.py +++ b/src/apps/timesheets/telegrambot/commands/requestoverview.py @@ -1,17 +1,16 @@ """Request Overview command for the Telegram bot.""" -from django_telegram_app.bot.base import BaseCommand, Step - +from apps.telegram.telegrambot.base import TelegramCommand from apps.timesheets.telegrambot.steps import SelectOverviewType, SelectTimesheet, ShowOverview -class Command(BaseCommand): +class Command(TelegramCommand): """Represent the request overview command.""" description = "Request an overview of a timesheet and its items." @property - def steps(self) -> list[Step]: + def steps(self): """Return the steps of the command.""" return [ SelectTimesheet(self, filter_kwargs={"user": self.settings.user}), diff --git a/src/apps/timesheets/telegrambot/steps/act.py b/src/apps/timesheets/telegrambot/steps/act.py index ce23fe4..6f85c8f 100644 --- a/src/apps/timesheets/telegrambot/steps/act.py +++ b/src/apps/timesheets/telegrambot/steps/act.py @@ -8,19 +8,21 @@ from django.core.exceptions import ValidationError from django.db import transaction -from django_telegram_app.bot.base import Step from django_telegram_app.bot.bot import send_message +from apps.telegram.telegrambot.base import TelegramStep from apps.timesheets.models import TimeRangeItemTypeRule, Timesheet, TimesheetItem, WeekdayItemTypeRule if TYPE_CHECKING: - from django_telegram_app.bot.base import BaseCommand, TelegramUpdate + from django_telegram_app.bot.base import TelegramUpdate + from apps.telegram.telegrambot.base import TelegramCommand -class CombineDateTime(Step): + +class CombineDateTime(TelegramStep): """Represent the combine date and time step in a Telegram bot command.""" - def __init__(self, command: BaseCommand, date_key: str, time_key: str, unique_id: str | None = None): + def __init__(self, command: TelegramCommand, date_key: str, time_key: str, unique_id: str | None = None): """Initialize the combine date and time step.""" self.date_key = date_key self.time_key = time_key @@ -34,7 +36,7 @@ def handle(self, telegram_update: "TelegramUpdate"): combined_datetime = datetime.combine(date_part, time_part) data[self.time_key] = combined_datetime.isoformat() data.pop(self.date_key) - telegram_update.callback_data = self.next_step_callback(**data) + telegram_update.callback_data = self.next_step_callback(data) return self.command.next_step(self.name, telegram_update) def _validate_time_format(self, time_str: str): @@ -59,7 +61,7 @@ def _validate_time_format(self, time_str: str): raise exc -class EditWorkedHours(Step): +class EditWorkedHours(TelegramStep): """Represent the editing of work step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -86,7 +88,7 @@ def _editwork(self, data: dict): timesheet_item.save() -class InsertTimesheetItems(Step): +class InsertTimesheetItems(TelegramStep): """Represent the step to insert timesheet items.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -257,7 +259,7 @@ def _add_item( ) -class MarkTimesheetAsCompleted(Step): +class MarkTimesheetAsCompleted(TelegramStep): """Represent the step to mark the selected timesheet as completed.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -274,7 +276,7 @@ def handle(self, telegram_update: "TelegramUpdate"): self.command.next_step(self.name, telegram_update) -class RegisterWorkedHours(Step): +class RegisterWorkedHours(TelegramStep): """Represent the registration of work step in a Telegram bot command.""" def handle(self, telegram_update): diff --git a/src/apps/timesheets/telegrambot/steps/confirm.py b/src/apps/timesheets/telegrambot/steps/confirm.py index 12236f8..83b21ed 100644 --- a/src/apps/timesheets/telegrambot/steps/confirm.py +++ b/src/apps/timesheets/telegrambot/steps/confirm.py @@ -5,11 +5,14 @@ from collections.abc import Callable from typing import TYPE_CHECKING -from django_telegram_app.bot.base import Step from django_telegram_app.bot.bot import send_message +from apps.telegram.telegrambot.base import TelegramStep + if TYPE_CHECKING: - from django_telegram_app.bot.base import BaseCommand, TelegramUpdate + from django_telegram_app.bot.base import TelegramUpdate + + from apps.telegram.telegrambot.base import TelegramCommand def prettyprint(data: dict): @@ -17,12 +20,12 @@ def prettyprint(data: dict): return "\n".join([f"{k}={v}" for k, v in data.items()]) -class Confirm(Step): +class Confirm(TelegramStep): """Represent the confirmation step in a Telegram bot command.""" def __init__( self, - command: BaseCommand, + command: TelegramCommand, steps_back: int = 0, unique_id: str | None = None, data_transform_func: Callable[[dict], str] = prettyprint, @@ -34,17 +37,15 @@ def __init__( def handle(self, telegram_update: "TelegramUpdate"): """Show the confirmation step.""" data = self.get_callback_data(telegram_update) - data_confirmed = dict(data, confirmed=True) - confirmation_yes = self.next_step_callback(**data_confirmed) - data_declined = dict(data, confirmed=False) - confirmation_no = self.cancel_callback(**data_declined) + confirmation_yes = self.next_step_callback(data, confirmed=True) + confirmation_no = self.cancel_callback(data, confirmed=False) keyboard = [ [{"text": "✅ Ok", "callback_data": confirmation_yes}], [{"text": "❌ Cancel", "callback_data": confirmation_no}], ] - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) message = f"{self.command.get_name()} with the following data?\n{self.data_transform_func(data)}" send_message( diff --git a/src/apps/timesheets/telegrambot/steps/select.py b/src/apps/timesheets/telegrambot/steps/select.py index d6873a2..8f25baf 100644 --- a/src/apps/timesheets/telegrambot/steps/select.py +++ b/src/apps/timesheets/telegrambot/steps/select.py @@ -12,23 +12,25 @@ from django.utils import timezone from django.utils.translation import gettext -from django_telegram_app.bot.base import Step from django_telegram_app.bot.bot import DO_NOTHING, send_message from apps.projects.models import Project +from apps.telegram.telegrambot.base import TelegramStep from apps.timesheets.models import Timesheet, TimesheetItem from apps.timesheets.telegrambot.steps._types import OverviewType if TYPE_CHECKING: - from django_telegram_app.bot.base import BaseCommand, TelegramUpdate + from django_telegram_app.bot.base import TelegramUpdate + from apps.telegram.telegrambot.base import TelegramCommand -class SelectDate(Step): + +class SelectDate(TelegramStep): """Represent the date selection step in a Telegram bot command.""" def __init__( self, - command: BaseCommand, + command: TelegramCommand, key: str, initial_date_key: str = "", steps_back: int = 0, @@ -45,13 +47,13 @@ def handle(self, telegram_update: "TelegramUpdate"): now = timezone.now() display_date = self._get_display_date(data, now) - data_previous = {**data, self.key: self._get_previous_display_date(display_date)} - data_next = {**data, self.key: self._get_next_display_date(display_date)} + prev_kw = {self.key: self._get_previous_display_date(display_date)} + next_kw = {self.key: self._get_next_display_date(display_date)} keyboard = [] header = [ - {"text": "<<", "callback_data": self.current_step_callback(**data_previous)}, + {"text": "<<", "callback_data": self.current_step_callback(data, **prev_kw)}, {"text": f"{str(display_date.month).zfill(2)}/{display_date.year}", "callback_data": DO_NOTHING}, - {"text": ">>", "callback_data": self.current_step_callback(**data_next)}, + {"text": ">>", "callback_data": self.current_step_callback(data, **next_kw)}, ] keyboard.append(header) @@ -68,11 +70,11 @@ def handle(self, telegram_update: "TelegramUpdate"): text = str(day).zfill(2) if selected_date == now.date(): text = f"({text})" - data_dict = {**data, self.key: selected_date} - row.append({"text": text, "callback_data": self.next_step_callback(**data_dict)}) + kwargs = {self.key: selected_date} + row.append({"text": text, "callback_data": self.next_step_callback(data, **kwargs)}) keyboard.append(row) - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) reply_markup = {"inline_keyboard": keyboard} send_message( @@ -113,7 +115,7 @@ def _get_previous_display_date(self, displayed_date: date): return displayed_date.replace(year=previous_year, month=previous_month) -class SelectDay(Step): +class SelectDay(TelegramStep): """Represent the day selection step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -133,7 +135,7 @@ def handle(self, telegram_update: "TelegramUpdate"): self._maybe_add_pagination_buttons(keyboard, days, data, current_page, end) - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) reply_markup = {"inline_keyboard": keyboard} send_message( @@ -153,11 +155,11 @@ def get_keyboard(self, days: list[tuple], data: dict, start: int, end: int): def _maybe_add_pagination_buttons(self, keyboard: list, days: list, data: dict, current_page: int, end: int): if current_page > 1: - data_back = dict(data, current_page=current_page - 1) - keyboard.append([{"text": "⬅️ Back", "callback_data": self.current_step_callback(**data_back)}]) + callback_back = self.current_step_callback(data, current_page=current_page - 1) + keyboard.append([{"text": "⬅️ Back", "callback_data": callback_back}]) if len(days) > end: - data_next = dict(data, current_page=current_page + 1) - keyboard.append([{"text": "➡️ Next", "callback_data": self.current_step_callback(**data_next)}]) + callback_next = self.current_step_callback(data, current_page=current_page + 1) + keyboard.append([{"text": "➡️ Next", "callback_data": callback_next}]) class SelectExistingDay(SelectDay): @@ -180,21 +182,16 @@ def get_keyboard(self, days: list[tuple[Project, TimesheetItem]], data: dict, st """Get the keyboard for the given days and data.""" keyboard = [] for project, item in days[start:end]: - data_day = dict( + callback_next = self.next_step_callback( data, start_date=item.date, project_id=project.pk, project_name=project.name, item_pk=item.pk ) keyboard.append( - [ - { - "text": f"{project}: {item.date} ({item.worked_hours}h)", - "callback_data": self.next_step_callback(**data_day), - } - ] + [{"text": f"{project}: {item.date} ({item.worked_hours}h)", "callback_data": callback_next}] ) return keyboard -class SelectItemType(Step): +class SelectItemType(TelegramStep): """Represent the item type selection step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -202,28 +199,21 @@ def handle(self, telegram_update: "TelegramUpdate"): data = self.get_callback_data(telegram_update) keyboard = [] for item_type in TimesheetItem.ItemType: - data_item = dict(data, item_type=item_type.value, item_type_label=item_type.label) - keyboard.append( - [ - { - "text": str(item_type.label), # Needs str cast for lazy translation objects - "callback_data": self.next_step_callback(**data_item), - } - ] - ) + item_label = str(item_type.label) # Needs str cast for lazy translation objects + next_callback = self.next_step_callback(data, item_type=item_type.value, item_type_label=item_type.label) + keyboard.append([{"text": item_label, "callback_data": next_callback}]) # Add the infer item type - data_infer = dict(data, item_type=0, item_type_label="Inferred") keyboard.append( [ { "text": "Inferred", - "callback_data": self.next_step_callback(**data_infer), + "callback_data": self.next_step_callback(data, item_type=0, item_type_label="Inferred"), } ] ) - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) send_message( "Select the item type:", @@ -246,12 +236,14 @@ def get_keyboard(self, days: list[tuple[Project, date]], data: dict, start: int, """Get the keyboard for the given days and data.""" keyboard = [] for project, day in days[start:end]: - data_day = dict(data, start_date=day, project_id=project.pk, project_name=project.name) - keyboard.append([{"text": f"{project}: {day}", "callback_data": self.next_step_callback(**data_day)}]) + callback_day = self.next_step_callback( + data, start_date=day, project_id=project.pk, project_name=project.name + ) + keyboard.append([{"text": f"{project}: {day}", "callback_data": callback_day}]) return keyboard -class SelectOverviewType(Step): +class SelectOverviewType(TelegramStep): """Represent the overview type selection step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -262,23 +254,23 @@ def handle(self, telegram_update: "TelegramUpdate"): [ { "text": "Summary Overview", - "callback_data": self.next_step_callback(**data, overview_type=OverviewType.SUMMARY.value), + "callback_data": self.next_step_callback(data, overview_type=OverviewType.SUMMARY.value), } ], [ { "text": "Detailed Overview", - "callback_data": self.next_step_callback(**data, overview_type=OverviewType.DETAILED.value), + "callback_data": self.next_step_callback(data, overview_type=OverviewType.DETAILED.value), } ], [ { "text": "Holidays Overview", - "callback_data": self.next_step_callback(**data, overview_type=OverviewType.HOLIDAYS.value), + "callback_data": self.next_step_callback(data, overview_type=OverviewType.HOLIDAYS.value), } ], ] - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) send_message( "Which type of overview would you like to see?", @@ -288,7 +280,7 @@ def handle(self, telegram_update: "TelegramUpdate"): ) -class SelectProject(Step): +class SelectProject(TelegramStep): """Represent the project selection step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): @@ -305,18 +297,17 @@ def handle(self, telegram_update: "TelegramUpdate"): data = self.get_callback_data(telegram_update) if len(projects) == 1: - data["project_id"] = projects[0].pk - data["project_name"] = str(projects[0]) - telegram_update.callback_data = self.next_step_callback(**data) + project = projects[0] + callback_next = self.next_step_callback(data, project_id=project.pk, project_name=str(project)) + telegram_update.callback_data = callback_next return self.command.next_step(self.name, telegram_update) keyboard = [] for project in projects: - data["project_id"] = project.pk - data["project_name"] = str(project) - keyboard.append([{"text": str(project), "callback_data": self.next_step_callback(**data)}]) + callback_next = self.next_step_callback(data, project_id=project.pk, project_name=str(project)) + keyboard.append([{"text": str(project), "callback_data": callback_next}]) - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) send_message( "Select a project:", @@ -326,12 +317,12 @@ def handle(self, telegram_update: "TelegramUpdate"): ) -class SelectTimesheet(Step): +class SelectTimesheet(TelegramStep): """Represent the timesheet selection step in a Telegram bot command.""" def __init__( self, - command: BaseCommand, + command: TelegramCommand, steps_back: int = 0, filter_kwargs: dict | None = None, order_by: tuple | None = None, @@ -353,18 +344,17 @@ def handle(self, telegram_update: "TelegramUpdate"): data = self.get_callback_data(telegram_update) if len(timesheets) == 1: - data["timesheet_id"] = timesheets[0].pk - data["timesheet_name"] = str(timesheets[0]) - telegram_update.callback_data = self.next_step_callback(**data) + timesheet = timesheets[0] + next_callback = self.next_step_callback(data, timesheet_id=timesheet.pk, timesheet_name=str(timesheet)) + telegram_update.callback_data = next_callback return self.command.next_step(self.name, telegram_update) keyboard = [] for timesheet in timesheets: - data["timesheet_id"] = timesheet.pk - data["timesheet_name"] = str(timesheet) - keyboard.append([{"text": str(timesheet), "callback_data": self.next_step_callback(**data)}]) + next_callback = self.next_step_callback(data, timesheet_id=timesheet.pk, timesheet_name=str(timesheet)) + keyboard.append([{"text": str(timesheet), "callback_data": next_callback}]) - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) send_message( "Select a timesheet:", @@ -374,7 +364,7 @@ def handle(self, telegram_update: "TelegramUpdate"): ) -class SelectWorkedHours(Step): +class SelectWorkedHours(TelegramStep): """Represent the hours worked selection step in a Telegram bot command.""" def handle(self, telegram_update): @@ -383,10 +373,9 @@ def handle(self, telegram_update): options = {"Full day (8h)": 8, "Half day (4h)": 4, "Holiday (0h)": 0} keyboard = [] for key, value in options.items(): - data_duration = dict(data, duration=value) - keyboard.append([{"text": key, "callback_data": self.next_step_callback(**data_duration)}]) + keyboard.append([{"text": key, "callback_data": self.next_step_callback(data, duration=value)}]) - self.maybe_add_previous_button(keyboard, **data) + self.maybe_add_previous_button(keyboard, data) reply_markup = {"inline_keyboard": keyboard} send_message( diff --git a/src/apps/timesheets/telegrambot/steps/show.py b/src/apps/timesheets/telegrambot/steps/show.py index 522cdf2..4e87839 100644 --- a/src/apps/timesheets/telegrambot/steps/show.py +++ b/src/apps/timesheets/telegrambot/steps/show.py @@ -2,9 +2,9 @@ from typing import TYPE_CHECKING -from django_telegram_app.bot.base import Step from django_telegram_app.bot.bot import send_message +from apps.telegram.telegrambot.base import TelegramStep from apps.timesheets.models import Timesheet from apps.timesheets.telegrambot.steps._types import OverviewType @@ -12,7 +12,7 @@ from django_telegram_app.bot.base import TelegramUpdate -class ShowOverview(Step): +class ShowOverview(TelegramStep): """Represent the show overview step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): diff --git a/src/apps/timesheets/telegrambot/steps/wait.py b/src/apps/timesheets/telegrambot/steps/wait.py index 1c92380..0073372 100644 --- a/src/apps/timesheets/telegrambot/steps/wait.py +++ b/src/apps/timesheets/telegrambot/steps/wait.py @@ -4,17 +4,20 @@ from typing import TYPE_CHECKING -from django_telegram_app.bot.base import Step from django_telegram_app.bot.bot import send_message +from apps.telegram.telegrambot.base import TelegramStep + if TYPE_CHECKING: - from django_telegram_app.bot.base import BaseCommand, TelegramUpdate + from django_telegram_app.bot.base import TelegramUpdate + + from apps.telegram.telegrambot.base import TelegramCommand -class WaitForTime(Step): +class WaitForTime(TelegramStep): """Represent the wait for time input step in a Telegram bot command.""" - def __init__(self, command: BaseCommand, key: str, date_key: str, unique_id: str | None = None): + def __init__(self, command: TelegramCommand, key: str, date_key: str, unique_id: str | None = None): """Initialize the wait for time input step.""" self.key = key self.date_key = date_key @@ -31,15 +34,14 @@ def handle(self, telegram_update: "TelegramUpdate"): ) -class WaitForDescription(Step): +class WaitForDescription(TelegramStep): """Represent the description selection step in a Telegram bot command.""" def handle(self, telegram_update: "TelegramUpdate"): """Prompt the user to input a description or select no description.""" data = self.get_callback_data(telegram_update) self.add_waiting_for("description", data) - data_dict = dict(data, description="") - keyboard = [[{"text": "No description.", "callback_data": self.next_step_callback(**data_dict)}]] + keyboard = [[{"text": "No description.", "callback_data": self.next_step_callback(data, description="")}]] send_message( "Send the description (or select 'No description'):", self.command.settings.chat_id, diff --git a/src/ida/settings.py b/src/ida/settings.py index a4e4007..e676079 100644 --- a/src/ida/settings.py +++ b/src/ida/settings.py @@ -47,6 +47,7 @@ "apps.users", "apps.timesheets", "apps.projects", + "apps.telegram", "django_telegram_app", ] @@ -208,6 +209,7 @@ "WEBHOOK_TOKEN": env.read("TELEGRAM_WEBHOOK_TOKEN"), "ROOT_URL": env.read("TELEGRAM_ROOT_URL", "telegram/"), "WEBHOOK_URL": env.read("TELEGRAM_WEBHOOK_URL", "webhook"), + "REGISTER_DEFAULT_ADMIN": False, } -DOMAIN_NAME = env.read("DJANGO_DOMAIN_NAME", "http://localhost:8000") +TELEGRAM_SETTINGS_MODEL = "telegram.TelegramSettings"