diff --git a/.gitignore b/.gitignore index ca291c340..546e6a6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.egg-info *.pyc .coverage +coverage/ +coverage.xml .eslintcache .mypy_cache .pytest_cache @@ -46,7 +48,6 @@ tests/cypress/screenshots tests/cypress/videos tests/screenshots tests/unit/utils/markdown/data/output.html -coverage/ # Instance files that should not be checked-in instance/development.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c389cbabf..0a038c7fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,7 @@ ci: 'no-commit-to-branch', # 'hadolint-docker', 'docker-compose-check', + 'mypy', # Runs as a local hook now ] repos: - repo: https://github.com/pre-commit-ci/pre-commit-ci-config @@ -51,7 +52,7 @@ repos: - id: pyupgrade args: ['--keep-runtime-typing', '--py311-plus'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.1.11 hooks: - id: ruff args: ['--fix', '--exit-non-zero-on-fix'] @@ -100,28 +101,8 @@ repos: rev: 23.12.1 hooks: - id: black - # Mypy is temporarily disabled until the SQLAlchemy 2.0 migration is complete - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v1.3.0 - # hooks: - # - id: mypy - # # warn-unused-ignores is unsafe with pre-commit, see - # # https://github.com/python/mypy/issues/2960 - # args: ['--no-warn-unused-ignores', '--ignore-missing-imports'] - # additional_dependencies: - # - flask - # - lxml-stubs - # - sqlalchemy - # - toml - # - tomli - # - types-chevron - # - types-geoip2 - # - types-python-dateutil - # - types-pytz - # - types-requests - # - typing-extensions - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: *flake8deps @@ -149,23 +130,12 @@ repos: rev: v3.0.0 hooks: - id: creosote + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.9.0.6 + hooks: + - id: shellcheck args: - - --venv=.venv - - --path=funnel - - --path=tests - - --path=migrations/versions - - --deps-file=requirements/base.in - - --exclude-dep=argon2-cffi # Optional dep for passlib - - --exclude-dep=bcrypt # Optional dep for passlib - - --exclude-dep=greenlet # Optional dep for SQLAlchemy's asyncio support - - --exclude-dep=gunicorn # Not imported, used as server - - --exclude-dep=linkify-it-py # Optional dep for markdown-it-py - - --exclude-dep=psycopg # Optional dep for SQLAlchemy - - --exclude-dep=rq-dashboard # Creosote fails to recognise the import - - --exclude-dep=tzdata # Data-only dep, therefore no import statement - - --exclude-dep=urllib3 # Required to silence a pip-audit warning - - --exclude-dep=wtforms-sqlalchemy # Temp dep on an unreleased git branch - + - --external-sources - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: @@ -231,3 +201,16 @@ repos: rev: v3.0.1 hooks: - id: docker-compose-check + - repo: local + hooks: + - id: mypy + name: mypy + entry: .venv/bin/mypy + args: + - --no-warn-unused-ignores + - --no-warn-redundant-casts + - . # Required to honour settings in pyproject.toml + language: system + types_or: [python, pyi] + require_serial: true + pass_filenames: false # Mypy is unreliable if module import order varies diff --git a/.testenv b/.testenv index f60567fe7..12a9accf4 100644 --- a/.testenv +++ b/.testenv @@ -21,7 +21,7 @@ FLASK_LOG_TELEGRAM_APIKEY=null FLASK_LOG_SLACK_WEBHOOKS='[]' # Run RQ jobs inline in tests FLASK_RQ_ASYNC=false -# Recaptcha keys from https://developers.google.com/recaptcha/docfaq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do +# Recaptcha keys from https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do FLASK_RECAPTCHA_USE_SSL=true FLASK_RECAPTCHA_PUBLIC_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI FLASK_RECAPTCHA_PRIVATE_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe diff --git a/docker/entrypoints/ci-test.sh b/docker/entrypoints/ci-test.sh index 4da374e54..ad996e1fa 100755 --- a/docker/entrypoints/ci-test.sh +++ b/docker/entrypoints/ci-test.sh @@ -1,5 +1,5 @@ #!/bin/sh make install-test -pytest --allow-hosts=127.0.0.1,::1,$(hostname -i),$(getent ahosts db-test | awk '/STREAM/ { print $1}'),$(getent ahosts redis-test | awk '/STREAM/ { print $1}') --gherkin-terminal-reporter -vv --showlocals --cov=funnel +pytest "--allow-hosts=127.0.0.1,::1,$(hostname -i),$(getent ahosts db-test | awk '/STREAM/ { print $1}'),$(getent ahosts redis-test | awk '/STREAM/ { print $1}')" --gherkin-terminal-reporter -vv --showlocals --cov=funnel coverage lcov -o coverage/funnel.lcov diff --git a/docker/entrypoints/dev.sh b/docker/entrypoints/dev.sh index 9135c8ff1..a1930f1ef 100755 --- a/docker/entrypoints/dev.sh +++ b/docker/entrypoints/dev.sh @@ -1,6 +1,6 @@ #!/bin/bash -if [ "$(psql -XtA -U postgres -h $DB_HOST funnel -c "select count(*) from information_schema.tables where table_schema = 'public';")" = "0" ]; then +if [ "$(psql -XtA -U postgres -h "$DB_HOST" funnel -c "select count(*) from information_schema.tables where table_schema = 'public';")" = "0" ]; then flask dbcreate flask db stamp fi diff --git a/funnel/__init__.py b/funnel/__init__.py index 210bd508e..534228ceb 100644 --- a/funnel/__init__.py +++ b/funnel/__init__.py @@ -89,7 +89,7 @@ # present, it is loaded in debug and testing modes only for each_app in all_apps: coaster.app.init_app( - each_app, ['py', 'env'], env_prefix=['FLASK', f'APP_{each_app.name.upper()}'] + each_app, ['env'], env_prefix=['FLASK', f'APP_{each_app.name.upper()}'] ) # Force specific config settings, overriding deployment config @@ -147,7 +147,7 @@ baseframe.init_app( app, requires=['funnel'], - theme='funnel', # type: ignore[arg-type] + theme='funnel', # type: ignore[arg-type] # FIXME error_handlers=False, ) @@ -162,7 +162,7 @@ transports.init() # Register JS and CSS assets on both apps -app.assets.register( # type: ignore[attr-defined] +app.assets.register( # type: ignore[attr-defined] # FIXME 'js_fullcalendar', Bundle( assets.require( @@ -178,7 +178,7 @@ filters='rjsmin', ), ) -app.assets.register( # type: ignore[attr-defined] +app.assets.register( # type: ignore[attr-defined] # FIXME 'css_fullcalendar', Bundle( assets.require('jquery.fullcalendar.css', 'spectrum.css'), @@ -186,7 +186,7 @@ filters='cssmin', ), ) -app.assets.register( # type: ignore[attr-defined] +app.assets.register( # type: ignore[attr-defined] # FIXME 'js_schedules', Bundle( assets.require('schedules.js'), @@ -199,12 +199,9 @@ # --- Serve static files with WhiteNoise ----------------------------------------------- -app.wsgi_app = WhiteNoise( # type: ignore[method-assign] - app.wsgi_app, root=app.static_folder, prefix=app.static_url_path -) -app.wsgi_app.add_files( # type: ignore[attr-defined] - baseframe.static_folder, prefix=baseframe.static_url_path -) +_wn = WhiteNoise(app.wsgi_app, root=app.static_folder, prefix=app.static_url_path) +_wn.add_files(baseframe.static_folder, prefix=baseframe.static_url_path) +app.wsgi_app = _wn # type: ignore[method-assign] # --- Init SQLAlchemy mappers ---------------------------------------------------------- diff --git a/funnel/auth.py b/funnel/auth.py new file mode 100644 index 000000000..d43f2e3d9 --- /dev/null +++ b/funnel/auth.py @@ -0,0 +1,41 @@ +"""Auth proxy.""" + +from coaster.auth import ( + CurrentAuth as CurrentAuthBase, + GetCurrentAuth, + add_auth_anchor, + add_auth_attribute, + request_has_auth, +) + +from . import all_apps +from .models import User + +__all__ = [ + 'CurrentAuth', + 'add_auth_attribute', + 'add_auth_anchor', + 'current_auth', + 'request_has_auth', +] + + +class CurrentAuth(CurrentAuthBase): + """CurrentAuth for Funnel.""" + + # These attrs are typed as not-optional because they're typically accessed in a view + # that is already gated with the `@requires_login` or related decorator, so they're + # guaranteed to be present within the view. However, this will require a type-ignore + # for any code that tests `if current_auth.actor`, so those will need a rewrite to + # `if current_auth`. When auth clients become supported actors, this may need some + # form of PEP 647 typeguard to identify the actor's exact type. + + user: User + actor: User + + +current_auth = GetCurrentAuth.proxy(CurrentAuth) + +# Install this proxy in all apps, overriding the proxy provided by coaster.app.init_app +for _app in all_apps: + _app.jinja_env.globals['current_auth'] = current_auth diff --git a/funnel/cli/geodata.py b/funnel/cli/geodata.py index 978f03850..839d29b1b 100644 --- a/funnel/cli/geodata.py +++ b/funnel/cli/geodata.py @@ -167,7 +167,7 @@ def load_country_info(filename: str) -> None: GeoCountryInfo.query.all() # Load everything into session cache for item in countryinfo: if item.geonameid: - ci = GeoCountryInfo.query.get(int(item.geonameid)) + ci = db.session.get(GeoCountryInfo, int(item.geonameid)) if ci is None: ci = GeoCountryInfo(geonameid=int(item.geonameid)) db.session.add(ci) @@ -276,7 +276,7 @@ def load_geonames(filename: str) -> None: for item in rich.progress.track(geonames): if item.geonameid: - gn = GeoName.query.get(int(item.geonameid)) + gn = db.session.get(GeoName, int(item.geonameid)) if gn is None: gn = GeoName(geonameid=int(item.geonameid)) db.session.add(gn) @@ -335,7 +335,7 @@ def load_alt_names(filename: str) -> None: for item in rich.progress.track(altnames): if item.geonameid: - rec = GeoAltName.query.get(int(item.id)) + rec = db.session.get(GeoAltName, int(item.id)) if rec is None: rec = GeoAltName(id=int(item.id)) db.session.add(rec) @@ -368,7 +368,7 @@ def load_admin1_codes(filename: str) -> None: GeoAdmin1Code.query.all() # Load all data into session cache for faster lookup for item in rich.progress.track(admincodes): if item.geonameid: - rec = GeoAdmin1Code.query.get(item.geonameid) + rec = db.session.get(GeoAdmin1Code, item.geonameid) if rec is None: rec = GeoAdmin1Code(geonameid=int(item.geonameid)) db.session.add(rec) @@ -397,7 +397,7 @@ def load_admin2_codes(filename: str) -> None: GeoAdmin2Code.query.all() # Load all data into session cache for faster lookup for item in rich.progress.track(admincodes): if item.geonameid: - rec = GeoAdmin2Code.query.get(item.geonameid) + rec = db.session.get(GeoAdmin2Code, item.geonameid) if rec is None: rec = GeoAdmin2Code(geonameid=int(item.geonameid)) db.session.add(rec) diff --git a/funnel/cli/refresh/markdown.py b/funnel/cli/refresh/markdown.py index 2fa846732..544883b5e 100644 --- a/funnel/cli/refresh/markdown.py +++ b/funnel/cli/refresh/markdown.py @@ -9,16 +9,19 @@ import rich.progress from ... import models -from ...models import MarkdownModelUnion, db, sa_orm +from ...models import db, sa_orm from . import refresh -_M = TypeVar('_M', bound=MarkdownModelUnion) +_M = TypeVar('_M', bound=models.ModelIdProtocol) class MarkdownModel(Generic[_M]): """Holding class for a model that has markdown fields with custom configuration.""" + #: Dict of ``{MarkdownModel().name: MarkdownModel()}`` registry: ClassVar[dict[str, MarkdownModel]] = {} + #: Dict of ``{config_name: MarkdownModel()}``, where the fields on the model using + #: that config are enumerated in :attr:`config_fields` config_registry: ClassVar[dict[str, set[MarkdownModel]]] = {} def __init__(self, model: type[_M], fields: set[str]) -> None: @@ -54,12 +57,12 @@ def reparse(self, config: str | None = None, obj: _M | None = None) -> None: iter_total = 1 else: load_columns = ( - [self.model.id] + [self.model.id_] + [getattr(self.model, f'{field}_text'.lstrip('_')) for field in fields] + [getattr(self.model, f'{field}_html'.lstrip('_')) for field in fields] ) iter_list = ( - self.model.query.order_by(self.model.id) + self.model.query.order_by(self.model.id_) .options(sa_orm.load_only(*load_columns)) .yield_per(10) ) diff --git a/funnel/devtest.py b/funnel/devtest.py index e3bfea795..6c5117eb4 100644 --- a/funnel/devtest.py +++ b/funnel/devtest.py @@ -18,7 +18,7 @@ from flask import Flask -from . import app as main_app, shortlinkapp, transports, unsubscribeapp +from . import all_apps, app as main_app, transports from .models import db from .typing import ReturnView @@ -100,7 +100,7 @@ def __call__(self, environ: Any, start_response: Any) -> Iterable[bytes]: return use_app(environ, start_response) -devtest_app = AppByHostWsgi(main_app, shortlinkapp, unsubscribeapp) +devtest_app = AppByHostWsgi(*all_apps) # --- Background worker ---------------------------------------------------------------- @@ -143,7 +143,7 @@ def _signature_without_annotations(func) -> inspect.Signature: ) -def install_mock(func: Callable, mock: Callable) -> None: +def install_mock(func: Any, mock: Any) -> None: """ Patch all existing references to :attr:`func` with :attr:`mock`. @@ -161,9 +161,9 @@ def install_mock(func: Callable, mock: Callable) -> None: # Use weakref to dereference func from local namespace func = weakref.ref(func) gc.collect() - refs = gc.get_referrers(func()) # type: ignore[misc] # Typeshed says not callable + refs = gc.get_referrers(func()) # Recover func from the weakref so we can do an `is` match in referrers - func = func() # type: ignore[misc] + func = func() for ref in refs: if isinstance(ref, dict): # We have a namespace dict. Iterate through contents to find the reference @@ -312,7 +312,7 @@ def start(self) -> None: raise RuntimeError(f"Server exited with code {self._process.exitcode}") def _is_ready(self) -> bool: - """Probe for readyness with a socket connection.""" + """Probe for readiness with a socket connection.""" if not self.probe_at: return False sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/funnel/forms/account.py b/funnel/forms/account.py index 1182ffd3e..a749863b4 100644 --- a/funnel/forms/account.py +++ b/funnel/forms/account.py @@ -197,7 +197,7 @@ class PasswordPolicyForm(forms.Form): ) def validate_password(self, field: forms.Field) -> None: - """Test password strength and save resuls (no errors raised).""" + """Test password strength and save results (no errors raised).""" user_inputs = [] if self.edit_user: @@ -283,6 +283,7 @@ class PasswordResetForm(forms.Form): __returns__ = ('password_strength',) password_strength: int | None = None + edit_user: User # TODO: This form has been deprecated with OTP-based reset as that doesn't need # username and now uses :class:`PasswordCreateForm`. This form is retained in the @@ -502,7 +503,7 @@ class EnableNotificationsDescriptionProtoMixin: enable_notifications: forms.Field - def set_queries(self) -> None: + def __post_init__(self) -> None: """Change the description to include a link.""" self.enable_notifications.description = Markup( _( @@ -614,7 +615,7 @@ class ModeratorReportForm(forms.Form): __("Report type"), coerce=int, validators=[forms.validators.InputRequired()] ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.report_type.choices = [ (idx, report_type.title) diff --git a/funnel/forms/helpers.py b/funnel/forms/helpers.py index e40ea4883..d789aafb0 100644 --- a/funnel/forms/helpers.py +++ b/funnel/forms/helpers.py @@ -9,9 +9,9 @@ from flask import flash from baseframe import _, __, forms -from coaster.auth import current_auth from .. import app +from ..auth import current_auth from ..models import ( Account, AccountEmailClaim, @@ -40,7 +40,7 @@ class AccountSelectField(forms.AutocompleteField): """Render an autocomplete field for selecting an account.""" - data: Account | None # type: ignore[assignment] + data: Account | None # type: ignore[assignment] # FIXME widget = forms.Select2Widget() multiple = False widget_autocomplete = True @@ -231,9 +231,7 @@ def image_url_validator() -> forms.validators.ValidUrl: """Customise ValidUrl for hosted image URL validation.""" return forms.validators.ValidUrl( allowed_schemes=lambda: app.config.get('IMAGE_URL_SCHEMES', ('https',)), - allowed_domains=lambda: app.config.get( # type: ignore[arg-type, return-value] - 'IMAGE_URL_DOMAINS' - ), + allowed_domains=lambda: app.config.get('IMAGE_URL_DOMAINS'), message_schemes=__("A https:// URL is required"), message_domains=__("Images must be hosted at images.hasgeek.com"), ) diff --git a/funnel/forms/login.py b/funnel/forms/login.py index 9df9f7e73..acbd76e2b 100644 --- a/funnel/forms/login.py +++ b/funnel/forms/login.py @@ -297,8 +297,7 @@ def validate_otp(self, field: forms.Field) -> None: class EmailOtpForm(OtpForm): """Verify an OTP sent to email.""" - def set_queries(self) -> None: - super().set_queries() + def __post_init__(self) -> None: self.otp.description = _("One-time password sent to your email address") diff --git a/funnel/forms/notification.py b/funnel/forms/notification.py index f0609355d..3e34d6a39 100644 --- a/funnel/forms/notification.py +++ b/funnel/forms/notification.py @@ -143,7 +143,7 @@ class UnsubscribeForm(forms.Form): __("Unsubscribe token type"), validators=[forms.validators.DataRequired()] ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" # Populate choices with all notification types that the user has a preference # row for. @@ -188,7 +188,7 @@ def set_types(self, obj) -> None: # self.types.data will only contain the enabled preferences. Therefore, iterate # through all choices and toggle true or false based on whether it's in the # enabled list. This uses dict access instead of .get because the rows are known - # to exist (set_queries loaded from this source). + # to exist (`__post_init__` loaded from this source). for ntype, _title in self.types.choices: obj.notification_preferences[ntype].set_transport( self.transport, ntype in self.types.data @@ -205,7 +205,7 @@ class SetNotificationPreferenceForm(forms.Form): ) enabled = forms.BooleanField(__("Enable this transport")) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" # The main switch is special-cased with an empty string for notification type self.notification_type.choices = [('', __("Main switch"))] + [ diff --git a/funnel/forms/profile.py b/funnel/forms/profile.py index 046c656cd..dc0815141 100644 --- a/funnel/forms/profile.py +++ b/funnel/forms/profile.py @@ -58,7 +58,7 @@ class ProfileForm(OrganizationForm): filters=nullable_strip_filters, ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.logo_url.profile = self.account.name or self.account.buid @@ -89,7 +89,7 @@ class ProfileTransitionForm(forms.Form): __("Account visibility"), validators=[forms.validators.DataRequired()] ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.transition.choices = list( self.edit_obj.profile_state.transitions().items() @@ -117,7 +117,7 @@ class ProfileLogoForm(forms.Form): filters=nullable_strip_filters, ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.logo_url.widget_type = 'modal' self.logo_url.profile = self.account.name or self.account.buid @@ -144,7 +144,7 @@ class ProfileBannerForm(forms.Form): filters=nullable_strip_filters, ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.banner_image_url.widget_type = 'modal' self.banner_image_url.profile = self.account.name or self.account.buid diff --git a/funnel/forms/project.py b/funnel/forms/project.py index 01879d6ac..51af407be 100644 --- a/funnel/forms/project.py +++ b/funnel/forms/project.py @@ -126,7 +126,7 @@ def validate_location(self, field: forms.Field) -> None: __("Quotes are not necessary in the location name") ) - def set_queries(self) -> None: + def __post_init__(self) -> None: self.bg_image.profile = self.account.name or self.account.buid if self.edit_obj is not None and self.edit_obj.schedule_start_at: # Don't allow user to directly manipulate timestamps when it's done via @@ -186,7 +186,7 @@ class ProjectNameForm(forms.Form): """Form to change the URL name of a project.""" # TODO: Add validators for `account` and unique name here instead of delegating to - # the view. Also add `set_queries` method to change ``name.prefix`` + # the view. Also add `__post_init__` method to change ``name.prefix`` name = forms.AnnotatedTextField( __("Custom URL"), @@ -236,7 +236,7 @@ class ProjectBannerForm(forms.Form): filters=nullable_strip_filters, ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.bg_image.widget_type = 'modal' self.bg_image.profile = self.account.name or self.account.buid @@ -280,7 +280,7 @@ class ProjectTransitionForm(forms.Form): __("Status"), validators=[forms.validators.DataRequired()] ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.transition.choices = list(self.edit_obj.state.transitions().items()) @@ -349,7 +349,7 @@ class RsvpTransitionForm(forms.Form): __("Status"), validators=[forms.validators.DataRequired()] ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" # Usually you need to use an instance's state.transitions to find # all the valid transitions for the current state of the instance. diff --git a/funnel/forms/proposal.py b/funnel/forms/proposal.py index a7e677ff5..deb243220 100644 --- a/funnel/forms/proposal.py +++ b/funnel/forms/proposal.py @@ -125,7 +125,7 @@ class ProposalLabelsForm(forms.Form): edit_parent: Project formlabels = forms.FormField(forms.Form, __("Labels")) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.formlabels.form = proposal_label_form( project=self.edit_parent, proposal=self.edit_obj @@ -139,7 +139,7 @@ class ProposalLabelsAdminForm(forms.Form): edit_parent: Project formlabels = forms.FormField(forms.Form, __("Labels")) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.formlabels.form = proposal_label_admin_form( project=self.edit_parent, proposal=self.edit_obj @@ -175,7 +175,7 @@ class ProposalForm(forms.Form): ) formlabels = forms.FormField(forms.Form, __("Labels")) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" label_form = proposal_label_form( project=self.edit_parent, proposal=self.edit_obj @@ -229,7 +229,7 @@ class ProposalTransitionForm(forms.Form): __("Status"), validators=[forms.validators.DataRequired()] ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" # value: transition method name # label: transition object itself @@ -251,6 +251,6 @@ class ProposalMoveForm(forms.Form): get_label='title', ) - def set_queries(self) -> None: + def __post_init__(self) -> None: """Prepare form for use.""" self.target.query = self.user.projects_as_editor diff --git a/funnel/forms/sync_ticket.py b/funnel/forms/sync_ticket.py index 4a7445ffa..b00a88ba8 100644 --- a/funnel/forms/sync_ticket.py +++ b/funnel/forms/sync_ticket.py @@ -9,10 +9,10 @@ from baseframe import __, forms from ..models import ( - PROJECT_RSVP_STATE, Account, AccountEmail, Project, + ProjectRsvpStateEnum, TicketClient, TicketEvent, TicketParticipant, @@ -71,8 +71,8 @@ class ProjectBoxofficeForm(forms.Form): ) rsvp_state = forms.RadioField( __("Registrations"), - choices=PROJECT_RSVP_STATE.items(), - default=PROJECT_RSVP_STATE.NONE, + choices=[(int(member.value), member.title) for member in ProjectRsvpStateEnum], + default=int(ProjectRsvpStateEnum.NONE), ) is_subscription = forms.BooleanField( __("Paid tickets are for a subscription"), @@ -94,7 +94,7 @@ class ProjectBoxofficeForm(forms.Form): validators=[forms.validators.Optional(), validate_and_convert_json], ) - def set_queries(self): + def __post_init__(self): """Set form schema description.""" self.register_form_schema.description = Markup( '
{description}
{schema}
'
@@ -214,7 +214,7 @@ class TicketParticipantForm(forms.Form):
validators=[forms.validators.DataRequired("Select at least one event")],
)
- def set_queries(self) -> None:
+ def __post_init__(self) -> None:
"""Prepare form for use."""
self.ticket_events.query = self.edit_parent.ticket_events
diff --git a/funnel/forms/venue.py b/funnel/forms/venue.py
index b43cc44ff..8df27ce76 100644
--- a/funnel/forms/venue.py
+++ b/funnel/forms/venue.py
@@ -68,7 +68,7 @@ class VenueForm(forms.Form):
validators=[forms.validators.Optional(), forms.validators.ValidCoordinates()],
)
- def set_queries(self) -> None:
+ def __post_init__(self) -> None:
"""Prepare form for use."""
pycountry_locale = gettext.translation(
'iso3166-2', pycountry.LOCALES_DIR, languages=[str(get_locale()), 'en']
@@ -124,6 +124,6 @@ class VenuePrimaryForm(forms.Form):
render_kw={'autocorrect': 'off', 'autocapitalize': 'off'},
)
- def set_queries(self) -> None:
+ def __post_init__(self) -> None:
"""Prepare form for use."""
self.venue.query = self.edit_parent.venues
diff --git a/funnel/models/__init__.py b/funnel/models/__init__.py
index 7392d5834..127f39e5a 100644
--- a/funnel/models/__init__.py
+++ b/funnel/models/__init__.py
@@ -10,6 +10,7 @@
import sqlalchemy.exc as sa_exc
import sqlalchemy.orm as sa_orm
from flask_sqlalchemy import SQLAlchemy
+from sqlalchemy import Table
from sqlalchemy.dialects import postgresql
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase, Mapped, declarative_mixin, declared_attr
@@ -42,14 +43,14 @@
class Model(ModelBase, DeclarativeBase):
"""Base for all models."""
- __table__: ClassVar[sa.Table]
+ __table__: ClassVar[Table]
__with_timezone__ = True
class GeonameModel(ModelBase, DeclarativeBase):
"""Base for geoname models."""
- __table__: ClassVar[sa.Table]
+ __table__: ClassVar[Table]
__bind_key__ = 'geoname'
__with_timezone__ = True
@@ -57,7 +58,10 @@ class GeonameModel(ModelBase, DeclarativeBase):
# This must be set _before_ any of the models using db.Model are imported
TimestampMixin.__with_timezone__ = True
-db: SQLAlchemy = SQLAlchemy(query_class=Query, metadata=Model.metadata) # type: ignore[arg-type]
+db: SQLAlchemy = SQLAlchemy(
+ query_class=Query, # type: ignore[arg-type]
+ metadata=Model.metadata,
+)
Model.init_flask_sqlalchemy(db)
GeonameModel.init_flask_sqlalchemy(db)
diff --git a/funnel/models/account.py b/funnel/models/account.py
index 5cf9f93f1..927d2c2e5 100644
--- a/funnel/models/account.py
+++ b/funnel/models/account.py
@@ -8,7 +8,7 @@
import itertools
from collections.abc import Iterable, Iterator, Sequence
from datetime import datetime
-from typing import TYPE_CHECKING, ClassVar, Literal, Self, cast, overload
+from typing import TYPE_CHECKING, ClassVar, Literal, Self, TypeAlias, cast, overload
from uuid import UUID
import phonenumbers
@@ -158,7 +158,7 @@ def __eq__(self, other: object) -> sa.ColumnElement[bool]: # type: ignore[overr
# --- Models ---------------------------------------------------------------------------
-class Account(UuidMixin, BaseMixin, Model):
+class Account(UuidMixin, BaseMixin[int, 'Account'], Model):
"""Account model."""
__tablename__ = 'account'
@@ -203,10 +203,10 @@ class Account(UuidMixin, BaseMixin, Model):
#: Alias title as user's fullname
fullname: Mapped[str] = sa_orm.synonym('title')
#: Alias name as user's username
- username: Mapped[str] = sa_orm.synonym('name')
+ username: Mapped[str | None] = sa_orm.synonym('name')
#: Argon2 or Bcrypt hash of the user's password
- pw_hash: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode, nullable=True)
+ pw_hash: Mapped[str | None] = sa_orm.mapped_column()
#: Timestamp for when the user's password last changed
pw_set_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
@@ -221,22 +221,18 @@ class Account(UuidMixin, BaseMixin, Model):
read={'owner'},
)
#: Update timezone automatically from browser activity
- auto_timezone: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=True, nullable=False
- )
+ auto_timezone: Mapped[bool] = sa_orm.mapped_column(default=True)
#: User's preferred/last known locale
locale: Mapped[Locale | None] = with_roles(
sa_orm.mapped_column(LocaleType, nullable=True), read={'owner'}
)
#: Update locale automatically from browser activity
- auto_locale: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=True, nullable=False
- )
+ auto_locale: Mapped[bool] = sa_orm.mapped_column(default=True)
#: User's state code (active, suspended, merged, deleted)
_state: Mapped[int] = sa_orm.mapped_column(
'state',
sa.SmallInteger,
- StateManager.check_constraint('state', ACCOUNT_STATE),
+ StateManager.check_constraint('state', ACCOUNT_STATE, sa.SmallInteger),
nullable=False,
default=ACCOUNT_STATE.ACTIVE,
)
@@ -250,7 +246,7 @@ class Account(UuidMixin, BaseMixin, Model):
_profile_state: Mapped[int] = sa_orm.mapped_column(
'profile_state',
sa.SmallInteger,
- StateManager.check_constraint('profile_state', PROFILE_STATE),
+ StateManager.check_constraint('profile_state', PROFILE_STATE, sa.SmallInteger),
nullable=False,
default=PROFILE_STATE.AUTO,
)
@@ -259,7 +255,7 @@ class Account(UuidMixin, BaseMixin, Model):
)
tagline: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, sa.CheckConstraint("tagline <> ''"), nullable=True
+ sa.CheckConstraint("tagline <> ''")
)
description, description_text, description_html = MarkdownCompositeDocument.create(
'description', default='', nullable=False
@@ -279,20 +275,18 @@ class Account(UuidMixin, BaseMixin, Model):
#: Protected accounts cannot be deleted
is_protected: Mapped[bool] = with_roles(
- immutable(sa_orm.mapped_column(sa.Boolean, default=False, nullable=False)),
+ immutable(sa_orm.mapped_column(default=False)),
read={'owner', 'admin'},
)
#: Verified accounts get listed on the home page and are not considered throwaway
#: accounts for spam control. There are no other privileges at this time
is_verified: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, default=False, nullable=False, index=True),
+ sa_orm.mapped_column(default=False, index=True),
read={'all'},
)
#: Revision number maintained by SQLAlchemy, starting at 1
- revisionid: Mapped[int] = with_roles(
- sa_orm.mapped_column(sa.Integer, nullable=False), read={'all'}
- )
+ revisionid: Mapped[int] = with_roles(sa_orm.mapped_column(), read={'all'})
search_vector: Mapped[str] = sa_orm.mapped_column(
TSVectorType(
@@ -986,6 +980,7 @@ def ticket_followers(self) -> Query[Account]:
'is_verified',
},
}
+ __json_datasets__ = ('primary', 'related')
profile_state.add_conditional_state(
'ACTIVE_AND_PUBLIC',
@@ -1080,9 +1075,7 @@ def _set_password(self, password: str | None):
# Also see :meth:`password_is` for transparent upgrade
self.pw_set_at = sa.func.utcnow()
# Expire passwords after one year. TODO: make this configurable
- self.pw_expires_at = self.pw_set_at + sa.cast( # type: ignore[assignment]
- '1 year', sa.Interval
- )
+ self.pw_expires_at = sa.func.utcnow() + sa.cast('1 year', sa.Interval)
#: Write-only property (passwords cannot be read back in plain text)
password = property(fset=_set_password, doc=_set_password.__doc__)
@@ -1119,14 +1112,11 @@ def add_email(
) -> AccountEmail:
"""Add an email address (assumed to be verified)."""
accountemail = AccountEmail(account=self, email=email, private=private)
- accountemail = cast(
- AccountEmail,
- failsafe_add(
- db.session,
- accountemail,
- account=self,
- email_address=accountemail.email_address,
- ),
+ accountemail = failsafe_add(
+ db.session,
+ accountemail,
+ account=self,
+ email_address=accountemail.email_address,
)
if primary:
self.primary_email = accountemail
@@ -1176,14 +1166,11 @@ def add_phone(
) -> AccountPhone:
"""Add a phone number (assumed to be verified)."""
accountphone = AccountPhone(account=self, phone=phone, private=private)
- accountphone = cast(
- AccountPhone,
- failsafe_add(
- db.session,
- accountphone,
- account=self,
- phone_number=accountphone.phone_number,
- ),
+ accountphone = failsafe_add(
+ db.session,
+ accountphone,
+ account=self,
+ phone_number=accountphone.phone_number,
)
if primary:
self.primary_phone = accountphone
@@ -1427,18 +1414,24 @@ def do_delete(self):
raise ValueError("Account cannot be deleted")
# 1. Delete contact information
- for contact_source in (
- self.emails,
- self.emailclaims,
- self.phones,
- self.externalids,
+ for contact_source in cast(
+ list,
+ (
+ self.emails,
+ self.emailclaims,
+ self.phones,
+ self.externalids,
+ ),
):
for contact in contact_source:
db.session.delete(contact)
# 2. Revoke all active memberships
for membership in self.active_memberships():
- membership = membership.freeze_member_attribution(self)
+ if callable(
+ freeze := getattr(membership, 'freeze_member_attribution', None)
+ ):
+ membership = freeze(self)
if membership.revoke_on_member_delete:
membership.revoke(actor=self)
# TODO: freeze fullname in unrevoked memberships (pending title column there)
@@ -1508,7 +1501,7 @@ def uuid_zbase32(self) -> str:
@classmethod
def _uuid_zbase32_comparator(cls) -> ZBase32Comparator:
"""Return SQL comparator for :prop:`uuid_zbase32`."""
- return ZBase32Comparator(cls.uuid)
+ return ZBase32Comparator(cls.uuid) # type: ignore[arg-type]
@classmethod
def name_is(cls, name: str) -> ColumnElement:
@@ -1889,7 +1882,7 @@ def membership_project(self) -> Project | None:
add_search_trigger(Account, 'name_vector')
-class AccountOldId(UuidMixin, BaseMixin[UUID], Model):
+class AccountOldId(UuidMixin, BaseMixin[UUID, Account], Model):
"""Record of an older UUID for an account, after account merger."""
__tablename__ = 'account_oldid'
@@ -1902,7 +1895,7 @@ class AccountOldId(UuidMixin, BaseMixin[UUID], Model):
)
#: User id of new user
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
#: New account
account: Mapped[Account] = relationship(
@@ -2047,7 +2040,7 @@ class Placeholder(Account):
is_placeholder_profile = True
-class Team(UuidMixin, BaseMixin, Model):
+class Team(UuidMixin, BaseMixin[int, Account], Model):
"""A team of users within an organization."""
__tablename__ = 'team'
@@ -2058,7 +2051,7 @@ class Team(UuidMixin, BaseMixin, Model):
)
#: Organization
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False, index=True
+ sa.ForeignKey('account.id'), default=None, nullable=False, index=True
)
account: Mapped[Account] = with_roles(
relationship(foreign_keys=[account_id], back_populates='teams'),
@@ -2071,9 +2064,7 @@ class Team(UuidMixin, BaseMixin, Model):
grants={'member'},
)
- is_public: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_public: Mapped[bool] = sa_orm.mapped_column(default=False)
# --- Backrefs
client_permissions: Mapped[list[AuthClientTeamPermissions]] = relationship(
@@ -2103,7 +2094,7 @@ def migrate_account(
# `migrate_account` methods as team_membership is an unmapped table.
new_account.member_teams.append(team)
old_account.member_teams.remove(team)
- return [cast(sa.Table, cls.__table__).name, team_membership.name]
+ return [cls.__table__.name, team_membership.name]
@classmethod
def get(cls, buid: str, with_parent: bool = False) -> Team | None:
@@ -2122,7 +2113,7 @@ def get(cls, buid: str, with_parent: bool = False) -> Team | None:
# --- Account email/phone and misc
-class AccountEmail(EmailAddressMixin, BaseMixin, Model):
+class AccountEmail(EmailAddressMixin, BaseMixin[int, Account], Model):
"""An email address linked to an account."""
__tablename__ = 'account_email'
@@ -2130,18 +2121,13 @@ class AccountEmail(EmailAddressMixin, BaseMixin, Model):
__email_is_exclusive__ = True
__email_for__ = 'account'
- # Tell mypy that these are not optional
- email_address: Mapped[EmailAddress] # type: ignore[assignment]
-
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = relationship(back_populates='emails')
user: Mapped[Account] = sa_orm.synonym('account')
- private: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ private: Mapped[bool] = sa_orm.mapped_column(default=False)
__datasets__ = {
'primary': {'member', 'email', 'private', 'type'},
@@ -2297,10 +2283,10 @@ def migrate_account(
if new_account.primary_email is None:
new_account.primary_email = primary_email
old_account.primary_email = None
- return [cast(sa.Table, cls.__table__).name, user_email_primary_table.name]
+ return [cls.__table__.name, user_email_primary_table.name]
-class AccountEmailClaim(EmailAddressMixin, BaseMixin, Model):
+class AccountEmailClaim(EmailAddressMixin, BaseMixin[int, Account], Model):
"""Claimed but unverified email address for a user."""
__tablename__ = 'account_email_claim'
@@ -2308,21 +2294,16 @@ class AccountEmailClaim(EmailAddressMixin, BaseMixin, Model):
__email_for__ = 'account'
__email_is_exclusive__ = False
- # Tell mypy that these are not optional
- email_address: Mapped[EmailAddress] # type: ignore[assignment]
-
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = relationship(back_populates='emailclaims')
user: Mapped[Account] = sa_orm.synonym('account')
verification_code: Mapped[str] = sa_orm.mapped_column(
- sa.String(44), nullable=False, default=newsecret
+ sa.String(44), nullable=False, insert_default=newsecret, default=None
)
- private: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ private: Mapped[bool] = sa_orm.mapped_column(default=False)
__table_args__ = (sa.UniqueConstraint('account_id', 'email_address_id'),)
@@ -2489,7 +2470,7 @@ def all(cls, email: str) -> Query[Self]: # noqa: A003
auto_init_default(AccountEmailClaim.verification_code)
-class AccountPhone(PhoneNumberMixin, BaseMixin, Model):
+class AccountPhone(PhoneNumberMixin, BaseMixin[int, Account], Model):
"""A phone number linked to an account."""
__tablename__ = 'account_phone'
@@ -2498,14 +2479,12 @@ class AccountPhone(PhoneNumberMixin, BaseMixin, Model):
__phone_for__ = 'account'
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None
)
account: Mapped[Account] = relationship(back_populates='phones')
user: Mapped[Account] = sa_orm.synonym('account')
- private: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ private: Mapped[bool] = sa_orm.mapped_column(default=False)
__datasets__ = {
'primary': {'member', 'phone', 'private', 'type'},
@@ -2674,17 +2653,17 @@ def migrate_account(
if new_account.primary_phone is None:
new_account.primary_phone = primary_phone
old_account.primary_phone = None
- return [cast(sa.Table, cls.__table__).name, user_phone_primary_table.name]
+ return [cls.__table__.name, user_phone_primary_table.name]
-class AccountExternalId(BaseMixin, Model):
+class AccountExternalId(BaseMixin[int, Account], Model):
"""An external connected account for a user."""
__tablename__ = 'account_externalid'
__at_username_services__: ClassVar[list[str]] = []
#: Foreign key to user table
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None
)
#: User that this connected account belongs to
account: Mapped[Account] = relationship(back_populates='externalids')
@@ -2698,10 +2677,8 @@ class AccountExternalId(BaseMixin, Model):
sa.UnicodeText, nullable=False
) # Unique id (or obsolete OpenID)
#: Optional public-facing username on the external service
- # FIXME: change to sa.Unicode
- username: Mapped[str | None] = sa_orm.mapped_column(
- sa.UnicodeText, nullable=True
- ) # LinkedIn once used full URLs
+ # FIXME: change to sa.Unicode. LinkedIn once used full URLs
+ username: Mapped[str | None] = sa_orm.mapped_column(sa.UnicodeText, nullable=True)
#: OAuth or OAuth2 access token
# FIXME: change to sa.Unicode
oauth_token: Mapped[str | None] = sa_orm.mapped_column(
@@ -2723,9 +2700,7 @@ class AccountExternalId(BaseMixin, Model):
sa.UnicodeText, nullable=True
)
#: OAuth2 token expiry in seconds, as sent by service provider
- oauth_expires_in: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, nullable=True
- )
+ oauth_expires_in: Mapped[int | None] = sa_orm.mapped_column()
#: OAuth2 token expiry timestamp, estimate from created_at + oauth_expires_in
oauth_expires_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True, index=True
@@ -2733,7 +2708,7 @@ class AccountExternalId(BaseMixin, Model):
#: Timestamp of when this connected account was last (re-)authorised by the user
last_used_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), default=sa.func.utcnow(), nullable=False
+ sa.TIMESTAMP(timezone=True), insert_default=sa.func.utcnow(), default=None
)
__table_args__ = (
@@ -2801,7 +2776,9 @@ def get(
)
#: Anchor type
-Anchor = AccountEmail | AccountEmailClaim | AccountPhone | EmailAddress | PhoneNumber
+Anchor: TypeAlias = (
+ AccountEmail | AccountEmailClaim | AccountPhone | EmailAddress | PhoneNumber
+)
# Tail imports
from .account_membership import AccountMembership
diff --git a/funnel/models/account_membership.py b/funnel/models/account_membership.py
index ec16f669b..d9f91aeda 100644
--- a/funnel/models/account_membership.py
+++ b/funnel/models/account_membership.py
@@ -8,12 +8,12 @@
from . import Mapped, Model, relationship, sa, sa_orm
from .account import Account
-from .membership_mixin import ImmutableUserMembershipMixin
+from .membership_mixin import ImmutableMembershipMixin
__all__ = ['AccountMembership']
-class AccountMembership(ImmutableUserMembershipMixin, Model):
+class AccountMembership(ImmutableMembershipMixin, Model):
"""
An account can be a member of another account as an owner, admin or follower.
@@ -50,7 +50,7 @@ class AccountMembership(ImmutableUserMembershipMixin, Model):
'account_admin': {
'read': {
'record_type',
- 'record_type_label',
+ 'record_type_enum',
'granted_at',
'granted_by',
'revoked_at',
@@ -78,8 +78,8 @@ class AccountMembership(ImmutableUserMembershipMixin, Model):
#: Organization that this membership is being granted on
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('account.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
)
account: Mapped[Account] = with_roles(
@@ -91,9 +91,7 @@ class AccountMembership(ImmutableUserMembershipMixin, Model):
parent: Mapped[Account] = sa_orm.synonym('account')
# Organization roles:
- is_owner: Mapped[bool] = immutable(
- sa_orm.mapped_column(sa.Boolean, nullable=False, default=False)
- )
+ is_owner: Mapped[bool] = immutable(sa_orm.mapped_column(default=False))
@cached_property
def offered_roles(self) -> set[str]:
diff --git a/funnel/models/auth_client.py b/funnel/models/auth_client.py
index 1b07404da..82bb7bf90 100644
--- a/funnel/models/auth_client.py
+++ b/funnel/models/auth_client.py
@@ -84,14 +84,14 @@ def add_scope(self, additional: str | Collection) -> None:
self.scope = set(self.scope).union(set(additional))
-class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
+class AuthClient(ScopeMixin, UuidMixin, BaseMixin[int, Account], Model):
"""OAuth client application."""
__tablename__ = 'auth_client'
__scope_null_allowed__ = True
#: Account that owns this client
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = with_roles(
relationship(back_populates='clients'),
@@ -113,7 +113,7 @@ class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
)
#: Confidential or public client? Public has no secret key
confidential: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False), read={'all'}, write={'owner'}
+ sa_orm.mapped_column(), read={'all'}, write={'owner'}
)
#: Website
website: Mapped[str] = with_roles(
@@ -130,12 +130,10 @@ class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
sa_orm.mapped_column(sa.UnicodeText, nullable=True, default=''), rw={'owner'}
)
#: Active flag
- active: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=True
- )
+ active: Mapped[bool] = sa_orm.mapped_column(default=True)
#: Allow anyone to login to this app?
allow_any_login: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False, default=True),
+ sa_orm.mapped_column(default=True),
read={'all'},
write={'owner'},
)
@@ -146,7 +144,7 @@ class AuthClient(ScopeMixin, UuidMixin, BaseMixin, Model):
#: However, resources in the scope column (via ScopeMixin) are granted for
#: any arbitrary user without explicit user authorization.
trusted: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False, default=False), read={'all'}
+ sa_orm.mapped_column(default=False), read={'all'}
)
# --- Backrefs
@@ -203,8 +201,7 @@ def redirect_uris(self, value: Sequence[str]) -> None:
@property
def redirect_uri(self) -> str | None:
"""Return the first redirect URI, if present."""
- uris = self.redirect_uris # Assign to local var to avoid splitting twice
- if uris:
+ if uris := self.redirect_uris:
return uris[0]
return None
@@ -275,7 +272,7 @@ def all_for(cls, account: Account | None) -> Query[Self]:
).order_by(cls.title)
-class AuthClientCredential(BaseMixin, Model):
+class AuthClientCredential(BaseMixin[int, Account], Model):
"""
AuthClient key and secret hash.
@@ -296,7 +293,7 @@ class AuthClientCredential(BaseMixin, Model):
__tablename__ = 'auth_client_credential'
auth_client_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False
+ sa.ForeignKey('auth_client.id'), default=None, nullable=False
)
auth_client: Mapped[AuthClient] = with_roles(
relationship(back_populates='credentials'),
@@ -305,14 +302,18 @@ class AuthClientCredential(BaseMixin, Model):
#: OAuth client key
name: Mapped[str] = sa_orm.mapped_column(
- sa.String(22), nullable=False, unique=True, default=make_buid
+ sa.String(22),
+ insert_default=make_buid,
+ default=None,
+ nullable=False,
+ unique=True,
)
#: User description for this credential
title: Mapped[str] = sa_orm.mapped_column(
sa.Unicode(250), nullable=False, default=''
)
#: OAuth client secret, hashed
- secret_hash: Mapped[str] = sa_orm.mapped_column(sa.Unicode, nullable=False)
+ secret_hash: Mapped[str] = sa_orm.mapped_column()
#: When was this credential last used for an API call?
accessed_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
@@ -371,27 +372,27 @@ def new(cls, auth_client: AuthClient) -> tuple[AuthClientCredential, str]:
return cred, secret
-class AuthCode(ScopeMixin, BaseMixin, Model):
+class AuthCode(ScopeMixin, BaseMixin[int, Account], Model):
"""Short-lived authorization tokens."""
__tablename__ = 'auth_code'
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = relationship(foreign_keys=[account_id])
auth_client_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False
+ sa.ForeignKey('auth_client.id'), default=None, nullable=False
)
auth_client: Mapped[AuthClient] = relationship(back_populates='authcodes')
login_session_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('login_session.id'), nullable=True
+ sa.ForeignKey('login_session.id'), default=None, nullable=True
)
login_session: Mapped[LoginSession | None] = relationship()
code: Mapped[str] = sa_orm.mapped_column(
- sa.String(44), default=newsecret, nullable=False
+ sa.String(44), insert_default=newsecret, default=None, nullable=False
)
redirect_uri: Mapped[str] = sa_orm.mapped_column(sa.UnicodeText, nullable=False)
- used: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, default=False, nullable=False)
+ used: Mapped[bool] = sa_orm.mapped_column(default=False)
def is_valid(self) -> bool:
"""Test if this auth code is still valid."""
@@ -412,19 +413,19 @@ def get_for_client(cls, auth_client: AuthClient, code: str) -> AuthCode | None:
).one_or_none()
-class AuthToken(ScopeMixin, BaseMixin, Model):
+class AuthToken(ScopeMixin, BaseMixin[int, Account], Model):
"""Access tokens for access to data."""
__tablename__ = 'auth_token'
# Account id is null for client-only tokens and public clients as the account is
# identified via login_session.account there
account_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=True
+ sa.ForeignKey('account.id'), default=None, nullable=True
)
account: Mapped[Account | None] = relationship(back_populates='authtokens')
#: The session in which this token was issued, null for confidential clients
login_session_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('login_session.id'), nullable=True
+ sa.ForeignKey('login_session.id'), default=None, nullable=True
)
login_session: Mapped[LoginSession | None] = with_roles(
relationship(back_populates='authtokens'),
@@ -432,7 +433,7 @@ class AuthToken(ScopeMixin, BaseMixin, Model):
)
#: The client this auth token is for
auth_client_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False, index=True
+ sa.ForeignKey('auth_client.id'), default=None, nullable=False, index=True
)
auth_client: Mapped[AuthClient] = with_roles(
relationship(back_populates='authtokens'),
@@ -440,7 +441,11 @@ class AuthToken(ScopeMixin, BaseMixin, Model):
)
#: The token
token: Mapped[str] = sa_orm.mapped_column(
- sa.String(22), default=make_buid, nullable=False, unique=True
+ sa.String(22),
+ insert_default=make_buid,
+ default=None,
+ nullable=False,
+ unique=True,
)
#: The token's type, 'bearer', 'mac' or a URL
token_type: Mapped[str] = sa_orm.mapped_column(
@@ -633,19 +638,19 @@ def all_for(cls, account: Account) -> Query[Self]:
# This model's name is in plural because it defines multiple permissions within each
# instance
-class AuthClientPermissions(BaseMixin, Model):
+class AuthClientPermissions(BaseMixin[int, Account], Model):
"""Permissions assigned to an account on a client app."""
__tablename__ = 'auth_client_permissions'
__tablename__ = 'auth_client_permissions'
#: User account that has these permissions
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = relationship(back_populates='client_permissions')
#: AuthClient app they are assigned on
auth_client_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False, index=True
+ sa.ForeignKey('auth_client.id'), default=None, nullable=False, index=True
)
auth_client: Mapped[AuthClient] = with_roles(
relationship(back_populates='account_permissions'),
@@ -710,18 +715,18 @@ def all_forclient(cls, auth_client: AuthClient) -> Query[Self]:
# This model's name is in plural because it defines multiple permissions within each
# instance
-class AuthClientTeamPermissions(BaseMixin, Model):
+class AuthClientTeamPermissions(BaseMixin[int, Account], Model):
"""Permissions assigned to a team on a client app."""
__tablename__ = 'auth_client_team_permissions'
#: Team which has these permissions
team_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('team.id'), nullable=False
+ sa.ForeignKey('team.id'), default=None, nullable=False
)
team: Mapped[Team] = relationship(back_populates='client_permissions')
#: AuthClient app they are assigned on
auth_client_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('auth_client.id'), nullable=False, index=True
+ sa.ForeignKey('auth_client.id'), default=None, nullable=False, index=True
)
auth_client: Mapped[AuthClient] = with_roles(
relationship(back_populates='team_permissions'),
diff --git a/funnel/models/comment.py b/funnel/models/comment.py
index 389b80df2..25ec784d3 100644
--- a/funnel/models/comment.py
+++ b/funnel/models/comment.py
@@ -81,13 +81,13 @@ class SET_TYPE: # noqa: N801
# --- Models ---------------------------------------------------------------------------
-class Commentset(UuidMixin, BaseMixin, Model):
+class Commentset(UuidMixin, BaseMixin[int, Account], Model):
__tablename__ = 'commentset'
#: Commentset state code
_state: Mapped[int] = sa_orm.mapped_column(
'state',
sa.SmallInteger,
- StateManager.check_constraint('state', COMMENTSET_STATE),
+ StateManager.check_constraint('state', COMMENTSET_STATE, sa.SmallInteger),
nullable=False,
default=COMMENTSET_STATE.OPEN,
)
@@ -97,13 +97,13 @@ class Commentset(UuidMixin, BaseMixin, Model):
)
#: Type of parent object
settype: Mapped[int | None] = with_roles(
- sa_orm.mapped_column('type', sa.Integer, nullable=True),
+ sa_orm.mapped_column('type', nullable=True),
read={'all'},
datasets={'primary'},
)
#: Count of comments, stored to avoid count(*) queries
count: Mapped[int] = with_roles(
- sa_orm.mapped_column(sa.Integer, default=0, nullable=False),
+ sa_orm.mapped_column(default=0, nullable=False),
read={'all'},
datasets={'primary'},
)
@@ -296,18 +296,18 @@ def remove_subscriber(self, actor: Account, member: Account) -> bool:
return False
-class Comment(UuidMixin, BaseMixin, Model):
+class Comment(UuidMixin, BaseMixin[int, Account], Model):
__tablename__ = 'comment'
posted_by_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=True
+ sa.ForeignKey('account.id'), default=None, nullable=True
)
_posted_by: Mapped[Account | None] = with_roles(
relationship(back_populates='comments'),
grants={'author'},
)
commentset_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('commentset.id'), nullable=False
+ sa.ForeignKey('commentset.id'), default=None, nullable=False
)
commentset: Mapped[Commentset] = with_roles(
relationship(back_populates='comments'),
@@ -315,7 +315,7 @@ class Comment(UuidMixin, BaseMixin, Model):
)
in_reply_to_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('comment.id'), nullable=True
+ sa.ForeignKey('comment.id'), default=None, nullable=True
)
in_reply_to: Mapped[Comment] = relationship(
back_populates='replies', remote_side='Comment.id'
@@ -328,8 +328,7 @@ class Comment(UuidMixin, BaseMixin, Model):
_state: Mapped[int] = sa_orm.mapped_column(
'state',
- sa.Integer,
- StateManager.check_constraint('state', COMMENT_STATE),
+ StateManager.check_constraint('state', COMMENT_STATE, sa.Integer),
default=COMMENT_STATE.SUBMITTED,
nullable=False,
)
@@ -344,9 +343,7 @@ class Comment(UuidMixin, BaseMixin, Model):
)
#: Revision number maintained by SQLAlchemy, starting at 1
- revisionid: Mapped[int] = with_roles(
- sa_orm.mapped_column(sa.Integer, nullable=False), read={'all'}
- )
+ revisionid: Mapped[int] = with_roles(sa_orm.mapped_column(), read={'all'})
search_vector: Mapped[str] = sa_orm.mapped_column(
TSVectorType(
@@ -383,6 +380,7 @@ class Comment(UuidMixin, BaseMixin, Model):
'json': {'created_at', 'urls', 'uuid_b58', 'absolute_url'},
'minimal': {'created_at', 'uuid_b58'},
}
+ __json_datasets__ = ('json',)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
@@ -418,7 +416,7 @@ def posted_by(self) -> Account | DuckTypeAccount:
def _posted_by_setter(self, value: Account | None) -> None:
self._posted_by = value
- @posted_by.inplace.expression
+ @posted_by.inplace.expression # type: ignore[arg-type]
@classmethod
def _posted_by_expression(cls) -> sa_orm.InstrumentedAttribute[Account | None]:
"""Return SQL Expression."""
diff --git a/funnel/models/commentset_membership.py b/funnel/models/commentset_membership.py
index 409332f6d..5565fc1ac 100644
--- a/funnel/models/commentset_membership.py
+++ b/funnel/models/commentset_membership.py
@@ -9,15 +9,12 @@
from . import Mapped, Model, Query, relationship, sa, sa_orm
from .account import Account
-from .membership_mixin import ImmutableUserMembershipMixin
-from .project import Project
-from .proposal import Proposal
-from .update import Update
+from .membership_mixin import ImmutableMembershipMixin
__all__ = ['CommentsetMembership']
-class CommentsetMembership(ImmutableUserMembershipMixin, Model):
+class CommentsetMembership(ImmutableMembershipMixin, Model):
"""Membership roles for users who are commentset users and subscribers."""
__tablename__ = 'commentset_membership'
@@ -38,9 +35,7 @@ class CommentsetMembership(ImmutableUserMembershipMixin, Model):
}
commentset_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer,
- sa.ForeignKey('commentset.id', ondelete='CASCADE'),
- nullable=False,
+ sa.ForeignKey('commentset.id', ondelete='CASCADE'), default=None, nullable=False
)
commentset: Mapped[Commentset] = relationship()
@@ -49,9 +44,7 @@ class CommentsetMembership(ImmutableUserMembershipMixin, Model):
parent: Mapped[Commentset] = sa_orm.synonym('commentset')
#: Flag to indicate notifications are muted
- is_muted: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_muted: Mapped[bool] = sa_orm.mapped_column(default=False)
#: When the user visited this commentset last
last_seen_at: Mapped[datetime] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
@@ -104,11 +97,14 @@ def for_user(cls, account: Account) -> Query[Self]:
# Tail imports
from .comment import Comment, Commentset
+from .project import Project
+from .proposal import Proposal
+from .update import Update
CommentsetMembership.new_comment_count = sa_orm.column_property(
sa.select(sa.func.count(Comment.id))
.where(Comment.commentset_id == CommentsetMembership.commentset_id)
- .where(Comment.state.PUBLIC)
+ .where(Comment.state.PUBLIC) # type: ignore[has-type] # FIXME
.where(Comment.created_at > CommentsetMembership.last_seen_at)
.correlate_except(Comment)
.scalar_subquery()
diff --git a/funnel/models/contact_exchange.py b/funnel/models/contact_exchange.py
index d9398bc4a..d6f17de2d 100644
--- a/funnel/models/contact_exchange.py
+++ b/funnel/models/contact_exchange.py
@@ -9,7 +9,7 @@
from typing import Self
from uuid import UUID
-from pytz import timezone
+from pytz import BaseTzInfo, timezone
from sqlalchemy.ext.associationproxy import association_proxy
from coaster.sqlalchemy import LazyRoleSet
@@ -42,7 +42,7 @@ class ProjectId:
uuid: UUID
uuid_b58: str
title: str
- timezone: str
+ timezone: BaseTzInfo
@dataclass
@@ -51,7 +51,7 @@ class DateCountContacts:
date: datetime
count: int
- contacts: Collection[ContactExchange]
+ contacts: Collection[ContactExchange] | Query[ContactExchange]
class ContactExchange(TimestampMixin, RoleMixin, Model):
@@ -60,14 +60,14 @@ class ContactExchange(TimestampMixin, RoleMixin, Model):
__tablename__ = 'contact_exchange'
#: User who scanned this contact
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id', ondelete='CASCADE'), primary_key=True
+ sa.ForeignKey('account.id', ondelete='CASCADE'), primary_key=True, default=None
)
account: Mapped[Account] = relationship(back_populates='scanned_contacts')
#: Participant whose contact was scanned
ticket_participant_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('ticket_participant.id', ondelete='CASCADE'),
primary_key=True,
+ default=None,
index=True,
)
ticket_participant: Mapped[TicketParticipant] = relationship(
@@ -75,16 +75,17 @@ class ContactExchange(TimestampMixin, RoleMixin, Model):
)
#: Datetime at which the scan happened
scanned_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
+ sa.TIMESTAMP(timezone=True),
+ nullable=False,
+ insert_default=sa.func.utcnow(),
+ default=None,
)
#: Note recorded by the user (plain text)
description: Mapped[str] = sa_orm.mapped_column(
sa.UnicodeText, nullable=False, default=''
)
#: Archived flag
- archived: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ archived: Mapped[bool] = sa_orm.mapped_column(default=False)
__roles__ = {
'owner': {
@@ -133,8 +134,8 @@ def grouped_counts_for(
cls.scanned_at.label('scanned_at'),
Project.id.label('project_id'),
Project.uuid.label('project_uuid'),
- Project.timezone.label('project_timezone'),
Project.title.label('project_title'),
+ Project.timezone.label('project_timezone'),
).filter(
cls.ticket_participant_id == TicketParticipant.id,
TicketParticipant.project_id == Project.id,
@@ -224,7 +225,7 @@ def grouped_counts_for(
[
DateCountContacts(
r.scan_date,
- r.count,
+ r.count, # type: ignore[arg-type] # FIXME
cls.contacts_for_project_and_date(
account, k, r.scan_date, archived
),
@@ -234,12 +235,12 @@ def grouped_counts_for(
)
for k, g in groupby(
query,
- lambda r: ProjectId(
- id=r.project_id,
- uuid=r.project_uuid,
- uuid_b58=uuid_to_base58(r.project_uuid),
- title=r.project_title,
- timezone=timezone(r.project_timezone),
+ lambda row: ProjectId(
+ id=row.project_id,
+ uuid=row.project_uuid,
+ uuid_b58=uuid_to_base58(row.project_uuid),
+ title=row.project_title,
+ timezone=timezone(row.project_timezone),
),
)
]
@@ -248,7 +249,11 @@ def grouped_counts_for(
@classmethod
def contacts_for_project_and_date(
- cls, account: Account, project: Project, date: date_type, archived: bool = False
+ cls,
+ account: Account,
+ project: Project | ProjectId,
+ date: date_type,
+ archived: bool = False,
) -> Query[Self]:
"""Return contacts for a given user, project and date."""
query = cls.query.join(TicketParticipant).filter(
diff --git a/funnel/models/email_address.py b/funnel/models/email_address.py
index 773b9f99c..2a14e0d2e 100644
--- a/funnel/models/email_address.py
+++ b/funnel/models/email_address.py
@@ -157,7 +157,7 @@ class EmailAddressInUseError(EmailAddressError):
"""Email address is in use by another owner."""
-class EmailAddress(BaseMixin, Model):
+class EmailAddress(BaseMixin[int, 'Account'], Model):
"""
Represents an email address as a standalone entity, with associated metadata.
@@ -199,12 +199,10 @@ class methods, depending on whether the email address is linked to an owner or n
#: The email address, centrepiece of this model. Case preserving.
#: Validated by the :func:`_validate_email` event handler
- email: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode, nullable=True)
+ email: Mapped[str | None] = sa_orm.mapped_column()
#: The domain of the email, stored for quick lookup of related addresses
#: Read-only, accessible via the :property:`domain` property
- _domain: Mapped[str | None] = sa_orm.mapped_column(
- 'domain', sa.Unicode, nullable=True, index=True
- )
+ _domain: Mapped[str | None] = sa_orm.mapped_column('domain', index=True)
# email_normalized is defined below
@@ -233,10 +231,10 @@ class methods, depending on whether the email address is linked to an owner or n
#: Does this email address work? Records last known delivery state
_delivery_state: Mapped[int] = sa_orm.mapped_column(
'delivery_state',
- sa.Integer,
StateManager.check_constraint(
'delivery_state',
EMAIL_DELIVERY_STATE,
+ sa.Integer,
name='email_address_delivery_state_check',
),
nullable=False,
@@ -249,7 +247,10 @@ class methods, depending on whether the email address is linked to an owner or n
)
#: Timestamp of last known delivery state
delivery_state_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
+ sa.TIMESTAMP(timezone=True),
+ nullable=False,
+ insert_default=sa.func.utcnow(),
+ default=None,
)
#: Timestamp of last known recipient activity resulting from sent mail
active_at: Mapped[datetime | None] = sa_orm.mapped_column(
@@ -261,9 +262,7 @@ class methods, depending on whether the email address is linked to an owner or n
#: so a test for whether an address is blocked should use blake2b160_canonical to
#: load the record. Other records with the same canonical hash _may_ exist without
#: setting the flag due to a lack of database-side enforcement
- _is_blocked: Mapped[bool] = sa_orm.mapped_column(
- 'is_blocked', sa.Boolean, nullable=False, default=False
- )
+ _is_blocked: Mapped[bool] = sa_orm.mapped_column('is_blocked', default=False)
__table_args__ = (
# `domain` must be lowercase always. Note that Python `.lower()` is not
@@ -743,6 +742,7 @@ def email_address_id(cls) -> Mapped[int | None]:
return sa_orm.mapped_column(
sa.Integer,
sa.ForeignKey('email_address.id', ondelete='SET NULL'),
+ default=None,
nullable=cls.__email_optional__,
unique=cls.__email_unique__,
index=not cls.__email_unique__,
diff --git a/funnel/models/geoname.py b/funnel/models/geoname.py
index c8755361b..ace2cd2fb 100644
--- a/funnel/models/geoname.py
+++ b/funnel/models/geoname.py
@@ -113,7 +113,10 @@ class GeoAdmin1Code(BaseMixin, GeonameModel):
title: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
ascii_title: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
country_id: Mapped[str | None] = sa_orm.mapped_column(
- 'country', sa.CHAR(2), sa.ForeignKey('geo_country_info.iso_alpha2')
+ 'country',
+ sa.CHAR(2),
+ sa.ForeignKey('geo_country_info.iso_alpha2'),
+ default=None,
)
country: Mapped[GeoCountryInfo | None] = relationship()
admin1_code: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
@@ -138,7 +141,10 @@ class GeoAdmin2Code(BaseMixin, GeonameModel):
title: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
ascii_title: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
country_id: Mapped[str | None] = sa_orm.mapped_column(
- 'country', sa.CHAR(2), sa.ForeignKey('geo_country_info.iso_alpha2')
+ 'country',
+ sa.CHAR(2),
+ sa.ForeignKey('geo_country_info.iso_alpha2'),
+ default=None,
)
country: Mapped[GeoCountryInfo | None] = relationship()
admin1_code: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
@@ -161,7 +167,10 @@ class GeoName(BaseNameMixin, GeonameModel):
fclass: Mapped[str | None] = sa_orm.mapped_column(sa.CHAR(1))
fcode: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
country_id: Mapped[str | None] = sa_orm.mapped_column(
- 'country', sa.CHAR(2), sa.ForeignKey('geo_country_info.iso_alpha2')
+ 'country',
+ sa.CHAR(2),
+ sa.ForeignKey('geo_country_info.iso_alpha2'),
+ default=None,
)
country: Mapped[GeoCountryInfo | None] = relationship()
cc2: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
@@ -175,7 +184,7 @@ class GeoName(BaseNameMixin, GeonameModel):
viewonly=True,
)
admin1_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('geo_admin1_code.id'), nullable=True
+ sa.ForeignKey('geo_admin1_code.id'), default=None, nullable=True
)
admin1code: Mapped[GeoAdmin1Code | None] = relationship(
uselist=False, foreign_keys=[admin1_id]
@@ -192,7 +201,7 @@ class GeoName(BaseNameMixin, GeonameModel):
viewonly=True,
)
admin2_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('geo_admin2_code.id'), nullable=True
+ sa.ForeignKey('geo_admin2_code.id'), default=None, nullable=True
)
admin2code: Mapped[GeoAdmin2Code | None] = relationship(
uselist=False, foreign_keys=[admin2_id]
@@ -201,9 +210,9 @@ class GeoName(BaseNameMixin, GeonameModel):
admin4: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
admin3: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
population: Mapped[int | None] = sa_orm.mapped_column(sa.BigInteger)
- elevation: Mapped[int | None] = sa_orm.mapped_column(sa.Integer)
+ elevation: Mapped[int | None] = sa_orm.mapped_column()
#: Digital Elevation Model
- dem: Mapped[int | None] = sa_orm.mapped_column(sa.Integer)
+ dem: Mapped[int | None] = sa_orm.mapped_column()
timezone: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode)
moddate: Mapped[date | None] = sa_orm.mapped_column(sa.Date)
@@ -382,7 +391,7 @@ def related_geonames(self) -> dict[str, GeoName]:
and self.country
and self.country.continent
):
- continent = GeoName.query.get(continent_codes[self.country.continent])
+ continent = db.session.get(GeoName, continent_codes[self.country.continent])
if continent:
related['continent'] = continent
@@ -627,17 +636,15 @@ class GeoAltName(BaseMixin, GeonameModel):
__tablename__ = 'geo_alt_name'
geonameid: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('geo_name.id'), nullable=False
+ sa.ForeignKey('geo_name.id'), default=None, nullable=False
)
geoname: Mapped[GeoName] = relationship(back_populates='alternate_titles')
- lang: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, nullable=True, index=True
- )
- title: Mapped[str] = sa_orm.mapped_column(sa.Unicode, nullable=False)
- is_preferred_name: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
- is_short_name: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
- is_colloquial: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
- is_historic: Mapped[bool] = sa_orm.mapped_column(sa.Boolean, nullable=False)
+ lang: Mapped[str | None] = sa_orm.mapped_column(index=True)
+ title: Mapped[str] = sa_orm.mapped_column()
+ is_preferred_name: Mapped[bool] = sa_orm.mapped_column()
+ is_short_name: Mapped[bool] = sa_orm.mapped_column()
+ is_colloquial: Mapped[bool] = sa_orm.mapped_column()
+ is_historic: Mapped[bool] = sa_orm.mapped_column()
__table_args__ = (
sa.Index(
diff --git a/funnel/models/helpers.py b/funnel/models/helpers.py
index 105fc0755..549f25a51 100644
--- a/funnel/models/helpers.py
+++ b/funnel/models/helpers.py
@@ -22,6 +22,8 @@
from sqlalchemy.orm import Mapped, composite
from zxcvbn import zxcvbn
+from coaster.utils import DataclassFromType
+
from .. import app
from ..typing import T
from ..utils import MarkdownConfig, MarkdownString, markdown_escape
@@ -31,6 +33,7 @@
'RESERVED_NAMES',
'PASSWORD_MIN_LENGTH',
'PASSWORD_MAX_LENGTH',
+ 'IntTitle',
'check_password_strength',
'profanity',
'add_to_class',
@@ -132,6 +135,14 @@
}
+@dataclass(frozen=True)
+class IntTitle(DataclassFromType, int):
+ """Integer value with a title (for enums)."""
+
+ # The empty default is required for Mypy's enum plugin's `Enum.__call__` analysis
+ title: str = ''
+
+
@dataclass
class PasswordCheckType:
"""
diff --git a/funnel/models/label.py b/funnel/models/label.py
index 3f0a4c150..83ecae347 100644
--- a/funnel/models/label.py
+++ b/funnel/models/label.py
@@ -18,10 +18,11 @@
sa,
sa_orm,
)
+from .account import Account
from .helpers import add_search_trigger, visual_field_delimiter
from .project_membership import project_child_role_map
-proposal_label = sa.Table(
+proposal_label: sa.Table = sa.Table(
'proposal_label',
Model.metadata,
sa.Column(
@@ -43,11 +44,11 @@
)
-class Label(BaseScopedNameMixin, Model):
+class Label(BaseScopedNameMixin[int, Account], Model):
__tablename__ = 'label'
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id', ondelete='CASCADE'), nullable=False
+ sa.ForeignKey('project.id', ondelete='CASCADE'), default=None, nullable=False
)
# Backref from project is defined in the Project model with an ordering list
project: Mapped[Project] = with_roles(
@@ -61,8 +62,8 @@ class Label(BaseScopedNameMixin, Model):
#: ability to : validate the value within the app. Always use the :attr:`main_label`
#: relationship.
main_label_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('label.id', ondelete='CASCADE'),
+ default=None,
index=True,
nullable=True,
)
@@ -82,7 +83,7 @@ class Label(BaseScopedNameMixin, Model):
# add_primary_relationship)
#: Sequence number for this label, used in UI for ordering
- seq: Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False)
+ seq: Mapped[int] = sa_orm.mapped_column()
# A single-line description of this label, shown when picking labels (optional)
description: Mapped[str] = sa_orm.mapped_column(
@@ -97,22 +98,16 @@ class Label(BaseScopedNameMixin, Model):
#: Restricted mode specifies that this label may only be applied by someone with
#: an editorial role (TODO: name the role). If this label is a parent, it applies
#: to all its children
- _restricted: Mapped[bool] = sa_orm.mapped_column(
- 'restricted', sa.Boolean, nullable=False, default=False
- )
+ _restricted: Mapped[bool] = sa_orm.mapped_column('restricted', default=False)
#: Required mode signals to UI that if this label is a parent, one of its
#: children must be mandatorily applied to the proposal. The value of this
#: field must be ignored if the label is not a parent
- _required: Mapped[bool] = sa_orm.mapped_column(
- 'required', sa.Boolean, nullable=False, default=False
- )
+ _required: Mapped[bool] = sa_orm.mapped_column('required', default=False)
#: Archived mode specifies that the label is no longer available for use
#: although all the previous records will stay in database.
- _archived: Mapped[bool] = sa_orm.mapped_column(
- 'archived', sa.Boolean, nullable=False, default=False
- )
+ _archived: Mapped[bool] = sa_orm.mapped_column('archived', default=False)
search_vector: Mapped[str] = sa_orm.mapped_column(
TSVectorType(
diff --git a/funnel/models/login_session.py b/funnel/models/login_session.py
index f983a4912..422542c45 100644
--- a/funnel/models/login_session.py
+++ b/funnel/models/login_session.py
@@ -87,32 +87,28 @@ class LoginSessionInactiveUserError(LoginSessionError):
)
-class LoginSession(UuidMixin, BaseMixin, Model):
+class LoginSession(UuidMixin, BaseMixin[int, Account], Model):
__tablename__ = 'login_session'
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = relationship(back_populates='all_login_sessions')
#: User's last known IP address
ipaddr: Mapped[str] = sa_orm.mapped_column(sa.String(45), nullable=False)
#: City geonameid from IP address
- geonameid_city: Mapped[int | None] = sa_orm.mapped_column(sa.Integer, nullable=True)
+ geonameid_city: Mapped[int | None] = sa_orm.mapped_column()
#: State/subdivision geonameid from IP address
- geonameid_subdivision: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, nullable=True
- )
+ geonameid_subdivision: Mapped[int | None] = sa_orm.mapped_column()
#: Country geonameid from IP address
- geonameid_country: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, nullable=True
- )
+ geonameid_country: Mapped[int | None] = sa_orm.mapped_column()
#: User's network, from IP address
- geoip_asn: Mapped[int | None] = sa_orm.mapped_column(sa.Integer, nullable=True)
+ geoip_asn: Mapped[int | None] = sa_orm.mapped_column()
#: User agent
user_agent: Mapped[str] = sa_orm.mapped_column(sa.UnicodeText, nullable=False)
#: The login service that was used to make this session
- login_service: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode, nullable=True)
+ login_service: Mapped[str | None] = sa_orm.mapped_column()
accessed_at: Mapped[datetime] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=False
@@ -121,7 +117,10 @@ class LoginSession(UuidMixin, BaseMixin, Model):
sa.TIMESTAMP(timezone=True), nullable=True
)
sudo_enabled_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
+ sa.TIMESTAMP(timezone=True),
+ nullable=False,
+ insert_default=sa.func.utcnow(),
+ default=None,
)
# --- Backrefs
diff --git a/funnel/models/mailer.py b/funnel/models/mailer.py
index 9dec9157a..c60e7f6d7 100644
--- a/funnel/models/mailer.py
+++ b/funnel/models/mailer.py
@@ -5,7 +5,7 @@
import re
from collections.abc import Collection, Iterator
from datetime import datetime
-from enum import IntEnum
+from enum import ReprEnum
from typing import Any, Self
from uuid import UUID
@@ -31,6 +31,7 @@
sa_orm,
)
from .account import Account
+from .helpers import IntTitle
from .types import jsonb
__all__ = [
@@ -48,41 +49,31 @@
EMAIL_TAGS[_key].append('style')
-class MailerState(IntEnum):
+class MailerState(IntTitle, ReprEnum):
"""Send state for :class:`Mailer`."""
- DRAFT = 0
- QUEUED = 1
- SENDING = 2
- SENT = 3
+ DRAFT = 0, __("Draft")
+ QUEUED = (1, __("Queued"))
+ SENDING = 2, __("Sending")
+ SENT = 3, __("Sent")
- __titles__ = {
- DRAFT: __("Draft"),
- QUEUED: __("Queued"),
- SENDING: __("Sending"),
- SENT: __("Sent"),
- }
- def __init__(self, value: int) -> None:
- self.title = self.__titles__[value]
-
-
-class Mailer(BaseNameMixin, Model):
+class Mailer(BaseNameMixin[int, Account], Model):
"""A mailer sent via email to multiple recipients."""
__tablename__ = 'mailer'
- user_uuid: Mapped[UUID] = sa_orm.mapped_column(sa.ForeignKey('account.uuid'))
+ user_uuid: Mapped[UUID] = sa_orm.mapped_column(
+ sa.ForeignKey('account.uuid'), default=None
+ )
user: Mapped[Account] = relationship(back_populates='mailers')
status: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, nullable=False, default=MailerState.DRAFT
+ nullable=False, default=MailerState.DRAFT
)
_fields: Mapped[str] = sa_orm.mapped_column(
'fields', sa.UnicodeText, nullable=False, default=''
)
- trackopens: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ trackopens: Mapped[bool] = sa_orm.mapped_column(default=False)
stylesheet: Mapped[str] = sa_orm.mapped_column(
sa.UnicodeText, nullable=False, default=''
)
@@ -165,7 +156,7 @@ def recipients_iter(self) -> Iterator[MailerRecipient]:
.all()
]
for rid in ids:
- recipient = MailerRecipient.query.get(rid)
+ recipient = db.session.get(MailerRecipient, rid)
if recipient:
yield recipient
@@ -194,13 +185,13 @@ def render_preview(self, text: str) -> str:
return ''
-class MailerDraft(BaseScopedIdMixin, Model):
+class MailerDraft(BaseScopedIdMixin[int, Account], Model):
"""Revision-controlled draft of mailer text (a Mustache template)."""
__tablename__ = 'mailer_draft'
mailer_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('mailer.id'), nullable=False
+ sa.ForeignKey('mailer.id'), default=None, nullable=False
)
mailer: Mapped[Mailer] = relationship(back_populates='drafts')
parent: Mapped[Mailer] = sa_orm.synonym('mailer')
@@ -223,7 +214,7 @@ def get_preview(self) -> str:
return self.mailer.render_preview(self.template)
-class MailerRecipient(BaseScopedIdMixin, Model):
+class MailerRecipient(BaseScopedIdMixin[int, Account], Model):
"""Recipient of a mailer."""
__tablename__ = 'mailer_recipient'
@@ -259,11 +250,13 @@ class MailerRecipient(BaseScopedIdMixin, Model):
# Support email open tracking
opentoken: Mapped[str] = sa_orm.mapped_column(
- sa.Unicode(44), nullable=False, default=newsecret, unique=True
- )
- opened: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
+ sa.Unicode(44),
+ nullable=False,
+ insert_default=newsecret,
+ default=None,
+ unique=True,
)
+ opened: Mapped[bool] = sa_orm.mapped_column(default=False)
opened_ipaddr: Mapped[str | None] = sa_orm.mapped_column(
sa.Unicode(45), nullable=True
)
@@ -273,13 +266,15 @@ class MailerRecipient(BaseScopedIdMixin, Model):
opened_last_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
)
- opened_count: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, nullable=False, default=0
- )
+ opened_count: Mapped[int] = sa_orm.mapped_column(nullable=False, default=0)
# Support RSVP if the email requires it
rsvptoken: Mapped[str] = sa_orm.mapped_column(
- sa.Unicode(44), nullable=False, default=newsecret, unique=True
+ sa.Unicode(44),
+ nullable=False,
+ insert_default=newsecret,
+ default=None,
+ unique=True,
)
# Y/N/M response
rsvp: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode(1), nullable=True)
@@ -301,7 +296,7 @@ class MailerRecipient(BaseScopedIdMixin, Model):
# Draft of the mailer template that the custom template is linked to (for updating
# before finalising)
draft_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('mailer_draft.id')
+ sa.ForeignKey('mailer_draft.id'), default=None
)
draft: Mapped[MailerDraft | None] = relationship()
diff --git a/funnel/models/membership_mixin.py b/funnel/models/membership_mixin.py
index e3fc1c46b..c5d32f136 100644
--- a/funnel/models/membership_mixin.py
+++ b/funnel/models/membership_mixin.py
@@ -2,10 +2,11 @@
from __future__ import annotations
-from collections.abc import Callable, Iterable
+from collections.abc import Iterable
from datetime import datetime as datetime_type
+from enum import ReprEnum
from types import SimpleNamespace
-from typing import TYPE_CHECKING, Any, ClassVar, Generic, Self, TypeVar
+from typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, Self, TypeVar
from uuid import UUID
from sqlalchemy import event
@@ -14,12 +15,10 @@
from baseframe import __
from coaster.sqlalchemy import StateManager, immutable, with_roles
-from coaster.utils import LabeledEnum
from . import (
BaseMixin,
Mapped,
- Model,
UuidMixin,
db,
declarative_mixin,
@@ -30,11 +29,12 @@
sa_orm,
)
from .account import Account
-from .reorder_mixin import ReorderProtoMixin
+from .helpers import IntTitle
+from .reorder_mixin import ReorderMixin
# Export only symbols needed in views.
__all__ = [
- 'MEMBERSHIP_RECORD_TYPE',
+ 'MembershipRecordTypeEnum',
'MembershipError',
'MembershipRevokedError',
'MembershipRecordTypeError',
@@ -43,29 +43,58 @@
# --- Typing ---------------------------------------------------------------------------
MembershipType = TypeVar('MembershipType', bound='ImmutableMembershipMixin')
+
+
+class MembershipMixinProtocol(Protocol):
+ member_id: Mapped[int]
+ member: declared_attr[Account]
+ _local_data_only: bool
+ parent_id_column: ClassVar[str]
+
+ def replace(self, actor: Account, **data: Any) -> Self:
+ ...
+
+
+class FrozenAttributionSubclassProtocol(MembershipMixinProtocol, Protocol):
+ _title: declared_attr[str | None]
+
+
+class ReorderSubclassProtocol(Protocol):
+ seq: Mapped[Any]
+ parent_id: Mapped[Any]
+ parent: Mapped[Any]
+ is_active: hybrid_property[bool]
+
+ @property
+ def parent_scoped_reorder_query_filter(
+ self: ReorderSubclassProtocol,
+ ) -> ColumnElement[bool]:
+ ...
+
+
+MembershipMixinType = TypeVar('MembershipMixinType', bound=MembershipMixinProtocol)
FrozenAttributionType = TypeVar(
- 'FrozenAttributionType', bound='FrozenAttributionProtoMixin'
+ 'FrozenAttributionType', bound=FrozenAttributionSubclassProtocol
)
+
# --- Enum -----------------------------------------------------------------------------
-class MEMBERSHIP_RECORD_TYPE(LabeledEnum): # noqa: N801
+class MembershipRecordTypeEnum(IntTitle, ReprEnum):
"""Membership record types."""
- # TODO: Convert into IntEnum
-
#: An invite represents a potential future membership, but not a current membership
- INVITE = (1, 'invite', __("Invite"))
+ INVITE = 1, __("Invite")
#: An accept recognises a conversion from an invite into a current membership
- ACCEPT = (2, 'accept', __("Accept"))
+ ACCEPT = 2, __("Accept")
#: A direct add recognises a current membership without proof of consent
- DIRECT_ADD = (3, 'direct_add', __("Direct add"))
+ DIRECT_ADD = 3, __("Direct add")
#: An amendment is when data in the record has been changed
- AMEND = (4, 'amend', __("Amend"))
+ AMEND = 4, __("Amend")
#: A migrate record says this used to be some other form of membership and has been
#: created due to a technical change in the product
- # Forthcoming: MIGRATE = (5, 'migrate', __("Migrate"))
+ # Forthcoming: MIGRATE = 5, __("Migrate")
# --- Exceptions -----------------------------------------------------------------------
@@ -87,25 +116,83 @@ class MembershipRecordTypeError(MembershipError):
@declarative_mixin
-class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID]):
+class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID, Account]):
"""Support class for immutable memberships."""
- #: Can granted_by be null? Only in memberships based on legacy data
- __null_granted_by__: ClassVar[bool] = False
- #: List of columns that will be copied into a new row when a membership is amended
- __data_columns__: ClassVar[Iterable[str]] = ()
- #: Name of the parent id column, used in SQL constraints
- parent_id_column: ClassVar[str | None]
if TYPE_CHECKING:
#: Subclass has a table name
__tablename__: str
#: Parent column (declare as synonym of 'profile_id' or 'project_id' in
#: subclasses)
- parent_id: Mapped[int] | None
+ parent_id: Mapped[Any] | None
#: Parent object
- parent: Mapped[Model] | None
- #: Subject of this membership (subclasses must define)
- member: Mapped[Account]
+ parent: Mapped[Any] | None
+
+ #: Can granted_by be null? Only in memberships based on legacy data
+ __null_granted_by__: ClassVar[bool] = False
+ #: List of columns that will be copied into a new row when a membership is amended
+ __data_columns__: ClassVar[Iterable[str]] = ()
+ #: Name of the parent id column, used in SQL constraints
+ parent_id_column: ClassVar[str]
+ #: Foreign key column to account table
+ member_id: Mapped[int] = sa_orm.mapped_column(
+ sa.ForeignKey('account.id', ondelete='CASCADE'),
+ default=None,
+ nullable=False,
+ index=True,
+ )
+
+ @classmethod
+ def __member(cls) -> Mapped[Account]:
+ """Member in this membership record."""
+ return relationship(Account, foreign_keys=[cls.member_id])
+
+ member = with_roles(
+ declared_attr(__member),
+ read={'member', 'editor'},
+ grants_via={None: {'admin': 'member'}},
+ )
+ del __member
+
+ @declared_attr
+ @classmethod
+ def user(cls) -> Mapped[Account]:
+ """Legacy alias for member in this membership record."""
+ return sa_orm.synonym('member')
+
+ __table_args__: tuple # pyright: ignore[reportGeneralTypeIssues]
+
+ @declared_attr.directive # type: ignore[no-redef]
+ @classmethod
+ def __table_args__(cls) -> tuple:
+ """Table arguments for SQLAlchemy."""
+ try:
+ args = list(super().__table_args__) # type: ignore[misc]
+ except AttributeError:
+ args = []
+ kwargs = args.pop(-1) if args and isinstance(args[-1], dict) else None
+ if cls.parent_id_column:
+ args.append(
+ sa.Index(
+ 'ix_' + cls.__tablename__ + '_active',
+ cls.parent_id_column,
+ 'member_id',
+ unique=True,
+ postgresql_where='revoked_at IS NULL',
+ ),
+ )
+ else:
+ args.append(
+ sa.Index(
+ 'ix_' + cls.__tablename__ + '_active',
+ 'member_id',
+ unique=True,
+ postgresql_where='revoked_at IS NULL',
+ ),
+ )
+ if kwargs:
+ args.append(kwargs)
+ return tuple(args)
#: Should an active membership record be revoked when the member is soft-deleted?
#: (Hard deletes will cascade and also delete all membership records.)
@@ -120,7 +207,10 @@ class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID]):
granted_at: Mapped[datetime_type] = with_roles(
immutable(
sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
+ sa.TIMESTAMP(timezone=True),
+ nullable=False,
+ insert_default=sa.func.utcnow(),
+ default=None,
)
),
read={'member', 'editor'},
@@ -134,9 +224,10 @@ class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID]):
record_type: Mapped[int] = with_roles(
immutable(
sa_orm.mapped_column(
- sa.Integer,
- StateManager.check_constraint('record_type', MEMBERSHIP_RECORD_TYPE),
- default=MEMBERSHIP_RECORD_TYPE.DIRECT_ADD,
+ StateManager.check_constraint(
+ 'record_type', MembershipRecordTypeEnum, sa.Integer
+ ),
+ default=MembershipRecordTypeEnum.DIRECT_ADD,
nullable=False,
)
),
@@ -144,17 +235,19 @@ class ImmutableMembershipMixin(UuidMixin, BaseMixin[UUID]):
)
@cached_property
- def record_type_label(self):
- return MEMBERSHIP_RECORD_TYPE[self.record_type]
+ def record_type_enum(self):
+ return MembershipRecordTypeEnum(self.record_type)
- with_roles(record_type_label, read={'member', 'editor'})
+ with_roles(record_type_enum, read={'member', 'editor'})
@declared_attr
@classmethod
def revoked_by_id(cls) -> Mapped[int | None]:
"""Id of user who revoked the membership."""
return sa_orm.mapped_column(
- sa.ForeignKey('account.id', ondelete='SET NULL'), nullable=True
+ sa.ForeignKey('account.id', ondelete='SET NULL'),
+ default=None,
+ nullable=True,
)
@with_roles(read={'member', 'editor'}, grants={'editor'})
@@ -174,8 +267,8 @@ def granted_by_id(cls) -> Mapped[int | None]:
for granted_by.
"""
return sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('account.id', ondelete='SET NULL'),
+ default=None,
nullable=cls.__null_granted_by__,
)
@@ -191,7 +284,7 @@ def is_active(self) -> bool:
"""Test if membership record is active (not revoked, not an invite)."""
return (
self.revoked_at is None
- and self.record_type != MEMBERSHIP_RECORD_TYPE.INVITE
+ and self.record_type != MembershipRecordTypeEnum.INVITE
)
@is_active.inplace.expression
@@ -199,7 +292,7 @@ def is_active(self) -> bool:
def _is_active_expression(cls) -> sa.ColumnElement[bool]:
"""Test if membership record is active as a SQL expression."""
return sa.and_(
- cls.revoked_at.is_(None), cls.record_type != MEMBERSHIP_RECORD_TYPE.INVITE
+ cls.revoked_at.is_(None), cls.record_type != MembershipRecordTypeEnum.INVITE
)
with_roles(is_active, read={'member'})
@@ -207,14 +300,14 @@ def _is_active_expression(cls) -> sa.ColumnElement[bool]:
@hybrid_property
def is_invite(self) -> bool:
"""Test if membership record is an invitation."""
- return self.record_type == MEMBERSHIP_RECORD_TYPE.INVITE
+ return self.record_type == MembershipRecordTypeEnum.INVITE
with_roles(is_invite, read={'member', 'editor'})
@hybrid_property
def is_amendment(self) -> bool:
"""Test if membership record is an amendment."""
- return self.record_type == MEMBERSHIP_RECORD_TYPE.AMEND
+ return self.record_type == MembershipRecordTypeEnum.AMEND
with_roles(is_amendment, read={'member', 'editor'})
@@ -243,14 +336,8 @@ def revoke(self, actor: Account) -> None:
self.revoked_at = sa.func.utcnow()
self.revoked_by = actor
- def copy_template(self: MembershipType, **kwargs) -> MembershipType:
- """Make a copy of self for customization."""
- raise NotImplementedError("Subclasses must implement copy_template")
-
@with_roles(call={'editor'})
- def replace(
- self: MembershipType, actor: Account, _accept: bool = False, **data: Any
- ) -> MembershipType:
+ def replace(self, actor: Account, _accept: bool = False, **data: Any) -> Self:
"""Replace this membership record with changes to role columns."""
if self.revoked_at is not None:
raise MembershipRevokedError(
@@ -261,7 +348,7 @@ def replace(
# Perform sanity check. If nothing changed, just return self
has_changes = False
- if self.record_type == MEMBERSHIP_RECORD_TYPE.INVITE and _accept:
+ if self.record_type == MembershipRecordTypeEnum.INVITE and _accept:
# If the existing record is an INVITE and this is an ACCEPT, we have
# a record change even if no data changed
has_changes = True
@@ -287,13 +374,13 @@ def replace(
# if existing record type is INVITE, then ACCEPT or amend as new INVITE
# else replace it with AMEND
- if self.record_type == MEMBERSHIP_RECORD_TYPE.INVITE:
+ if self.record_type == MembershipRecordTypeEnum.INVITE:
if _accept:
- new.record_type = MEMBERSHIP_RECORD_TYPE.ACCEPT
+ new.record_type = MembershipRecordTypeEnum.ACCEPT
else:
- new.record_type = MEMBERSHIP_RECORD_TYPE.INVITE
+ new.record_type = MembershipRecordTypeEnum.INVITE
else:
- new.record_type = MEMBERSHIP_RECORD_TYPE.AMEND
+ new.record_type = MembershipRecordTypeEnum.AMEND
self._local_data_only = True
for column in self.__data_columns__:
@@ -310,9 +397,7 @@ def amend_by(self, actor: Account):
"""Amend a membership in a `with` context."""
return AmendMembership(self, actor)
- def merge_and_replace(
- self: MembershipType, actor: Account, other: MembershipType
- ) -> MembershipType:
+ def merge_and_replace(self, actor: Account, other: Self) -> Self:
"""Replace this record by merging data from an independent record."""
if self.__class__ is not other.__class__:
raise TypeError("Merger requires membership records of the same type")
@@ -322,8 +407,8 @@ def merge_and_replace(
raise MembershipRevokedError("Can't merge with a revoked membership record")
if (
- self.record_type == MEMBERSHIP_RECORD_TYPE.INVITE
- and other.record_type != MEMBERSHIP_RECORD_TYPE.INVITE
+ self.record_type == MembershipRecordTypeEnum.INVITE
+ and other.record_type != MembershipRecordTypeEnum.INVITE
):
# If we are an INVITE but the other is not an INVITE, then we must ACCEPT
# the INVITE before proceeding to an AMEND merger
@@ -348,87 +433,14 @@ def merge_and_replace(
return replacement
@with_roles(call={'member'})
- def accept(self: MembershipType, actor: Account) -> MembershipType:
+ def accept(self, actor: Account) -> Self:
"""Accept a membership invitation."""
- if self.record_type != MEMBERSHIP_RECORD_TYPE.INVITE:
+ if self.record_type != MembershipRecordTypeEnum.INVITE:
raise MembershipRecordTypeError("This membership record is not an invite")
if 'member' not in self.roles_for(actor):
raise ValueError("Invite must be accepted by the invited user")
return self.replace(actor, _accept=True)
- @with_roles(call={'owner', 'member'})
- def freeze_member_attribution(
- self: MembershipType, actor: Account
- ) -> MembershipType:
- """
- Freeze member attribution and return a replacement record.
-
- Subclasses that support member attribution must override this method. The
- default implementation returns `self`.
- """
- return self
-
-
-@declarative_mixin
-class ImmutableUserMembershipMixin(ImmutableMembershipMixin):
- """Support class for immutable memberships for users."""
-
- @declared_attr
- @classmethod
- def member_id(cls) -> Mapped[int]:
- """Foreign key column to account table."""
- return sa_orm.mapped_column(
- sa.Integer,
- sa.ForeignKey('account.id', ondelete='CASCADE'),
- nullable=False,
- index=True,
- )
-
- @with_roles(read={'member', 'editor'}, grants_via={None: {'admin': 'member'}})
- @declared_attr
- @classmethod
- def member(cls) -> Mapped[Account]: # type: ignore[override]
- """Member in this membership record."""
- return relationship(Account, foreign_keys=[cls.member_id])
-
- @declared_attr
- @classmethod
- def user(cls) -> Mapped[Account]:
- """Legacy alias for member in this membership record."""
- return sa_orm.synonym('member')
-
- @declared_attr.directive
- @classmethod
- def __table_args__(cls) -> tuple:
- """Table arguments for SQLAlchemy."""
- try:
- args = list(super().__table_args__) # type: ignore[misc]
- except AttributeError:
- args = []
- kwargs = args.pop(-1) if args and isinstance(args[-1], dict) else None
- if cls.parent_id_column is not None:
- args.append(
- sa.Index(
- 'ix_' + cls.__tablename__ + '_active',
- cls.parent_id_column,
- 'member_id',
- unique=True,
- postgresql_where='revoked_at IS NULL',
- ),
- )
- else:
- args.append(
- sa.Index(
- 'ix_' + cls.__tablename__ + '_active',
- 'member_id',
- unique=True,
- postgresql_where='revoked_at IS NULL',
- ),
- )
- if kwargs:
- args.append(kwargs)
- return tuple(args)
-
@hybrid_property
def is_self_granted(self) -> bool:
"""Return True if the member in this record is also the granting actor."""
@@ -447,8 +459,9 @@ def is_self_revoked(self) -> bool:
with_roles(is_self_revoked, read={'member', 'editor'})
- def copy_template(self: MembershipType, **kwargs) -> MembershipType:
- return type(self)(member=self.member, **kwargs) # type: ignore
+ def copy_template(self, **kwargs) -> Self:
+ """Make a copy of self for customization."""
+ return self.__class__(member=self.member, **kwargs) # type: ignore[call-arg]
@classmethod
def migrate_account(cls, old_account: Account, new_account: Account) -> None:
@@ -503,12 +516,9 @@ def migrate_account(cls, old_account: Account, new_account: Account) -> None:
@declarative_mixin
-class ReorderMembershipProtoMixin(ReorderProtoMixin):
+class ReorderMembershipMixin(ImmutableMembershipMixin, ReorderMixin):
"""Customizes ReorderMixin for membership models."""
- if TYPE_CHECKING:
- parent_id_column: ClassVar[str]
-
#: Sequence number. Not immutable, and may be overwritten by ReorderMixin as a
#: side-effect of reordering other records. This is not considered a revision.
#: However, it can be argued that relocating a sponsor in the list constitutes a
@@ -516,19 +526,21 @@ class ReorderMembershipProtoMixin(ReorderProtoMixin):
#: on `seq` being mutable in a future iteration.
seq: Mapped[int] = sa_orm.mapped_column(nullable=False)
- @declared_attr.directive
+ __table_args__: tuple # pyright: ignore[reportGeneralTypeIssues]
+
+ @declared_attr.directive # type: ignore[no-redef]
@classmethod
- def __table_args__(cls) -> tuple:
+ def __table_args__(cls) -> tuple: # type: ignore[override]
"""Table arguments."""
try:
- args = list(super().__table_args__) # type: ignore[misc]
+ args = list(super().__table_args__)
except AttributeError:
args = []
kwargs = args.pop(-1) if args and isinstance(args[-1], dict) else None
# Add unique constraint on :attr:`seq` for active records
args.append(
sa.Index(
- 'ix_' + cls.__tablename__ + '_seq', # type: ignore[attr-defined]
+ 'ix_' + cls.__tablename__ + '_seq',
cls.parent_id_column,
'seq',
unique=True,
@@ -539,18 +551,20 @@ def __table_args__(cls) -> tuple:
args.append(kwargs)
return tuple(args)
- def __init__(self, **kwargs) -> None:
+ def __init__(self: ReorderSubclassProtocol, **kwargs) -> None:
super().__init__(**kwargs)
# Assign a default value to `seq`
if self.seq is None: # Will be None until first commit
- self.seq = ( # type: ignore[unreachable]
+ self.seq = (
sa.select(sa.func.coalesce(sa.func.max(self.__class__.seq) + 1, 1))
.where(self.parent_scoped_reorder_query_filter)
.scalar_subquery()
)
@property
- def parent_scoped_reorder_query_filter(self) -> ColumnElement:
+ def parent_scoped_reorder_query_filter(
+ self: ReorderSubclassProtocol,
+ ) -> ColumnElement[bool]:
"""
Return a query filter that includes a scope limitation to active records.
@@ -564,26 +578,20 @@ def parent_scoped_reorder_query_filter(self) -> ColumnElement:
if self.parent_id is not None:
return sa.and_(
cls.parent_id == self.parent_id,
- cls.is_active, # type: ignore[attr-defined]
+ cls.is_active,
)
- return sa.and_( # type: ignore[unreachable]
+ return sa.and_(
cls.parent == self.parent,
cls.is_active,
)
@declarative_mixin
-class FrozenAttributionProtoMixin:
+class FrozenAttributionMixin:
"""Provides a `title` data column and support method to freeze it."""
- if TYPE_CHECKING:
- member: Mapped[Account]
- replace: Callable[..., Self]
- _local_data_only: bool
-
- @declared_attr
@classmethod
- def _title(cls) -> Mapped[str | None]:
+ def __title(cls) -> Mapped[str | None]:
"""Create optional attribution title for this membership record."""
return immutable(
sa_orm.mapped_column(
@@ -591,8 +599,11 @@ def _title(cls) -> Mapped[str | None]:
)
)
+ _title = declared_attr(__title)
+ del __title
+
@property
- def title(self) -> str:
+ def title(self: FrozenAttributionSubclassProtocol) -> str:
"""Attribution title for this record."""
if self._local_data_only:
# self._title may be None when returning local data
@@ -607,12 +618,14 @@ def title(self, value: str | None) -> None:
self._title = value or None # Don't set empty string
@property
- def pickername(self) -> str:
+ def pickername(self: FrozenAttributionSubclassProtocol) -> str:
"""Return member's pickername, but only if attribution isn't frozen."""
return self._title if self._title else self.member.pickername
@with_roles(call={'owner', 'member'})
- def freeze_member_attribution(self, actor: Account) -> Self:
+ def freeze_member_attribution(
+ self: FrozenAttributionType, actor: Account
+ ) -> FrozenAttributionType:
"""Freeze member attribution and return a replacement record."""
if self._title is None:
membership = self.replace(actor=actor, title=self.member.title)
@@ -691,7 +704,7 @@ def _confirm_enumerated_mixins(_mapper: Any, cls: type[Account]) -> None:
"""Confirm that the membership collection attributes actually exist."""
expected_class = ImmutableMembershipMixin
if issubclass(cls, Account):
- expected_class = ImmutableUserMembershipMixin
+ expected_class = ImmutableMembershipMixin
for source in (
cls.__active_membership_attrs__,
cls.__noninvite_membership_attrs__,
diff --git a/funnel/models/moderation.py b/funnel/models/moderation.py
index 95a388603..08cd28e53 100644
--- a/funnel/models/moderation.py
+++ b/funnel/models/moderation.py
@@ -22,25 +22,30 @@ class MODERATOR_REPORT_TYPE(LabeledEnum): # noqa: N801
SPAM = (2, 'spam', __("Spam"))
-class CommentModeratorReport(UuidMixin, BaseMixin[UUID], Model):
+class CommentModeratorReport(UuidMixin, BaseMixin[UUID, Account], Model):
__tablename__ = 'comment_moderator_report'
comment_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('comment.id'), nullable=False, index=True
+ sa.ForeignKey('comment.id'), default=None, nullable=False, index=True
)
comment: Mapped[Comment] = relationship(back_populates='moderator_reports')
reported_by_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False, index=True
+ sa.ForeignKey('account.id'), default=None, nullable=False, index=True
)
reported_by: Mapped[Account] = relationship(back_populates='moderator_reports')
report_type: Mapped[int] = sa_orm.mapped_column(
sa.SmallInteger,
- StateManager.check_constraint('report_type', MODERATOR_REPORT_TYPE),
+ StateManager.check_constraint(
+ 'report_type', MODERATOR_REPORT_TYPE, sa.SmallInteger
+ ),
nullable=False,
default=MODERATOR_REPORT_TYPE.SPAM,
)
reported_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), default=sa.func.utcnow(), nullable=False
+ sa.TIMESTAMP(timezone=True),
+ insert_default=sa.func.utcnow(),
+ default=None,
+ nullable=False,
)
resolved_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True, index=True
diff --git a/funnel/models/notification.py b/funnel/models/notification.py
index 145dea0de..50407cbe4 100644
--- a/funnel/models/notification.py
+++ b/funnel/models/notification.py
@@ -84,7 +84,8 @@
from collections.abc import Callable, Generator, Sequence
from dataclasses import dataclass
from datetime import datetime
-from types import SimpleNamespace
+from enum import ReprEnum
+from types import SimpleNamespace, UnionType
from typing import (
Any,
ClassVar,
@@ -113,7 +114,7 @@
immutable,
with_roles,
)
-from coaster.utils import LabeledEnum, uuid_from_base58, uuid_to_base58
+from coaster.utils import utcnow, uuid_from_base58, uuid_to_base58
from ..typing import T
from . import (
@@ -131,11 +132,12 @@
sa_orm,
)
from .account import Account, AccountEmail, AccountPhone
+from .helpers import IntTitle
from .phone_number import PhoneNumber, PhoneNumberMixin
-from .typing import UuidModelUnion
+from .typing import ModelUuidProtocol
__all__ = [
- 'SMS_STATUS',
+ 'SmsStatusEnum',
'notification_categories',
'SmsMessage',
'NotificationType',
@@ -151,9 +153,9 @@
# --- Typing ---------------------------------------------------------------------------
# Document generic type
-_D = TypeVar('_D', bound=UuidModelUnion)
+_D = TypeVar('_D', bound=ModelUuidProtocol)
# Fragment generic type
-_F = TypeVar('_F', bound=Optional[UuidModelUnion])
+_F = TypeVar('_F', bound=ModelUuidProtocol | None)
# Type of None (required to detect Optional)
NoneType = type(None)
@@ -219,20 +221,20 @@ class NotificationCategory:
# --- Flags ----------------------------------------------------------------------------
-class SMS_STATUS(LabeledEnum): # noqa: N801
+class SmsStatusEnum(IntTitle, ReprEnum):
"""SMS delivery status."""
- QUEUED = (1, __("Queued"))
- PENDING = (2, __("Pending"))
- DELIVERED = (3, __("Delivered"))
- FAILED = (4, __("Failed"))
- UNKNOWN = (5, __("Unknown"))
+ QUEUED = 1, __("Queued")
+ PENDING = 2, __("Pending")
+ DELIVERED = 3, __("Delivered")
+ FAILED = 4, __("Failed")
+ UNKNOWN = 5, __("Unknown")
# --- Legacy models --------------------------------------------------------------------
-class SmsMessage(PhoneNumberMixin, BaseMixin, Model):
+class SmsMessage(PhoneNumberMixin, BaseMixin[int, Account], Model):
"""An outbound SMS message."""
__tablename__ = 'sms_message'
@@ -249,7 +251,7 @@ class SmsMessage(PhoneNumberMixin, BaseMixin, Model):
)
# Flags
status: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, default=SMS_STATUS.QUEUED, nullable=False
+ default=SmsStatusEnum.QUEUED, nullable=False
)
status_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
@@ -271,6 +273,9 @@ def __init__(self, **kwargs) -> None:
class NotificationType(Generic[_D, _F], Protocol):
"""Protocol for :class:`Notification` and :class:`PreviewNotification`."""
+ preference_context: ClassVar[Any]
+ for_private_recipient: bool
+
type: str # noqa: A003
eventid: UUID
id: UUID # noqa: A003
@@ -279,6 +284,7 @@ class NotificationType(Generic[_D, _F], Protocol):
document_uuid: UUID
fragment: _F | None
fragment_uuid: UUID | None
+ created_at: datetime
created_by_id: int | None
created_by: Account | None
@@ -306,14 +312,22 @@ class Notification(NoIdMixin, Model, Generic[_D, _F]):
#: instance of a UserNotification per-event rather than per-notification
eventid: Mapped[UUID] = immutable(
sa_orm.mapped_column(
- postgresql.UUID, primary_key=True, nullable=False, default=uuid4
+ postgresql.UUID,
+ primary_key=True,
+ nullable=False,
+ insert_default=uuid4,
+ default=None,
)
)
#: Notification id
id: Mapped[UUID] = immutable( # noqa: A003
sa_orm.mapped_column(
- postgresql.UUID, primary_key=True, nullable=False, default=uuid4
+ postgresql.UUID,
+ primary_key=True,
+ nullable=False,
+ insert_default=uuid4,
+ default=None,
)
)
@@ -330,12 +344,12 @@ class Notification(NoIdMixin, Model, Generic[_D, _F]):
pref_type: ClassVar[str] = ''
#: Document model, must be specified in subclasses
- document_model: ClassVar[type[UuidModelUnion]]
+ document_model: ClassVar[type[ModelUuidProtocol]]
#: SQL table name for document type, auto-populated from the document model
document_type: ClassVar[str]
#: Fragment model, optional for subclasses
- fragment_model: ClassVar[type[UuidModelUnion] | None] = None
+ fragment_model: ClassVar[type[ModelUuidProtocol] | None] = None
#: SQL table name for fragment type, auto-populated from the fragment model
fragment_type: ClassVar[str | None]
@@ -355,13 +369,13 @@ class Notification(NoIdMixin, Model, Generic[_D, _F]):
preference_context: ClassVar[Any] = None
#: Notification type (identifier for subclass of :class:`NotificationType`)
- type_: Mapped[str] = immutable(
- sa_orm.mapped_column('type', sa.Unicode, nullable=False)
- )
+ type_: Mapped[str] = immutable(sa_orm.mapped_column('type'))
#: Id of user that triggered this notification
created_by_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('account.id', ondelete='SET NULL'), nullable=True
+ sa.ForeignKey('account.id', ondelete='SET NULL'),
+ default=None,
+ nullable=True,
)
#: User that triggered this notification. Optional, as not all notifications are
#: caused by user activity. Used to optionally exclude user from receiving
@@ -492,7 +506,7 @@ def __init_subclass__( # pylint: disable=arguments-differ
fragment_model = None
elif get_origin(fragment_model) is Optional:
fragment_model = get_args(fragment_model)[0]
- elif get_origin(fragment_model) is Union:
+ elif get_origin(fragment_model) in (Union, UnionType):
_union_args = get_args(fragment_model)
if len(_union_args) == 2 and _union_args[1] is NoneType:
fragment_model = _union_args[0]
@@ -553,7 +567,7 @@ def __init__(
# pylint: disable=isinstance-second-argument-not-valid-type
if not isinstance(fragment, self.fragment_model):
raise TypeError(f"{fragment!r} is not of type {self.fragment_model!r}")
- kwargs['fragment_uuid'] = fragment.uuid # type: ignore[union-attr]
+ kwargs['fragment_uuid'] = fragment.uuid
super().__init__(**kwargs)
@property
@@ -634,9 +648,9 @@ def allow_transport(cls, transport: str) -> bool:
return getattr(cls, 'allow_' + transport)
@property
- def role_provider_obj(self) -> _F | _D:
+ def role_provider_obj(self) -> ModelUuidProtocol:
"""Return fragment if exists, document otherwise, indicating role provider."""
- return cast(_F | _D, self.fragment or self.document)
+ return self.fragment or self.document # type: ignore[return-value] # FIXME
def dispatch(self) -> Generator[NotificationRecipient, None, None]:
"""
@@ -678,8 +692,8 @@ def dispatch(self) -> Generator[NotificationRecipient, None, None]:
# Since this query uses SQLAlchemy's session cache, we don't have to
# bother with a local cache for the first case.
- existing_notification = NotificationRecipient.query.get(
- (account.id, self.eventid)
+ existing_notification = db.session.get(
+ NotificationRecipient, (account.id, self.eventid)
)
if existing_notification is None:
recipient = NotificationRecipient(
@@ -709,11 +723,13 @@ class PreviewNotification(NotificationType):
)
"""
+ preference_context = None
+
def __init__( # pylint: disable=super-init-not-called
self,
cls: type[Notification],
- document: UuidModelUnion,
- fragment: UuidModelUnion | None = None,
+ document: ModelUuidProtocol,
+ fragment: ModelUuidProtocol | None = None,
user: Account | None = None,
) -> None:
self.eventid = uuid4()
@@ -721,12 +737,14 @@ def __init__( # pylint: disable=super-init-not-called
self.eventid_b58 = uuid_to_base58(self.eventid)
self.cls = cls
self.type = cls.cls_type
+ self.for_private_recipient = cls.for_private_recipient
self.document = document
self.document_uuid = document.uuid
self.fragment = fragment
self.fragment_uuid = fragment.uuid if fragment is not None else None
+ self.created_at = utcnow()
self.created_by = user
- self.created_by_id = cast(int, user.id) if user is not None else None
+ self.created_by_id = user.id if user is not None else None
def __getattr__(self, attr: str) -> Any:
"""Get an attribute."""
@@ -756,14 +774,14 @@ def notification_pref_type(self) -> str:
with_roles(notification_pref_type, read={'owner'})
@cached_property
- def document(self) -> UuidModelUnion | None:
+ def document(self) -> ModelUuidProtocol | None:
"""Document that this notification is for."""
return self.notification.document
with_roles(document, read={'owner'})
@cached_property
- def fragment(self) -> UuidModelUnion | None:
+ def fragment(self) -> ModelUuidProtocol | None:
"""Fragment within this document that this notification is for."""
return self.notification.fragment
@@ -784,7 +802,7 @@ def is_not_deleted(self, revoke: bool = False) -> bool:
pass
if revoke:
self.is_revoked = True
- # Do not set self.rollupid because this is not a rollup
+ # Do not set `self.rollupid` because this is not a rollup
return False
@@ -802,9 +820,9 @@ class NotificationRecipient(NoIdMixin, NotificationRecipientProtoMixin, Model):
#: Id of user being notified
recipient_id: Mapped[int] = immutable(
sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('account.id', ondelete='CASCADE'),
primary_key=True,
+ default=None,
nullable=False,
)
)
@@ -839,9 +857,7 @@ class NotificationRecipient(NoIdMixin, NotificationRecipientProtoMixin, Model):
#: Note: This column represents the first instance of a role shifting from being an
#: entirely in-app symbol (i.e., code refactorable) to being data in the database
#: (i.e., requiring a data migration alongside a code refactor)
- role: Mapped[str] = with_roles(
- immutable(sa_orm.mapped_column(sa.Unicode, nullable=False)), read={'owner'}
- )
+ role: Mapped[str] = with_roles(immutable(sa_orm.mapped_column()), read={'owner'})
#: Timestamp for when this notification was marked as read
read_at: Mapped[datetime | None] = with_roles(
@@ -866,23 +882,15 @@ class NotificationRecipient(NoIdMixin, NotificationRecipientProtoMixin, Model):
)
#: Message id for email delivery
- messageid_email: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, nullable=True
- )
+ messageid_email: Mapped[str | None] = sa_orm.mapped_column(default=None)
#: Message id for SMS delivery
- messageid_sms: Mapped[str | None] = sa_orm.mapped_column(sa.Unicode, nullable=True)
+ messageid_sms: Mapped[str | None] = sa_orm.mapped_column(default=None)
#: Message id for web push delivery
- messageid_webpush: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, nullable=True
- )
+ messageid_webpush: Mapped[str | None] = sa_orm.mapped_column(default=None)
#: Message id for Telegram delivery
- messageid_telegram: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, nullable=True
- )
+ messageid_telegram: Mapped[str | None] = sa_orm.mapped_column(default=None)
#: Message id for WhatsApp delivery
- messageid_whatsapp: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, nullable=True
- )
+ messageid_whatsapp: Mapped[str | None] = sa_orm.mapped_column(default=None)
__table_args__ = (
sa.ForeignKeyConstraint(
@@ -1154,7 +1162,7 @@ def rolledup_fragments(self) -> Query | None:
@classmethod
def get_for(cls, user: Account, eventid_b58: str) -> NotificationRecipient | None:
"""Retrieve a :class:`UserNotification` using SQLAlchemy session cache."""
- return cls.query.get((user.id, uuid_from_base58(eventid_b58)))
+ return db.session.get(cls, (user.id, uuid_from_base58(eventid_b58)))
@classmethod
def web_notifications_for(
@@ -1190,7 +1198,9 @@ def migrate_account(cls, old_account: Account, new_account: Account) -> None:
for notification_recipient in cls.query.filter_by(
recipient_id=old_account.id
).all():
- existing = cls.query.get((new_account.id, notification_recipient.eventid))
+ existing = db.session.get(
+ cls, (new_account.id, notification_recipient.eventid)
+ )
# TODO: Instead of dropping old_user's dupe notifications, check which of
# the two has a higher priority role and keep that. This may not be possible
# if the two copies are for different notifications under the same eventid.
@@ -1245,15 +1255,15 @@ def rolledup_fragments(self) -> Query | None:
# --- Notification preferences ---------------------------------------------------------
-class NotificationPreferences(BaseMixin, Model):
+class NotificationPreferences(BaseMixin[int, Account], Model):
"""Holds a user's preferences for a particular :class:`Notification` type."""
__tablename__ = 'notification_preferences'
#: Id of account whose preferences are represented here
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('account.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
index=True,
)
@@ -1266,25 +1276,13 @@ class NotificationPreferences(BaseMixin, Model):
# Notification type, corresponding to Notification.type (a class attribute there)
# notification_type = '' holds the veto switch to disable a transport entirely
- notification_type: Mapped[str] = immutable(
- sa_orm.mapped_column(sa.Unicode, nullable=False)
- )
+ notification_type: Mapped[str] = immutable(sa_orm.mapped_column())
- by_email: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False), rw={'owner'}
- )
- by_sms: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False), rw={'owner'}
- )
- by_webpush: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False), rw={'owner'}
- )
- by_telegram: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False), rw={'owner'}
- )
- by_whatsapp: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, nullable=False), rw={'owner'}
- )
+ by_email: Mapped[bool] = with_roles(sa_orm.mapped_column(), rw={'owner'})
+ by_sms: Mapped[bool] = with_roles(sa_orm.mapped_column(), rw={'owner'})
+ by_webpush: Mapped[bool] = with_roles(sa_orm.mapped_column(), rw={'owner'})
+ by_telegram: Mapped[bool] = with_roles(sa_orm.mapped_column(), rw={'owner'})
+ by_whatsapp: Mapped[bool] = with_roles(sa_orm.mapped_column(), rw={'owner'})
__table_args__ = (sa.UniqueConstraint('account_id', 'notification_type'),)
diff --git a/funnel/models/notification_types.py b/funnel/models/notification_types.py
index ca11d5a04..3ba50ba9b 100644
--- a/funnel/models/notification_types.py
+++ b/funnel/models/notification_types.py
@@ -2,8 +2,6 @@
from __future__ import annotations
-from typing import Optional
-
from baseframe import __
from .account import Account
@@ -150,7 +148,7 @@ class ProposalSubmittedNotification(
class ProjectStartingNotification(
DocumentHasAccount,
- Notification[Project, Optional[Session]],
+ Notification[Project, Session | None],
type='project_starting',
):
"""Notification of a session about to start."""
diff --git a/funnel/models/phone_number.py b/funnel/models/phone_number.py
index 2eacfb36e..619faa288 100644
--- a/funnel/models/phone_number.py
+++ b/funnel/models/phone_number.py
@@ -235,7 +235,7 @@ def phone_blake2b160_hash(
# --- Models ---------------------------------------------------------------------------
-class PhoneNumber(BaseMixin, Model):
+class PhoneNumber(BaseMixin[int, 'Account'], Model):
"""
Represents a phone number as a standalone entity, with associated metadata.
@@ -263,9 +263,7 @@ class PhoneNumber(BaseMixin, Model):
#: The phone number, centrepiece of this model. Stored normalized in E164 format.
#: Validated by the :func:`_validate_phone` event handler
- number: Mapped[str | None] = sa_orm.mapped_column(
- sa.Unicode, nullable=True, unique=True
- )
+ number: Mapped[str | None] = sa_orm.mapped_column(unique=True)
#: BLAKE2b 160-bit hash of :attr:`phone`. Kept permanently even if phone is
#: removed. SQLAlchemy type LargeBinary maps to PostgreSQL BYTEA. Despite the name,
@@ -287,13 +285,13 @@ class PhoneNumber(BaseMixin, Model):
# device, we record distinct timestamps for last sent, delivery and failure.
#: Cached state for whether this phone number is known to have SMS support
- has_sms: Mapped[bool | None] = sa_orm.mapped_column(sa.Boolean, nullable=True)
+ has_sms: Mapped[bool | None] = sa_orm.mapped_column()
#: Timestamp at which this number was determined to be valid/invalid for SMS
has_sms_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
)
#: Cached state for whether this phone number is known to be on WhatsApp or not
- has_wa: Mapped[bool | None] = sa_orm.mapped_column(sa.Boolean, nullable=True)
+ has_wa: Mapped[bool | None] = sa_orm.mapped_column()
#: Timestamp at which this number was tested for availability on WhatsApp
has_wa_at: Mapped[datetime | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
@@ -768,8 +766,8 @@ class OptionalPhoneNumberMixin:
def phone_number_id(cls) -> Mapped[int | None]:
"""Foreign key to phone_number table."""
return sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('phone_number.id', ondelete='SET NULL'),
+ default=None,
nullable=cls.__phone_optional__,
unique=cls.__phone_unique__,
index=not cls.__phone_unique__,
diff --git a/funnel/models/project.py b/funnel/models/project.py
index a77422729..5067b2694 100644
--- a/funnel/models/project.py
+++ b/funnel/models/project.py
@@ -6,6 +6,7 @@
from collections import OrderedDict, defaultdict
from collections.abc import Sequence
from datetime import datetime, timedelta
+from enum import ReprEnum
from typing import TYPE_CHECKING, Any, Literal, Self, cast, overload
from flask_babel import format_date, get_locale
@@ -44,17 +45,17 @@
types,
)
from .account import Account
-from .comment import SET_TYPE, Commentset
from .helpers import (
RESERVED_NAMES,
ImgeeType,
+ IntTitle,
MarkdownCompositeDocument,
add_search_trigger,
valid_name,
visual_field_delimiter,
)
-__all__ = ['PROJECT_RSVP_STATE', 'Project', 'ProjectLocation', 'ProjectRedirect']
+__all__ = ['ProjectRsvpStateEnum', 'Project', 'ProjectLocation', 'ProjectRedirect']
# --- Constants ---------------------------------------------------------------
@@ -76,25 +77,25 @@ class CFP_STATE(LabeledEnum): # noqa: N801
ANY = {NONE, PUBLIC, CLOSED}
-class PROJECT_RSVP_STATE(LabeledEnum): # noqa: N801
- NONE = (1, __("Not accepting registrations"))
- ALL = (2, __("Anyone can register"))
- MEMBERS = (3, __("Only members can register"))
+class ProjectRsvpStateEnum(IntTitle, ReprEnum):
+ NONE = 1, __("Not accepting registrations")
+ ALL = 2, __("Anyone can register")
+ MEMBERS = 3, __("Only members can register")
# --- Models ------------------------------------------------------------------
-class Project(UuidMixin, BaseScopedNameMixin, Model):
+class Project(UuidMixin, BaseScopedNameMixin[int, Account], Model):
__tablename__ = 'project'
reserved_names = RESERVED_NAMES
created_by_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
created_by: Mapped[Account] = relationship(foreign_keys=[created_by_id])
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
account: Mapped[Account] = with_roles(
relationship(foreign_keys=[account_id], back_populates='projects'),
@@ -141,15 +142,19 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
datasets={'primary', 'without_parent'},
)
timezone: Mapped[BaseTzInfo] = with_roles(
- sa_orm.mapped_column(TimezoneType(backend='pytz'), nullable=False, default=utc),
+ sa_orm.mapped_column(
+ TimezoneType(backend='pytz'),
+ nullable=False,
+ insert_default=utc,
+ default=None,
+ ),
read={'all'},
datasets={'primary', 'without_parent', 'related'},
)
_state: Mapped[int] = sa_orm.mapped_column(
'state',
- sa.Integer,
- StateManager.check_constraint('state', PROJECT_STATE),
+ StateManager.check_constraint('state', PROJECT_STATE, sa.Integer),
default=PROJECT_STATE.DRAFT,
nullable=False,
index=True,
@@ -160,8 +165,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
)
_cfp_state: Mapped[int] = sa_orm.mapped_column(
'cfp_state',
- sa.Integer,
- StateManager.check_constraint('cfp_state', CFP_STATE),
+ StateManager.check_constraint('cfp_state', CFP_STATE, sa.Integer),
default=CFP_STATE.NONE,
nullable=False,
index=True,
@@ -174,8 +178,10 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
rsvp_state: Mapped[int] = with_roles(
sa_orm.mapped_column(
sa.SmallInteger,
- StateManager.check_constraint('rsvp_state', PROJECT_RSVP_STATE),
- default=PROJECT_RSVP_STATE.NONE,
+ StateManager.check_constraint(
+ 'rsvp_state', ProjectRsvpStateEnum, sa.SmallInteger
+ ),
+ default=ProjectRsvpStateEnum.NONE,
nullable=False,
),
read={'all'},
@@ -251,11 +257,11 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
sa_orm.mapped_column(UrlType, nullable=True), read={'all'}
)
hasjob_embed_limit: Mapped[int | None] = with_roles(
- sa_orm.mapped_column(sa.Integer, default=8, nullable=True), read={'all'}
+ sa_orm.mapped_column(default=8, nullable=True), read={'all'}
)
commentset_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('commentset.id'), nullable=False
+ sa.ForeignKey('commentset.id'), default=None, nullable=False
)
commentset: Mapped[Commentset] = relationship(
uselist=False, single_parent=True, back_populates='project'
@@ -264,6 +270,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
parent_project_id: Mapped[int | None] = sa_orm.mapped_column(
'parent_id', # TODO: Migration required
sa.ForeignKey('project.id', ondelete='SET NULL'),
+ default=None,
nullable=True,
)
parent_project: Mapped[Project | None] = relationship(
@@ -274,7 +281,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
#: Featured project flag. This can only be set by website editors, not
#: project editors or account admins.
site_featured: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, default=False, nullable=False),
+ sa_orm.mapped_column(default=False),
read={'all'},
write={'site_editor'},
datasets={'primary', 'without_parent'},
@@ -285,21 +292,20 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
sa.ARRAY(sa.UnicodeText, dimensions=1),
nullable=True, # For legacy data
server_default=sa.text("'{}'::text[]"),
+ default=None,
),
read={'all'},
datasets={'primary', 'without_parent'},
)
is_restricted_video: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, default=False, nullable=False),
+ sa_orm.mapped_column(default=False),
read={'all'},
datasets={'primary', 'without_parent'},
)
#: Revision number maintained by SQLAlchemy, used for vCal files, starting at 1
- revisionid: Mapped[int] = with_roles(
- sa_orm.mapped_column(sa.Integer, nullable=False), read={'all'}
- )
+ revisionid: Mapped[int] = with_roles(sa_orm.mapped_column(), read={'all'})
search_vector: Mapped[str] = sa_orm.mapped_column(
TSVectorType(
@@ -357,7 +363,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
lazy='dynamic',
primaryjoin=lambda: sa.and_(
ProjectMembership.project_id == Project.id,
- ProjectMembership.is_active,
+ ProjectMembership.is_active, # type: ignore[has-type] # FIXME
),
viewonly=True,
),
@@ -368,7 +374,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
lazy='dynamic',
primaryjoin=lambda: sa.and_(
ProjectMembership.project_id == Project.id,
- ProjectMembership.is_active,
+ ProjectMembership.is_active, # type: ignore[has-type] # FIXME
ProjectMembership.is_editor.is_(True),
),
viewonly=True,
@@ -378,7 +384,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
lazy='dynamic',
primaryjoin=lambda: sa.and_(
ProjectMembership.project_id == Project.id,
- ProjectMembership.is_active,
+ ProjectMembership.is_active, # type: ignore[has-type] # FIXME
ProjectMembership.is_promoter.is_(True),
),
viewonly=True,
@@ -388,7 +394,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
lazy='dynamic',
primaryjoin=lambda: sa.and_(
ProjectMembership.project_id == Project.id,
- ProjectMembership.is_active,
+ ProjectMembership.is_active, # type: ignore[has-type] # FIXME
ProjectMembership.is_usher.is_(True),
),
viewonly=True,
@@ -437,7 +443,7 @@ class Project(UuidMixin, BaseScopedNameMixin, Model):
lazy='dynamic',
primaryjoin=lambda: sa.and_(
ProjectSponsorMembership.project_id == Project.id,
- ProjectSponsorMembership.is_active,
+ ProjectSponsorMembership.is_active, # type: ignore[has-type] # FIXME
),
order_by=lambda: ProjectSponsorMembership.seq,
viewonly=True,
@@ -895,7 +901,7 @@ def end_at_localized(self):
@hybrid_property
def allow_rsvp(self) -> bool:
"""RSVP state as a boolean value (allowed for all or not)."""
- return self.rsvp_state == PROJECT_RSVP_STATE.ALL
+ return self.rsvp_state == ProjectRsvpStateEnum.ALL
@property
def active_rsvps(self) -> Query[Rsvp]:
@@ -912,16 +918,12 @@ def rsvp_for(
...
def rsvp_for(self, account: Account | None, create=False) -> Rsvp | None:
- return Rsvp.get_for(cast(Project, self), account, create)
+ return Rsvp.get_for(self, account, create)
- def rsvps_with(self, status: str):
- return (
- cast(Project, self)
- .rsvps.join(Account)
- .filter(
- Account.state.ACTIVE,
- Rsvp._state == status, # pylint: disable=protected-access
- )
+ def rsvps_with(self, state: RsvpStateEnum) -> Query[Rsvp]:
+ return self.rsvps.join(Account).filter(
+ Account.state.ACTIVE,
+ Rsvp._state == state, # pylint: disable=protected-access
)
def rsvp_counts(self) -> dict[str, int]:
@@ -940,8 +942,7 @@ def rsvp_counts(self) -> dict[str, int]:
@cached_property
def rsvp_count_going(self) -> int:
return (
- cast(Project, self)
- .rsvps.join(Account)
+ self.rsvps.join(Account)
.filter(Account.state.ACTIVE, Rsvp.state.YES)
.count()
)
@@ -1459,7 +1460,7 @@ class ProjectRedirect(TimestampMixin, Model):
__tablename__ = 'project_redirect'
account_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False, primary_key=True
+ sa.ForeignKey('account.id'), default=None, nullable=False, primary_key=True
)
account: Mapped[Account] = relationship(back_populates='project_redirects')
parent: Mapped[Account] = sa_orm.synonym('account')
@@ -1468,7 +1469,7 @@ class ProjectRedirect(TimestampMixin, Model):
)
project_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id', ondelete='SET NULL'), nullable=True
+ sa.ForeignKey('project.id', ondelete='SET NULL'), default=None, nullable=True
)
project: Mapped[Project | None] = relationship(back_populates='redirects')
@@ -1508,7 +1509,7 @@ def add(
account = project.account
if name is None:
name = project.name
- redirect = cls.query.get((account.id, name))
+ redirect = db.session.get(cls, (account.id, name))
if redirect is None:
redirect = cls(account=account, name=name, project=project)
db.session.add(redirect)
@@ -1539,16 +1540,14 @@ class ProjectLocation(TimestampMixin, Model):
__tablename__ = 'project_location'
#: Project we are tagging
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), primary_key=True, nullable=False
+ sa.ForeignKey('project.id'), default=None, primary_key=True, nullable=False
)
project: Mapped[Project] = relationship(back_populates='locations')
#: Geonameid for this project
geonameid: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, primary_key=True, nullable=False, index=True
- )
- primary: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=True, nullable=False
+ primary_key=True, nullable=False, index=True
)
+ primary: Mapped[bool] = sa_orm.mapped_column(default=True, nullable=False)
def __repr__(self) -> str:
"""Represent :class:`ProjectLocation` as a string."""
@@ -1562,7 +1561,7 @@ def __repr__(self) -> str:
from .label import Label
from .project_membership import ProjectMembership
from .proposal import Proposal
-from .rsvp import Rsvp
+from .rsvp import Rsvp, RsvpStateEnum
from .session import Session
from .sponsor_membership import ProjectSponsorMembership
from .update import Update
@@ -1641,3 +1640,7 @@ def __repr__(self) -> str:
# joined model, not the first
grants_via={Rsvp.participant: {'participant', 'project_participant'}},
)
+
+
+# Tail imports
+from .comment import SET_TYPE, Commentset
diff --git a/funnel/models/project_membership.py b/funnel/models/project_membership.py
index 897b2e1cc..60a146bc6 100644
--- a/funnel/models/project_membership.py
+++ b/funnel/models/project_membership.py
@@ -7,7 +7,7 @@
from coaster.sqlalchemy import immutable, with_roles
from . import Mapped, Model, declared_attr, relationship, sa, sa_orm
-from .membership_mixin import ImmutableUserMembershipMixin
+from .membership_mixin import ImmutableMembershipMixin
from .project import Project
__all__ = ['ProjectMembership', 'project_child_role_map', 'project_child_role_set']
@@ -35,7 +35,7 @@
)
-class ProjectMembership(ImmutableUserMembershipMixin, Model):
+class ProjectMembership(ImmutableMembershipMixin, Model):
"""Users can be crew members of projects, with specified access rights."""
__tablename__ = 'project_membership'
@@ -60,7 +60,7 @@ class ProjectMembership(ImmutableUserMembershipMixin, Model):
},
'project_crew': {
'read': {
- 'record_type_label',
+ 'record_type_enum',
'granted_at',
'granted_by',
'revoked_at',
@@ -104,7 +104,7 @@ class ProjectMembership(ImmutableUserMembershipMixin, Model):
}
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id', ondelete='CASCADE'), nullable=False
+ sa.ForeignKey('project.id', ondelete='CASCADE'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='crew_memberships'),
@@ -117,35 +117,27 @@ class ProjectMembership(ImmutableUserMembershipMixin, Model):
# Project crew roles (at least one must be True):
#: Editors can edit all common and editorial details of an event
- is_editor: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_editor: Mapped[bool] = sa_orm.mapped_column(default=False)
#: Promoters are responsible for promotion and have write access
#: to common details plus read access to everything else. Unlike
#: editors, they cannot edit the schedule
- is_promoter: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_promoter: Mapped[bool] = sa_orm.mapped_column(default=False)
#: Ushers help participants find their way around an event and have
#: the ability to scan badges at the door
- is_usher: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_usher: Mapped[bool] = sa_orm.mapped_column(default=False)
#: Optional label, indicating the member's role in the project
label: Mapped[str | None] = immutable(
sa_orm.mapped_column(
- sa.Unicode,
sa.CheckConstraint(
"label <> ''", name='project_crew_membership_label_check'
- ),
- nullable=True,
+ )
)
)
@declared_attr.directive
@classmethod
- def __table_args__(cls) -> tuple:
+ def __table_args__(cls) -> tuple: # type: ignore[override]
"""Table arguments."""
try:
args = list(super().__table_args__)
diff --git a/funnel/models/proposal.py b/funnel/models/proposal.py
index 071c7fc54..ba1f8374c 100644
--- a/funnel/models/proposal.py
+++ b/funnel/models/proposal.py
@@ -33,7 +33,6 @@
sa_orm,
)
from .account import Account
-from .comment import SET_TYPE, Commentset
from .helpers import (
MarkdownCompositeDocument,
add_search_trigger,
@@ -42,7 +41,7 @@
from .label import Label, ProposalLabelProxy, proposal_label
from .project import Project
from .project_membership import project_child_role_map
-from .reorder_mixin import ReorderProtoMixin
+from .reorder_mixin import ReorderMixin
from .video_mixin import VideoMixin
__all__ = ['PROPOSAL_STATE', 'Proposal', 'ProposalSuuidRedirect']
@@ -127,20 +126,18 @@ class PROPOSAL_STATE(LabeledEnum): # noqa: N801
# --- Models ------------------------------------------------------------------
-class Proposal( # type: ignore[misc]
- UuidMixin, BaseScopedIdNameMixin, VideoMixin, ReorderProtoMixin, Model
-):
+class Proposal(UuidMixin, BaseScopedIdNameMixin, VideoMixin, ReorderMixin, Model):
__tablename__ = 'proposal'
created_by_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False
+ sa.ForeignKey('account.id'), default=None, nullable=False
)
created_by: Mapped[Account] = with_roles(
relationship(back_populates='created_proposals'),
grants={'creator', 'participant'},
)
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='proposals'),
@@ -165,8 +162,7 @@ class Proposal( # type: ignore[misc]
_state: Mapped[int] = sa_orm.mapped_column(
'state',
- sa.Integer,
- StateManager.check_constraint('state', PROPOSAL_STATE),
+ StateManager.check_constraint('state', PROPOSAL_STATE, sa.Integer),
default=PROPOSAL_STATE.SUBMITTED,
nullable=False,
)
@@ -175,7 +171,7 @@ class Proposal( # type: ignore[misc]
)
commentset_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('commentset.id'), nullable=False
+ sa.ForeignKey('commentset.id'), default=None, nullable=False
)
commentset: Mapped[Commentset] = relationship(
uselist=False, lazy='joined', single_parent=True, back_populates='proposal'
@@ -184,27 +180,17 @@ class Proposal( # type: ignore[misc]
body, body_text, body_html = MarkdownCompositeDocument.create(
'body', nullable=False, default=''
)
- description: Mapped[str] = sa_orm.mapped_column(
- sa.Unicode, nullable=False, default=''
- )
- custom_description: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
- template: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
- featured: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ description: Mapped[str] = sa_orm.mapped_column(default='')
+ custom_description: Mapped[bool] = sa_orm.mapped_column(default=False)
+ template: Mapped[bool] = sa_orm.mapped_column(default=False)
+ featured: Mapped[bool] = sa_orm.mapped_column(default=False)
edited_at: Mapped[datetime_type | None] = sa_orm.mapped_column(
sa.TIMESTAMP(timezone=True), nullable=True
)
#: Revision number maintained by SQLAlchemy, starting at 1
- revisionid: Mapped[int] = with_roles(
- sa_orm.mapped_column(sa.Integer, nullable=False), read={'all'}
- )
+ revisionid: Mapped[int] = with_roles(sa_orm.mapped_column(), read={'all'})
search_vector: Mapped[str] = sa_orm.mapped_column(
TSVectorType(
@@ -245,7 +231,7 @@ class Proposal( # type: ignore[misc]
relationship(
primaryjoin=lambda: sa.and_(
ProposalMembership.proposal_id == Proposal.id,
- ProposalMembership.is_active,
+ ProposalMembership.is_active, # type: ignore[has-type] # FIXME
),
order_by=lambda: ProposalMembership.seq,
viewonly=True,
@@ -267,7 +253,7 @@ class Proposal( # type: ignore[misc]
lazy='dynamic',
primaryjoin=lambda: sa.and_(
ProposalSponsorMembership.proposal_id == Proposal.id,
- ProposalSponsorMembership.is_active,
+ ProposalSponsorMembership.is_active, # type: ignore[has-type] # FIXME
),
order_by=lambda: ProposalSponsorMembership.seq,
viewonly=True,
@@ -514,8 +500,9 @@ def first_user(self) -> Account:
def move_to(self, project: Project) -> None:
"""Move to a new project and reset :attr:`url_id`."""
self.project = project
- self.url_id = None # pylint: disable=attribute-defined-outside-init
- self.make_id()
+ # pylint: disable=attribute-defined-outside-init
+ self.url_id = None
+ self.make_scoped_id()
def update_description(self) -> None:
if not self.custom_description:
@@ -579,7 +566,7 @@ def get( # type: ignore[override] # pylint: disable=arguments-differ
add_search_trigger(Proposal, 'search_vector')
-class ProposalSuuidRedirect(BaseMixin, Model):
+class ProposalSuuidRedirect(BaseMixin[int, Account], Model):
"""Holds Proposal SUUIDs from before when they were deprecated."""
__tablename__ = 'proposal_suuid_redirect'
@@ -588,12 +575,13 @@ class ProposalSuuidRedirect(BaseMixin, Model):
sa.Unicode(22), nullable=False, index=True
)
proposal_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('proposal.id', ondelete='CASCADE'), nullable=False
+ sa.ForeignKey('proposal.id', ondelete='CASCADE'), default=None, nullable=False
)
proposal: Mapped[Proposal] = relationship()
# Tail imports
+from .comment import SET_TYPE, Commentset
from .proposal_membership import ProposalMembership
from .sponsor_membership import ProposalSponsorMembership
diff --git a/funnel/models/proposal_membership.py b/funnel/models/proposal_membership.py
index cfa141ce7..3d01df05b 100644
--- a/funnel/models/proposal_membership.py
+++ b/funnel/models/proposal_membership.py
@@ -9,22 +9,13 @@
from coaster.sqlalchemy import immutable, with_roles
from . import Mapped, Model, relationship, sa, sa_orm
-from .membership_mixin import (
- FrozenAttributionProtoMixin,
- ImmutableUserMembershipMixin,
- ReorderMembershipProtoMixin,
-)
+from .membership_mixin import FrozenAttributionMixin, ReorderMembershipMixin
from .proposal import Proposal
__all__ = ['ProposalMembership']
-class ProposalMembership( # type: ignore[misc]
- ImmutableUserMembershipMixin,
- FrozenAttributionProtoMixin,
- ReorderMembershipProtoMixin,
- Model,
-):
+class ProposalMembership(FrozenAttributionMixin, ReorderMembershipMixin, Model):
"""Users can be presenters or reviewers on proposals."""
__tablename__ = 'proposal_membership'
@@ -78,8 +69,8 @@ class ProposalMembership( # type: ignore[misc]
proposal_id: Mapped[int] = with_roles(
sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('proposal.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
),
read={'member', 'editor'},
@@ -97,16 +88,12 @@ class ProposalMembership( # type: ignore[misc]
#: Uncredited members are not listed in the main display, but can edit and may be
#: listed in a details section. Uncredited memberships are for support roles such
#: as copy editors.
- is_uncredited: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_uncredited: Mapped[bool] = sa_orm.mapped_column(default=False)
#: Optional label, indicating the member's role on the proposal
label: Mapped[str | None] = immutable(
sa_orm.mapped_column(
- sa.Unicode,
- sa.CheckConstraint("label <> ''", name='proposal_membership_label_check'),
- nullable=True,
+ sa.CheckConstraint("label <> ''", name='proposal_membership_label_check')
)
)
diff --git a/funnel/models/reorder_mixin.py b/funnel/models/reorder_mixin.py
index a77720ecc..055554cf6 100644
--- a/funnel/models/reorder_mixin.py
+++ b/funnel/models/reorder_mixin.py
@@ -2,42 +2,43 @@
from __future__ import annotations
-from datetime import datetime
-from typing import TYPE_CHECKING, ClassVar, TypeVar
-from uuid import UUID
+from typing import Any, Protocol, TypeVar
-from . import Mapped, QueryProperty, db, declarative_mixin, sa, sa_orm
+from . import Mapped, db, declarative_mixin, sa, sa_orm
+from .typing import ModelIdProtocol
-__all__ = ['ReorderProtoMixin']
+__all__ = ['ReorderMixin']
# Use of TypeVar for subclasses of ReorderMixin as defined in these mypy tickets:
# https://github.com/python/mypy/issues/1212
# https://github.com/python/mypy/issues/7191
-Reorderable = TypeVar('Reorderable', bound='ReorderProtoMixin')
+
+
+class ReorderSubclassProtocol(ModelIdProtocol, Protocol):
+ parent_id: Mapped[Any]
+ parent: Mapped[Any]
+ seq: Mapped[Any]
+
+ @property
+ def parent_scoped_reorder_query_filter(self) -> sa.ColumnElement[bool]:
+ ...
+
+ def reorder_item(self: Reorderable, other: Reorderable, before: bool) -> None:
+ ...
+
+
+Reorderable = TypeVar('Reorderable', bound='ReorderSubclassProtocol')
@declarative_mixin
-class ReorderProtoMixin:
+class ReorderMixin:
"""Adds support for re-ordering sequences within a parent container."""
- if TYPE_CHECKING:
- #: Subclasses must have a created_at column
- created_at: Mapped[datetime]
- #: Subclass must have a primary key that is int or uuid
- id: Mapped[int | UUID] # noqa: A001
- #: Subclass must declare a parent_id synonym to the parent model fkey column
- parent_id: Mapped[int | UUID]
- #: Subclass must declare a seq column or synonym, holding a sequence id. It
- #: need not be unique, but reordering is meaningless when both items have the
- #: same number
- seq: Mapped[int]
-
- #: Subclass must offer a SQLAlchemy query (this is standard from base classes)
- query: ClassVar[QueryProperty]
-
@property
- def parent_scoped_reorder_query_filter(self: Reorderable) -> sa.ColumnElement[bool]:
+ def parent_scoped_reorder_query_filter(
+ self: ReorderSubclassProtocol,
+ ) -> sa.ColumnElement[bool]:
"""
Return a query filter that includes a scope limitation to the parent.
@@ -83,13 +84,13 @@ def reorder_item(self: Reorderable, other: Reorderable, before: bool) -> None:
)
.populate_existing() # Force reload `.seq` into session cache
.with_for_update(of=cls) # Lock these rows to prevent a parallel update
- .options(sa_orm.load_only(cls.id, cls.seq))
+ .options(sa_orm.load_only(cls.id_, cls.seq))
.order_by(*order_columns)
.all()
)
# Pop-off items that share a sequence number and don't need to be moved
- while items_to_reorder[0].id != self.id:
+ while items_to_reorder[0].id_ != self.id_:
items_to_reorder.pop(0)
# Reordering! Move down the list (reversed if `before`), reassigning numbers.
@@ -101,7 +102,7 @@ def reorder_item(self: Reorderable, other: Reorderable, before: bool) -> None:
new_seq_number = self.seq
# Temporarily give self an out-of-bounds number
- self.seq = (
+ self.seq: Mapped[int] = (
sa.select(sa.func.coalesce(sa.func.max(cls.seq) + 1, 1))
.where(self.parent_scoped_reorder_query_filter)
.scalar_subquery()
@@ -115,7 +116,7 @@ def reorder_item(self: Reorderable, other: Reorderable, before: bool) -> None:
# of SQLAlchemy 2.0.x. Should that behaviour change, a switch to
# bulk_update_mappings will be required
db.session.flush()
- if reorderable_item.id == other.id:
+ if reorderable_item.id_ == other.id_:
# Don't bother reordering anything after `other`
break
# Assign other's previous sequence number to self
diff --git a/funnel/models/rsvp.py b/funnel/models/rsvp.py
index 16fa3181d..e8a9394e9 100644
--- a/funnel/models/rsvp.py
+++ b/funnel/models/rsvp.py
@@ -2,20 +2,33 @@
from __future__ import annotations
-from typing import Literal, Self, overload
+from dataclasses import dataclass
+from enum import ReprEnum
+from typing import TYPE_CHECKING, Any, Literal, Self, overload
from flask import current_app
from baseframe import __
from coaster.sqlalchemy import StateManager, with_roles
-from coaster.utils import LabeledEnum
-
-from . import Mapped, Model, NoIdMixin, UuidMixin, db, relationship, sa, sa_orm, types
+from coaster.utils import DataclassFromType, LabeledEnum
+
+from . import (
+ Mapped,
+ Model,
+ NoIdMixin,
+ UuidMixin,
+ db,
+ declared_attr,
+ relationship,
+ sa,
+ sa_orm,
+ types,
+)
from .account import Account, AccountEmail, AccountEmailClaim, AccountPhone
from .project import Project
from .project_membership import project_child_role_map
-__all__ = ['Rsvp', 'RSVP_STATUS']
+__all__ = ['RSVP_STATUS', 'RsvpStateEnum', 'Rsvp']
class RSVP_STATUS(LabeledEnum): # noqa: N801
@@ -27,10 +40,26 @@ class RSVP_STATUS(LabeledEnum): # noqa: N801
AWAITING = ('A', 'awaiting', __("Awaiting"))
+@dataclass(frozen=True)
+class _RsvpOptions(DataclassFromType, str):
+ """RSVP options."""
+
+ # The empty default is required for Mypy's enum plugin's `Enum.__call__` analysis
+ response: str = ''
+ label: str = ''
+
+
+class RsvpStateEnum(_RsvpOptions, ReprEnum):
+ YES = 'Y', __("Yes"), __("Going")
+ NO = 'N', __("No"), __("Not going")
+ MAYBE = 'M', __("Maybe"), __("Maybe")
+ AWAITING = 'A', __("Invite"), __("Awaiting")
+
+
class Rsvp(UuidMixin, NoIdMixin, Model):
__tablename__ = 'rsvp'
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False, primary_key=True
+ sa.ForeignKey('project.id'), default=None, nullable=False, primary_key=True
)
project: Mapped[Project] = with_roles(
relationship(back_populates='rsvps'),
@@ -39,7 +68,7 @@ class Rsvp(UuidMixin, NoIdMixin, Model):
datasets={'primary'},
)
participant_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False, primary_key=True
+ sa.ForeignKey('account.id'), default=None, nullable=False, primary_key=True
)
participant: Mapped[Account] = with_roles(
relationship(back_populates='rsvps'),
@@ -57,8 +86,8 @@ class Rsvp(UuidMixin, NoIdMixin, Model):
_state: Mapped[str] = sa_orm.mapped_column(
'state',
sa.CHAR(1),
- StateManager.check_constraint('state', RSVP_STATUS),
- default=RSVP_STATUS.AWAITING,
+ StateManager.check_constraint('state', RsvpStateEnum, sa.CHAR(1)),
+ default=RsvpStateEnum.AWAITING,
nullable=False,
)
state = with_roles(
@@ -66,6 +95,9 @@ class Rsvp(UuidMixin, NoIdMixin, Model):
call={'owner', 'project_promoter'},
)
+ if TYPE_CHECKING:
+ id_: declared_attr[Any] # Fake entry for compatibility with ModelUuidProtocol
+
__roles__ = {
'owner': {'read': {'created_at', 'updated_at'}},
'project_promoter': {'read': {'created_at', 'updated_at'}},
@@ -179,7 +211,7 @@ def get_for(
cls, project: Project, account: Account | None, create: bool = False
) -> Self | None:
if account is not None:
- result = cls.query.get((project.id, account.id))
+ result = db.session.get(cls, (project.id, account.id))
if not result and create:
result = cls(project=project, participant=account)
db.session.add(result)
diff --git a/funnel/models/saved.py b/funnel/models/saved.py
index 4fb250b80..4595b3060 100644
--- a/funnel/models/saved.py
+++ b/funnel/models/saved.py
@@ -19,6 +19,7 @@ class SavedProject(NoIdMixin, Model):
#: User account that saved this project
account_id: Mapped[int] = sa_orm.mapped_column(
sa.ForeignKey('account.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
primary_key=True,
)
@@ -27,8 +28,8 @@ class SavedProject(NoIdMixin, Model):
)
#: Project that was saved
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('project.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
primary_key=True,
index=True,
@@ -36,7 +37,10 @@ class SavedProject(NoIdMixin, Model):
project: Mapped[Project] = relationship(back_populates='saves')
#: Timestamp when the save happened
saved_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
+ sa.TIMESTAMP(timezone=True),
+ nullable=False,
+ insert_default=sa.func.utcnow(),
+ default=None,
)
#: User's plaintext note to self on why they saved this (optional)
description: Mapped[str | None] = sa_orm.mapped_column(
@@ -60,14 +64,15 @@ class SavedSession(NoIdMixin, Model):
#: User account that saved this session
account_id: Mapped[int] = sa_orm.mapped_column(
sa.ForeignKey('account.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
primary_key=True,
)
account: Mapped[Account] = relationship(back_populates='saved_sessions')
#: Session that was saved
session_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer,
sa.ForeignKey('session.id', ondelete='CASCADE'),
+ default=None,
nullable=False,
primary_key=True,
index=True,
@@ -75,7 +80,10 @@ class SavedSession(NoIdMixin, Model):
session: Mapped[Session] = relationship(back_populates='saves')
#: Timestamp when the save happened
saved_at: Mapped[datetime] = sa_orm.mapped_column(
- sa.TIMESTAMP(timezone=True), nullable=False, default=sa.func.utcnow()
+ sa.TIMESTAMP(timezone=True),
+ nullable=False,
+ insert_default=sa.func.utcnow(),
+ default=None,
)
#: User's plaintext note to self on why they saved this (optional)
description: Mapped[str | None] = sa_orm.mapped_column(
diff --git a/funnel/models/session.py b/funnel/models/session.py
index 60013938d..6ed90e24c 100644
--- a/funnel/models/session.py
+++ b/funnel/models/session.py
@@ -39,11 +39,11 @@
__all__ = ['Session']
-class Session(UuidMixin, BaseScopedIdNameMixin, VideoMixin, Model):
+class Session(UuidMixin, BaseScopedIdNameMixin[int, Account], VideoMixin, Model):
__tablename__ = 'session'
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='sessions'),
@@ -54,7 +54,7 @@ class Session(UuidMixin, BaseScopedIdNameMixin, VideoMixin, Model):
'description', default='', nullable=False
)
proposal_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('proposal.id'), nullable=True, unique=True
+ sa.ForeignKey('proposal.id'), default=None, nullable=True, unique=True
)
proposal: Mapped[Proposal | None] = relationship(back_populates='session')
speaker: Mapped[str | None] = sa_orm.mapped_column(
@@ -67,26 +67,18 @@ class Session(UuidMixin, BaseScopedIdNameMixin, VideoMixin, Model):
sa.TIMESTAMP(timezone=True), nullable=True, index=True
)
venue_room_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('venue_room.id'), nullable=True
+ sa.ForeignKey('venue_room.id'), default=None, nullable=True
)
venue_room: Mapped[VenueRoom | None] = relationship(back_populates='sessions')
- is_break: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=False, nullable=False
- )
- featured: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=False, nullable=False
- )
- is_restricted_video: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=False, nullable=False
- )
+ is_break: Mapped[bool] = sa_orm.mapped_column(default=False)
+ featured: Mapped[bool] = sa_orm.mapped_column(default=False)
+ is_restricted_video: Mapped[bool] = sa_orm.mapped_column(default=False)
banner_image_url: Mapped[str | None] = sa_orm.mapped_column(
ImgeeType, nullable=True
)
#: Version number maintained by SQLAlchemy, used for vCal files, starting at 1
- revisionid: Mapped[int] = with_roles(
- sa_orm.mapped_column(sa.Integer, nullable=False), read={'all'}
- )
+ revisionid: Mapped[int] = with_roles(sa_orm.mapped_column(), read={'all'})
search_vector: Mapped[str] = sa_orm.mapped_column(
TSVectorType(
@@ -231,7 +223,7 @@ def _scheduled_expression(cls) -> sa.ColumnElement[bool]:
def start_at_localized(self) -> datetime | None:
return (
localize_timezone(self.start_at, tz=self.project.timezone)
- if self.start_at
+ if self.start_at is not None
else None
)
@@ -239,7 +231,7 @@ def start_at_localized(self) -> datetime | None:
def end_at_localized(self) -> datetime | None:
return (
localize_timezone(self.end_at, tz=self.project.timezone)
- if self.end_at
+ if self.end_at is not None
else None
)
diff --git a/funnel/models/shortlink.py b/funnel/models/shortlink.py
index 3b8c1b918..d44b3e343 100644
--- a/funnel/models/shortlink.py
+++ b/funnel/models/shortlink.py
@@ -222,15 +222,13 @@ class Shortlink(NoIdMixin, Model):
)
#: Id of account that created this shortlink (optional)
created_by_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('account.id', ondelete='SET NULL'), nullable=True
+ sa.ForeignKey('account.id', ondelete='SET NULL'), default=None, nullable=True
)
#: Account that created this shortlink (optional)
created_by: Mapped[Account | None] = relationship()
#: Is this link enabled? If not, render 410 Gone
- enabled: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=True
- )
+ enabled: Mapped[bool] = sa_orm.mapped_column(default=True)
@hybrid_property
def name(self) -> str:
diff --git a/funnel/models/site_membership.py b/funnel/models/site_membership.py
index f8576ff64..7d8eb6663 100644
--- a/funnel/models/site_membership.py
+++ b/funnel/models/site_membership.py
@@ -5,12 +5,12 @@
from werkzeug.utils import cached_property
from . import Mapped, Model, declared_attr, sa, sa_orm
-from .membership_mixin import ImmutableUserMembershipMixin
+from .membership_mixin import ImmutableMembershipMixin
__all__ = ['SiteMembership']
-class SiteMembership(ImmutableUserMembershipMixin, Model):
+class SiteMembership(ImmutableMembershipMixin, Model):
"""Membership roles for users who are site administrators."""
__tablename__ = 'site_membership'
@@ -38,36 +38,29 @@ class SiteMembership(ImmutableUserMembershipMixin, Model):
#: SiteMembership doesn't have a container limiting its scope
parent_id = None
- parent_id_column = None
+ parent_id_column = '' # Must be of type str, not None
parent = None
# Site admin roles (at least one must be True):
#: Comment moderators can delete comments
- is_comment_moderator: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_comment_moderator: Mapped[bool] = sa_orm.mapped_column(default=False)
#: User moderators can suspend users
- is_user_moderator: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_user_moderator: Mapped[bool] = sa_orm.mapped_column(default=False)
#: Site editors can feature or reject projects
- is_site_editor: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_site_editor: Mapped[bool] = sa_orm.mapped_column(default=False)
#: Sysadmins can manage technical settings
- is_sysadmin: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, nullable=False, default=False
- )
+ is_sysadmin: Mapped[bool] = sa_orm.mapped_column(default=False)
@declared_attr.directive
@classmethod
- def __table_args__(cls) -> tuple:
+ def __table_args__(cls) -> tuple: # type: ignore[override]
"""Table arguments."""
try:
args = list(super().__table_args__)
except AttributeError:
args = []
+ kwargs = args.pop(-1) if args and isinstance(args[-1], dict) else None
args.append(
sa.CheckConstraint(
sa.or_(
@@ -79,6 +72,8 @@ def __table_args__(cls) -> tuple:
name='site_membership_has_role',
)
)
+ if kwargs:
+ args.append(kwargs)
return tuple(args)
def __repr__(self) -> str:
diff --git a/funnel/models/sponsor_membership.py b/funnel/models/sponsor_membership.py
index 829d7186f..563d3fa81 100644
--- a/funnel/models/sponsor_membership.py
+++ b/funnel/models/sponsor_membership.py
@@ -9,23 +9,14 @@
from coaster.sqlalchemy import immutable
from . import Mapped, Model, relationship, sa, sa_orm
-from .membership_mixin import (
- FrozenAttributionProtoMixin,
- ImmutableUserMembershipMixin,
- ReorderMembershipProtoMixin,
-)
+from .membership_mixin import FrozenAttributionMixin, ReorderMembershipMixin
from .project import Project
from .proposal import Proposal
__all__ = ['ProjectSponsorMembership', 'ProposalSponsorMembership']
-class ProjectSponsorMembership( # type: ignore[misc]
- ImmutableUserMembershipMixin,
- FrozenAttributionProtoMixin,
- ReorderMembershipProtoMixin,
- Model,
-):
+class ProjectSponsorMembership(FrozenAttributionMixin, ReorderMembershipMixin, Model):
"""Sponsor of a project."""
__tablename__ = 'project_sponsor_membership'
@@ -83,7 +74,7 @@ class ProjectSponsorMembership( # type: ignore[misc]
revoke_on_member_delete: ClassVar[bool] = False
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id', ondelete='CASCADE'), nullable=False
+ sa.ForeignKey('project.id', ondelete='CASCADE'), default=None, nullable=False
)
project: Mapped[Project] = relationship(back_populates='all_sponsor_memberships')
parent_id: Mapped[int] = sa_orm.synonym('project_id')
@@ -92,18 +83,14 @@ class ProjectSponsorMembership( # type: ignore[misc]
#: Is this sponsor being promoted for commercial reasons? Projects may have a legal
#: obligation to reveal this. This column records a declaration from the project.
- is_promoted: Mapped[bool] = immutable(
- sa_orm.mapped_column(sa.Boolean, nullable=False)
- )
+ is_promoted: Mapped[bool] = immutable(sa_orm.mapped_column())
#: Optional label, indicating the type of sponsor
label: Mapped[str | None] = immutable(
sa_orm.mapped_column(
- sa.Unicode,
sa.CheckConstraint(
"label <> ''", name='project_sponsor_membership_label_check'
- ),
- nullable=True,
+ )
)
)
@@ -120,12 +107,7 @@ def offered_roles(self) -> set[str]:
# FIXME: Replace this with existing proposal collaborator as they're now both related
# to "account"
-class ProposalSponsorMembership( # type: ignore[misc]
- FrozenAttributionProtoMixin,
- ReorderMembershipProtoMixin,
- ImmutableUserMembershipMixin,
- Model,
-):
+class ProposalSponsorMembership(FrozenAttributionMixin, ReorderMembershipMixin, Model):
"""Sponsor of a proposal."""
__tablename__ = 'proposal_sponsor_membership'
@@ -183,7 +165,7 @@ class ProposalSponsorMembership( # type: ignore[misc]
revoke_on_member_delete: ClassVar[bool] = False
proposal_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('proposal.id', ondelete='CASCADE'), nullable=False
+ sa.ForeignKey('proposal.id', ondelete='CASCADE'), default=None, nullable=False
)
proposal: Mapped[Proposal] = relationship(back_populates='all_sponsor_memberships')
parent_id: Mapped[int] = sa_orm.synonym('proposal_id')
@@ -192,18 +174,14 @@ class ProposalSponsorMembership( # type: ignore[misc]
#: Is this sponsor being promoted for commercial reasons? Proposals may have a legal
#: obligation to reveal this. This column records a declaration from the proposal.
- is_promoted: Mapped[bool] = immutable(
- sa_orm.mapped_column(sa.Boolean, nullable=False)
- )
+ is_promoted: Mapped[bool] = immutable(sa_orm.mapped_column())
#: Optional label, indicating the type of sponsor
label: Mapped[str | None] = immutable(
sa_orm.mapped_column(
- sa.Unicode,
sa.CheckConstraint(
"label <> ''", name='proposal_sponsor_membership_label_check'
- ),
- nullable=True,
+ )
)
)
diff --git a/funnel/models/sync_ticket.py b/funnel/models/sync_ticket.py
index 19969fed4..d2f31eb99 100644
--- a/funnel/models/sync_ticket.py
+++ b/funnel/models/sync_ticket.py
@@ -112,7 +112,7 @@ class TicketEvent(GetTitleMixin, Model):
__tablename__ = 'ticket_event'
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='ticket_events'),
@@ -171,7 +171,7 @@ class TicketType(GetTitleMixin, Model):
__tablename__ = 'ticket_type'
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='ticket_types'),
@@ -202,7 +202,9 @@ class TicketType(GetTitleMixin, Model):
}
-class TicketParticipant(OptionalEmailAddressMixin, UuidMixin, BaseMixin, Model):
+class TicketParticipant(
+ OptionalEmailAddressMixin, UuidMixin, BaseMixin[int, Account], Model
+):
"""A participant in one or more events, synced from an external ticket source."""
__tablename__ = 'ticket_participant'
@@ -239,22 +241,28 @@ class TicketParticipant(OptionalEmailAddressMixin, UuidMixin, BaseMixin, Model):
)
# public key
puk: Mapped[str] = sa_orm.mapped_column(
- sa.Unicode(44), nullable=False, default=make_public_key, unique=True
+ sa.Unicode(44),
+ nullable=False,
+ insert_default=make_public_key,
+ default=None,
+ unique=True,
)
key: Mapped[str] = sa_orm.mapped_column(
- sa.Unicode(44), nullable=False, default=make_private_key, unique=True
- )
- badge_printed: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=False, nullable=False
+ sa.Unicode(44),
+ nullable=False,
+ insert_default=make_private_key,
+ default=None,
+ unique=True,
)
+ badge_printed: Mapped[bool] = sa_orm.mapped_column(default=False)
participant_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=True
+ sa.ForeignKey('account.id'), default=None, nullable=True
)
participant: Mapped[Account | None] = relationship(
back_populates='ticket_participants'
)
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(Project, back_populates='ticket_participants'),
@@ -294,7 +302,7 @@ def roles_for(
if actor is not None:
if actor == self.participant:
roles.add('member')
- cx = ContactExchange.query.get((actor.id, self.id))
+ cx = db.session.get(ContactExchange, (actor.id, self.id))
if cx is not None:
roles.add('scanner')
return roles
@@ -403,28 +411,26 @@ def checkin_list(cls, ticket_event: TicketEvent) -> list: # TODO: List type?
return query.all()
-class TicketEventParticipant(BaseMixin, Model):
+class TicketEventParticipant(BaseMixin[int, Account], Model):
"""Join model between :class:`TicketParticipant` and :class:`TicketEvent`."""
__tablename__ = 'ticket_event_participant'
ticket_participant_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('ticket_participant.id'), nullable=False
+ sa.ForeignKey('ticket_participant.id'), default=None, nullable=False
)
ticket_participant: Mapped[TicketParticipant] = relationship(
back_populates='ticket_event_participants',
overlaps='ticket_events,ticket_participants',
)
ticket_event_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('ticket_event.id'), nullable=False
+ sa.ForeignKey('ticket_event.id'), default=None, nullable=False
)
ticket_event: Mapped[TicketEvent] = relationship(
back_populates='ticket_event_participants',
overlaps='ticket_events,ticket_participants',
)
- checked_in: Mapped[bool] = sa_orm.mapped_column(
- sa.Boolean, default=False, nullable=False
- )
+ checked_in: Mapped[bool] = sa_orm.mapped_column(default=False)
__table_args__ = (
# Uses a custom name that is not as per convention because the default name is
@@ -450,7 +456,7 @@ def get(
)
-class TicketClient(BaseMixin, Model):
+class TicketClient(BaseMixin[int, Account], Model):
__tablename__ = 'ticket_client'
name: Mapped[str] = with_roles(
sa_orm.mapped_column(sa.Unicode(80), nullable=False), rw={'project_promoter'}
@@ -468,7 +474,7 @@ class TicketClient(BaseMixin, Model):
sa_orm.mapped_column(sa.Unicode(80), nullable=False), rw={'project_promoter'}
)
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='ticket_clients'),
@@ -523,7 +529,7 @@ def import_from_list(self, ticket_list):
ticket.ticket_participant.add_events(ticket_type.ticket_events)
-class SyncTicket(BaseMixin, Model):
+class SyncTicket(BaseMixin[int, Account], Model):
"""Model for a ticket that was bought elsewhere, like Boxoffice or Explara."""
__tablename__ = 'sync_ticket'
@@ -531,17 +537,17 @@ class SyncTicket(BaseMixin, Model):
ticket_no: Mapped[str] = sa_orm.mapped_column(sa.Unicode(80), nullable=False)
order_no: Mapped[str] = sa_orm.mapped_column(sa.Unicode(80), nullable=False)
ticket_type_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('ticket_type.id'), nullable=False
+ sa.ForeignKey('ticket_type.id'), default=None, nullable=False
)
ticket_type: Mapped[TicketType] = relationship(back_populates='sync_tickets')
ticket_participant_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('ticket_participant.id'), nullable=False
+ sa.ForeignKey('ticket_participant.id'), default=None, nullable=False
)
ticket_participant: Mapped[TicketParticipant] = relationship(
back_populates='sync_tickets'
)
ticket_client_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('ticket_client.id'), nullable=False
+ sa.ForeignKey('ticket_client.id'), default=None, nullable=False
)
ticket_client: Mapped[TicketClient] = relationship(back_populates='sync_tickets')
__table_args__ = (sa.UniqueConstraint('ticket_client_id', 'order_no', 'ticket_no'),)
diff --git a/funnel/models/typing.py b/funnel/models/typing.py
index 694350b24..ca09f3d1a 100644
--- a/funnel/models/typing.py
+++ b/funnel/models/typing.py
@@ -1,49 +1,95 @@
"""Union types for models with shared functionality."""
-from typing import Union
-
-from .account import Account, AccountOldId, Team
-from .auth_client import AuthClient
-from .comment import Comment, Commentset
-from .label import Label
-from .login_session import LoginSession
-from .membership_mixin import ImmutableMembershipMixin
-from .moderation import CommentModeratorReport
-from .project import Project
-from .proposal import Proposal
-from .rsvp import Rsvp
-from .session import Session
-from .sync_ticket import TicketParticipant
-from .update import Update
-from .venue import Venue, VenueRoom
-
-__all__ = ['UuidModelUnion', 'SearchModelUnion', 'MarkdownModelUnion']
-
-# All models with a `uuid` attr
-UuidModelUnion = Union[
- Account,
- AccountOldId,
- AuthClient,
- Comment,
- CommentModeratorReport,
- Commentset,
- ImmutableMembershipMixin,
- LoginSession,
- Project,
- Proposal,
- Rsvp,
- Session,
- Team,
- TicketParticipant,
- Update,
- Venue,
- VenueRoom,
-]
+from __future__ import annotations
+
+from collections.abc import Iterable, Iterator, Sequence
+from datetime import datetime
+from typing import Any, ClassVar, Literal, Protocol, overload, runtime_checkable
+from uuid import UUID
-# All models with a `search_vector` attr
-SearchModelUnion = Union[Account, Comment, Label, Project, Proposal, Session, Update]
+from sqlalchemy import Table
+from sqlalchemy.orm import Mapped, declared_attr
-# All models with one or more markdown composite columns
-MarkdownModelUnion = Union[
- Account, Comment, Project, Proposal, Session, Update, Venue, VenueRoom
+from coaster.sqlalchemy import LazyRoleSet, QueryProperty
+from coaster.utils import InspectableSet
+
+__all__ = [
+ 'ModelProtocol',
+ 'ModelTimestampProtocol',
+ 'ModelUrlProtocol',
+ 'ModelRoleProtocol',
+ 'ModelIdProtocol',
+ 'ModelUuidProtocol',
+ 'ModelSearchProtocol',
]
+
+
+class ModelProtocol(Protocol):
+ __tablename__: str
+ __table__: ClassVar[Table]
+ query: ClassVar[QueryProperty]
+
+
+class ModelTimestampProtocol(ModelProtocol, Protocol):
+ created_at: declared_attr[datetime]
+ updated_at: declared_attr[datetime]
+
+
+class ModelUrlProtocol(Protocol):
+ @property
+ def absolute_url(self) -> str | None:
+ ...
+
+ def url_for(self, action: str = 'view', **kwargs) -> str:
+ ...
+
+
+class ModelRoleProtocol(Protocol):
+ def roles_for(
+ self, actor: Account | None = None, anchors: Sequence[Any] = ()
+ ) -> LazyRoleSet:
+ ...
+
+ @property
+ def current_roles(self) -> InspectableSet[LazyRoleSet]:
+ ...
+
+ @overload
+ def actors_with(
+ self, roles: Iterable[str], with_role: Literal[False] = False
+ ) -> Iterator[Account]:
+ ...
+
+ @overload
+ def actors_with(
+ self, roles: Iterable[str], with_role: Literal[True]
+ ) -> Iterator[tuple[Account, str]]:
+ ...
+
+ def actors_with(
+ self, roles: Iterable[str], with_role: bool = False
+ ) -> Iterator[Account | tuple[Account, str]]:
+ ...
+
+
+class ModelIdProtocol(
+ ModelTimestampProtocol, ModelUrlProtocol, ModelRoleProtocol, Protocol
+):
+ id_: declared_attr[Any]
+
+
+@runtime_checkable # FIXME: This is never used, but needed to make type checkers happy
+class ModelUuidProtocol(ModelIdProtocol, Protocol):
+ uuid: declared_attr[UUID]
+
+
+class ModelSearchProtocol(ModelUuidProtocol, Protocol):
+ search_vector: Mapped[str]
+
+ @property
+ def title(self) -> Mapped[str] | declared_attr[str]:
+ ...
+
+
+# Tail imports
+from .account import Account
diff --git a/funnel/models/update.py b/funnel/models/update.py
index 266dccc34..7aca005c5 100644
--- a/funnel/models/update.py
+++ b/funnel/models/update.py
@@ -45,13 +45,15 @@ class VISIBILITY_STATE(LabeledEnum): # noqa: N801
RESTRICTED = (2, 'restricted', __("Restricted"))
-class Update(UuidMixin, BaseScopedIdNameMixin, Model):
+class Update(UuidMixin, BaseScopedIdNameMixin[int, Account], Model):
__tablename__ = 'update'
_visibility_state: Mapped[int] = sa_orm.mapped_column(
'visibility_state',
sa.SmallInteger,
- StateManager.check_constraint('visibility_state', VISIBILITY_STATE),
+ StateManager.check_constraint(
+ 'visibility_state', VISIBILITY_STATE, sa.SmallInteger
+ ),
default=VISIBILITY_STATE.PUBLIC,
nullable=False,
index=True,
@@ -63,7 +65,7 @@ class Update(UuidMixin, BaseScopedIdNameMixin, Model):
_state: Mapped[int] = sa_orm.mapped_column(
'state',
sa.SmallInteger,
- StateManager.check_constraint('state', UPDATE_STATE),
+ StateManager.check_constraint('state', UPDATE_STATE, sa.SmallInteger),
default=UPDATE_STATE.DRAFT,
nullable=False,
index=True,
@@ -71,7 +73,7 @@ class Update(UuidMixin, BaseScopedIdNameMixin, Model):
state = StateManager['Update']('_state', UPDATE_STATE, doc="Update state")
created_by_id: Mapped[int] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=False, index=True
+ sa.ForeignKey('account.id'), default=None, nullable=False, index=True
)
created_by: Mapped[Account] = with_roles(
relationship(
@@ -83,7 +85,7 @@ class Update(UuidMixin, BaseScopedIdNameMixin, Model):
)
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False, index=True
+ sa.ForeignKey('project.id'), default=None, nullable=False, index=True
)
project: Mapped[Project] = with_roles(
relationship(back_populates='updates'),
@@ -123,17 +125,17 @@ class Update(UuidMixin, BaseScopedIdNameMixin, Model):
#: Update number, for Project updates, assigned when the update is published
number: Mapped[int | None] = with_roles(
- sa_orm.mapped_column(sa.Integer, nullable=True, default=None), read={'all'}
+ sa_orm.mapped_column(default=None), read={'all'}
)
#: Like pinned tweets. You can keep posting updates,
#: but might want to pin an update from a week ago.
is_pinned: Mapped[bool] = with_roles(
- sa_orm.mapped_column(sa.Boolean, default=False, nullable=False), read={'all'}
+ sa_orm.mapped_column(default=False), read={'all'}
)
published_by_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=True, index=True
+ sa.ForeignKey('account.id'), default=None, nullable=True, index=True
)
published_by: Mapped[Account | None] = with_roles(
relationship(
@@ -147,7 +149,7 @@ class Update(UuidMixin, BaseScopedIdNameMixin, Model):
)
deleted_by_id: Mapped[int | None] = sa_orm.mapped_column(
- sa.ForeignKey('account.id'), nullable=True, index=True
+ sa.ForeignKey('account.id'), default=None, nullable=True, index=True
)
deleted_by: Mapped[Account | None] = with_roles(
relationship(back_populates='deleted_updates', foreign_keys=[deleted_by_id]),
@@ -163,7 +165,7 @@ class Update(UuidMixin, BaseScopedIdNameMixin, Model):
)
commentset_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('commentset.id'), nullable=False
+ sa.ForeignKey('commentset.id'), default=None, nullable=False
)
commentset: Mapped[Commentset] = with_roles(
relationship(
diff --git a/funnel/models/utils.py b/funnel/models/utils.py
index 3d7fa93f3..83579f3c7 100644
--- a/funnel/models/utils.py
+++ b/funnel/models/utils.py
@@ -2,13 +2,14 @@
from __future__ import annotations
-from typing import Literal, NamedTuple, overload
+from typing import Literal, NamedTuple, TypeVar, overload
import phonenumbers
from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint
from .. import app
from ..typing import OptionalMigratedTables
+from . import Model, db
from .account import (
Account,
AccountEmail,
@@ -16,10 +17,9 @@
AccountExternalId,
AccountPhone,
Anchor,
- Model,
- db,
)
from .phone_number import PHONE_LOOKUP_REGIONS
+from .typing import ModelIdProtocol
__all__ = [
'IncompleteUserMigrationError',
@@ -89,7 +89,7 @@ def getuser(name: str, anchor: bool = False) -> Account | AccountAndAnchor | Non
return AccountAndAnchor(None, None)
return None
else:
- # If it wasn't an email address or an @username, check if it's a phone number
+ # If it was not an email address or an @username, check if it's a phone number
try:
# Assume unprefixed numbers to be a local number in one of our supported
# regions, in order of priority. Also see
@@ -107,9 +107,9 @@ def getuser(name: str, anchor: bool = False) -> Account | AccountAndAnchor | Non
if anchor:
return AccountAndAnchor(accountphone.account, accountphone)
return accountphone.account
- # No matching accountphone? Continue to trying as a username
+ # No matching account phone? Continue to trying as a username
except phonenumbers.NumberParseException:
- # This was not a parseable phone number. Continue to trying as a username
+ # This was not a parsable phone number. Continue to trying as a username
pass
# Last guess: username
@@ -137,8 +137,15 @@ def getextid(service: str, userid: str) -> AccountExternalId | None:
return AccountExternalId.get(service=service, userid=userid)
-def merge_accounts(current_account: Account, other_account: Account) -> Account | None:
+_A1 = TypeVar('_A1', bound=Account)
+_A2 = TypeVar('_A2', bound=Account)
+
+
+def merge_accounts(current_account: _A1, other_account: _A2) -> _A1 | _A2 | None:
"""Merge two user accounts and return the new user account."""
+ keep_account: _A1 | _A2
+ merge_account: _A1 | _A2
+
app.logger.info(
"Preparing to merge accounts %s and %s", current_account, other_account
)
@@ -156,7 +163,7 @@ def merge_accounts(current_account: Account, other_account: Account) -> Account
# keep_account.
safe = do_migrate_instances(merge_account, keep_account, 'migrate_account')
if safe:
- # 2. Add merge_account's uuid to oldids and mark account as merged
+ # 2. Add merge_account's uuid to `oldids` and mark account as merged
merge_account.mark_merged_into(keep_account)
# 3. Transfer name and password if required
if not keep_account.name:
@@ -188,8 +195,8 @@ def merge_accounts(current_account: Account, other_account: Account) -> Account
def do_migrate_instances(
- old_instance: Model,
- new_instance: Model,
+ old_instance: ModelIdProtocol,
+ new_instance: ModelIdProtocol,
helper_method: str | None = None,
) -> bool:
"""
@@ -270,8 +277,8 @@ def do_migrate_table(table):
for column in target_columns:
session.execute(
table.update()
- .where(column == old_instance.id)
- .values(**{column.name: new_instance.id})
+ .where(column == old_instance.id_)
+ .values(**{column.name: new_instance.id_})
)
session.flush()
diff --git a/funnel/models/venue.py b/funnel/models/venue.py
index b57450922..e3791a19f 100644
--- a/funnel/models/venue.py
+++ b/funnel/models/venue.py
@@ -16,6 +16,7 @@
sa,
sa_orm,
)
+from .account import Account
from .helpers import MarkdownCompositeBasic
from .project import Project
from .project_membership import project_child_role_map, project_child_role_set
@@ -23,11 +24,11 @@
__all__ = ['Venue', 'VenueRoom']
-class Venue(UuidMixin, BaseScopedNameMixin, CoordinatesMixin, Model):
+class Venue(UuidMixin, BaseScopedNameMixin[int, Account], CoordinatesMixin, Model):
__tablename__ = 'venue'
project_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('project.id'), nullable=False
+ sa.ForeignKey('project.id'), default=None, nullable=False
)
project: Mapped[Project] = with_roles(
relationship(back_populates='venues'), grants_via={None: project_child_role_map}
@@ -59,7 +60,7 @@ class Venue(UuidMixin, BaseScopedNameMixin, CoordinatesMixin, Model):
back_populates='venue',
)
- seq: Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False)
+ seq: Mapped[int] = sa_orm.mapped_column()
__table_args__ = (sa.UniqueConstraint('project_id', 'name'),)
@@ -110,11 +111,11 @@ class Venue(UuidMixin, BaseScopedNameMixin, CoordinatesMixin, Model):
}
-class VenueRoom(UuidMixin, BaseScopedNameMixin, Model):
+class VenueRoom(UuidMixin, BaseScopedNameMixin[int, Account], Model):
__tablename__ = 'venue_room'
venue_id: Mapped[int] = sa_orm.mapped_column(
- sa.Integer, sa.ForeignKey('venue.id'), nullable=False
+ sa.ForeignKey('venue.id'), default=None, nullable=False
)
venue: Mapped[Venue] = with_roles(
relationship(back_populates='rooms'),
@@ -129,12 +130,13 @@ class VenueRoom(UuidMixin, BaseScopedNameMixin, Model):
sa.Unicode(6), nullable=False, default='229922'
)
- seq: Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False)
+ seq: Mapped[int] = sa_orm.mapped_column()
sessions: Mapped[list[Session]] = relationship(back_populates='venue_room')
scheduled_sessions: Mapped[list[Session]] = relationship(
primaryjoin=lambda: sa.and_(
- Session.venue_room_id == VenueRoom.id, Session.scheduled
+ Session.venue_room_id == VenueRoom.id,
+ Session.scheduled, # type: ignore[has-type] # FIXME
),
viewonly=True,
)
diff --git a/funnel/registry.py b/funnel/registry.py
index fe75600ea..61951d7d4 100644
--- a/funnel/registry.py
+++ b/funnel/registry.py
@@ -7,7 +7,7 @@
from collections.abc import Callable, Collection
from dataclasses import dataclass
from functools import wraps
-from typing import Any, NoReturn
+from typing import Any, NoReturn, cast
from flask import Response, abort, jsonify, request
from werkzeug.datastructures import MultiDict
@@ -16,7 +16,7 @@
from baseframe.signals import exception_catchall
from .models import AccountExternalId, AuthToken
-from .typing import P, ReturnResponse
+from .typing import ReturnResponse
# Bearer token, as per
# http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-15#section-2.1
@@ -32,7 +32,9 @@ def resource(
description: str | None = None,
trusted: bool = False,
scope: str | None = None,
- ) -> Callable[[Callable[P, Any]], Callable[[], ReturnResponse]]:
+ ) -> Callable[
+ [Callable[[AuthToken, MultiDict, MultiDict], Any]], Callable[[], ReturnResponse]
+ ]:
"""
Decorate a resource function.
@@ -137,7 +139,7 @@ def wrapper() -> ReturnResponse:
'trusted': trusted,
'f': f,
}
- return wrapper
+ return cast(Callable[[], ReturnResponse], wrapper)
return decorator
diff --git a/funnel/transports/email/send.py b/funnel/transports/email/send.py
index 14e38a2e0..c83e0b542 100644
--- a/funnel/transports/email/send.py
+++ b/funnel/transports/email/send.py
@@ -5,7 +5,7 @@
import smtplib
from dataclasses import dataclass
from email.utils import formataddr, getaddresses, make_msgid, parseaddr
-from typing import Optional, Union
+from typing import Union
from flask import current_app
from flask_mailman import EmailMultiAlternatives
@@ -30,14 +30,14 @@
]
# Email recipient type
-EmailRecipient = Union[Account, tuple[Optional[str], str], str]
+EmailRecipient = Union[Account, tuple[str | None, str], str]
@dataclass
class EmailAttachment:
"""An email attachment. Must have content, filename and mimetype."""
- content: str
+ content: bytes
filename: str
mimetype: str
@@ -74,7 +74,6 @@ def jsonld_confirm_action(description: str, url: str, title: str) -> dict[str, o
def jsonld_event_reservation(rsvp: Rsvp) -> dict[str, object]:
"""Schema.org JSON-LD markup for an event reservation."""
location: str | dict[str, object]
- event_mode: str
venue = rsvp.project.primary_venue
if venue is not None:
location = {
diff --git a/funnel/utils/misc.py b/funnel/utils/misc.py
index 99a3b36fd..11388698c 100644
--- a/funnel/utils/misc.py
+++ b/funnel/utils/misc.py
@@ -143,7 +143,7 @@ def extract_twitter_handle(handle: str) -> str | None:
)
-def format_twitter_handle(handle: str) -> str:
+def format_twitter_handle(handle: str | None) -> str:
"""Format twitter handle as an @ mention."""
return f"@{handle}" if handle else ""
diff --git a/funnel/views/account.py b/funnel/views/account.py
index 3890faf5d..ddb2e691c 100644
--- a/funnel/views/account.py
+++ b/funnel/views/account.py
@@ -21,11 +21,11 @@
from baseframe import _, forms
from baseframe.forms import render_delete_sqla, render_form, render_message
-from coaster.auth import current_auth
from coaster.sqlalchemy import RoleAccessProxy
from coaster.views import ClassView, get_next_url, render_with, route
from .. import app
+from ..auth import current_auth
from ..forms import (
AccountDeleteForm,
AccountForm,
@@ -106,9 +106,8 @@ def user_locale(obj: Account) -> str:
@Account.views('timezone')
def user_timezone(obj: Account) -> str:
"""Human-friendly identifier for user's timezone, defaulting to timezone name."""
- return timezone_identifiers.get(
- str(obj.timezone) if obj.timezone else '', obj.timezone
- )
+ timezone = str(u_tz) if (u_tz := obj.timezone) is not None else ''
+ return timezone_identifiers.get(timezone, timezone)
@Account.views()
diff --git a/funnel/views/api/account.py b/funnel/views/api/account.py
index e09a83593..0aebafe44 100644
--- a/funnel/views/api/account.py
+++ b/funnel/views/api/account.py
@@ -7,9 +7,9 @@
from flask import request
from baseframe import _
-from coaster.auth import current_auth
from ... import app
+from ...auth import current_auth
from ...forms import PasswordPolicyForm, UsernameAvailableForm
from ...typing import ReturnView
from ..helpers import progressive_rate_limit_validator, validate_rate_limit
@@ -78,7 +78,9 @@ def account_username_availability() -> ReturnView:
# Allow user or source IP to check for up to 20 usernames every 10 minutes (600s)
validate_rate_limit(
'account_username-available',
- current_auth.actor.uuid_b58 if current_auth.actor else request.remote_addr,
+ current_auth.actor.uuid_b58
+ if current_auth.actor
+ else (request.remote_addr or 'unknown-ipaddr'),
# 20 username candidates
20,
# per every 10 minutes (600s)
diff --git a/funnel/views/api/geoname.py b/funnel/views/api/geoname.py
index dc993a497..9d2db4715 100644
--- a/funnel/views/api/geoname.py
+++ b/funnel/views/api/geoname.py
@@ -8,7 +8,7 @@
from coaster.views import requestargs
from ... import app
-from ...models import GeoName
+from ...models import GeoName, db
from ...typing import ReturnView
@@ -19,7 +19,7 @@ def geo_get_by_name(
) -> ReturnView:
"""Get a geoname record given a single URL stub name or geoname id."""
if name.isdigit():
- geoname = GeoName.query.get(int(name))
+ geoname = db.session.get(GeoName, int(name))
else:
geoname = GeoName.get(name)
return (
@@ -43,7 +43,7 @@ def geo_get_by_names(
geonames = []
for n in name:
if n.isdigit():
- geoname = GeoName.query.get(int(n))
+ geoname = db.session.get(GeoName, int(n))
else:
geoname = GeoName.get(n)
if geoname:
diff --git a/funnel/views/api/oauth.py b/funnel/views/api/oauth.py
index 945ae619d..3bb427d18 100644
--- a/funnel/views/api/oauth.py
+++ b/funnel/views/api/oauth.py
@@ -2,17 +2,24 @@
from __future__ import annotations
-from collections.abc import Iterable
-from typing import Optional, cast
-
-from flask import get_flashed_messages, jsonify, redirect, render_template, request
+from collections.abc import Collection, Iterable
+from typing import cast
+
+from flask import (
+ abort,
+ get_flashed_messages,
+ jsonify,
+ redirect,
+ render_template,
+ request,
+)
from baseframe import _, forms
-from coaster.auth import current_auth
from coaster.sqlalchemy import failsafe_add
from coaster.utils import newsecret
from ... import app
+from ...auth import current_auth
from ...models import (
Account,
AuthClient,
@@ -178,8 +185,8 @@ def oauth_authorize() -> ReturnView:
form = forms.Form()
response_type = request.args.get('response_type')
- client_id = cast(Optional[str], request.args.get('client_id'))
- redirect_uri = cast(Optional[str], request.args.get('redirect_uri'))
+ client_id = request.args.get('client_id')
+ redirect_uri = request.args.get('redirect_uri')
scope = cast(str, request.args.get('scope', '')).split(' ')
state = cast(str, request.args.get('state', ''))
@@ -196,7 +203,11 @@ def oauth_authorize() -> ReturnView:
# Validation 1.2.1: Is the client active?
if not auth_client.active:
- return oauth_auth_error(auth_client.redirect_uri, state, 'unauthorized_client')
+ if auth_client.redirect_uri:
+ return oauth_auth_error(
+ auth_client.redirect_uri, state, 'unauthorized_client'
+ )
+ abort(422)
# Validation 1.3: Cross-check redirect_uri
if not redirect_uri:
@@ -207,7 +218,7 @@ def oauth_authorize() -> ReturnView:
redirect_uri
):
return oauth_auth_error(
- auth_client.redirect_uri,
+ redirect_uri,
state,
'invalid_request',
_("Redirect URI hostname doesn't match"),
@@ -216,7 +227,7 @@ def oauth_authorize() -> ReturnView:
# Validation 1.4: AuthClient allows access for this user
if not auth_client.allow_access_for(current_auth.user):
return oauth_auth_error(
- auth_client.redirect_uri,
+ redirect_uri,
state,
'invalid_scope',
_("You do not have access to this application"),
@@ -349,7 +360,7 @@ def oauth_token_error(
def oauth_make_token(
user: Account | None,
auth_client: AuthClient,
- scope: Iterable,
+ scope: Collection[str],
login_session: LoginSession | None = None,
) -> AuthToken:
"""Make an OAuth2 token for the given user, client, scope and optional session."""
@@ -364,28 +375,25 @@ def oauth_make_token(
if auth_client.confidential:
if user is None:
raise ValueError("User not provided")
- token = AuthToken( # nosec
+ token = AuthToken( # nosec B106
account=user, auth_client=auth_client, scope=scope, token_type='bearer'
)
- token = cast(
- AuthToken,
- failsafe_add(db.session, token, account=user, auth_client=auth_client),
+ token = failsafe_add(
+ db.session, token, account=user, auth_client=auth_client
)
+
elif login_session is not None:
- token = AuthToken( # nosec
+ token = AuthToken( # nosec B106
login_session=login_session,
auth_client=auth_client,
scope=scope,
token_type='bearer',
)
- token = cast(
- AuthToken,
- failsafe_add(
- db.session,
- token,
- login_session=login_session,
- auth_client=auth_client,
- ),
+ token = failsafe_add(
+ db.session,
+ token,
+ login_session=login_session,
+ auth_client=auth_client,
)
else:
raise ValueError("login_session not provided")
@@ -415,21 +423,18 @@ def oauth_token_success(token: AuthToken, **params) -> ReturnView:
def oauth_token() -> ReturnView:
"""Provide token endpoint for OAuth2 server."""
# Always required parameters
- grant_type = cast(Optional[str], request.form.get('grant_type'))
+ grant_type = request.form.get('grant_type')
auth_client = current_auth.auth_client # Provided by @requires_client_login
scope = request.form.get('scope', '').split(' ')
# if grant_type == 'authorization_code' (POST)
- code = cast(Optional[str], request.form.get('code'))
- redirect_uri = cast(Optional[str], request.form.get('redirect_uri'))
+ code = request.form.get('code')
+ redirect_uri = request.form.get('redirect_uri')
# if grant_type == 'password' (POST)
- username = cast(Optional[str], request.form.get('username'))
- password = cast(Optional[str], request.form.get('password'))
+ username = request.form.get('username')
+ password = request.form.get('password')
# if grant_type == 'client_credentials'
- buid = cast(
- Optional[str],
- # XXX: Deprecated userid parameter
- request.form.get('buid') or request.form.get('userid'),
- )
+ # XXX: Deprecated userid parameter
+ buid = request.form.get('buid') or request.form.get('userid')
# Validations 1: Required parameters
if not grant_type:
@@ -483,12 +488,12 @@ def oauth_token() -> ReturnView:
db.session.delete(authcode)
db.session.commit()
return oauth_token_error('invalid_grant', _("Expired auth code"))
- # Validations 3.1: scope in authcode
+ # Validations 3.1: scope in auth code
if not scope or scope[0] == '':
return oauth_token_error('invalid_scope', _("Scope is blank"))
if not set(scope).issubset(set(authcode.scope)):
return oauth_token_error('invalid_scope', _("Scope expanded"))
- # Scope not exceeded. Use whatever the authcode allows
+ # Scope not exceeded. Use whatever the auth code allows
scope = list(authcode.scope)
if redirect_uri != authcode.redirect_uri:
return oauth_token_error('invalid_client', _("redirect_uri does not match"))
diff --git a/funnel/views/api/resource.py b/funnel/views/api/resource.py
index 22226b961..3640c27ad 100644
--- a/funnel/views/api/resource.py
+++ b/funnel/views/api/resource.py
@@ -3,17 +3,17 @@
from __future__ import annotations
from collections.abc import Container
-from typing import Any, Literal, cast
+from typing import Any, Literal
from flask import abort, jsonify, render_template, request
from werkzeug.datastructures import MultiDict
from baseframe import __
-from coaster.auth import current_auth
from coaster.utils import getbool
from coaster.views import jsonp, requestargs
from ... import app
+from ...auth import current_auth
from ...models import (
Account,
AuthClient,
@@ -547,7 +547,7 @@ def resource_login_providers(
response = {}
for extid in authtoken.effective_user.externalids:
if service is None or extid.service == service:
- response[cast(str, extid.service)] = {
+ response[extid.service] = {
'userid': str(extid.userid),
'username': str(extid.username),
'oauth_token': str(extid.oauth_token),
diff --git a/funnel/views/api/shortlink.py b/funnel/views/api/shortlink.py
index 68c657d41..4a020a4f2 100644
--- a/funnel/views/api/shortlink.py
+++ b/funnel/views/api/shortlink.py
@@ -4,11 +4,11 @@
from furl import furl
from baseframe import _
-from coaster.auth import current_auth
from coaster.utils import getbool
from coaster.views import requestform
from ... import app, shortlinkapp
+from ...auth import current_auth
from ...models import Shortlink, db
from ..helpers import app_url_for, validate_is_app_url
diff --git a/funnel/views/api/sms_events.py b/funnel/views/api/sms_events.py
index 22053843d..932db32aa 100644
--- a/funnel/views/api/sms_events.py
+++ b/funnel/views/api/sms_events.py
@@ -9,10 +9,10 @@
from ... import app
from ...models import (
- SMS_STATUS,
PhoneNumber,
PhoneNumberError,
SmsMessage,
+ SmsStatusEnum,
canonical_phone_number,
db,
sa,
@@ -69,26 +69,26 @@ def process_twilio_event() -> ReturnView:
if request.form['MessageStatus'] == 'queued':
if sms_message:
- sms_message.status = SMS_STATUS.QUEUED
+ sms_message.status = SmsStatusEnum.QUEUED
elif request.form['MessageStatus'] == 'sent':
if phone_number:
phone_number.msg_sms_sent_at = sa.func.utcnow()
if sms_message:
- sms_message.status = SMS_STATUS.PENDING
+ sms_message.status = SmsStatusEnum.PENDING
elif request.form['MessageStatus'] == 'failed':
if phone_number:
phone_number.msg_sms_failed_at = sa.func.utcnow()
if sms_message:
- sms_message.status = SMS_STATUS.FAILED
+ sms_message.status = SmsStatusEnum.FAILED
elif request.form['MessageStatus'] == 'delivered':
if phone_number:
phone_number.msg_sms_delivered_at = sa.func.utcnow()
phone_number.mark_has_sms(True)
if sms_message:
- sms_message.status = SMS_STATUS.DELIVERED
+ sms_message.status = SmsStatusEnum.DELIVERED
else:
if sms_message:
- sms_message.status = SMS_STATUS.UNKNOWN
+ sms_message.status = SmsStatusEnum.UNKNOWN
db.session.commit()
current_app.logger.info(
@@ -158,26 +158,26 @@ def process_exotel_event(secret_token: str) -> ReturnView:
if request.form['Status'] == 'queued':
if sms_message:
- sms_message.status = SMS_STATUS.QUEUED
+ sms_message.status = SmsStatusEnum.QUEUED
elif request.form['Status'] in ('sending', 'submitted'):
if phone_number:
phone_number.msg_sms_sent_at = sa.func.utcnow()
if sms_message:
- sms_message.status = SMS_STATUS.PENDING
+ sms_message.status = SmsStatusEnum.PENDING
elif request.form['Status'] in ('failed', 'failed_dnd'):
if phone_number:
phone_number.msg_sms_failed_at = sa.func.utcnow()
if sms_message:
- sms_message.status = SMS_STATUS.FAILED
+ sms_message.status = SmsStatusEnum.FAILED
elif request.form['Status'] == 'sent':
if phone_number:
phone_number.msg_sms_delivered_at = sa.func.utcnow()
phone_number.mark_has_sms(True)
if sms_message:
- sms_message.status = SMS_STATUS.DELIVERED
+ sms_message.status = SmsStatusEnum.DELIVERED
else:
if sms_message:
- sms_message.status = SMS_STATUS.UNKNOWN
+ sms_message.status = SmsStatusEnum.UNKNOWN
db.session.commit()
current_app.logger.info(
diff --git a/funnel/views/api/support.py b/funnel/views/api/support.py
index d495ae2af..6a314cf39 100644
--- a/funnel/views/api/support.py
+++ b/funnel/views/api/support.py
@@ -52,7 +52,7 @@ def support_callerid(number: str) -> tuple[dict[str, Any], int]:
'error_description': _("Unknown phone number"),
}, 422
- info = {
+ info: dict[str, Any] = {
'number': phone_number.number,
'created_at': phone_number.created_at,
'active_at': phone_number.active_at,
diff --git a/funnel/views/auth_client.py b/funnel/views/auth_client.py
index 6eb7e2e36..364ff9c9b 100644
--- a/funnel/views/auth_client.py
+++ b/funnel/views/auth_client.py
@@ -6,7 +6,6 @@
from baseframe import _
from baseframe.forms import render_delete_sqla, render_form
-from coaster.auth import current_auth
from coaster.views import (
ClassView,
ModelView,
@@ -17,6 +16,7 @@
)
from .. import app
+from ..auth import current_auth
from ..forms import (
AuthClientCredentialForm,
AuthClientForm,
diff --git a/funnel/views/auth_notify.py b/funnel/views/auth_notify.py
index 2162ee0e5..ed536848b 100644
--- a/funnel/views/auth_notify.py
+++ b/funnel/views/auth_notify.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from ..models import Account, AuthToken, LoginSession, Organization, Team
+from ..models import Account, AuthClient, AuthToken, LoginSession, Team
from ..signals import (
org_data_changed,
session_revoked,
@@ -93,7 +93,7 @@ def notify_user_data_changed(user: Account, changes) -> None:
@org_data_changed.connect
def notify_org_data_changed(
- org: Organization, user: Account, changes, team: Team | None = None
+ org: Account, user: Account, changes, team: Team | None = None
) -> None:
"""
Send notifications to trusted auth clients about org data changes.
@@ -101,7 +101,7 @@ def notify_org_data_changed(
Like :func:`notify_user_data_changed`, except also looks at all other owners of this
org to find apps that need to be notified.
"""
- client_users = {}
+ client_users: dict[AuthClient, list[Account]] = {}
for token in AuthToken.all(accounts=org.admin_users):
if (
token.auth_client.trusted
diff --git a/funnel/views/comment.py b/funnel/views/comment.py
index c693a52ea..0c51e73f3 100644
--- a/funnel/views/comment.py
+++ b/funnel/views/comment.py
@@ -6,7 +6,6 @@
from baseframe import _, forms
from baseframe.forms import Form, render_form
-from coaster.auth import current_auth
from coaster.views import (
ClassView,
ModelView,
@@ -18,6 +17,7 @@
)
from .. import app
+from ..auth import current_auth
from ..forms import CommentForm, CommentsetSubscribeForm
from ..models import (
Account,
diff --git a/funnel/views/contact.py b/funnel/views/contact.py
index 062fec84a..2729bce6b 100644
--- a/funnel/views/contact.py
+++ b/funnel/views/contact.py
@@ -10,11 +10,11 @@
from sqlalchemy.exc import IntegrityError
from baseframe import _
-from coaster.auth import current_auth
from coaster.utils import getbool, make_name, midnight_to_utc, utcnow
from coaster.views import ClassView, render_with, requestargs, route
from .. import app
+from ..auth import current_auth
from ..models import ContactExchange, Project, TicketParticipant, db, sa_orm
from ..typing import ReturnRenderWith, ReturnView
from ..utils import format_twitter_handle
@@ -47,7 +47,7 @@ def get_project(self, uuid_b58):
@render_with('contacts.html.jinja2')
def contacts(self) -> ReturnRenderWith:
"""Return contacts grouped by project and date."""
- archived = getbool(request.args.get('archived'))
+ archived = getbool(request.args.get('archived')) or False
return {
'contacts': ContactExchange.grouped_counts_for(
current_auth.user, archived=archived
@@ -104,7 +104,7 @@ def contacts_to_csv(self, contacts, timezone, filename):
@requires_login
def project_date_csv(self, uuid_b58: str, datestr: str) -> ReturnView:
"""Return contacts for a given project and date in CSV format."""
- archived = getbool(request.args.get('archived'))
+ archived = getbool(request.args.get('archived')) or False
project = self.get_project(uuid_b58)
date = datetime.strptime(datestr, '%Y-%m-%d').date()
@@ -121,7 +121,7 @@ def project_date_csv(self, uuid_b58: str, datestr: str) -> ReturnView:
@requires_login
def project_csv(self, uuid_b58: str) -> ReturnView:
"""Return contacts for a given project in CSV format."""
- archived = getbool(request.args.get('archived'))
+ archived = getbool(request.args.get('archived')) or False
project = self.get_project(uuid_b58)
contacts = ContactExchange.contacts_for_project(
diff --git a/funnel/views/decorators.py b/funnel/views/decorators.py
index aa2a5afee..b001c4adc 100644
--- a/funnel/views/decorators.py
+++ b/funnel/views/decorators.py
@@ -6,13 +6,12 @@
from datetime import datetime, timedelta
from functools import wraps
from hashlib import blake2b
-from typing import cast
from flask import Response, make_response, request, url_for
from baseframe import cache
-from coaster.auth import current_auth
+from ..auth import current_auth
from ..proxies import request_wants
from ..typing import P, ReturnResponse, ReturnView, T
from .helpers import compress_response, render_redirect
@@ -59,7 +58,7 @@ def etag_cache_for_user(
timeout: int,
max_age: int | None = None,
query_params: set | None = None,
-) -> Callable[[Callable[P, ReturnView]], Callable[P, Response]]:
+) -> Callable[[Callable[P, ReturnView]], Callable[P, ReturnView]]:
"""
Cache and compress a response, and add an ETag header for browser cache.
@@ -72,9 +71,9 @@ def etag_cache_for_user(
if max_age is None:
max_age = timeout
- def decorator(f: Callable[P, ReturnView]) -> Callable[P, Response]:
+ def decorator(f: Callable[P, ReturnView]) -> Callable[P, ReturnView]:
@wraps(f)
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnResponse:
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnView:
# No ETag or cache storage if the request is not GET or HEAD
if request.method not in ('GET', 'HEAD'):
return f(*args, **kwargs)
@@ -87,7 +86,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnResponse:
# Hash of request args, query parameters and common headers that influence
# output. May need to be expanded to also add headers from Vary (which must
# be specified in the decorator)
- rhash = blake2b(
+ request_hash = blake2b(
'\n'.join(
[
request.headers.get('Accept', ''),
@@ -112,13 +111,12 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnResponse:
).hexdigest()
# 2. Get existing data from cache. There may be multiple copies of data,
- # for each distinct rhash. Look for the one matching our rhash
+ # for each distinct request_hash. Look for the one matching our request_hash
- # Optional[str]
cache_data: dict | None = cache.get(cache_key)
response_data = None
if cache_data:
- rhash_data = cache_data.get(rhash, {})
+ rhash_data = cache_data.get(request_hash, {})
try:
response_data = rhash_data['response_data']
content_encoding = rhash_data['content_encoding']
@@ -155,10 +153,10 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnResponse:
chash = blake2b(response.get_data()).hexdigest()
etag = blake2b(
f'{identifier}/{view_version}/{current_auth.user.uuid_b64}'
- f'/{chash}/{rhash}'.encode()
+ f'/{chash}/{request_hash}'.encode()
).hexdigest()
last_modified = datetime.utcnow()
- cache_data[rhash] = {
+ cache_data[request_hash] = {
'response_data': response_data,
'content_encoding': content_encoding,
'cash': chash,
@@ -177,7 +175,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnResponse:
response.cache_control.max_age = max_age
response.expires = (
response.last_modified or datetime.utcnow()
- ) + timedelta(seconds=cast(int, max_age))
+ ) + timedelta(seconds=max_age)
return response.make_conditional(request)
diff --git a/funnel/views/errors.py b/funnel/views/errors.py
index 4d5eb550d..bdda81e7c 100644
--- a/funnel/views/errors.py
+++ b/funnel/views/errors.py
@@ -2,9 +2,12 @@
from __future__ import annotations
+from typing import cast
+
from flask import current_app, json, redirect, render_template, request
from werkzeug.exceptions import HTTPException, MethodNotAllowed, NotFound
from werkzeug.routing import RequestRedirect
+from werkzeug.wrappers import Response
from .. import app
from ..models import db
@@ -27,7 +30,7 @@ def handle_error(exc: HTTPException) -> ReturnView:
"""Render all errors with a custom template."""
db.session.rollback()
json_response = request_wants.json or request.path.startswith('/api/')
- response = exc.get_response()
+ response = cast(Response, exc.get_response())
if json_response:
response.data = json.dumps(
{
diff --git a/funnel/views/helpers.py b/funnel/views/helpers.py
index 63c43ce40..e4185e9f9 100644
--- a/funnel/views/helpers.py
+++ b/funnel/views/helpers.py
@@ -6,13 +6,12 @@
import zlib
import zoneinfo
from base64 import urlsafe_b64encode
-from collections.abc import Callable
+from collections.abc import Callable, Mapping
from contextlib import nullcontext
from datetime import datetime, timedelta
from hashlib import blake2b
from importlib import resources
from os import urandom
-from typing import Any
from urllib.parse import quote, unquote, urljoin, urlsplit
import brotli
@@ -30,16 +29,17 @@
url_for,
)
from furl import furl
-from pytz import timezone as pytz_timezone, utc
+from pytz import BaseTzInfo, timezone as pytz_timezone, utc
from werkzeug.exceptions import MethodNotAllowed, NotFound
from werkzeug.routing import BuildError, RequestRedirect
+from werkzeug.wrappers import Response as BaseResponse
from baseframe import cache, statsd
-from coaster.auth import current_auth
from coaster.sqlalchemy import RoleMixin
from coaster.utils import utcnow
from .. import app, shortlinkapp
+from ..auth import current_auth
from ..forms import supported_locales
from ..models import Account, Shortlink, db, profanity
from ..proxies import request_wants
@@ -53,9 +53,11 @@
# --- Timezone data --------------------------------------------------------------------
# Get all known timezones from zoneinfo and make a lowercased lookup table
-valid_timezones = {tz.lower(): tz for tz in zoneinfo.available_timezones()}
+valid_timezones = {_tz.lower(): _tz for _tz in zoneinfo.available_timezones()}
# Get timezone aliases from tzinfo.zi and place them in the lookup table
-with resources.open_text('tzdata.zoneinfo', 'tzdata.zi') as _tzdata:
+with (resources.files('tzdata.zoneinfo') / 'tzdata.zi').open(
+ 'r', encoding='utf-8', errors='strict'
+) as _tzdata:
for _tzline in _tzdata.readlines():
if _tzline.startswith('L'):
_tzlink, _tznew, _tzold = _tzline.strip().split()
@@ -90,14 +92,36 @@ def __delitem__(self, key: str) -> None:
self.keys_at.remove(f'{key}_at')
super().__delitem__(key)
- def has_intersection(self, other: Any) -> bool:
+ def has_overlap_with(self, other: Mapping) -> bool:
"""Check for intersection with other dictionary-like object."""
okeys = other.keys()
return not (self.keys_at.isdisjoint(okeys) and self.keys().isdisjoint(okeys))
+ def crosscheck_session(self, response: ResponseType) -> ResponseType:
+ """Add timestamps to timed values in session, and remove expired values."""
+ # Process timestamps only if there is at least one match. Most requests will
+ # have no match.
+ if self.has_overlap_with(session):
+ now = utcnow()
+ for var, delta in self.items():
+ var_at = f'{var}_at'
+ if var in session:
+ if var_at not in session:
+ # Session has var but not timestamp, so add a timestamp
+ session[var_at] = now
+ elif session[var_at] < now - delta:
+ # Session var has expired, so remove var and timestamp
+ session.pop(var)
+ session.pop(var_at)
+ elif var_at in session:
+ # Timestamp present without var, so remove it
+ session.pop(var_at)
+ return response
+
#: Temporary values that must be periodically expunged from the cookie session
session_timeouts = SessionTimeouts()
+app.after_request(session_timeouts.crosscheck_session)
# --- Utilities ------------------------------------------------------------------------
@@ -140,7 +164,11 @@ def app_url_for(
The provided app must have `SERVER_NAME` in its config for URL construction to work.
"""
# pylint: disable=protected-access
- if current_app and current_app._get_current_object() is target_app:
+ if (
+ current_app
+ and current_app._get_current_object() # type: ignore[attr-defined]
+ is target_app
+ ):
return url_for(
endpoint,
_external=_external,
@@ -171,14 +199,16 @@ def app_url_for(
def validate_is_app_url(url: str | furl, method: str = 'GET') -> bool:
"""Confirm if an external URL is served by the current app (runtime-only)."""
# Parse or copy URL and remove username and password before further analysis
- parsed_url = furl(url).remove(username=True, password=True)
+ if isinstance(url, str):
+ url = furl(url)
+ parsed_url = url.remove(username=True, password=True)
if not parsed_url.host or not parsed_url.scheme:
return False # This validator requires a full URL
if current_app.url_map.host_matching:
# This URL adapter matches explicit hosts, so we just give it the URL as its
# server_name
- server_name = parsed_url.netloc
+ server_name = parsed_url.netloc or '' # Fallback blank str for type checking
subdomain = None
else:
# Next, validate whether the URL's host/netloc is valid for this app's config
@@ -192,6 +222,7 @@ def validate_is_app_url(url: str | furl, method: str = 'GET') -> bool:
parsed_url.netloc == server_name
or (
current_app.subdomain_matching
+ and parsed_url.netloc is not None
and parsed_url.netloc.endswith(f'.{server_name}')
)
):
@@ -209,13 +240,13 @@ def validate_is_app_url(url: str | furl, method: str = 'GET') -> bool:
return False
server_name = request.host
- # Host is validated, now make an adapter to match the path
- adapter = current_app.url_map.bind(
- server_name,
- subdomain=subdomain,
- script_name=current_app.config['APPLICATION_ROOT'],
- url_scheme=current_app.config['PREFERRED_URL_SCHEME'],
- )
+ # Host is validated, now make an adapter to match the path
+ adapter = current_app.url_map.bind(
+ server_name,
+ subdomain=subdomain,
+ script_name=current_app.config['APPLICATION_ROOT'],
+ url_scheme=current_app.config['PREFERRED_URL_SCHEME'],
+ )
while True: # Keep looping on redirects
try:
@@ -226,15 +257,21 @@ def validate_is_app_url(url: str | furl, method: str = 'GET') -> bool:
return False
-def localize_micro_timestamp(timestamp, from_tz=utc, to_tz=utc):
+def localize_micro_timestamp(
+ timestamp: float, from_tz: BaseTzInfo | str = utc, to_tz: BaseTzInfo | str = utc
+) -> datetime:
return localize_timestamp(int(timestamp) / 1000, from_tz, to_tz)
-def localize_timestamp(timestamp, from_tz=utc, to_tz=utc):
+def localize_timestamp(
+ timestamp: float, from_tz: BaseTzInfo | str = utc, to_tz: BaseTzInfo | str = utc
+) -> datetime:
return localize_date(datetime.fromtimestamp(int(timestamp)), from_tz, to_tz)
-def localize_date(date, from_tz=utc, to_tz=utc):
+def localize_date(
+ date: datetime, from_tz: BaseTzInfo | str = utc, to_tz: BaseTzInfo | str = utc
+) -> datetime:
if from_tz and to_tz:
if isinstance(from_tz, str):
from_tz = pytz_timezone(from_tz)
@@ -333,10 +370,11 @@ def validate_rate_limit(
:func:`progressive_rate_limit_validator` and its users.
"""
# statsd.set requires an ASCII string. The identifier parameter is typically UGC,
- # meaning it can contain just about any character and any length. The identifier
- # is hashed using BLAKE2b here to bring it down to a meaningful length. It is not
+ # meaning it can contain just about any character and any length. The identifier is
+ # hashed using BLAKE2b here to bring it down to a meaningful length. It is not
# reversible should that be needed for debugging, but the obvious alternative Base64
- # encoding (for convering to 7-bit ASCII) cannot be used as it does not limit length
+ # encoding (for converting to 7-bit ASCII) cannot be used as it does not limit
+ # length
statsd.set(
'rate_limit',
blake2b(identifier.encode(), digest_size=32).hexdigest(),
@@ -470,7 +508,7 @@ def decompress(data: bytes, algorithm: str) -> bytes:
raise ValueError("Unknown compression algorithm")
-def compress_response(response: ResponseType) -> None:
+def compress_response(response: BaseResponse) -> None:
"""
Conditionally compress a response based on request parameters.
@@ -609,29 +647,6 @@ def commit_db_session(response: ResponseType) -> ResponseType:
return response
-@app.after_request
-def track_temporary_session_vars(response: ResponseType) -> ResponseType:
- """Add timestamps to timed values in session, and remove expired values."""
- # Process timestamps only if there is at least one match. Most requests will
- # have no match.
- if session_timeouts.has_intersection(session):
- for var, delta in session_timeouts.items():
- var_at = f'{var}_at'
- if var in session:
- if var_at not in session:
- # Session has var but not timestamp, so add a timestamp
- session[var_at] = utcnow()
- elif session[var_at] < utcnow() - delta:
- # Session var has expired, so remove var and timestamp
- session.pop(var)
- session.pop(var_at)
- elif var_at in session:
- # Timestamp present without var, so remove it
- session.pop(var_at)
-
- return response
-
-
@app.after_request
def cache_expiry_headers(response: ResponseType) -> ResponseType:
if response.expires is None:
diff --git a/funnel/views/jobs.py b/funnel/views/jobs.py
index 4a6f67d0f..78bae910f 100644
--- a/funnel/views/jobs.py
+++ b/funnel/views/jobs.py
@@ -61,7 +61,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T_co:
@rqjob()
def import_tickets(ticket_client_id: int) -> None:
"""Import tickets from Boxoffice."""
- ticket_client = TicketClient.query.get(ticket_client_id)
+ ticket_client = db.session.get(TicketClient, ticket_client_id)
if ticket_client is not None:
if ticket_client.name.lower() == 'explara':
ticket_list = ExplaraAPI(
@@ -79,7 +79,7 @@ def import_tickets(ticket_client_id: int) -> None:
@rqjob()
def tag_locations(project_id: int) -> None:
"""Tag a project with geoname locations. This is legacy code pending a rewrite."""
- project = Project.query.get(project_id)
+ project = db.session.get(Project, project_id)
if project is None:
return
if not project.location:
@@ -116,7 +116,7 @@ def tag_locations(project_id: int) -> None:
project.parsed_location = {'tokens': tokens}
for locdata in geonames.values():
- loc = ProjectLocation.query.get((project_id, locdata['geonameid']))
+ loc = db.session.get(ProjectLocation, (project_id, locdata['geonameid']))
if loc is None:
loc = ProjectLocation(project=project, geonameid=locdata['geonameid'])
db.session.add(loc)
diff --git a/funnel/views/label.py b/funnel/views/label.py
index 6aad55b28..71dc80157 100644
--- a/funnel/views/label.py
+++ b/funnel/views/label.py
@@ -151,7 +151,7 @@ def edit(self) -> ReturnRenderWith:
# existing option
subl = Label.query.filter_by(
project=self.obj.project, name=name
- ).first()
+ ).one()
subl.title = title
subl.icon_emoji = emoji
subl.seq = counter + 1 # Counter is 0-indexed, seq is 1-indexed
diff --git a/funnel/views/login.py b/funnel/views/login.py
index 805b8f700..cdd41b59d 100644
--- a/funnel/views/login.py
+++ b/funnel/views/login.py
@@ -22,11 +22,11 @@
from baseframe import _, __, forms, statsd
from baseframe.forms import render_message
from baseframe.signals import exception_catchall
-from coaster.auth import current_auth
from coaster.utils import getbool, utcnow
from coaster.views import get_next_url, requestargs
from .. import app
+from ..auth import current_auth
from ..forms import (
LoginForm,
LoginPasswordResetException,
@@ -44,6 +44,7 @@
AccountExternalId,
AuthClientCredential,
LoginSession,
+ User,
db,
getextid,
merge_accounts,
@@ -174,7 +175,7 @@ def login() -> ReturnView:
if success:
user = loginform.user
if TYPE_CHECKING:
- assert isinstance(user, Account) # nosec
+ assert isinstance(user, User) # nosec B101
login_internal(user, login_service='password')
db.session.commit()
if loginform.weak_password:
@@ -278,7 +279,7 @@ def login() -> ReturnView:
# Register an account
user = register_internal(None, otp_form.fullname.data, None)
if TYPE_CHECKING:
- assert isinstance(user, Account) # nosec
+ assert isinstance(user, User) # nosec B101
if otp_session.email:
db.session.add(user.add_email(otp_session.email, primary=True))
if otp_session.phone:
@@ -408,7 +409,8 @@ def account_logout() -> ReturnView:
for field_errors in form.errors.values():
for error in field_errors:
- flash(error, 'error')
+ if error is not None:
+ flash(error, 'error')
return render_redirect(url_for('account'))
@@ -456,7 +458,9 @@ def login_service_callback(service: str) -> ReturnView:
return login_service_postcallback(service, userdata)
-def get_user_extid(service, userdata):
+def get_user_extid(
+ service: str, userdata: LoginProviderData
+) -> tuple[Account | None, AccountExternalId | None, AccountEmail | None]:
"""Retrieve user, extid and email from the given service and userdata."""
provider = login_registry[service]
extid = getextid(service=service, userid=userdata.userid)
@@ -587,6 +591,8 @@ def login_service_postcallback(service: str, userdata: LoginProviderData) -> Ret
user.fullname = userdata.fullname
if not current_auth: # If a user isn't already logged in, login now.
+ if TYPE_CHECKING:
+ assert isinstance(user, User) # nosec B101
login_internal(user, login_service=service)
flash(
_("You have logged in via {service}").format(
@@ -629,6 +635,8 @@ def account_merge() -> ReturnView:
if 'merge' in request.form:
new_user = merge_accounts(current_auth.user, other_user)
if new_user is not None:
+ if TYPE_CHECKING:
+ assert isinstance(new_user, User) # nosec B101
login_internal(
new_user,
login_service=current_auth.session.login_service
@@ -759,6 +767,8 @@ def hasjobapp_login_callback(token):
login_session = LoginSession.get(request_token['sessionid'])
if login_session is not None:
user = login_session.account
+ if TYPE_CHECKING:
+ assert isinstance(user, User) # nosec B101
login_internal(user, login_session)
db.session.commit()
flash(_("You are now logged in"), category='success')
diff --git a/funnel/views/login_session.py b/funnel/views/login_session.py
index 294c1be59..d6c0dfc70 100644
--- a/funnel/views/login_session.py
+++ b/funnel/views/login_session.py
@@ -25,11 +25,11 @@
from baseframe import _, __, statsd
from baseframe.forms import render_form
-from coaster.auth import add_auth_attribute, current_auth, request_has_auth
from coaster.utils import utcnow
from coaster.views import get_current_url, get_next_url
from .. import app
+from ..auth import add_auth_attribute, current_auth, request_has_auth
from ..forms import OtpForm, PasswordForm
from ..geoip import GeoIP2Error, geoip
from ..models import (
@@ -49,7 +49,7 @@
from ..proxies import request_wants
from ..serializers import lastuser_serializer
from ..signals import user_login, user_registered
-from ..typing import P, ResponseType, ReturnResponse, T
+from ..typing import P, ResponseType, ReturnResponse, ReturnView, T
from ..utils import abort_null
from .helpers import (
app_context,
@@ -389,7 +389,7 @@ def save_session_next_url() -> bool:
return False
-def reload_for_cookies(f: Callable[P, T]) -> Callable[P, T | ReturnResponse]:
+def reload_for_cookies(f: Callable[P, ReturnView]) -> Callable[P, ReturnView]:
"""
Decorate a view to reload to obtain SameSite=strict cookies.
@@ -405,7 +405,7 @@ def view() -> ReturnView:
"""
@wraps(f)
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ReturnResponse:
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnView:
if 'lastuser' not in request.cookies:
add_auth_attribute('suppress_empty_cookie', True)
attempt = request.args.get('cookiereload')
@@ -426,15 +426,15 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ReturnResponse:
def requires_user_not_spammy(
- get_current: Callable[P, str] | None = None
-) -> Callable[[Callable[P, T]], Callable[P, T | ReturnResponse]]:
+ get_current: Callable[..., str] | None = None
+) -> Callable[[Callable[P, ReturnView]], Callable[P, ReturnView]]:
"""Decorate a view to require the user to prove they are not likely a spammer."""
- def decorator(f: Callable[P, T]) -> Callable[P, T | ReturnResponse]:
+ def decorator(f: Callable[P, ReturnView]) -> Callable[P, ReturnView]:
"""Apply decorator using the specified :attr:`get_current` function."""
@wraps(f)
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ReturnResponse:
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnView:
"""Validate user rights in a view."""
if not current_auth.is_authenticated:
flash(_("Confirm your phone number to continue"), 'info')
@@ -543,7 +543,7 @@ def del_sudo_preference_context() -> None:
session.pop('sudo_context', None)
-def requires_sudo(f: Callable[P, T]) -> Callable[P, T | ReturnResponse]:
+def requires_sudo(f: Callable[P, ReturnView]) -> Callable[P, ReturnView]:
"""
Decorate a view to require the current user to have re-authenticated recently.
@@ -555,7 +555,7 @@ def requires_sudo(f: Callable[P, T]) -> Callable[P, T | ReturnResponse]:
"""
@wraps(f)
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ReturnResponse:
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> ReturnView:
"""Prompt for re-authentication to proceed."""
add_auth_attribute('login_required', True)
# If the user is not logged in, require login first
@@ -661,7 +661,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T | ReturnResponse:
abort(422)
# Allow 5 password or OTP guesses per 60 seconds
- validate_rate_limit('account_sudo', current_auth.user.userid, 5, 60)
+ validate_rate_limit('account_sudo', current_auth.user.uuid_b64, 5, 60)
if form.validate_on_submit():
# User has successfully authenticated. Update their sudo timestamp
# and reload the page with a GET request, as the wrapped view may
diff --git a/funnel/views/membership.py b/funnel/views/membership.py
index 8da6f48cf..5a13a6c2e 100644
--- a/funnel/views/membership.py
+++ b/funnel/views/membership.py
@@ -6,7 +6,6 @@
from baseframe import _
from baseframe.forms import Form, render_form
-from coaster.auth import current_auth
from coaster.views import (
ModelView,
UrlChangeCheck,
@@ -17,6 +16,7 @@
)
from .. import app, signals
+from ..auth import current_auth
from ..forms import (
OrganizationMembershipForm,
ProjectCrewMembershipForm,
diff --git a/funnel/views/mixins.py b/funnel/views/mixins.py
index b87524b15..1cbaadf8c 100644
--- a/funnel/views/mixins.py
+++ b/funnel/views/mixins.py
@@ -10,17 +10,17 @@
from werkzeug.datastructures import MultiDict
from baseframe import _, forms
-from coaster.auth import current_auth
from coaster.views import ModelView, UrlChangeCheck, UrlForView, route
+from ..auth import current_auth
from ..forms import SavedProjectForm
from ..models import (
Account,
Draft,
+ ModelUuidProtocol,
Project,
ProjectRedirect,
TicketEvent,
- UuidModelUnion,
db,
)
from ..typing import ReturnView
@@ -133,14 +133,14 @@ class DraftViewProtoMixin:
model: Any
obj: Any
- def get_draft(self, obj: UuidModelUnion | None = None) -> Draft | None:
+ def get_draft(self, obj: ModelUuidProtocol | None = None) -> Draft | None:
"""
Return the draft object for `obj`. Defaults to `self.obj`.
`obj` is needed in case of multi-model views.
"""
obj = obj if obj is not None else self.obj
- return Draft.query.get((self.model.__tablename__, obj.uuid))
+ return db.session.get(Draft, (self.model.__tablename__, obj.uuid))
def delete_draft(self, obj=None):
"""Delete draft for `obj`, or `self.obj` if `obj` is `None`."""
@@ -151,7 +151,7 @@ def delete_draft(self, obj=None):
raise ValueError(_("There is no draft for the given object"))
def get_draft_data(
- self, obj: UuidModelUnion | None = None
+ self, obj: ModelUuidProtocol | None = None
) -> tuple[None, None] | tuple[UUID | None, dict]:
"""
Return a tuple of draft data.
@@ -163,7 +163,7 @@ def get_draft_data(
return draft.revision, draft.formdata
return None, None
- def autosave_post(self, obj: UuidModelUnion | None = None) -> ReturnView:
+ def autosave_post(self, obj: ModelUuidProtocol | None = None) -> ReturnView:
"""Handle autosave POST requests."""
obj = obj if obj is not None else self.obj
if 'form.revision' not in request.form:
diff --git a/funnel/views/notification.py b/funnel/views/notification.py
index bcc02efc8..57e90fa29 100644
--- a/funnel/views/notification.py
+++ b/funnel/views/notification.py
@@ -9,7 +9,7 @@
from email.utils import formataddr
from functools import wraps
from itertools import islice
-from typing import Any, ClassVar, Literal
+from typing import Any, ClassVar, Literal, cast
from uuid import UUID, uuid4
from flask import url_for
@@ -17,19 +17,20 @@
from werkzeug.utils import cached_property
from baseframe import __, statsd
-from coaster.auth import current_auth
from coaster.sqlalchemy import RoleAccessProxy
from .. import app
+from ..auth import current_auth
from ..models import (
Account,
AccountEmail,
AccountPhone,
+ ModelUuidProtocol,
Notification,
NotificationFor,
NotificationRecipient,
- UuidModelUnion,
db,
+ sa,
)
from ..serializers import token_serializer
from ..transports import TransportError, email, platform_transports, sms
@@ -253,11 +254,15 @@ def unsubscribe_token(self, transport: str) -> str:
# This payload is consumed by :meth:`AccountNotificationView.unsubscribe`
# in `views/notification_preferences.py`
return token_serializer().dumps(
+ # pylint: disable=used-before-assignment
+ # https://github.com/pylint-dev/pylint/issues/8486
{
'buid': self.notification_recipient.recipient.buid,
'notification_type': self.notification.type,
'transport': transport,
- 'hash': self.transport_for(transport).transport_hash,
+ 'hash': anchor.transport_hash
+ if (anchor := self.transport_for(transport)) is not None
+ else '',
}
)
@@ -281,11 +286,15 @@ def unsubscribe_short_url(self, transport: str = 'sms') -> str:
# use this, and can't add utm_* tags to the URL as it only examines the token
# after cleaning up the URL, so there are no more redirects left.
token = make_cached_token(
+ # pylint: disable=used-before-assignment
+ # https://github.com/pylint-dev/pylint/issues/8486
{
'buid': self.notification_recipient.recipient.buid,
'notification_type': self.notification.type,
'transport': transport,
- 'hash': self.transport_for(transport).transport_hash,
+ 'hash': anchor.transport_hash
+ if (anchor := self.transport_for(transport)) is not None
+ else '',
'eventid_b58': self.notification.eventid_b58,
'timestamp': datetime.utcnow(), # Naive timestamp
},
@@ -299,7 +308,7 @@ def unsubscribe_short_url(self, transport: str = 'sms') -> str:
return url_for('notification_unsubscribe_short', token=token, _external=True)
@cached_property
- def fragments_order_by(self) -> list: # TODO: Full spec
+ def fragments_order_by(self) -> list[sa.UnaryExpression]:
"""Provide a list of order_by columns for loading fragments."""
if self.notification.fragment_model is None:
return []
@@ -310,20 +319,21 @@ def fragments_order_by(self) -> list: # TODO: Full spec
]
@property
- def fragments_query_options(self) -> list: # TODO: full spec
+ def fragments_query_options(self) -> Sequence:
"""Provide a list of SQLAlchemy options for loading fragments."""
return []
@cached_property
- def fragments(self) -> list[RoleAccessProxy[UuidModelUnion]]:
- if not self.notification.fragment_model:
+ def fragments(
+ self,
+ ) -> list[RoleAccessProxy[ModelUuidProtocol]]: # type: ignore[type-var] # FIXME
+ query = self.notification_recipient.rolledup_fragments()
+ if query is None:
return []
- query = self.notification_recipient.rolledup_fragments().order_by(
- *self.fragments_order_by
- )
- if self.fragments_query_options:
- query = query.options(*self.fragments_query_options)
+ query = query.order_by(*self.fragments_order_by)
+ if query_options := self.fragments_query_options:
+ query = query.options(*query_options)
return [
_f.access_for(actor=self.notification_recipient.recipient)
@@ -358,7 +368,7 @@ def web(self) -> str:
@property
def email_base_url(self) -> str:
"""Base URL for relative links in email."""
- return self.notification.role_provider_obj.absolute_url
+ return cast(str, self.notification.role_provider_obj.absolute_url)
def email_subject(self) -> str:
"""
@@ -388,6 +398,12 @@ def email_from(self) -> str:
return f"{self.notification.preference_context.title} (via Hasgeek)"
return "Hasgeek"
+ def sms_with_unsubscribe(self) -> sms.SmsTemplate:
+ """Add an unsubscribe link to the SMS message."""
+ msg = self.sms()
+ msg.unsubscribe_url = self.unsubscribe_short_url('sms')
+ return msg
+
def sms(self) -> sms.SmsTemplate:
"""
Render a short text message. Templates must use a single line with a link.
@@ -400,12 +416,6 @@ def text(self) -> str:
"""Render a short plain text notification using the SMS template."""
return self.sms().text
- def sms_with_unsubscribe(self) -> sms.SmsTemplate:
- """Add an unsubscribe link to the SMS message."""
- msg = self.sms()
- msg.unsubscribe_url = self.unsubscribe_short_url('sms')
- return msg
-
def webpush(self) -> str:
"""
Render a web push notification.
@@ -499,7 +509,7 @@ def transport_worker_wrapper(
def inner(notification_recipient_ids: Sequence[tuple[int, UUID]]) -> None:
"""Convert a notification id into an object for worker to process."""
queue = [
- NotificationRecipient.query.get(identity)
+ db.session.get(NotificationRecipient, identity)
for identity in notification_recipient_ids
]
for notification_recipient in queue:
@@ -617,7 +627,9 @@ def dispatch_transport_sms(
@rqjob()
def dispatch_notification_job(eventid: UUID, notification_ids: Sequence[UUID]) -> None:
"""Process :class:`Notification` into batches of :class:`UserNotification`."""
- notifications = [Notification.query.get((eventid, nid)) for nid in notification_ids]
+ notifications = [
+ db.session.get(Notification, (eventid, nid)) for nid in notification_ids
+ ]
# Dispatch, creating batches of DISPATCH_BATCH_SIZE each
for notification in notifications:
@@ -645,7 +657,7 @@ def dispatch_notification_recipients_job(
"""Process notifications for users and enqueue transport delivery."""
# TODO: Can this be a single query instead of a loop of queries?
queue = [
- NotificationRecipient.query.get(identity)
+ db.session.get(NotificationRecipient, identity)
for identity in notification_recipient_ids
]
transport_batch: dict[str, list[tuple[int, UUID]]] = defaultdict(list)
diff --git a/funnel/views/notification_feed.py b/funnel/views/notification_feed.py
index df3faeb9d..452dd996d 100644
--- a/funnel/views/notification_feed.py
+++ b/funnel/views/notification_feed.py
@@ -5,10 +5,10 @@
from flask import abort
from baseframe import forms
-from coaster.auth import current_auth
from coaster.views import ClassView, render_with, requestargs, route
from .. import app
+from ..auth import current_auth
from ..models import NotificationRecipient, db
from ..typing import ReturnRenderWith
from .login_session import requires_login
diff --git a/funnel/views/notification_preferences.py b/funnel/views/notification_preferences.py
index 52c6a54fa..99f4fd4f3 100644
--- a/funnel/views/notification_preferences.py
+++ b/funnel/views/notification_preferences.py
@@ -9,11 +9,11 @@
from baseframe import _, __
from baseframe.forms import render_form, render_message
-from coaster.auth import current_auth
from coaster.utils import getbool
from coaster.views import ClassView, render_with, requestargs, route
from .. import app
+from ..auth import current_auth
from ..forms import SetNotificationPreferenceForm, UnsubscribeForm, transport_labels
from ..models import (
Account,
diff --git a/funnel/views/notifications/comment_notification.py b/funnel/views/notifications/comment_notification.py
index 574066536..75d007cbd 100644
--- a/funnel/views/notifications/comment_notification.py
+++ b/funnel/views/notifications/comment_notification.py
@@ -1,7 +1,9 @@
-"""̌Comment noficiations."""
+"""Comment notifications."""
from __future__ import annotations
+from typing import cast
+
from flask import render_template, url_for
from markupsafe import Markup, escape
from werkzeug.utils import cached_property
@@ -15,7 +17,6 @@
CommentReplyNotification,
CommentReportReceivedNotification,
Commentset,
- DuckTypeAccount,
NewCommentNotification,
Project,
Proposal,
@@ -129,10 +130,16 @@ class CommentNotification(RenderNotification):
hero_image = 'img/email/chars-v1/comment.png'
email_heading = __("New comment!")
+ @cached_property
+ def commentset(self) -> Commentset:
+ if isinstance(self.document, Commentset):
+ return self.document
+ return self.document.commentset
+
@property
- def actor(self) -> Account | DuckTypeAccount:
+ def actor(self) -> Account:
"""Actor who commented."""
- return self.comment.posted_by
+ return cast(Account, self.comment.posted_by)
@cached_property
def commenters(self) -> list[Account]:
@@ -150,16 +157,16 @@ def commenters(self) -> list[Account]:
@property
def project(self) -> Project | None:
- if self.document_type == 'project':
- return self.document.project
- if self.document_type == 'proposal':
- return self.document.proposal.project
+ if (document_type := self.document_type) == 'project':
+ return self.commentset.project
+ if document_type == 'proposal':
+ return self.commentset.proposal.project # type: ignore[union-attr]
return None
@property
def proposal(self) -> Proposal | None:
if self.document_type == 'proposal':
- return self.document.proposal
+ return self.commentset.proposal
return None
@property
@@ -167,25 +174,25 @@ def document_type(self) -> str:
"""Return type of document this comment is on ('comment' for replies)."""
if self.notification.document_type == 'comment':
return 'comment'
- return self.document.parent_type
+ return self.commentset.parent_type
def document_comments_url(self, **kwargs) -> str:
"""URL to comments view on the document."""
- if self.document_type == 'project':
- return self.document.parent.url_for('comments', **kwargs)
- if self.document_type == 'proposal':
- return self.document.parent.url_for('view', **kwargs) + '#comments'
+ if (document_type := self.document_type) == 'project':
+ return self.commentset.parent.url_for('comments', **kwargs)
+ if document_type == 'proposal':
+ return self.commentset.parent.url_for('view', **kwargs) + '#comments'
return self.document.url_for('view', **kwargs)
def activity_template_standalone(self, comment: Comment | None = None) -> str:
"""Activity template for standalone use, such as email subject."""
if comment is None:
comment = self.comment
- if self.document_type == 'comment':
+ if (document_type := self.document_type) == 'comment':
return _("{actor} replied to your comment in {project}")
- if self.document_type == 'project':
+ if document_type == 'project':
return _("{actor} commented in {project}")
- if self.document_type == 'proposal':
+ if document_type == 'proposal':
return _("{actor} commented on {proposal}")
# Unknown document type
return _("{actor} replied to you")
@@ -194,11 +201,11 @@ def activity_template_inline(self, comment: Comment | None = None) -> str:
"""Activity template for inline use with other content, like SMS with URL."""
if comment is None:
comment = self.comment
- if self.document_type == 'comment':
+ if (document_type := self.document_type) == 'comment':
return _("{actor} replied to your comment in {project}:")
- if self.document_type == 'project':
+ if document_type == 'project':
return _("{actor} commented in {project}:")
- if self.document_type == 'proposal':
+ if document_type == 'proposal':
return _("{actor} commented on {proposal}:")
# Unknown document type
return _("{actor} replied to you:")
diff --git a/funnel/views/notifications/organization_membership_notification.py b/funnel/views/notifications/organization_membership_notification.py
index 97faf526e..0c7737e42 100644
--- a/funnel/views/notifications/organization_membership_notification.py
+++ b/funnel/views/notifications/organization_membership_notification.py
@@ -8,16 +8,19 @@
from flask import render_template
from markupsafe import Markup, escape
+from werkzeug.utils import cached_property
from baseframe import _, __
from ...models import (
Account,
AccountMembership,
+ MembershipRecordTypeEnum,
+ Notification,
NotificationRecipient,
- NotificationType,
OrganizationAdminMembershipNotification,
OrganizationAdminMembershipRevokedNotification,
+ sa,
)
from ...transports.sms import MessageTemplate
from ..notification import DecisionBranchBase, DecisionFactorBase, RenderNotification
@@ -29,7 +32,7 @@ class DecisionFactorFields:
is_member: bool | None = None
for_actor: bool | None = None
- rtypes: Collection[str] = ()
+ rtypes: Collection[MembershipRecordTypeEnum] = ()
is_owner: bool | None = None
is_actor: bool | None = None
@@ -43,7 +46,7 @@ def is_match(
return (
(self.is_member is None or self.is_member is is_member)
and (self.for_actor is None or self.for_actor is for_actor)
- and (not self.rtypes or membership.record_type_label.name in self.rtypes)
+ and (not self.rtypes or membership.record_type_enum in self.rtypes)
and (self.is_owner is None or self.is_owner is membership.is_owner)
and (self.is_actor is None or (self.is_actor is membership.is_self_granted))
)
@@ -63,7 +66,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
grant_amend_templates = DecisionBranch(
factors=[
DecisionBranch(
- rtypes=['invite'],
+ rtypes=[MembershipRecordTypeEnum.INVITE],
factors=[
DecisionBranch(
for_actor=False,
@@ -121,7 +124,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
],
),
DecisionBranch(
- rtypes=['direct_add'],
+ rtypes=[MembershipRecordTypeEnum.DIRECT_ADD],
factors=[
DecisionBranch(
for_actor=False,
@@ -169,7 +172,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
],
),
DecisionBranch(
- rtypes=['accept'],
+ rtypes=[MembershipRecordTypeEnum.ACCEPT],
factors=[
DecisionBranch(
for_actor=False,
@@ -229,7 +232,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
],
),
DecisionBranch(
- rtypes=['amend'],
+ rtypes=[MembershipRecordTypeEnum.AMEND],
factors=[
DecisionBranch(
for_actor=False,
@@ -345,7 +348,7 @@ class RenderShared:
organization: Account
membership: AccountMembership
- notification: NotificationType
+ notification: Notification
notification_recipient: NotificationRecipient
template_picker: DecisionBranch
@@ -439,7 +442,9 @@ class RenderOrganizationAdminMembershipNotification(RenderShared, RenderNotifica
email_heading = __("Membership granted!")
template_picker = grant_amend_templates
- fragments_order_by = [AccountMembership.granted_at.desc()]
+ @cached_property
+ def fragments_order_by(self) -> list[sa.UnaryExpression]:
+ return [AccountMembership.granted_at.desc()]
def membership_actor(
self, membership: AccountMembership | None = None
@@ -472,7 +477,9 @@ class RenderOrganizationAdminMembershipRevokedNotification(
email_heading = __("Membership revoked")
template_picker = revoke_templates
- fragments_order_by = [AccountMembership.revoked_at.desc()]
+ @cached_property
+ def fragments_order_by(self) -> list[sa.UnaryExpression]:
+ return [AccountMembership.revoked_at.desc()]
def membership_actor(
self, membership: AccountMembership | None = None
diff --git a/funnel/views/notifications/project_crew_notification.py b/funnel/views/notifications/project_crew_notification.py
index e6e3d1227..3c7c08109 100644
--- a/funnel/views/notifications/project_crew_notification.py
+++ b/funnel/views/notifications/project_crew_notification.py
@@ -4,21 +4,24 @@
from collections.abc import Callable, Collection
from dataclasses import dataclass
-from typing import ClassVar
+from typing import TYPE_CHECKING, ClassVar
from flask import render_template
from markupsafe import Markup, escape
+from werkzeug.utils import cached_property
from baseframe import _, __
from ...models import (
Account,
+ MembershipRecordTypeEnum,
+ Notification,
NotificationRecipient,
- NotificationType,
Project,
ProjectCrewMembershipNotification,
ProjectCrewMembershipRevokedNotification,
ProjectMembership,
+ sa,
)
from ...transports.sms import OneLineTemplate
from ..helpers import shortlink
@@ -31,7 +34,7 @@ class DecisionFactorFields:
is_member: bool | None = None
for_actor: bool | None = None
- rtypes: Collection[str] = ()
+ rtypes: Collection[MembershipRecordTypeEnum] = ()
is_editor: bool | None = None
is_promoter: bool | None = None
is_usher: bool | None = None
@@ -46,7 +49,7 @@ def is_match(
return (
(self.is_member is None or self.is_member is is_member)
and (self.for_actor is None or self.for_actor is for_actor)
- and (not self.rtypes or membership.record_type_label.name in self.rtypes)
+ and (not self.rtypes or membership.record_type_enum in self.rtypes)
and (self.is_editor is None or self.is_editor is membership.is_editor)
and (self.is_promoter is None or self.is_promoter is membership.is_promoter)
and (self.is_usher is None or self.is_usher is membership.is_usher)
@@ -77,7 +80,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
grant_amend_templates = DecisionBranch(
factors=[
DecisionBranch(
- rtypes=['invite'],
+ rtypes=[MembershipRecordTypeEnum.INVITE],
factors=[
DecisionBranch(
for_actor=False,
@@ -142,7 +145,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
"{actor} invited you to join the crew of {project}"
),
is_member=True,
- rtypes=['invite'],
+ rtypes=[MembershipRecordTypeEnum.INVITE],
),
],
),
@@ -172,7 +175,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
template=__(
"You invited {user} to join the crew of {project}"
),
- rtypes=['invite'],
+ rtypes=[MembershipRecordTypeEnum.INVITE],
for_actor=True,
),
],
@@ -180,7 +183,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
],
),
DecisionBranch(
- rtypes=['accept'],
+ rtypes=[MembershipRecordTypeEnum.ACCEPT],
factors=[
DecisionBranch(
for_actor=False,
@@ -248,7 +251,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
],
),
DecisionBranch(
- rtypes=['direct_add'],
+ rtypes=[MembershipRecordTypeEnum.DIRECT_ADD],
factors=[
DecisionBranch(
for_actor=False,
@@ -344,7 +347,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
),
DecisionFactor(
template=__("You made {user} promoter of {project}"),
- rtypes=['direct_add'],
+ rtypes=[MembershipRecordTypeEnum.DIRECT_ADD],
is_promoter=True,
),
DecisionFactor(
@@ -377,7 +380,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
],
),
DecisionBranch(
- rtypes=['amend'],
+ rtypes=[MembershipRecordTypeEnum.AMEND],
factors=[
DecisionBranch(
for_actor=False,
@@ -530,7 +533,7 @@ class DecisionBranch(DecisionFactorFields, DecisionBranchBase):
template=__(
"You changed your role to crew member of {project}"
),
- rtypes=['amend'],
+ rtypes=[MembershipRecordTypeEnum.AMEND],
is_self_granted=True,
),
],
@@ -667,7 +670,7 @@ class RenderShared:
project: Project
membership: ProjectMembership
- notification: NotificationType
+ notification: Notification
notification_recipient: NotificationRecipient
#: Subclasses must specify a base template picker
template_picker: DecisionBranch
@@ -771,12 +774,18 @@ class RenderProjectCrewMembershipNotification(RenderShared, RenderNotification):
aliases = {'document': 'project', 'fragment': 'membership'}
hero_image = 'img/email/chars-v1/access-granted.png'
email_heading = __("Crew membership granted!")
- fragments_order_by = [ProjectMembership.granted_at.desc()]
template_picker = grant_amend_templates
+ @cached_property
+ def fragments_order_by(self) -> list[sa.UnaryExpression]:
+ return [ProjectMembership.granted_at.desc()]
+
def membership_actor(self, membership: ProjectMembership | None = None) -> Account:
"""Actual actor who granted (or edited) the membership, for the template."""
- return (membership or self.membership).granted_by
+ actor = (membership or self.membership).granted_by
+ if TYPE_CHECKING:
+ assert actor is not None # nosec B101
+ return actor
def web(self):
return render_template(
diff --git a/funnel/views/notifications/proposal_notification.py b/funnel/views/notifications/proposal_notification.py
index f80052951..9a13ec1c9 100644
--- a/funnel/views/notifications/proposal_notification.py
+++ b/funnel/views/notifications/proposal_notification.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from collections.abc import Sequence
+
from flask import render_template
from werkzeug.utils import cached_property
@@ -12,6 +14,7 @@
Proposal,
ProposalReceivedNotification,
ProposalSubmittedNotification,
+ sa,
sa_orm,
)
from ...transports.sms import SmsPriority, SmsTemplate
@@ -71,11 +74,11 @@ class RenderProposalReceivedNotification(RenderNotification):
email_heading = __("New submission!")
@cached_property
- def fragments_order_by(self):
+ def fragments_order_by(self) -> list[sa.UnaryExpression]:
return [Proposal.datetime.desc()]
@property
- def fragments_query_options(self):
+ def fragments_query_options(self) -> Sequence:
return [
sa_orm.load_only(
Proposal.name, Proposal.title, Proposal.project_id, Proposal.uuid
@@ -123,7 +126,7 @@ class RenderProposalSubmittedNotification(RenderNotification):
emoji_prefix = "📤 "
reason = __("You are receiving this because you made this submission")
hero_image = 'img/email/chars-v1/sent-submission.png'
- email_heading = __("Proposal sumbitted!")
+ email_heading = __("Proposal submitted!")
def web(self) -> str:
return render_template(
diff --git a/funnel/views/organization.py b/funnel/views/organization.py
index bf62a912f..08b3b18a8 100644
--- a/funnel/views/organization.py
+++ b/funnel/views/organization.py
@@ -6,10 +6,10 @@
from baseframe import _
from baseframe.forms import render_delete_sqla, render_form, render_message
-from coaster.auth import current_auth
from coaster.views import ModelView, UrlChangeCheck, UrlForView, requires_roles, route
from .. import app
+from ..auth import current_auth
from ..forms import OrganizationForm, TeamForm
from ..models import Account, Organization, Team, db
from ..signals import org_data_changed, team_data_changed
diff --git a/funnel/views/otp.py b/funnel/views/otp.py
index cc9c7b711..16746f68d 100644
--- a/funnel/views/otp.py
+++ b/funnel/views/otp.py
@@ -12,15 +12,16 @@
from werkzeug.utils import cached_property
from baseframe import _
-from coaster.auth import current_auth
from coaster.utils import newpin, require_one_of
from .. import app
+from ..auth import current_auth
from ..models import (
Account,
AccountEmail,
AccountEmailClaim,
AccountPhone,
+ Anchor,
EmailAddress,
EmailAddressBlockedError,
PhoneNumber,
@@ -134,7 +135,7 @@ def make(
cls: type[OtpSessionType],
reason: str,
user: OptionalAccountType,
- anchor: AccountEmail | AccountEmailClaim | AccountPhone | EmailAddress | None,
+ anchor: Anchor | None,
phone: str | None = None,
email: str | None = None,
) -> OtpSessionType:
diff --git a/funnel/views/profile.py b/funnel/views/profile.py
index 455dd3e2f..e253843d0 100644
--- a/funnel/views/profile.py
+++ b/funnel/views/profile.py
@@ -2,12 +2,13 @@
from __future__ import annotations
+from typing import Any
+
from flask import abort, current_app, flash, render_template, request
from baseframe import _
from baseframe.filters import date_filter
from baseframe.forms import render_form
-from coaster.auth import current_auth
from coaster.views import (
UrlChangeCheck,
get_next_url,
@@ -18,6 +19,7 @@
)
from .. import app
+from ..auth import current_auth
from ..forms import (
ProfileBannerForm,
ProfileForm,
@@ -77,7 +79,7 @@ class ProfileView(UrlChangeCheck, AccountViewBase):
@render_with({'text/html': template_switcher}, json=True)
def view(self) -> ReturnRenderWith:
template_name = None
- ctx = {}
+ ctx: dict[str, Any] = {}
if self.obj.is_user_profile:
template_name = 'user_profile.html.jinja2'
@@ -173,7 +175,7 @@ def view(self) -> ReturnRenderWith:
# If the user is an admin of this account, show all draft projects.
# Else, only show the drafts they have a crew role in
if self.obj.current_roles.admin:
- draft_projects = self.obj.draft_projects
+ draft_projects: list[Project] = self.obj.draft_projects.all()
unscheduled_projects = self.obj.projects.filter(
Project.state.PUBLISHED_WITHOUT_SESSIONS
).all()
@@ -278,7 +280,7 @@ def user_proposals(self) -> ReturnRenderWith:
@route('past.projects')
@requestargs(('page', int), ('per_page', int))
@render_with('past_projects_section.html.jinja2')
- def past_projects(self, page: int = 1, per_page: int = 10) -> ReturnView:
+ def past_projects(self, page: int = 1, per_page: int = 10) -> ReturnRenderWith:
projects = self.obj.listed_projects.order_by(None)
past_projects = projects.filter(Project.state.PAST).order_by(
Project.start_at.desc()
@@ -305,7 +307,7 @@ def past_projects(self, page: int = 1, per_page: int = 10) -> ReturnView:
@route('past.sessions')
@requestargs(('page', int), ('per_page', int))
@render_with('past_sessions_section.html.jinja2')
- def past_sessions(self, page: int = 1, per_page: int = 10) -> ReturnView:
+ def past_sessions(self, page: int = 1, per_page: int = 10) -> ReturnRenderWith:
featured_sessions = (
Session.query.join(Project, Session.project_id == Project.id)
.filter(
diff --git a/funnel/views/project.py b/funnel/views/project.py
index 98d713f55..5a41ec5c2 100644
--- a/funnel/views/project.py
+++ b/funnel/views/project.py
@@ -12,11 +12,11 @@
from baseframe import _, __, forms
from baseframe.forms import render_delete_sqla, render_form, render_message
-from coaster.auth import current_auth
from coaster.utils import getbool, make_name
from coaster.views import get_next_url, render_with, requires_roles, route
from .. import app
+from ..auth import current_auth
from ..forms import (
CfpForm,
ProjectBannerForm,
@@ -30,13 +30,13 @@
ProjectTransitionForm,
)
from ..models import (
- PROJECT_RSVP_STATE,
- RSVP_STATUS,
Account,
Project,
+ ProjectRsvpStateEnum,
RegistrationCancellationNotification,
RegistrationConfirmationNotification,
Rsvp,
+ RsvpStateEnum,
SavedProject,
db,
sa,
@@ -164,9 +164,9 @@ def feature_project_rsvp(obj: Project) -> bool:
obj.state.PUBLISHED
and (obj.start_at is None or not obj.state.PAST)
and (
- obj.rsvp_state == PROJECT_RSVP_STATE.ALL
+ obj.rsvp_state == ProjectRsvpStateEnum.ALL
or (
- obj.rsvp_state == PROJECT_RSVP_STATE.MEMBERS
+ obj.rsvp_state == ProjectRsvpStateEnum.MEMBERS
and obj.current_roles.account_member
)
)
@@ -178,7 +178,7 @@ def feature_project_rsvp_for_members(obj: Project) -> bool:
return bool(
obj.state.PUBLISHED
and (obj.start_at is None or not obj.state.PAST)
- and obj.rsvp_state == PROJECT_RSVP_STATE.MEMBERS
+ and obj.rsvp_state == ProjectRsvpStateEnum.MEMBERS
)
@@ -222,7 +222,7 @@ def feature_project_register(obj: Project) -> bool:
@Project.features('rsvp_registered', cached_property=True)
def feature_project_deregister(obj: Project) -> bool:
rsvp = obj.rsvp_for(current_auth.user)
- return rsvp is not None and rsvp.state.YES
+ return rsvp is not None and bool(rsvp.state.YES)
@Project.features('schedule_no_sessions')
@@ -744,7 +744,7 @@ def rsvp_list(self) -> ReturnRenderWith:
'project': self.obj.current_access(datasets=('primary', 'related')),
'going_rsvps': [
_r.current_access(datasets=('without_parent', 'related', 'related'))
- for _r in self.obj.rsvps_with(RSVP_STATUS.YES)
+ for _r in self.obj.rsvps_with(RsvpStateEnum.YES)
],
'rsvp_form_fields': [
field.get('name', '')
@@ -755,7 +755,7 @@ def rsvp_list(self) -> ReturnRenderWith:
else None,
}
- def get_rsvp_state_csv(self, state):
+ def get_rsvp_state_csv(self, state: RsvpStateEnum) -> Response:
"""Export participant list as a CSV."""
outfile = io.StringIO(newline='')
out = csv.writer(outfile)
@@ -789,14 +789,14 @@ def get_rsvp_state_csv(self, state):
@requires_roles({'promoter'})
def rsvp_list_yes_csv(self) -> ReturnView:
"""Return a CSV of RSVP participants who answered Yes."""
- return self.get_rsvp_state_csv(state=RSVP_STATUS.YES)
+ return self.get_rsvp_state_csv(RsvpStateEnum.YES)
@route('rsvp_list/maybe.csv')
@requires_login
@requires_roles({'promoter'})
def rsvp_list_maybe_csv(self) -> ReturnView:
"""Return a CSV of RSVP participants who answered Maybe."""
- return self.get_rsvp_state_csv(state=RSVP_STATUS.MAYBE)
+ return self.get_rsvp_state_csv(RsvpStateEnum.MAYBE)
@route('save', methods=['POST'])
@requires_login
diff --git a/funnel/views/project_sponsor.py b/funnel/views/project_sponsor.py
index 1360d3dc4..a64456449 100644
--- a/funnel/views/project_sponsor.py
+++ b/funnel/views/project_sponsor.py
@@ -9,11 +9,11 @@
from baseframe import _
from baseframe.forms import Form
from baseframe.forms.auto import ConfirmDeleteForm
-from coaster.auth import current_auth
from coaster.utils import getbool
from coaster.views import ModelView, UrlChangeCheck, UrlForView, requestform, route
from .. import app
+from ..auth import current_auth
from ..forms import ProjectSponsorForm
from ..models import Account, Project, ProjectSponsorMembership, db, sa_orm
from ..typing import ReturnView
diff --git a/funnel/views/proposal.py b/funnel/views/proposal.py
index 5e48b89ce..3f809d81a 100644
--- a/funnel/views/proposal.py
+++ b/funnel/views/proposal.py
@@ -6,7 +6,6 @@
from baseframe import _, __
from baseframe.forms import Form, render_delete_sqla, render_form, render_template
-from coaster.auth import current_auth
from coaster.utils import getbool, make_name
from coaster.views import (
ModelView,
@@ -19,6 +18,7 @@
)
from .. import app
+from ..auth import current_auth
from ..forms import (
ProposalFeaturedForm,
ProposalForm,
@@ -72,9 +72,9 @@ class ProjectProposalView(ProjectViewBase):
@route('sub/new', methods=['GET', 'POST'])
@route('proposals/new', methods=['GET', 'POST'])
@requires_login
- @render_with('submission_form.html.jinja2')
@requires_roles({'reader'})
@requires_user_not_spammy()
+ @render_with('submission_form.html.jinja2')
def new_proposal(self) -> ReturnRenderWith:
# This along with the `reader` role makes it possible for
# anyone to submit a proposal if the CFP is open.
diff --git a/funnel/views/schedule.py b/funnel/views/schedule.py
index 82bc496a7..cc3058801 100644
--- a/funnel/views/schedule.py
+++ b/funnel/views/schedule.py
@@ -5,12 +5,12 @@
from collections import defaultdict
from datetime import timedelta
from types import SimpleNamespace
-from typing import Any, cast
+from typing import TYPE_CHECKING, Any, cast
from flask import Response, current_app, json
from icalendar import Alarm, Calendar, Event, vCalAddress, vText
from pytz import utc
-from sqlalchemy.orm.exc import NoResultFound
+from sqlalchemy.exc import NoResultFound
from baseframe import _, localize_timezone
from coaster.utils import utcnow
@@ -69,7 +69,9 @@ def session_data(
def session_list_data(
- sessions: list[Session], with_modal_url=False, with_delete_url=False
+ sessions: list[Session],
+ with_modal_url: str | None = None,
+ with_delete_url: bool = False,
):
return [
session_data(session, with_modal_url, with_delete_url) for session in sessions
@@ -115,7 +117,7 @@ def schedule_data(
def schedule_ical(
project: Project, rsvp: Rsvp | None = None, future_only: bool = False
-):
+) -> bytes:
cal = Calendar()
cal.add('prodid', "-//HasGeek//NONSGML Funnel//EN")
cal.add('version', '2.0')
@@ -130,7 +132,7 @@ def schedule_ical(
cal.add('x-published-ttl', 'PT12H')
now = utcnow()
for session in project.scheduled_sessions:
- if not future_only or session.end_at > now:
+ if not future_only or (session.end_at is not None and session.end_at > now):
cal.add_component(session_ical(session, rsvp))
if not project.scheduled_sessions and project.start_at:
cal.add_component(
@@ -291,10 +293,12 @@ def edit_schedule(self) -> ReturnRenderWith:
'project': self.obj,
'proposals': proposals,
'from_date': (
- self.obj.start_at_localized.isoformat() if self.obj.start_at else None
+ start_at.isoformat()
+ if (start_at := self.obj.start_at_localized)
+ else None
),
'to_date': (
- self.obj.end_at_localized.isoformat() if self.obj.end_at else None
+ end_at.isoformat() if (end_at := self.obj.end_at_localized) else None
),
'timezone': self.obj.timezone.zone,
'venues': [venue.current_access() for venue in self.obj.venues],
@@ -400,6 +404,9 @@ def updates(self) -> ReturnRenderWith:
.first()
)
if current_session is not None:
+ if TYPE_CHECKING:
+ assert current_session.start_at is not None # nosec B101
+ assert current_session.end_at is not None # nosec B101
current_session.start_at = localize_date(
current_session.start_at, to_tz=self.obj.venue.project.timezone
)
@@ -408,14 +415,18 @@ def updates(self) -> ReturnRenderWith:
)
nextdiff = None
if next_session is not None:
+ if TYPE_CHECKING:
+ assert next_session.start_at is not None # nosec B101
+ assert next_session.end_at is not None # nosec B101
next_session.start_at = localize_date(
next_session.start_at, to_tz=self.obj.venue.project.timezone
)
next_session.end_at = localize_date(
next_session.end_at, to_tz=self.obj.venue.project.timezone
)
- nextdiff = next_session.start_at.date() - now.date()
- nextdiff = nextdiff.total_seconds() / 86400
+ nextdiff = (
+ next_session.start_at.date() - now.date()
+ ).total_seconds() / 86400
return {
'room': self.obj,
'current_session': current_session,
diff --git a/funnel/views/search.py b/funnel/views/search.py
index 3bfa89659..86bd71765 100644
--- a/funnel/views/search.py
+++ b/funnel/views/search.py
@@ -4,7 +4,7 @@
import re
from html import unescape as html_unescape
-from typing import Any, TypedDict, TypeVar
+from typing import Any, ClassVar, Generic, TypedDict, TypeVar
from urllib.parse import quote as urlquote
from flask import request, url_for
@@ -19,11 +19,11 @@
Account,
Comment,
Commentset,
+ ModelSearchProtocol,
Project,
Proposal,
ProposalMembership,
Query,
- SearchModelUnion,
Session,
Update,
db,
@@ -39,6 +39,7 @@
# --- Definitions ----------------------------------------------------------------------
_Q = TypeVar('_Q', bound=Query)
+_ST = TypeVar('_ST', bound=ModelSearchProtocol)
# PostgreSQL ts_headline markers
@@ -61,19 +62,19 @@
# --- Search provider types ------------------------------------------------------------
-class SearchProvider:
+class SearchProvider(Generic[_ST]):
"""Base class for search providers."""
#: Label to use in UI
label: str
#: Model to query against
- model: type[SearchModelUnion]
+ model: type[_ST]
#: Does this model have a title column?
- has_title: bool = True
+ has_title: ClassVar[bool] = True
@property
def regconfig(self) -> str:
- """Return PostgreSQL regconfig language, defaulting to English."""
+ """Return PostgreSQL `regconfig` language, defaulting to English."""
return self.model.search_vector.type.options.get('regconfig', 'english')
@property
@@ -83,7 +84,7 @@ def title_column(self) -> sa.ColumnElement[str]:
# That makes this return value incorrect, but here we ignore the error as
# class:`CommentSearch` explicitly overrides :meth:`hltitle_column`, and that is
# the only place this property is accessed
- return self.model.title # type: ignore[return-value]
+ return self.model.title # type: ignore[return-value] # FIXME
@property
def hltext(self) -> sa.ColumnElement[str]:
@@ -145,10 +146,10 @@ def all_query(self, tsquery: sa.Function) -> Query:
def all_count(self, tsquery: sa.Function) -> int:
"""Return count of results for :meth:`all_query`."""
- return self.all_query(tsquery).options(sa_orm.load_only(self.model.id)).count()
+ return self.all_query(tsquery).options(sa_orm.load_only(self.model.id_)).count()
-class SearchInAccountProvider(SearchProvider):
+class SearchInAccountProvider(SearchProvider[_ST]):
"""Base class for search providers that support searching in an account."""
def account_query(self, tsquery: sa.Function, account: Account) -> Query:
@@ -159,12 +160,12 @@ def account_count(self, tsquery: sa.Function, account: Account) -> int:
"""Return count of results for :meth:`account_query`."""
return (
self.account_query(tsquery, account)
- .options(sa_orm.load_only(self.model.id))
+ .options(sa_orm.load_only(self.model.id_))
.count()
)
-class SearchInProjectProvider(SearchInAccountProvider):
+class SearchInProjectProvider(SearchInAccountProvider[_ST]):
"""Base class for search providers that support searching in a project."""
def project_query(self, tsquery: sa.Function, project: Project) -> Query:
@@ -175,7 +176,7 @@ def project_count(self, tsquery: sa.Function, project: Project) -> int:
"""Return count of results for :meth:`project_query`."""
return (
self.project_query(tsquery, project)
- .options(sa_orm.load_only(self.model.id))
+ .options(sa_orm.load_only(self.model.id_))
.count()
)
@@ -230,7 +231,7 @@ def all_query(self, tsquery: sa.Function) -> Query[Project]:
def all_count(self, tsquery: sa.Function) -> int:
"""Return count of matching projects across the entire site."""
return (
- db.session.query(sa.func.count('*'))
+ db.session.query(sa.func.count(sa.text('*')))
.select_from(Project)
.join(Account, Project.account)
.filter(
diff --git a/funnel/views/session.py b/funnel/views/session.py
index e2a658c1f..7b352e819 100644
--- a/funnel/views/session.py
+++ b/funnel/views/session.py
@@ -2,16 +2,16 @@
from __future__ import annotations
-from typing import cast
+from typing import TYPE_CHECKING
from flask import render_template, request
from baseframe import _
-from coaster.auth import current_auth
from coaster.sqlalchemy import failsafe_add
from coaster.views import ModelView, UrlChangeCheck, UrlForView, requires_roles, route
from .. import app
+from ..auth import current_auth
from ..forms import SavedProjectForm, SavedSessionForm, SessionForm
from ..models import Account, Project, Proposal, SavedSession, Session, db
from ..proxies import request_wants
@@ -61,7 +61,7 @@ def session_edit(
else:
form = SessionForm()
if proposal is not None:
- form.description.data = proposal.body
+ form.description.data = str(proposal.body)
form.speaker.data = proposal.first_user.fullname
form.title.data = proposal.title
@@ -92,7 +92,8 @@ def session_edit(
else:
db.session.add(session)
db.session.commit()
- session = cast(Session, session) # Tell mypy session is not None
+ if TYPE_CHECKING: # FIXME: Needed for Mypy in pre-commit only, unclear why
+ assert session is not None # nosec B101
session.project.update_schedule_timestamps()
db.session.commit()
if request_wants.html_in_json:
@@ -177,13 +178,13 @@ def view(self) -> ReturnView:
datasets=('without_parent', 'related')
),
from_date=(
- self.obj.project.start_at_localized.isoformat()
- if self.obj.project.start_at
+ start_at.isoformat()
+ if (start_at := self.obj.project.start_at_localized)
else None
),
to_date=(
- self.obj.project.end_at_localized.isoformat()
- if self.obj.project.end_at
+ end_at.isoformat()
+ if (end_at := self.obj.project.end_at_localized)
else None
),
active_session=session_data(self.obj, with_modal_url='view'),
diff --git a/funnel/views/siteadmin.py b/funnel/views/siteadmin.py
index 6316c8373..82d037c47 100644
--- a/funnel/views/siteadmin.py
+++ b/funnel/views/siteadmin.py
@@ -21,10 +21,10 @@
from baseframe import _
from baseframe.forms import Form
-from coaster.auth import current_auth
from coaster.views import ClassView, render_with, requestargs, route
from .. import app
+from ..auth import current_auth
from ..forms import ModeratorReportForm
from ..models import (
MODERATOR_REPORT_TYPE,
@@ -215,7 +215,7 @@ def dashboard_data_users_by_client(self) -> ReturnView:
auth_client_login_session.c.login_session_id == LoginSession.id,
Account.state.ACTIVE,
auth_client_login_session.c.accessed_at
- >= sa.func.utcnow() - sa.func.cast(interval, INTERVAL),
+ >= sa.func.utcnow() - sa.func.cast(sa.text(interval), INTERVAL),
)
.group_by(
auth_client_login_session.c.auth_client_id, LoginSession.account_id
@@ -310,11 +310,7 @@ def comments(
'comment_spam_form': Form(),
}
- @route(
- 'comments/markspam',
- endpoint='siteadmin_comments_spam',
- methods=['POST'],
- )
+ @route('comments/markspam', endpoint='siteadmin_comments_spam', methods=['POST'])
@requires_comment_moderator
def markspam(self) -> ReturnResponse:
"""Mark comments as spam."""
diff --git a/funnel/views/ticket_participant.py b/funnel/views/ticket_participant.py
index ad9757895..84f5509f0 100644
--- a/funnel/views/ticket_participant.py
+++ b/funnel/views/ticket_participant.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from flask import flash, request, url_for
+from flask import abort, flash, request, url_for
from sqlalchemy.exc import IntegrityError
from baseframe import _, forms
@@ -260,6 +260,8 @@ def checkin(self) -> ReturnView:
ticket_participant_ids = request.form.getlist('puuid_b58')
for ticket_participant_id in ticket_participant_ids:
attendee = TicketEventParticipant.get(self.obj, ticket_participant_id)
+ if attendee is None:
+ abort(404)
attendee.checked_in = bool(checked_in)
db.session.commit()
if request_wants.json:
diff --git a/funnel/views/update.py b/funnel/views/update.py
index 99328566a..6202551b0 100644
--- a/funnel/views/update.py
+++ b/funnel/views/update.py
@@ -6,7 +6,6 @@
from baseframe import _, forms
from baseframe.forms import render_form
-from coaster.auth import current_auth
from coaster.utils import make_name
from coaster.views import (
ModelView,
@@ -18,6 +17,7 @@
)
from .. import app
+from ..auth import current_auth
from ..forms import SavedProjectForm, UpdateForm
from ..models import Account, NewUpdateNotification, Project, Update, db
from ..typing import ReturnRenderWith, ReturnView
diff --git a/funnel/views/video.py b/funnel/views/video.py
index 8978018e8..7dfe58ba2 100644
--- a/funnel/views/video.py
+++ b/funnel/views/video.py
@@ -57,7 +57,7 @@ def set_video_cache(obj: VideoMixin, data: VideoData, exists: bool = True) -> No
copied_data['uploaded_at'] = cast(
datetime, copied_data['uploaded_at']
).isoformat()
- redis_store.hmset(cache_key, copied_data)
+ redis_store.hset(cache_key, mapping=copied_data)
# if video exists at source, cache for 2 days, if not, for 6 hours
hours_to_cache = 2 * 24 if exists else 6
diff --git a/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py b/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py
index 4c9045d76..45383fdd2 100644
--- a/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py
+++ b/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py
@@ -59,7 +59,7 @@ def upgrade() -> None:
# Add blake2b column to UserEmail
op.add_column('user_email', sa.Column('blake2b', sa.LargeBinary(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user_email))
progress = get_progressbar("Emails", count)
progress.start()
items = conn.execute(sa.select(user_email.c.id, user_email.c.email))
@@ -85,7 +85,9 @@ def upgrade() -> None:
op.add_column(
'user_email_claim', sa.Column('blake2b', sa.LargeBinary(), nullable=True)
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email_claim))
+ count = conn.scalar(
+ sa.select(sa.func.count(sa.text('*'))).select_from(user_email_claim)
+ )
progress = get_progressbar("Email claims", count)
progress.start()
items = conn.execute(sa.select(user_email_claim.c.id, user_email_claim.c.email))
diff --git a/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py b/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py
index 2bcde53c8..f612ca46a 100644
--- a/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py
+++ b/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py
@@ -129,7 +129,7 @@ def downgrade(engine_name=''):
def upgrade_():
conn = op.get_bind()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(project))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(project))
progress = get_progressbar("Projects", count)
progress.start()
@@ -139,7 +139,7 @@ def upgrade_():
for counter, project_item in enumerate(projects):
# Create membership for existing RSVP
rsvp_count = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.where(rsvp.c.project_id == project_item.id)
.select_from(rsvp)
)
@@ -157,7 +157,7 @@ def upgrade_():
for rsvp_item in rsvps:
existing_counter = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.where(
commentset_membership.c.commentset_id == project_item.commentset_id
)
@@ -196,7 +196,7 @@ def upgrade_():
for crew in crews:
existing_counter = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.where(
commentset_membership.c.commentset_id == project_item.commentset_id
)
@@ -225,7 +225,9 @@ def upgrade_():
progress.finish()
# Create commentset membership for existing proposal memberships
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal_membership))
+ count = conn.scalar(
+ sa.select(sa.func.count(sa.text('*'))).select_from(proposal_membership)
+ )
progress = get_progressbar("Proposals", count)
progress.start()
@@ -243,7 +245,7 @@ def upgrade_():
)
for counter, proposal_item in enumerate(proposals):
existing_counter = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.where(commentset_membership.c.commentset_id == proposal_item.commentset_id)
.where(commentset_membership.c.user_id == proposal_item.user_id)
.where(commentset_membership.c.revoked_at.is_(None))
@@ -273,7 +275,7 @@ def upgrade_():
def downgrade_():
conn = op.get_bind()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(project))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(project))
progress = get_progressbar("Projects", count)
progress.start()
@@ -282,7 +284,7 @@ def downgrade_():
commentset_memberships = conn.execute(
sa.select(commentset_membership.c.id)
.where(commentset_membership.c.commentset_id == project_item.commentset_id)
- .where(commentset_membership.c.revoked_at is None)
+ .where(commentset_membership.c.revoked_at.is_(None))
.select_from(commentset_membership)
)
for membership_item in commentset_memberships:
@@ -294,7 +296,7 @@ def downgrade_():
progress.update(counter)
progress.finish()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(proposal))
progress = get_progressbar("Proposals", count)
progress.start()
@@ -307,7 +309,7 @@ def downgrade_():
commentset_memberships = conn.execute(
sa.select(commentset_membership.c.id)
.where(commentset_membership.c.commentset_id == proposal_item.commentset_id)
- .where(commentset_membership.c.revoked_at is None)
+ .where(commentset_membership.c.revoked_at.is_(None))
.select_from(commentset_membership)
)
for membership_item in commentset_memberships:
diff --git a/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py b/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py
index 149ff54a7..e61746248 100644
--- a/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py
+++ b/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py
@@ -58,7 +58,7 @@ def session_description(row):
def upgrade() -> None:
conn = op.get_bind()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(session))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(session))
progress = get_progressbar("Sessions", count)
progress.start()
items = conn.execute(session.select())
diff --git a/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py b/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py
index 227e8ab52..085c9ed92 100644
--- a/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py
+++ b/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py
@@ -43,7 +43,7 @@ def upgrade() -> None:
conn = op.get_bind()
op.add_column('proposal', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(proposal))
progress = get_progressbar("Proposals", count)
progress.start()
items = conn.execute(sa.select(proposal.c.id))
@@ -57,7 +57,7 @@ def upgrade() -> None:
op.create_unique_constraint('proposal_uuid_key', 'proposal', ['uuid'])
op.add_column('session', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(session))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(session))
progress = get_progressbar("Sessions", count)
progress.start()
items = conn.execute(sa.select(session.c.id))
diff --git a/migrations/versions/321b11b6a413_add_participant_uuid_field.py b/migrations/versions/321b11b6a413_add_participant_uuid_field.py
index 5df001c4e..feac414db 100644
--- a/migrations/versions/321b11b6a413_add_participant_uuid_field.py
+++ b/migrations/versions/321b11b6a413_add_participant_uuid_field.py
@@ -44,7 +44,7 @@ def upgrade() -> None:
op.add_column('participant', sa.Column('uuid', sa.Uuid(), nullable=True))
# migrate past participants
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(participant))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(participant))
progress = get_progressbar("Participants", count)
progress.start()
items = conn.execute(sa.select(participant.c.id))
diff --git a/migrations/versions/331a4250aa4b_account_merges_user_organization_and_.py b/migrations/versions/331a4250aa4b_account_merges_user_organization_and_.py
index 1459a7ba2..16573aade 100644
--- a/migrations/versions/331a4250aa4b_account_merges_user_organization_and_.py
+++ b/migrations/versions/331a4250aa4b_account_merges_user_organization_and_.py
@@ -152,7 +152,7 @@ def __post_init__(self):
# If not renaming (new is None), use old name from `self.old`
rn.table_name = self.future_name or self.current_name
rn.old_table_name = self.current_name
- rn.symbol = symbol
+ rn.symbol = symbol # type: ignore[assignment]
def upgrade(self) -> None:
"""Do the rename."""
diff --git a/migrations/versions/5f1ab3e04f73_drop_support_for_128_bit_blake2b_hash_.py b/migrations/versions/5f1ab3e04f73_drop_support_for_128_bit_blake2b_hash_.py
index 25cdb531c..eee61ac8d 100644
--- a/migrations/versions/5f1ab3e04f73_drop_support_for_128_bit_blake2b_hash_.py
+++ b/migrations/versions/5f1ab3e04f73_drop_support_for_128_bit_blake2b_hash_.py
@@ -63,7 +63,9 @@ def downgrade() -> None:
sa.Column('blake2b', postgresql.BYTEA(), autoincrement=False, nullable=True),
)
# Recalculate blake2b hashes
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email_claim))
+ count = conn.scalar(
+ sa.select(sa.func.count(sa.text('*'))).select_from(user_email_claim)
+ )
progress = get_progressbar("Email claims", count)
progress.start()
items = conn.execute(
diff --git a/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py b/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py
index e5e8afd1b..a6c634290 100644
--- a/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py
+++ b/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py
@@ -110,7 +110,7 @@ def upgrade_() -> None:
op.add_column(
'user_phone', sa.Column('phone_number_id', sa.Integer(), nullable=True)
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_phone))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user_phone))
items = conn.execute(
sa.select(
user_phone.c.id,
@@ -148,10 +148,8 @@ def upgrade_() -> None:
allow_sm=False,
)
.returning(phone_number.c.id)
- ).fetchone()[
- 0
- ] # type: ignore[index]
-
+ ).first()
+ assert pn_id is not None # nosec B101
conn.execute(
user_phone.update()
.where(user_phone.c.id == item.id)
@@ -175,7 +173,7 @@ def upgrade_() -> None:
op.drop_column('user_phone', 'gets_text')
# --- SmsMessage -------------------------------------------------------------------
- # Remove rows with no transactionid, as the data is not validated in any way
+ # Remove rows with no `transactionid`, as the data is not validated in any way
conn.execute(sa.delete(sms_message).where(sms_message.c.transactionid.is_(None)))
op.add_column(
'sms_message', sa.Column('phone_number_id', sa.Integer(), nullable=True)
@@ -183,7 +181,7 @@ def upgrade_() -> None:
rows_to_delete = set()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(sms_message))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(sms_message))
items = conn.execute(
sa.select(
sms_message.c.id,
@@ -279,9 +277,8 @@ def upgrade_() -> None:
**timestamps,
)
.returning(phone_number.c.id)
- ).fetchone()[
- 0
- ] # type: ignore[index]
+ ).first()
+ assert pn_id is not None # nosec B101
conn.execute(
sms_message.update()
.where(sms_message.c.id == item.id)
@@ -328,7 +325,7 @@ def downgrade_() -> None:
nullable=True,
),
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(sms_message))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(sms_message))
items = conn.execute(
sa.select(sms_message.c.id, phone_number.c.number).where(
sms_message.c.phone_number_id == phone_number.c.id
@@ -362,7 +359,7 @@ def downgrade_() -> None:
op.add_column(
'user_phone', sa.Column('phone', sa.TEXT(), autoincrement=False, nullable=True)
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_phone))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user_phone))
items = conn.execute(
sa.select(user_phone.c.id, phone_number.c.number).where(
user_phone.c.phone_number_id == phone_number.c.id
diff --git a/migrations/versions/69c2ced88981_team_org_uuid.py b/migrations/versions/69c2ced88981_team_org_uuid.py
index 42ad39f46..7feb122dc 100644
--- a/migrations/versions/69c2ced88981_team_org_uuid.py
+++ b/migrations/versions/69c2ced88981_team_org_uuid.py
@@ -46,7 +46,7 @@ def upgrade() -> None:
conn = op.get_bind()
op.add_column('team', sa.Column('org_uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(team))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(team))
progress = get_progressbar("Teams", count)
progress.start()
items = conn.execute(sa.select(team.c.id, team.c.orgid))
@@ -70,7 +70,7 @@ def downgrade() -> None:
op.add_column(
'team', sa.Column('orgid', sa.String(22), autoincrement=False, nullable=True)
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(team))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(team))
progress = get_progressbar("Teams", count)
progress.start()
items = conn.execute(sa.select(team.c.id, team.c.org_uuid))
diff --git a/migrations/versions/7f8114c73092_add_rsvp_uuid.py b/migrations/versions/7f8114c73092_add_rsvp_uuid.py
index 4836eb1a2..7fced4679 100644
--- a/migrations/versions/7f8114c73092_add_rsvp_uuid.py
+++ b/migrations/versions/7f8114c73092_add_rsvp_uuid.py
@@ -49,7 +49,7 @@ def upgrade() -> None:
conn = op.get_bind()
op.add_column('rsvp', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(rsvp))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(rsvp))
progress = get_progressbar("Rsvps", count)
progress.start()
diff --git a/migrations/versions/887db555cca9_adding_uuid_to_commentset.py b/migrations/versions/887db555cca9_adding_uuid_to_commentset.py
index 7702f2e50..3e4291924 100644
--- a/migrations/versions/887db555cca9_adding_uuid_to_commentset.py
+++ b/migrations/versions/887db555cca9_adding_uuid_to_commentset.py
@@ -45,7 +45,7 @@ def upgrade() -> None:
op.add_column('commentset', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(commentset))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(commentset))
progress = get_progressbar("Commentsets", count)
progress.start()
items = conn.execute(sa.select(commentset.c.id))
diff --git a/migrations/versions/ad5013552ec6_simplify_proposal_fields.py b/migrations/versions/ad5013552ec6_simplify_proposal_fields.py
index 1551e0219..4fa98c8a6 100644
--- a/migrations/versions/ad5013552ec6_simplify_proposal_fields.py
+++ b/migrations/versions/ad5013552ec6_simplify_proposal_fields.py
@@ -87,7 +87,7 @@ def upgrade() -> None:
op.add_column('proposal', sa.Column('body_html', sa.UnicodeText(), nullable=True))
op.add_column('proposal', sa.Column('description', sa.Unicode(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(proposal))
progress = get_progressbar("Proposals", count)
progress.start()
items = conn.execute(proposal.select())
diff --git a/migrations/versions/ae075a249493_migrate_email_addresses.py b/migrations/versions/ae075a249493_migrate_email_addresses.py
index a6ea10cff..dd639e74f 100644
--- a/migrations/versions/ae075a249493_migrate_email_addresses.py
+++ b/migrations/versions/ae075a249493_migrate_email_addresses.py
@@ -167,7 +167,7 @@ def upgrade() -> None:
'user_email', sa.Column('email_address_id', sa.Integer(), nullable=True)
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user_email))
progress = get_progressbar("Emails", count)
progress.start()
items = conn.execute(
@@ -259,7 +259,9 @@ def upgrade() -> None:
ondelete='SET NULL',
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email_claim))
+ count = conn.scalar(
+ sa.select(sa.func.count(sa.text('*'))).select_from(user_email_claim)
+ )
progress = get_progressbar("Email claims", count)
progress.start()
items = conn.execute(
@@ -348,7 +350,7 @@ def upgrade() -> None:
)
count = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.select_from(proposal)
.where(proposal.c.email.is_not(None))
)
@@ -413,7 +415,7 @@ def downgrade() -> None:
)
count = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.select_from(proposal)
.where(proposal.c.email_address_id.is_not(None))
)
@@ -449,7 +451,9 @@ def downgrade() -> None:
sa.Column('email', sa.VARCHAR(length=254), autoincrement=False, nullable=True),
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email_claim))
+ count = conn.scalar(
+ sa.select(sa.func.count(sa.text('*'))).select_from(user_email_claim)
+ )
progress = get_progressbar("Email claims", count)
progress.start()
items = conn.execute(
@@ -519,7 +523,7 @@ def downgrade() -> None:
sa.Column('blake2b', postgresql.BYTEA(), autoincrement=False, nullable=True),
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user_email))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user_email))
progress = get_progressbar("Emails", count)
progress.start()
items = conn.execute(
diff --git a/migrations/versions/aebd5a9e5af1_project_timestamps.py b/migrations/versions/aebd5a9e5af1_project_timestamps.py
index 804c56f03..5e9e44797 100644
--- a/migrations/versions/aebd5a9e5af1_project_timestamps.py
+++ b/migrations/versions/aebd5a9e5af1_project_timestamps.py
@@ -88,7 +88,7 @@ def upgrade() -> None:
# Update project start_at/end_at timestamps where sessions exist
conn = op.get_bind()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(project))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(project))
progress = get_progressbar("Projects", count)
progress.start()
project_ids = conn.execute(sa.select(project.c.id))
diff --git a/migrations/versions/b34aa62af7fc_uuid_columns_for_project_profile_user_team.py b/migrations/versions/b34aa62af7fc_uuid_columns_for_project_profile_user_team.py
index 1ecfc660c..0cb59af67 100644
--- a/migrations/versions/b34aa62af7fc_uuid_columns_for_project_profile_user_team.py
+++ b/migrations/versions/b34aa62af7fc_uuid_columns_for_project_profile_user_team.py
@@ -65,7 +65,7 @@ def upgrade() -> None:
# --- Project
op.add_column('project', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(project))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(project))
progress = get_progressbar("Projects", count)
progress.start()
items = conn.execute(sa.select(project.c.id))
@@ -80,7 +80,7 @@ def upgrade() -> None:
# --- Profile
op.add_column('profile', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(profile))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(profile))
progress = get_progressbar("Profiles", count)
progress.start()
items = conn.execute(sa.select(profile.c.id, profile.c.userid))
@@ -99,7 +99,7 @@ def upgrade() -> None:
# --- Team
op.add_column('team', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(team))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(team))
progress = get_progressbar("Teams", count)
progress.start()
items = conn.execute(sa.select(team.c.id, team.c.userid))
@@ -118,7 +118,7 @@ def upgrade() -> None:
# --- User
op.add_column('user', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user))
progress = get_progressbar("Users", count)
progress.start()
items = conn.execute(sa.select(user.c.id, user.c.userid))
@@ -142,7 +142,7 @@ def downgrade() -> None:
# --- User
op.add_column('user', sa.Column('userid', sa.String(22), nullable=True))
op.create_unique_constraint('user_userid_key', 'user', ['userid'])
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(user))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(user))
progress = get_progressbar("Users", count)
progress.start()
items = conn.execute(sa.select(user.c.id, user.c.uuid))
@@ -161,7 +161,7 @@ def downgrade() -> None:
# --- Team
op.add_column('team', sa.Column('userid', sa.String(22), nullable=True))
op.create_unique_constraint('team_userid_key', 'team', ['userid'])
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(team))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(team))
progress = get_progressbar("Teams", count)
progress.start()
items = conn.execute(sa.select(team.c.id, team.c.uuid))
@@ -180,7 +180,7 @@ def downgrade() -> None:
# --- Profile
op.add_column('profile', sa.Column('userid', sa.String(22), nullable=True))
op.create_unique_constraint('profile_userid_key', 'profile', ['userid'])
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(profile))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(profile))
progress = get_progressbar("Profiles", count)
progress.start()
items = conn.execute(sa.select(profile.c.id, profile.c.uuid))
diff --git a/migrations/versions/c3069d33419a_comment_uuid_field.py b/migrations/versions/c3069d33419a_comment_uuid_field.py
index b873631f1..34fbd17a1 100644
--- a/migrations/versions/c3069d33419a_comment_uuid_field.py
+++ b/migrations/versions/c3069d33419a_comment_uuid_field.py
@@ -41,7 +41,7 @@ def upgrade() -> None:
op.add_column('comment', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(comment))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(comment))
progress = get_progressbar("Comments", count)
progress.start()
items = conn.execute(sa.select(comment.c.id))
diff --git a/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py b/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py
index 9b7af1fc7..5afb523fe 100644
--- a/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py
+++ b/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py
@@ -72,7 +72,7 @@ def upgrade() -> None:
if geoip_city is not None or geoip_asn is not None:
conn = op.get_bind()
count = conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.select_from(user_session)
.where(
sa.or_(
diff --git a/migrations/versions/d79beb04a529_remove_incorrect_nullable_flag.py b/migrations/versions/d79beb04a529_remove_incorrect_nullable_flag.py
new file mode 100644
index 000000000..e37afe34d
--- /dev/null
+++ b/migrations/versions/d79beb04a529_remove_incorrect_nullable_flag.py
@@ -0,0 +1,40 @@
+"""Remove incorrect nullable flag.
+
+Revision ID: d79beb04a529
+Revises: f0ed25eed4bc
+Create Date: 2023-12-21 00:55:05.801455
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = 'd79beb04a529'
+down_revision: str = 'f0ed25eed4bc'
+branch_labels: str | tuple[str, ...] | None = None
+depends_on: str | tuple[str, ...] | None = None
+
+
+def upgrade(engine_name: str = '') -> None:
+ """Upgrade all databases."""
+ # Do not modify. Edit `upgrade_` instead
+ globals().get(f'upgrade_{engine_name}', lambda: None)()
+
+
+def downgrade(engine_name: str = '') -> None:
+ """Downgrade all databases."""
+ # Do not modify. Edit `downgrade_` instead
+ globals().get(f'downgrade_{engine_name}', lambda: None)()
+
+
+def upgrade_() -> None:
+ """Upgrade default database."""
+ with op.batch_alter_table('auth_client', schema=None) as batch_op:
+ batch_op.alter_column('account_id', existing_type=sa.INTEGER(), nullable=False)
+
+
+def downgrade_() -> None:
+ """Downgrade default database."""
+ with op.batch_alter_table('auth_client', schema=None) as batch_op:
+ batch_op.alter_column('account_id', existing_type=sa.INTEGER(), nullable=True)
diff --git a/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py b/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py
index c72feedf5..5b67d654d 100644
--- a/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py
+++ b/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py
@@ -74,7 +74,7 @@ def upgrade() -> None:
)
conn = op.get_bind()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(proposal))
progress = get_progressbar("Proposals", count)
progress.start()
items = conn.execute(sa.select(proposal.c.id, proposal.c.uuid))
diff --git a/migrations/versions/e2be4ab896d3_migrate_old_labels.py b/migrations/versions/e2be4ab896d3_migrate_old_labels.py
index 8eadf602d..830676efb 100644
--- a/migrations/versions/e2be4ab896d3_migrate_old_labels.py
+++ b/migrations/versions/e2be4ab896d3_migrate_old_labels.py
@@ -71,7 +71,7 @@ def upgrade() -> None:
sec_count = cast(
int,
conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.select_from(section)
.where(section.c.project_id == proj.id)
),
@@ -236,7 +236,7 @@ def upgrade() -> None:
duplicate_count = cast(
int,
conn.scalar(
- sa.select(sa.func.count('*'))
+ sa.select(sa.func.count(sa.text('*')))
.select_from(label)
.where(label.c.name == st_name)
.where(label.c.project_id == proj.id)
diff --git a/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py b/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py
index 906475372..38e9cbe06 100644
--- a/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py
+++ b/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py
@@ -139,7 +139,7 @@ def upgrade() -> None:
ondelete='SET NULL',
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(participant))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(participant))
progress = get_progressbar("Participants", count)
progress.start()
items = conn.execute(
@@ -244,7 +244,7 @@ def downgrade() -> None:
'participant',
sa.Column('email', sa.VARCHAR(length=254), autoincrement=False, nullable=True),
)
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(participant))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(participant))
progress = get_progressbar("Participants", count)
progress.start()
items = conn.execute(
diff --git a/migrations/versions/eec2fad0f3e9_venue_uuid_field.py b/migrations/versions/eec2fad0f3e9_venue_uuid_field.py
index f9d951ad7..366d6b5b4 100644
--- a/migrations/versions/eec2fad0f3e9_venue_uuid_field.py
+++ b/migrations/versions/eec2fad0f3e9_venue_uuid_field.py
@@ -40,7 +40,7 @@ def upgrade() -> None:
conn = op.get_bind()
op.add_column('venue', sa.Column('uuid', sa.Uuid(), nullable=True))
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(venue))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(venue))
progress = get_progressbar("Venues", count)
progress.start()
items = conn.execute(sa.select(venue.c.id))
diff --git a/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py b/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py
index 025b31446..7bbbe085f 100644
--- a/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py
+++ b/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py
@@ -87,7 +87,7 @@ def get_progressbar(label, maxval):
def upgrade() -> None:
# Adapts from `proposal` table to an empty `proposal_membership` table.
conn = op.get_bind()
- count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal))
+ count = conn.scalar(sa.select(sa.func.count(sa.text('*'))).select_from(proposal))
progress = get_progressbar("Proposals", count)
progress.start()
diff --git a/package-lock.json b/package-lock.json
index d33c92e94..12200f8c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -106,12 +106,12 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.13",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
- "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
"dev": true,
"dependencies": {
- "@babel/highlight": "^7.22.13",
+ "@babel/highlight": "^7.23.4",
"chalk": "^2.4.2"
},
"engines": {
@@ -119,30 +119,30 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz",
- "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
+ "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
- "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz",
+ "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
- "@babel/helper-compilation-targets": "^7.22.15",
- "@babel/helper-module-transforms": "^7.23.0",
- "@babel/helpers": "^7.23.2",
- "@babel/parser": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.6",
+ "@babel/helper-compilation-targets": "^7.23.6",
+ "@babel/helper-module-transforms": "^7.23.3",
+ "@babel/helpers": "^7.23.7",
+ "@babel/parser": "^7.23.6",
"@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0",
+ "@babel/traverse": "^7.23.7",
+ "@babel/types": "^7.23.6",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -158,12 +158,12 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
- "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
+ "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.23.0",
+ "@babel/types": "^7.23.6",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -197,14 +197,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
- "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
+ "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.22.9",
- "@babel/helper-validator-option": "^7.22.15",
- "browserslist": "^4.21.9",
+ "@babel/compat-data": "^7.23.5",
+ "@babel/helper-validator-option": "^7.23.5",
+ "browserslist": "^4.22.2",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
@@ -213,17 +213,17 @@
}
},
"node_modules/@babel/helper-create-class-features-plugin": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz",
- "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz",
+ "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
- "@babel/helper-member-expression-to-functions": "^7.22.15",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-member-expression-to-functions": "^7.23.0",
"@babel/helper-optimise-call-expression": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-replace-supers": "^7.22.20",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"semver": "^6.3.1"
@@ -253,9 +253,9 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz",
- "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==",
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz",
+ "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==",
"dev": true,
"dependencies": {
"@babel/helper-compilation-targets": "^7.22.6",
@@ -327,9 +327,9 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz",
- "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz",
+ "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
@@ -437,9 +437,9 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
- "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -454,9 +454,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz",
- "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz",
+ "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -477,23 +477,23 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz",
- "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.7.tgz",
+ "integrity": "sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==",
"dev": true,
"dependencies": {
"@babel/template": "^7.22.15",
- "@babel/traverse": "^7.23.2",
- "@babel/types": "^7.23.0"
+ "@babel/traverse": "^7.23.7",
+ "@babel/types": "^7.23.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
- "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
@@ -504,9 +504,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
- "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
+ "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -515,9 +515,9 @@
}
},
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz",
- "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz",
+ "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -530,14 +530,14 @@
}
},
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz",
- "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz",
+ "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
- "@babel/plugin-transform-optional-chaining": "^7.22.15"
+ "@babel/plugin-transform-optional-chaining": "^7.23.3"
},
"engines": {
"node": ">=6.9.0"
@@ -546,6 +546,22 @@
"@babel/core": "^7.13.0"
}
},
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz",
+ "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/@babel/plugin-proposal-private-property-in-object": {
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
@@ -622,9 +638,9 @@
}
},
"node_modules/@babel/plugin-syntax-import-assertions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz",
- "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz",
+ "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -637,9 +653,9 @@
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz",
- "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz",
+ "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -794,9 +810,9 @@
}
},
"node_modules/@babel/plugin-transform-arrow-functions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz",
- "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz",
+ "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -809,9 +825,9 @@
}
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz",
- "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz",
+ "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==",
"dev": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.22.20",
@@ -827,14 +843,14 @@
}
},
"node_modules/@babel/plugin-transform-async-to-generator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz",
- "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz",
+ "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==",
"dev": true,
"dependencies": {
- "@babel/helper-module-imports": "^7.22.5",
+ "@babel/helper-module-imports": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-remap-async-to-generator": "^7.22.5"
+ "@babel/helper-remap-async-to-generator": "^7.22.20"
},
"engines": {
"node": ">=6.9.0"
@@ -844,9 +860,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoped-functions": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz",
- "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz",
+ "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -859,9 +875,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz",
- "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz",
+ "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -874,12 +890,12 @@
}
},
"node_modules/@babel/plugin-transform-class-properties": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz",
- "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz",
+ "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==",
"dev": true,
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-create-class-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -890,12 +906,12 @@
}
},
"node_modules/@babel/plugin-transform-class-static-block": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz",
- "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz",
+ "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==",
"dev": true,
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.22.11",
+ "@babel/helper-create-class-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-class-static-block": "^7.14.5"
},
@@ -907,18 +923,18 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz",
- "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz",
+ "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-compilation-targets": "^7.22.15",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-optimise-call-expression": "^7.22.5",
"@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-replace-supers": "^7.22.20",
"@babel/helper-split-export-declaration": "^7.22.6",
"globals": "^11.1.0"
},
@@ -930,13 +946,13 @@
}
},
"node_modules/@babel/plugin-transform-computed-properties": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz",
- "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz",
+ "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
- "@babel/template": "^7.22.5"
+ "@babel/template": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
@@ -946,9 +962,9 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz",
- "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz",
+ "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -961,12 +977,12 @@
}
},
"node_modules/@babel/plugin-transform-dotall-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz",
- "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz",
+ "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==",
"dev": true,
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-create-regexp-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -977,9 +993,9 @@
}
},
"node_modules/@babel/plugin-transform-duplicate-keys": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz",
- "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz",
+ "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -992,9 +1008,9 @@
}
},
"node_modules/@babel/plugin-transform-dynamic-import": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz",
- "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz",
+ "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1008,12 +1024,12 @@
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz",
- "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz",
+ "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==",
"dev": true,
"dependencies": {
- "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1024,9 +1040,9 @@
}
},
"node_modules/@babel/plugin-transform-export-namespace-from": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz",
- "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz",
+ "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1040,12 +1056,13 @@
}
},
"node_modules/@babel/plugin-transform-for-of": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz",
- "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz",
+ "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==",
"dev": true,
"dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1055,13 +1072,13 @@
}
},
"node_modules/@babel/plugin-transform-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz",
- "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz",
+ "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==",
"dev": true,
"dependencies": {
- "@babel/helper-compilation-targets": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1072,9 +1089,9 @@
}
},
"node_modules/@babel/plugin-transform-json-strings": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz",
- "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz",
+ "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1088,9 +1105,9 @@
}
},
"node_modules/@babel/plugin-transform-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz",
- "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz",
+ "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1103,9 +1120,9 @@
}
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz",
- "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz",
+ "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1119,9 +1136,9 @@
}
},
"node_modules/@babel/plugin-transform-member-expression-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz",
- "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz",
+ "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1134,12 +1151,12 @@
}
},
"node_modules/@babel/plugin-transform-modules-amd": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz",
- "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz",
+ "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==",
"dev": true,
"dependencies": {
- "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-module-transforms": "^7.23.3",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1150,12 +1167,12 @@
}
},
"node_modules/@babel/plugin-transform-modules-commonjs": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz",
- "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz",
+ "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==",
"dev": true,
"dependencies": {
- "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-module-transforms": "^7.23.3",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-simple-access": "^7.22.5"
},
@@ -1167,13 +1184,13 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz",
- "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz",
+ "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==",
"dev": true,
"dependencies": {
"@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-module-transforms": "^7.23.0",
+ "@babel/helper-module-transforms": "^7.23.3",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20"
},
@@ -1185,12 +1202,12 @@
}
},
"node_modules/@babel/plugin-transform-modules-umd": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz",
- "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz",
+ "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==",
"dev": true,
"dependencies": {
- "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-module-transforms": "^7.23.3",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1217,9 +1234,9 @@
}
},
"node_modules/@babel/plugin-transform-new-target": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz",
- "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz",
+ "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1232,9 +1249,9 @@
}
},
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz",
- "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz",
+ "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1248,9 +1265,9 @@
}
},
"node_modules/@babel/plugin-transform-numeric-separator": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz",
- "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz",
+ "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1264,16 +1281,16 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz",
- "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz",
+ "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.22.9",
+ "@babel/compat-data": "^7.23.3",
"@babel/helper-compilation-targets": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
- "@babel/plugin-transform-parameters": "^7.22.15"
+ "@babel/plugin-transform-parameters": "^7.23.3"
},
"engines": {
"node": ">=6.9.0"
@@ -1283,13 +1300,13 @@
}
},
"node_modules/@babel/plugin-transform-object-super": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz",
- "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz",
+ "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-replace-supers": "^7.22.5"
+ "@babel/helper-replace-supers": "^7.22.20"
},
"engines": {
"node": ">=6.9.0"
@@ -1299,9 +1316,9 @@
}
},
"node_modules/@babel/plugin-transform-optional-catch-binding": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz",
- "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz",
+ "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1315,9 +1332,9 @@
}
},
"node_modules/@babel/plugin-transform-optional-chaining": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz",
- "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz",
+ "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1332,9 +1349,9 @@
}
},
"node_modules/@babel/plugin-transform-parameters": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz",
- "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz",
+ "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1347,12 +1364,12 @@
}
},
"node_modules/@babel/plugin-transform-private-methods": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz",
- "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz",
+ "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==",
"dev": true,
"dependencies": {
- "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-create-class-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1363,13 +1380,13 @@
}
},
"node_modules/@babel/plugin-transform-private-property-in-object": {
- "version": "7.22.11",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz",
- "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==",
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz",
+ "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==",
"dev": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
- "@babel/helper-create-class-features-plugin": "^7.22.11",
+ "@babel/helper-create-class-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5",
"@babel/plugin-syntax-private-property-in-object": "^7.14.5"
},
@@ -1381,9 +1398,9 @@
}
},
"node_modules/@babel/plugin-transform-property-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz",
- "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz",
+ "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1396,9 +1413,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz",
- "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz",
+ "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1412,9 +1429,9 @@
}
},
"node_modules/@babel/plugin-transform-reserved-words": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz",
- "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz",
+ "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1427,9 +1444,9 @@
}
},
"node_modules/@babel/plugin-transform-shorthand-properties": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz",
- "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz",
+ "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1442,9 +1459,9 @@
}
},
"node_modules/@babel/plugin-transform-spread": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz",
- "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz",
+ "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5",
@@ -1458,9 +1475,9 @@
}
},
"node_modules/@babel/plugin-transform-sticky-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz",
- "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz",
+ "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1473,9 +1490,9 @@
}
},
"node_modules/@babel/plugin-transform-template-literals": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz",
- "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz",
+ "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1488,9 +1505,9 @@
}
},
"node_modules/@babel/plugin-transform-typeof-symbol": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz",
- "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz",
+ "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1503,9 +1520,9 @@
}
},
"node_modules/@babel/plugin-transform-unicode-escapes": {
- "version": "7.22.10",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz",
- "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz",
+ "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.22.5"
@@ -1518,12 +1535,12 @@
}
},
"node_modules/@babel/plugin-transform-unicode-property-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz",
- "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz",
+ "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==",
"dev": true,
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-create-regexp-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1534,12 +1551,12 @@
}
},
"node_modules/@babel/plugin-transform-unicode-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz",
- "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz",
+ "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==",
"dev": true,
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-create-regexp-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1550,12 +1567,12 @@
}
},
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz",
- "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz",
+ "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==",
"dev": true,
"dependencies": {
- "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-create-regexp-features-plugin": "^7.22.15",
"@babel/helper-plugin-utils": "^7.22.5"
},
"engines": {
@@ -1566,25 +1583,26 @@
}
},
"node_modules/@babel/preset-env": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz",
- "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz",
+ "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.23.2",
- "@babel/helper-compilation-targets": "^7.22.15",
+ "@babel/compat-data": "^7.23.5",
+ "@babel/helper-compilation-targets": "^7.23.6",
"@babel/helper-plugin-utils": "^7.22.5",
- "@babel/helper-validator-option": "^7.22.15",
- "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15",
- "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15",
+ "@babel/helper-validator-option": "^7.23.5",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7",
"@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
"@babel/plugin-syntax-async-generators": "^7.8.4",
"@babel/plugin-syntax-class-properties": "^7.12.13",
"@babel/plugin-syntax-class-static-block": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-export-namespace-from": "^7.8.3",
- "@babel/plugin-syntax-import-assertions": "^7.22.5",
- "@babel/plugin-syntax-import-attributes": "^7.22.5",
+ "@babel/plugin-syntax-import-assertions": "^7.23.3",
+ "@babel/plugin-syntax-import-attributes": "^7.23.3",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-json-strings": "^7.8.3",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
@@ -1596,59 +1614,58 @@
"@babel/plugin-syntax-private-property-in-object": "^7.14.5",
"@babel/plugin-syntax-top-level-await": "^7.14.5",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
- "@babel/plugin-transform-arrow-functions": "^7.22.5",
- "@babel/plugin-transform-async-generator-functions": "^7.23.2",
- "@babel/plugin-transform-async-to-generator": "^7.22.5",
- "@babel/plugin-transform-block-scoped-functions": "^7.22.5",
- "@babel/plugin-transform-block-scoping": "^7.23.0",
- "@babel/plugin-transform-class-properties": "^7.22.5",
- "@babel/plugin-transform-class-static-block": "^7.22.11",
- "@babel/plugin-transform-classes": "^7.22.15",
- "@babel/plugin-transform-computed-properties": "^7.22.5",
- "@babel/plugin-transform-destructuring": "^7.23.0",
- "@babel/plugin-transform-dotall-regex": "^7.22.5",
- "@babel/plugin-transform-duplicate-keys": "^7.22.5",
- "@babel/plugin-transform-dynamic-import": "^7.22.11",
- "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
- "@babel/plugin-transform-export-namespace-from": "^7.22.11",
- "@babel/plugin-transform-for-of": "^7.22.15",
- "@babel/plugin-transform-function-name": "^7.22.5",
- "@babel/plugin-transform-json-strings": "^7.22.11",
- "@babel/plugin-transform-literals": "^7.22.5",
- "@babel/plugin-transform-logical-assignment-operators": "^7.22.11",
- "@babel/plugin-transform-member-expression-literals": "^7.22.5",
- "@babel/plugin-transform-modules-amd": "^7.23.0",
- "@babel/plugin-transform-modules-commonjs": "^7.23.0",
- "@babel/plugin-transform-modules-systemjs": "^7.23.0",
- "@babel/plugin-transform-modules-umd": "^7.22.5",
+ "@babel/plugin-transform-arrow-functions": "^7.23.3",
+ "@babel/plugin-transform-async-generator-functions": "^7.23.7",
+ "@babel/plugin-transform-async-to-generator": "^7.23.3",
+ "@babel/plugin-transform-block-scoped-functions": "^7.23.3",
+ "@babel/plugin-transform-block-scoping": "^7.23.4",
+ "@babel/plugin-transform-class-properties": "^7.23.3",
+ "@babel/plugin-transform-class-static-block": "^7.23.4",
+ "@babel/plugin-transform-classes": "^7.23.5",
+ "@babel/plugin-transform-computed-properties": "^7.23.3",
+ "@babel/plugin-transform-destructuring": "^7.23.3",
+ "@babel/plugin-transform-dotall-regex": "^7.23.3",
+ "@babel/plugin-transform-duplicate-keys": "^7.23.3",
+ "@babel/plugin-transform-dynamic-import": "^7.23.4",
+ "@babel/plugin-transform-exponentiation-operator": "^7.23.3",
+ "@babel/plugin-transform-export-namespace-from": "^7.23.4",
+ "@babel/plugin-transform-for-of": "^7.23.6",
+ "@babel/plugin-transform-function-name": "^7.23.3",
+ "@babel/plugin-transform-json-strings": "^7.23.4",
+ "@babel/plugin-transform-literals": "^7.23.3",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.23.4",
+ "@babel/plugin-transform-member-expression-literals": "^7.23.3",
+ "@babel/plugin-transform-modules-amd": "^7.23.3",
+ "@babel/plugin-transform-modules-commonjs": "^7.23.3",
+ "@babel/plugin-transform-modules-systemjs": "^7.23.3",
+ "@babel/plugin-transform-modules-umd": "^7.23.3",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
- "@babel/plugin-transform-new-target": "^7.22.5",
- "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11",
- "@babel/plugin-transform-numeric-separator": "^7.22.11",
- "@babel/plugin-transform-object-rest-spread": "^7.22.15",
- "@babel/plugin-transform-object-super": "^7.22.5",
- "@babel/plugin-transform-optional-catch-binding": "^7.22.11",
- "@babel/plugin-transform-optional-chaining": "^7.23.0",
- "@babel/plugin-transform-parameters": "^7.22.15",
- "@babel/plugin-transform-private-methods": "^7.22.5",
- "@babel/plugin-transform-private-property-in-object": "^7.22.11",
- "@babel/plugin-transform-property-literals": "^7.22.5",
- "@babel/plugin-transform-regenerator": "^7.22.10",
- "@babel/plugin-transform-reserved-words": "^7.22.5",
- "@babel/plugin-transform-shorthand-properties": "^7.22.5",
- "@babel/plugin-transform-spread": "^7.22.5",
- "@babel/plugin-transform-sticky-regex": "^7.22.5",
- "@babel/plugin-transform-template-literals": "^7.22.5",
- "@babel/plugin-transform-typeof-symbol": "^7.22.5",
- "@babel/plugin-transform-unicode-escapes": "^7.22.10",
- "@babel/plugin-transform-unicode-property-regex": "^7.22.5",
- "@babel/plugin-transform-unicode-regex": "^7.22.5",
- "@babel/plugin-transform-unicode-sets-regex": "^7.22.5",
+ "@babel/plugin-transform-new-target": "^7.23.3",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4",
+ "@babel/plugin-transform-numeric-separator": "^7.23.4",
+ "@babel/plugin-transform-object-rest-spread": "^7.23.4",
+ "@babel/plugin-transform-object-super": "^7.23.3",
+ "@babel/plugin-transform-optional-catch-binding": "^7.23.4",
+ "@babel/plugin-transform-optional-chaining": "^7.23.4",
+ "@babel/plugin-transform-parameters": "^7.23.3",
+ "@babel/plugin-transform-private-methods": "^7.23.3",
+ "@babel/plugin-transform-private-property-in-object": "^7.23.4",
+ "@babel/plugin-transform-property-literals": "^7.23.3",
+ "@babel/plugin-transform-regenerator": "^7.23.3",
+ "@babel/plugin-transform-reserved-words": "^7.23.3",
+ "@babel/plugin-transform-shorthand-properties": "^7.23.3",
+ "@babel/plugin-transform-spread": "^7.23.3",
+ "@babel/plugin-transform-sticky-regex": "^7.23.3",
+ "@babel/plugin-transform-template-literals": "^7.23.3",
+ "@babel/plugin-transform-typeof-symbol": "^7.23.3",
+ "@babel/plugin-transform-unicode-escapes": "^7.23.3",
+ "@babel/plugin-transform-unicode-property-regex": "^7.23.3",
+ "@babel/plugin-transform-unicode-regex": "^7.23.3",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.23.3",
"@babel/preset-modules": "0.1.6-no-external-plugins",
- "@babel/types": "^7.23.0",
- "babel-plugin-polyfill-corejs2": "^0.4.6",
- "babel-plugin-polyfill-corejs3": "^0.8.5",
- "babel-plugin-polyfill-regenerator": "^0.5.3",
+ "babel-plugin-polyfill-corejs2": "^0.4.7",
+ "babel-plugin-polyfill-corejs3": "^0.8.7",
+ "babel-plugin-polyfill-regenerator": "^0.5.4",
"core-js-compat": "^3.31.0",
"semver": "^6.3.1"
},
@@ -1680,9 +1697,9 @@
"dev": true
},
"node_modules/@babel/runtime": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
- "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
+ "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1705,20 +1722,20 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
- "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "version": "7.23.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz",
+ "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==",
"dev": true,
"dependencies": {
- "@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.23.0",
+ "@babel/code-frame": "^7.23.5",
+ "@babel/generator": "^7.23.6",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.23.0",
- "@babel/types": "^7.23.0",
- "debug": "^4.1.0",
+ "@babel/parser": "^7.23.6",
+ "@babel/types": "^7.23.6",
+ "debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
@@ -1726,12 +1743,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
- "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
+ "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"dev": true,
"dependencies": {
- "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
@@ -1745,9 +1762,9 @@
"integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
},
"node_modules/@codemirror/autocomplete": {
- "version": "6.10.2",
- "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.10.2.tgz",
- "integrity": "sha512-3dCL7b0j2GdtZzWN5j7HDpRAJ26ip07R4NGYz7QYthIYMiX8I4E4TNrYcdTayPJGeVQtd/xe7lWU4XL7THFb/w==",
+ "version": "6.11.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.11.1.tgz",
+ "integrity": "sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -1762,12 +1779,12 @@
}
},
"node_modules/@codemirror/commands": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.0.tgz",
- "integrity": "sha512-tFfcxRIlOWiQDFhjBSWJ10MxcvbCIsRr6V64SgrcaY0MwNk32cUOcCuNlWo8VjV4qRQCgNgUAnIeo0svkk4R5Q==",
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz",
+ "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==",
"dependencies": {
"@codemirror/language": "^6.0.0",
- "@codemirror/state": "^6.2.0",
+ "@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.1.0"
}
@@ -1785,9 +1802,9 @@
}
},
"node_modules/@codemirror/lang-html": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.6.tgz",
- "integrity": "sha512-E4C8CVupBksXvgLSme/zv31x91g06eZHSph7NczVxZW+/K+3XgJGWNT//2WLzaKSBoxpAjaOi5ZnPU1SHhjh3A==",
+ "version": "6.4.7",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.7.tgz",
+ "integrity": "sha512-y9hWSSO41XlcL4uYwWyk0lEgTHcelWWfRuqmvcAmxfCs0HNWZdriWo/EU43S63SxEZpc1Hd50Itw7ktfQvfkUg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@@ -1815,9 +1832,9 @@
}
},
"node_modules/@codemirror/lang-markdown": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.2.tgz",
- "integrity": "sha512-wmwM9Y5n/e4ndU51KcYDaQnb9goYdhXjU71dDW9goOc1VUTIPph6WKAPdJ6BzC0ZFy+UTsDwTXGWSP370RH69Q==",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.3.tgz",
+ "integrity": "sha512-wCewRLWpdefWi7uVkHIDiE8+45Fe4buvMDZkihqEom5uRUQrl76Zb13emjeK3W+8pcRgRfAmwelURBbxNEKCIg==",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
@@ -1829,12 +1846,12 @@
}
},
"node_modules/@codemirror/language": {
- "version": "6.9.2",
- "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.2.tgz",
- "integrity": "sha512-QGTQXSpAKDIzaSE96zNK1UfIUhPgkT1CLjh1N5qVzZuxgsEOhz5RqaN8QCIdyOQklGLx3MgHd9YrE3X3+Pl1ow==",
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.0.tgz",
+ "integrity": "sha512-2vaNn9aPGCRFKWcHPFksctzJ8yS5p7YoaT+jHpc0UGKzNuAIx4qy6R5wiqbP+heEEdyaABA582mNqSHzSoYdmg==",
"dependencies": {
"@codemirror/state": "^6.0.0",
- "@codemirror/view": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
@@ -1852,16 +1869,16 @@
}
},
"node_modules/@codemirror/state": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.3.1.tgz",
- "integrity": "sha512-88e4HhMtKJyw6fKprGaN/yZfiaoGYOi2nM45YCUC6R/kex9sxFWBDGatS1vk4lMgnWmdIIB9tk8Gj1LmL8YfvA=="
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz",
+ "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A=="
},
"node_modules/@codemirror/view": {
- "version": "6.22.0",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.22.0.tgz",
- "integrity": "sha512-6zLj4YIoIpfTGKrDMTbeZRpa8ih4EymMCKmddEDcJWrCdp/N1D46B38YEz4creTb4T177AVS9EyXkLeC/HL2jA==",
+ "version": "6.23.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.23.0.tgz",
+ "integrity": "sha512-/51px9N4uW8NpuWkyUX+iam5+PM6io2fm+QmRnzwqBy5v/pwGg9T0kILFtYeum8hjuvENtgsGNKluOfqIICmeQ==",
"dependencies": {
- "@codemirror/state": "^6.1.4",
+ "@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
@@ -1953,9 +1970,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -2056,41 +2073,42 @@
}
},
"node_modules/@lezer/common": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.0.tgz",
- "integrity": "sha512-XPIN3cYDXsoJI/oDWoR2tD++juVrhgIago9xyKhZ7IhGlzdDM9QgC8D8saKNCz5pindGcznFr2HBSsEQSWnSjw=="
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.0.tgz",
+ "integrity": "sha512-Wmvlm4q6tRpwiy20TnB3yyLTZim38Tkc50dPY8biQRwqE+ati/wD84rm3N15hikvdT4uSg9phs9ubjvcLmkpKg=="
},
"node_modules/@lezer/css": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.3.tgz",
- "integrity": "sha512-SjSM4pkQnQdJDVc80LYzEaMiNy9txsFbI7HsMgeVF28NdLaAdHNtQ+kB/QqDUzRBV/75NTXjJ/R5IdC8QQGxMg==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.6.tgz",
+ "integrity": "sha512-/HhbnfXchRc995VdDH9TBzd1B2CO/A4uhOhELqGjd7Bymgc+tGlb0W9Vp5GA1Otq8Ef4JCXpuKmr4hH3aFny6A==",
"dependencies": {
+ "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/highlight": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz",
- "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
+ "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/html": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.6.tgz",
- "integrity": "sha512-Kk9HJARZTc0bAnMQUqbtuhFVsB4AnteR2BFUWfZV7L/x1H0aAKz6YabrfJ2gk/BEgjh9L3hg5O4y2IDZRBdzuQ==",
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.8.tgz",
+ "integrity": "sha512-EXseJ3pUzWxE6XQBQdqWHZqqlGQRSuNMBcLb6mZWS2J2v+QZhOObD+3ZIKIcm59ntTzyor4LqFTb72iJc3k23Q==",
"dependencies": {
- "@lezer/common": "^1.0.0",
+ "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
- "version": "1.4.9",
- "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.9.tgz",
- "integrity": "sha512-7Uv8mBBE6l44spgWEZvEMdDqGV+FIuY7kJ1o5TFm+jxIuxydO3PcKJYiINij09igd1D/9P7l2KDqpkN8c3bM6A==",
+ "version": "1.4.12",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.12.tgz",
+ "integrity": "sha512-kwO5MftUiyfKBcECMEDc4HYnc10JME9kTJNPVoCXqJj/Y+ASWF0rgstORi3BThlQI6SoPSshrK5TjuiLFnr29A==",
"dependencies": {
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
@@ -2105,9 +2123,9 @@
}
},
"node_modules/@lezer/markdown": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.1.0.tgz",
- "integrity": "sha512-JYOI6Lkqbl83semCANkO3CKbKc0pONwinyagBufWBm+k4yhIcqfCF8B8fpEpvJLmIy7CAfwiq7dQ/PzUZA340g==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.2.0.tgz",
+ "integrity": "sha512-d7MwsfAukZJo1GpPrcPGa3MxaFFOqNp0gbqF+3F7pTeNDOgeJN1muXzx1XXDPt+Ac+/voCzsH7qXqnn+xReG/g==",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
@@ -2237,9 +2255,9 @@
}
},
"node_modules/@types/d3": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.2.tgz",
- "integrity": "sha512-Y4g2Yb30ZJmmtqAJTqMRaqXwRawfvpdpVmyEYEcyGNhrQI/Zvkq3k7yE1tdN07aFSmNBfvmegMQ9Fe2qy9ZMhw==",
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
@@ -2274,185 +2292,185 @@
}
},
"node_modules/@types/d3-array": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.0.tgz",
- "integrity": "sha512-tjU8juPSfhMnu6mJZPOCVVGba4rZoE0tjHDPb81PYwA8CzbaFscGjgkUM7juUJu6iWA1cCVWNEVwxZ5HN9Jj8Q=="
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
},
"node_modules/@types/d3-axis": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.5.tgz",
- "integrity": "sha512-ufDAV3SQzju+uB3Jlty7SUb/jMigjpIlvDDcSGvGmmO6OT/sNO93UE0dRzwWOZeBLzrLSA0CQM4bf3iq1std3A==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.5.tgz",
- "integrity": "sha512-JROQXZNq1X6QdWstESDUv1VilwZ2hBCQnWB91yal+5yZvYwGQvYsGCjrkHGfKK/8/AcX1JnERmpQzdDDuLRUsA==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.5.tgz",
- "integrity": "sha512-rs26AIhJjtc+XLR4YQU8IjPTLOlDVO4PR1y+pVFYEHzKh2tE5tYz3MF4QV6iz7HboXQEaYpJQt8dH9uUkne8yA=="
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="
},
"node_modules/@types/d3-color": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.2.tgz",
- "integrity": "sha512-At+Ski7dL8Bs58E8g8vPcFJc8tGcaC12Z4m07+p41+DRqnZQcAlp3NfYjLrhNYv+zEyQitU1CUxXNjqUyf+c0g=="
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-contour": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.5.tgz",
- "integrity": "sha512-wLvjwdOQVd1NL1IcW90CCt1VtpeZ3V20p/OTXlkT8uAiprrJnq2PNNnRNe1QCez4U9aMU29Z14zpJQVLW1+Lcg==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.3.tgz",
- "integrity": "sha512-+Lf5NPKZ4JBC9tbudVkKceQXRxU3jJs0el9aKQvinMtdnFSOG84eVXyhCNgIFuXNQO3iIcYs7sgzN359FEOZnQ=="
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="
},
"node_modules/@types/d3-dispatch": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.5.tgz",
- "integrity": "sha512-hxvq2kc+9hydVppo21JCGfcM0tLTh1DXnG3MLN0KlxsNZJH4bsdl1iXDuWtXFpWWlBrCMwSqlnoLPDxNAZU3Bg=="
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+ "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ=="
},
"node_modules/@types/d3-drag": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.5.tgz",
- "integrity": "sha512-arHyAGvO0NEGGPCU2jTb31TlXeSxwty1bIxr5wOFOCVqVjgriXloLWXoRp39Oa0Y/qXxcAVMIonAWLrtLxUZAQ==",
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-dsv": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.5.tgz",
- "integrity": "sha512-73WZR3QFOaSRVz9iOrebTbTnbo7xjcgS/i0Cq5zy0jMXPO3v/JbkTD3Zqii1eYE6v4EJ78g5VP407rm+p8fdlA=="
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="
},
"node_modules/@types/d3-ease": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.1.tgz",
- "integrity": "sha512-VZofjpEt8HWv3nxUAosj5o/+4JflnJ7Bbv07k17VO3T2WRuzGdZeookfaF60iVh5RdhVG49LE5w6LIshVUC6rg=="
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
},
"node_modules/@types/d3-fetch": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.5.tgz",
- "integrity": "sha512-Rc8pb6H0RRLpAV2hEXduykUgcDUOhjSLTLmCIeo6ejzgs4SaITh/EteMb3p5Env3Hqjsqw0fCksyqopHHzMkMg==",
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.7.tgz",
- "integrity": "sha512-rsok4CEvPLyVWRPsFiBhanJc3up03H/EARVz4d8soPh8drv82YMuAckYy4yv8g4/81JwCng5U5/o9aj9d0T6bQ=="
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz",
+ "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA=="
},
"node_modules/@types/d3-format": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.3.tgz",
- "integrity": "sha512-kxuLXSAEJykTeL/EI3tUiEfGqru7PRdqEy099YBnqFl+fF167UVSB4+wntlZv86ZdoYf0DHjsRHnTIm8kcH7qw=="
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="
},
"node_modules/@types/d3-geo": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.6.tgz",
- "integrity": "sha512-wblAES3b+C3hvp4VakwECEKtHquT/xc6K4HOna95LM1j1fd7s7WmU4V+JMQZfKhNCMkV2vWD+ZUgY2Uj6gqfuA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.5.tgz",
- "integrity": "sha512-DEcBUj1IL3WyPLDlh4m2nsNXnMLITXM5Vwcu4G85yJHtf2cVGPBjgky3L11WBnT+ayHKf06Tchk5mY1eGmd4WQ=="
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz",
+ "integrity": "sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw=="
},
"node_modules/@types/d3-interpolate": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.3.tgz",
- "integrity": "sha512-6OZ2EIB4lLj+8cUY7I/Cgn9Q+hLdA4DjJHYOQDiHL0SzqS1K9DL5xIOVBSIHgF+tiuO9MU1D36qvdIvRDRPh+Q==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.1.tgz",
- "integrity": "sha512-blRhp7ki7pVznM8k6lk5iUU9paDbVRVq+/xpf0RRgSJn5gr6SE7RcFtxooYGMBOc1RZiGyqRpVdu5AD0z0ooMA=="
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz",
+ "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA=="
},
"node_modules/@types/d3-polygon": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.1.tgz",
- "integrity": "sha512-nrcWPk7B9qs6xnpq60Cls44zm9eDmFAv65qi/N/emh/oftnG6uYz49aIS0mdFaGeJxVN8H3pHneMuZMV8EwFdw=="
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="
},
"node_modules/@types/d3-quadtree": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.4.tgz",
- "integrity": "sha512-B725MopFDIOQ6njFbeOxIEf42HVO2Xv+FmcxQISdOKErvLbFqWz3Riu+OWujUYoogreqqyHBHcGGL/JzzXQYsw=="
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="
},
"node_modules/@types/d3-random": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.2.tgz",
- "integrity": "sha512-8QhsqkKs6mymAZMrg3ZFXPxKA34rdgp3ZrtB8o6mhFsKAd1gOvR1gocWnca+kmXypQdwgnzKm9gZE2Uw8NjjKw=="
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="
},
"node_modules/@types/d3-scale": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.6.tgz",
- "integrity": "sha512-lo3oMLSiqsQUovv8j15X4BNEDOsnHuGjeVg7GRbAuB2PUa1prK5BNSOu6xixgNf3nqxPl4I1BqJWrPvFGlQoGQ==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
+ "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.1.tgz",
- "integrity": "sha512-Ob7OrwiTeQXY/WBBbRHGZBOn6rH1h7y3jjpTSKYqDEeqFjktql6k2XSgNwLrLDmAsXhEn8P9NHDY4VTuo0ZY1w=="
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz",
+ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw=="
},
"node_modules/@types/d3-selection": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.8.tgz",
- "integrity": "sha512-pxCZUfQyedq/DIlPXIR5wE1mIH37omOdx1yxRudL3KZ4AC+156jMjOv1z5RVlGq62f8WX2kyO0hTVgEx627QFg=="
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz",
+ "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg=="
},
"node_modules/@types/d3-shape": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.4.tgz",
- "integrity": "sha512-M2/xsWPsjaZc5ifMKp1EBp0gqJG0eO/zlldJNOC85Y/5DGsBQ49gDkRJ2h5GY7ZVD6KUumvZWsylSbvTaJTqKg==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
+ "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.2.tgz",
- "integrity": "sha512-kbdRXTmUgNfw5OTE3KZnFQn6XdIc4QGroN5UixgdrXATmYsdlPQS6pEut9tVlIojtzuFD4txs/L+Rq41AHtLpg=="
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
+ "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
},
"node_modules/@types/d3-time-format": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.2.tgz",
- "integrity": "sha512-wr08C1Gh77qaN8JIkrn5Rz/bdt5M9bdEqFmEOcYhUSq2t2sHvLTBfb4XAtGB3D4hm0ubj50NXWWXoXyp5tPXDg=="
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="
},
"node_modules/@types/d3-timer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.1.tgz",
- "integrity": "sha512-GGTvzKccVEhxmRfJEB6zhY9ieT4UhGVUIQaBzFpUO9OXy2ycAlnPCSJLzmGGgqt3KVjqN3QCQB4g1rsZnHsWhg=="
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
"node_modules/@types/d3-transition": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.6.tgz",
- "integrity": "sha512-K0To23B5UxNwFtKORnS5JoNYvw/DnknU5MzhHIS9czJ/lTqFFDeU6w9lArOdoTl0cZFNdNrMJSFCbRCEHccH2w==",
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz",
+ "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.6.tgz",
- "integrity": "sha512-dGZQaXEu7aNcCL71LPpjB58IjoQNM9oDPfQuMUJ7N/fbkcIWGX2PnmUWO1jPJ+RLbZBpRUggJUX8twKRvo2hKQ==",
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
@@ -2468,23 +2486,23 @@
}
},
"node_modules/@types/eslint-scope": {
- "version": "3.7.6",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz",
- "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==",
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
}
},
"node_modules/@types/estree": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz",
- "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw=="
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/geojson": {
- "version": "7946.0.12",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.12.tgz",
- "integrity": "sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA=="
+ "version": "7946.0.13",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz",
+ "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ=="
},
"node_modules/@types/glob": {
"version": "7.2.0",
@@ -2497,9 +2515,9 @@
}
},
"node_modules/@types/json-schema": {
- "version": "7.0.14",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
- "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw=="
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"node_modules/@types/json5": {
"version": "0.0.29",
@@ -2514,9 +2532,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.8.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
- "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
+ "version": "20.10.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz",
+ "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -2537,42 +2555,42 @@
"dev": true
},
"node_modules/@types/sizzle": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz",
- "integrity": "sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ==",
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
+ "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
"dev": true
},
"node_modules/@types/source-list-map": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.4.tgz",
- "integrity": "sha512-Kdfm7Sk5VX8dFW7Vbp18+fmAatBewzBILa1raHYxrGEFXT0jNl9x3LWfuW7bTbjEKFNey9Dfkj/UzT6z/NvRlg==",
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz",
+ "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==",
"dev": true
},
"node_modules/@types/tapable": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.10.tgz",
- "integrity": "sha512-q8F20SdXG5fdVJQ5yxsVlH+f+oekP42QeHv4s5KlrxTMT0eopXn7ol1rhxMcksf8ph7XNv811iVDE2hOpUvEPg==",
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz",
+ "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==",
"dev": true
},
"node_modules/@types/trusted-types": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.5.tgz",
- "integrity": "sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA==",
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
},
"node_modules/@types/uglify-js": {
- "version": "3.17.3",
- "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.3.tgz",
- "integrity": "sha512-ToldSfJ6wxO21cakcz63oFD1GjqQbKzhZCD57eH7zWuYT5UEZvfUoqvrjX5d+jB9g4a/sFO0n6QSVzzn5sMsjg==",
+ "version": "3.17.4",
+ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.4.tgz",
+ "integrity": "sha512-Hm/T0kV3ywpJyMGNbsItdivRhYNCQQf1IIsYsXnoVPES4t+FMLyDe0/K+Ea7ahWtMtSNb22ZdY7MIyoD9rqARg==",
"dev": true,
"dependencies": {
"source-map": "^0.6.1"
}
},
"node_modules/@types/webpack": {
- "version": "4.41.35",
- "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.35.tgz",
- "integrity": "sha512-XRC6HLGHtNfN8/xWeu1YUQV1GSE+28q8lSqvcJ+0xt/zW9Wmn4j9pCSvaXPyRlCKrl5OuqECQNEJUy2vo8oWqg==",
+ "version": "4.41.38",
+ "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.38.tgz",
+ "integrity": "sha512-oOW7E931XJU1mVfCnxCVgv8GLFL768pDO5u2Gzk82i8yTIgX6i7cntyZOkZYb/JtYM8252SN9bQp9tgkVDSsRw==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -2584,9 +2602,9 @@
}
},
"node_modules/@types/webpack-sources": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.2.tgz",
- "integrity": "sha512-acCzhuVe+UJy8abiSFQWXELhhNMZjQjQKpLNEi1pKGgKXZj0ul614ATcx4kkhunPost6Xw+aCq8y8cn1/WwAiA==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==",
"dev": true,
"dependencies": {
"@types/node": "*",
@@ -2604,9 +2622,9 @@
}
},
"node_modules/@types/yauzl": {
- "version": "2.10.2",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.2.tgz",
- "integrity": "sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==",
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"dev": true,
"optional": true,
"dependencies": {
@@ -2614,13 +2632,16 @@
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "2.7.15",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.15.tgz",
- "integrity": "sha512-FCvIEevPmgCgqFBH7wD+3B97y7u7oj/Wr69zADBf403Tui377bThTjBvekaZvlRr4IwUAu3M6hYZeULZFJbdYg==",
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
+ "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
"dependencies": {
- "@babel/parser": "^7.18.4",
+ "@babel/parser": "^7.23.5",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
+ },
+ "optionalDependencies": {
+ "prettier": "^1.18.2 || ^2.0.0"
}
},
"node_modules/@webassemblyjs/ast": {
@@ -3303,13 +3324,13 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
- "version": "0.4.6",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz",
- "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==",
+ "version": "0.4.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
+ "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==",
"dev": true,
"dependencies": {
"@babel/compat-data": "^7.22.6",
- "@babel/helper-define-polyfill-provider": "^0.4.3",
+ "@babel/helper-define-polyfill-provider": "^0.4.4",
"semver": "^6.3.1"
},
"peerDependencies": {
@@ -3317,12 +3338,12 @@
}
},
"node_modules/babel-plugin-polyfill-corejs3": {
- "version": "0.8.6",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz",
- "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==",
+ "version": "0.8.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz",
+ "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==",
"dev": true,
"dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.4.3",
+ "@babel/helper-define-polyfill-provider": "^0.4.4",
"core-js-compat": "^3.33.1"
},
"peerDependencies": {
@@ -3330,12 +3351,12 @@
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz",
- "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==",
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz",
+ "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==",
"dev": true,
"dependencies": {
- "@babel/helper-define-polyfill-provider": "^0.4.3"
+ "@babel/helper-define-polyfill-provider": "^0.4.4"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -3426,9 +3447,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
- "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
+ "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"funding": [
{
"type": "opencollective",
@@ -3444,9 +3465,9 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001541",
- "electron-to-chromium": "^1.4.535",
- "node-releases": "^2.0.13",
+ "caniuse-lite": "^1.0.30001565",
+ "electron-to-chromium": "^1.4.601",
+ "node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@@ -3538,9 +3559,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001561",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz",
- "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==",
+ "version": "1.0.30001574",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz",
+ "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==",
"funding": [
{
"type": "opencollective",
@@ -3893,12 +3914,12 @@
}
},
"node_modules/core-js-compat": {
- "version": "3.33.2",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz",
- "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==",
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz",
+ "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==",
"dev": true,
"dependencies": {
- "browserslist": "^4.22.1"
+ "browserslist": "^4.22.2"
},
"funding": {
"type": "opencollective",
@@ -3946,14 +3967,14 @@
}
},
"node_modules/csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/cypress": {
- "version": "13.4.0",
- "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.4.0.tgz",
- "integrity": "sha512-KeWNC9xSHG/ewZURVbaQsBQg2mOKw4XhjJZFKjWbEjgZCdxpPXLpJnfq5Jns1Gvnjp6AlnIfpZfWFlDgVKXdWQ==",
+ "version": "13.6.2",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz",
+ "integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -4009,9 +4030,9 @@
}
},
"node_modules/cypress/node_modules/@types/node": {
- "version": "18.18.8",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.8.tgz",
- "integrity": "sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==",
+ "version": "18.19.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.5.tgz",
+ "integrity": "sha512-22MG6T02Hos2JWfa1o5jsIByn+bc5iOt1IS4xyg6OG68Bu+wMonVZzdrgCw693++rpLE9RUT/Bx15BeDzO0j+g==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -4136,9 +4157,9 @@
"dev": true
},
"node_modules/cytoscape": {
- "version": "3.27.0",
- "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.27.0.tgz",
- "integrity": "sha512-pPZJilfX9BxESwujODz5pydeGi+FBrXq1rcaB1mfhFXXFJ9GjE6CNndAk+8jPzoXGD+16LtSS4xlYEIUiW4Abg==",
+ "version": "3.28.1",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz",
+ "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==",
"dependencies": {
"heap": "^0.2.6",
"lodash": "^4.17.21"
@@ -4827,9 +4848,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.576",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz",
- "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA=="
+ "version": "1.4.623",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.623.tgz",
+ "integrity": "sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A=="
},
"node_modules/elkjs": {
"version": "0.8.2",
@@ -4995,9 +5016,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
- "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q=="
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
+ "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="
},
"node_modules/es-set-tostringtag": {
"version": "2.0.2",
@@ -5220,9 +5241,9 @@
}
},
"node_modules/eslint-plugin-cypress/node_modules/globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -5245,9 +5266,9 @@
}
},
"node_modules/eslint-plugin-import": {
- "version": "2.29.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
- "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+ "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.7",
@@ -5266,7 +5287,7 @@
"object.groupby": "^1.0.1",
"object.values": "^1.1.7",
"semver": "^6.3.1",
- "tsconfig-paths": "^3.14.2"
+ "tsconfig-paths": "^3.15.0"
},
"engines": {
"node": ">=4"
@@ -5589,9 +5610,9 @@
}
},
"node_modules/eslint/node_modules/globals": {
- "version": "13.23.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
- "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -5842,9 +5863,9 @@
"dev": true
},
"node_modules/fast-glob": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
- "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -5892,9 +5913,9 @@
}
},
"node_modules/fastq": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
+ "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
"dependencies": {
"reusify": "^1.0.4"
}
@@ -6053,16 +6074,16 @@
}
},
"node_modules/flat-cache": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
- "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.3",
"rimraf": "^3.0.2"
},
"engines": {
- "node": ">=12.0.0"
+ "node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flat-cache/node_modules/rimraf": {
@@ -6408,9 +6429,9 @@
}
},
"node_modules/globby/node_modules/ignore": {
- "version": "5.2.4",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
- "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"engines": {
"node": ">= 4"
}
@@ -6537,9 +6558,9 @@
"integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg=="
},
"node_modules/htmx.org": {
- "version": "1.9.7",
- "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.7.tgz",
- "integrity": "sha512-bHONSJ3WBtMG5Ex6xmC+t///rCKINrrPNeZ0Nq9cylVbzZ9ZGjWn1z60hripbqZeIM3WSpEATgm+MglDIK5WVQ=="
+ "version": "1.9.10",
+ "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz",
+ "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA=="
},
"node_modules/http-signature": {
"version": "1.3.6",
@@ -7906,9 +7927,9 @@
}
},
"node_modules/lint-staged/node_modules/npm-run-path": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
- "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
+ "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
"dev": true,
"dependencies": {
"path-key": "^4.0.0"
@@ -8435,14 +8456,14 @@
}
},
"node_modules/markmap-common": {
- "version": "0.15.3",
- "resolved": "https://registry.npmjs.org/markmap-common/-/markmap-common-0.15.3.tgz",
- "integrity": "sha512-a40FfdzFEKoyIhd5KDsV6FfkM55WWi2spRq/cUCsOZd8e4PAHMc9auCEjdxTWRiS2EGzET9sPpvyAmPSTva+/w==",
+ "version": "0.15.6",
+ "resolved": "https://registry.npmjs.org/markmap-common/-/markmap-common-0.15.6.tgz",
+ "integrity": "sha512-uBJkdHvkppTiaw+IXau6aVQgN6F/o2BT6l6QghavQLrd6r7H5Ce7/EcMAg+T4RwBKqzJoraLARkIWARXuqmOgw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.22.6",
"@gera2ld/jsx-dom": "^2.2.2",
- "npm2url": "^0.2.1"
+ "npm2url": "^0.2.4"
}
},
"node_modules/markmap-lib": {
@@ -8504,9 +8525,9 @@
}
},
"node_modules/markmap-view": {
- "version": "0.15.4",
- "resolved": "https://registry.npmjs.org/markmap-view/-/markmap-view-0.15.4.tgz",
- "integrity": "sha512-6PJnoPZHiQIb0YE+fg0Ht1ptEXgf9QOOTBEiC2DrzjAbZ3rrRa2g+0VhpZ+9vSzDsUa+m/IGz4xkiNepuPfs5w==",
+ "version": "0.15.8",
+ "resolved": "https://registry.npmjs.org/markmap-view/-/markmap-view-0.15.8.tgz",
+ "integrity": "sha512-/QrVf2cxgsMUEO256SQd9w1VykT4XTF6cJwUHiUO/dF4UjyIkTQ2TMzhoVGjj2XzppQtS2mjXcjPq2ePX16RUA==",
"dependencies": {
"@babel/runtime": "^7.22.6",
"@gera2ld/jsx-dom": "^2.2.2",
@@ -8785,9 +8806,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.13",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
- "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
"node_modules/non-layered-tidy-tree-layout": {
"version": "2.0.2",
@@ -8815,9 +8836,9 @@
}
},
"node_modules/npm2url": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/npm2url/-/npm2url-0.2.1.tgz",
- "integrity": "sha512-Ls7mMyud1Kk0EisqsTt2TPtM7gLRvgmvDxOg3FPI5zjfhQ+ZFNBXX2K9VT7vo+HqUsz/uCiIxkcO0SvIuneVug==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/npm2url/-/npm2url-0.2.4.tgz",
+ "integrity": "sha512-arzGp/hQz0Ey+ZGhF64XVH7Xqwd+1Q/po5uGiBbzph8ebX6T0uvt3N7c1nBHQNsQVykQgHhqoRTX7JFcHecGuw==",
"peer": true
},
"node_modules/object-assign": {
@@ -8847,13 +8868,13 @@
}
},
"node_modules/object.assign": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
- "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
"has-symbols": "^1.0.3",
"object-keys": "^1.1.1"
},
@@ -9198,9 +9219,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.31",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
- "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "version": "8.4.33",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
+ "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
"funding": [
{
"type": "opencollective",
@@ -9216,7 +9237,7 @@
}
],
"dependencies": {
- "nanoid": "^3.3.6",
+ "nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
@@ -9236,7 +9257,7 @@
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
- "dev": true,
+ "devOptional": true,
"bin": {
"prettier": "bin-prettier.js"
},
@@ -9510,9 +9531,9 @@
}
},
"node_modules/regenerator-runtime": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
- "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
@@ -9899,9 +9920,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
- "version": "1.69.5",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz",
- "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==",
+ "version": "1.69.7",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz",
+ "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -9916,9 +9937,9 @@
}
},
"node_modules/sass-loader": {
- "version": "13.3.2",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz",
- "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==",
+ "version": "13.3.3",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz",
+ "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==",
"dev": true,
"dependencies": {
"neo-async": "^2.6.2"
@@ -10376,9 +10397,9 @@
"integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA=="
},
"node_modules/stylis": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz",
- "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ=="
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz",
+ "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ=="
},
"node_modules/supports-color": {
"version": "5.5.0",
@@ -10532,9 +10553,9 @@
}
},
"node_modules/terser": {
- "version": "5.24.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
- "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
+ "version": "5.26.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz",
+ "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -10549,15 +10570,15 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.9",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
- "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
+ "version": "5.3.10",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
+ "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.17",
+ "@jridgewell/trace-mapping": "^0.3.20",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.1",
- "terser": "^5.16.8"
+ "terser": "^5.26.0"
},
"engines": {
"node": ">= 10.13.0"
@@ -10599,9 +10620,9 @@
}
},
"node_modules/terser/node_modules/acorn": {
- "version": "8.11.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
- "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": {
"acorn": "bin/acorn"
},
@@ -10620,10 +10641,13 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
},
"node_modules/throttleit": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
- "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==",
- "dev": true
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz",
+ "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
},
"node_modules/through": {
"version": "2.3.8",
@@ -10764,9 +10788,9 @@
}
},
"node_modules/tsconfig-paths": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
- "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
"dev": true,
"dependencies": {
"@types/json5": "^0.0.29",
@@ -11070,9 +11094,9 @@
"integrity": "sha512-nah+xbVInVJaO6+C5PEUqaougmv8BN8aa7ZCtmVQcX6eWIZGRukckFtseXSI7KD7/nXtwkJe624y42T0r+L+AQ=="
},
"node_modules/vega": {
- "version": "5.25.0",
- "resolved": "https://registry.npmjs.org/vega/-/vega-5.25.0.tgz",
- "integrity": "sha512-lr+uj0mhYlSN3JOKbMNp1RzZBenWp9DxJ7kR3lha58AFNCzzds7pmFa7yXPbtbaGhB7Buh/t6n+Bzk3Y0VnF5g==",
+ "version": "5.27.0",
+ "resolved": "https://registry.npmjs.org/vega/-/vega-5.27.0.tgz",
+ "integrity": "sha512-iYMQZYb2nlJBLCsUZ88pvun2sTcFcLE7GKJWisndLo+KYNMQIRePQ7X2FRuy8yvRRNxfO8XhjImh4OwxZvyYVA==",
"dependencies": {
"vega-crossfilter": "~4.1.1",
"vega-dataflow": "~5.7.5",
@@ -11081,25 +11105,25 @@
"vega-expression": "~5.1.0",
"vega-force": "~4.2.0",
"vega-format": "~1.1.1",
- "vega-functions": "~5.13.2",
+ "vega-functions": "~5.14.0",
"vega-geo": "~4.4.1",
"vega-hierarchy": "~4.1.1",
"vega-label": "~1.2.1",
"vega-loader": "~4.5.1",
- "vega-parser": "~6.2.0",
+ "vega-parser": "~6.2.1",
"vega-projection": "~1.6.0",
"vega-regression": "~1.2.0",
"vega-runtime": "~6.1.4",
- "vega-scale": "~7.3.0",
- "vega-scenegraph": "~4.10.2",
+ "vega-scale": "~7.3.1",
+ "vega-scenegraph": "~4.11.2",
"vega-statistics": "~1.9.0",
"vega-time": "~2.1.1",
- "vega-transforms": "~4.10.2",
- "vega-typings": "~0.24.0",
+ "vega-transforms": "~4.11.1",
+ "vega-typings": "~1.1.0",
"vega-util": "~1.17.2",
- "vega-view": "~5.11.1",
+ "vega-view": "~5.12.0",
"vega-view-transforms": "~4.5.9",
- "vega-voronoi": "~4.2.1",
+ "vega-voronoi": "~4.2.2",
"vega-wordcloud": "~4.1.4"
}
},
@@ -11129,9 +11153,9 @@
}
},
"node_modules/vega-embed": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-6.23.0.tgz",
- "integrity": "sha512-8Iava57LdROsatqsOhMLErHYaBpZBB7yZJlSVU3/xOK3l8Ft5WFnj5fm3OOAVML97/0yULE7LRVjXW5hV3fSpg==",
+ "version": "6.24.0",
+ "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-6.24.0.tgz",
+ "integrity": "sha512-ANCksO3lXhdLzQn7Mfistm1dsRwQhxTYICVpru7TMc+Ywe7C4vOLWuaVv9Qvem2IgQyuVCal0EoTMKvIVYykFg==",
"bundleDependencies": [
"yallist"
],
@@ -11139,11 +11163,11 @@
"fast-json-patch": "^3.1.1",
"json-stringify-pretty-compact": "^3.0.0",
"semver": "^7.5.4",
- "tslib": "^2.6.1",
+ "tslib": "^2.6.2",
"vega-interpreter": "^1.0.5",
"vega-schema-url-parser": "^2.2.0",
"vega-themes": "^2.14.0",
- "vega-tooltip": "^0.33.0",
+ "vega-tooltip": "^0.34.0",
"yallist": "*"
},
"peerDependencies": {
@@ -11231,9 +11255,9 @@
}
},
"node_modules/vega-functions": {
- "version": "5.13.2",
- "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.13.2.tgz",
- "integrity": "sha512-YE1Xl3Qi28kw3vdXVYgKFMo20ttd3+SdKth1jUNtBDGGdrOpvPxxFhZkVqX+7FhJ5/1UkDoAYs/cZY0nRKiYgA==",
+ "version": "5.14.0",
+ "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.14.0.tgz",
+ "integrity": "sha512-Q0rocHmJDfQ0tS91kdN8WcEosq1e3HPK1Yf5z36SPYPmTzKw3uxUGE52tLxC832acAYqPmi8R41wAoI/yFQTPg==",
"dependencies": {
"d3-array": "^3.2.2",
"d3-color": "^3.1.0",
@@ -11242,7 +11266,7 @@
"vega-expression": "^5.1.0",
"vega-scale": "^7.3.0",
"vega-scenegraph": "^4.10.2",
- "vega-selections": "^5.4.1",
+ "vega-selections": "^5.4.2",
"vega-statistics": "^1.8.1",
"vega-time": "^2.1.1",
"vega-util": "^1.17.1"
@@ -11298,9 +11322,9 @@
}
},
"node_modules/vega-lite": {
- "version": "5.16.1",
- "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.16.1.tgz",
- "integrity": "sha512-3iXmzdAVZCGHrvdh6hIM8OY55auXA1EIDzFLaYdq27e99Dr+WXTEa00ilqQUPdrpS0sE1ZqK4Ikhgg5x8SOtLw==",
+ "version": "5.16.3",
+ "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.16.3.tgz",
+ "integrity": "sha512-F3HO/BqlyyB1D0tf/+qy1JOmq7bHtG/nvsXcgNVUFjgVgvVKL4sMnxVnYzSsIg10x/6RFxLfwWJSd0cA8MuuUA==",
"dependencies": {
"json-stringify-pretty-compact": "~3.0.0",
"tslib": "~2.6.2",
@@ -11335,15 +11359,15 @@
}
},
"node_modules/vega-parser": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-6.2.0.tgz",
- "integrity": "sha512-as+QnX8Qxe9q51L1C2sVBd+YYYctP848+zEvkBT2jlI2g30aZ6Uv7sKsq7QTL6DUbhXQKR0XQtzlanckSFdaOQ==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-6.2.1.tgz",
+ "integrity": "sha512-F79bQXt6fMkACR+TfFl7ueehKO26yCR/3iRZxhU7/pgHerx/d8K8pf2onMguu3NAN4eitT+PPuTgkDZtcqo9Qg==",
"dependencies": {
"vega-dataflow": "^5.7.5",
"vega-event-selector": "^3.0.1",
- "vega-functions": "^5.13.1",
- "vega-scale": "^7.3.0",
- "vega-util": "^1.17.1"
+ "vega-functions": "^5.14.0",
+ "vega-scale": "^7.3.1",
+ "vega-util": "^1.17.2"
}
},
"node_modules/vega-projection": {
@@ -11377,9 +11401,9 @@
}
},
"node_modules/vega-scale": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-7.3.0.tgz",
- "integrity": "sha512-pMOAI2h+e1z7lsqKG+gMfR6NKN2sTcyjZbdJwntooW0uFHwjLGjMSY7kSd3nSEquF0HQ8qF7zR6gs1eRwlGimw==",
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-7.3.1.tgz",
+ "integrity": "sha512-tyTlaaCpHN2Ik/PPKl/j9ThadBDjPtypqW1D7IsUSkzfoZ7RPlI2jwAaoj2C/YW5jFRbEOx3njmjogp48I5CvA==",
"dependencies": {
"d3-array": "^3.2.2",
"d3-interpolate": "^3.0.1",
@@ -11389,9 +11413,9 @@
}
},
"node_modules/vega-scenegraph": {
- "version": "4.10.2",
- "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.10.2.tgz",
- "integrity": "sha512-R8m6voDZO5+etwNMcXf45afVM3XAtokMqxuDyddRl9l1YqSJfS+3u8hpolJ50c2q6ZN20BQiJwKT1o0bB7vKkA==",
+ "version": "4.11.2",
+ "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.11.2.tgz",
+ "integrity": "sha512-PXSvv/L7Ek+9mwOTPLpzgkXdfGCR+AcWV5aquPGrqCWoiIF49VJkKFNT1HWxj3RZJX0XKo2r7SuXvRBb9EJ1aA==",
"dependencies": {
"d3-path": "^3.1.0",
"d3-shape": "^3.2.0",
@@ -11407,26 +11431,15 @@
"integrity": "sha512-yAtdBnfYOhECv9YC70H2gEiqfIbVkq09aaE4y/9V/ovEFmH9gPKaEgzIZqgT7PSPQjKhsNkb6jk6XvSoboxOBw=="
},
"node_modules/vega-selections": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.4.1.tgz",
- "integrity": "sha512-EtYc4DvA+wXqBg9tq+kDomSoVUPCmQfS7hUxy2qskXEed79YTimt3Hcl1e1fW226I4AVDBEqTTKebmKMzbSgAA==",
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.4.2.tgz",
+ "integrity": "sha512-99FUhYmg0jOJr2/K4TcEURmJRkuibrCDc8KBUX7qcQEITzrZ5R6a4QE+sarCvbb3hi8aA9GV2oyST6MQeA9mgQ==",
"dependencies": {
- "d3-array": "3.2.2",
+ "d3-array": "3.2.4",
"vega-expression": "^5.0.1",
"vega-util": "^1.17.1"
}
},
- "node_modules/vega-selections/node_modules/d3-array": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.2.tgz",
- "integrity": "sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ==",
- "dependencies": {
- "internmap": "1 - 2"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/vega-statistics": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.9.0.tgz",
@@ -11455,17 +11468,17 @@
}
},
"node_modules/vega-tooltip": {
- "version": "0.33.0",
- "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.33.0.tgz",
- "integrity": "sha512-jMcvH2lP20UfyvO2KAEdloiwRyasikaiLuNFhzwrrzf2RamGTxP4G7B2OZ2QENfrGUH05Z9ei5tn/eErdzOaZQ==",
+ "version": "0.34.0",
+ "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.34.0.tgz",
+ "integrity": "sha512-TtxwkcLZ5aWQTvKGlfWDou8tISGuxmqAW1AgGZjrDpf75qsXvgtbPdRAAls2LZMqDxpr5T1kMEZs9XbSpiI8yw==",
"dependencies": {
"vega-util": "^1.17.2"
}
},
"node_modules/vega-transforms": {
- "version": "4.10.2",
- "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.10.2.tgz",
- "integrity": "sha512-sJELfEuYQ238PRG+GOqQch8D69RYnJevYSGLsRGQD2LxNz3j+GlUX6Pid+gUEH5HJy22Q5L0vsTl2ZNhIr4teQ==",
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.11.1.tgz",
+ "integrity": "sha512-DDbqEQnvy9/qEvv0bAKPqAuzgaNb7Lh2xKJFom2Yzx4tZHCl8dnKxC1lH9JnJlAMdtZuiNLPARUkf3pCNQ/olw==",
"dependencies": {
"d3-array": "^3.2.2",
"vega-dataflow": "^5.7.5",
@@ -11475,14 +11488,14 @@
}
},
"node_modules/vega-typings": {
- "version": "0.24.2",
- "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.24.2.tgz",
- "integrity": "sha512-fW02GElYoqweCCaPqH6iH44UZnzXiX9kbm1qyecjU3k5s0vtufLI7Yuz/a/uL37mEAqTMQplBBAlk0T9e2e1Dw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-1.1.0.tgz",
+ "integrity": "sha512-uI6RWlMiGRhsgmw/LzJtjCc0kwhw2f0JpyNMTAnOy90kE4e4CiaZN5nJp8S9CcfcBoPEZHc166AOn2SSNrKn3A==",
"dependencies": {
"@types/geojson": "7946.0.4",
"vega-event-selector": "^3.0.1",
- "vega-expression": "^5.0.1",
- "vega-util": "^1.17.1"
+ "vega-expression": "^5.1.0",
+ "vega-util": "^1.17.2"
}
},
"node_modules/vega-typings/node_modules/@types/geojson": {
@@ -11496,9 +11509,9 @@
"integrity": "sha512-omNmGiZBdjm/jnHjZlywyYqafscDdHaELHx1q96n5UOz/FlO9JO99P4B3jZg391EFG8dqhWjQilSf2JH6F1mIw=="
},
"node_modules/vega-view": {
- "version": "5.11.1",
- "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.11.1.tgz",
- "integrity": "sha512-RoWxuoEMI7xVQJhPqNeLEHCezudsf3QkVMhH5tCovBqwBADQGqq9iWyax3ZzdyX1+P3eBgm7cnLvpqtN2hU8kA==",
+ "version": "5.12.0",
+ "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.12.0.tgz",
+ "integrity": "sha512-T3GY7UJNVZGrCUrAmE/OCrkoJQyOT/2dCgXgy9EvDMVv/sdrn7o1TMKhSV18nIr0m5A7m4mgKwrmguAfROY85g==",
"dependencies": {
"d3-array": "^3.2.2",
"d3-timer": "^3.0.1",
@@ -11521,9 +11534,9 @@
}
},
"node_modules/vega-voronoi": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.2.1.tgz",
- "integrity": "sha512-zzi+fxU/SBad4irdLLsG3yhZgXWZezraGYVQfZFWe8kl7W/EHUk+Eqk/eetn4bDeJ6ltQskX+UXH3OP5Vh0Q0Q==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.2.2.tgz",
+ "integrity": "sha512-Bq2YOp2MGphhQnUuLwl3dsyBs6MuEU86muTjDbBJg33+HkZtE1kIoQZr+EUHa46NBsY1NzSKddOTu8wcaFrWiQ==",
"dependencies": {
"d3-delaunay": "^6.0.2",
"vega-dataflow": "^5.7.5",
@@ -11563,11 +11576,12 @@
"dev": true
},
"node_modules/vue": {
- "version": "2.7.15",
- "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.15.tgz",
- "integrity": "sha512-a29fsXd2G0KMRqIFTpRgpSbWaNBK3lpCTOLuGLEDnlHWdjB8fwl6zyYZ8xCrqkJdatwZb4mGHiEfJjnw0Q6AwQ==",
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
+ "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
+ "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
"dependencies": {
- "@vue/compiler-sfc": "2.7.15",
+ "@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
}
},
@@ -11594,9 +11608,9 @@
}
},
"node_modules/web-worker": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz",
- "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA=="
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz",
+ "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
@@ -11757,9 +11771,9 @@
}
},
"node_modules/webpack/node_modules/acorn": {
- "version": "8.11.2",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
- "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": {
"acorn": "bin/acorn"
},
diff --git a/pyproject.toml b/pyproject.toml
index f563024c2..f51823ab2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,23 @@ exclude = '''
)/
'''
+[tool.creosote]
+venvs = [".venv"]
+paths = ["funnel", "tests", "migrations/versions"]
+deps-file = "requirements/base.in"
+exclude-deps = [
+ "argon2-cffi", # Optional dep for passlib
+ "bcrypt", # Optional dep for passlib
+ "greenlet", # Optional dep for SQLAlchemy's asyncio support
+ "gunicorn", # Not imported, used as server
+ "linkify-it-py", # Optional dep for markdown-it-py
+ "psycopg", # Optional dep for SQLAlchemy
+ "rq-dashboard", # Creosote fails to recognise the import
+ "tzdata", # Resource-only dep, no code to import
+ "urllib3", # Unused but required to silence a pip-audit warning
+ "wtforms-sqlalchemy", # Temp dep on an unreleased git branch
+]
+
[tool.djlint]
profile = 'jinja'
extension = '.html.jinja2'
@@ -139,8 +156,8 @@ ignore_missing_imports = true
show_error_codes = true
warn_unreachable = true
warn_unused_ignores = true
-warn_redundant_casts = false
-check_untyped_defs = false
+warn_redundant_casts = true
+check_untyped_defs = true
[[tool.mypy.overrides]]
module = 'tests.*'
diff --git a/requirements/base.in b/requirements/base.in
index 8c78879a4..232b1f8b5 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -45,7 +45,7 @@ pycountry
pyIsEmail
python-dateutil
python-dotenv
-python-telegram-bot
+python-telegram-bot>=20.7
pytz
PyVimeo
qrcode
@@ -64,6 +64,6 @@ urllib3[socks] # Not direct dep; pip-audit complains of dupe urllib[socks] in t
user-agents
werkzeug
whitenoise
-wtforms-sqlalchemy @ git+https://github.com/jace/wtforms-sqlalchemy # See /pull/1
+wtforms-sqlalchemy
z-base-32
zxcvbn
diff --git a/requirements/base.txt b/requirements/base.txt
index b3dedcadc..05360f085 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -1,4 +1,4 @@
-# SHA1:61d921a696418138a9c8fbf50898b94a0a942f13
+# SHA1:af16bb123ee5c105566f3a83ba6f330975c48d6f
#
# This file is autogenerated by pip-compile-multi
# To update, run:
@@ -21,7 +21,7 @@ aiohttp-retry==2.8.3
# via twilio
aiosignal==1.3.1
# via aiohttp
-alembic==1.13.0
+alembic==1.13.1
# via
# -r requirements/base.in
# flask-migrate
@@ -35,7 +35,7 @@ argon2-cffi-bindings==21.2.0
# via argon2-cffi
arrow==1.3.0
# via rq-dashboard
-attrs==23.1.0
+attrs==23.2.0
# via aiohttp
babel==2.14.0
# via
@@ -155,7 +155,7 @@ flask-wtf==1.2.1
# via
# -r requirements/base.in
# baseframe
-freezegun==1.3.1
+freezegun==1.4.0
# via rq-scheduler
frozenlist==1.4.1
# via
@@ -228,7 +228,7 @@ joblib==1.3.2
# via nltk
linkify-it-py==2.0.2
# via -r requirements/base.in
-lxml==4.9.3
+lxml==5.0.1
# via premailer
mako==1.3.0
# via alembic
@@ -282,7 +282,7 @@ packaging==23.2
# marshmallow
passlib==1.7.4
# via -r requirements/base.in
-phonenumbers==8.13.26
+phonenumbers==8.13.27
# via -r requirements/base.in
playwright==1.40.0
# via -r requirements/base.in
@@ -290,9 +290,9 @@ premailer==3.10.0
# via -r requirements/base.in
progressbar2==4.3.2
# via -r requirements/base.in
-psycopg[binary]==3.1.15
+psycopg[binary]==3.1.17
# via -r requirements/base.in
-psycopg-binary==3.1.15
+psycopg-binary==3.1.17
# via psycopg
pyasn1==0.5.1
# via
@@ -318,7 +318,7 @@ pyisemail==2.0.1
# mxsniff
pyjwt==2.8.0
# via twilio
-pymdown-extensions==10.5
+pymdown-extensions==10.7
# via coaster
pyparsing==3.1.1
# via httplib2
@@ -365,7 +365,7 @@ redis==5.0.1
# rq-dashboard
redis-sentinel-url==1.0.1
# via rq-dashboard
-regex==2023.10.3
+regex==2023.12.25
# via nltk
requests==2.31.0
# via
@@ -420,7 +420,7 @@ sniffio==1.3.0
# via
# anyio
# httpx
-sqlalchemy[asyncio]==2.0.23
+sqlalchemy[asyncio]==2.0.25
# via
# -r requirements/base.in
# alembic
@@ -451,7 +451,7 @@ tweepy==4.14.0
# via -r requirements/base.in
twilio==8.11.0
# via -r requirements/base.in
-types-python-dateutil==2.8.19.14
+types-python-dateutil==2.8.19.20240106
# via arrow
typing-extensions==4.9.0
# via
@@ -467,7 +467,7 @@ typing-extensions==4.9.0
# typing-inspect
typing-inspect==0.9.0
# via dataclasses-json
-tzdata==2023.3
+tzdata==2023.4
# via -r requirements/base.in
ua-parser==0.18.0
# via user-agents
@@ -496,12 +496,12 @@ werkzeug==3.0.1
# flask
whitenoise==6.6.0
# via -r requirements/base.in
-wtforms==3.1.1
+wtforms==3.1.2
# via
# baseframe
# flask-wtf
# wtforms-sqlalchemy
-wtforms-sqlalchemy @ git+https://github.com/jace/wtforms-sqlalchemy
+wtforms-sqlalchemy==0.4.1
# via
# -r requirements/base.in
# baseframe
diff --git a/requirements/dev.txt b/requirements/dev.txt
index ccb172374..9fe958b35 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -16,7 +16,7 @@ astroid==3.0.2
# via pylint
bandit==1.7.6
# via -r requirements/dev.in
-black==23.12.0
+black==23.12.1
# via -r requirements/dev.in
build==1.0.3
# via pip-tools
@@ -24,19 +24,21 @@ cattrs==22.1.0
# via reformat-gherkin
cfgv==3.4.0
# via pre-commit
+colorama==0.4.6
+ # via djlint
cssbeautifier==1.14.11
# via djlint
dill==0.3.7
# via pylint
distlib==0.3.8
# via virtualenv
-djlint==1.34.0
+djlint==1.34.1
# via -r requirements/dev.in
editorconfig==0.12.3
# via
# cssbeautifier
# jsbeautifier
-flake8==6.1.0
+flake8==7.0.0
# via
# -r requirements/dev.in
# flake8-annotations
@@ -106,13 +108,13 @@ mccabe==0.7.0
# via
# flake8
# pylint
-mypy==1.7.1
+mypy==1.8.0
# via -r requirements/dev.in
-mypy-json-report==1.0.4
+mypy-json-report==1.1.0
# via -r requirements/dev.in
nodeenv==1.8.0
# via pre-commit
-pathspec==0.11.2
+pathspec==0.12.1
# via
# black
# djlint
@@ -139,7 +141,7 @@ pycodestyle==2.11.1
# flake8-print
pydocstyle==6.3.0
# via flake8-docstrings
-pyflakes==3.1.0
+pyflakes==3.2.0
# via flake8
pylint==3.0.3
# via -r requirements/dev.in
@@ -149,7 +151,7 @@ pyupgrade==3.15.0
# via -r requirements/dev.in
reformat-gherkin==3.0.1
# via -r requirements/dev.in
-ruff==0.1.8
+ruff==0.1.11
# via -r requirements/dev.in
smmap==5.0.1
# via gitdb
@@ -161,7 +163,7 @@ tokenize-rt==5.2.0
# via pyupgrade
toposort==1.10
# via pip-compile-multi
-types-chevron==0.14.2.5
+types-chevron==0.14.2.20240106
# via -r requirements/dev.in
types-geoip2==3.0.0
# via -r requirements/dev.in
@@ -169,19 +171,19 @@ types-ipaddress==1.0.8
# via types-maxminddb
types-maxminddb==1.5.0
# via types-geoip2
-types-mock==5.1.0.3
+types-mock==5.1.0.20240106
# via -r requirements/dev.in
-types-pyopenssl==23.3.0.0
+types-pyopenssl==23.3.0.20240106
# via types-redis
types-pytz==2023.3.1.1
# via -r requirements/dev.in
-types-redis==4.6.0.11
+types-redis==4.6.0.20240106
# via -r requirements/dev.in
-types-requests==2.31.0.10
+types-requests==2.31.0.20240106
# via -r requirements/dev.in
virtualenv==20.25.0
# via pre-commit
-wcwidth==0.2.12
+wcwidth==0.2.13
# via reformat-gherkin
wheel==0.42.0
# via pip-tools
diff --git a/requirements/test.in b/requirements/test.in
index 2e53a1c45..8685f2415 100644
--- a/requirements/test.in
+++ b/requirements/test.in
@@ -1,10 +1,8 @@
-r base.in
beautifulsoup4
-colorama
coverage
coveralls
lxml
-Pygments
pytest
pytest-asyncio
pytest-bdd
diff --git a/requirements/test.txt b/requirements/test.txt
index 5cd5ba219..a0f831626 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,4 +1,4 @@
-# SHA1:1e77cd95c84f71006a8b56e292c4046a43da0859
+# SHA1:53380f1a9e10714a4d23fbf55796f24dc0f348d8
#
# This file is autogenerated by pip-compile-multi
# To update, run:
@@ -14,8 +14,6 @@
# baseframe
beautifulsoup4==4.12.2
# via -r requirements/test.in
-colorama==0.4.6
- # via -r requirements/test.in
coverage[toml]==6.5.0
# via
# -r requirements/test.in
@@ -35,7 +33,7 @@ parse-type==0.6.2
# via pytest-bdd
pluggy==1.3.0
# via pytest
-pytest==7.4.3
+pytest==7.4.4
# via
# -r requirements/test.in
# pytest-asyncio
@@ -47,7 +45,7 @@ pytest==7.4.3
# pytest-playwright
# pytest-rerunfailures
# pytest-socket
-pytest-asyncio==0.23.2
+pytest-asyncio==0.23.3
# via -r requirements/test.in
pytest-base-url==2.0.0
# via pytest-playwright
diff --git a/runfrontendtests.sh b/runfrontendtests.sh
index 158adaad7..753da125f 100755
--- a/runfrontendtests.sh
+++ b/runfrontendtests.sh
@@ -3,8 +3,11 @@
# Load config into environment variables
set -o allexport
source .flaskenv
+# shellcheck disable=SC1091
source .env
+# shellcheck disable=SC1091
source .testenv
+# shellcheck disable=SC1091
source .env.testing
set +o allexport
@@ -12,9 +15,9 @@ set +o allexport
set -o errexit
python -m tests.cypress.cypress_initdb_test
-flask run -p 3002 --no-reload --debugger 2>&1 1>/tmp/funnel-server.log & echo $! > /tmp/funnel-server.pid
+flask run -p 3002 --no-reload --debugger 1>/tmp/funnel-server.log 2>&1 & echo $! > /tmp/funnel-server.pid
function killserver() {
- kill $(cat /tmp/funnel-server.pid)
+ kill "$(cat /tmp/funnel-server.pid)"
python -m tests.cypress.cypress_dropdb_test
rm /tmp/funnel-server.pid
}
diff --git a/tests/conftest.py b/tests/conftest.py
index 49ac6d1a4..977326ba2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,7 @@
from __future__ import annotations
+import os.path
import re
import time
import warnings
@@ -11,31 +12,49 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from difflib import unified_diff
+from dis import disassemble
+from functools import partial
+from inspect import stack as inspect_stack
+from io import StringIO
+from pprint import saferepr
+from textwrap import indent
from types import MethodType, ModuleType, SimpleNamespace
-from typing import TYPE_CHECKING, Any, NamedTuple, get_type_hints
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ NamedTuple,
+ Protocol,
+ get_type_hints,
+ runtime_checkable,
+)
from unittest.mock import patch
-import flask_wtf.csrf
+import flask
import pytest
import sqlalchemy as sa
import sqlalchemy.exc as sa_exc
import sqlalchemy.orm as sa_orm
import typeguard
-from flask import session
+from flask import Flask, session
+from flask.testing import FlaskClient
+from flask.wrappers import Response
from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.session import Session as FsaSession
+from flask_wtf.csrf import generate_csrf
+from lxml.html import FormElement, HtmlElement, fromstring # nosec
+from rich.console import Console
+from rich.highlighter import RegexHighlighter, ReprHighlighter
+from rich.markup import escape as rich_escape
+from rich.syntax import Syntax
+from rich.text import Text
from sqlalchemy import event
-from sqlalchemy.orm import Session as DatabaseSessionClass
+from sqlalchemy.orm import Session as DatabaseSessionClass, scoped_session
+from werkzeug import run_simple
+from werkzeug.test import TestResponse
if TYPE_CHECKING:
- from flask import Flask
- from flask.testing import FlaskClient
- from rich.console import Console
- from werkzeug.test import TestResponse
-
import funnel.models as funnel_models
-
# --- Pytest config --------------------------------------------------------------------
@@ -115,12 +134,16 @@ def pytest_runtest_call(item: pytest.Function) -> None:
# get_type_hints may fail on Python <3.10 because pytest-bdd appears to have
# `dict[str, str]` as a type somewhere, and builtin type subscripting isn't
# supported yet
- warnings.warn(
- f"Type annotations could not be retrieved for {item.obj!r}",
+ warnings.warn( # noqa: B028
+ f"Type annotations could not be retrieved for {item.obj.__qualname__}",
RuntimeWarning,
- stacklevel=1,
)
return
+ except NameError as exc:
+ pytest.fail(
+ f"{item.obj.__qualname__} has an unknown annotation for {exc.name}."
+ " Is it imported within a TYPE_CHECKING test?"
+ )
for attr, type_ in annotations.items():
if attr in item.funcargs:
@@ -165,9 +188,6 @@ def funnel_devtest() -> ModuleType:
@pytest.fixture(scope='session')
def response_with_forms() -> Any: # Since the actual return type is defined within
- from flask.wrappers import Response
- from lxml.html import FormElement, HtmlElement, fromstring # nosec
-
# --- ResponseWithForms, to make form submission in the test client testing easier
# --- Adapted from the abandoned Flask-Fillin package
@@ -302,107 +322,107 @@ def metarefresh(self) -> MetaRefreshContent | None:
@pytest.fixture(scope='session')
def rich_console() -> Console:
- """Provide a rich console for color output."""
- from rich.console import Console
-
- return Console()
+ """Provide a rich console for colour output."""
+ return Console(highlight=False)
-@pytest.fixture(scope='session')
-def colorama() -> Iterator[SimpleNamespace]:
- """Provide the colorama print colorizer."""
- from colorama import Back, Fore, Style, deinit, init
-
- init()
- yield SimpleNamespace(Fore=Fore, Back=Back, Style=Style)
- deinit()
+class PrintStackProtocol(Protocol):
+ def __call__(self, skip: int = 0, limit: int | None = None) -> None:
+ ...
@pytest.fixture(scope='session')
-def colorize_code(rich_console: Console) -> Callable[[str, str | None], str]:
- """Return colorized output for a string of code, for current terminal's colors."""
-
- def no_colorize(code_string: str, lang: str | None = 'python') -> str:
- # Pygments is not available or terminal does not support colour output
- return code_string
-
- try:
- from pygments import highlight
- from pygments.formatters import (
- Terminal256Formatter,
- TerminalFormatter,
- TerminalTrueColorFormatter,
- )
- from pygments.lexers import get_lexer_by_name, guess_lexer
- except ModuleNotFoundError:
- return no_colorize
-
- if rich_console.color_system == 'truecolor':
- formatter = TerminalTrueColorFormatter()
- elif rich_console.color_system == '256':
- formatter = Terminal256Formatter()
- elif rich_console.color_system == 'standard':
- formatter = TerminalFormatter()
- else:
- # color_system is `None` or `'windows'` or something unrecognised. No colours.
- return no_colorize
-
- def colorize(code_string: str, lang: str | None = 'python') -> str:
- if lang in (None, 'auto'):
- lexer = guess_lexer(code_string)
- else:
- lexer = get_lexer_by_name(lang)
- return highlight(code_string, lexer, formatter).rstrip()
-
- return colorize
-
-
-@pytest.fixture(scope='session')
-def print_stack(pytestconfig, colorama, colorize_code) -> Callable[[int, int], None]:
+def print_stack(pytestconfig, rich_console: Console) -> PrintStackProtocol:
"""Print a stack trace up to an outbound call from within this repository."""
- import os.path
- from inspect import stack as inspect_stack
-
boundary_path = str(pytestconfig.rootpath)
if not boundary_path.endswith('/'):
boundary_path += '/'
- def func(skip: int = 0, indent: int = 2) -> None:
+ class PathHighlighter(RegexHighlighter):
+ highlights = [
+ r'^(?=(?:\.\./|\.venv/))(?PText1
" diff --git a/tests/unit/models/phone_number_test.py b/tests/unit/models/phone_number_test.py index 57edad9dd..959935203 100644 --- a/tests/unit/models/phone_number_test.py +++ b/tests/unit/models/phone_number_test.py @@ -62,7 +62,7 @@ class PhoneLink(models.PhoneNumberMixin, models.BaseMixin, models.Model): __phone_is_exclusive__ = True phoneuser_id: Mapped[int] = sa_orm.mapped_column( - sa.Integer, sa.ForeignKey('test_phone_user.id'), nullable=False + sa.ForeignKey('test_phone_user.id'), default=None, nullable=False ) phoneuser: Mapped[PhoneUser] = relationship() @@ -82,7 +82,7 @@ class PhoneLinkedDocument( __phone_for__ = 'phoneuser' phoneuser_id: Mapped[int | None] = sa_orm.mapped_column( - sa.Integer, sa.ForeignKey('test_phone_user.id'), nullable=True + sa.ForeignKey('test_phone_user.id'), default=None, nullable=True ) phoneuser: Mapped[PhoneUser | None] = relationship() diff --git a/tests/unit/models/project_crew_membership_test.py b/tests/unit/models/project_crew_membership_test.py index 40c75b7dc..13248b530 100644 --- a/tests/unit/models/project_crew_membership_test.py +++ b/tests/unit/models/project_crew_membership_test.py @@ -38,7 +38,7 @@ def test_project_crew_membership( assert 'editor' in new_project.roles_for(new_user_owner) assert new_membership.is_active assert new_membership in new_project.active_crew_memberships - assert new_membership.record_type == models.MEMBERSHIP_RECORD_TYPE.DIRECT_ADD + assert new_membership.record_type == models.MembershipRecordTypeEnum.DIRECT_ADD # only one membership can be active for a user at a time. # so adding a new membership without revoking the previous one @@ -86,7 +86,7 @@ def test_project_crew_membership( assert 'editor' in new_project.roles_for(new_user) assert 'promoter' not in new_project.roles_for(new_user) assert 'usher' not in new_project.roles_for(new_user) - assert new_membership3.record_type == models.MEMBERSHIP_RECORD_TYPE.AMEND + assert new_membership3.record_type == models.MembershipRecordTypeEnum.AMEND # replace() can replace a single role as well, rest stays as they were new_membership4 = new_membership3.replace(actor=new_user_owner, is_usher=True) @@ -101,7 +101,7 @@ def test_project_crew_membership( 'editor', 'usher', } - assert new_membership4.record_type == models.MEMBERSHIP_RECORD_TYPE.AMEND + assert new_membership4.record_type == models.MembershipRecordTypeEnum.AMEND # can't replace with an unknown role with pytest.raises(AttributeError): diff --git a/tests/unit/utils/markdown/data/vega-lite.toml b/tests/unit/utils/markdown/data/vega-lite.toml index a2aed9168..7cab069fc 100644 --- a/tests/unit/utils/markdown/data/vega-lite.toml +++ b/tests/unit/utils/markdown/data/vega-lite.toml @@ -352,11 +352,11 @@ markdown = """ """ [config] -profiles = [ "basic", "document",] +profiles = ["basic", "document"] [config.custom_profiles.vega_lite] preset = "default" -plugins = [ "vega_lite",] +plugins = ["vega_lite"] [expected_output] basic = """