Source code for onboarding.models.assistant

import inspect
import time


from typing import overload, Any, TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
    from collections.abc import Callable
    from onegov.core.request import CoreRequest
    from onegov.form import Form
    from onegov.onboarding.app import OnboardingApp
    from typing import Self, TypeAlias

[docs] _T = TypeVar('_T')
_F = TypeVar('_F', bound='Callable[..., Any]') _ViewF = TypeVar('_ViewF', bound='Callable[[Any, CoreRequest], Any]') _FormT = TypeVar('_FormT', bound='Form') _FormView: TypeAlias = Callable[[Any, CoreRequest, _FormT], _T]
[docs] class Assistant: """ Describes an assistant guiding a user through onboarding. """ def __init__(self, app: 'OnboardingApp', current_step_number: int = 1):
[docs] self.app = app
[docs] methods = (fn[1] for fn in inspect.getmembers(self))
methods = (fn for fn in methods if inspect.ismethod(fn)) methods = (fn for fn in methods if hasattr(fn, 'is_step'))
[docs] self.steps = sorted(Step(fn, fn.order, fn.form) for fn in methods)
if current_step_number < 1: raise KeyError('Invalid current step') if current_step_number > len(self.steps): raise KeyError('Invalid current step')
[docs] self.current_step_number = current_step_number
@property
[docs] def current_step(self) -> 'Step': return self.steps[self.current_step_number - 1]
@property
[docs] def progress(self) -> tuple[int, int]: return self.current_step_number, len(self.steps)
@property
[docs] def is_first_step(self) -> bool: return self.current_step_number == 1
@property
[docs] def is_last_step(self) -> bool: return self.current_step_number == len(self.steps)
[docs] def for_next_step(self) -> 'Self': assert not self.is_last_step return self.__class__(self.app, self.current_step_number + 1)
[docs] def for_prev_step(self) -> 'Self': assert not self.is_first_step return self.__class__(self.app, self.current_step_number - 1)
[docs] def for_first_step(self) -> 'Self': return self.__class__(self.app, 1)
@overload @classmethod
[docs] def step(cls, form: None = None) -> 'Callable[[_ViewF], _ViewF]': ...
@overload @classmethod def step( cls, form: type['_FormT'] ) -> 'Callable[[_FormView[_FormT, _T]], _FormView[_FormT, _T]]': ... @classmethod def step(cls, form: type['Form'] | None = None) -> 'Callable[[_F], _F]': def decorator(fn: '_F') -> '_F': fn.is_step = True # type:ignore[attr-defined] # FIXME: monotonic may be more reliable fn.order = time.process_time() # type:ignore[attr-defined] fn.form = form # type:ignore[attr-defined] return fn return decorator
[docs] class Step: """ Describes a step in an assistant. """ @overload def __init__( self, view_handler: 'Callable[[CoreRequest], Any]', order: float, form: None ): ... @overload def __init__( self, view_handler: 'Callable[[CoreRequest, Form], Any]', order: float, form: 'Form' ): ... def __init__( self, view_handler: 'Callable[..., Any]', order: float, form: 'Form | None' ):
[docs] self.view_handler = view_handler
[docs] self.order = order
[docs] self.form = form
[docs] def __lt__(self, other: 'Step') -> bool: return self.order < other.order
[docs] def handle_view(self, request: 'CoreRequest', form: 'Form | None') -> Any: if form is None: return self.view_handler(request) else: return self.view_handler(request, form)
[docs] class DefaultAssistant: def __init__(self, assistant: Assistant):
[docs] self.assistant = assistant