diff --git a/README.md b/README.md index 2e88cc7e..41c023a1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Viewflow comes in two flavors: ## Installation -Viewflow works with Python 3.7 or greater and Django 3.1+ +Viewflow works with Python 3.7 or greater and Django 3.2+ Viewflow: diff --git a/setup.py b/setup.py index 3047dc95..99ed742e 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setuptools.setup( name='django-viewflow', - version='2.0.0a0', + version='2.0.0a2', author_email='kmmbvnr@gmail.com', author='Mikhail Podgurskiy', description="Reusable library to build business applications fast", diff --git a/tox.ini b/tox.ini index b5a4c221..a95a5665 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{37}-dj{31} +envlist = py{37}-dj{32} skipsdist = True [testenv] @@ -8,39 +8,43 @@ commands = {posargs:./manage.py test --exclude-tag=selenium --exclude-tag=integr deps = # core dependences - dj31: Django==3.1.6 - dj32: Django==3.2a1 - django-filter==2.4.0 - djangorestframework==3.12.2 - pyyaml==5.3.1 - uritemplate==3.0.1 + dj32: Django==3.2.9 + dj40: Django==4.0b1 + django-filter==21.1 + djangorestframework==3.12.4 + pyyaml==6.0 + uritemplate==4.1.1 # 3d party integration - celery==5.0.5 - django-formtools==2.2 - django-guardian==2.3.0 - django-redis==4.12.1 - django-reversion==3.0.9 - django-polymodels==1.6.0 - django-allauth==0.44.0 + celery==5.2.0 dash==2.0.0 - - # developement - django-environ==0.4.5 - dj-database-url==0.5.0 - django-extensions==3.1.0 - sentry-sdk==0.19.5 - flake8==3.8.3 - ipython==7.20.0 - ipdb==0.13.4 - tblib==1.7.0 + django-allauth==0.46.0 + django-formtools==2.3 + django-guardian==2.4.0 + django-import-export==2.6.1 + django-polymodels==1.7.0 + django-redis==5.0.0 + django-reversion==4.0.1 + + # development ansible==2.10.6 - twine==3.3.0 - psycopg2-binary==2.8.6 + colorlover==0.3.0 + dj-database-url==0.5.0 django_sendmail_backend==0.1.2 - selenium==3.141.0 + django-environ==0.8.1 + django-extensions==3.1.5 + flake8==4.0.1 + hiredis==2.0.0 html5lib==1.1 + ipdb==0.13.4 + ipython==7.20.0 numpy==1.21.2 + pandas==1.3.3 + psycopg2-binary==2.8.6 + selenium==3.141.0 + sentry-sdk==0.19.5 + tblib==1.7.0 + twine==3.6.0 # packaging pyc-wheel==1.2.4 diff --git a/viewflow/__init__.py b/viewflow/__init__.py index e6411151..d6ed6226 100644 --- a/viewflow/__init__.py +++ b/viewflow/__init__.py @@ -1,4 +1,12 @@ -"""Viewflow - dev toolkit for backoffice automation.""" +"""Viewflow - The Django extension for perfectionists with yesterday’s deadlines.""" + +# Copyright (c) 2017-2020, Mikhail Podgurskiy +# All Rights Reserved. + +# This work is dual-licensed under AGPL defined in file 'LICENSE' with +# LICENSE_EXCEPTION and the Commercial license defined in file 'COMM_LICENSE', +# which is part of this source code package. + from django.conf import settings as django_settings from .conf import settings from .this_object import this @@ -21,6 +29,6 @@ site_middleware = 'viewflow.middleware.SiteMiddleware' if site_middleware not in django_settings.MIDDLEWARE: django_settings.MIDDLEWARE += (site_middleware, ) - turbolinks_middleware = 'viewflow.middleware.TurbolinksMiddleware' - if turbolinks_middleware not in django_settings.MIDDLEWARE: - django_settings.MIDDLEWARE += (turbolinks_middleware, ) + turbo_middleware = 'viewflow.middleware.HotwireTurboMiddleware' + if turbo_middleware not in django_settings.MIDDLEWARE: + django_settings.MIDDLEWARE += (turbo_middleware, ) diff --git a/viewflow/components/vf-card/index.scss b/viewflow/components/vf-card/index.scss index d1fe2bdd..9dced82a 100644 --- a/viewflow/components/vf-card/index.scss +++ b/viewflow/components/vf-card/index.scss @@ -84,4 +84,6 @@ justify-content: flex-end; padding: 24px; padding-bottom: 12px; + padding-left: 8px; + padding-top: 32px; } diff --git a/viewflow/components/vf-field-checkbox/index.js b/viewflow/components/vf-field-checkbox/index.js index fec94d8a..d86bf81c 100644 --- a/viewflow/components/vf-field-checkbox/index.js +++ b/viewflow/components/vf-field-checkbox/index.js @@ -1,5 +1,4 @@ -import {noShadowDOM} from 'component-register'; -import {onCleanup, createEffect} from 'solid-js'; +import {onCleanup, createEffect} from 'solid-js'; import {customElement} from 'solid-element'; import {MDCCheckbox} from '@material/checkbox'; import cc from 'classcat'; @@ -23,7 +22,9 @@ const VCheckboxField = customElement('vf-field-checkbox', defaultProps, (props, let control; let checkbox; - noShadowDOM(element); + Object.defineProperty(element, 'renderRoot', { + value: element, + }); createEffect(() => { checkbox = new MDCCheckbox(control); diff --git a/viewflow/components/vf-field-input/index.js b/viewflow/components/vf-field-input/index.js index 58f07cf9..e0a4d833 100644 --- a/viewflow/components/vf-field-input/index.js +++ b/viewflow/components/vf-field-input/index.js @@ -2,7 +2,6 @@ import {customElement} from 'solid-element'; import {MDCTextField} from '@material/textfield'; -import {noShadowDOM} from 'component-register'; import {onCleanup, createEffect} from 'solid-js'; import cc from 'classcat'; @@ -48,6 +47,13 @@ export const Input = (props) => { }); }); + createEffect(() => { + props.value; + if (textfield) { + textfield.layout(); + } + }); + onCleanup(() => { textfield.destroy(); }); @@ -90,6 +96,7 @@ export const Input = (props) => { required={ !!props.required } step = { props.step } type={ props.type } + tabindex={ props.tabIndex } value={ props.value } aria-labelledby={ props.id + '_label' } oninput={props.onInput} @@ -119,7 +126,9 @@ export const Input = (props) => { }; const VInputField = customElement('vf-field-input', defaultProps, (props, {element}) => { - noShadowDOM(element); + Object.defineProperty(element, 'renderRoot', { + value: element, + }); return (
diff --git a/viewflow/components/vf-field-password/index.js b/viewflow/components/vf-field-password/index.js index dd3da1e1..dcf5e491 100644 --- a/viewflow/components/vf-field-password/index.js +++ b/viewflow/components/vf-field-password/index.js @@ -2,7 +2,6 @@ import {customElement} from 'solid-element'; import {createState} from 'solid-js'; -import {noShadowDOM} from 'component-register'; import {Input, HelpText} from '../vf-field-input'; import './index.scss'; @@ -28,18 +27,22 @@ const VPasswordField = customElement('vf-field-password', defaultProps, (props, 'visible': props.type !== 'password', }); - noShadowDOM(element); + Object.defineProperty(element, 'renderRoot', { + value: element, + }); const onBtnClick = (event) => { event.preventDefault(); setState({'visible': !state.visible}); }; + const inputType = () => state.visible ? 'text': 'password'; + return (
state.visible ? 'text': 'password'} + type={inputType()} trailingButton={() => state.visible ? 'visibility' : 'visibility_off'} onTrailingButtonClick={onBtnClick}/> { props.helpText || props.error ? : '' } diff --git a/viewflow/components/vf-field-radio-select/index.js b/viewflow/components/vf-field-radio-select/index.js index 07324869..6e289ac7 100644 --- a/viewflow/components/vf-field-radio-select/index.js +++ b/viewflow/components/vf-field-radio-select/index.js @@ -1,4 +1,3 @@ -import {noShadowDOM} from 'component-register'; import {customElement} from 'solid-element'; import cc from 'classcat'; @@ -20,7 +19,9 @@ const defaultProps = { const VRadioSelectField = customElement('vf-field-radio-select', defaultProps, (props, {element}) => { - noShadowDOM(element); + Object.defineProperty(element, 'renderRoot', { + value: element, + }); const items = (props) => { const items = []; diff --git a/viewflow/components/vf-field/jhtml.js b/viewflow/components/vf-field/jhtml.js index 9d2ecc7b..39490404 100644 --- a/viewflow/components/vf-field/jhtml.js +++ b/viewflow/components/vf-field/jhtml.js @@ -35,7 +35,7 @@ class DOMBuilder { element.textContent = this.textContent; } - for (let attr in this.attrs) { + for (const attr in this.attrs) { if (this.attrs.hasOwnProperty(attr)) { let value = this.attrs[attr]; @@ -54,8 +54,8 @@ class DOMBuilder { } } else if (! (typeof(value) === 'string' || typeof(value) === 'number')) { // join hash key if enabled - let valuesList = []; - for (let key in value) { + const valuesList = []; + for (const key in value) { if (value.hasOwnProperty(key)) { const keyValue = value[key]; if (keyValue) { diff --git a/viewflow/components/vf-page/_menu.scss b/viewflow/components/vf-page/_menu.scss index 68914f0d..bf6e01b4 100644 --- a/viewflow/components/vf-page/_menu.scss +++ b/viewflow/components/vf-page/_menu.scss @@ -129,6 +129,21 @@ >i.material-icons { color: inherit; } + + margin-left: 8px; + margin-right: 8px; + padding-left: 8px; + padding-right: 8px; + + &::before { + background: #3f51b5; + background: var(--mdc-theme-primary, #3f51b5); + opacity: 0.1; + content: " "; + width: 100%; + height: 100; + position: absolute; + } } } diff --git a/viewflow/components/vf-page/index.js b/viewflow/components/vf-page/index.js index 5dd15aca..1cb57690 100644 --- a/viewflow/components/vf-page/index.js +++ b/viewflow/components/vf-page/index.js @@ -1,4 +1,6 @@ /* eslint-env browser */ +import './_grid.scss'; +import './_menu.scss'; import './index.scss'; import {drawer, topAppBar} from 'material-components-web'; diff --git a/viewflow/components/vf-page/index.scss b/viewflow/components/vf-page/index.scss index 62e21847..18f1c388 100644 --- a/viewflow/components/vf-page/index.scss +++ b/viewflow/components/vf-page/index.scss @@ -1,6 +1,3 @@ -@import './_grid.scss'; -@import './_menu.scss'; - html { height: 100%; } diff --git a/viewflow/fsm/base.py b/viewflow/fsm/base.py index d0375201..76f5cbb2 100644 --- a/viewflow/fsm/base.py +++ b/viewflow/fsm/base.py @@ -4,16 +4,23 @@ # All Rights Reserved. # This work is dual-licensed under AGPL defined in file 'LICENSE' with -# LICENSE_EXCEPTION and the Commercial licence defined in file 'COMM_LICENSE', +# LICENSE_EXCEPTION and the Commercial license defined in file 'COMM_LICENSE', # which is part of this source code package. from __future__ import annotations import inspect -from typing import Callable, List, Type +from typing import Any, Dict, Mapping, Iterable, List, Type, Optional from viewflow.this_object import ThisObject from viewflow.utils import MARKER -from .typing import Condition, StateTransitions +from .typing import ( + UserModel, + Condition, + StateTransitions, + Permission, + StateValue, + TransitionFunction, +) class TransitionNotAllowed(Exception): @@ -23,41 +30,51 @@ class TransitionNotAllowed(Exception): class Transition(object): """State transition definition.""" - def __init__(self, func, source, target, label=None, conditions=None, permission=None): # noqa D102 + def __init__( + self, + func: TransitionFunction, + source: StateValue, + target: Optional[StateValue], + label: Optional[str] = None, + conditions: Optional[List[Condition]] = None, + permission: Optional[Permission] = None, + ): # noqa D102 self.func = func self.source = source self.target = target self._label = label self.permission = permission - self.conditions: List[Condition] = conditions if conditions else [] + self.conditions = conditions if conditions else [] - def __str__(self): - return f'{self.label} Transition' + def __str__(self) -> str: + return f"{self.label} Transition" @property - def label(self): + def label(self) -> str: """Transition human-readable label.""" if self._label: return self._label else: try: - return self.func.label + return self.func.label # type: ignore except AttributeError: return self.func.__name__.title() @property - def slug(self): + def slug(self) -> str: return self.func.__name__ - def conditions_met(self, instance) -> bool: + def conditions_met(self, instance: object) -> bool: """Check that all associated conditions is True.""" conditions = [ - condition.resolve(instance.__class__) if isinstance(condition, ThisObject) else condition + condition.resolve(instance.__class__) + if isinstance(condition, ThisObject) + else condition for condition in self.conditions ] return all(map(lambda condition: condition(instance), conditions)) - def has_perm(self, instance, user) -> bool: + def has_perm(self, instance: object, user: UserModel) -> bool: """Check the permission of the transition.""" if self.permission is None: return True @@ -65,7 +82,7 @@ def has_perm(self, instance, user) -> bool: return self.permission(instance, user) elif isinstance(self.permission, ThisObject): permission = self.permission.resolve(instance) - return permission(user) + return permission(user) # type: ignore else: raise ValueError(f"Unknown permission type {type(self.permission)}") @@ -80,7 +97,13 @@ class TransitionMethod(object): do_not_call_in_templates = True - def __init__(self, state: State, func: Callable, descriptor: TransitionDescriptor, owner: Type): + def __init__( + self, + state: State, + func: TransitionFunction, + descriptor: TransitionDescriptor, + owner: Type[object], + ): self._state = state self._func = func self._descriptor = descriptor @@ -88,13 +111,13 @@ def __init__(self, state: State, func: Callable, descriptor: TransitionDescripto self.__doc__ = func.__doc__ - def get_transitions(self) -> List[Transition]: + def get_transitions(self) -> Iterable[Transition]: return self._descriptor.get_transitions() @property - def slug(self): + def slug(self) -> str: """Transition name.""" - return self.func.__name__ + return self._func.__name__ class TransitionBoundMethod(object): @@ -104,18 +127,21 @@ class TransitionBoundMethod(object): class Wrapper(object): """Wrapper context object, to simplify __call__ method debug""" - def __init__(self, parent: 'TransitionBoundMethod', kwargs): + + def __init__(self, parent: "TransitionBoundMethod", kwargs: Mapping[str, Any]): self.parent = parent self.caller_kwargs = kwargs - self.initial_state = None - self.target_state = None + self.initial_state: StateValue + self.target_state: StateValue - def __enter__(self): + def __enter__(self) -> None: self.initial_state = self.parent._state.get(self.parent._instance) transition = self.parent._descriptor.get_transition(self.initial_state) if transition is None: - raise TransitionNotAllowed(f'{self.parent.label} :: no transition from "{self.initial_state}"') + raise TransitionNotAllowed( + f'{self.parent.label} :: no transition from "{self.initial_state}"' + ) if not transition.conditions_met(self.parent._instance): raise TransitionNotAllowed( @@ -126,27 +152,40 @@ def __enter__(self): if self.target_state: self.parent._state.set(self.parent._instance, self.target_state) - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Any, + ) -> None: if exc_type is not None: self.parent._state.set(self.parent._instance, self.initial_state) else: self.parent._state.transition_succeed( - self.parent._instance, self.parent, - self.initial_state, self.target_state, - **self.caller_kwargs + self.parent._instance, + self.parent, + self.initial_state, + self.target_state, + **self.caller_kwargs, ) - def __init__(self, state, func, descriptor, instance): - self._state: State = state - self._func: Callable = func - self._descriptor: TransitionDescriptor = descriptor + def __init__( + self, + state: State, + func: TransitionFunction, + descriptor: TransitionDescriptor, + instance: object, + ): + self._state = state + self._func = func + self._descriptor = descriptor self._instance = instance - def original(self, *args, **kwargs): + def original(self, *args: Any, **kwargs: Any) -> Any: """Call the unwrapped class method.""" return self._func(self._instance, *args, **kwargs) - def can_proceed(self, check_conditions=True): + def can_proceed(self, check_conditions: bool = True) -> bool: """Check is transition available.""" current_state = self._state.get(self._instance) transition = self._descriptor.get_transition(current_state) @@ -154,7 +193,7 @@ def can_proceed(self, check_conditions=True): return transition.conditions_met(self._instance) return False - def has_perm(self, user): + def has_perm(self, user: UserModel) -> bool: current_state = self._state.get(self._instance) transition = self._descriptor.get_transition(current_state) if transition: @@ -162,7 +201,7 @@ def has_perm(self, user): return False @property - def label(self): + def label(self) -> str: """Transition human-readable label.""" current_state = self._state.get(self._instance) transition = self._descriptor.get_transition(current_state) @@ -170,15 +209,15 @@ def label(self): return transition.label else: try: - return self._func.label + return self._func.label # type: ignore except AttributeError: return self._func.__name__.title() - def __call__(self, *args, **kwargs): + def __call__(self, *args: Any, **kwargs: Any) -> Any: with TransitionBoundMethod.Wrapper(self, kwargs=kwargs): return self._func(self._instance, *args, **kwargs) - def get_transitions(self): + def get_transitions(self) -> Iterable[Transition]: return self._descriptor.get_transitions() @@ -187,25 +226,26 @@ class TransitionDescriptor(object): do_not_call_in_templates = True - def __init__(self, state, func): # noqa D102 + def __init__(self, state: StateValue, func: TransitionFunction): # noqa D102 self._state = state self._func = func - self._transitions = {} + self._transitions: Dict[StateValue, Transition] = {} - def __get__(self, instance, owner=None): + def __get__(self, instance: object, owner: Optional[Type[object]] = None) -> TransitionMethod | TransitionBoundMethod: if instance: return TransitionBoundMethod(self._state, self._func, self, instance) else: + assert owner is not None # make mypy happy return TransitionMethod(self._state, self._func, self, owner) - def add_transition(self, transition): + def add_transition(self, transition: Transition) -> None: self._transitions[transition.source] = transition - def get_transitions(self) -> List[Transition]: + def get_transitions(self) -> Iterable[Transition]: """List of all transitions.""" return self._transitions.values() - def get_transition(self, source_state): + def get_transition(self, source_state: StateValue) -> Optional[Transition]: """Get a transition of a source_state. Returns None if there is no outgoing transitions. @@ -219,17 +259,25 @@ def get_transition(self, source_state): class SuperTransitionDescriptor(object): do_not_call_in_templates = True - def __init__(self, state, func): # noqa D102 + def __init__(self, state: State, func: TransitionFunction): # noqa D102 self._state = state self._func = func - def __get__(self, instance, owner=None): + def __get__(self, instance: object, owner: Optional[Type[object]] = None) -> TransitionBoundMethod | TransitionMethod: if instance: - return TransitionBoundMethod(self._state, self._func, self.get_descriptor(instance.__class__), instance) + return TransitionBoundMethod( + self._state, + self._func, + self.get_descriptor(instance.__class__), + instance, + ) else: - return TransitionMethod(self._state, self._func, self.get_descriptor(owner), owner) + assert owner is not None # make mypy happy + return TransitionMethod( + self._state, self._func, self.get_descriptor(owner), owner + ) - def get_descriptor(self, owner) -> TransitionDescriptor: + def get_descriptor(self, owner: Type[object]) -> TransitionDescriptor: """Lookup for the transition descriptor in the base classes.""" for cls in owner.__mro__[1:]: if hasattr(cls, self._func.__name__): @@ -237,7 +285,7 @@ def get_descriptor(self, owner) -> TransitionDescriptor: if isinstance(super_method, TransitionMethod): break else: - raise ValueError('Base transition not found') + raise ValueError("Base transition not found") return super_method._descriptor @@ -250,60 +298,58 @@ class StateDescriptor(object): Review.state.get_transitions() """ - def __init__(self, state: 'State', owner: type): + def __init__(self, state: "State", owner: type): self._state = state self._owner = owner - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: return getattr(self._state, attr) def get_transitions(self) -> StateTransitions: - propname = '__fsm_{}_transitions'.format(self._state.propname) - transitions = self._owner.__dict__.get(propname, None) + propname = "__fsm_{}_transitions".format(self._state.propname) + transitions: StateTransitions = self._owner.__dict__.get(propname, None) if transitions is None: transitions = {} methods = inspect.getmembers( - self._owner, - lambda attr: isinstance(attr, TransitionMethod) + self._owner, lambda attr: isinstance(attr, TransitionMethod) ) - transitions = { - method: method.get_transitions() - for _, method in methods - } + transitions = {method: method.get_transitions() for _, method in methods} setattr(self._owner, propname, transitions) return transitions - def get_outgoing_transitions(self, state) -> List[Transition]: + def get_outgoing_transitions(self, state: State) -> List[Transition]: return [ transition for transitions in self.get_transitions().values() for transition in transitions - if transition.source == state or (transition.source == State.ANY and transition.target != state) + if transition.source == state + or (transition.source == State.ANY and transition.target != state) ] class State(object): """State slot field.""" - ANY = MARKER('ANY') + ANY = MARKER("ANY") - def __init__(self, states, default=None): + def __init__(self, states: Any, default: StateValue = None): self._default = default self._setter = None self._getter = None self._on_success = None - def __get__(self, instance, owner=None): + def __get__(self, instance: object, owner: Optional[Type[object]] = None) -> Any: if instance: return self.get(instance) + assert owner is not None # make mypy happy return StateDescriptor(self, owner) - def __set__(self, instance, value): - raise AttributeError('Direct state modification is not allowed') + def __set__(self, instance: object, value: StateValue) -> None: + raise AttributeError("Direct state modification is not allowed") - def get(self, instance): + def get(self, instance: object) -> Any: """Get the state from the underline class instance.""" if self._getter: value = self._getter(instance) @@ -313,28 +359,40 @@ def get(self, instance): return value return getattr(instance, self.propname, self._default) - def set(self, instance, value): + def set(self, instance: object, value: StateValue) -> None: """Get the state of the underline class instance.""" if self._setter: self._setter(instance, value) else: setattr(instance, self.propname, value) - def transition_succeed(self, instance, descriptor, source, target, **kwargs): + def transition_succeed( + self, + instance: object, + descriptor: TransitionBoundMethod, + source: State, + target: State, + **kwargs: Any, + ) -> None: if self._on_success: self._on_success(instance, descriptor, source, target, **kwargs) @property - def propname(self): + def propname(self) -> str: """State storage attribute.""" - return '__fsm{}'.format(id(self)) + return "__fsm{}".format(id(self)) def transition( - self, source=None, target=None, label=None, - conditions=None, permission=None, - ): + self, + source: StateValue, + target: Optional[StateValue] = None, + label: Optional[str] = None, + conditions: Optional[List[Condition]] = None, + permission: Optional[Permission] = None, + ) -> Any: """Transition method decorator.""" - def _wrapper(func): + + def _wrapper(func: Any) -> Any: if isinstance(func, TransitionDescriptor): descriptor = func else: @@ -351,34 +409,39 @@ def _wrapper(func): target=target, label=label, conditions=conditions, - permission=permission + permission=permission, ) descriptor.add_transition(transition) return descriptor + return _wrapper - def super(self): - def _wrapper(func): + def super(self) -> Any: + def _wrapper(func: Any) -> Any: return SuperTransitionDescriptor(self, func) + return _wrapper - def setter(self): - def _wrapper(func): + def setter(self) -> Any: + def _wrapper(func: Any) -> Any: self._setter = func return func + return _wrapper - def getter(self): - def _wrapper(func): + def getter(self) -> Any: + def _wrapper(func: Any) -> Any: self._getter = func return func + return _wrapper - def on_success(self): - def _wrapper(func): + def on_success(self) -> Any: + def _wrapper(func: Any) -> Any: self._on_success = func return func + return _wrapper class CONDITION(object): @@ -389,8 +452,8 @@ def __init__(self, is_true: bool, unmet: str = ""): self.unmet = unmet @property - def message(self): - return self.message if self.unmet else '' + def message(self) -> str: + return self.message if self.unmet else "" - def __bool__(self): + def __bool__(self) -> bool: return self.is_true diff --git a/viewflow/fsm/typing.py b/viewflow/fsm/typing.py index 79c86003..119018eb 100644 --- a/viewflow/fsm/typing.py +++ b/viewflow/fsm/typing.py @@ -2,14 +2,18 @@ # All Rights Reserved. # This work is dual-licensed under AGPL defined in file 'LICENSE' with -# LICENSE_EXCEPTION and the Commercial licence defined in file 'COMM_LICENSE', +# LICENSE_EXCEPTION and the Commercial license defined in file 'COMM_LICENSE', # which is part of this source code package. -from typing import Callable, Any, List, Mapping, TYPE_CHECKING +from typing import Callable, Any, List, Union, Mapping, TYPE_CHECKING +from viewflow.this_object import ThisObject if TYPE_CHECKING: from .base import TransitionMethod, Transition # NOQA +UserModel = Any StateValue = Any -Condition = Callable[[Any], bool] +Condition = Union[ThisObject, Callable[[object, object], bool]] +Permission = Union[ThisObject, Callable[[object, Any], bool]] StateTransitions = Mapping['TransitionMethod', List['Transition']] +TransitionFunction = Callable[..., Any] diff --git a/viewflow/templates/viewflow/base_page.html b/viewflow/templates/viewflow/base_page.html index 83daa921..0284b328 100644 --- a/viewflow/templates/viewflow/base_page.html +++ b/viewflow/templates/viewflow/base_page.html @@ -39,8 +39,8 @@ {% block page-menu-primary %}
{% block page-menu-app %} - {% if app and app.menu_template_name %}{% include app.menu_template_name with app=app %}{% endif %} - {% if site and site.menu_template_name %}{% include site.menu_template_name with site=site user=request.user only %}{% endif %} + {% if app and app.menu_template_name %}{% include app.menu_template_name %}{% endif %} + {% if site and site.menu_template_name %}{% include site.menu_template_name %}{% endif %} {% endblock %}
{% endblock page-menu-primary %} @@ -51,13 +51,13 @@ {% block page-menu-user-actions %} {% url 'profile' as profile_url %} {% if profile_url %} - + account_box {% trans 'Profile' %} {% endif %} {% url 'logout' as logout_url %} {% if logout_url %} - + exit_to_app {% trans 'Log out' %} {% endif %} diff --git a/viewflow/templates/viewflow/includes/app_menu.html b/viewflow/templates/viewflow/includes/app_menu.html index 7284baaa..bcc7f584 100644 --- a/viewflow/templates/viewflow/includes/app_menu.html +++ b/viewflow/templates/viewflow/includes/app_menu.html @@ -4,7 +4,7 @@

{{ app.title }}

{% block viewset_links %}{% for viewset in app.menu_items %} {% reverse viewset 'index' as index_url %}{% if index_url %} - + {% if viewset.icon %}{{ viewset.icon }}{% else %}view_carousel{% endif %}{{ viewset.title }} {% endif %} diff --git a/viewflow/templates/viewflow/includes/site_menu.html b/viewflow/templates/viewflow/includes/site_menu.html index 212c7525..3ac802a9 100644 --- a/viewflow/templates/viewflow/includes/site_menu.html +++ b/viewflow/templates/viewflow/includes/site_menu.html @@ -9,7 +9,7 @@

{% trans 'Applicat {% endif %}{% endwith %}{% endblock %} {% block app_links %}{% for app in site.menu_items %}{% if app|has_perm:user %} {% reverse app 'index' as index_url %} - + {{ app.icon }}{{ app.title }} {% endif %}{% empty %} diff --git a/viewflow/templates/viewflow/includes/snackbar.html b/viewflow/templates/viewflow/includes/snackbar.html index cab3394f..ef8a0f57 100644 --- a/viewflow/templates/viewflow/includes/snackbar.html +++ b/viewflow/templates/viewflow/includes/snackbar.html @@ -1,5 +1,5 @@ - {% block snackbar %}{% if messages %} + {% block snackbar %} - {% endif %}{% endblock snackbar%} + {% endblock snackbar%} diff --git a/viewflow/templates/viewflow/includes/viewflow_css.html b/viewflow/templates/viewflow/includes/viewflow_css.html index a38d6c0d..f067f743 100644 --- a/viewflow/templates/viewflow/includes/viewflow_css.html +++ b/viewflow/templates/viewflow/includes/viewflow_css.html @@ -1,6 +1,6 @@ {% load static %} - + {% if not debug %} diff --git a/viewflow/this_object.py b/viewflow/this_object.py index 0ecbaed3..ce0f0491 100644 --- a/viewflow/this_object.py +++ b/viewflow/this_object.py @@ -6,6 +6,7 @@ # This work is dual-licensed under AGPL defined in file 'LICENSE' with # LICENSE_EXCEPTION and the Commercial licence defined in file 'COMM_LICENSE', # which is part of this source code package. +from typing import Any class ThisMethod(object): @@ -27,7 +28,7 @@ class ThisObject(object): def __init__(self, name): # noqa D102 self.name = name - def resolve(self, instance): + def resolve(self, instance: object) -> Any: # TODO meaningfull exception return getattr(instance, self.name) diff --git a/viewflow/urls/base.py b/viewflow/urls/base.py index e5f36b83..edb8e921 100644 --- a/viewflow/urls/base.py +++ b/viewflow/urls/base.py @@ -150,6 +150,7 @@ class Viewset(BaseViewset, metaclass=ViewsetMeta): Viewset classes could be inherited, extended, and have overridden attributes. """ viewsets = None + turbo_disabled = False def __init__(self, urls=None, **initkwargs): super().__init__()