From 0b15e03477480145545d8efdd790b9261e251ba6 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Thu, 11 May 2023 22:37:51 +0530 Subject: [PATCH 001/175] Separate repo modules from local folder in isort (#1709) --- pyproject.toml | 5 +++-- tests/integration/views/login_test.py | 1 + tests/unit/forms/account_test.py | 1 + tests/unit/models/auth_client_AuthClient_test.py | 1 + tests/unit/models/auth_client_AuthToken_test.py | 1 + tests/unit/models/profile_test.py | 1 + tests/unit/models/project_test.py | 1 + tests/unit/models/sponsor_membership_test.py | 1 + tests/unit/models/sync_ticket_test.py | 1 + tests/unit/models/user_Organization_test.py | 1 + tests/unit/models/user_User_test.py | 1 + tests/unit/models/user_session_test.py | 1 + tests/unit/views/session_temp_vars_test.py | 1 + tests/unit/views/sitemap_test.py | 1 + 14 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2dac36058..f1c4cf577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,8 @@ use_parentheses = true from_first = true # add_imports = 'from __future__ import annotations' known_future_library = ['__future__', 'six'] -known_first_party = ['baseframe', 'coaster', 'funnel'] +known_repo = ['funnel'] +known_first_party = ['baseframe', 'coaster', 'flask_lastuser'] known_sqlalchemy = ['alembic', 'sqlalchemy', 'sqlalchemy_utils', 'flask_sqlalchemy', 'psycopg2', 'sqlalchemy_json'] known_flask = [ 'flask', @@ -99,7 +100,7 @@ known_flask = [ 'flask_rq2', ] default_section = 'THIRDPARTY' -sections = ['FUTURE', 'STDLIB', 'SQLALCHEMY', 'FLASK', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] +sections = ['FUTURE', 'STDLIB', 'SQLALCHEMY', 'FLASK', 'THIRDPARTY', 'FIRSTPARTY', 'REPO', 'LOCALFOLDER'] [tool.pytest.ini_options] minversion = "6.1" # For config.rootpath diff --git a/tests/integration/views/login_test.py b/tests/integration/views/login_test.py index 3f2db4b85..b02503233 100644 --- a/tests/integration/views/login_test.py +++ b/tests/integration/views/login_test.py @@ -11,6 +11,7 @@ from coaster.auth import current_auth from coaster.utils import utcnow + from funnel.registry import LoginCallbackError, LoginInitError, LoginProviderData from funnel.transports import TransportConnectionError, TransportRecipientError from funnel.views.otp import OtpSession diff --git a/tests/unit/forms/account_test.py b/tests/unit/forms/account_test.py index 9bd02842a..03b1c6005 100644 --- a/tests/unit/forms/account_test.py +++ b/tests/unit/forms/account_test.py @@ -6,6 +6,7 @@ import pytest from baseframe.forms.validators import StopValidation + from funnel import forms diff --git a/tests/unit/models/auth_client_AuthClient_test.py b/tests/unit/models/auth_client_AuthClient_test.py index 218fe1760..6533f9362 100644 --- a/tests/unit/models/auth_client_AuthClient_test.py +++ b/tests/unit/models/auth_client_AuthClient_test.py @@ -3,6 +3,7 @@ import pytest from coaster.utils import utcnow + from funnel import models from .db_test import TestDatabaseFixture diff --git a/tests/unit/models/auth_client_AuthToken_test.py b/tests/unit/models/auth_client_AuthToken_test.py index 745d271f1..6e39f37dd 100644 --- a/tests/unit/models/auth_client_AuthToken_test.py +++ b/tests/unit/models/auth_client_AuthToken_test.py @@ -6,6 +6,7 @@ import pytest from coaster.utils import buid, utcnow + from funnel import models from .db_test import TestDatabaseFixture diff --git a/tests/unit/models/profile_test.py b/tests/unit/models/profile_test.py index f9eb57939..75b7859e2 100644 --- a/tests/unit/models/profile_test.py +++ b/tests/unit/models/profile_test.py @@ -6,6 +6,7 @@ import pytest from coaster.sqlalchemy import StateTransitionError + from funnel import models diff --git a/tests/unit/models/project_test.py b/tests/unit/models/project_test.py index d0bcd9dbb..ad01a300c 100644 --- a/tests/unit/models/project_test.py +++ b/tests/unit/models/project_test.py @@ -5,6 +5,7 @@ import pytest from coaster.utils import utcnow + from funnel import models diff --git a/tests/unit/models/sponsor_membership_test.py b/tests/unit/models/sponsor_membership_test.py index 217c4c370..9c44f4950 100644 --- a/tests/unit/models/sponsor_membership_test.py +++ b/tests/unit/models/sponsor_membership_test.py @@ -2,6 +2,7 @@ import pytest from coaster.sqlalchemy import ImmutableColumnError + from funnel import models diff --git a/tests/unit/models/sync_ticket_test.py b/tests/unit/models/sync_ticket_test.py index 4eca240b2..8ede737c6 100644 --- a/tests/unit/models/sync_ticket_test.py +++ b/tests/unit/models/sync_ticket_test.py @@ -4,6 +4,7 @@ import pytest from coaster.utils import uuid_b58 + from funnel import models # --- Fixture data diff --git a/tests/unit/models/user_Organization_test.py b/tests/unit/models/user_Organization_test.py index 965841df2..f35aba699 100644 --- a/tests/unit/models/user_Organization_test.py +++ b/tests/unit/models/user_Organization_test.py @@ -5,6 +5,7 @@ import pytest from coaster.sqlalchemy import StateTransitionError + from funnel import models diff --git a/tests/unit/models/user_User_test.py b/tests/unit/models/user_User_test.py index 0889bbecc..70015a9d3 100644 --- a/tests/unit/models/user_User_test.py +++ b/tests/unit/models/user_User_test.py @@ -5,6 +5,7 @@ import pytest from coaster.utils import utcnow + from funnel import models pytestmark = pytest.mark.filterwarnings( diff --git a/tests/unit/models/user_session_test.py b/tests/unit/models/user_session_test.py index 32198dcba..0b113b347 100644 --- a/tests/unit/models/user_session_test.py +++ b/tests/unit/models/user_session_test.py @@ -5,6 +5,7 @@ import pytest from coaster.utils import buid, utcnow + from funnel import models sample_user_agent = ( diff --git a/tests/unit/views/session_temp_vars_test.py b/tests/unit/views/session_temp_vars_test.py index 1f14896b6..2cbf9be26 100644 --- a/tests/unit/views/session_temp_vars_test.py +++ b/tests/unit/views/session_temp_vars_test.py @@ -6,6 +6,7 @@ import pytest from coaster.utils import utcnow + from funnel.views.helpers import SessionTimeouts, session_timeouts test_timeout_seconds = 1 diff --git a/tests/unit/views/sitemap_test.py b/tests/unit/views/sitemap_test.py index 4eb3107da..e6a027e65 100644 --- a/tests/unit/views/sitemap_test.py +++ b/tests/unit/views/sitemap_test.py @@ -9,6 +9,7 @@ import pytest from coaster.utils import utcnow + from funnel.views import sitemap From 673b015462d456ce6ff5f7b0f2f9e3d64a7dfcca Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Mon, 15 May 2023 01:59:23 +0530 Subject: [PATCH 002/175] Change cookie serializer (#1710) Funnel's cookie serializer used JWT, which was removed from itsdangerous, forcing us to pin the old version. We could not switch to another JWT implementation because our key rotation wrapper only works with itsdangerous. Rather than extend key rotation to another library, with fragile tests, we've opted to switch to a supported serializer in itsdangerous. This switch will invalidate all existing cookies, forcing a re-login for all users. A corresponding change in hasgeek/flask-lastuser#66 is required for all apps hosted in subdomains of Funnel. In addition, RQ Dashboard now requires config on register and so has a new init function. Flask 2.3 no longer supports the `before_first_request` callback, so the blueprint must expect config from the app when first registered on it. --- funnel/__init__.py | 2 + funnel/serializers.py | 4 +- funnel/views/login_session.py | 13 ++-- funnel/views/notification_preferences.py | 4 +- funnel/views/siteadmin.py | 15 +++-- package-lock.json | 81 ++++++++++++------------ requirements/base.in | 3 +- requirements/base.txt | 27 ++++++-- requirements/dev.txt | 22 +++---- requirements/test.txt | 2 +- 10 files changed, 95 insertions(+), 78 deletions(-) diff --git a/funnel/__init__.py b/funnel/__init__.py index 39312e872..20f3728f1 100644 --- a/funnel/__init__.py +++ b/funnel/__init__.py @@ -202,6 +202,8 @@ ), ) +views.siteadmin.init_rq_dashboard() + # --- Serve static files with Whitenoise ----------------------------------------------- app.wsgi_app = WhiteNoise( # type: ignore[assignment] diff --git a/funnel/serializers.py b/funnel/serializers.py index d1236ebb3..1bbb5ec85 100644 --- a/funnel/serializers.py +++ b/funnel/serializers.py @@ -2,7 +2,7 @@ from __future__ import annotations -from itsdangerous import JSONWebSignatureSerializer, URLSafeTimedSerializer +from itsdangerous import URLSafeTimedSerializer from coaster.app import KeyRotationWrapper @@ -12,7 +12,7 @@ # Lastuser cookie serializer def lastuser_serializer() -> KeyRotationWrapper: return KeyRotationWrapper( - JSONWebSignatureSerializer, app.config['LASTUSER_SECRET_KEYS'] + URLSafeTimedSerializer, app.config['LASTUSER_SECRET_KEYS'] ) diff --git a/funnel/views/login_session.py b/funnel/views/login_session.py index 7b4314b95..63d36abc9 100644 --- a/funnel/views/login_session.py +++ b/funnel/views/login_session.py @@ -111,7 +111,6 @@ def _load_user(): add_auth_attribute('session', None) lastuser_cookie = {} - _lastuser_cookie_headers = {} # Ignored for now, intended for future changes # Migrate data from Flask cookie session if 'sessionid' in session: @@ -121,11 +120,9 @@ def _load_user(): if 'lastuser' in request.cookies: try: - ( - lastuser_cookie, - _lastuser_cookie_headers, - ) = lastuser_serializer().loads( - request.cookies['lastuser'], return_header=True + lastuser_cookie = lastuser_serializer().loads( + request.cookies['lastuser'], + max_age=365 * 86400, # Validity 1 year (365 days) ) except itsdangerous.BadSignature: lastuser_cookie = {} @@ -313,9 +310,7 @@ def set_lastuser_cookie(response: ResponseType) -> ResponseType: expires = utcnow() + current_app.config['PERMANENT_SESSION_LIFETIME'] response.set_cookie( 'lastuser', - value=lastuser_serializer().dumps( - current_auth.cookie, header_fields={'v': 1} - ), + value=lastuser_serializer().dumps(current_auth.cookie), # Keep this cookie for a year. max_age=31557600, # Expire one year from now. diff --git a/funnel/views/notification_preferences.py b/funnel/views/notification_preferences.py index 9a6fb35ac..a95d740b4 100644 --- a/funnel/views/notification_preferences.py +++ b/funnel/views/notification_preferences.py @@ -186,7 +186,7 @@ def unsubscribe_auto(self, token: str): # if they'd like to resubscribe try: payload = token_serializer().loads( - token, max_age=365 * 24 * 60 * 60 # Validity 1 year (365 days) + token, max_age=365 * 86400 # Validity 1 year (365 days) ) except itsdangerous.SignatureExpired: # Link has expired. It's been over a year! @@ -323,7 +323,7 @@ def unsubscribe( # in the POST request because we'll move it over during the GET request. payload = token_serializer().loads( session.get('unsub_token') or request.form['token'], - max_age=365 * 24 * 60 * 60, # Validity 1 year (365 days) + max_age=365 * 86400, # Validity 1 year (365 days) ) except itsdangerous.SignatureExpired: # Link has expired. It's been over a year! diff --git a/funnel/views/siteadmin.py b/funnel/views/siteadmin.py index 446f9890e..42f21a81d 100644 --- a/funnel/views/siteadmin.py +++ b/funnel/views/siteadmin.py @@ -442,8 +442,13 @@ def review_comment(self, report: str) -> ReturnRenderWith: SiteadminView.init_app(app) -if rq_dashboard is not None: - rq_dashboard.blueprint.before_request( - lambda: None if current_auth and current_auth.user.is_sysadmin else abort(403) - ) - app.register_blueprint(rq_dashboard.blueprint, url_prefix='/siteadmin/rq') + +def init_rq_dashboard(): + """Register RQ Dashboard Blueprint if available for import.""" + if rq_dashboard is not None: + rq_dashboard.blueprint.before_request( + lambda: None + if current_auth and current_auth.user.is_sysadmin + else abort(403) + ) + app.register_blueprint(rq_dashboard.blueprint, url_prefix='/siteadmin/rq') diff --git a/package-lock.json b/package-lock.json index a0eddd747..fd9992460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1678,9 +1678,9 @@ "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==" }, "node_modules/@codemirror/autocomplete": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.6.1.tgz", - "integrity": "sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.7.1.tgz", + "integrity": "sha512-hSxf9S0uB+GV+gBsjY1FZNo53e1FFdzPceRfCfD1gWOnV6o21GfB5J5Wg9G/4h76XZMPrF0A6OCK/Rz5+V1egg==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -1734,9 +1734,9 @@ } }, "node_modules/@codemirror/lang-javascript": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.7.tgz", - "integrity": "sha512-KXKqxlZ4W6t5I7i2ScmITUD3f/F5Cllk3kj0De9P9mFeYVfhOVOWuDLgYiLpk357u7Xh4dhqjJAnsNPPoTLghQ==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.8.tgz", + "integrity": "sha512-5cIA6IOkslTu1DtldcYnj7hsBm3p+cD37qSaKvW1kV16M6q9ysKvKrveCOWgbrj4+ilSWRL2JtSLudbeB158xg==", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -1789,9 +1789,9 @@ "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" }, "node_modules/@codemirror/view": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.11.1.tgz", - "integrity": "sha512-ffKhfty5XcadA2/QSmDCnG6ZQnDfKT4YsH9ACWluhoTpkHuW5gMAK07s9Y76j/OzUqyoUuF+/VISr9BuCWzPqw==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.11.2.tgz", + "integrity": "sha512-AzxJ9Aub6ubBvoPBGvjcd4zITqcBBiLpJ89z0ZjnphOHncbvUvQcb9/WMVGpuwTT95+jW4knkH6gFIy0oLdaUQ==", "dependencies": { "@codemirror/state": "^6.1.4", "style-mod": "^4.0.0", @@ -2448,9 +2448,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.1.tgz", - "integrity": "sha512-uKBEevTNb+l6/aCQaKVnUModfEMjAl98lw2Si9P5y4hLu9tm6AlX2ZIoXZX6Wh9lJueYPrGPKk5WMCNHg/u6/A==" + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz", + "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==" }, "node_modules/@types/resolve": { "version": "1.17.1", @@ -3407,9 +3407,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001486", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz", - "integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==", + "version": "1.0.30001487", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz", + "integrity": "sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==", "funding": [ { "type": "opencollective", @@ -3930,9 +3930,9 @@ } }, "node_modules/cypress/node_modules/@types/node": { - "version": "14.18.46", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.46.tgz", - "integrity": "sha512-n4yVT5FuY5NCcGHCosQSGvvCT74HhowymPN2OEcsHPw6U1NuxV9dvxWbrM2dnBukWjdMYzig1WfIkWdTTQJqng==", + "version": "14.18.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.47.tgz", + "integrity": "sha512-OuJi8bIng4wYHHA3YpKauL58dZrPxro3d0tabPHyiNF8rKfGKuVfr83oFlPLmKri1cX+Z3cJP39GXmnqkP11Gw==", "dev": true }, "node_modules/cypress/node_modules/ansi-styles": { @@ -4018,9 +4018,9 @@ } }, "node_modules/cypress/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4750,9 +4750,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.388", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.388.tgz", - "integrity": "sha512-xZ0y4zjWZgp65okzwwt00f2rYibkFPHUv9qBz+Vzn8cB9UXIo9Zc6Dw81LJYhhNt0G/vR1OJEfStZ49NKl0YxQ==" + "version": "1.4.394", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.394.tgz", + "integrity": "sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -5509,9 +5509,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6085,13 +6085,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -7191,9 +7192,9 @@ } }, "node_modules/jquery": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", - "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz", + "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==" }, "node_modules/jquery-locationpicker": { "version": "0.1.12", @@ -11446,9 +11447,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.82.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.0.tgz", - "integrity": "sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==", + "version": "5.82.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz", + "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -11459,7 +11460,7 @@ "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.13.0", + "enhanced-resolve": "^5.14.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -11607,9 +11608,9 @@ } }, "node_modules/webpack/node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "peerDependencies": { "acorn": "^8" } diff --git a/requirements/base.in b/requirements/base.in index c9a5f940f..a11df6afd 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -7,6 +7,7 @@ base58 bcrypt better_profanity!=0.7.0 # https://github.com/snguyenthanh/better_profanity/issues/19 blinker +boto3 # Required in Imgee not here, but has a pin on urllib3 version Brotli chevron click @@ -29,7 +30,7 @@ greenlet html2text icalendar idna -itsdangerous<2.1.0 # https://github.com/pallets/itsdangerous/pull/273 +itsdangerous linkify-it-py markdown-it-py mdit-py-plugins diff --git a/requirements/base.txt b/requirements/base.txt index 1543e758b..3b59295c4 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:1d57afe0d223c0ec7bb100b34a50bceb22b6ed40 +# SHA1:b18889a0dd028d4bed02dadaa4618b22db682e05 # # This file is autogenerated by pip-compile-multi # To update, run: @@ -59,6 +59,12 @@ blinker==1.6.2 # via # -r requirements/base.in # coaster +boto3==1.26.133 + # via -r requirements/base.in +botocore==1.29.133 + # via + # boto3 + # s3transfer brotli==1.0.9 # via -r requirements/base.in cachelib==0.9.0 @@ -172,7 +178,7 @@ furl==2.1.3 # coaster future==0.18.3 # via tuspy -geoip2==4.6.0 +geoip2==4.7.0 # via -r requirements/base.in grapheme==0.6.0 # via baseframe @@ -202,7 +208,7 @@ importlib-metadata==6.6.0 # markdown isoweek==1.3.3 # via coaster -itsdangerous==2.0.1 +itsdangerous==2.1.2 # via # -r requirements/base.in # flask @@ -212,6 +218,10 @@ jinja2==3.1.2 # flask # flask-babel # flask-flatpages +jmespath==1.0.1 + # via + # boto3 + # botocore joblib==1.2.0 # via nltk linkify-it-py==2.0.2 @@ -244,7 +254,7 @@ marshmallow==3.19.0 # marshmallow-enum marshmallow-enum==1.5.1 # via dataclasses-json -maxminddb==2.2.0 +maxminddb==2.3.0 # via geoip2 mdit-py-plugins==0.3.5 # via -r requirements/base.in @@ -312,7 +322,7 @@ pyisemail==2.0.1 # -r requirements/base.in # baseframe # mxsniff -pyjwt==2.6.0 +pyjwt==2.7.0 # via twilio pymdown-extensions==9.11 # via coaster @@ -331,6 +341,7 @@ python-dateutil==2.8.2 # -r requirements/base.in # arrow # baseframe + # botocore # freezegun # icalendar # rq-scheduler @@ -403,6 +414,8 @@ rq-scheduler==0.13.1 # via flask-rq2 rsa==4.9 # via oauth2client +s3transfer==0.6.1 + # via boto3 semantic-version==2.10.0 # via # baseframe @@ -424,7 +437,7 @@ six==1.16.0 # requests-mock # sqlalchemy-json # tuspy -sqlalchemy==2.0.12 +sqlalchemy==2.0.13 # via # -r requirements/base.in # alembic @@ -479,7 +492,7 @@ unidecode==1.3.6 urllib3[socks]==1.26.15 # via # -r requirements/base.in - # geoip2 + # botocore # requests # sentry-sdk user-agents==2.2.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 1ea4c4bfd..f8fca5784 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,7 +12,7 @@ # via # -r requirements/base.in # baseframe -astroid==2.15.4 +astroid==2.15.5 # via pylint bandit==1.7.5 # via -r requirements/dev.in @@ -44,7 +44,7 @@ flake8-assertive==2.1.0 # via -r requirements/dev.in flake8-blind-except==0.2.1 # via -r requirements/dev.in -flake8-bugbear==23.3.23 +flake8-bugbear==23.5.9 # via -r requirements/dev.in flake8-builtins==2.1.0 # via -r requirements/dev.in @@ -87,9 +87,9 @@ mccabe==0.7.0 # via # flake8 # pylint -mypy==1.2.0 +mypy==1.3.0 # via -r requirements/dev.in -nodeenv==1.7.0 +nodeenv==1.8.0 # via pre-commit pathspec==0.11.1 # via black @@ -101,7 +101,7 @@ pip-compile-multi==2.6.3 # via -r requirements/dev.in pip-tools==6.13.0 # via pip-compile-multi -platformdirs==3.5.0 +platformdirs==3.5.1 # via # black # pylint @@ -126,7 +126,7 @@ pyupgrade==3.4.0 # via -r requirements/dev.in reformat-gherkin==3.0.1 # via -r requirements/dev.in -ruff==0.0.265 +ruff==0.0.267 # via -r requirements/dev.in smmap==5.0.0 # via gitdb @@ -160,23 +160,23 @@ types-maxminddb==1.5.0 # via # -r requirements/dev.in # types-geoip2 -types-pyopenssl==23.1.0.2 +types-pyopenssl==23.1.0.3 # via types-redis -types-python-dateutil==2.8.19.12 +types-python-dateutil==2.8.19.13 # via -r requirements/dev.in types-pytz==2023.3.0.0 # via -r requirements/dev.in -types-redis==4.5.5.0 +types-redis==4.5.5.2 # via -r requirements/dev.in types-requests==2.30.0.0 # via -r requirements/dev.in -types-setuptools==67.7.0.1 +types-setuptools==67.7.0.2 # via -r requirements/dev.in types-six==1.16.21.8 # via -r requirements/dev.in types-toml==0.10.8.6 # via -r requirements/dev.in -types-urllib3==1.26.25.12 +types-urllib3==1.26.25.13 # via types-requests types-werkzeug==1.0.9 # via diff --git a/requirements/test.txt b/requirements/test.txt index 8da454c2d..ba23ef584 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -108,7 +108,7 @@ trio==0.22.0 # trio-websocket trio-websocket==0.10.2 # via selenium -typeguard==3.0.2 +typeguard==4.0.0 # via -r requirements/test.in wsproto==1.2.0 # via trio-websocket From eced3adcbdd0382bd507ce91803e171d083102cc Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Mon, 15 May 2023 08:11:31 +0530 Subject: [PATCH 003/175] Reorder imports and extend pylint to tests in pre-commit (#1711) --- .pre-commit-config.yaml | 2 +- funnel/__init__.py | 1 - funnel/cli/geodata.py | 3 +-- funnel/cli/periodic.py | 3 +-- funnel/cli/refresh/markdown.py | 1 - funnel/devtest.py | 4 +--- funnel/extapi/boxoffice.py | 1 - funnel/forms/account.py | 1 - funnel/forms/helpers.py | 1 - funnel/forms/venue.py | 1 - funnel/loginproviders/github.py | 1 - funnel/loginproviders/google.py | 1 - funnel/loginproviders/linkedin.py | 1 - funnel/loginproviders/twitter.py | 1 - funnel/loginproviders/zoom.py | 1 - funnel/models/auth_client.py | 1 - funnel/models/comment.py | 1 - funnel/models/contact_exchange.py | 3 +-- funnel/models/email_address.py | 8 +++---- funnel/models/helpers.py | 10 ++++----- funnel/models/membership_mixin.py | 1 - funnel/models/notification.py | 4 +--- funnel/models/phone_number.py | 4 +--- funnel/models/profile.py | 3 +-- funnel/models/project.py | 4 +--- funnel/models/rsvp.py | 3 +-- funnel/models/session.py | 3 +-- funnel/models/shortlink.py | 3 +-- funnel/models/user.py | 6 ++---- funnel/models/utils.py | 1 - funnel/transports/email/send.py | 1 - funnel/transports/sms/send.py | 3 +-- funnel/typing.py | 4 +--- funnel/utils/markdown/base.py | 3 +-- funnel/utils/misc.py | 1 - funnel/utils/mustache.py | 3 +-- funnel/views/account.py | 1 - funnel/views/api/email_events.py | 1 - funnel/views/api/sms_events.py | 1 - funnel/views/contact.py | 3 +-- funnel/views/helpers.py | 7 +++---- funnel/views/jobs.py | 1 - funnel/views/login_session.py | 3 +-- funnel/views/notification.py | 3 +-- funnel/views/otp.py | 1 - funnel/views/schedule.py | 4 +--- funnel/views/search.py | 6 ++---- funnel/views/siteadmin.py | 3 +-- funnel/views/sitemap.py | 3 +-- funnel/views/ticket_event.py | 3 +-- funnel/views/ticket_participant.py | 3 +-- funnel/views/video.py | 1 - migrations/env.py | 3 +-- ...8b_complement_email_md5sum_with_blake2b.py | 5 ++--- .../1c9cbf3a1e5e_add_commenset_memberships.py | 5 ++--- ...c10efdbce_deprecate_session_speaker_bio.py | 5 ++--- ...7_uuid_columns_for_proposal_and_session.py | 5 ++--- ...321b11b6a413_add_participant_uuid_field.py | 5 ++--- ..._drop_support_for_128_bit_blake2b_hash_.py | 5 ++--- .../63c44675b6cd_migrate_to_phonenumber.py | 3 +-- .../versions/69c2ced88981_team_org_uuid.py | 5 ++--- .../versions/7f8114c73092_add_rsvp_uuid.py | 5 ++--- .../887db555cca9_adding_uuid_to_commentset.py | 5 ++--- .../ad5013552ec6_simplify_proposal_fields.py | 5 ++--- .../ae075a249493_migrate_email_addresses.py | 5 ++--- .../aebd5a9e5af1_project_timestamps.py | 5 ++--- ...d_columns_for_project_profile_user_team.py | 5 ++--- .../c3069d33419a_comment_uuid_field.py | 5 ++--- ...populate_usersession_geonameid_from_ip_.py | 5 ++--- ...2b28adfa135_add_proposal_suuid_redirect.py | 5 ++--- ...9_move_participant_to_emailaddressmixin.py | 5 ++--- .../versions/eec2fad0f3e9_venue_uuid_field.py | 5 ++--- ...a05ebecbc0f_populate_proposalmembership.py | 5 ++--- pyproject.toml | 21 +------------------ tests/conftest.py | 11 ++++------ tests/e2e/account/register_test.py | 2 ++ tests/e2e/conftest.py | 1 + .../integration/views/account_delete_test.py | 2 ++ .../views/comment_moderation_test.py | 1 - tests/integration/views/login_test.py | 2 +- tests/unit/forms/account_test.py | 1 + tests/unit/forms/label_forms_test.py | 1 - tests/unit/forms/login_test.py | 1 + tests/unit/forms/project_forms_test.py | 1 - .../models/auth_client_ScopeMixin_test.py | 1 - tests/unit/models/db_test.py | 2 +- tests/unit/models/email_address_test.py | 6 +++--- tests/unit/models/helpers_test.py | 8 +++---- tests/unit/models/merge_membership_test.py | 1 + tests/unit/models/merge_notification_test.py | 4 ++-- tests/unit/models/merge_team_test.py | 4 ++-- tests/unit/models/notification_test.py | 3 +-- tests/unit/models/phone_number_test.py | 4 ++-- tests/unit/models/profile_name_test.py | 1 - tests/unit/models/profile_test.py | 3 +-- .../models/project_crew_membership_test.py | 1 - tests/unit/models/project_test.py | 1 + tests/unit/models/session_test.py | 7 +++---- tests/unit/models/site_membership_test.py | 1 - tests/unit/models/sponsor_membership_test.py | 2 ++ tests/unit/models/sync_ticket_test.py | 2 +- tests/unit/models/user_Organization_test.py | 2 +- tests/unit/proxies/request_wants_test.py | 4 ++-- .../transports/aws_ses/ses_notices_test.py | 1 - tests/unit/transports/sms_send_test.py | 1 - tests/unit/transports/sms_template_test.py | 3 +-- tests/unit/utils/markdown/conftest.py | 3 +-- tests/unit/utils/markdown/markdown_test.py | 1 - tests/unit/utils/misc_test.py | 1 - tests/unit/utils/mustache_test.py | 4 ++-- tests/unit/views/api_shortlink_test.py | 2 +- tests/unit/views/helpers_test.py | 4 ++-- tests/unit/views/login_session_test.py | 1 - tests/unit/views/notification_test.py | 2 +- tests/unit/views/project_spa_test.py | 1 + tests/unit/views/project_sponsorship_test.py | 1 + tests/unit/views/search_test.py | 2 +- tests/unit/views/session_temp_vars_test.py | 1 + tests/unit/views/shortlink_test.py | 1 + tests/unit/views/siteadmin_test.py | 1 + tests/unit/views/sitemap_test.py | 3 +-- 121 files changed, 132 insertions(+), 240 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7c2f0dd03..f62354752 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -118,7 +118,7 @@ repos: '--disable=import-error', '-rn', # Disable full report '-sn', # Disable evaluation score - '--ignore-paths=tests,migrations', + '--ignore-paths=migrations', ] additional_dependencies: - tomli diff --git a/funnel/__init__.py b/funnel/__init__.py index 20f3728f1..7e9dbb38c 100644 --- a/funnel/__init__.py +++ b/funnel/__init__.py @@ -16,7 +16,6 @@ from flask_migrate import Migrate from flask_redis import FlaskRedis from flask_rq2 import RQ - from whitenoise import WhiteNoise import geoip2.database diff --git a/funnel/cli/geodata.py b/funnel/cli/geodata.py index 68d199b84..87502f6de 100644 --- a/funnel/cli/geodata.py +++ b/funnel/cli/geodata.py @@ -14,9 +14,8 @@ import zipfile from flask.cli import AppGroup -import click - from unidecode import unidecode +import click import requests import rich.progress diff --git a/funnel/cli/periodic.py b/funnel/cli/periodic.py index 43e05b47b..535675e47 100644 --- a/funnel/cli/periodic.py +++ b/funnel/cli/periodic.py @@ -6,10 +6,9 @@ from datetime import timedelta from typing import Any, Dict +from dateutil.relativedelta import relativedelta from flask.cli import AppGroup import click - -from dateutil.relativedelta import relativedelta import pytz import requests diff --git a/funnel/cli/refresh/markdown.py b/funnel/cli/refresh/markdown.py index f2f3ab427..5349fda16 100644 --- a/funnel/cli/refresh/markdown.py +++ b/funnel/cli/refresh/markdown.py @@ -5,7 +5,6 @@ from typing import ClassVar, Dict, List, Optional, Set import click - import rich.progress from ... import models diff --git a/funnel/devtest.py b/funnel/devtest.py index f2f6a246e..18304b862 100644 --- a/funnel/devtest.py +++ b/funnel/devtest.py @@ -15,10 +15,8 @@ import time import weakref -from sqlalchemy.engine import Engine - from flask import Flask - +from sqlalchemy.engine import Engine from typing_extensions import Protocol from . import app as main_app diff --git a/funnel/extapi/boxoffice.py b/funnel/extapi/boxoffice.py index 1f3300e55..8be6d37d0 100644 --- a/funnel/extapi/boxoffice.py +++ b/funnel/extapi/boxoffice.py @@ -5,7 +5,6 @@ from urllib.parse import urljoin from flask import current_app - import requests from ..utils import extract_twitter_handle diff --git a/funnel/forms/account.py b/funnel/forms/account.py index c0ddc7b73..71956c57c 100644 --- a/funnel/forms/account.py +++ b/funnel/forms/account.py @@ -6,7 +6,6 @@ from typing import Dict, Iterable, Optional from flask_babel import ngettext - import requests from baseframe import _, __, forms diff --git a/funnel/forms/helpers.py b/funnel/forms/helpers.py index 8e4c8c035..ed422b6aa 100644 --- a/funnel/forms/helpers.py +++ b/funnel/forms/helpers.py @@ -5,7 +5,6 @@ from typing import Optional from flask import flash - from typing_extensions import Literal from baseframe import _, __, forms diff --git a/funnel/forms/venue.py b/funnel/forms/venue.py index 37f7d94bf..97e2cffbb 100644 --- a/funnel/forms/venue.py +++ b/funnel/forms/venue.py @@ -6,7 +6,6 @@ import re from flask_babel import get_locale - import pycountry from baseframe import _, __, forms diff --git a/funnel/loginproviders/github.py b/funnel/loginproviders/github.py index 59ffc5770..992688e85 100644 --- a/funnel/loginproviders/github.py +++ b/funnel/loginproviders/github.py @@ -3,7 +3,6 @@ from __future__ import annotations from flask import current_app, redirect, request - from furl import furl from sentry_sdk import capture_exception import requests diff --git a/funnel/loginproviders/google.py b/funnel/loginproviders/google.py index c2af4c9b8..254a49f69 100644 --- a/funnel/loginproviders/google.py +++ b/funnel/loginproviders/google.py @@ -3,7 +3,6 @@ from __future__ import annotations from flask import current_app, redirect, request, session - from oauth2client import client from sentry_sdk import capture_exception import requests diff --git a/funnel/loginproviders/linkedin.py b/funnel/loginproviders/linkedin.py index a5a601ed5..71a000fc4 100644 --- a/funnel/loginproviders/linkedin.py +++ b/funnel/loginproviders/linkedin.py @@ -5,7 +5,6 @@ from secrets import token_urlsafe from flask import current_app, redirect, request, session - from furl import furl from sentry_sdk import capture_exception import requests diff --git a/funnel/loginproviders/twitter.py b/funnel/loginproviders/twitter.py index 74849ae81..196b6f574 100644 --- a/funnel/loginproviders/twitter.py +++ b/funnel/loginproviders/twitter.py @@ -3,7 +3,6 @@ from __future__ import annotations from flask import redirect, request - import tweepy from baseframe import _ diff --git a/funnel/loginproviders/zoom.py b/funnel/loginproviders/zoom.py index 3aaec6424..2d4ef1e70 100644 --- a/funnel/loginproviders/zoom.py +++ b/funnel/loginproviders/zoom.py @@ -5,7 +5,6 @@ from base64 import b64encode from flask import current_app, redirect, request, session - from furl import furl from sentry_sdk import capture_exception import requests diff --git a/funnel/models/auth_client.py b/funnel/models/auth_client.py index c4f81bb07..197a39900 100644 --- a/funnel/models/auth_client.py +++ b/funnel/models/auth_client.py @@ -19,7 +19,6 @@ from sqlalchemy.orm import attribute_keyed_dict, load_only from sqlalchemy.orm.query import Query as QueryBaseClass - from werkzeug.utils import cached_property from baseframe import _ diff --git a/funnel/models/comment.py b/funnel/models/comment.py index deeb6e3ac..b11b57186 100644 --- a/funnel/models/comment.py +++ b/funnel/models/comment.py @@ -6,7 +6,6 @@ from typing import Iterable, List, Optional, Set, Union from sqlalchemy.orm import CompositeProperty - from werkzeug.utils import cached_property from baseframe import _, __ diff --git a/funnel/models/contact_exchange.py b/funnel/models/contact_exchange.py index 2d5ce0b57..8b7ba2063 100644 --- a/funnel/models/contact_exchange.py +++ b/funnel/models/contact_exchange.py @@ -9,9 +9,8 @@ from typing import Collection, Iterable, Optional from uuid import UUID -from sqlalchemy.ext.associationproxy import association_proxy - from pytz import timezone +from sqlalchemy.ext.associationproxy import association_proxy from coaster.sqlalchemy import LazyRoleSet from coaster.utils import uuid_to_base58 diff --git a/funnel/models/email_address.py b/funnel/models/email_address.py index 181cd18f3..841f95776 100644 --- a/funnel/models/email_address.py +++ b/funnel/models/email_address.py @@ -6,16 +6,14 @@ import hashlib import unicodedata +from pyisemail import is_email +from pyisemail.diagnosis import BaseDiagnosis from sqlalchemy import event, inspect from sqlalchemy.orm import mapper from sqlalchemy.orm.attributes import NO_VALUE from sqlalchemy.sql.expression import ColumnElement - -from werkzeug.utils import cached_property - -from pyisemail import is_email -from pyisemail.diagnosis import BaseDiagnosis from typing_extensions import Literal +from werkzeug.utils import cached_property import base58 import idna diff --git a/funnel/models/helpers.py b/funnel/models/helpers.py index f18401473..744804b7e 100644 --- a/funnel/models/helpers.py +++ b/funnel/models/helpers.py @@ -20,18 +20,16 @@ import os.path import re +from better_profanity import profanity +from furl import furl +from markupsafe import Markup +from markupsafe import escape as html_escape from sqlalchemy.dialects.postgresql import TSQUERY from sqlalchemy.dialects.postgresql.base import ( RESERVED_WORDS as POSTGRESQL_RESERVED_WORDS, ) from sqlalchemy.ext.mutable import MutableComposite from sqlalchemy.orm import composite - -from markupsafe import Markup -from markupsafe import escape as html_escape - -from better_profanity import profanity -from furl import furl from zxcvbn import zxcvbn from .. import app diff --git a/funnel/models/membership_mixin.py b/funnel/models/membership_mixin.py index 707958fb3..ffc376042 100644 --- a/funnel/models/membership_mixin.py +++ b/funnel/models/membership_mixin.py @@ -17,7 +17,6 @@ from sqlalchemy import event from sqlalchemy.sql.expression import ColumnElement - from werkzeug.utils import cached_property from baseframe import __ diff --git a/funnel/models/notification.py b/funnel/models/notification.py index 1fd27ec4d..40d69e7a2 100644 --- a/funnel/models/notification.py +++ b/funnel/models/notification.py @@ -103,10 +103,8 @@ from sqlalchemy import event from sqlalchemy.orm import column_keyed_dict from sqlalchemy.orm.exc import NoResultFound - -from werkzeug.utils import cached_property - from typing_extensions import Protocol +from werkzeug.utils import cached_property from baseframe import __ from coaster.sqlalchemy import ( diff --git a/funnel/models/phone_number.py b/funnel/models/phone_number.py index 0e6b5e9bf..3a973376e 100644 --- a/funnel/models/phone_number.py +++ b/funnel/models/phone_number.py @@ -9,10 +9,8 @@ from sqlalchemy.orm import mapper from sqlalchemy.orm.attributes import NO_VALUE from sqlalchemy.sql.expression import ColumnElement - -from werkzeug.utils import cached_property - from typing_extensions import Literal +from werkzeug.utils import cached_property import base58 import phonenumbers diff --git a/funnel/models/profile.py b/funnel/models/profile.py index 2ddc63c6b..115485f33 100644 --- a/funnel/models/profile.py +++ b/funnel/models/profile.py @@ -5,11 +5,10 @@ from typing import Any, Iterable, List, Optional, Union from uuid import UUID # noqa: F401 # pylint: disable=unused-import +from furl import furl from sqlalchemy.sql import expression from sqlalchemy.sql.expression import ColumnElement -from furl import furl - from baseframe import __ from coaster.sqlalchemy import LazyRoleSet, Query, StateManager, immutable, with_roles from coaster.utils import LabeledEnum diff --git a/funnel/models/project.py b/funnel/models/project.py index 22d177eac..da97ec034 100644 --- a/funnel/models/project.py +++ b/funnel/models/project.py @@ -4,12 +4,10 @@ from typing import Iterable, List, Optional +from pytz import utc from sqlalchemy.orm import attribute_keyed_dict - from werkzeug.utils import cached_property -from pytz import utc - from baseframe import __, localize_timezone from coaster.sqlalchemy import LazyRoleSet, StateManager, with_roles from coaster.utils import LabeledEnum, buid, utcnow diff --git a/funnel/models/rsvp.py b/funnel/models/rsvp.py index bd92cb435..2e20b9caf 100644 --- a/funnel/models/rsvp.py +++ b/funnel/models/rsvp.py @@ -5,9 +5,8 @@ from typing import Dict, Optional, Tuple, Union, cast, overload from flask import current_app -from werkzeug.utils import cached_property - from typing_extensions import Literal +from werkzeug.utils import cached_property from baseframe import __ from coaster.sqlalchemy import StateManager, with_roles diff --git a/funnel/models/session.py b/funnel/models/session.py index ec40d4f3d..c8423e9fc 100644 --- a/funnel/models/session.py +++ b/funnel/models/session.py @@ -8,9 +8,8 @@ from uuid import UUID # noqa: F401 # pylint: disable=unused-import from flask_babel import format_date, get_locale -from werkzeug.utils import cached_property - from isoweek import Week +from werkzeug.utils import cached_property from baseframe import localize_timezone from coaster.sqlalchemy import with_roles diff --git a/funnel/models/shortlink.py b/funnel/models/shortlink.py index f093b50e5..3cb2ab1e8 100644 --- a/funnel/models/shortlink.py +++ b/funnel/models/shortlink.py @@ -8,10 +8,9 @@ import hashlib import re +from furl import furl from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.hybrid import Comparator - -from furl import furl from typing_extensions import Literal from coaster.sqlalchemy import immutable, with_roles diff --git a/funnel/models/user.py b/funnel/models/user.py index c502feb05..307760c6f 100644 --- a/funnel/models/user.py +++ b/funnel/models/user.py @@ -8,12 +8,10 @@ import hashlib import itertools -from sqlalchemy.ext.associationproxy import association_proxy - -from werkzeug.utils import cached_property - from passlib.hash import argon2, bcrypt +from sqlalchemy.ext.associationproxy import association_proxy from typing_extensions import Literal +from werkzeug.utils import cached_property import phonenumbers from baseframe import __ diff --git a/funnel/models/utils.py b/funnel/models/utils.py index 7cf152b27..4303f69e7 100644 --- a/funnel/models/utils.py +++ b/funnel/models/utils.py @@ -5,7 +5,6 @@ from typing import NamedTuple, Optional, Set, Union, overload from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint - from typing_extensions import Literal import phonenumbers diff --git a/funnel/transports/email/send.py b/funnel/transports/email/send.py index d38aabd6e..c9c752eec 100644 --- a/funnel/transports/email/send.py +++ b/funnel/transports/email/send.py @@ -9,7 +9,6 @@ from flask import current_app from flask_mailman import EmailMultiAlternatives from flask_mailman.message import sanitize_address - from html2text import html2text from premailer import transform diff --git a/funnel/transports/sms/send.py b/funnel/transports/sms/send.py index 935991fa9..738b02825 100644 --- a/funnel/transports/sms/send.py +++ b/funnel/transports/sms/send.py @@ -6,10 +6,9 @@ from typing import Callable, List, Optional, Tuple, Union, cast from flask import url_for -import itsdangerous - from twilio.base.exceptions import TwilioRestException from twilio.rest import Client +import itsdangerous import phonenumbers import requests diff --git a/funnel/typing.py b/funnel/typing.py index cec057efb..9a6e12a6a 100644 --- a/funnel/typing.py +++ b/funnel/typing.py @@ -6,10 +6,8 @@ from uuid import UUID from sqlalchemy.orm import Mapped - -from werkzeug.wrappers import Response # Base class for Flask Response - from typing_extensions import ParamSpec, Protocol +from werkzeug.wrappers import Response # Base class for Flask Response from coaster.sqlalchemy import Query diff --git a/funnel/utils/markdown/base.py b/funnel/utils/markdown/base.py index a2e6b4224..9ab79b049 100644 --- a/funnel/utils/markdown/base.py +++ b/funnel/utils/markdown/base.py @@ -16,9 +16,8 @@ ) import re -from markupsafe import Markup - from markdown_it import MarkdownIt +from markupsafe import Markup from mdit_py_plugins import anchors, container, deflist, footnote, tasklists from typing_extensions import Literal diff --git a/funnel/utils/misc.py b/funnel/utils/misc.py index 1f1cff684..ee0bc05d2 100644 --- a/funnel/utils/misc.py +++ b/funnel/utils/misc.py @@ -8,7 +8,6 @@ import urllib.parse from flask import abort - import phonenumbers import qrcode import qrcode.image.svg diff --git a/funnel/utils/mustache.py b/funnel/utils/mustache.py index ee61fdffc..b957a9e3f 100644 --- a/funnel/utils/mustache.py +++ b/funnel/utils/mustache.py @@ -5,9 +5,8 @@ import functools import types -from markupsafe import escape as html_escape - from chevron import render +from markupsafe import escape as html_escape from .markdown import markdown_escape diff --git a/funnel/views/account.py b/funnel/views/account.py index 223d07e9f..2bb71d0e7 100644 --- a/funnel/views/account.py +++ b/funnel/views/account.py @@ -7,7 +7,6 @@ from flask import abort, current_app, flash, redirect, render_template, request, url_for from markupsafe import Markup, escape - import geoip2.errors import user_agents diff --git a/funnel/views/api/email_events.py b/funnel/views/api/email_events.py index 1496e6d97..595be10cc 100644 --- a/funnel/views/api/email_events.py +++ b/funnel/views/api/email_events.py @@ -6,7 +6,6 @@ from typing import List from flask import current_app, request - import requests from baseframe import statsd diff --git a/funnel/views/api/sms_events.py b/funnel/views/api/sms_events.py index 7caf880a3..a47420668 100644 --- a/funnel/views/api/sms_events.py +++ b/funnel/views/api/sms_events.py @@ -3,7 +3,6 @@ from __future__ import annotations from flask import current_app, request - from twilio.request_validator import RequestValidator from baseframe import statsd diff --git a/funnel/views/contact.py b/funnel/views/contact.py index 96b397d4b..15ea383ad 100644 --- a/funnel/views/contact.py +++ b/funnel/views/contact.py @@ -7,9 +7,8 @@ from typing import Dict, Optional import csv -from sqlalchemy.exc import IntegrityError - from flask import Response, current_app, render_template, request +from sqlalchemy.exc import IntegrityError from baseframe import _ from coaster.auth import current_auth diff --git a/funnel/views/helpers.py b/funnel/views/helpers.py index 38873f8ca..2adcb0f85 100644 --- a/funnel/views/helpers.py +++ b/funnel/views/helpers.py @@ -25,14 +25,13 @@ session, url_for, ) -from werkzeug.exceptions import MethodNotAllowed, NotFound -from werkzeug.routing import BuildError, RequestRedirect -from werkzeug.urls import url_quote - from furl import furl from pytz import common_timezones from pytz import timezone as pytz_timezone from pytz import utc +from werkzeug.exceptions import MethodNotAllowed, NotFound +from werkzeug.routing import BuildError, RequestRedirect +from werkzeug.urls import url_quote import brotli from baseframe import cache, statsd diff --git a/funnel/views/jobs.py b/funnel/views/jobs.py index 9ef3aabbc..764cba6d8 100644 --- a/funnel/views/jobs.py +++ b/funnel/views/jobs.py @@ -6,7 +6,6 @@ from functools import wraps from flask import g - import requests from baseframe import statsd diff --git a/funnel/views/login_session.py b/funnel/views/login_session.py index 63d36abc9..42e6354e3 100644 --- a/funnel/views/login_session.py +++ b/funnel/views/login_session.py @@ -19,10 +19,9 @@ session, url_for, ) -import itsdangerous - from furl import furl import geoip2.errors +import itsdangerous from baseframe import _, statsd from baseframe.forms import render_form diff --git a/funnel/views/notification.py b/funnel/views/notification.py index 9e4ed18fa..82a06cd9a 100644 --- a/funnel/views/notification.py +++ b/funnel/views/notification.py @@ -13,9 +13,8 @@ from flask import url_for from flask_babel import force_locale -from werkzeug.utils import cached_property - from typing_extensions import Literal +from werkzeug.utils import cached_property from baseframe import __, statsd from coaster.auth import current_auth diff --git a/funnel/views/otp.py b/funnel/views/otp.py index 9e7264935..91bdf36d5 100644 --- a/funnel/views/otp.py +++ b/funnel/views/otp.py @@ -9,7 +9,6 @@ from flask import current_app, flash, render_template, request, session, url_for from werkzeug.exceptions import Forbidden, RequestTimeout, TooManyRequests from werkzeug.utils import cached_property - import phonenumbers from baseframe import _ diff --git a/funnel/views/schedule.py b/funnel/views/schedule.py index ce6dcb225..d49f863c1 100644 --- a/funnel/views/schedule.py +++ b/funnel/views/schedule.py @@ -7,12 +7,10 @@ from types import SimpleNamespace from typing import Any, Dict, List, Optional, cast -from sqlalchemy.orm.exc import NoResultFound - 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 baseframe import _, localize_timezone from coaster.utils import utcnow diff --git a/funnel/views/search.py b/funnel/views/search.py index 4c9a22cf3..7bffaea4c 100644 --- a/funnel/views/search.py +++ b/funnel/views/search.py @@ -7,12 +7,10 @@ from urllib.parse import quote as urlquote import re -from sqlalchemy.sql import expression -from sqlalchemy.sql.elements import ColumnElement - from flask import request, url_for from markupsafe import Markup - +from sqlalchemy.sql import expression +from sqlalchemy.sql.elements import ColumnElement from typing_extensions import TypedDict from baseframe import __ diff --git a/funnel/views/siteadmin.py b/funnel/views/siteadmin.py index 42f21a81d..6a379b47a 100644 --- a/funnel/views/siteadmin.py +++ b/funnel/views/siteadmin.py @@ -10,9 +10,8 @@ from typing import Any, Dict, Optional, cast import csv -from sqlalchemy.dialects.postgresql import INTERVAL - from flask import abort, current_app, flash, render_template, request, url_for +from sqlalchemy.dialects.postgresql import INTERVAL try: import rq_dashboard diff --git a/funnel/views/sitemap.py b/funnel/views/sitemap.py index 669de9d4e..38b6c6a52 100644 --- a/funnel/views/sitemap.py +++ b/funnel/views/sitemap.py @@ -7,10 +7,9 @@ from enum import Enum from typing import Optional, Tuple, Union -from flask import abort, render_template, url_for - from dateutil.relativedelta import relativedelta from dateutil.rrule import DAILY, MONTHLY, rrule +from flask import abort, render_template, url_for from pytz import utc from baseframe import cache diff --git a/funnel/views/ticket_event.py b/funnel/views/ticket_event.py index 39a4b78c2..0949107a3 100644 --- a/funnel/views/ticket_event.py +++ b/funnel/views/ticket_event.py @@ -4,9 +4,8 @@ from typing import Optional -from sqlalchemy.exc import IntegrityError - from flask import abort, flash, request +from sqlalchemy.exc import IntegrityError from baseframe import _, forms from baseframe.forms import render_delete_sqla, render_form diff --git a/funnel/views/ticket_participant.py b/funnel/views/ticket_participant.py index 8d9393ec4..2d502854c 100644 --- a/funnel/views/ticket_participant.py +++ b/funnel/views/ticket_participant.py @@ -4,9 +4,8 @@ from typing import Optional -from sqlalchemy.exc import IntegrityError - from flask import abort, flash, request, url_for +from sqlalchemy.exc import IntegrityError from baseframe import _, forms from baseframe.forms import render_form diff --git a/funnel/views/video.py b/funnel/views/video.py index 8dab4c6de..1bc7903be 100644 --- a/funnel/views/video.py +++ b/funnel/views/video.py @@ -6,7 +6,6 @@ from typing import Optional, Union, cast from flask import current_app - from pytz import utc from sentry_sdk import capture_exception from typing_extensions import TypedDict diff --git a/migrations/env.py b/migrations/env.py index 1f8d8e3e0..ce11425d2 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -7,9 +7,8 @@ import logging from alembic import context -from sqlalchemy import MetaData - from flask import current_app +from sqlalchemy import MetaData USE_TWOPHASE = False diff --git a/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py b/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py index 9bf994b82..4f4fe1dba 100644 --- a/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py +++ b/migrations/versions/047ebdac558b_complement_email_md5sum_with_blake2b.py @@ -10,11 +10,10 @@ import hashlib from alembic import op -from sqlalchemy.sql import column, table -import sqlalchemy as sa - from progressbar import ProgressBar +from sqlalchemy.sql import column, table import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '047ebdac558b' diff --git a/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py b/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py index 1c04b01b8..d411c6eac 100644 --- a/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py +++ b/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py @@ -10,12 +10,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '1c9cbf3a1e5e' diff --git a/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py b/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py index 4f1676da3..b9f019db9 100644 --- a/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py +++ b/migrations/versions/284c10efdbce_deprecate_session_speaker_bio.py @@ -10,11 +10,10 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.sql import column, table -import sqlalchemy as sa - from progressbar import ProgressBar +from sqlalchemy.sql import column, table import progressbar.widgets +import sqlalchemy as sa from coaster.utils import markdown 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 f9acb7221..9473c98e1 100644 --- a/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py +++ b/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py @@ -13,12 +13,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa proposal = table( 'proposal', column('id', sa.Integer()), column('uuid', postgresql.UUID()) diff --git a/migrations/versions/321b11b6a413_add_participant_uuid_field.py b/migrations/versions/321b11b6a413_add_participant_uuid_field.py index 8987e7ff3..224ad9fa1 100644 --- a/migrations/versions/321b11b6a413_add_participant_uuid_field.py +++ b/migrations/versions/321b11b6a413_add_participant_uuid_field.py @@ -13,12 +13,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa participant = table( 'participant', column('id', sa.Integer()), column('uuid', postgresql.UUID()) 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 c312470de..a5000f24f 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 @@ -10,12 +10,11 @@ import hashlib from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '5f1ab3e04f73' diff --git a/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py b/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py index 4640395e6..6fe98346c 100644 --- a/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py +++ b/migrations/versions/63c44675b6cd_migrate_to_phonenumber.py @@ -11,10 +11,9 @@ from alembic import op from sqlalchemy.sql import column, table -import sqlalchemy as sa - import phonenumbers import rich.progress +import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = '63c44675b6cd' diff --git a/migrations/versions/69c2ced88981_team_org_uuid.py b/migrations/versions/69c2ced88981_team_org_uuid.py index a04b7544c..b78563b50 100644 --- a/migrations/versions/69c2ced88981_team_org_uuid.py +++ b/migrations/versions/69c2ced88981_team_org_uuid.py @@ -11,12 +11,11 @@ down_revision = 'b34aa62af7fc' from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa from coaster.utils import buid2uuid, uuid2buid diff --git a/migrations/versions/7f8114c73092_add_rsvp_uuid.py b/migrations/versions/7f8114c73092_add_rsvp_uuid.py index 8ad6b1757..df678c668 100644 --- a/migrations/versions/7f8114c73092_add_rsvp_uuid.py +++ b/migrations/versions/7f8114c73092_add_rsvp_uuid.py @@ -10,12 +10,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '7f8114c73092' diff --git a/migrations/versions/887db555cca9_adding_uuid_to_commentset.py b/migrations/versions/887db555cca9_adding_uuid_to_commentset.py index 469cd5951..ab0fa9b21 100644 --- a/migrations/versions/887db555cca9_adding_uuid_to_commentset.py +++ b/migrations/versions/887db555cca9_adding_uuid_to_commentset.py @@ -10,12 +10,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '887db555cca9' diff --git a/migrations/versions/ad5013552ec6_simplify_proposal_fields.py b/migrations/versions/ad5013552ec6_simplify_proposal_fields.py index 54a26d6d0..10994ed64 100644 --- a/migrations/versions/ad5013552ec6_simplify_proposal_fields.py +++ b/migrations/versions/ad5013552ec6_simplify_proposal_fields.py @@ -10,11 +10,10 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.sql import column, table -import sqlalchemy as sa - from progressbar import ProgressBar +from sqlalchemy.sql import column, table import progressbar.widgets +import sqlalchemy as sa from coaster.utils import markdown diff --git a/migrations/versions/ae075a249493_migrate_email_addresses.py b/migrations/versions/ae075a249493_migrate_email_addresses.py index 20731c227..1a8104de1 100644 --- a/migrations/versions/ae075a249493_migrate_email_addresses.py +++ b/migrations/versions/ae075a249493_migrate_email_addresses.py @@ -10,13 +10,12 @@ import hashlib from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import idna import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'ae075a249493' diff --git a/migrations/versions/aebd5a9e5af1_project_timestamps.py b/migrations/versions/aebd5a9e5af1_project_timestamps.py index ee0f145f9..89d652705 100644 --- a/migrations/versions/aebd5a9e5af1_project_timestamps.py +++ b/migrations/versions/aebd5a9e5af1_project_timestamps.py @@ -9,11 +9,10 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.sql import column, table -import sqlalchemy as sa - from progressbar import ProgressBar +from sqlalchemy.sql import column, table import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'aebd5a9e5af1' 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 8c4360f4f..3a168b606 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 @@ -13,12 +13,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa from coaster.utils import buid2uuid, uuid2buid diff --git a/migrations/versions/c3069d33419a_comment_uuid_field.py b/migrations/versions/c3069d33419a_comment_uuid_field.py index 4074782e6..2861eee78 100644 --- a/migrations/versions/c3069d33419a_comment_uuid_field.py +++ b/migrations/versions/c3069d33419a_comment_uuid_field.py @@ -12,12 +12,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa comment = table( 'comment', column('id', sa.Integer()), column('uuid', postgresql.UUID()) diff --git a/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py b/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py index 848d9ff16..faa25374e 100644 --- a/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py +++ b/migrations/versions/ca578c1b82e8_populate_usersession_geonameid_from_ip_.py @@ -10,11 +10,10 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.sql import column, table -import sqlalchemy as sa - from progressbar import ProgressBar +from sqlalchemy.sql import column, table import progressbar.widgets +import sqlalchemy as sa try: import geoip2.database as geoip2_database diff --git a/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py b/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py index 612431936..789a1cd27 100644 --- a/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py +++ b/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py @@ -9,12 +9,11 @@ from typing import Optional, Tuple, Union from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'e2b28adfa135' diff --git a/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py b/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py index 638906ef7..fd68cee1c 100644 --- a/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py +++ b/migrations/versions/e3b3ccbca3b9_move_participant_to_emailaddressmixin.py @@ -11,13 +11,12 @@ import hashlib from alembic import op -from sqlalchemy.sql import column, table -import sqlalchemy as sa - from progressbar import ProgressBar from pyisemail import is_email +from sqlalchemy.sql import column, table import idna import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'e3b3ccbca3b9' diff --git a/migrations/versions/eec2fad0f3e9_venue_uuid_field.py b/migrations/versions/eec2fad0f3e9_venue_uuid_field.py index a72131711..ffad68967 100644 --- a/migrations/versions/eec2fad0f3e9_venue_uuid_field.py +++ b/migrations/versions/eec2fad0f3e9_venue_uuid_field.py @@ -12,12 +12,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa venue = table('venue', column('id', sa.Integer()), column('uuid', postgresql.UUID())) diff --git a/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py b/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py index 5ec9e0047..626f3b748 100644 --- a/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py +++ b/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py @@ -10,12 +10,11 @@ from uuid import uuid4 from alembic import op +from progressbar import ProgressBar from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table -import sqlalchemy as sa - -from progressbar import ProgressBar import progressbar.widgets +import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'fa05ebecbc0f' diff --git a/pyproject.toml b/pyproject.toml index f1c4cf577..b6e4f4a9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,26 +81,8 @@ from_first = true known_future_library = ['__future__', 'six'] known_repo = ['funnel'] known_first_party = ['baseframe', 'coaster', 'flask_lastuser'] -known_sqlalchemy = ['alembic', 'sqlalchemy', 'sqlalchemy_utils', 'flask_sqlalchemy', 'psycopg2', 'sqlalchemy_json'] -known_flask = [ - 'flask', - 'click', - 'werkzeug', - 'markupsafe', - 'itsdangerous', - 'wtforms', - 'webassets', - 'flask_assets', - 'flask_babel', - 'flask_executor', - 'flask_flatpages', - 'flask_mailman', - 'flask_migrate', - 'flask_redis', - 'flask_rq2', -] default_section = 'THIRDPARTY' -sections = ['FUTURE', 'STDLIB', 'SQLALCHEMY', 'FLASK', 'THIRDPARTY', 'FIRSTPARTY', 'REPO', 'LOCALFOLDER'] +sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'REPO', 'LOCALFOLDER'] [tool.pytest.ini_options] minversion = "6.1" # For config.rootpath @@ -170,7 +152,6 @@ module = ['flask_sqlalchemy'] follow_imports = 'skip' [tool.pylint.master] -# load-plugins = ['pylint_pytest'] # Plugin has been abandoned max-parents = 10 init-hook = """ import os, astroid.bases, pathlib diff --git a/tests/conftest.py b/tests/conftest.py index 3fb5e2734..fdc561ed9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ """Test configuration and fixtures.""" -# pylint: disable=import-outside-toplevel, redefined-outer-name +# pylint: disable=import-outside-toplevel,redefined-outer-name from __future__ import annotations @@ -13,15 +13,13 @@ import typing as t import warnings +from flask import session from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy.session import Session as FsaSession from sqlalchemy.orm import Session as DatabaseSessionClass -import sqlalchemy as sa - -from flask import session - import flask_wtf.csrf import pytest +import sqlalchemy as sa import typeguard if t.TYPE_CHECKING: @@ -152,7 +150,6 @@ def funnel_devtest(funnel): @pytest.fixture(scope='session') def response_with_forms() -> t.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 @@ -856,7 +853,7 @@ def db_session_rollback( connection = engine.connect() transaction = connection.begin() bindcts[bind] = BindConnectionTransaction(engine, connection, transaction) - database.session = database._make_scoped_session( + database.session = database._make_scoped_session( # pylint: disable=W0212 { 'class_': BoundSession, 'bindcts': bindcts, diff --git a/tests/e2e/account/register_test.py b/tests/e2e/account/register_test.py index 0a47e28b7..324d6d804 100644 --- a/tests/e2e/account/register_test.py +++ b/tests/e2e/account/register_test.py @@ -1,3 +1,5 @@ +"""Test account registration.""" + import time from pytest_bdd import given, parsers, scenarios, then, when diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 9c43d34fd..6392f3089 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -1,4 +1,5 @@ """Feature test configuration.""" +# pylint: disable=redefined-outer-name import pytest diff --git a/tests/integration/views/account_delete_test.py b/tests/integration/views/account_delete_test.py index 8021bc3d1..3656d3cd8 100644 --- a/tests/integration/views/account_delete_test.py +++ b/tests/integration/views/account_delete_test.py @@ -1,3 +1,5 @@ +"""Tests for account deletion.""" + from pytest_bdd import given, parsers, scenarios, then, when from funnel import models diff --git a/tests/integration/views/comment_moderation_test.py b/tests/integration/views/comment_moderation_test.py index 6f04aef37..5874d2e03 100644 --- a/tests/integration/views/comment_moderation_test.py +++ b/tests/integration/views/comment_moderation_test.py @@ -3,7 +3,6 @@ from flask import url_for from werkzeug.datastructures import MultiDict - import pytest from funnel import models diff --git a/tests/integration/views/login_test.py b/tests/integration/views/login_test.py index b02503233..5c7866ce5 100644 --- a/tests/integration/views/login_test.py +++ b/tests/integration/views/login_test.py @@ -1,4 +1,5 @@ """Tests for the login, logout and register views.""" +# pylint: disable=redefined-outer-name from datetime import timedelta from types import SimpleNamespace @@ -6,7 +7,6 @@ from flask import redirect, request, session from werkzeug.datastructures import MultiDict - import pytest from coaster.auth import current_auth diff --git a/tests/unit/forms/account_test.py b/tests/unit/forms/account_test.py index 03b1c6005..5f4e4bf67 100644 --- a/tests/unit/forms/account_test.py +++ b/tests/unit/forms/account_test.py @@ -1,4 +1,5 @@ """Test account forms.""" +# pylint: disable=redefined-outer-name from contextlib import nullcontext as does_not_raise from types import SimpleNamespace diff --git a/tests/unit/forms/label_forms_test.py b/tests/unit/forms/label_forms_test.py index 2d8b2571d..c8a66eef3 100644 --- a/tests/unit/forms/label_forms_test.py +++ b/tests/unit/forms/label_forms_test.py @@ -1,7 +1,6 @@ """Test Label forms.""" from werkzeug.datastructures import MultiDict - import pytest from funnel import forms diff --git a/tests/unit/forms/login_test.py b/tests/unit/forms/login_test.py index 346239a52..6ec714348 100644 --- a/tests/unit/forms/login_test.py +++ b/tests/unit/forms/login_test.py @@ -1,4 +1,5 @@ """Test main login form for password and OTP flows.""" +# pylint: disable=redefined-outer-name import pytest diff --git a/tests/unit/forms/project_forms_test.py b/tests/unit/forms/project_forms_test.py index 04b26aee9..8b49a16d6 100644 --- a/tests/unit/forms/project_forms_test.py +++ b/tests/unit/forms/project_forms_test.py @@ -1,7 +1,6 @@ """Tests for Project forms.""" from werkzeug.datastructures import MultiDict - import pytest import requests_mock diff --git a/tests/unit/models/auth_client_ScopeMixin_test.py b/tests/unit/models/auth_client_ScopeMixin_test.py index 0e0ce7b58..0e2be1e85 100644 --- a/tests/unit/models/auth_client_ScopeMixin_test.py +++ b/tests/unit/models/auth_client_ScopeMixin_test.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from sqlalchemy.exc import IntegrityError - import pytest from funnel import models diff --git a/tests/unit/models/db_test.py b/tests/unit/models/db_test.py index c82a50692..334ddf197 100644 --- a/tests/unit/models/db_test.py +++ b/tests/unit/models/db_test.py @@ -1,5 +1,5 @@ """Fixtures for legacy tests.""" -# pylint: disable=attribute-defined-outside-init +# pylint: disable=attribute-defined-outside-init,redefined-outer-name import pytest diff --git a/tests/unit/models/email_address_test.py b/tests/unit/models/email_address_test.py index 0bfb52020..055e450ce 100644 --- a/tests/unit/models/email_address_test.py +++ b/tests/unit/models/email_address_test.py @@ -1,13 +1,13 @@ """Tests for EmailAddress model.""" -# pylint: disable=possibly-unused-variable +# pylint: disable=possibly-unused-variable,redefined-outer-name + from types import SimpleNamespace from typing import Generator from sqlalchemy.exc import IntegrityError -import sqlalchemy as sa - import pytest +import sqlalchemy as sa from funnel import models diff --git a/tests/unit/models/helpers_test.py b/tests/unit/models/helpers_test.py index b2e7bdc2b..3896634d0 100644 --- a/tests/unit/models/helpers_test.py +++ b/tests/unit/models/helpers_test.py @@ -1,14 +1,12 @@ """Tests for model helpers.""" -# pylint: disable=possibly-unused-variable +# pylint: disable=possibly-unused-variable,redefined-outer-name from types import SimpleNamespace -from sqlalchemy.exc import StatementError -import sqlalchemy as sa - from flask_babel import lazy_gettext - +from sqlalchemy.exc import StatementError import pytest +import sqlalchemy as sa from funnel import models import funnel.models.helpers as mhelpers diff --git a/tests/unit/models/merge_membership_test.py b/tests/unit/models/merge_membership_test.py index 47d150fe4..c4057666c 100644 --- a/tests/unit/models/merge_membership_test.py +++ b/tests/unit/models/merge_membership_test.py @@ -1,4 +1,5 @@ """Tests for membership model mergers when merging user accounts.""" +# pylint: disable=redefined-outer-name import pytest diff --git a/tests/unit/models/merge_notification_test.py b/tests/unit/models/merge_notification_test.py index 9f69d49ec..973608aea 100644 --- a/tests/unit/models/merge_notification_test.py +++ b/tests/unit/models/merge_notification_test.py @@ -1,12 +1,12 @@ """Tests for merging notifications with user account merger.""" +# pylint: disable=redefined-outer-name from datetime import timedelta from types import SimpleNamespace from typing import Any -import sqlalchemy as sa - import pytest +import sqlalchemy as sa from funnel import models diff --git a/tests/unit/models/merge_team_test.py b/tests/unit/models/merge_team_test.py index 6aafa40e2..f5d06736a 100644 --- a/tests/unit/models/merge_team_test.py +++ b/tests/unit/models/merge_team_test.py @@ -1,11 +1,11 @@ """Tests for Team member merger when merging user accounts.""" +# pylint: disable=redefined-outer-name from datetime import timedelta from types import SimpleNamespace -import sqlalchemy as sa - import pytest +import sqlalchemy as sa from funnel import models diff --git a/tests/unit/models/notification_test.py b/tests/unit/models/notification_test.py index 2309eee49..b84e79394 100644 --- a/tests/unit/models/notification_test.py +++ b/tests/unit/models/notification_test.py @@ -1,5 +1,5 @@ """Tests for Notification and UserNotification models.""" -# pylint: disable=possibly-unused-variable +# pylint: disable=possibly-unused-variable,redefined-outer-name from __future__ import annotations @@ -7,7 +7,6 @@ from typing import Dict, List, Set from sqlalchemy.exc import IntegrityError - import pytest from funnel import models diff --git a/tests/unit/models/phone_number_test.py b/tests/unit/models/phone_number_test.py index 2aad365ed..2da2db0d1 100644 --- a/tests/unit/models/phone_number_test.py +++ b/tests/unit/models/phone_number_test.py @@ -1,14 +1,14 @@ """Tests for PhoneNumber model.""" +# pylint: disable=redefined-outer-name from contextlib import nullcontext as does_not_raise from types import SimpleNamespace from typing import Generator from sqlalchemy.exc import IntegrityError -import sqlalchemy as sa - import phonenumbers import pytest +import sqlalchemy as sa from funnel import models diff --git a/tests/unit/models/profile_name_test.py b/tests/unit/models/profile_name_test.py index 5b6f89d15..ad47ab378 100644 --- a/tests/unit/models/profile_name_test.py +++ b/tests/unit/models/profile_name_test.py @@ -1,7 +1,6 @@ """Tests for Account (nee Profile) name.""" from sqlalchemy.exc import IntegrityError - import pytest from funnel import models diff --git a/tests/unit/models/profile_test.py b/tests/unit/models/profile_test.py index 75b7859e2..92955ef5d 100644 --- a/tests/unit/models/profile_test.py +++ b/tests/unit/models/profile_test.py @@ -1,8 +1,7 @@ """Tests for Account (nee Profile) model.""" -from sqlalchemy.exc import StatementError - from furl import furl +from sqlalchemy.exc import StatementError import pytest from coaster.sqlalchemy import StateTransitionError diff --git a/tests/unit/models/project_crew_membership_test.py b/tests/unit/models/project_crew_membership_test.py index 6ceb3984a..cd889eb12 100644 --- a/tests/unit/models/project_crew_membership_test.py +++ b/tests/unit/models/project_crew_membership_test.py @@ -1,7 +1,6 @@ """Tests for ProjectCrewMembership membership model.""" from sqlalchemy.exc import IntegrityError - import pytest from funnel import models diff --git a/tests/unit/models/project_test.py b/tests/unit/models/project_test.py index ad01a300c..0447d7e2e 100644 --- a/tests/unit/models/project_test.py +++ b/tests/unit/models/project_test.py @@ -1,4 +1,5 @@ """Tests for Project model.""" +# pylint: disable=redefined-outer-name from datetime import datetime, timedelta diff --git a/tests/unit/models/session_test.py b/tests/unit/models/session_test.py index d42478a5e..f686415a4 100644 --- a/tests/unit/models/session_test.py +++ b/tests/unit/models/session_test.py @@ -1,15 +1,14 @@ """Test sessions.""" -# pylint: disable=possibly-unused-variable +# pylint: disable=possibly-unused-variable,redefined-outer-name from datetime import datetime, timedelta from types import SimpleNamespace from typing import Dict, List, Optional -from sqlalchemy.exc import IntegrityError -import sqlalchemy as sa - from pytz import utc +from sqlalchemy.exc import IntegrityError import pytest +import sqlalchemy as sa from funnel import models diff --git a/tests/unit/models/site_membership_test.py b/tests/unit/models/site_membership_test.py index a6081553d..c1a7ad5cf 100644 --- a/tests/unit/models/site_membership_test.py +++ b/tests/unit/models/site_membership_test.py @@ -1,7 +1,6 @@ """Tests for SiteMembership model.""" from sqlalchemy.exc import IntegrityError - import pytest from funnel import models diff --git a/tests/unit/models/sponsor_membership_test.py b/tests/unit/models/sponsor_membership_test.py index 9c44f4950..7bb478dd2 100644 --- a/tests/unit/models/sponsor_membership_test.py +++ b/tests/unit/models/sponsor_membership_test.py @@ -1,4 +1,6 @@ """Test ProjectSponsorMembership.""" +# pylint: disable=redefined-outer-name + import pytest from coaster.sqlalchemy import ImmutableColumnError diff --git a/tests/unit/models/sync_ticket_test.py b/tests/unit/models/sync_ticket_test.py index 8ede737c6..08aa38971 100644 --- a/tests/unit/models/sync_ticket_test.py +++ b/tests/unit/models/sync_ticket_test.py @@ -1,5 +1,5 @@ """Test for project ticket sync models.""" -# pylint: disable=attribute-defined-outside-init +# pylint: disable=attribute-defined-outside-init,redefined-outer-name import pytest diff --git a/tests/unit/models/user_Organization_test.py b/tests/unit/models/user_Organization_test.py index f35aba699..e5b289e34 100644 --- a/tests/unit/models/user_Organization_test.py +++ b/tests/unit/models/user_Organization_test.py @@ -45,7 +45,7 @@ def test_organization_all(db_session, org_ankhmorpork, org_citywatch, org_uu) -> """Test for getting all organizations (takes buid or name optionally).""" db_session.commit() # scenario 1: when neither buids nor names are given - assert models.Organization.all() == [] + assert not models.Organization.all() # scenario 2: when buids are passed orglist = {org_ankhmorpork, org_citywatch} all_by_buids = models.Organization.all(buids=[_org.buid for _org in orglist]) diff --git a/tests/unit/proxies/request_wants_test.py b/tests/unit/proxies/request_wants_test.py index 2e132672e..b9a9a8fe9 100644 --- a/tests/unit/proxies/request_wants_test.py +++ b/tests/unit/proxies/request_wants_test.py @@ -1,8 +1,8 @@ """Tests for request_wants proxy.""" -# pylint: disable=import-error +# pylint: disable=import-error,redefined-outer-name -from flask import Flask +from flask import Flask import pytest from funnel.proxies import init_app, request_wants diff --git a/tests/unit/transports/aws_ses/ses_notices_test.py b/tests/unit/transports/aws_ses/ses_notices_test.py index 045dfc87c..e13df0c34 100644 --- a/tests/unit/transports/aws_ses/ses_notices_test.py +++ b/tests/unit/transports/aws_ses/ses_notices_test.py @@ -5,7 +5,6 @@ import os from flask import Response - import pytest # Data Directory which contains JSON Files diff --git a/tests/unit/transports/sms_send_test.py b/tests/unit/transports/sms_send_test.py index 7b214d1da..69a1661ea 100644 --- a/tests/unit/transports/sms_send_test.py +++ b/tests/unit/transports/sms_send_test.py @@ -3,7 +3,6 @@ from unittest.mock import patch from flask import Response - import pytest import requests diff --git a/tests/unit/transports/sms_template_test.py b/tests/unit/transports/sms_template_test.py index 3f01b0de8..bfa767c96 100644 --- a/tests/unit/transports/sms_template_test.py +++ b/tests/unit/transports/sms_template_test.py @@ -1,10 +1,9 @@ """Test SMS templates.""" -# pylint: disable=possibly-unused-variable +# pylint: disable=possibly-unused-variable,redefined-outer-name from types import SimpleNamespace from flask import Flask - import pytest from funnel.transports import sms diff --git a/tests/unit/utils/markdown/conftest.py b/tests/unit/utils/markdown/conftest.py index 2b83ae1e8..262c2748c 100644 --- a/tests/unit/utils/markdown/conftest.py +++ b/tests/unit/utils/markdown/conftest.py @@ -5,9 +5,8 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple, Union -from markupsafe import Markup - from bs4 import BeautifulSoup +from markupsafe import Markup import tomlkit from funnel.utils.markdown import MarkdownConfig diff --git a/tests/unit/utils/markdown/markdown_test.py b/tests/unit/utils/markdown/markdown_test.py index 67ca0a136..e5fca16f1 100644 --- a/tests/unit/utils/markdown/markdown_test.py +++ b/tests/unit/utils/markdown/markdown_test.py @@ -3,7 +3,6 @@ import warnings from markupsafe import Markup - import pytest from funnel.utils.markdown import MarkdownConfig diff --git a/tests/unit/utils/misc_test.py b/tests/unit/utils/misc_test.py index 8112fb128..41973e4bd 100644 --- a/tests/unit/utils/misc_test.py +++ b/tests/unit/utils/misc_test.py @@ -1,7 +1,6 @@ """Tests for base utilities.""" from werkzeug.exceptions import BadRequest - import pytest from funnel import utils diff --git a/tests/unit/utils/mustache_test.py b/tests/unit/utils/mustache_test.py index 89aec20a1..2dbe0d28a 100644 --- a/tests/unit/utils/mustache_test.py +++ b/tests/unit/utils/mustache_test.py @@ -87,7 +87,7 @@ ids=templates_and_output.keys(), ) def test_mustache_md(template, expected_output): - output = mustache_md(template, test_data) + output = mustache_md(template, test_data) # pylint: disable=not-callable assert expected_output == output @@ -176,5 +176,5 @@ def test_mustache_md(template, expected_output): ) def test_mustache_md_markdown(template, config, expected_output): assert expected_output == MarkdownConfig.registry[config].render( - mustache_md(template, test_data) + mustache_md(template, test_data) # pylint: disable=not-callable ) diff --git a/tests/unit/views/api_shortlink_test.py b/tests/unit/views/api_shortlink_test.py index fb4a9d39c..d330906e9 100644 --- a/tests/unit/views/api_shortlink_test.py +++ b/tests/unit/views/api_shortlink_test.py @@ -1,7 +1,7 @@ """Test shortlink API views.""" +# pylint: disable=redefined-outer-name from flask import url_for - from furl import furl import pytest diff --git a/tests/unit/views/helpers_test.py b/tests/unit/views/helpers_test.py index ef06081d8..68b0204d4 100644 --- a/tests/unit/views/helpers_test.py +++ b/tests/unit/views/helpers_test.py @@ -1,4 +1,5 @@ """Tests for view helpers.""" +# pylint: disable=redefined-outer-name from base64 import urlsafe_b64decode from datetime import datetime, timezone @@ -7,9 +8,8 @@ from urllib.parse import urlsplit from flask import Flask, request -from werkzeug.routing import BuildError - from furl import furl +from werkzeug.routing import BuildError import pytest import funnel.views.helpers as vhelpers diff --git a/tests/unit/views/login_session_test.py b/tests/unit/views/login_session_test.py index 900e0b13b..edd17a9ed 100644 --- a/tests/unit/views/login_session_test.py +++ b/tests/unit/views/login_session_test.py @@ -1,7 +1,6 @@ """Test login session helpers.""" from flask import session - import pytest from funnel.views.login_session import save_session_next_url diff --git a/tests/unit/views/notification_test.py b/tests/unit/views/notification_test.py index 66a8f8439..31757d937 100644 --- a/tests/unit/views/notification_test.py +++ b/tests/unit/views/notification_test.py @@ -1,9 +1,9 @@ """Test Notification views.""" +# pylint: disable=redefined-outer-name from urllib.parse import urlsplit from flask import url_for - import pytest from funnel import models diff --git a/tests/unit/views/project_spa_test.py b/tests/unit/views/project_spa_test.py index 5a2f273ab..610806c62 100644 --- a/tests/unit/views/project_spa_test.py +++ b/tests/unit/views/project_spa_test.py @@ -1,4 +1,5 @@ """Test response types for project SPA endpoints.""" +# pylint: disable=redefined-outer-name from typing import Optional from urllib.parse import urlsplit diff --git a/tests/unit/views/project_sponsorship_test.py b/tests/unit/views/project_sponsorship_test.py index ad87fffb1..f315899be 100644 --- a/tests/unit/views/project_sponsorship_test.py +++ b/tests/unit/views/project_sponsorship_test.py @@ -1,4 +1,5 @@ """Test ProjectSponsorship views.""" +# pylint: disable=redefined-outer-name import pytest diff --git a/tests/unit/views/search_test.py b/tests/unit/views/search_test.py index 66b2df2cb..39dee0322 100644 --- a/tests/unit/views/search_test.py +++ b/tests/unit/views/search_test.py @@ -5,11 +5,11 @@ views are returning expected results (at this time). Proper search testing requires a corpus of searchable data in fixtures. """ +# pylint: disable=redefined-outer-name from typing import cast from flask import url_for - import pytest from funnel.views.search import ( diff --git a/tests/unit/views/session_temp_vars_test.py b/tests/unit/views/session_temp_vars_test.py index 2cbf9be26..640f2896a 100644 --- a/tests/unit/views/session_temp_vars_test.py +++ b/tests/unit/views/session_temp_vars_test.py @@ -1,4 +1,5 @@ """Test handling of temporary variables in cookie session.""" +# pylint: disable=redefined-outer-name from datetime import timedelta import time diff --git a/tests/unit/views/shortlink_test.py b/tests/unit/views/shortlink_test.py index d91bb1f90..182ff14ac 100644 --- a/tests/unit/views/shortlink_test.py +++ b/tests/unit/views/shortlink_test.py @@ -1,4 +1,5 @@ """Test shortlink views.""" +# pylint: disable=redefined-outer-name from urllib.parse import urlsplit diff --git a/tests/unit/views/siteadmin_test.py b/tests/unit/views/siteadmin_test.py index 6d2765663..09e90ca5b 100644 --- a/tests/unit/views/siteadmin_test.py +++ b/tests/unit/views/siteadmin_test.py @@ -1,4 +1,5 @@ """Test siteadmin endpoints.""" +# pylint: disable=redefined-outer-name from __future__ import annotations diff --git a/tests/unit/views/sitemap_test.py b/tests/unit/views/sitemap_test.py index e6a027e65..5024f7b5e 100644 --- a/tests/unit/views/sitemap_test.py +++ b/tests/unit/views/sitemap_test.py @@ -2,10 +2,9 @@ from datetime import datetime, timedelta -from werkzeug.exceptions import NotFound - from dateutil.relativedelta import relativedelta from pytz import utc +from werkzeug.exceptions import NotFound import pytest from coaster.utils import utcnow From 9d2f5ba1b8b114381e8923c7fd19c8b389a7a43e Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Mon, 15 May 2023 18:41:48 +0530 Subject: [PATCH 004/175] Drop UgliPyJS as it's unmaintained (#1712) --- .pre-commit-config.yaml | 2 +- funnel/__init__.py | 4 ++-- package-lock.json | 6 +++--- requirements/base.txt | 11 ++--------- requirements/dev.txt | 2 +- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f62354752..5c5f94d72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: ] files: ^requirements/.*\.txt$ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.265 + rev: v0.0.267 hooks: - id: ruff args: ['--fix', '--exit-non-zero-on-fix'] diff --git a/funnel/__init__.py b/funnel/__init__.py index 7e9dbb38c..4192d2271 100644 --- a/funnel/__init__.py +++ b/funnel/__init__.py @@ -181,7 +181,7 @@ 'jquery.ui.sortable.touch.js', ), output='js/fullcalendar.packed.js', - filters='uglipyjs', + filters='rjsmin', ), ) app.assets.register( @@ -197,7 +197,7 @@ Bundle( assets.require('schedules.js'), output='js/schedules.packed.js', - filters='uglipyjs', + filters='rjsmin', ), ) diff --git a/package-lock.json b/package-lock.json index fd9992460..cfd6a58d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1990,9 +1990,9 @@ "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" }, "node_modules/@lezer/css": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.1.tgz", - "integrity": "sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.2.tgz", + "integrity": "sha512-5TKMAReXukfEmIiZprDlGfZVfOOCyEStFi1YLzxclm9H3G/HHI49/2wzlRT6bQw5r7PoZVEtjTItEkb/UuZQyg==", "dependencies": { "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" diff --git a/requirements/base.txt b/requirements/base.txt index 3b59295c4..18d8d290e 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -311,8 +311,6 @@ pycountry==22.3.5 # baseframe pycparser==2.21 # via cffi -pyexecjs==1.5.1 - # via coaster pygments==2.15.1 # via # -r requirements/base.in @@ -420,7 +418,7 @@ semantic-version==2.10.0 # via # baseframe # coaster -sentry-sdk==1.22.2 +sentry-sdk==1.23.0 # via baseframe six==1.16.0 # via @@ -431,7 +429,6 @@ six==1.16.0 # mxsniff # oauth2client # orderedmultidict - # pyexecjs # python-dateutil # requests-file # requests-mock @@ -485,8 +482,6 @@ ua-parser==0.16.1 # via user-agents uc-micro-py==1.0.2 # via linkify-it-py -uglipyjs==0.2.5 - # via coaster unidecode==1.3.6 # via coaster urllib3[socks]==1.26.15 @@ -498,9 +493,7 @@ urllib3[socks]==1.26.15 user-agents==2.2.0 # via -r requirements/base.in webassets==2.0 - # via - # coaster - # flask-assets + # via flask-assets webencodings==0.5.1 # via # bleach diff --git a/requirements/dev.txt b/requirements/dev.txt index f8fca5784..d6d3a329b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -132,7 +132,7 @@ smmap==5.0.0 # via gitdb snowballstemmer==2.2.0 # via pydocstyle -stevedore==5.0.0 +stevedore==5.1.0 # via bandit tokenize-rt==5.0.0 # via pyupgrade From 4138523e73dc7a8614d5b653be95d5f73563cf34 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Wed, 17 May 2023 17:08:48 +0530 Subject: [PATCH 005/175] Upgrade dependencies (#1713) `pymdown-extensions` issued a security advisory that doesn't affect us, but upgrade anyway. --- package-lock.json | 66 +++++++++++++++++++++---------------------- requirements/base.txt | 12 ++++---- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfd6a58d4..f3008baec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1789,9 +1789,9 @@ "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" }, "node_modules/@codemirror/view": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.11.2.tgz", - "integrity": "sha512-AzxJ9Aub6ubBvoPBGvjcd4zITqcBBiLpJ89z0ZjnphOHncbvUvQcb9/WMVGpuwTT95+jW4knkH6gFIy0oLdaUQ==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.11.3.tgz", + "integrity": "sha512-JInirTUhmwDOEZZHcsx4/wfnBgJk0q3vnDZh1i2k7W+t1SqMugBCO/+J5zgfjJ5rXYFjnpBG9Dkz/ZMSn4bNzg==", "dependencies": { "@codemirror/state": "^6.1.4", "style-mod": "^4.0.0", @@ -2448,9 +2448,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.1.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.4.tgz", - "integrity": "sha512-At4pvmIOki8yuwLtd7BNHl3CiWNbtclUbNtScGx4OHfBd4/oWoJC8KRCIxXwkdndzhxOsPXihrsOoydxBjlE9Q==" + "version": "20.1.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.7.tgz", + "integrity": "sha512-WCuw/o4GSwDGMoonES8rcvwsig77dGCMbZDrZr2x4ZZiNW4P/gcoZXe/0twgtobcTkmg9TuKflxYL/DuwDyJzg==" }, "node_modules/@types/resolve": { "version": "1.17.1", @@ -3119,9 +3119,9 @@ "dev": true }, "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.1.tgz", + "integrity": "sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg==", "dev": true, "engines": { "node": ">=4" @@ -3407,9 +3407,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001487", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001487.tgz", - "integrity": "sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==", + "version": "1.0.30001488", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz", + "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==", "funding": [ { "type": "opencollective", @@ -4750,9 +4750,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.394", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.394.tgz", - "integrity": "sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==" + "version": "1.4.397", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.397.tgz", + "integrity": "sha512-jwnPxhh350Q/aMatQia31KAIQdhEsYS0fFZ0BQQlN9tfvOEwShu6ZNwI4kL/xBabjcB/nTy6lSt17kNIluJZ8Q==" }, "node_modules/elkjs": { "version": "0.8.2", @@ -6697,9 +6697,9 @@ } }, "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -7069,15 +7069,15 @@ "dev": true }, "node_modules/jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.6.tgz", + "integrity": "sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==", "dev": true, "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" + "filelist": "^1.0.4", + "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" @@ -10448,9 +10448,9 @@ } }, "node_modules/terser": { - "version": "5.17.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.3.tgz", - "integrity": "sha512-AudpAZKmZHkG9jueayypz4duuCFJMMNGRMwaPvQKWfxKedh8Z2x3OCoDqIIi1xx5+iwx1u6Au8XQcc9Lke65Yg==", + "version": "5.17.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.4.tgz", + "integrity": "sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==", "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", @@ -11137,9 +11137,9 @@ } }, "node_modules/vega-lite": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.9.0.tgz", - "integrity": "sha512-VA3XDlF6nd/t46KDMfq8eNKOJKy9gpJuM+6CIl3jbWqS97jWXRWXp8DpUyDmbV+iq8n4hqNTaoPqDP/e03kifw==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.9.2.tgz", + "integrity": "sha512-HLzsDCzZQGabicmnG8weS26xMXssvrTbyKcCKFXSnO2RBffD5Q6W2Mvyz5+VVVk9JjEW+Im04cB9y4QQwtDEOQ==", "dependencies": { "@types/clone": "~2.1.1", "clone": "~2.1.2", @@ -11150,7 +11150,7 @@ "vega-event-selector": "~3.0.1", "vega-expression": "~5.1.0", "vega-util": "~1.17.2", - "yargs": "~17.7.1" + "yargs": "~17.7.2" }, "bin": { "vl2pdf": "bin/vl2pdf", @@ -11420,9 +11420,9 @@ "integrity": "sha512-EDUOjQBFvhkJXwmWuUR9ijlF7/4JtmvjXSKaHSa/LNTMy9ltjgKgYB68aqlxgq8ORdSxowd5eo24P1syjZJnBA==" }, "node_modules/w3c-keyname": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", - "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.7.tgz", + "integrity": "sha512-XB8aa62d4rrVfoZYQaYNy3fy+z4nrfy2ooea3/0BnBzXW0tSdZ+lRgjzBZhk0La0H6h8fVyYCxx/qkQcAIuvfg==" }, "node_modules/watchpack": { "version": "2.4.0", diff --git a/requirements/base.txt b/requirements/base.txt index 18d8d290e..58dc88e23 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -21,7 +21,7 @@ aiohttp-retry==2.8.3 # via twilio aiosignal==1.3.1 # via aiohttp -alembic==1.10.4 +alembic==1.11.0 # via # -r requirements/base.in # flask-migrate @@ -59,9 +59,9 @@ blinker==1.6.2 # via # -r requirements/base.in # coaster -boto3==1.26.133 +boto3==1.26.135 # via -r requirements/base.in -botocore==1.29.133 +botocore==1.29.135 # via # boto3 # s3transfer @@ -322,7 +322,7 @@ pyisemail==2.0.1 # mxsniff pyjwt==2.7.0 # via twilio -pymdown-extensions==9.11 +pymdown-extensions==10.0.1 # via coaster pyopenssl==23.1.1 # via @@ -418,7 +418,7 @@ semantic-version==2.10.0 # via # baseframe # coaster -sentry-sdk==1.23.0 +sentry-sdk==1.23.1 # via baseframe six==1.16.0 # via @@ -453,7 +453,7 @@ statsd==4.0.1 # via baseframe tinydb==4.7.1 # via tuspy -tldextract==3.4.1 +tldextract==3.4.2 # via # coaster # mxsniff From 14f1abf211e79150586935667c7c242f9e95f6b5 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Thu, 18 May 2023 18:10:55 +0530 Subject: [PATCH 006/175] SQLAlchemy 2.0 introduced Uuid as a primary CamelCase data type (#1714) --- funnel/models/draft.py | 6 ++--- funnel/models/notification.py | 24 +++++++------------ ...02ced3_merge_comment_notification_types.py | 15 ++++++------ .../1c9cbf3a1e5e_add_commenset_memberships.py | 11 ++++----- ...2ca9e3e_add_proposal_sponsor_membership.py | 3 +-- ...7_uuid_columns_for_proposal_and_session.py | 13 ++++------ ...321b11b6a413_add_participant_uuid_field.py | 5 ++-- .../34a95ee0c3a0_site_membership_models.py | 3 +-- .../382cde270594_create_lastuser_tables.py | 15 ++++++------ .../3d3df26524b7_commentset_membership.py | 3 +-- .../41a4531be082_remove_account_name.py | 6 ++--- .../versions/69c2ced88981_team_org_uuid.py | 5 ++-- ...71fcac85957c_populate_membership_models.py | 11 ++++----- .../79719ee38228_moderator_report_models.py | 3 +-- .../versions/7f8114c73092_add_rsvp_uuid.py | 5 ++-- .../8829241430b6_add_membership_models.py | 7 +++--- .../887db555cca9_adding_uuid_to_commentset.py | 7 ++---- .../931be3605dc4_notification_models.py | 15 ++++++------ .../versions/94ce3a9b7a3a_draft_model.py | 5 ++-- .../versions/a1ab7bd78649_post_model.py | 3 +-- .../a9cb0e1c52ed_uuid_fields_venue_room.py | 7 ++---- ...d_columns_for_project_profile_user_team.py | 19 +++++++-------- .../bd465803af3a_add_sponsormembership.py | 3 +-- .../c3069d33419a_comment_uuid_field.py | 7 ++---- ...c3d8e3f33_drop_old_user_and_team_tables.py | 6 ++--- ...2b28adfa135_add_proposal_suuid_redirect.py | 3 +-- .../versions/eec2fad0f3e9_venue_uuid_field.py | 5 ++-- ...a05ebecbc0f_populate_proposalmembership.py | 3 +-- 28 files changed, 87 insertions(+), 131 deletions(-) diff --git a/funnel/models/draft.py b/funnel/models/draft.py index 04d91875d..873030d53 100644 --- a/funnel/models/draft.py +++ b/funnel/models/draft.py @@ -7,7 +7,7 @@ from werkzeug.datastructures import MultiDict -from . import Mapped, NoIdMixin, db, json_type, postgresql, sa +from . import Mapped, NoIdMixin, db, json_type, sa __all__ = ['Draft'] @@ -19,9 +19,9 @@ class Draft(NoIdMixin, db.Model): # type: ignore[name-defined] __allow_unmapped__ = True table = sa.Column(sa.UnicodeText, primary_key=True) - table_row_id: Mapped[UUID] = sa.Column(postgresql.UUID, primary_key=True) + table_row_id: Mapped[UUID] = sa.Column(sa.Uuid, primary_key=True) body = sa.Column(json_type, nullable=False, server_default='{}') - revision: Mapped[Optional[UUID]] = sa.Column(postgresql.UUID) + revision: Mapped[Optional[UUID]] = sa.Column(sa.Uuid) @property def formdata(self): diff --git a/funnel/models/notification.py b/funnel/models/notification.py index 40d69e7a2..8f02cfdcd 100644 --- a/funnel/models/notification.py +++ b/funnel/models/notification.py @@ -118,7 +118,7 @@ from coaster.utils import LabeledEnum, uuid_from_base58, uuid_to_base58 from ..typing import OptionalMigratedTables, T, UuidModelType -from . import BaseMixin, Mapped, NoIdMixin, db, hybrid_property, postgresql, sa +from . import BaseMixin, Mapped, NoIdMixin, db, hybrid_property, sa from .helpers import reopen from .phone_number import PhoneNumber, PhoneNumberMixin from .user import User, UserEmail, UserPhone @@ -290,16 +290,12 @@ class Notification(NoIdMixin, db.Model): # type: ignore[name-defined] #: be shared across notifications, and will be used to enforce a limit of one #: 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 - ) + sa.orm.mapped_column(sa.Uuid, primary_key=True, nullable=False, default=uuid4) ) #: Notification id id: Mapped[UUID] = immutable( # noqa: A003 - sa.orm.mapped_column( - postgresql.UUID, primary_key=True, nullable=False, default=uuid4 - ) + sa.orm.mapped_column(sa.Uuid, primary_key=True, nullable=False, default=uuid4) ) #: Default category of notification. Subclasses MUST override @@ -355,14 +351,14 @@ class Notification(NoIdMixin, db.Model): # type: ignore[name-defined] #: UUID of document that the notification refers to document_uuid: Mapped[UUID] = immutable( - sa.orm.mapped_column(postgresql.UUID, nullable=False, index=True) + sa.orm.mapped_column(sa.Uuid, nullable=False, index=True) ) #: Optional fragment within document that the notification refers to. This may be #: the document itself, or something within it, such as a comment. Notifications for #: multiple fragments are collapsed into a single notification fragment_uuid: Mapped[Optional[UUID]] = immutable( - sa.orm.mapped_column(postgresql.UUID, nullable=True) + sa.orm.mapped_column(sa.Uuid, nullable=True) ) __table_args__ = ( @@ -759,16 +755,12 @@ class UserNotification( #: Random eventid, shared with the Notification instance eventid: Mapped[UUID] = with_roles( - immutable( - sa.orm.mapped_column(postgresql.UUID, primary_key=True, nullable=False) - ), + immutable(sa.orm.mapped_column(sa.Uuid, primary_key=True, nullable=False)), read={'owner'}, ) #: Id of notification that this user received (fkey in __table_args__ below) - notification_id: Mapped[UUID] = sa.orm.mapped_column( - postgresql.UUID, nullable=False - ) + notification_id: Mapped[UUID] = sa.orm.mapped_column(sa.Uuid, nullable=False) #: Notification that this user received notification = with_roles( @@ -806,7 +798,7 @@ class UserNotification( #: When a roll-up is performed, record an identifier for the items rolled up rollupid: Mapped[Optional[UUID]] = with_roles( - sa.orm.mapped_column(postgresql.UUID, nullable=True, index=True), + sa.orm.mapped_column(sa.Uuid, nullable=True, index=True), read={'owner'}, ) diff --git a/migrations/versions/1bd91b02ced3_merge_comment_notification_types.py b/migrations/versions/1bd91b02ced3_merge_comment_notification_types.py index d2dfda46c..c037e017b 100644 --- a/migrations/versions/1bd91b02ced3_merge_comment_notification_types.py +++ b/migrations/versions/1bd91b02ced3_merge_comment_notification_types.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import sqlalchemy as sa @@ -22,31 +21,31 @@ notification = table( 'notification', - column('eventid', postgresql.UUID()), - column('id', postgresql.UUID()), + column('eventid', sa.Uuid()), + column('id', sa.Uuid()), column('type', sa.Unicode()), - column('document_uuid', postgresql.UUID()), - column('fragment_uuid', postgresql.UUID()), + column('document_uuid', sa.Uuid()), + column('fragment_uuid', sa.Uuid()), ) project = table( 'project', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('commentset_id', sa.Integer()), ) proposal = table( 'proposal', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('commentset_id', sa.Integer()), ) commentset = table( 'commentset', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), ) diff --git a/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py b/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py index d411c6eac..7be1a5f84 100644 --- a/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py +++ b/migrations/versions/1c9cbf3a1e5e_add_commenset_memberships.py @@ -11,7 +11,6 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa @@ -42,7 +41,7 @@ commentset = table( 'commentset', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('type', sa.Integer()), column('count', sa.Integer()), ) @@ -51,7 +50,7 @@ proposal = table( 'proposal', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('user_id', sa.Integer()), column('commentset_id', sa.Integer()), ) @@ -59,7 +58,7 @@ commentset_membership = table( 'commentset_membership', - column('id', postgresql.UUID()), + column('id', sa.Uuid()), column('user_id', sa.Integer()), column('commentset_id', sa.Integer()), column('record_type', sa.Integer()), @@ -75,7 +74,7 @@ project_crew_membership = table( 'project_crew_membership', - column('id', postgresql.UUID()), + column('id', sa.Uuid()), column('user_id', sa.Integer()), column('project_id', sa.Integer()), column('granted_at', sa.TIMESTAMP(timezone=True)), @@ -85,7 +84,7 @@ proposal_membership = table( 'proposal_membership', - column('id', postgresql.UUID()), + column('id', sa.Uuid()), column('user_id', sa.Integer()), column('proposal_id', sa.Integer()), column('granted_at', sa.TIMESTAMP(timezone=True)), diff --git a/migrations/versions/277ba2ca9e3e_add_proposal_sponsor_membership.py b/migrations/versions/277ba2ca9e3e_add_proposal_sponsor_membership.py index f5f5f597a..bb624add6 100644 --- a/migrations/versions/277ba2ca9e3e_add_proposal_sponsor_membership.py +++ b/migrations/versions/277ba2ca9e3e_add_proposal_sponsor_membership.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -42,7 +41,7 @@ def upgrade_(): sa.Column('profile_id', sa.Integer(), nullable=False), sa.Column('revoked_by_id', sa.Integer(), nullable=True), sa.Column('granted_by_id', sa.Integer(), nullable=False), - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.ForeignKeyConstraint(['granted_by_id'], ['user.id'], ondelete='SET NULL'), 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 9473c98e1..ffa588d93 100644 --- a/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py +++ b/migrations/versions/2cbfbcca4737_uuid_columns_for_proposal_and_session.py @@ -14,18 +14,13 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa -proposal = table( - 'proposal', column('id', sa.Integer()), column('uuid', postgresql.UUID()) -) +proposal = table('proposal', column('id', sa.Integer()), column('uuid', sa.Uuid())) -session = table( - 'session', column('id', sa.Integer()), column('uuid', postgresql.UUID()) -) +session = table('session', column('id', sa.Integer()), column('uuid', sa.Uuid())) def get_progressbar(label, maxval): @@ -47,7 +42,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('proposal', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('proposal', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(proposal)) progress = get_progressbar("Proposals", count) progress.start() @@ -61,7 +56,7 @@ def upgrade(): op.alter_column('proposal', 'uuid', nullable=False) op.create_unique_constraint('proposal_uuid_key', 'proposal', ['uuid']) - op.add_column('session', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('session', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(session)) progress = get_progressbar("Sessions", count) progress.start() diff --git a/migrations/versions/321b11b6a413_add_participant_uuid_field.py b/migrations/versions/321b11b6a413_add_participant_uuid_field.py index 224ad9fa1..0e8e8642d 100644 --- a/migrations/versions/321b11b6a413_add_participant_uuid_field.py +++ b/migrations/versions/321b11b6a413_add_participant_uuid_field.py @@ -14,13 +14,12 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa participant = table( - 'participant', column('id', sa.Integer()), column('uuid', postgresql.UUID()) + 'participant', column('id', sa.Integer()), column('uuid', sa.Uuid()) ) @@ -43,7 +42,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('participant', sa.Column('uuid', postgresql.UUID(), nullable=True)) + 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)) progress = get_progressbar("Participants", count) diff --git a/migrations/versions/34a95ee0c3a0_site_membership_models.py b/migrations/versions/34a95ee0c3a0_site_membership_models.py index d7b7c5fd6..62f470085 100644 --- a/migrations/versions/34a95ee0c3a0_site_membership_models.py +++ b/migrations/versions/34a95ee0c3a0_site_membership_models.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -22,7 +21,7 @@ def upgrade(): op.create_table( 'site_membership', - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('granted_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('revoked_at', sa.TIMESTAMP(timezone=True), nullable=True), sa.Column('record_type', sa.Integer(), nullable=False), diff --git a/migrations/versions/382cde270594_create_lastuser_tables.py b/migrations/versions/382cde270594_create_lastuser_tables.py index e2ef47117..238dfb741 100644 --- a/migrations/versions/382cde270594_create_lastuser_tables.py +++ b/migrations/versions/382cde270594_create_lastuser_tables.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -74,7 +73,7 @@ def upgrade(): op.create_table( 'organization', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('uuid', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('owners_id', sa.Integer(), nullable=True), @@ -100,7 +99,7 @@ def upgrade(): op.create_table( 'user', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('uuid', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('fullname', sa.Unicode(length=80), nullable=False), @@ -115,7 +114,7 @@ def upgrade(): ) op.create_table( 'account_name', - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('name', sa.Unicode(length=63), nullable=False), @@ -141,7 +140,7 @@ def upgrade(): op.create_table( 'auth_client', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('uuid', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('user_id', sa.Integer(), nullable=True), @@ -186,7 +185,7 @@ def upgrade(): op.create_table( 'team', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('uuid', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('title', sa.Unicode(length=250), nullable=False), @@ -258,7 +257,7 @@ def upgrade(): ) op.create_table( 'user_oldid', - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), @@ -301,7 +300,7 @@ def upgrade(): op.create_table( 'user_session', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('uuid', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), diff --git a/migrations/versions/3d3df26524b7_commentset_membership.py b/migrations/versions/3d3df26524b7_commentset_membership.py index 0077db8d5..79819c1af 100644 --- a/migrations/versions/3d3df26524b7_commentset_membership.py +++ b/migrations/versions/3d3df26524b7_commentset_membership.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -30,7 +29,7 @@ def upgrade(): sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('revoked_by_id', sa.Integer(), nullable=True), sa.Column('granted_by_id', sa.Integer(), nullable=True), - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.ForeignKeyConstraint( diff --git a/migrations/versions/41a4531be082_remove_account_name.py b/migrations/versions/41a4531be082_remove_account_name.py index b7b5de643..eb445587b 100644 --- a/migrations/versions/41a4531be082_remove_account_name.py +++ b/migrations/versions/41a4531be082_remove_account_name.py @@ -21,7 +21,7 @@ account_name = table( 'account_name', - column('id', postgresql.UUID()), + column('id', sa.Uuid()), column('created_at', sa.TIMESTAMP(timezone=True)), column('updated_at', sa.TIMESTAMP(timezone=True)), column('name', sa.Unicode(63)), @@ -32,7 +32,7 @@ profile = table( 'profile', - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('created_at', sa.TIMESTAMP(timezone=True)), column('updated_at', sa.TIMESTAMP(timezone=True)), column('name', sa.Unicode(63)), @@ -50,7 +50,7 @@ def upgrade(): def downgrade(): op.create_table( 'account_name', - sa.Column('id', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('id', sa.Uuid(), autoincrement=False, nullable=False), sa.Column( 'created_at', postgresql.TIMESTAMP(timezone=True), diff --git a/migrations/versions/69c2ced88981_team_org_uuid.py b/migrations/versions/69c2ced88981_team_org_uuid.py index b78563b50..6a3ab1900 100644 --- a/migrations/versions/69c2ced88981_team_org_uuid.py +++ b/migrations/versions/69c2ced88981_team_org_uuid.py @@ -12,7 +12,6 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa @@ -23,7 +22,7 @@ 'team', column('id', sa.Integer()), column('orgid', sa.String(22)), - column('org_uuid', postgresql.UUID()), + column('org_uuid', sa.Uuid()), ) @@ -46,7 +45,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('team', sa.Column('org_uuid', postgresql.UUID(), nullable=True)) + op.add_column('team', sa.Column('org_uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(team)) progress = get_progressbar("Teams", count) progress.start() diff --git a/migrations/versions/71fcac85957c_populate_membership_models.py b/migrations/versions/71fcac85957c_populate_membership_models.py index ca1654dd8..8a8c4a10e 100644 --- a/migrations/versions/71fcac85957c_populate_membership_models.py +++ b/migrations/versions/71fcac85957c_populate_membership_models.py @@ -10,7 +10,6 @@ from uuid import uuid4 from alembic import op -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import sqlalchemy as sa @@ -32,13 +31,13 @@ class MEMBERSHIP_RECORD_TYPE: # noqa: N801 'organization', column('id', sa.Integer()), column('owners_id', sa.Integer()), - column('uuid', postgresql.UUID(as_uuid=True)), + column('uuid', sa.Uuid(as_uuid=True)), ) profile = table( 'profile', column('id', sa.Integer()), - column('uuid', postgresql.UUID(as_uuid=True)), + column('uuid', sa.Uuid(as_uuid=True)), column('user_id', sa.Integer()), column('organization_id', sa.Integer()), column('admin_team_id', sa.Integer()), @@ -77,7 +76,7 @@ class MEMBERSHIP_RECORD_TYPE: # noqa: N801 project_crew_membership = table( 'project_crew_membership', - column('id', postgresql.UUID(as_uuid=True)), + column('id', sa.Uuid(as_uuid=True)), column('project_id', sa.Integer()), column('user_id', sa.Integer()), column('is_editor', sa.Boolean()), @@ -94,7 +93,7 @@ class MEMBERSHIP_RECORD_TYPE: # noqa: N801 organization_membership = table( 'organization_membership', - column('id', postgresql.UUID(as_uuid=True)), + column('id', sa.Uuid(as_uuid=True)), column('organization_id', sa.Integer()), column('user_id', sa.Integer()), column('is_owner', sa.Boolean()), @@ -109,7 +108,7 @@ class MEMBERSHIP_RECORD_TYPE: # noqa: N801 proposal_membership = table( 'proposal_membership', - column('id', postgresql.UUID(as_uuid=True)), + column('id', sa.Uuid(as_uuid=True)), column('proposal_id', sa.Integer()), column('user_id', sa.Integer()), column('is_reviewer', sa.Boolean()), diff --git a/migrations/versions/79719ee38228_moderator_report_models.py b/migrations/versions/79719ee38228_moderator_report_models.py index 6226b4b55..e06b32b33 100644 --- a/migrations/versions/79719ee38228_moderator_report_models.py +++ b/migrations/versions/79719ee38228_moderator_report_models.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -26,7 +25,7 @@ def upgrade(): sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('report_type', sa.SmallInteger(), nullable=False), sa.Column('reported_at', sa.TIMESTAMP(timezone=True), nullable=False), - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.ForeignKeyConstraint(['comment_id'], ['comment.id']), diff --git a/migrations/versions/7f8114c73092_add_rsvp_uuid.py b/migrations/versions/7f8114c73092_add_rsvp_uuid.py index df678c668..1e582e121 100644 --- a/migrations/versions/7f8114c73092_add_rsvp_uuid.py +++ b/migrations/versions/7f8114c73092_add_rsvp_uuid.py @@ -11,7 +11,6 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa @@ -27,7 +26,7 @@ 'rsvp', column('project_id', sa.Integer()), column('user_id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), ) @@ -49,7 +48,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('rsvp', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('rsvp', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(rsvp)) progress = get_progressbar("Rsvps", count) diff --git a/migrations/versions/8829241430b6_add_membership_models.py b/migrations/versions/8829241430b6_add_membership_models.py index 9f1151d08..c82353ab6 100644 --- a/migrations/versions/8829241430b6_add_membership_models.py +++ b/migrations/versions/8829241430b6_add_membership_models.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -22,7 +21,7 @@ def upgrade(): op.create_table( 'organization_membership', - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('granted_at', sa.TIMESTAMP(timezone=True), nullable=False), @@ -56,7 +55,7 @@ def upgrade(): ) op.create_table( 'project_crew_membership', - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('granted_at', sa.TIMESTAMP(timezone=True), nullable=False), @@ -103,7 +102,7 @@ def upgrade(): sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('revoked_by_id', sa.Integer(), nullable=True), sa.Column('granted_by_id', sa.Integer(), nullable=True), - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.CheckConstraint( diff --git a/migrations/versions/887db555cca9_adding_uuid_to_commentset.py b/migrations/versions/887db555cca9_adding_uuid_to_commentset.py index ab0fa9b21..6643582e6 100644 --- a/migrations/versions/887db555cca9_adding_uuid_to_commentset.py +++ b/migrations/versions/887db555cca9_adding_uuid_to_commentset.py @@ -11,7 +11,6 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa @@ -23,9 +22,7 @@ depends_on: Optional[Union[str, Tuple[str, ...]]] = None -commentset = table( - 'commentset', column('id', sa.Integer()), column('uuid', postgresql.UUID()) -) +commentset = table('commentset', column('id', sa.Integer()), column('uuid', sa.Uuid())) def get_progressbar(label, maxval): @@ -47,7 +44,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('commentset', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('commentset', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(commentset)) progress = get_progressbar("Commentsets", count) diff --git a/migrations/versions/931be3605dc4_notification_models.py b/migrations/versions/931be3605dc4_notification_models.py index d1f9d9c10..01e47f22b 100644 --- a/migrations/versions/931be3605dc4_notification_models.py +++ b/migrations/versions/931be3605dc4_notification_models.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -22,12 +21,12 @@ def upgrade(): op.create_table( 'notification', - sa.Column('eventid', postgresql.UUID(), nullable=False), - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('eventid', sa.Uuid(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('type', sa.Unicode(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('document_uuid', postgresql.UUID(), nullable=False), - sa.Column('fragment_uuid', postgresql.UUID(), nullable=True), + sa.Column('document_uuid', sa.Uuid(), nullable=False), + sa.Column('fragment_uuid', sa.Uuid(), nullable=True), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='SET NULL'), @@ -64,12 +63,12 @@ def upgrade(): op.create_table( 'user_notification', sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('eventid', postgresql.UUID(), nullable=False), - sa.Column('notification_id', postgresql.UUID(), nullable=False), + sa.Column('eventid', sa.Uuid(), nullable=False), + sa.Column('notification_id', sa.Uuid(), nullable=False), sa.Column('role', sa.Unicode(), nullable=False), sa.Column('read_at', sa.TIMESTAMP(timezone=True), nullable=True), sa.Column('is_revoked', sa.Boolean(), nullable=False), - sa.Column('rollupid', postgresql.UUID(), nullable=True), + sa.Column('rollupid', sa.Uuid(), nullable=True), sa.Column('messageid_email', sa.Unicode(), nullable=True), sa.Column('messageid_sms', sa.Unicode(), nullable=True), sa.Column('messageid_webpush', sa.Unicode(), nullable=True), diff --git a/migrations/versions/94ce3a9b7a3a_draft_model.py b/migrations/versions/94ce3a9b7a3a_draft_model.py index 4368aeff1..ae7e068ba 100644 --- a/migrations/versions/94ce3a9b7a3a_draft_model.py +++ b/migrations/versions/94ce3a9b7a3a_draft_model.py @@ -10,7 +10,6 @@ down_revision = 'a9cb0e1c52ed' from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa from coaster.sqlalchemy.columns import JsonDict @@ -22,9 +21,9 @@ def upgrade(): sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.Column('table', sa.UnicodeText(), nullable=False), - sa.Column('table_row_id', postgresql.UUID(), nullable=False), + sa.Column('table_row_id', sa.Uuid(), nullable=False), sa.Column('body', JsonDict(), server_default='{}', nullable=False), - sa.Column('revision', postgresql.UUID(), nullable=True), + sa.Column('revision', sa.Uuid(), nullable=True), sa.PrimaryKeyConstraint('table', 'table_row_id'), ) diff --git a/migrations/versions/a1ab7bd78649_post_model.py b/migrations/versions/a1ab7bd78649_post_model.py index d347d45de..6456a89fa 100644 --- a/migrations/versions/a1ab7bd78649_post_model.py +++ b/migrations/versions/a1ab7bd78649_post_model.py @@ -10,7 +10,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql from sqlalchemy_utils import TSVectorType import sqlalchemy as sa @@ -40,7 +39,7 @@ def upgrade(): sa.Column('voteset_id', sa.Integer(), nullable=False), sa.Column('commentset_id', sa.Integer(), nullable=False), sa.Column('search_vector', TSVectorType(), nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('uuid', sa.Uuid(), nullable=False), sa.Column('id', sa.Integer(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), diff --git a/migrations/versions/a9cb0e1c52ed_uuid_fields_venue_room.py b/migrations/versions/a9cb0e1c52ed_uuid_fields_venue_room.py index 28e870794..a897e52aa 100644 --- a/migrations/versions/a9cb0e1c52ed_uuid_fields_venue_room.py +++ b/migrations/versions/a9cb0e1c52ed_uuid_fields_venue_room.py @@ -13,19 +13,16 @@ from uuid import uuid4 from alembic import op -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import sqlalchemy as sa -venue_room = table( - 'venue_room', column('id', sa.Integer()), column('uuid', postgresql.UUID()) -) +venue_room = table('venue_room', column('id', sa.Integer()), column('uuid', sa.Uuid())) def upgrade(): conn = op.get_bind() - op.add_column('venue_room', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('venue_room', sa.Column('uuid', sa.Uuid(), nullable=True)) items = conn.execute(sa.select(venue_room.c.id)) for item in items: conn.execute( 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 3a168b606..cc6f8ea2c 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 @@ -14,35 +14,32 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa from coaster.utils import buid2uuid, uuid2buid -project = table( - 'project', column('id', sa.Integer()), column('uuid', postgresql.UUID()) -) +project = table('project', column('id', sa.Integer()), column('uuid', sa.Uuid())) profile = table( 'profile', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('userid', sa.String(22)), ) user = table( 'user', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('userid', sa.String(22)), ) team = table( 'team', column('id', sa.Integer()), - column('uuid', postgresql.UUID()), + column('uuid', sa.Uuid()), column('userid', sa.String(22)), ) @@ -67,7 +64,7 @@ def upgrade(): conn = op.get_bind() # --- Project - op.add_column('project', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('project', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(project)) progress = get_progressbar("Projects", count) progress.start() @@ -82,7 +79,7 @@ def upgrade(): op.create_unique_constraint('project_uuid_key', 'project', ['uuid']) # --- Profile - op.add_column('profile', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('profile', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(profile)) progress = get_progressbar("Profiles", count) progress.start() @@ -101,7 +98,7 @@ def upgrade(): op.drop_column('profile', 'userid') # --- Team - op.add_column('team', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('team', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(team)) progress = get_progressbar("Teams", count) progress.start() @@ -120,7 +117,7 @@ def upgrade(): op.drop_column('team', 'userid') # --- User - op.add_column('user', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('user', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(user)) progress = get_progressbar("Users", count) progress.start() diff --git a/migrations/versions/bd465803af3a_add_sponsormembership.py b/migrations/versions/bd465803af3a_add_sponsormembership.py index 45fe9c343..291d5ed1b 100644 --- a/migrations/versions/bd465803af3a_add_sponsormembership.py +++ b/migrations/versions/bd465803af3a_add_sponsormembership.py @@ -9,7 +9,6 @@ from typing import Optional, Tuple, Union from alembic import op -from sqlalchemy.dialects import postgresql import sqlalchemy as sa # revision identifiers, used by Alembic. @@ -32,7 +31,7 @@ def upgrade(): sa.Column('profile_id', sa.Integer(), nullable=False), sa.Column('revoked_by_id', sa.Integer(), nullable=True), sa.Column('granted_by_id', sa.Integer(), nullable=False), - sa.Column('id', postgresql.UUID(), nullable=False), + sa.Column('id', sa.Uuid(), nullable=False), sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False), sa.ForeignKeyConstraint(['granted_by_id'], ['user.id'], ondelete='SET NULL'), diff --git a/migrations/versions/c3069d33419a_comment_uuid_field.py b/migrations/versions/c3069d33419a_comment_uuid_field.py index 2861eee78..52cd86a96 100644 --- a/migrations/versions/c3069d33419a_comment_uuid_field.py +++ b/migrations/versions/c3069d33419a_comment_uuid_field.py @@ -13,14 +13,11 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa -comment = table( - 'comment', column('id', sa.Integer()), column('uuid', postgresql.UUID()) -) +comment = table('comment', column('id', sa.Integer()), column('uuid', sa.Uuid())) def get_progressbar(label, maxval): @@ -42,7 +39,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('comment', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('comment', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(comment)) progress = get_progressbar("Comments", count) diff --git a/migrations/versions/d50c3d8e3f33_drop_old_user_and_team_tables.py b/migrations/versions/d50c3d8e3f33_drop_old_user_and_team_tables.py index f12a9aae5..15b32b0db 100644 --- a/migrations/versions/d50c3d8e3f33_drop_old_user_and_team_tables.py +++ b/migrations/versions/d50c3d8e3f33_drop_old_user_and_team_tables.py @@ -51,8 +51,8 @@ def downgrade(): ), sa.Column('title', sa.VARCHAR(length=250), autoincrement=False, nullable=False), sa.Column('owners', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column('uuid', postgresql.UUID(), autoincrement=False, nullable=False), - sa.Column('org_uuid', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('uuid', sa.Uuid(), autoincrement=False, nullable=False), + sa.Column('org_uuid', sa.Uuid(), autoincrement=False, nullable=False), sa.PrimaryKeyConstraint('id', name='old_team_pkey'), sa.UniqueConstraint('uuid', name='old_team_uuid_key'), postgresql_ignore_search_path=False, @@ -108,7 +108,7 @@ def downgrade(): nullable=True, ), sa.Column('status', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('uuid', postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column('uuid', sa.Uuid(), autoincrement=False, nullable=False), sa.PrimaryKeyConstraint('id', name='old_user_pkey'), sa.UniqueConstraint('email', name='old_user_email_key'), sa.UniqueConstraint('lastuser_token', name='old_user_lastuser_token_key'), diff --git a/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py b/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py index 789a1cd27..b45ea0eaf 100644 --- a/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py +++ b/migrations/versions/e2b28adfa135_add_proposal_suuid_redirect.py @@ -10,7 +10,6 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa @@ -24,7 +23,7 @@ proposal = table( 'proposal', column('id', sa.Integer()), - column('uuid', postgresql.UUID(as_uuid=True)), + column('uuid', sa.Uuid(as_uuid=True)), ) proposal_suuid_redirect = table( diff --git a/migrations/versions/eec2fad0f3e9_venue_uuid_field.py b/migrations/versions/eec2fad0f3e9_venue_uuid_field.py index ffad68967..d8a919296 100644 --- a/migrations/versions/eec2fad0f3e9_venue_uuid_field.py +++ b/migrations/versions/eec2fad0f3e9_venue_uuid_field.py @@ -13,12 +13,11 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa -venue = table('venue', column('id', sa.Integer()), column('uuid', postgresql.UUID())) +venue = table('venue', column('id', sa.Integer()), column('uuid', sa.Uuid())) def get_progressbar(label, maxval): @@ -40,7 +39,7 @@ def get_progressbar(label, maxval): def upgrade(): conn = op.get_bind() - op.add_column('venue', sa.Column('uuid', postgresql.UUID(), nullable=True)) + op.add_column('venue', sa.Column('uuid', sa.Uuid(), nullable=True)) count = conn.scalar(sa.select(sa.func.count('*')).select_from(venue)) progress = get_progressbar("Venues", count) progress.start() diff --git a/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py b/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py index 626f3b748..7ba779a7e 100644 --- a/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py +++ b/migrations/versions/fa05ebecbc0f_populate_proposalmembership.py @@ -11,7 +11,6 @@ from alembic import op from progressbar import ProgressBar -from sqlalchemy.dialects import postgresql from sqlalchemy.sql import column, table import progressbar.widgets import sqlalchemy as sa @@ -43,7 +42,7 @@ class MEMBERSHIP_RECORD_TYPE: # noqa: N801 proposal_membership = table( 'proposal_membership', - column('id', postgresql.UUID()), + column('id', sa.Uuid()), column('created_at', sa.TIMESTAMP(timezone=True)), column('updated_at', sa.TIMESTAMP(timezone=True)), column('granted_at', sa.TIMESTAMP(timezone=True)), From 4691dcfc36eb16552b51dbc8380232f1ef2d2758 Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Sun, 21 May 2023 14:31:44 +0530 Subject: [PATCH 007/175] Grant project_* roles within Project too, and drop transition validator (#1716) ProjectStartingNotification depends on the `project_crew` and `project_participant` roles, which it expects to find in the Session model, but which are not granted in the project itself. The notification is also dispatched on projects with no session, causing a discovery of 0 recipients. The `if_` validator on the `Profile.make_public` transition is redundant as a conditional state serves the same purpose. Coaster will be removing transition validators. --- funnel/models/notification.py | 15 ++++------ funnel/models/profile.py | 17 +++++++---- funnel/models/project_membership.py | 30 +++++++++++-------- funnel/models/rsvp.py | 4 ++- funnel/models/venue.py | 3 +- .../project_starting_email.html.jinja2 | 2 +- .../project_starting_web.html.jinja2 | 2 +- funnel/views/api/resource.py | 11 +++++-- .../project_starting_notification.py | 8 +++-- 9 files changed, 55 insertions(+), 37 deletions(-) diff --git a/funnel/models/notification.py b/funnel/models/notification.py index 8f02cfdcd..02966e1f0 100644 --- a/funnel/models/notification.py +++ b/funnel/models/notification.py @@ -763,7 +763,7 @@ class UserNotification( notification_id: Mapped[UUID] = sa.orm.mapped_column(sa.Uuid, nullable=False) #: Notification that this user received - notification = with_roles( + notification: Mapped[Notification] = with_roles( sa.orm.relationship( Notification, backref=sa.orm.backref('recipients', lazy='dynamic') ), @@ -906,22 +906,19 @@ def is_revoked(self) -> bool: # pylint: disable=invalid-overridden-method """Whether this notification has been marked as revoked.""" return self.revoked_at is not None - @is_revoked.setter # type: ignore[no-redef] - def is_revoked(self, value: bool) -> None: + @is_revoked.inplace.setter + def _is_revoked_setter(self, value: bool) -> None: if value: if not self.revoked_at: self.revoked_at = sa.func.utcnow() else: self.revoked_at = None - # PyLint complains because the hybrid property doesn't resemble the mixin's property - # pylint: disable=no-self-argument,arguments-renamed,invalid-overridden-method - @is_revoked.expression # type: ignore[no-redef] - def is_revoked(cls): + @is_revoked.inplace.expression + @classmethod + def _is_revoked_expression(cls): return cls.revoked_at.isnot(None) - # pylint: enable=no-self-argument,arguments-renamed,invalid-overridden-method - with_roles(is_revoked, rw={'owner'}) # --- Dispatch helper methods ------------------------------------------------------ diff --git a/funnel/models/profile.py b/funnel/models/profile.py index 115485f33..dd9f9b478 100644 --- a/funnel/models/profile.py +++ b/funnel/models/profile.py @@ -259,6 +259,16 @@ class Profile( 'ACTIVE_AND_PUBLIC', state.PUBLIC, lambda profile: profile.is_active ) + state.add_conditional_state( + 'PUBLISHABLE', + state.NOT_PUBLIC, + lambda profile: ( + profile.reserved is False + and profile.is_active + and (profile.user is None or profile.user.features.not_likely_throwaway) + ), + ) + def __repr__(self) -> str: """Represent :class:`Profile` as a string.""" return f'' @@ -479,14 +489,9 @@ def teams(self) -> List[Team]: @with_roles(call={'owner'}) @state.transition( - state.NOT_PUBLIC, + state.PUBLISHABLE, state.PUBLIC, title=__("Make public"), - if_=lambda profile: ( - profile.reserved is False - and profile.is_active - and (profile.user is None or profile.user.features.not_likely_throwaway) - ), ) def make_public(self) -> None: """Make an account public if it is eligible.""" diff --git a/funnel/models/project_membership.py b/funnel/models/project_membership.py index 5d658ed60..d9b599e26 100644 --- a/funnel/models/project_membership.py +++ b/funnel/models/project_membership.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Set, Union +from typing import Set from uuid import UUID # noqa: F401 # pylint: disable=unused-import from werkzeug.utils import cached_property @@ -18,20 +18,18 @@ __all__ = ['ProjectCrewMembership', 'project_child_role_map'] #: Roles in a project and their remapped names in objects attached to a project -project_child_role_map: Dict[str, str] = { - 'editor': 'project_editor', - 'promoter': 'project_promoter', - 'usher': 'project_usher', - 'crew': 'project_crew', - 'participant': 'project_participant', - 'reader': 'reader', +project_child_role_map = { + 'editor': {'project_editor'}, + 'promoter': {'project_promoter'}, + 'usher': {'project_usher'}, + 'crew': {'project_crew'}, + 'participant': {'project_participant'}, + 'reader': {'reader'}, } #: ProjectCrewMembership maps project's `profile_admin` role to membership's `editor` #: role in addition to the recurring role grant map -project_membership_role_map: Dict[str, Union[str, Set[str]]] = { - 'profile_admin': {'profile_admin', 'editor'} -} +project_membership_role_map = {'profile_admin': {'profile_admin', 'editor'}} project_membership_role_map.update(project_child_role_map) @@ -195,7 +193,15 @@ class __Project: ), viewonly=True, ), - grants_via={'user': {'editor', 'promoter', 'usher', 'participant', 'crew'}}, + grants_via={ + 'user': { + 'editor': {'editor', 'project_editor'}, + 'promoter': {'promoter', 'project_promoter'}, + 'usher': {'usher', 'project_usher'}, + 'participant': {'participant', 'project_participant'}, + 'crew': {'crew', 'project_crew'}, + } + }, ) active_editor_memberships = sa.orm.relationship( diff --git a/funnel/models/rsvp.py b/funnel/models/rsvp.py index 2e20b9caf..629ab5e2c 100644 --- a/funnel/models/rsvp.py +++ b/funnel/models/rsvp.py @@ -200,7 +200,9 @@ class __Project: def active_rsvps(self): return self.rsvps.join(User).filter(Rsvp.state.YES, User.state.ACTIVE) - with_roles(active_rsvps, grants_via={Rsvp.user: {'participant'}}) + with_roles( + active_rsvps, grants_via={Rsvp.user: {'participant', 'project_participant'}} + ) @overload def rsvp_for(self, user: User, create: Literal[True]) -> Rsvp: diff --git a/funnel/models/venue.py b/funnel/models/venue.py index f78cbc4d5..d26c573da 100644 --- a/funnel/models/venue.py +++ b/funnel/models/venue.py @@ -4,6 +4,7 @@ from typing import List from uuid import UUID # noqa: F401 # pylint: disable=unused-import +import itertools from sqlalchemy.ext.orderinglist import ordering_list @@ -117,7 +118,7 @@ class VenueRoom(UuidMixin, BaseScopedNameMixin, db.Model): # type: ignore[name- venue: Mapped[Venue] = with_roles( sa.orm.relationship(Venue, back_populates='rooms'), # Since Venue already remaps Project roles, we just want the remapped role names - grants_via={None: set(project_child_role_map.values())}, + grants_via={None: set(itertools.chain(*project_child_role_map.values()))}, ) parent: Mapped[Venue] = sa.orm.synonym('venue') description = MarkdownCompositeBasic.create( diff --git a/funnel/templates/notifications/project_starting_email.html.jinja2 b/funnel/templates/notifications/project_starting_email.html.jinja2 index b433d151d..4aeb13471 100644 --- a/funnel/templates/notifications/project_starting_email.html.jinja2 +++ b/funnel/templates/notifications/project_starting_email.html.jinja2 @@ -3,7 +3,7 @@ {%- block content -%}

- {%- trans project=view.project.joined_title, start_time=view.session.start_at_localized|time -%} + {%- trans project=view.project.joined_title, start_time=(view.session or view.project).start_at_localized|time -%} {{ project }} starts at {{ start_time }} {%- endtrans -%}

diff --git a/funnel/templates/notifications/project_starting_web.html.jinja2 b/funnel/templates/notifications/project_starting_web.html.jinja2 index 63389859e..72c1cafeb 100644 --- a/funnel/templates/notifications/project_starting_web.html.jinja2 +++ b/funnel/templates/notifications/project_starting_web.html.jinja2 @@ -7,7 +7,7 @@ {%- block content -%}

- {% trans project=view.project.joined_title, url=view.project.url_for(), start_time=view.session.start_at_localized|time -%} + {% trans project=view.project.joined_title, url=view.project.url_for(), start_time=(view.session or view.project).start_at_localized|time -%} {{ project }} starts at {{ start_time }} {%- endtrans %}

diff --git a/funnel/views/api/resource.py b/funnel/views/api/resource.py index 8eac9f78f..b1b662374 100644 --- a/funnel/views/api/resource.py +++ b/funnel/views/api/resource.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import Any, Container, Dict, List, Optional, cast +from typing import Any, Container, Dict, List, Optional, Union, cast from flask import abort, jsonify, render_template, request +from typing_extensions import Literal from baseframe import __ from coaster.auth import current_auth @@ -126,11 +127,15 @@ def resource_error(error, description=None, uri=None) -> Response: return response -def api_result(status, _jsonp=False, **params) -> Response: +def api_result( + status: Union[Literal['ok'], Literal['error'], Literal[200], Literal[201]], + _jsonp: bool = False, + **params: Any, +) -> Response: """Return an API result.""" status_code = 200 if status in (200, 201): - status_code = status + status_code = status # type: ignore[assignment] status = 'ok' elif status == 'error': status_code = 422 diff --git a/funnel/views/notifications/project_starting_notification.py b/funnel/views/notifications/project_starting_notification.py index 1dcac1fbb..2dac5767c 100644 --- a/funnel/views/notifications/project_starting_notification.py +++ b/funnel/views/notifications/project_starting_notification.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Optional + from flask import render_template from baseframe import _, __ @@ -18,7 +20,7 @@ class RenderProjectStartingNotification(RenderNotification): """Notify crew and participants when the project's schedule is about to start.""" project: Project - session: Session + session: Optional[Session] aliases = {'document': 'project', 'fragment': 'session'} emoji_prefix = "⏰ " reason = __("You are receiving this because you have registered for this project") @@ -31,7 +33,7 @@ def web(self): def email_subject(self): return self.emoji_prefix + _("{project} starts at {time}").format( project=self.project.joined_title, - time=time_filter(self.session.start_at_localized), + time=time_filter((self.session or self.project).start_at_localized), ) def email_content(self): @@ -43,7 +45,7 @@ def sms(self) -> OneLineTemplate: return OneLineTemplate( text1=_("{project} starts at {time}.").format( project=self.project.joined_title, - time=time_filter(self.session.start_at_localized), + time=time_filter((self.session or self.project).start_at_localized), ), url=shortlink( self.project.url_for(_external=True, **self.tracking_tags('sms')), From 65e54f23742a69681376f9310ded8a52a7ba35ae Mon Sep 17 00:00:00 2001 From: Amogh M Aradhya Date: Sun, 21 May 2023 15:43:51 +0530 Subject: [PATCH 008/175] Swap UTM tracking tags for source and campaign (#1715) `utm_source` and `utm_campaign` were incorrectly used. This commit swaps their usage, and also identifies notifications by their type and timestamp instead of their UUID. Co-authored-by: Kiran Jonnalagadda --- funnel/templates/js/comments.js.jinja2 | 2 +- funnel/templates/js/update.js.jinja2 | 2 +- funnel/templates/macros.html.jinja2 | 4 +-- funnel/templates/project_layout.html.jinja2 | 2 +- .../templates/session_view_popup.html.jinja2 | 2 +- funnel/templates/submission.html.jinja2 | 2 +- funnel/views/email.py | 4 +-- funnel/views/notification.py | 34 ++++++++++++++----- funnel/views/otp.py | 2 +- tests/unit/views/api_shortlink_test.py | 6 ++-- 10 files changed, 37 insertions(+), 23 deletions(-) diff --git a/funnel/templates/js/comments.js.jinja2 b/funnel/templates/js/comments.js.jinja2 index 912650354..b502576eb 100644 --- a/funnel/templates/js/comments.js.jinja2 +++ b/funnel/templates/js/comments.js.jinja2 @@ -81,7 +81,7 @@ - +
- +
diff --git a/funnel/templates/submission.html.jinja2 b/funnel/templates/submission.html.jinja2 index 7f3a9fc3c..d0ec8a29b 100644 --- a/funnel/templates/submission.html.jinja2 +++ b/funnel/templates/submission.html.jinja2 @@ -29,7 +29,7 @@ class="hg-link-btn mui--hide mui--text-dark" data-ga="Share dropdown" data-cy="share-project" - data-url="{{ proposal.url_for(_external=true, utm_campaign='webshare') }}" + data-url="{{ proposal.url_for(_external=true, utm_source='webshare') }}" aria-label="{% trans %}Share{% endtrans %}">{{ faicon(icon='share-alt', icon_size=icon_size, baseline=false, css_class="mui--align-middle") }}
str: email_hash=useremail.email_address.email_hash, secret=useremail.verification_code, utm_medium='email', - utm_campaign='verify', + utm_source='account-verify', ) jsonld = jsonld_confirm_action(subject, url, _("Verify email address")) content = render_template( @@ -39,7 +39,7 @@ def send_password_reset_link(email: str, user: User, otp: str, token: str) -> st _external=True, token=token, utm_medium='email', - utm_campaign='reset', + utm_source='account-reset', ) jsonld = jsonld_view_action(subject, url, _("Reset password")) content = render_template( diff --git a/funnel/views/notification.py b/funnel/views/notification.py index 82a06cd9a..ce9013fbb 100644 --- a/funnel/views/notification.py +++ b/funnel/views/notification.py @@ -188,20 +188,36 @@ def transport_for(self, transport): ) def tracking_tags( - self, transport: str = 'email', campaign: str = 'notification' + self, + medium: str = 'email', + source: str = 'notification', + campaign: Optional[str] = None, ) -> Dict[str, str]: """ Provide tracking tags for URL parameters. Subclasses may override if required. - :param transport: Transport (or medium) over which this link is being delivered + :param medium: Medium (or transport) over which this link is being delivered (default 'email' as that's the most common use case for tracked links) - :param campaign: Reason for this link being sent (default 'notification' but - unsubscribe links and other specialized links will want to specify another) + :param source: Source of this link (default 'notification' but unsubscribe + links and other specialised links will want to specify another) + :param campaign: Reason for this link being sent (defaults to notification type + and timestamp) """ - tags = {'utm_campaign': campaign, 'utm_medium': transport} - if not self.notification.for_private_recipient: - tags['utm_source'] = self.notification.eventid_b58 - return tags + if campaign is None: + if self.notification.for_private_recipient: + # Do not include a timestamp when it's a private notification, as that + # can be used to identify the event + campaign = self.notification.type + else: + campaign = ( + f'{self.notification.type}' + f'-{self.notification.created_at.strftime("%Y%m%d-%H%M")}' + ) + return { + 'utm_campaign': campaign, + 'utm_medium': medium, + 'utm_source': source, + } def unsubscribe_token(self, transport): """ @@ -234,7 +250,7 @@ def unsubscribe_url(self, transport): 'notification_unsubscribe', token=self.unsubscribe_token(transport=transport), _external=True, - **self.tracking_tags(transport=transport, campaign='unsubscribe'), + **self.tracking_tags(transport, source='unsubscribe'), ) @cached_property diff --git a/funnel/views/otp.py b/funnel/views/otp.py index 91bdf36d5..5e83859a7 100644 --- a/funnel/views/otp.py +++ b/funnel/views/otp.py @@ -477,7 +477,7 @@ def send_email( _external=True, token=self.link_token, utm_medium='email', - utm_campaign='reset', + utm_source='account-reset', ) jsonld = jsonld_view_action(subject, url, _("Reset password")) content = render_template( diff --git a/tests/unit/views/api_shortlink_test.py b/tests/unit/views/api_shortlink_test.py index d330906e9..1d6900c72 100644 --- a/tests/unit/views/api_shortlink_test.py +++ b/tests/unit/views/api_shortlink_test.py @@ -76,9 +76,7 @@ def test_create_shortlink(app, client, user_rincewind, create_shortlink) -> None rv = client.post( create_shortlink, data={ - 'url': user_rincewind.profile.url_for( - _external=True, utm_campaign='webshare' - ) + 'url': user_rincewind.profile.url_for(_external=True, utm_source='webshare') }, ) assert rv.status_code == 201 @@ -87,7 +85,7 @@ def test_create_shortlink(app, client, user_rincewind, create_shortlink) -> None assert len(str(sl3.path)) <= 5 # API defaults to the shorter form (max 4 chars) assert sl3.path != sl1.path # We got a different shortlink assert rv.json['url'] == user_rincewind.profile.url_for( - _external=True, utm_campaign='webshare' + _external=True, utm_source='webshare' ) From 18c327d9f9c46a46c2ae021983a8a3c99a192a29 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 22:10:54 +0530 Subject: [PATCH 009/175] [pre-commit.ci] pre-commit autoupdate (#1717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/charliermarsh/ruff-pre-commit: v0.0.267 → v0.0.269](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.267...v0.0.269) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c5f94d72..be71516da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: ] files: ^requirements/.*\.txt$ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.267 + rev: v0.0.269 hooks: - id: ruff args: ['--fix', '--exit-non-zero-on-fix'] From f83ecec5fd70c58915409f6bc82d60805d6eccdc Mon Sep 17 00:00:00 2001 From: Vidya Ramakrishnan Date: Tue, 23 May 2023 12:14:27 +0530 Subject: [PATCH 010/175] Move select2 import from baseframe to webpack (#1700) * Move all functions from script.js * Move form widget functions to a file * Refactor JS function calls, remove global functions. Move pending JS to webpack * Cleanup and reorganize JS functions * Add activated class to codemirror enabled textarea * rename form widgets file * Replace the username in test fixtures to underscore * Add fullname to fixtures * Move select2 to webpack. Change it to dynamic import * Remove activating select2 dropdown --- funnel/__init__.py | 1 - funnel/assets/js/form.js | 25 ++-- funnel/assets/js/submission_form.js | 1 - funnel/assets/js/utils/autocomplete_widget.js | 120 +++++++++++++++++ funnel/assets/js/utils/form_widgets.js | 121 +----------------- funnel/assets/sass/form.scss | 47 +++++++ funnel/assets/sass/mui/mixins/_util.scss | 2 +- funnel/assets/sass/pages/schedule.scss | 2 +- package-lock.json | 23 ++++ package.json | 1 + tests/cypress/e2e/08_addSubmission.cy.js | 5 +- tests/cypress/e2e/23_addSponsor.cy.js | 3 +- 12 files changed, 214 insertions(+), 137 deletions(-) create mode 100644 funnel/assets/js/utils/autocomplete_widget.js diff --git a/funnel/__init__.py b/funnel/__init__.py index 4192d2271..c895b92a8 100644 --- a/funnel/__init__.py +++ b/funnel/__init__.py @@ -135,7 +135,6 @@ requires=['funnel'], ext_requires=[ 'pygments', - 'select2-material', 'getdevicepixelratio', 'funnel-mui', ], diff --git a/funnel/assets/js/form.js b/funnel/assets/js/form.js index 963a834d6..b4d896ccd 100644 --- a/funnel/assets/js/form.js +++ b/funnel/assets/js/form.js @@ -1,18 +1,15 @@ /* global grecaptcha */ - -import { - activateFormWidgets, - EnableAutocompleteWidgets, - MapMarker, -} from './utils/form_widgets'; +import { activateFormWidgets, MapMarker } from './utils/form_widgets'; import Form from './utils/formhelper'; import 'htmx.org'; window.Hasgeek.initWidgets = async function init(fieldName, config) { switch (fieldName) { - case 'AutocompleteField': - EnableAutocompleteWidgets.textAutocomplete(config); + case 'AutocompleteField': { + const { default: widget } = await import('./utils/autocomplete_widget'); + widget.textAutocomplete(config); break; + } case 'ImgeeField': window.addEventListener('message', (event) => { if (event.origin === config.host) { @@ -29,12 +26,16 @@ window.Hasgeek.initWidgets = async function init(fieldName, config) { $(`#imgee-loader-${config.fieldId}`).addClass('mui--hide'); }); break; - case 'UserSelectField': - EnableAutocompleteWidgets.lastuserAutocomplete(config); + case 'UserSelectField': { + const { default: lastUserWidget } = await import('./utils/autocomplete_widget'); + lastUserWidget.lastuserAutocomplete(config); break; - case 'GeonameSelectField': - EnableAutocompleteWidgets.geonameAutocomplete(config); + } + case 'GeonameSelectField': { + const { default: geonameWidget } = await import('./utils/autocomplete_widget'); + geonameWidget.geonameAutocomplete(config); break; + } case 'CoordinatesField': /* eslint-disable no-new */ await import('jquery-locationpicker'); diff --git a/funnel/assets/js/submission_form.js b/funnel/assets/js/submission_form.js index ca6fbb43d..61ff6aafb 100644 --- a/funnel/assets/js/submission_form.js +++ b/funnel/assets/js/submission_form.js @@ -79,7 +79,6 @@ $(() => { $('body').on($.modal.OPEN, '.modal', (event) => { event.preventDefault(); - $('select.select2').select2('open').trigger('select2:open'); const modalFormId = $('.modal').find('form').attr('id'); const url = Form.getActionUrl(modalFormId); const onSuccess = (responseData) => { diff --git a/funnel/assets/js/utils/autocomplete_widget.js b/funnel/assets/js/utils/autocomplete_widget.js new file mode 100644 index 000000000..0e59a4d8c --- /dev/null +++ b/funnel/assets/js/utils/autocomplete_widget.js @@ -0,0 +1,120 @@ +import 'select2'; + +const EnableAutocompleteWidgets = { + lastuserAutocomplete(options) { + const assembleUsers = function (users) { + return users.map((user) => { + return { id: user.buid, text: user.label }; + }); + }; + + $(`#${options.id}`).select2({ + placeholder: 'Search for a user', + multiple: options.multiple, + minimumInputLength: 2, + ajax: { + url: options.autocompleteEndpoint, + dataType: 'jsonp', + data(params) { + if ('clientId' in options) { + return { + q: params.term, + client_id: options.clientId, + session: options.sessionId, + }; + } + return { + q: params.term, + }; + }, + processResults(data) { + let users = []; + if (data.status === 'ok') { + users = assembleUsers(data.users); + } + return { more: false, results: users }; + }, + }, + }); + }, + textAutocomplete(options) { + $(`#${options.id}`).select2({ + placeholder: 'Type to select', + multiple: options.multiple, + minimumInputLength: 2, + ajax: { + url: options.autocompleteEndpoint, + dataType: 'json', + data(params, page) { + return { + q: params.term, + page, + }; + }, + processResults(data) { + return { + more: false, + results: data[options.key].map((item) => { + return { id: item, text: item }; + }), + }; + }, + }, + }); + }, + geonameAutocomplete(options) { + $(options.selector).select2({ + placeholder: 'Search for a location', + multiple: true, + minimumInputLength: 2, + ajax: { + url: options.autocompleteEndpoint, + dataType: 'jsonp', + data(params) { + return { + q: params.term, + }; + }, + processResults(data) { + const rdata = []; + if (data.status === 'ok') { + for (let i = 0; i < data.result.length; i += 1) { + rdata.push({ + id: data.result[i].geonameid, + text: data.result[i].picker_title, + }); + } + } + return { more: false, results: rdata }; + }, + }, + }); + + // Setting label for Geoname ids + let val = $(options.selector).val(); + if (val) { + val = val.map((id) => { + return `name=${id}`; + }); + const qs = val.join('&'); + $.ajax(`${options.getnameEndpoint}?${qs}`, { + accepts: 'application/json', + dataType: 'jsonp', + }).done((data) => { + $(options.selector).empty(); + const rdata = []; + if (data.status === 'ok') { + for (let i = 0; i < data.result.length; i += 1) { + $(options.selector).append( + `` + ); + rdata.push(data.result[i].geonameid); + } + $(options.selector).val(rdata).trigger('change'); + } + }); + } + }, +}; + +export default EnableAutocompleteWidgets; diff --git a/funnel/assets/js/utils/form_widgets.js b/funnel/assets/js/utils/form_widgets.js index 7efb8a33f..e298b7d3a 100644 --- a/funnel/assets/js/utils/form_widgets.js +++ b/funnel/assets/js/utils/form_widgets.js @@ -60,7 +60,7 @@ export const Widgets = { }; this.activateToggleSwitch(checkboxId, onSuccess); }, - activate_select2() { + activateSelect2Focus() { /* Upgrade to jquery 3.6 select2 autofocus isn't working. This is to fix that problem. select2/select2#5993 */ $(document).on('select2:open', () => { @@ -164,126 +164,9 @@ export async function activateFormWidgets() { ); } - Widgets.activate_select2(); + Widgets.activateSelect2Focus(); } -export const EnableAutocompleteWidgets = { - lastuserAutocomplete(options) { - const assembleUsers = function getUsers(users) { - return users.map((user) => { - return { id: user.buid, text: user.label }; - }); - }; - - $(`#${options.id}`).select2({ - placeholder: 'Search for a user', - multiple: options.multiple, - minimumInputLength: 2, - ajax: { - url: options.autocompleteEndpoint, - dataType: 'jsonp', - data(params) { - if ('clientId' in options) { - return { - q: params.term, - client_id: options.clientId, - session: options.sessionId, - }; - } - return { - q: params.term, - }; - }, - processResults(data) { - let users = []; - if (data.status === 'ok') { - users = assembleUsers(data.users); - } - return { more: false, results: users }; - }, - }, - }); - }, - textAutocomplete(options) { - $(`#${options.id}`).select2({ - placeholder: 'Type to select', - multiple: options.multiple, - minimumInputLength: 2, - ajax: { - url: options.autocompleteEndpoint, - dataType: 'json', - data(params, page) { - return { - q: params.term, - page, - }; - }, - processResults(data) { - return { - more: false, - results: data[options.key].map((item) => { - return { id: item, text: item }; - }), - }; - }, - }, - }); - }, - geonameAutocomplete(options) { - $(options.selector).select2({ - placeholder: 'Search for a location', - multiple: true, - minimumInputLength: 2, - ajax: { - url: options.autocompleteEndpoint, - dataType: 'jsonp', - data(params) { - return { - q: params.term, - }; - }, - processResults(data) { - const rdata = []; - if (data.status === 'ok') { - for (let i = 0; i < data.result.length; i += 1) { - rdata.push({ - id: data.result[i].geonameid, - text: data.result[i].picker_title, - }); - } - } - return { more: false, results: rdata }; - }, - }, - }); - - // Setting label for Geoname ids - let val = $(options.selector).val(); - if (val) { - val = val.map((id) => { - return `name=${id}`; - }); - const qs = val.join('&'); - $.ajax(`${options.getnameEndpoint}?${qs}`, { - accepts: 'application/json', - dataType: 'jsonp', - }).done((data) => { - $(options.selector).empty(); - const rdata = []; - if (data.status === 'ok') { - for (let i = 0; i < data.result.length; i += 1) { - $(options.selector).append( - `` - ); - rdata.push(data.result[i].geonameid); - } - $(options.selector).val(rdata).trigger('change'); - } - }); - } - }, -}; - export class MapMarker { constructor(field) { this.field = field; diff --git a/funnel/assets/sass/form.scss b/funnel/assets/sass/form.scss index 11fb2356c..d75e4317c 100644 --- a/funnel/assets/sass/form.scss +++ b/funnel/assets/sass/form.scss @@ -4,6 +4,7 @@ 'mui/form'; @import 'base/variable', 'base/typography', 'components/draggablebox', 'components/switch', 'components/codemirror'; +@import 'node_modules/select2/dist/css/select2'; // ============================================================================ // Form @@ -419,3 +420,49 @@ .field-toggle { display: none; } + +// ============================================================================ +// Select2 +// ============================================================================ + +.select2-hidden-accessible ~ .mui-select__menu { + display: none !important; +} + +.select2-container .select2-selection { + border: none; + border-radius: 0; + background-image: none; + background-color: transparent; + border-bottom: 1px solid #ccc; + box-shadow: none; +} + +.select2-container .select2-dropdown { + border: none; + border-radius: 0; + -webkit-box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); +} + +.select2-container.select2-container--focus .select2-selection, +.select2-container.select2-container--open .select2-selection { + box-shadow: none; + border: none; + border-bottom: 1px solid #ccc; +} + +.select2-container .select2-results__option--highlighted[aria-selected] { + background-color: #eee; + color: #1f2d3d; +} + +.select2-container .select2-selection--single .select2-selection__arrow { + background-color: transparent; + border: none; + background-image: none; +} diff --git a/funnel/assets/sass/mui/mixins/_util.scss b/funnel/assets/sass/mui/mixins/_util.scss index eff085687..27d70093d 100644 --- a/funnel/assets/sass/mui/mixins/_util.scss +++ b/funnel/assets/sass/mui/mixins/_util.scss @@ -187,7 +187,7 @@ b, strong { - font-weight: 700er; + font-weight: 700; } /** diff --git a/funnel/assets/sass/pages/schedule.scss b/funnel/assets/sass/pages/schedule.scss index d2d93bbb9..af218a8af 100644 --- a/funnel/assets/sass/pages/schedule.scss +++ b/funnel/assets/sass/pages/schedule.scss @@ -407,7 +407,7 @@ } .fc-event .fc-event-custom a { - font-weight: 700er; + font-weight: 700; color: #c33; font-size: 1.2em; } diff --git a/package-lock.json b/package-lock.json index f3008baec..c17b3feb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "po2json": "^1.0.0-beta-3", "prismjs": "^1.29.0", "ractive": "^0.8.0", + "select2": "^4.0.3", "sprintf-js": "^1.1.2", "timeago.js": "^4.0.2", "toastr": "^2.1.4", @@ -2822,6 +2823,14 @@ "ajv": "^6.9.1" } }, + "node_modules/almond": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/almond/-/almond-0.3.3.tgz", + "integrity": "sha512-Eh5QhyxrKnTI0OuGpwTRvzRrnu1NF3F2kbQJRwpXj/uMy0uycwqw2/RhdDrD1cBTD1JFFHFrxGIU8HQztowR0g==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -7206,6 +7215,11 @@ "resolved": "https://registry.npmjs.org/jquery-modal/-/jquery-modal-0.9.2.tgz", "integrity": "sha512-Bx6jTBuiUbdywriWd0UAZK9v7FKEDCOD5uRh47qd4coGvx+dG4w8cOGe4TG2OoR1dNrXn6Aqaeu8MAA+Oz7vOw==" }, + "node_modules/jquery-mousewheel": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", + "integrity": "sha512-GXhSjfOPyDemM005YCEHvzrEALhKDIswtxSHSR2e4K/suHVJKJxxRCGz3skPjNxjJjQa9AVSGGlYjv1M3VLIPg==" + }, "node_modules/jquery-textcomplete": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/jquery-textcomplete/-/jquery-textcomplete-1.8.5.tgz", @@ -9917,6 +9931,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/select2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.3.tgz", + "integrity": "sha512-VrrDb1ZYi+tkiqbjSQzgaQ6BcltynTfp/2gYrvV32TyvrqTOtiMzH3YlPlhtuO1y+1JDy9gc9vTznFhnsiRQVA==", + "dependencies": { + "almond": "~0.3.1", + "jquery-mousewheel": "~3.1.13" + } + }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", diff --git a/package.json b/package.json index 4655699a0..a17d8af1e 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "po2json": "^1.0.0-beta-3", "prismjs": "^1.29.0", "ractive": "^0.8.0", + "select2": "^4.0.3", "sprintf-js": "^1.1.2", "timeago.js": "^4.0.2", "toastr": "^2.1.4", diff --git a/tests/cypress/e2e/08_addSubmission.cy.js b/tests/cypress/e2e/08_addSubmission.cy.js index b2b7802fa..5e61ef9d9 100644 --- a/tests/cypress/e2e/08_addSubmission.cy.js +++ b/tests/cypress/e2e/08_addSubmission.cy.js @@ -30,6 +30,7 @@ describe('Add a new submission', () => { cy.get('a[data-cy="add-label"]').click(); cy.wait(1000); cy.get('fieldset').find('.listwidget').eq(0).find('input').eq(0).click(); + cy.get('fieldset').find('.listwidget').eq(1).find('input').eq(0).click(); cy.wait(2000); cy.get('a[data-cy="save"]:visible').click(); @@ -44,6 +45,7 @@ describe('Add a new submission', () => { cy.get('a[data-cy="add-collaborator-modal"]').click(); cy.get('a[data-cy="add-collaborator"]').click(); cy.wait('@get-collaborator-form'); + cy.get('.select2-selection__arrow').click({ multiple: true }); cy.get('.select2-search__field').type(usher.username, { force: true, }); @@ -52,7 +54,8 @@ describe('Add a new submission', () => { ); cy.get('.select2-results__option').contains(usher.username).click(); cy.get('.select2-results__options', { timeout: 10000 }).should('not.exist'); - cy.get('#field-label').type('Editor'); + cy.wait(1000); + cy.get('#field-label').click().type('Editor'); cy.get('.modal').find('button[data-cy="form-submit-btn"]:visible').click(); cy.wait('@add-collaborator'); cy.get('a.modal__close').click(); diff --git a/tests/cypress/e2e/23_addSponsor.cy.js b/tests/cypress/e2e/23_addSponsor.cy.js index 795880455..3ed08c242 100644 --- a/tests/cypress/e2e/23_addSponsor.cy.js +++ b/tests/cypress/e2e/23_addSponsor.cy.js @@ -30,13 +30,14 @@ describe('Add sponsor to project', () => { cy.get('.select2-results__option').contains(sponsor.name).click(); cy.get('.select2-results__options', { timeout: 10000 }).should('not.exist'); cy.get('button[data-cy="form-submit-btn"]').click(); + cy.wait(2000); cy.get('[data-cy="profile-link"]').contains(sponsor.title); cy.get('a[data-cy="edit-sponsor"]:visible').click(); cy.wait('@edit-sponsor-form'); cy.get('#is_promoted').click(); cy.get('button[data-cy="form-submit-btn"]').click(); - + cy.wait(2000); cy.get('[data-cy="sponsor-card"]').find('[data-cy="promoted"]').should('exist'); cy.get('a[data-cy="remove-sponsor"]:visible').click(); From 8926dc02812750fdc4c03c0566a1c0574f8f2483 Mon Sep 17 00:00:00 2001 From: Mitesh Ashar Date: Tue, 23 May 2023 12:17:56 +0530 Subject: [PATCH 011/175] Upgrade from psycopg2 to psycopg3 (#1718) Co-authored-by: Kiran Jonnalagadda --- instance/settings-sample.py | 4 ++-- instance/testing.py | 4 ++-- requirements/base.in | 2 +- requirements/base.txt | 35 ++++++++++++++++------------------- requirements/dev.txt | 6 +++--- requirements/test.txt | 9 +-------- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/instance/settings-sample.py b/instance/settings-sample.py index 969b49c2a..961766e92 100644 --- a/instance/settings-sample.py +++ b/instance/settings-sample.py @@ -13,9 +13,9 @@ #: Default domain (server name without port number) DEFAULT_DOMAIN = 'funnel.test' #: Database backend -SQLALCHEMY_DATABASE_URI = 'postgresql://host/database' +SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://host/database' SQLALCHEMY_BINDS = { - 'geoname': 'postgresql://host/geoname', + 'geoname': 'postgresql+psycopg://host/geoname', } #: Shortlink domain for SMS links (must be served via wsgi:shortlinkapp) SHORTLINK_DOMAIN = 'domain.tld' diff --git a/instance/testing.py b/instance/testing.py index a56f17fcc..ae2c4d539 100644 --- a/instance/testing.py +++ b/instance/testing.py @@ -8,9 +8,9 @@ SECRET_KEYS = ['testkey'] # nosec LASTUSER_SECRET_KEYS = ['testkey'] # nosec SITE_TITLE = 'Hasgeek' -SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/funnel_testing' +SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg://localhost/funnel_testing' SQLALCHEMY_BINDS = { - 'geoname': 'postgresql://localhost/geoname_testing', + 'geoname': 'postgresql+psycopg://localhost/geoname_testing', } SERVER_NAME = 'funnel.test:3002' SHORTLINK_DOMAIN = 'f.test:3002' diff --git a/requirements/base.in b/requirements/base.in index a11df6afd..c4a3c4013 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -40,7 +40,7 @@ passlib phonenumbers premailer progressbar2 -psycopg2-binary +psycopg[binary] pycountry Pygments pyIsEmail diff --git a/requirements/base.txt b/requirements/base.txt index 58dc88e23..50b64ac55 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:b18889a0dd028d4bed02dadaa4618b22db682e05 +# SHA1:55bc0fd12589ced1130010057bb44121288be5c2 # # 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.11.0 +alembic==1.11.1 # via # -r requirements/base.in # flask-migrate @@ -34,9 +34,7 @@ argon2-cffi-bindings==21.2.0 arrow==1.2.3 # via rq-dashboard async-timeout==4.0.2 - # via - # aiohttp - # redis + # via aiohttp attrs==23.1.0 # via aiohttp babel==2.12.1 @@ -59,9 +57,9 @@ blinker==1.6.2 # via # -r requirements/base.in # coaster -boto3==1.26.135 +boto3==1.26.138 # via -r requirements/base.in -botocore==1.29.135 +botocore==1.29.138 # via # boto3 # s3transfer @@ -183,7 +181,9 @@ geoip2==4.7.0 grapheme==0.6.0 # via baseframe greenlet==2.0.2 - # via -r requirements/base.in + # via + # -r requirements/base.in + # sqlalchemy html2text==2020.1.16 # via -r requirements/base.in html5lib==1.1 @@ -202,10 +202,6 @@ idna==3.4 # requests # tldextract # yarl -importlib-metadata==6.6.0 - # via - # flask - # markdown isoweek==1.3.3 # via coaster itsdangerous==2.1.2 @@ -294,8 +290,10 @@ premailer==3.10.0 # via -r requirements/base.in progressbar2==4.2.0 # via -r requirements/base.in -psycopg2-binary==2.9.6 +psycopg[binary]==3.1.9 # via -r requirements/base.in +psycopg-binary==3.1.9 + # via psycopg pyasn1==0.5.0 # via # baseframe @@ -377,7 +375,7 @@ regex==2022.10.31 # via # -r requirements/base.in # nltk -requests==2.30.0 +requests==2.31.0 # via # -r requirements/base.in # baseframe @@ -434,7 +432,7 @@ six==1.16.0 # requests-mock # sqlalchemy-json # tuspy -sqlalchemy==2.0.13 +sqlalchemy==2.0.15 # via # -r requirements/base.in # alembic @@ -453,7 +451,7 @@ statsd==4.0.1 # via baseframe tinydb==4.7.1 # via tuspy -tldextract==3.4.2 +tldextract==3.4.4 # via # coaster # mxsniff @@ -465,7 +463,7 @@ tuspy==1.0.0 # via pyvimeo tweepy==4.14.0 # via -r requirements/base.in -twilio==8.2.0 +twilio==8.2.1 # via -r requirements/base.in typing-extensions==4.5.0 # via @@ -473,6 +471,7 @@ typing-extensions==4.5.0 # alembic # baseframe # coaster + # psycopg # qrcode # sqlalchemy # typing-inspect @@ -515,8 +514,6 @@ wtforms-sqlalchemy==0.3 # via baseframe yarl==1.9.2 # via aiohttp -zipp==3.15.0 - # via importlib-metadata zxcvbn==4.4.28 # via -r requirements/base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index d6d3a329b..9642af52a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -108,7 +108,7 @@ platformdirs==3.5.1 # virtualenv po2json==0.2.2 # via -r requirements/dev.in -pre-commit==3.3.1 +pre-commit==3.3.2 # via -r requirements/dev.in pycodestyle==2.10.0 # via @@ -126,7 +126,7 @@ pyupgrade==3.4.0 # via -r requirements/dev.in reformat-gherkin==3.0.1 # via -r requirements/dev.in -ruff==0.0.267 +ruff==0.0.269 # via -r requirements/dev.in smmap==5.0.0 # via gitdb @@ -170,7 +170,7 @@ types-redis==4.5.5.2 # via -r requirements/dev.in types-requests==2.30.0.0 # via -r requirements/dev.in -types-setuptools==67.7.0.2 +types-setuptools==67.8.0.0 # via -r requirements/dev.in types-six==1.16.21.8 # via -r requirements/dev.in diff --git a/requirements/test.txt b/requirements/test.txt index ba23ef584..a97d1740a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -28,10 +28,7 @@ coveralls==3.3.1 docopt==0.6.2 # via coveralls exceptiongroup==1.1.1 - # via - # pytest - # trio - # trio-websocket + # via trio-websocket h11==0.14.0 # via wsproto iniconfig==2.0.0 @@ -96,10 +93,6 @@ sttable==0.0.1 # via -r requirements/test.in tenacity==8.2.2 # via pytest-selenium -tomli==2.0.1 - # via - # coverage - # pytest tomlkit==0.11.8 # via -r requirements/test.in trio==0.22.0 From 1ea460439bf3f22c921885512d37f9af05f77c2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 12:26:04 +0530 Subject: [PATCH 012/175] Bump select2 from 4.0.3 to 4.0.6 (#1719) Bumps [select2](https://github.com/select2/select2) from 4.0.3 to 4.0.6. - [Release notes](https://github.com/select2/select2/releases) - [Changelog](https://github.com/select2/select2/blob/develop/CHANGELOG.md) - [Commits](https://github.com/select2/select2/compare/4.0.3...4.0.6) --- updated-dependencies: - dependency-name: select2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c17b3feb6..50614cbfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "po2json": "^1.0.0-beta-3", "prismjs": "^1.29.0", "ractive": "^0.8.0", - "select2": "^4.0.3", + "select2": "^4.0.6", "sprintf-js": "^1.1.2", "timeago.js": "^4.0.2", "toastr": "^2.1.4", @@ -9932,9 +9932,9 @@ } }, "node_modules/select2": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.3.tgz", - "integrity": "sha512-VrrDb1ZYi+tkiqbjSQzgaQ6BcltynTfp/2gYrvV32TyvrqTOtiMzH3YlPlhtuO1y+1JDy9gc9vTznFhnsiRQVA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.6.tgz", + "integrity": "sha512-P5NqUcH+FFBwHAbC2mEWMDS446YdIGvXRM9tSGFLuNIFRP/ng0IOtjJ4zkzVyCl43RM/NyXJa6SAPT9MGg634A==", "dependencies": { "almond": "~0.3.1", "jquery-mousewheel": "~3.1.13" diff --git a/package.json b/package.json index a17d8af1e..7848f6034 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "po2json": "^1.0.0-beta-3", "prismjs": "^1.29.0", "ractive": "^0.8.0", - "select2": "^4.0.3", + "select2": "^4.0.6", "sprintf-js": "^1.1.2", "timeago.js": "^4.0.2", "toastr": "^2.1.4", From b135873e4f3117dc5d9eef8fe97e548571a412ee Mon Sep 17 00:00:00 2001 From: Vidya Ramakrishnan Date: Thu, 25 May 2023 11:26:50 +0530 Subject: [PATCH 013/175] Upgrade to jquery3.7 (#1721) * Upgrade to jquery3.7. Remove pygments. Downgrade select2 to 4.0.3 since 4.0.6 has issues * use request.endpoint instead current_view.current_handler * Fix indentations --- funnel/__init__.py | 1 - funnel/assets/js/utils/form_widgets.js | 9 - funnel/templates/layout.html.jinja2 | 641 +- funnel/templates/project_layout.html.jinja2 | 3 +- package-lock.json | 9340 ++++++++++++++++++- package.json | 2 +- 6 files changed, 9587 insertions(+), 409 deletions(-) diff --git a/funnel/__init__.py b/funnel/__init__.py index c895b92a8..6adf75ce8 100644 --- a/funnel/__init__.py +++ b/funnel/__init__.py @@ -134,7 +134,6 @@ app, requires=['funnel'], ext_requires=[ - 'pygments', 'getdevicepixelratio', 'funnel-mui', ], diff --git a/funnel/assets/js/utils/form_widgets.js b/funnel/assets/js/utils/form_widgets.js index e298b7d3a..ed0065d27 100644 --- a/funnel/assets/js/utils/form_widgets.js +++ b/funnel/assets/js/utils/form_widgets.js @@ -60,13 +60,6 @@ export const Widgets = { }; this.activateToggleSwitch(checkboxId, onSuccess); }, - activateSelect2Focus() { - /* Upgrade to jquery 3.6 select2 autofocus isn't working. This is to fix that problem. - select2/select2#5993 */ - $(document).on('select2:open', () => { - document.querySelector('.select2-search__field').focus(); - }); - }, handleDelete(elementClass, onSucessFn) { $('body').on('click', elementClass, async function remove(event) { event.preventDefault(); @@ -163,8 +156,6 @@ export async function activateFormWidgets() { } ); } - - Widgets.activateSelect2Focus(); } export class MapMarker { diff --git a/funnel/templates/layout.html.jinja2 b/funnel/templates/layout.html.jinja2 index ee282d76d..8ee997d4f 100644 --- a/funnel/templates/layout.html.jinja2 +++ b/funnel/templates/layout.html.jinja2 @@ -23,351 +23,358 @@ {%- endblock titletags %} - - - - - - - - - - - - {% if csrf_token -%} - - - {%- endif %} - - {%- block canonical_url %} - - - {%- endblock canonical_url %} - {%- block image_src %} - {%- if project and project.bg_image.url %} - - - - {%- elif project and project.profile and project.profile.logo_url.url %} - - - - {%- elif profile and profile.logo_url.url %} - - - - {%- else %} - - - - {% endif -%} - {%- endblock image_src %} - - {# Repeat manifest in upper case for adblock-type filters -#} - {# djlint:off #}{# djlint:on #} - - - - - - - {%- block font_icons %} - {%- endblock font_icons %} - {%- for asset in config.get('ext_css', []) %} - - {%- endfor %} - {% assets "css_all" -%} - - {%- endassets %} - {% block layoutheaders %} - - {% endblock layoutheaders %} - {% block pageheaders %} - {% endblock pageheaders %} - - - {% block headerbox -%} -
-
- {% block header -%} - + {%- endblock header %} +
+
+ {%- endblock headerbox %} + + {% block contentbox -%} +
+
+ {% block contenthead %} + {% block headline -%} +
+
+
+
+ {% block top_title %} +

{{ self.title()|e }}

+ {% endblock top_title %}
- {%- endblock headline %} - {% endblock contenthead %} - {%- block basecontentbox %} - {%- block messages %} - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
-
- {% for category, message in messages %}{{ alertbox(category, message) }}{% endfor %} -
+
+ {%- endblock headline %} + {% endblock contenthead %} + {%- block basecontentbox %} + {%- block messages %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+
+ {% for category, message in messages %}{{ alertbox(category, message) }}{% endfor %}
- {% endif %} - {% endwith %} - {% endblock messages %} - {%- block baseheadline %} - {% endblock baseheadline %} - {% block basecontent %} -
-
- {% block contentwrapper %} -
-
- {% block content %} - {% endblock content %} -
-
- {% endblock contentwrapper %}
+ {% endif %} + {% endwith %} + {% endblock messages %} + {%- block baseheadline %} + {% endblock baseheadline %} + {% block basecontent %} +
+
+ {% block contentwrapper %} +
+
+ {% block content %} + {% endblock content %} +
+
+ {% endblock contentwrapper %}
- {% endblock basecontent %} - {% endblock basecontentbox %} -
+
+ {% endblock basecontent %} + {% endblock basecontentbox %}
- {%- endblock contentbox %} - {% block basefooter -%} - {%- endblock basefooter %} - +
+ {%- endblock contentbox %} + + {% block basefooter -%} + {%- endblock basefooter %} + + + + {%- for asset in config.get('ext_js', []) %} + + {%- endfor %} + + {#- This block is to include JS assets of the app that are not required on all the pages but has to be included before baseframe bundle(assets "js_all") #} + {% block pagescripts %} + {% endblock pagescripts %} + + {% assets "js_all" -%} + + {%- endassets -%} + + {%- if config['MATOMO_URL'] and config['MATOMO_ID'] and not config['DEBUG'] -%} + + {%- endif -%} + + {%- if config['GA_CODE'] and not config['DEBUG'] -%} + + {%- endif -%} + + + + + {% block serviceworker %} -{%- for asset in config.get('ext_js', []) %} - -{%- endfor %} -{#- This block is to include JS assets of the app that are not required on all the pages but has to be included before baseframe bundle(assets "js_all") #} -{% block pagescripts %} -{% endblock pagescripts %} -{% assets "js_all" -%} - -{%- endassets -%} -{%- if config['MATOMO_URL'] and config['MATOMO_ID'] and not config['DEBUG'] -%} - -{%- endif -%} -{%- if config['GA_CODE'] and not config['DEBUG'] -%} - -{%- endif -%} - - -{% block serviceworker %} - + {% endblock serviceworker %} - // Setup a listener to track Add to Homescreen events. - window.addEventListener('beforeinstallprompt', event => { - event.userChoice.then(choice => { - if (window.ga) { - window.ga('send', 'event', 'Add to Home', choice.outcome); - } - }); - }); - } - -{% endblock serviceworker %} -{% block footerscripts %} -{% endblock footerscripts %} - + {% block footerscripts %} + {% endblock footerscripts %} + diff --git a/funnel/templates/project_layout.html.jinja2 b/funnel/templates/project_layout.html.jinja2 index dce64abbb..f8ba7a379 100644 --- a/funnel/templates/project_layout.html.jinja2 +++ b/funnel/templates/project_layout.html.jinja2 @@ -174,8 +174,7 @@ {% endif %}
{% if project.features.tickets() %} -
- {% if project.features.subscription %}{% trans %}Like this? Support the community{% endtrans %}{% endif %} +
@@ -68,7 +68,7 @@ {% macro sociallogin(login_registry) %}
diff --git a/funnel/templates/profile.html.jinja2 b/funnel/templates/profile.html.jinja2 index 4158436e6..792dc030c 100644 --- a/funnel/templates/profile.html.jinja2 +++ b/funnel/templates/profile.html.jinja2 @@ -73,52 +73,6 @@ {%- endwith %} {%- endif %} - {% if sponsored_projects %} -
-
-
-
-

{% trans %}Supported projects{% endtrans %}

-
-
-
    - {% for project in sponsored_projects %} -
  • - {{ projectcard(project, save_form_id_prefix='support_pro_') }} -
  • - {% endfor %} -
- {% if sponsored_projects|length > 6 %} - - {% endif %} -
-
- {% endif %} - - {% if sponsored_submissions %} -
-
-
-
-

{% trans %}Supported submissions{% endtrans %}

-
-
-
    - {% for proposal in sponsored_submissions %} -
  • {{ proposal_card(proposal, full_width=true, details=true, css_class=css_class, draggable=false, spa=false, show_sponsor=false) }}
  • - {% endfor %} -
- {% if sponsored_submissions|length > 4 %} - - {% endif %} -
-
- {% endif %} - {%- if profile.features.new_project() or draft_projects %}
@@ -189,6 +143,53 @@ {{ upcoming_section(upcoming_projects) }} {{ open_cfp_section(open_cfp_projects) }} {{ all_projects_section(all_projects) }} + + {% if sponsored_projects %} +
+
+
+
+

{% trans %}Supported projects{% endtrans %}

+
+
+
    + {% for project in sponsored_projects %} +
  • + {{ projectcard(project, save_form_id_prefix='support_pro_') }} +
  • + {% endfor %} +
+ {% if sponsored_projects|length > 6 %} + + {% endif %} +
+
+ {% endif %} + + {% if sponsored_submissions %} +
+
+
+
+

{% trans %}Supported submissions{% endtrans %}

+
+
+
    + {% for proposal in sponsored_submissions %} +
  • {{ proposal_card(proposal, full_width=true, details=true, css_class=css_class, draggable=false, spa=false, show_sponsor=false) }}
  • + {% endfor %} +
+ {% if sponsored_submissions|length > 4 %} + + {% endif %} +
+
+ {% endif %} + {{ past_projects_section(profile) }}
{% endblock basecontent %} From db46815b70797033e67fef9cae49f9e32376daac Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Thu, 29 Jun 2023 12:38:55 +0530 Subject: [PATCH 057/175] Configure Notification subtypes from type hints (#1772) --- funnel/models/notification.py | 45 +++++++++++-- funnel/models/notification_types.py | 69 ++++++++------------ tests/unit/models/merge_notification_test.py | 6 +- tests/unit/models/notification_test.py | 15 ++--- 4 files changed, 77 insertions(+), 58 deletions(-) diff --git a/funnel/models/notification.py b/funnel/models/notification.py index 26549c8d7..0114c0762 100644 --- a/funnel/models/notification.py +++ b/funnel/models/notification.py @@ -100,7 +100,7 @@ Union, cast, ) -from typing_extensions import Protocol +from typing_extensions import Protocol, get_args, get_origin, get_original_bases from uuid import UUID, uuid4 from sqlalchemy import event @@ -155,7 +155,9 @@ # Document generic type _D = TypeVar('_D', bound=UuidModelUnion) # Fragment generic type -_F = TypeVar('_F', bound=UuidModelUnion) +_F = TypeVar('_F', bound=Optional[UuidModelUnion]) +# Type of None (required to detect Optional) +NoneType = type(None) # --- Registries ----------------------------------------------------------------------- @@ -481,6 +483,39 @@ def __init_subclass__( # pylint: disable=arguments-differ cls.__mapper_args__ = {} cls.__mapper_args__['polymorphic_identity'] = type + # Get document and fragment models from type hints + for base in get_original_bases(cls): + if get_origin(base) is Notification: + document_model, fragment_model = get_args(base) + if fragment_model is NoneType: + fragment_model = None + elif get_origin(fragment_model) is Optional: + fragment_model = get_args(fragment_model)[0] + elif get_origin(fragment_model) is Union: + _union_args = get_args(fragment_model) + if len(_union_args) == 2 and _union_args[1] is NoneType: + fragment_model = _union_args[0] + else: + raise TypeError( + f"Unsupported notification fragment: {fragment_model}" + ) + if 'document_model' in cls.__dict__: + if cls.document_model != document_model: + raise TypeError(f"{cls} has a conflicting document_model") + else: + cls.document_model = document_model + if 'fragment_model' in cls.__dict__: + if cls.fragment_model != fragment_model: + raise TypeError(f"{cls} has a conflicting fragment_model") + else: + cls.fragment_model = fragment_model + break + + cls.document_type = cls.document_model.__tablename__ + cls.fragment_type = ( + cls.fragment_model.__tablename__ if cls.fragment_model else None + ) + # For notification type identification and preference management cls.cls_type = type if shadows is not None: @@ -1409,16 +1444,12 @@ def main_notification_preferences(self) -> NotificationPreferences: def _register_notification_types(mapper_: Any, cls: Type[Notification]) -> None: # Don't register the base class itself, or inactive types if cls is not Notification: - # Populate cls with helper attributes + # Add the subclass to the registry if cls.document_model is None: raise TypeError( f"Notification subclass {cls!r} must specify document_model" ) - cls.document_type = cls.document_model.__tablename__ - cls.fragment_type = ( - cls.fragment_model.__tablename__ if cls.fragment_model else None - ) # Exclude inactive notifications in the registry. It is used to populate the # user's notification preferences screen. diff --git a/funnel/models/notification_types.py b/funnel/models/notification_types.py index 30d771098..abb32fb09 100644 --- a/funnel/models/notification_types.py +++ b/funnel/models/notification_types.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Optional + from baseframe import __ from .comment import Comment, Commentset @@ -58,14 +60,13 @@ def preference_context(self) -> Profile: # --- Account notifications ------------------------------------------------------------ -class AccountPasswordNotification(Notification, type='user_password_set'): +class AccountPasswordNotification(Notification[User, None], type='user_password_set'): """Notification when the user's password changes.""" category = notification_categories.account title = __("When my account password changes") description = __("For your safety, in case this was not authorized") - document_model = User exclude_actor = False roles = ['owner'] for_private_recipient = True @@ -75,7 +76,7 @@ class AccountPasswordNotification(Notification, type='user_password_set'): class RegistrationConfirmationNotification( - DocumentHasProject, Notification, type='rsvp_yes' + DocumentHasProject, Notification[Rsvp, None], type='rsvp_yes' ): """Notification confirming registration to a project.""" @@ -83,7 +84,6 @@ class RegistrationConfirmationNotification( title = __("When I register for a project") description = __("This will prompt a calendar entry in Gmail and other apps") - document_model = Rsvp roles = ['owner'] exclude_actor = False # This is a notification to the actor for_private_recipient = True @@ -91,20 +91,21 @@ class RegistrationConfirmationNotification( class RegistrationCancellationNotification( DocumentHasProject, - Notification, + Notification[Rsvp, None], type='rsvp_no', shadows=RegistrationConfirmationNotification, ): """Notification confirming cancelling registration to a project.""" - document_model = Rsvp roles = ['owner'] exclude_actor = False # This is a notification to the actor for_private_recipient = True allow_web = False -class NewUpdateNotification(DocumentHasProject, Notification, type='update_new'): +class NewUpdateNotification( + DocumentHasProject, Notification[Update, None], type='update_new' +): """Notifications of new updates.""" category = notification_categories.participant @@ -113,13 +114,12 @@ class NewUpdateNotification(DocumentHasProject, Notification, type='update_new') "Typically contains critical information such as video conference links" ) - document_model = Update roles = ['project_crew', 'project_participant', 'account_participant'] exclude_actor = False # Send to everyone including the actor class ProposalSubmittedNotification( - DocumentHasProject, Notification, type='proposal_submitted' + DocumentHasProject, Notification[Proposal, None], type='proposal_submitted' ): """Notification to the proposer on a successful proposal submission.""" @@ -127,7 +127,6 @@ class ProposalSubmittedNotification( title = __("When I submit a proposal") description = __("Confirmation for your records") - document_model = Proposal roles = ['creator'] exclude_actor = False # This notification is for the actor @@ -140,7 +139,9 @@ class ProposalSubmittedNotification( class ProjectStartingNotification( - DocumentHasProfile, Notification, type='project_starting' + DocumentHasProfile, + Notification[Project, Optional[Session]], + type='project_starting', ): """Notification of a session about to start.""" @@ -148,8 +149,6 @@ class ProjectStartingNotification( title = __("When a project I’ve registered for is about to start") description = __("You will be notified 5-10 minutes before the starting time") - document_model = Project - fragment_model = Session roles = ['project_crew', 'project_participant'] # This is a notification triggered without an actor @@ -157,27 +156,25 @@ class ProjectStartingNotification( # --- Comment notifications ------------------------------------------------------------ -class NewCommentNotification(Notification, type='comment_new'): +class NewCommentNotification(Notification[Commentset, Comment], type='comment_new'): """Notification of new comment.""" category = notification_categories.participant title = __("When there is a new comment on something I’m involved in") exclude_actor = True - document_model = Commentset - fragment_model = Comment roles = ['replied_to_commenter', 'document_subscriber'] -class CommentReplyNotification(Notification, type='comment_reply'): +class CommentReplyNotification(Notification[Comment, Comment], type='comment_reply'): """Notification of comment replies and mentions.""" category = notification_categories.participant title = __("When someone replies to my comment or mentions me") exclude_actor = True - document_model = Comment # Parent comment (being replied to) - fragment_model = Comment # Child comment (the reply that triggered notification) + # document_model = Parent comment (being replied to) + # fragment_model = Child comment (the reply that triggered notification) roles = ['replied_to_commenter'] @@ -185,7 +182,9 @@ class CommentReplyNotification(Notification, type='comment_reply'): class ProjectCrewMembershipNotification( - DocumentHasProfile, Notification, type='project_crew_membership_granted' + DocumentHasProfile, + Notification[Project, ProjectCrewMembership], + type='project_crew_membership_granted', ): """Notification of being granted crew membership (including role changes).""" @@ -193,42 +192,36 @@ class ProjectCrewMembershipNotification( title = __("When a project crew member is added or removed") description = __("Crew members have access to the project’s settings and data") - document_model = Project - fragment_model = ProjectCrewMembership roles = ['subject', 'project_crew'] exclude_actor = True # Alerts other users of actor's actions; too noisy for actor class ProjectCrewMembershipRevokedNotification( DocumentHasProfile, - Notification, + Notification[Project, ProjectCrewMembership], type='project_crew_membership_revoked', shadows=ProjectCrewMembershipNotification, ): """Notification of being removed from crew membership (including role changes).""" - document_model = Project - fragment_model = ProjectCrewMembership roles = ['subject', 'project_crew'] exclude_actor = True # Alerts other users of actor's actions; too noisy for actor class ProposalReceivedNotification( - DocumentHasProfile, Notification, type='proposal_received' + DocumentHasProfile, Notification[Project, Proposal], type='proposal_received' ): """Notification to editors of new proposals.""" category = notification_categories.project_crew title = __("When my project receives a new proposal") - document_model = Project - fragment_model = Proposal roles = ['project_editor'] exclude_actor = True # Don't notify editor of proposal they submitted class RegistrationReceivedNotification( - DocumentHasProfile, Notification, type='rsvp_received' + DocumentHasProfile, Notification[Project, Rsvp], type='rsvp_received' ): """Notification to promoters of new registrations.""" @@ -237,8 +230,6 @@ class RegistrationReceivedNotification( category = notification_categories.project_crew title = __("When someone registers for my project") - document_model = Project - fragment_model = Rsvp roles = ['project_promoter'] exclude_actor = True @@ -247,7 +238,9 @@ class RegistrationReceivedNotification( class OrganizationAdminMembershipNotification( - DocumentHasProfile, Notification, type='organization_membership_granted' + DocumentHasProfile, + Notification[Organization, OrganizationMembership], + type='organization_membership_granted', ): """Notification of being granted admin membership (including role changes).""" @@ -255,22 +248,18 @@ class OrganizationAdminMembershipNotification( title = __("When account admins change") description = __("Account admins control all projects under the account") - document_model = Organization - fragment_model = OrganizationMembership roles = ['subject', 'profile_admin'] exclude_actor = True # Alerts other users of actor's actions; too noisy for actor class OrganizationAdminMembershipRevokedNotification( DocumentHasProfile, - Notification, + Notification[Organization, OrganizationMembership], type='organization_membership_revoked', shadows=OrganizationAdminMembershipNotification, ): """Notification of being granted admin membership (including role changes).""" - document_model = Organization - fragment_model = OrganizationMembership roles = ['subject', 'profile_admin'] exclude_actor = True # Alerts other users of actor's actions; too noisy for actor @@ -278,12 +267,12 @@ class OrganizationAdminMembershipRevokedNotification( # --- Site administrator notifications ------------------------------------------------- -class CommentReportReceivedNotification(Notification, type='comment_report_received'): +class CommentReportReceivedNotification( + Notification[Comment, CommentModeratorReport], type='comment_report_received' +): """Notification for comment moderators when a comment is reported as spam.""" category = notification_categories.site_admin title = __("When a comment is reported as spam") - document_model = Comment - fragment_model = CommentModeratorReport roles = ['comment_moderator'] diff --git a/tests/unit/models/merge_notification_test.py b/tests/unit/models/merge_notification_test.py index 973608aea..9173c5c95 100644 --- a/tests/unit/models/merge_notification_test.py +++ b/tests/unit/models/merge_notification_test.py @@ -13,11 +13,11 @@ @pytest.fixture(scope='session') def fixture_notification_type(database) -> Any: - class MergeTestNotification(models.Notification, type='merge_test'): + class MergeTestNotification( + models.Notification[models.User, None], type='merge_test' + ): """Test notification.""" - document_model = models.User - database.configure_mappers() return MergeTestNotification diff --git a/tests/unit/models/notification_test.py b/tests/unit/models/notification_test.py index 1aa85f8aa..d840def17 100644 --- a/tests/unit/models/notification_test.py +++ b/tests/unit/models/notification_test.py @@ -26,37 +26,36 @@ def preference_context(self) -> models.Profile: return self.document.project.profile class TestNewUpdateNotification( - ProjectIsParent, models.Notification, type='update_new_test' + ProjectIsParent, + models.Notification[models.Update, None], + type='update_new_test', ): """Notifications of new updates (test edition).""" category = models.notification_categories.participant description = "When a project posts an update" - document_model = models.Update roles = ['project_crew', 'project_participant'] class TestEditedUpdateNotification( ProjectIsParent, - models.Notification, + models.Notification[models.Update, None], type='update_edit_test', shadows=TestNewUpdateNotification, ): """Notifications of edited updates (test edition).""" - document_model = models.Update roles = ['project_crew', 'project_participant'] class TestProposalReceivedNotification( - ProjectIsParent, models.Notification, type='proposal_received_test' + ProjectIsParent, + models.Notification[models.Project, models.Proposal], + type='proposal_received_test', ): """Notifications of new proposals (test edition).""" category = models.notification_categories.project_crew description = "When my project receives a new proposal" - - document_model = models.Project - fragment_model = models.Proposal roles = ['project_editor'] database.configure_mappers() From 7ccd47898dd1611b7709f837d1a818b957e985a7 Mon Sep 17 00:00:00 2001 From: Amogh M Aradhya Date: Thu, 29 Jun 2023 12:58:05 +0530 Subject: [PATCH 058/175] SMS templates for submissions (#1771) Co-authored-by: Kiran Jonnalagadda --- .../notifications/proposal_notification.py | 71 ++++++++++++++----- sample.env | 2 + 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/funnel/views/notifications/proposal_notification.py b/funnel/views/notifications/proposal_notification.py index 5df16ca81..3f3dcc03b 100644 --- a/funnel/views/notifications/proposal_notification.py +++ b/funnel/views/notifications/proposal_notification.py @@ -13,9 +13,47 @@ ProposalSubmittedNotification, sa, ) -from ...transports.sms import TwoLineTemplate +from ...transports.sms import SmsTemplate from ..helpers import shortlink from ..notification import RenderNotification +from .mixins import ProjectTemplateMixin + + +class ProposalReceivedTemplate(ProjectTemplateMixin, SmsTemplate): + """DLT registered template for RSVP without a next session.""" + + registered_template = ( + "There's a new submission from {#var#} in {#var#}." + " Read it here: {#var#}\n\nhttps://bye.li to stop -Hasgeek" + ) + template = ( + "There's a new submission from {actor} in {project_title}." + " Read it here: {url}\n\nhttps://bye.li to stop -Hasgeek" + ) + plaintext_template = ( + "There's a new submission from {actor} in {project_title}. Read it here: {url}" + ) + + actor: str + url: str + + +class ProposalSubmittedTemplate(ProjectTemplateMixin, SmsTemplate): + """DLT registered template for RSVP without a next session.""" + + registered_template = ( + "{#var#} has received your submission. Here's the link to share: {#var#}" + "\n\nhttps://bye.li to stop -Hasgeek" + ) + template = ( + "{project_title} has received your submission. Here's the link to share: {url}" + "\n\nhttps://bye.li to stop -Hasgeek" + ) + plaintext_template = ( + "{project_title} has received your submission. Here's the link to share: {url}" + ) + + url: str @ProposalReceivedNotification.renderer @@ -35,7 +73,7 @@ class RenderProposalReceivedNotification(RenderNotification): ) ] - def web(self): + def web(self) -> str: return render_template( 'notifications/proposal_received_web.html.jinja2', view=self, @@ -43,12 +81,12 @@ def web(self): project=self.project, ) - def email_subject(self): + def email_subject(self) -> str: return self.emoji_prefix + _("New submission in {project}: {proposal}").format( proposal=self.proposal.title, project=self.project.joined_title ) - def email_content(self): + def email_content(self) -> str: return render_template( 'notifications/proposal_received_email.html.jinja2', view=self, @@ -56,12 +94,10 @@ def email_content(self): project=self.project, ) - def sms(self) -> TwoLineTemplate: - return TwoLineTemplate( - text1=_("New submission in {project}:").format( - project=self.project.joined_title - ), - text2=self.proposal.title, + def sms(self) -> ProposalReceivedTemplate: + return ProposalReceivedTemplate( + project=self.project, + actor=self.proposal.user.pickername, url=shortlink( self.proposal.url_for(_external=True, **self.tracking_tags('sms')), shorter=True, @@ -78,7 +114,7 @@ class RenderProposalSubmittedNotification(RenderNotification): emoji_prefix = "📤 " reason = __("You are receiving this because you made this submission") - def web(self): + def web(self) -> str: return render_template( 'notifications/proposal_submitted_web.html.jinja2', view=self, @@ -86,12 +122,12 @@ def web(self): project=self.proposal.project, ) - def email_subject(self): + def email_subject(self) -> str: return self.emoji_prefix + _("Submission made to {project}: {proposal}").format( project=self.proposal.project.joined_title, proposal=self.proposal.title ) - def email_content(self): + def email_content(self) -> str: return render_template( 'notifications/proposal_submitted_email.html.jinja2', view=self, @@ -99,12 +135,9 @@ def email_content(self): project=self.proposal.project, ) - def sms(self) -> TwoLineTemplate: - return TwoLineTemplate( - text1=_("Your submission has been received in {project}:").format( - project=self.proposal.project.joined_title - ), - text2=self.proposal.title, + def sms(self) -> ProposalSubmittedTemplate: + return ProposalSubmittedTemplate( + project=self.proposal.project, url=shortlink( self.proposal.url_for(_external=True, **self.tracking_tags('sms')), shorter=True, diff --git a/sample.env b/sample.env index cf51cafce..def9bcc11 100644 --- a/sample.env +++ b/sample.env @@ -185,3 +185,5 @@ FLASK_SMS_DLT_TEMPLATE_IDS__web_otp_template=null FLASK_SMS_DLT_TEMPLATE_IDS__project_starting_template=null FLASK_SMS_DLT_TEMPLATE_IDS__registration_confirmation_template=null FLASK_SMS_DLT_TEMPLATE_IDS__registration_confirmation_with_next_template=null +FLASK_SMS_DLT_TEMPLATE_IDS__proposal_received_template=null +FLASK_SMS_DLT_TEMPLATE_IDS__proposal_submitted_template=null From 915e1ce71deedf6d77a163090450bdda84b2371e Mon Sep 17 00:00:00 2001 From: Vidya Ramakrishnan Date: Fri, 30 Jun 2023 00:38:59 +0530 Subject: [PATCH 059/175] Support customizable registration forms (#1670) Co-authored-by: Kiran Jonnalagadda Co-authored-by: Amogh M Aradhya --- funnel/assets/js/project_header.js | 13 +- funnel/assets/js/rsvp_form_modal.js | 33 +++ .../assets/js/utils/codemirror_stylesheet.js | 51 +++++ funnel/assets/js/utils/form_widgets.js | 29 ++- funnel/assets/js/utils/formhelper.js | 8 +- funnel/assets/js/utils/jsonform.js | 51 +++++ funnel/assets/sass/form.scss | 3 +- funnel/forms/auth_client.py | 4 +- funnel/forms/helpers.py | 25 ++- funnel/forms/project.py | 33 +++ funnel/forms/sync_ticket.py | 49 ++++- funnel/models/rsvp.py | 22 +- funnel/templates/account.html.jinja2 | 4 +- .../templates/account_formlayout.html.jinja2 | 2 +- funnel/templates/js/json_form.js.jinja2 | 50 +++++ funnel/templates/login.html.jinja2 | 2 +- funnel/templates/project_layout.html.jinja2 | 24 +-- .../templates/project_rsvp_list.html.jinja2 | 10 + funnel/templates/rsvp_modal.html.jinja2 | 31 +++ funnel/views/project.py | 65 +++++- package-lock.json | 1 + package.json | 1 + requirements/base.txt | 26 +-- requirements/dev.txt | 12 +- requirements/test.in | 1 + requirements/test.txt | 10 +- tests/cypress/e2e/08_addSubmission.cy.js | 4 +- tests/cypress/e2e/11_respondToAttend.cy.js | 1 + tests/e2e/account/register_test.py | 80 +++++--- tests/unit/forms/rsvp_json_test.py | 87 ++++++++ tests/unit/views/rsvp_test.py | 190 ++++++++++++++++++ webpack.config.js | 1 + 32 files changed, 812 insertions(+), 111 deletions(-) create mode 100644 funnel/assets/js/rsvp_form_modal.js create mode 100644 funnel/assets/js/utils/codemirror_stylesheet.js create mode 100644 funnel/assets/js/utils/jsonform.js create mode 100644 funnel/templates/js/json_form.js.jinja2 create mode 100644 funnel/templates/rsvp_modal.html.jinja2 create mode 100644 tests/unit/forms/rsvp_json_test.py create mode 100644 tests/unit/views/rsvp_test.py diff --git a/funnel/assets/js/project_header.js b/funnel/assets/js/project_header.js index a32483286..fe5a85d0d 100644 --- a/funnel/assets/js/project_header.js +++ b/funnel/assets/js/project_header.js @@ -167,7 +167,8 @@ $(() => { saveProjectConfig = '', tickets = '', toggleId = '', - sort = '' + sort = '', + rsvpModalHash = 'register-modal' ) => { if (saveProjectConfig) { SaveProject(saveProjectConfig); @@ -189,10 +190,16 @@ $(() => { } $('a.js-register-btn').click(function showRegistrationModal() { - $(this).modal('show'); + window.history.pushState( + { + openModal: true, + }, + '', + `#${rsvpModalHash}` + ); }); - if (window.location.hash.includes('register-modal')) { + if (window.location.hash.includes(rsvpModalHash)) { $('a.js-register-btn').modal('show'); } diff --git a/funnel/assets/js/rsvp_form_modal.js b/funnel/assets/js/rsvp_form_modal.js new file mode 100644 index 000000000..96cd64e9c --- /dev/null +++ b/funnel/assets/js/rsvp_form_modal.js @@ -0,0 +1,33 @@ +import Vue from 'vue/dist/vue.esm'; +import jsonForm from './utils/jsonform'; + +Vue.config.devtools = true; + +const FormUI = { + init(jsonSchema) { + /* eslint-disable no-new */ + new Vue({ + el: '#register-form', + data() { + return { + jsonSchema, + }; + }, + components: { + jsonForm, + }, + methods: { + handleAjaxPost() { + window.location.hash = ''; + window.location.reload(); + }, + }, + }); + }, +}; + +$(() => { + window.Hasgeek.addRsvpForm = (jsonSchema) => { + FormUI.init(jsonSchema); + }; +}); diff --git a/funnel/assets/js/utils/codemirror_stylesheet.js b/funnel/assets/js/utils/codemirror_stylesheet.js new file mode 100644 index 000000000..88a88e4e0 --- /dev/null +++ b/funnel/assets/js/utils/codemirror_stylesheet.js @@ -0,0 +1,51 @@ +import { EditorView, keymap } from '@codemirror/view'; +import { css, cssLanguage } from '@codemirror/lang-css'; +import { closeBrackets } from '@codemirror/autocomplete'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; +import { + syntaxHighlighting, + defaultHighlightStyle, + foldGutter, +} from '@codemirror/language'; + +function codemirrorStylesheetHelper( + textareaId, + updateFnCallback = '', + callbackInterval = 1000 +) { + let textareaWaitTimer; + + const extensions = [ + EditorView.lineWrapping, + EditorView.contentAttributes.of({ autocapitalize: 'on' }), + closeBrackets(), + history(), + foldGutter(), + syntaxHighlighting(defaultHighlightStyle), + keymap.of([defaultKeymap, historyKeymap]), + css({ base: cssLanguage }), + ]; + + const view = new EditorView({ + doc: $(`#${textareaId}`).val(), + extensions, + dispatch: (tr) => { + view.update([tr]); + $(`#${textareaId}`).val(view.state.doc.toString()); + if (updateFnCallback) { + if (textareaWaitTimer) clearTimeout(textareaWaitTimer); + textareaWaitTimer = setTimeout(() => { + updateFnCallback(view); + }, callbackInterval); + } + }, + }); + if ($(`#${textareaId}`).hasClass('activated')) { + $(`#${textareaId}`).next().remove(); + } + $(`#${textareaId}`).addClass('activated').removeClass('activating'); + document.querySelector(`#${textareaId}`).parentNode.append(view.dom); + return view; +} + +export default codemirrorStylesheetHelper; diff --git a/funnel/assets/js/utils/form_widgets.js b/funnel/assets/js/utils/form_widgets.js index 6606512fe..240d2c452 100644 --- a/funnel/assets/js/utils/form_widgets.js +++ b/funnel/assets/js/utils/form_widgets.js @@ -149,13 +149,30 @@ export async function activateFormWidgets() { ).length ) { const { default: codemirrorHelper } = await import('./codemirror'); - $('textarea.markdown:not([style*="display: none"]').each( - function enableCodemirror() { - const markdownId = $(this).attr('id'); - $(`#${markdownId}`).addClass('activating'); - codemirrorHelper(markdownId); - } + $( + 'textarea.markdown:not([style*="display: none"]:not(.activating):not(.activated)' + ).each(function enableCodemirror() { + const markdownId = $(this).attr('id'); + $(`#${markdownId}`).addClass('activating'); + codemirrorHelper(markdownId); + }); + } + + if ( + $( + 'textarea.stylesheet:not([style*="display: none"]:not(.activating):not(.activated)' + ).length + ) { + const { default: codemirrorStylesheetHelper } = await import( + './codemirror_stylesheet' ); + $( + 'textarea.stylesheet:not([style*="display: none"]:not(.activating):not(.activated)' + ).each(function enableCodemirrorForStylesheet() { + const textareaId = $(this).attr('id'); + $(`#${textareaId}`).addClass('activating'); + codemirrorStylesheetHelper(textareaId); + }); } } diff --git a/funnel/assets/js/utils/formhelper.js b/funnel/assets/js/utils/formhelper.js index d695db6e3..9c957785d 100644 --- a/funnel/assets/js/utils/formhelper.js +++ b/funnel/assets/js/utils/formhelper.js @@ -158,17 +158,21 @@ const Form = { } }, ajaxFormSubmit(formId, url, onSuccess, onError, config) { + const formData = $(`#${formId}`).serialize(); $.ajax({ url, type: 'POST', - data: $(`#${formId}`).serialize(), + data: config.formData ? config.formData : formData, dataType: config.dataType ? config.dataType : 'json', + contentType: config.contentType + ? config.contentType + : 'application/x-www-form-urlencoded', beforeSend() { Form.preventDoubleSubmit(formId); if (config.beforeSend) config.beforeSend(); }, success(responseData) { - onSuccess(responseData); + if (onSuccess) onSuccess(responseData); }, error(xhr) { onError(xhr); diff --git a/funnel/assets/js/utils/jsonform.js b/funnel/assets/js/utils/jsonform.js new file mode 100644 index 000000000..c76b2954a --- /dev/null +++ b/funnel/assets/js/utils/jsonform.js @@ -0,0 +1,51 @@ +import Vue from 'vue/dist/vue.min'; +import Form from './formhelper'; + +const jsonForm = Vue.component('jsonform', { + template: '#form-template', + props: ['jsonschema', 'title', 'formid'], + methods: { + getFormData() { + const obj = {}; + const formData = $(`#${this.formid}`).serializeArray(); + formData.forEach((field) => { + if (field.name !== 'form_nonce' && field.name !== 'csrf_token') + obj[field.name] = field.value; + }); + return JSON.stringify(obj); + }, + activateForm() { + const form = this; + const url = Form.getActionUrl(this.formid); + const formValues = new FormData($(`#${this.formid}`)[0]); + const onSuccess = (response) => { + this.$emit('handle-submit-response', this.formid, response); + }; + const onError = (response) => { + Form.formErrorHandler(this.formid, response); + }; + $(`#${this.formid}`) + .find('button[type="submit"]') + .click((event) => { + event.preventDefault(); + Form.ajaxFormSubmit(this.formid, url, onSuccess, onError, { + contentType: 'application/json', + dataType: 'html', + formData: JSON.stringify({ + form_nonce: formValues.get('form_nonce'), + csrf_token: formValues.get('csrf_token'), + form: form.getFormData(), + }), + }); + }); + }, + getFieldId() { + return Math.random().toString(16).slice(2); + }, + }, + mounted() { + this.activateForm(); + }, +}); + +export default jsonForm; diff --git a/funnel/assets/sass/form.scss b/funnel/assets/sass/form.scss index d75e4317c..efdd069c7 100644 --- a/funnel/assets/sass/form.scss +++ b/funnel/assets/sass/form.scss @@ -88,7 +88,8 @@ } // Codemirror editor will be initialized - textarea.markdown { + textarea.markdown, + textarea.stylesheet { display: none; } diff --git a/funnel/forms/auth_client.py b/funnel/forms/auth_client.py index e7cb7141d..fcf6cfc68 100644 --- a/funnel/forms/auth_client.py +++ b/funnel/forms/auth_client.py @@ -156,7 +156,7 @@ class AuthClientCredentialForm(forms.Form): ) -def permission_validator(form, field) -> None: +def permission_validator(form: forms.Form, field: forms.Field) -> None: """Validate permission strings to be appropriately named.""" permlist = field.data.split() for perm in permlist: @@ -202,7 +202,7 @@ class TeamPermissionAssignForm(forms.Form): validators=[forms.validators.DataRequired(), permission_validator], ) - def validate_team_id(self, field) -> None: + def validate_team_id(self, field: forms.Field) -> None: """Validate selected team to belong to this organization.""" # FIXME: Replace with QuerySelectField using RadioWidget. teams = [team for team in self.organization.teams if team.buid == field.data] diff --git a/funnel/forms/helpers.py b/funnel/forms/helpers.py index 5a6595a85..066ca22aa 100644 --- a/funnel/forms/helpers.py +++ b/funnel/forms/helpers.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import Optional, Sequence +import json +from typing import Optional, Sequence, Union from typing_extensions import Literal from flask import flash @@ -261,5 +262,27 @@ def tostr(value: object) -> str: return '' +def format_json(data: Union[dict, str, None]) -> str: + """Return a dict as a formatted JSON string, and return a string unchanged.""" + if data: + if isinstance(data, str): + return data + return json.dumps(data, indent=2, sort_keys=True) + return '' + + +def validate_and_convert_json(form: forms.Form, field: forms.Field) -> None: + """Confirm form data is valid JSON, and store it back as a parsed dict.""" + try: + field.data = json.loads(field.data) + except ValueError: + raise forms.validators.StopValidation(_("Invalid JSON")) from None + + strip_filters = [tostr, forms.filters.strip()] nullable_strip_filters = [tostr, forms.filters.strip(), forms.filters.none_if_empty()] +nullable_json_filters = [ + format_json, + forms.filters.strip(), + forms.filters.none_if_empty(), +] diff --git a/funnel/forms/project.py b/funnel/forms/project.py index 20f5ab36b..f25f779ce 100644 --- a/funnel/forms/project.py +++ b/funnel/forms/project.py @@ -13,7 +13,9 @@ from .helpers import ( ProfileSelectField, image_url_validator, + nullable_json_filters, nullable_strip_filters, + validate_and_convert_json, video_url_list_validator, ) @@ -29,6 +31,7 @@ 'ProjectSponsorForm', 'RsvpTransitionForm', 'SavedProjectForm', + 'ProjectRegisterForm', ] double_quote_re = re.compile(r'["“”]') @@ -349,3 +352,33 @@ def set_queries(self) -> None: (transition_name, getattr(Rsvp, transition_name)) for transition_name in Rsvp.state.statemanager.transitions ] + + +@Project.forms('rsvp') +class ProjectRegisterForm(forms.Form): + """Register for a project with an optional custom JSON form.""" + + __expects__ = ('schema',) + schema: Optional[dict] + + form = forms.TextAreaField( + __("Form"), + filters=nullable_json_filters, + validators=[validate_and_convert_json], + ) + + def validate_form(self, field: forms.Field) -> None: + if self.form.data and not self.schema: + raise forms.validators.StopValidation( + _("This registration is not expecting any form fields") + ) + if self.schema: + form_keys = set(self.form.data.keys()) + schema_keys = {i['name'] for i in self.schema['fields']} + if not form_keys.issubset(schema_keys): + invalid_keys = form_keys.difference(schema_keys) + raise forms.validators.StopValidation( + _("The form is not expecting these fields: {fields}").format( + fields=', '.join(invalid_keys) + ) + ) diff --git a/funnel/forms/sync_ticket.py b/funnel/forms/sync_ticket.py index 75c825ab4..19eed618b 100644 --- a/funnel/forms/sync_ticket.py +++ b/funnel/forms/sync_ticket.py @@ -2,8 +2,11 @@ from __future__ import annotations +import json from typing import Optional +from flask import Markup + from baseframe import __, forms from ..models import ( @@ -15,8 +18,10 @@ UserEmail, db, ) +from .helpers import nullable_json_filters, validate_and_convert_json __all__ = [ + 'FORM_SCHEMA_PLACEHOLDER', 'ProjectBoxofficeForm', 'TicketClientForm', 'TicketEventForm', @@ -25,8 +30,31 @@ 'TicketTypeForm', ] + BOXOFFICE_DETAILS_PLACEHOLDER = {'org': 'hasgeek', 'item_collection_id': ''} +FORM_SCHEMA_PLACEHOLDER = { + 'fields': [ + { + 'name': 'field_name', + 'title': "Field label shown to user", + 'description': "An explanation for this field", + 'type': "string", + }, + { + 'name': 'has_checked', + 'title': "I accept the terms", + 'type': 'boolean', + }, + { + 'name': 'choice', + 'title': "Choose one", + 'type': 'select', + 'choices': ["First choice", "Second choice", "Third choice"], + }, + ] +} + @Project.forms('boxoffice') class ProjectBoxofficeForm(forms.Form): @@ -42,20 +70,33 @@ class ProjectBoxofficeForm(forms.Form): filters=[forms.filters.strip()], ) allow_rsvp = forms.BooleanField( - __("Allow rsvp"), + __("Allow free registrations"), default=False, - description=__("If checked, both free and buy tickets will shown on project"), ) is_subscription = forms.BooleanField( - __("This is a subscription"), + __("Paid tickets are for a subscription"), default=True, - description=__("If not checked, buy tickets button will be shown"), ) register_button_txt = forms.StringField( __("Register button text"), filters=[forms.filters.strip()], description=__("Optional – Use with care to replace the button text"), ) + register_form_schema = forms.StylesheetField( + __("Registration form"), + description=__("Optional – Specify fields as JSON (limited support)"), + filters=nullable_json_filters, + validators=[forms.validators.Optional(), validate_and_convert_json], + ) + + def set_queries(self): + """Set form schema description.""" + self.register_form_schema.description = Markup( + '

{description}

{schema}
' + ).format( + description=self.register_form_schema.description, + schema=json.dumps(FORM_SCHEMA_PLACEHOLDER, indent=2), + ) @TicketEvent.forms('main') diff --git a/funnel/models/rsvp.py b/funnel/models/rsvp.py index 3f8d1c6e0..6f598e58a 100644 --- a/funnel/models/rsvp.py +++ b/funnel/models/rsvp.py @@ -13,7 +13,7 @@ from coaster.utils import LabeledEnum from ..typing import OptionalMigratedTables -from . import Model, NoIdMixin, UuidMixin, db, relationship, sa +from . import Mapped, Model, NoIdMixin, UuidMixin, db, relationship, sa, types from .helpers import reopen from .project import Project from .project_membership import project_child_role_map @@ -43,6 +43,7 @@ class Rsvp(UuidMixin, NoIdMixin, Model): ), read={'owner', 'project_promoter'}, grants_via={None: project_child_role_map}, + datasets={'primary'}, ) user_id = sa.orm.mapped_column( sa.Integer, sa.ForeignKey('user.id'), nullable=False, primary_key=True @@ -53,6 +54,13 @@ class Rsvp(UuidMixin, NoIdMixin, Model): ), read={'owner', 'project_promoter'}, grants={'owner'}, + datasets={'primary', 'without_parent'}, + ) + form: Mapped[Optional[types.jsonb]] = with_roles( + sa.orm.mapped_column(), + rw={'owner'}, + read={'project_promoter'}, + datasets={'primary', 'without_parent', 'related'}, ) _state = sa.orm.mapped_column( @@ -72,18 +80,16 @@ class Rsvp(UuidMixin, NoIdMixin, Model): 'project_promoter': {'read': {'created_at', 'updated_at'}}, } - __datasets__ = { - 'primary': {'project', 'user', 'response'}, - 'without_parent': {'user', 'response'}, - 'related': {'response'}, - } - @property def response(self): """Return RSVP response as a raw value.""" return self._state - with_roles(response, read={'owner', 'project_promoter'}) + with_roles( + response, + read={'owner', 'project_promoter'}, + datasets={'primary', 'without_parent', 'related'}, + ) @with_roles(call={'owner'}) @state.transition( diff --git a/funnel/templates/account.html.jinja2 b/funnel/templates/account.html.jinja2 index aa0e0baf1..4398884e0 100644 --- a/funnel/templates/account.html.jinja2 +++ b/funnel/templates/account.html.jinja2 @@ -196,7 +196,7 @@ {% endif %} {% trans %}Add an email address{% endtrans %} + href="{{ url_for('add_email') }}" data-cy="add-new-email">{% trans %}Add an email address{% endtrans %}
@@ -247,7 +247,7 @@ {% endif %} {% trans %}Add a mobile number{% endtrans %} + href="{{ url_for('add_phone') }}" data-cy="add-new-phone">{% trans %}Add a mobile number{% endtrans %}
diff --git a/funnel/templates/account_formlayout.html.jinja2 b/funnel/templates/account_formlayout.html.jinja2 index 37960fa58..226cbecc6 100644 --- a/funnel/templates/account_formlayout.html.jinja2 +++ b/funnel/templates/account_formlayout.html.jinja2 @@ -23,7 +23,7 @@

{% trans %}Cookies are required to login. Please enable cookies in your browser’s settings and reload this page{% endtrans %}

-