From c3a36b94a8b3a791e1f48ee15cf17d75b05c2f65 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 13:47:49 +0200 Subject: [PATCH 01/12] Library updates #432 --- migrations/versions/00daa8c472ba_.py | 40 ++++++------ migrations/versions/27da3ceea723_.py | 36 +++++------ migrations/versions/3c5b34fd1156_.py | 63 ++++++++++++------- migrations/versions/e33f225323f3_.py | 32 +++++++--- project/forms/admin.py | 2 +- project/forms/admin_unit.py | 5 +- project/forms/admin_unit_member.py | 2 +- project/forms/common.py | 4 +- project/forms/event.py | 15 +++-- project/forms/event_place.py | 2 +- project/forms/event_suggestion.py | 6 +- project/forms/organizer.py | 2 +- project/forms/reference_request.py | 2 +- project/forms/security.py | 4 +- project/forms/widgets.py | 8 +-- project/models/admin_unit.py | 9 +-- project/models/event.py | 8 +-- project/models/event_mixin.py | 3 +- project/models/trackable_mixin.py | 3 +- project/models/user.py | 6 +- project/services/admin_unit.py | 3 +- project/services/event.py | 45 +++++++------ project/services/user.py | 3 +- project/views/event.py | 9 +++ project/views/event_date.py | 1 + project/views/manage.py | 2 + project/views/root.py | 4 +- tests/seeder.py | 11 +++- tests/test___init__.py | 13 ++-- tests/utils.py | 48 +++++++------- .../test_admin_unit_member_invitation.py | 12 ++-- tests/views/test_user.py | 6 +- 32 files changed, 233 insertions(+), 176 deletions(-) diff --git a/migrations/versions/00daa8c472ba_.py b/migrations/versions/00daa8c472ba_.py index c403cec..2e46508 100644 --- a/migrations/versions/00daa8c472ba_.py +++ b/migrations/versions/00daa8c472ba_.py @@ -32,7 +32,9 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, "eventsuggestion", type_="foreignkey") + op.drop_constraint( + "eventsuggestion_event_id_fkey", "eventsuggestion", type_="foreignkey" + ) op.create_foreign_key( "eventsuggestion_event_id_fkey", "eventsuggestion", @@ -40,22 +42,22 @@ def downgrade(): ["event_id"], ["id"], ) - op.create_table( - "spatial_ref_sys", - sa.Column("srid", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column( - "auth_name", sa.VARCHAR(length=256), autoincrement=False, nullable=True - ), - sa.Column("auth_srid", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column( - "srtext", sa.VARCHAR(length=2048), autoincrement=False, nullable=True - ), - sa.Column( - "proj4text", sa.VARCHAR(length=2048), autoincrement=False, nullable=True - ), - sa.CheckConstraint( - "(srid > 0) AND (srid <= 998999)", name="spatial_ref_sys_srid_check" - ), - sa.PrimaryKeyConstraint("srid", name="spatial_ref_sys_pkey"), - ) + # op.create_table( + # "spatial_ref_sys", + # sa.Column("srid", sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column( + # "auth_name", sa.VARCHAR(length=256), autoincrement=False, nullable=True + # ), + # sa.Column("auth_srid", sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column( + # "srtext", sa.VARCHAR(length=2048), autoincrement=False, nullable=True + # ), + # sa.Column( + # "proj4text", sa.VARCHAR(length=2048), autoincrement=False, nullable=True + # ), + # sa.CheckConstraint( + # "(srid > 0) AND (srid <= 998999)", name="spatial_ref_sys_srid_check" + # ), + # sa.PrimaryKeyConstraint("srid", name="spatial_ref_sys_pkey"), + # ) # ### end Alembic commands ### diff --git a/migrations/versions/27da3ceea723_.py b/migrations/versions/27da3ceea723_.py index 9e8ec06..92d178e 100644 --- a/migrations/versions/27da3ceea723_.py +++ b/migrations/versions/27da3ceea723_.py @@ -55,22 +55,22 @@ def downgrade(): op.drop_column("adminunit", "widget_link_color") op.drop_column("adminunit", "widget_font") op.drop_column("adminunit", "widget_background_color") - op.create_table( - "spatial_ref_sys", - sa.Column("srid", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column( - "auth_name", sa.VARCHAR(length=256), autoincrement=False, nullable=True - ), - sa.Column("auth_srid", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column( - "srtext", sa.VARCHAR(length=2048), autoincrement=False, nullable=True - ), - sa.Column( - "proj4text", sa.VARCHAR(length=2048), autoincrement=False, nullable=True - ), - sa.CheckConstraint( - "(srid > 0) AND (srid <= 998999)", name="spatial_ref_sys_srid_check" - ), - sa.PrimaryKeyConstraint("srid", name="spatial_ref_sys_pkey"), - ) + # op.create_table( + # "spatial_ref_sys", + # sa.Column("srid", sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column( + # "auth_name", sa.VARCHAR(length=256), autoincrement=False, nullable=True + # ), + # sa.Column("auth_srid", sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column( + # "srtext", sa.VARCHAR(length=2048), autoincrement=False, nullable=True + # ), + # sa.Column( + # "proj4text", sa.VARCHAR(length=2048), autoincrement=False, nullable=True + # ), + # sa.CheckConstraint( + # "(srid > 0) AND (srid <= 998999)", name="spatial_ref_sys_srid_check" + # ), + # sa.PrimaryKeyConstraint("srid", name="spatial_ref_sys_pkey"), + # ) # ### end Alembic commands ### diff --git a/migrations/versions/3c5b34fd1156_.py b/migrations/versions/3c5b34fd1156_.py index 75176e3..934baa5 100644 --- a/migrations/versions/3c5b34fd1156_.py +++ b/migrations/versions/3c5b34fd1156_.py @@ -65,7 +65,7 @@ def upgrade(): sa.PrimaryKeyConstraint("id"), ) - migrate_category_to_categories() + upgrade_category_to_categories() # op.drop_table('spatial_ref_sys') op.drop_constraint("event_category_id_fkey", "event", type_="foreignkey") @@ -73,7 +73,7 @@ def upgrade(): # ### end Alembic commands ### -def migrate_category_to_categories(): +def upgrade_category_to_categories(): bind = op.get_bind() session = orm.Session(bind=bind) @@ -83,32 +83,53 @@ def migrate_category_to_categories(): session.commit() +def downgrade_categories_to_category(): + bind = op.get_bind() + session = orm.Session(bind=bind) + + for event in session.query(Event): + event.category = event.categories[0] + + session.commit() + + def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column( "event", - sa.Column("category_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("category_id", sa.INTEGER(), autoincrement=False, nullable=True), ) + downgrade_categories_to_category() + op.alter_column( + "event", + sa.Column( + "category_id", + existing_type=sa.INTEGER(), + autoincrement=False, + nullable=False, + ), + ) + op.create_foreign_key( "event_category_id_fkey", "event", "eventcategory", ["category_id"], ["id"] ) - op.create_table( - "spatial_ref_sys", - sa.Column("srid", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column( - "auth_name", sa.VARCHAR(length=256), autoincrement=False, nullable=True - ), - sa.Column("auth_srid", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column( - "srtext", sa.VARCHAR(length=2048), autoincrement=False, nullable=True - ), - sa.Column( - "proj4text", sa.VARCHAR(length=2048), autoincrement=False, nullable=True - ), - sa.CheckConstraint( - "(srid > 0) AND (srid <= 998999)", name="spatial_ref_sys_srid_check" - ), - sa.PrimaryKeyConstraint("srid", name="spatial_ref_sys_pkey"), - ) + # op.create_table( + # "spatial_ref_sys", + # sa.Column("srid", sa.INTEGER(), autoincrement=False, nullable=False), + # sa.Column( + # "auth_name", sa.VARCHAR(length=256), autoincrement=False, nullable=True + # ), + # sa.Column("auth_srid", sa.INTEGER(), autoincrement=False, nullable=True), + # sa.Column( + # "srtext", sa.VARCHAR(length=2048), autoincrement=False, nullable=True + # ), + # sa.Column( + # "proj4text", sa.VARCHAR(length=2048), autoincrement=False, nullable=True + # ), + # sa.CheckConstraint( + # "(srid > 0) AND (srid <= 998999)", name="spatial_ref_sys_srid_check" + # ), + # sa.PrimaryKeyConstraint("srid", name="spatial_ref_sys_pkey"), + # ) op.drop_table("event_eventcategories") # ### end Alembic commands ### diff --git a/migrations/versions/e33f225323f3_.py b/migrations/versions/e33f225323f3_.py index 3691fe9..96dd343 100644 --- a/migrations/versions/e33f225323f3_.py +++ b/migrations/versions/e33f225323f3_.py @@ -73,34 +73,46 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, "settings", type_="foreignkey") + op.drop_constraint("settings_updated_by_id_fkey", "settings", type_="foreignkey") op.drop_column("settings", "updated_by_id") op.drop_column("settings", "updated_at") - op.drop_constraint(None, "location", type_="foreignkey") + op.drop_constraint("location_updated_by_id_fkey", "location", type_="foreignkey") op.drop_column("location", "updated_by_id") op.drop_column("location", "updated_at") - op.drop_constraint(None, "image", type_="foreignkey") + op.drop_constraint("image_updated_by_id_fkey", "image", type_="foreignkey") op.drop_column("image", "updated_by_id") op.drop_column("image", "updated_at") - op.drop_constraint(None, "eventsuggestion", type_="foreignkey") + op.drop_constraint( + "eventsuggestion_updated_by_id_fkey", "eventsuggestion", type_="foreignkey" + ) op.drop_column("eventsuggestion", "updated_by_id") op.drop_column("eventsuggestion", "updated_at") - op.drop_constraint(None, "eventreferencerequest", type_="foreignkey") + op.drop_constraint( + "eventreferencerequest_updated_by_id_fkey", + "eventreferencerequest", + type_="foreignkey", + ) op.drop_column("eventreferencerequest", "updated_by_id") op.drop_column("eventreferencerequest", "updated_at") - op.drop_constraint(None, "eventreference", type_="foreignkey") + op.drop_constraint( + "eventreference_updated_by_id_fkey", "eventreference", type_="foreignkey" + ) op.drop_column("eventreference", "updated_by_id") op.drop_column("eventreference", "updated_at") - op.drop_constraint(None, "eventplace", type_="foreignkey") + op.drop_constraint( + "eventplace_updated_by_id_fkey", "eventplace", type_="foreignkey" + ) op.drop_column("eventplace", "updated_by_id") op.drop_column("eventplace", "updated_at") - op.drop_constraint(None, "eventorganizer", type_="foreignkey") + op.drop_constraint( + "eventorganizer_updated_by_id_fkey", "eventorganizer", type_="foreignkey" + ) op.drop_column("eventorganizer", "updated_by_id") op.drop_column("eventorganizer", "updated_at") - op.drop_constraint(None, "event", type_="foreignkey") + op.drop_constraint("event_updated_by_id_fkey", "event", type_="foreignkey") op.drop_column("event", "updated_by_id") op.drop_column("event", "updated_at") - op.drop_constraint(None, "adminunit", type_="foreignkey") + op.drop_constraint("adminunit_updated_by_id_fkey", "adminunit", type_="foreignkey") op.drop_column("adminunit", "updated_by_id") op.drop_column("adminunit", "updated_at") # ### end Alembic commands ### diff --git a/project/forms/admin.py b/project/forms/admin.py index 79112cb..0557367 100644 --- a/project/forms/admin.py +++ b/project/forms/admin.py @@ -1,7 +1,7 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from wtforms import BooleanField, RadioField, StringField, SubmitField, TextAreaField -from wtforms.fields.html5 import EmailField +from wtforms.fields import EmailField from wtforms.validators import DataRequired, Optional from project.forms.widgets import MultiCheckboxField diff --git a/project/forms/admin_unit.py b/project/forms/admin_unit.py index 3fd687d..f0e2429 100644 --- a/project/forms/admin_unit.py +++ b/project/forms/admin_unit.py @@ -1,10 +1,9 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from wtforms import DecimalField, FormField, StringField, SubmitField, TextAreaField -from wtforms.fields.core import BooleanField -from wtforms.fields.html5 import EmailField, TelField, URLField +from wtforms.fields import BooleanField, EmailField, TelField, URLField from wtforms.validators import DataRequired, Length, Optional, Regexp -from wtforms.widgets.html5 import ColorInput +from wtforms.widgets import ColorInput from project.forms.common import Base64ImageForm from project.forms.widgets import HTML5StringField diff --git a/project/forms/admin_unit_member.py b/project/forms/admin_unit_member.py index 7797ba9..8213016 100644 --- a/project/forms/admin_unit_member.py +++ b/project/forms/admin_unit_member.py @@ -1,7 +1,7 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from wtforms import SubmitField -from wtforms.fields.html5 import EmailField +from wtforms.fields import EmailField from wtforms.validators import DataRequired from project.forms.widgets import MultiCheckboxField diff --git a/project/forms/common.py b/project/forms/common.py index 02cd531..4a6ab37 100644 --- a/project/forms/common.py +++ b/project/forms/common.py @@ -32,8 +32,8 @@ class Base64ImageForm(BaseImageForm): obj.data, obj.encoding_format ) - def validate(self): - result = super().validate() + def validate(self, extra_validators=None): + result = super().validate(extra_validators) if self.image_base64.data: image = get_image_from_base64_str(self.image_base64.data) diff --git a/project/forms/event.py b/project/forms/event.py index a25ee16..fab6e2a 100644 --- a/project/forms/event.py +++ b/project/forms/event.py @@ -14,8 +14,7 @@ from wtforms import ( SubmitField, TextAreaField, ) -from wtforms.fields.core import FieldList -from wtforms.fields.html5 import EmailField, URLField +from wtforms.fields import EmailField, FieldList, URLField from wtforms.validators import DataRequired, Length, Optional from project.forms.common import Base64ImageForm, distance_choices, event_rating_choices @@ -72,8 +71,8 @@ class EventDateDefinitionFormMixin: class EventDateDefinitionForm(FlaskForm, EventDateDefinitionFormMixin): - def validate(self): - result = super().validate() + def validate(self, extra_validators=None): + result = super().validate(extra_validators) if not self.validate_date_definition(): result = False @@ -270,8 +269,8 @@ class BaseEventForm(SharedEventForm): ), ) - def validate(self): - result = super().validate() + def validate(self, extra_validators=None): + result = super().validate(extra_validators) if self.co_organizer_ids.data and self.organizer_id.data: if self.organizer_id.data in self.co_organizer_ids.data: @@ -342,8 +341,8 @@ class CreateEventForm(BaseEventForm): PublicStatus.published if self.submit.data else PublicStatus.draft ) - def validate(self): - result = super().validate() + def validate(self, extra_validators=None): + result = super().validate(extra_validators) if ( not self.event_place_id.data or self.event_place_id.data == 0 diff --git a/project/forms/event_place.py b/project/forms/event_place.py index fea06f3..2790592 100644 --- a/project/forms/event_place.py +++ b/project/forms/event_place.py @@ -1,7 +1,7 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from wtforms import DecimalField, FormField, StringField, SubmitField, TextAreaField -from wtforms.fields.html5 import URLField +from wtforms.fields import URLField from wtforms.validators import DataRequired, Optional from project.forms.common import Base64ImageForm diff --git a/project/forms/event_suggestion.py b/project/forms/event_suggestion.py index 9a6492a..0b7c090 100644 --- a/project/forms/event_suggestion.py +++ b/project/forms/event_suggestion.py @@ -7,7 +7,7 @@ from wtforms import ( StringField, SubmitField, ) -from wtforms.fields.html5 import EmailField, TelField +from wtforms.fields import EmailField, TelField from wtforms.validators import DataRequired, Optional from project.forms.common import get_accept_tos_markup @@ -110,8 +110,8 @@ class CreateEventSuggestionForm(SharedEventForm, EventDateDefinitionFormMixin): else: field.populate_obj(obj, name) - def validate(self): - result = super().validate() + def validate(self, extra_validators=None): + result = super().validate(extra_validators) if not self.validate_date_definition(): result = False diff --git a/project/forms/organizer.py b/project/forms/organizer.py index 23ed23a..a5aef62 100644 --- a/project/forms/organizer.py +++ b/project/forms/organizer.py @@ -1,7 +1,7 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from wtforms import DecimalField, FormField, StringField, SubmitField -from wtforms.fields.html5 import EmailField, TelField, URLField +from wtforms.fields import EmailField, TelField, URLField from wtforms.validators import DataRequired, Optional from project.forms.common import Base64ImageForm diff --git a/project/forms/reference_request.py b/project/forms/reference_request.py index c1e5cbc..49b010e 100644 --- a/project/forms/reference_request.py +++ b/project/forms/reference_request.py @@ -1,7 +1,7 @@ from flask_babelex import lazy_gettext from flask_wtf import FlaskForm from wtforms import SelectField, StringField, SubmitField -from wtforms.fields.core import BooleanField +from wtforms.fields import BooleanField from wtforms.validators import DataRequired, Optional from project.forms.common import event_rating_choices diff --git a/project/forms/security.py b/project/forms/security.py index b1e2aaa..7e99da0 100644 --- a/project/forms/security.py +++ b/project/forms/security.py @@ -35,8 +35,8 @@ class ExtendedConfirmRegisterForm(ConfirmRegisterForm): class ExtendedLoginForm(LoginForm): - def validate(self): - result = super().validate() + def validate(self, **kwargs): + result = super().validate(**kwargs) if not result and self.requires_confirmation: flash_message( diff --git a/project/forms/widgets.py b/project/forms/widgets.py index b2bb123..a82a439 100644 --- a/project/forms/widgets.py +++ b/project/forms/widgets.py @@ -3,7 +3,7 @@ from datetime import datetime from flask_babelex import gettext, to_user_timezone from markupsafe import Markup from wtforms import DateTimeField, SelectField, SelectMultipleField -from wtforms.fields.core import StringField +from wtforms.fields import StringField from wtforms.validators import Length, StopValidation from wtforms.widgets import CheckboxInput, ListWidget, html_params @@ -164,14 +164,14 @@ class HTML5StringField(StringField): self, label=None, validators=None, - filters=tuple(), + filters=(), description="", id=None, default=None, widget=None, render_kw=None, + name=None, _form=None, - _name=None, _prefix="", _translations=None, _meta=None, @@ -194,8 +194,8 @@ class HTML5StringField(StringField): default, widget, render_kw, + name, _form, - _name, _prefix, _translations, _meta, diff --git a/project/models/admin_unit.py b/project/models/admin_unit.py index 125c6ef..c29b8b3 100644 --- a/project/models/admin_unit.py +++ b/project/models/admin_unit.py @@ -1,4 +1,4 @@ -from flask_security import RoleMixin +from flask_security import AsaList, RoleMixin from sqlalchemy import ( Boolean, Column, @@ -14,6 +14,7 @@ from sqlalchemy import ( ) from sqlalchemy.event import listens_for from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.mutable import MutableList from sqlalchemy.orm import aliased, backref, deferred, relationship from sqlalchemy.orm.relationships import remote from sqlalchemy.schema import CheckConstraint @@ -37,7 +38,7 @@ class AdminUnitMemberRole(db.Model, RoleMixin): name = Column(String(80), unique=True) title = Column(Unicode(255)) description = Column(String(255)) - permissions = Column(UnicodeText()) + permissions = Column(MutableList.as_mutable(AsaList()), nullable=True) class AdminUnitMember(db.Model): @@ -299,7 +300,7 @@ class AdminUnit(db.Model, TrackableMixin): AdminUnitRelation.source_admin_unit_id == SourceAdminUnit.id, ) return ( - select([func.count()]) + select(func.count()) .select_from(j) .where( and_( @@ -308,7 +309,7 @@ class AdminUnit(db.Model, TrackableMixin): SourceAdminUnit.can_verify_other, ) ) - .as_scalar() + .scalar_subquery() > 0 ) diff --git a/project/models/event.py b/project/models/event.py index abec5ec..0b055ce 100644 --- a/project/models/event.py +++ b/project/models/event.py @@ -91,11 +91,11 @@ class Event(db.Model, TrackableMixin, EventMixin): @min_start.expression def min_start(cls): return ( - select([EventDateDefinition.start]) + select(EventDateDefinition.start) .where(EventDateDefinition.event_id == cls.id) .order_by(EventDateDefinition.start) .limit(1) - .as_scalar() + .scalar_subquery() ) @hybrid_property @@ -108,7 +108,7 @@ class Event(db.Model, TrackableMixin, EventMixin): @is_recurring.expression def is_recurring(cls): return ( - select([func.count()]) + select(func.count()) .select_from(EventDateDefinition.__table__) .where( and_( @@ -116,7 +116,7 @@ class Event(db.Model, TrackableMixin, EventMixin): func.coalesce(EventDateDefinition.recurrence_rule, "") != "", ) ) - .as_scalar() + .scalar_subquery() ) > 0 date_definitions = relationship( diff --git a/project/models/event_mixin.py b/project/models/event_mixin.py index 7c2e684..81d580f 100644 --- a/project/models/event_mixin.py +++ b/project/models/event_mixin.py @@ -9,8 +9,7 @@ from sqlalchemy import ( Unicode, UnicodeText, ) -from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declared_attr, relationship from project.dbtypes import IntegerEnum from project.models.functions import create_tsvector diff --git a/project/models/trackable_mixin.py b/project/models/trackable_mixin.py index 9a3a06d..d2323d8 100644 --- a/project/models/trackable_mixin.py +++ b/project/models/trackable_mixin.py @@ -1,8 +1,7 @@ import datetime from sqlalchemy import Column, DateTime, ForeignKey -from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import deferred, relationship +from sqlalchemy.orm import declared_attr, deferred, relationship from project.models.functions import _current_user_id_or_none diff --git a/project/models/user.py b/project/models/user.py index ef7c206..c7db44f 100644 --- a/project/models/user.py +++ b/project/models/user.py @@ -1,7 +1,7 @@ import datetime from flask_dance.consumer.storage.sqla import OAuthConsumerMixin -from flask_security import RoleMixin, UserMixin +from flask_security import AsaList, RoleMixin, UserMixin from sqlalchemy import ( Boolean, Column, @@ -10,9 +10,9 @@ from sqlalchemy import ( Integer, String, Unicode, - UnicodeText, UniqueConstraint, ) +from sqlalchemy.ext.mutable import MutableList from sqlalchemy.orm import backref, deferred, relationship from project import db @@ -31,7 +31,7 @@ class Role(db.Model, RoleMixin): name = Column(String(80), unique=True) title = Column(Unicode(255)) description = Column(String(255)) - permissions = Column(UnicodeText()) + permissions = Column(MutableList.as_mutable(AsaList()), nullable=True) class User(db.Model, UserMixin): diff --git a/project/services/admin_unit.py b/project/services/admin_unit.py index 6ede52b..bf3d1ea 100644 --- a/project/services/admin_unit.py +++ b/project/services/admin_unit.py @@ -118,8 +118,7 @@ def upsert_admin_unit_member_role(role_name, role_title, permissions): db.session.add(result) result.title = role_title - result.remove_permissions(result.get_permissions()) - result.add_permissions(permissions) + result.permissions = permissions return result diff --git a/project/services/event.py b/project/services/event.py index e0f4222..25e8a09 100644 --- a/project/services/event.py +++ b/project/services/event.py @@ -7,7 +7,14 @@ from flask import url_for from flask_babelex import format_date, format_time, gettext from icalendar.prop import vDDDLists from sqlalchemy import and_, case, func, or_ -from sqlalchemy.orm import aliased, contains_eager, defaultload, joinedload, lazyload +from sqlalchemy.orm import ( + aliased, + contains_eager, + defaultload, + joinedload, + lazyload, + undefer_group, +) from sqlalchemy.sql import extract from project import app, db @@ -194,7 +201,7 @@ def get_event_dates_query(params): result = ( result.options( - contains_eager(EventDate.event) + joinedload(EventDate.event) .contains_eager(Event.event_place) .contains_eager(EventPlace.location), joinedload(EventDate.event) @@ -216,12 +223,10 @@ def get_event_dates_query(params): if admin_unit_reference: result = result.order_by( case( - [ - ( - admin_unit_reference.rating.isnot(None), - admin_unit_reference.rating, - ), - ], + ( + admin_unit_reference.rating.isnot(None), + admin_unit_reference.rating, + ), else_=Event.rating, ).desc() ) @@ -238,12 +243,12 @@ def get_event_date_with_details_or_404(event_id): .join(Event.event_place, isouter=True) .join(EventPlace.location, isouter=True) .options( - contains_eager(EventDate.event) + joinedload(EventDate.event) .contains_eager(Event.event_place) .contains_eager(EventPlace.location), joinedload(EventDate.event).undefer_group("trackable"), # Place - defaultload(EventDate.event) + joinedload(EventDate.event) .defaultload(Event.event_place) .joinedload(EventPlace.photo), # Category @@ -254,19 +259,19 @@ def get_event_date_with_details_or_404(event_id): joinedload(EventDate.event) .joinedload(Event.organizer) .undefer_group("detail") - .undefer("logo_id") + .undefer(EventOrganizer.logo_id) .joinedload(EventOrganizer.logo), # Photo joinedload(EventDate.event).joinedload(Event.photo), # Admin unit joinedload(EventDate.event) .joinedload(Event.admin_unit) - .undefer("logo_id") + .undefer(AdminUnit.logo_id) .undefer_group("detail") .undefer_group("widget") .joinedload(AdminUnit.location), # Admin unit logo - defaultload(EventDate.event) + joinedload(EventDate.event) .defaultload(Event.admin_unit) .joinedload(AdminUnit.logo), ) @@ -277,12 +282,12 @@ def get_event_date_with_details_or_404(event_id): def get_event_with_details_or_404(event_id): return ( - Event.query.join(EventPlace, isouter=True) + Event.query.join(Event.event_place, isouter=True) .join(Location, isouter=True) .options( - contains_eager(Event.event_place).contains_eager(EventPlace.location), - defaultload(Event).undefer_group("trackable"), + undefer_group("trackable"), # Place + joinedload(Event.event_place).contains_eager(EventPlace.location), joinedload(Event.event_place).joinedload(EventPlace.photo), # Category joinedload(Event.categories).load_only( @@ -291,13 +296,13 @@ def get_event_with_details_or_404(event_id): # Organizer joinedload(Event.organizer) .undefer_group("detail") - .undefer("logo_id") + .undefer(EventOrganizer.logo_id) .joinedload(EventOrganizer.logo), # Photo joinedload(Event.photo), # Admin unit with location joinedload(Event.admin_unit) - .undefer("logo_id") + .undefer(AdminUnit.logo_id) .undefer_group("detail") .undefer_group("widget") .joinedload(AdminUnit.location), @@ -635,8 +640,10 @@ def create_ical_events_for_search( def update_recurring_dates(): + from sqlalchemy import text + # Setting the timezone is neccessary for cli command - db.session.execute("SET timezone TO :val;", {"val": berlin_tz.zone}) + db.session.execute(text("SET timezone TO :val;"), {"val": berlin_tz.zone}) events = get_recurring_events() diff --git a/project/services/user.py b/project/services/user.py index bdfa294..841c25a 100644 --- a/project/services/user.py +++ b/project/services/user.py @@ -42,8 +42,7 @@ def set_roles_for_user(email, roles): def upsert_user_role(role_name, role_title, permissions): role = user_datastore.find_or_create_role(role_name) role.title = role_title - role.remove_permissions(role.get_permissions()) - role.add_permissions(permissions) + role.permissions = permissions return role diff --git a/project/views/event.py b/project/views/event.py index 704f947..76dc0b5 100644 --- a/project/views/event.py +++ b/project/views/event.py @@ -299,6 +299,9 @@ def prepare_event_place(form): if place: form.event_place_id.choices = [(place.id, get_place_str(place))] + if not form.event_place_id.choices: + form.event_place_id.choices = [] + def prepare_organizer(form): if form.organizer_id.data and form.organizer_id.data > 0: @@ -313,6 +316,12 @@ def prepare_organizer(form): ).all() form.co_organizer_ids.choices = [(o.id, o.name) for o in co_organizers] + if not form.organizer_id.choices: + form.organizer_id.choices = [] + + if not form.co_organizer_ids.choices: + form.co_organizer_ids.choices = [] + def prepare_event_form(form): form.category_ids.choices = get_event_category_choices() diff --git a/project/views/event_date.py b/project/views/event_date.py index e8970e8..100a179 100644 --- a/project/views/event_date.py +++ b/project/views/event_date.py @@ -26,6 +26,7 @@ from project.views.utils import ( def prepare_event_date_form(form): form.category_id.choices = get_event_category_choices() form.category_id.choices.insert(0, (0, "")) + form.location.choices = [] @app.route("/eventdates") diff --git a/project/views/manage.py b/project/views/manage.py index 6d1a6c3..7402c0b 100644 --- a/project/views/manage.py +++ b/project/views/manage.py @@ -176,6 +176,8 @@ def manage_admin_unit_events(id): if form.location.data: # pragma: no cover form.location.choices = [(form.location.data, form.location_name.data)] + else: + form.location.choices = [] if form.validate(): form.populate_obj(params) diff --git a/project/views/root.py b/project/views/root.py index 6e7ee65..4034031 100644 --- a/project/views/root.py +++ b/project/views/root.py @@ -4,6 +4,7 @@ import os.path from flask import render_template, request, send_from_directory, url_for from flask_babelex import gettext from markupsafe import Markup +from sqlalchemy import text from project import ( app, @@ -40,7 +41,8 @@ def home(): @app.route("/up") def up(): - db.engine.execute("SELECT 1") + with db.engine.connect() as conn: + conn.execute(text("SELECT 1")) if "REDIS_URL" in app.config and app.config["REDIS_URL"]: # pragma: no cover celery.control.ping() diff --git a/tests/seeder.py b/tests/seeder.py index e07e083..222610c 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -305,14 +305,19 @@ class Seeder(object): def get_event_category_id(self, category_name): from project.services.event import get_event_category - category = get_event_category(category_name) + with self._app.app_context(): + category = get_event_category(category_name) + return category.id def get_event_date_id(self, event_id): from project.models import Event - event = Event.query.get(event_id) - return event.dates[0].id + with self._app.app_context(): + event = Event.query.get(event_id) + event_date_id = event.dates[0].id + + return event_date_id def create_event( self, diff --git a/tests/test___init__.py b/tests/test___init__.py index 057f05b..941aa2f 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -13,9 +13,11 @@ def test_mail_server(): def drop_db(db): - db.drop_all() - db.engine.execute("DROP TABLE IF EXISTS alembic_version;") - db.engine.execute("DROP TABLE IF EXISTS analytics;") + with db.engine.connect() as conn: + with conn.begin(): + db.drop_all() + conn.execute(sqlalchemy.text("DROP TABLE IF EXISTS alembic_version;")) + conn.execute(sqlalchemy.text("DROP TABLE IF EXISTS analytics;")) def populate_db(db): @@ -33,7 +35,9 @@ BEGIN INSERT INTO event (name, admin_unit_id, event_place_id, organizer_id, start) VALUES ('Event', admin_unit_id, event_place_id, organizer_id, current_timestamp) RETURNING id INTO event_id; END $$; """ - db.engine.execute(sqlalchemy.text(sql).execution_options(autocommit=True)) + with db.engine.connect() as conn: + with conn.begin(): + conn.execute(sqlalchemy.text(sql).execution_options(autocommit=True)) def test_migrations(app, seeder): @@ -54,6 +58,7 @@ def test_migrations(app, seeder): seeder.create_event_suggestion(admin_unit_id) seeder.create_any_reference(admin_unit_id) seeder.create_reference_request(event_id, admin_unit_id) + db.session.commit() downgrade() diff --git a/tests/utils.py b/tests/utils.py index dbd6d28..c2f00ba 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -64,28 +64,28 @@ class UtilActions(object): response = self._client.get("/login") assert response.status_code == 200 - with self._client: - response = self._client.post( - "/login", - data={ - "email": email, - "password": password, - "csrf_token": self.get_csrf(response), - "submit": "Anmelden", - }, - follow_redirects=follow_redirects, - ) - - if follow_redirects: - assert response.status_code == 200 - else: - assert response.status_code == 302 - - assert g.identity.user.email == email - with self._app.app_context(): - user = find_user_by_email(email) - user_id = user.id + with self._client: + response = self._client.post( + "/login", + data={ + "email": email, + "password": password, + "csrf_token": self.get_csrf(response), + "submit": "Anmelden", + }, + follow_redirects=follow_redirects, + ) + + if follow_redirects: + assert response.status_code == 200 + else: + assert response.status_code == 302 + + assert g.identity.user.email == email + + user = find_user_by_email(email) + user_id = user.id return user_id @@ -300,8 +300,10 @@ class UtilActions(object): def assert_response_redirect(self, response, endpoint, **values): assert response.status_code == 302 - redirect_url = "http://localhost" + self.get_url(endpoint, **values) - assert response.headers["Location"] == redirect_url + response_location = response.headers["Location"] + redirect_url = self.get_url(endpoint, **values) + absolute_url = "http://localhost" + redirect_url + assert response_location == redirect_url or response_location == absolute_url def assert_response_contains_alert(self, response, category, message=None): assert response.status_code == 200 diff --git a/tests/views/test_admin_unit_member_invitation.py b/tests/views/test_admin_unit_member_invitation.py index b50cc2d..df04474 100644 --- a/tests/views/test_admin_unit_member_invitation.py +++ b/tests/views/test_admin_unit_member_invitation.py @@ -155,8 +155,7 @@ def test_read_decline(client, app, db, utils, seeder): "decline": "Ablehnen", }, ) - assert response.status_code == 302 - assert response.headers["Location"] == "http://localhost/manage" + utils.assert_response_redirect(response, "manage") with app.app_context(): from project.services.admin_unit import ( @@ -211,8 +210,7 @@ def test_read_new_member_not_registered(client, app, utils, seeder): url = "/invitations/%d" % invitation_id response = client.get(url) - assert response.status_code == 302 - assert response.headers["Location"] == "http://localhost/register" + utils.assert_response_redirect(response, "security.register") def test_read_new_member_not_authenticated(client, app, utils, seeder): @@ -226,8 +224,7 @@ def test_read_new_member_not_authenticated(client, app, utils, seeder): url = "/invitations/%d" % invitation_id response = client.get(url) - assert response.status_code == 302 - assert response.headers["Location"].startswith("http://localhost/login") + utils.assert_response_redirect(response, "security.login", next=url) @pytest.mark.parametrize("user_exists", [True, False]) @@ -245,9 +242,8 @@ def test_read_currentUserDoesNotMatchInvitationEmail( seeder.create_user(email) url = "/invitations/%d" % invitation_id - environ, response = client.get(url, follow_redirects=True, as_tuple=True) + response = client.get(url, follow_redirects=True) - assert environ["REQUEST_URI"] == "/profile" utils.assert_response_ok(response) utils.assert_response_contains( response, "Die Einladung wurde für einen anderen Nutzer ausgestellt." diff --git a/tests/views/test_user.py b/tests/views/test_user.py index 565b706..5f6abe2 100644 --- a/tests/views/test_user.py +++ b/tests/views/test_user.py @@ -25,8 +25,7 @@ def test_organization_invitation_not_authenticated(client, app, utils, seeder): seeder.create_user("invited@test.de") url = utils.get_url("user_organization_invitation", id=invitation_id) response = client.get(url) - assert response.status_code == 302 - assert response.headers["Location"].startswith("http://localhost/login") + utils.assert_response_redirect(response, "security.login", next=url) @pytest.mark.parametrize("user_exists", [True, False]) @@ -40,9 +39,8 @@ def test_organization_invitation_currentUserDoesNotMatchInvitationEmail( seeder.create_user("invited@test.de") url = utils.get_url("user_organization_invitation", id=invitation_id) - environ, response = client.get(url, follow_redirects=True, as_tuple=True) + response = client.get(url, follow_redirects=True) - assert environ["REQUEST_URI"] == "/profile" utils.assert_response_ok(response) utils.assert_response_contains( response, "Die Einladung wurde für einen anderen Nutzer ausgestellt." From f940d26a715873b272cc27388200fd8dc3136ce0 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 13:48:46 +0200 Subject: [PATCH 02/12] Library updates #432 --- requirements.txt | 50 ++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4fdf330..8f1ec5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -alembic==1.4.3 +alembic==1.10.3 amqp==5.1.1 aniso8601==8.1.0 +anytree==2.8.0 apispec==4.0.0 apispec-webframeworks==0.5.2 appdirs==1.4.4 @@ -16,6 +17,7 @@ billiard==3.6.4.0 black==23.1.0 blinker==1.4 cached-property==1.5.2 +cachetools==5.3.0 celery==5.2.7 certifi==2020.12.5 cffi==1.14.4 @@ -37,38 +39,40 @@ dominate==2.6.0 email-validator==1.1.2 filelock==3.0.12 flake8==3.8.4 -Flask==1.1.2 -flask-apispec==0.11.0 +Flask==2.2.3 +flask-apispec==0.11.4 Flask-BabelEx==0.9.4 Flask-Bootstrap==3.3.7.1 Flask-Cors==3.0.9 Flask-Dance==3.2.0 Flask-gzip==0.2 -Flask-Login==0.5.0 +Flask-Login==0.6.2 Flask-Mail==0.9.1 flask-marshmallow==0.14.0 -Flask-Migrate==2.5.3 +Flask-Migrate==4.0.4 Flask-Principal==0.4.0 Flask-QRcode==3.0.0 -Flask-RESTful==0.3.8 -Flask-Security-Too==4.0.0 -Flask-SQLAlchemy==2.4.4 -Flask-WTF==0.14.3 -GeoAlchemy2==0.8.4 +Flask-RESTful==0.3.9 +Flask-Security-Too==5.1.2 +Flask-SQLAlchemy==3.0.3 +Flask-WTF==1.1.1 +GeoAlchemy2==0.13.1 googlemaps==4.4.7 +greenlet==2.0.2 gunicorn==20.0.4 icalendar==4.0.7 identify==1.5.10 idna==2.10 -importlib-metadata==3.1.1 +importlib-metadata==6.3.0 iniconfig==1.1.1 isort==5.7.0 -itsdangerous==1.1.0 -Jinja2==2.11.3 +itsdangerous==2.1.2 +Jinja2==3.1.2 +johnnydep==1.18.0 jsonschema==3.2.0 kombu==5.2.4 Mako==1.1.3 -MarkupSafe==1.1.1 +MarkupSafe==2.1.2 marshmallow==3.10.0 marshmallow-enum==1.5.1 marshmallow-sqlalchemy==0.24.1 @@ -77,11 +81,14 @@ mistune==0.8.4 mypy-extensions==0.4.3 nodeenv==1.5.0 oauthlib==3.1.0 +oyaml==1.0 packaging==23.0 passlib==1.7.4 pathspec==0.11.0 pilkit==2.0 Pillow==9.0.0 +pipdeptree==2.7.0 +pkginfo==1.9.6 platformdirs==3.1.0 pluggy==0.13.1 pre-commit==2.9.3 @@ -94,6 +101,7 @@ pyflakes==2.2.0 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==6.1.2 +pytest-celery==0.0.0 pytest-cov==2.10.1 pytest-datadir==1.3.1 pytest-mock==3.3.1 @@ -113,14 +121,16 @@ rope==0.18.0 six==1.15.0 soupsieve==2.1 speaklater==1.3 -SQLAlchemy==1.3.20 -SQLAlchemy-Utils==0.36.8 +SQLAlchemy==2.0.9 +SQLAlchemy-Utils==0.41.0 +structlog==23.1.0 swagger-spec-validator==2.7.3 +tabulate==0.9.0 TatSu==4.4.0 toml==0.10.2 tomli==2.0.1 typed-ast==1.5.4 -typing-extensions==4.5.0 +typing_extensions==4.5.0 urllib3==1.26.5 URLObject==2.4.3 validators==0.18.2 @@ -129,7 +139,9 @@ virtualenv==20.2.2 visitor==0.1.3 wcwidth==0.2.6 webargs==7.0.1 -Werkzeug==1.0.1 -WTForms==2.3.3 +Werkzeug==2.2.3 +wimpy==0.6 +WTForms==3.0.1 WTForms-SQLAlchemy==0.2 +yarg==0.1.9 zipp==3.4.0 From 820407a69b15a8f47b8e9bb2a289e04f961ca80c Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 14:10:24 +0200 Subject: [PATCH 03/12] Library updates #432 --- .github/workflows/test.yml | 2 +- migrations/versions/3c5b34fd1156_.py | 2 +- migrations/versions/f350153a5691_.py | 2 +- project/dbtypes.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cefec5..c9703cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: - name: Run tests run: pytest --cov=project --splits 4 --group ${{ matrix.group }} env: - TEST_DATABASE_URL: postgresql://postgres:postgres@localhost/eventcally_tests + TEST_DATABASE_URL: postgresql://postgres:postgres@postgres/eventcally_tests TEST_REDIS_URL: redis://localhost:6379 - name: Upload coverage diff --git a/migrations/versions/3c5b34fd1156_.py b/migrations/versions/3c5b34fd1156_.py index 934baa5..cc7d037 100644 --- a/migrations/versions/3c5b34fd1156_.py +++ b/migrations/versions/3c5b34fd1156_.py @@ -9,7 +9,7 @@ import sqlalchemy as sa import sqlalchemy_utils from alembic import op from sqlalchemy import orm -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base from project import dbtypes diff --git a/migrations/versions/f350153a5691_.py b/migrations/versions/f350153a5691_.py index df338e3..f073731 100644 --- a/migrations/versions/f350153a5691_.py +++ b/migrations/versions/f350153a5691_.py @@ -10,7 +10,7 @@ import sqlalchemy_utils from alembic import op from sqlalchemy import orm from sqlalchemy.dialects import postgresql -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base from project import dbtypes diff --git a/project/dbtypes.py b/project/dbtypes.py index ea1e443..a7b2e02 100644 --- a/project/dbtypes.py +++ b/project/dbtypes.py @@ -4,6 +4,7 @@ from sqlalchemy.types import TypeDecorator class IntegerEnum(TypeDecorator): impl = Integer + cache_ok = True def __init__(self, enumtype, *args, **kwargs): super().__init__(*args, **kwargs) From 9889413e13273b88a0152b10a85e714f18452978 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 14:29:29 +0200 Subject: [PATCH 04/12] Library updates #432 --- .github/workflows/test.yml | 2 +- requirements.txt | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9703cd..6cefec5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: - name: Run tests run: pytest --cov=project --splits 4 --group ${{ matrix.group }} env: - TEST_DATABASE_URL: postgresql://postgres:postgres@postgres/eventcally_tests + TEST_DATABASE_URL: postgresql://postgres:postgres@localhost/eventcally_tests TEST_REDIS_URL: redis://localhost:6379 - name: Upload coverage diff --git a/requirements.txt b/requirements.txt index 8f1ec5e..2ac7ab5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,6 +37,7 @@ dnspython==2.0.0 docopt==0.6.2 dominate==2.6.0 email-validator==1.1.2 +exceptiongroup==1.1.1 filelock==3.0.12 flake8==3.8.4 Flask==2.2.3 @@ -100,12 +101,12 @@ pycparser==2.20 pyflakes==2.2.0 pyparsing==2.4.7 pyrsistent==0.17.3 -pytest==6.1.2 +pytest==7.3.1 pytest-celery==0.0.0 -pytest-cov==2.10.1 -pytest-datadir==1.3.1 -pytest-mock==3.3.1 -pytest-split==0.6.0 +pytest-cov==4.0.0 +pytest-datadir==1.4.1 +pytest-mock==3.10.0 +pytest-split==0.8.1 python-dateutil==2.8.1 python-dotenv==0.15.0 python-editor==1.0.4 From d8212ed73ffb21891f3dda697ef4ced4346e6830 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 14:57:54 +0200 Subject: [PATCH 05/12] Library updates #432 --- .github/workflows/cypress.yml | 12 +++--- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 12 +++--- Dockerfile | 2 +- doc/deployment.md | 2 +- requirements.txt | 73 ++++++++++++++++++----------------- tests/conftest.py | 2 +- 7 files changed, 53 insertions(+), 52 deletions(-) diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index b83ecb2..c2b522a 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -42,10 +42,10 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.9" - name: Install dependencies run: | @@ -116,10 +116,10 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.9" - name: Install dependencies run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1890a23..44d6745 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 - uses: jamescurtin/isort-action@master - uses: psf/black@stable - uses: TrueBrain/actions-flake8@v1.4.1 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cefec5..95a78b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,10 +52,10 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependencies run: | @@ -83,10 +83,10 @@ jobs: - name: Check out repository code uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.9' - name: Install dependencies run: | diff --git a/Dockerfile b/Dockerfile index 9b0b13b..01d8807 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.9 # Add rsync RUN apt update -qq && apt upgrade -y && apt autoremove -y diff --git a/doc/deployment.md b/doc/deployment.md index 614f72e..b54711d 100644 --- a/doc/deployment.md +++ b/doc/deployment.md @@ -14,7 +14,7 @@ docker run -p 5000:5000 -e "DATABASE_URL=postgresql://postgres@localhost/eventca ### Requirements -- Python 3.7 +- Python 3.9 - pip - Postgres with postgis diff --git a/requirements.txt b/requirements.txt index 2ac7ab5..98b05fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,16 +5,16 @@ anytree==2.8.0 apispec==4.0.0 apispec-webframeworks==0.5.2 appdirs==1.4.4 -argh==0.26.2 -arrow==0.14.7 +argh==0.28.1 +arrow==1.2.3 async-timeout==4.0.2 attrs==20.3.0 -Authlib==0.15.3 +Authlib==1.2.0 Babel==2.9.1 -bcrypt==3.2.0 -beautifulsoup4==4.9.3 +bcrypt==4.0.1 +beautifulsoup4==4.12.2 billiard==3.6.4.0 -black==23.1.0 +black==23.3.0 blinker==1.4 cached-property==1.5.2 cachetools==5.3.0 @@ -29,44 +29,44 @@ click-plugins==1.1.1 click-repl==0.2.0 colour==0.1.5 coverage==5.5 -coveralls==2.2.0 +coveralls==3.3.1 cryptography==3.3.2 decorator==5.1.0 -distlib==0.3.1 +distlib==0.3.6 dnspython==2.0.0 docopt==0.6.2 dominate==2.6.0 email-validator==1.1.2 exceptiongroup==1.1.1 -filelock==3.0.12 -flake8==3.8.4 +filelock==3.11.0 +flake8==6.0.0 Flask==2.2.3 flask-apispec==0.11.4 Flask-BabelEx==0.9.4 Flask-Bootstrap==3.3.7.1 -Flask-Cors==3.0.9 -Flask-Dance==3.2.0 +Flask-Cors==3.0.10 +Flask-Dance==6.2.0 Flask-gzip==0.2 Flask-Login==0.6.2 Flask-Mail==0.9.1 -flask-marshmallow==0.14.0 +flask-marshmallow==0.15.0 Flask-Migrate==4.0.4 Flask-Principal==0.4.0 -Flask-QRcode==3.0.0 +Flask-QRcode==3.1.0 Flask-RESTful==0.3.9 Flask-Security-Too==5.1.2 Flask-SQLAlchemy==3.0.3 Flask-WTF==1.1.1 GeoAlchemy2==0.13.1 -googlemaps==4.4.7 +googlemaps==4.10.0 greenlet==2.0.2 -gunicorn==20.0.4 -icalendar==4.0.7 +gunicorn==20.1.0 +icalendar==5.0.5 identify==1.5.10 idna==2.10 importlib-metadata==6.3.0 iniconfig==1.1.1 -isort==5.7.0 +isort==5.12.0 itsdangerous==2.1.2 Jinja2==3.1.2 johnnydep==1.18.0 @@ -74,11 +74,11 @@ jsonschema==3.2.0 kombu==5.2.4 Mako==1.1.3 MarkupSafe==2.1.2 -marshmallow==3.10.0 +marshmallow==3.19.0 marshmallow-enum==1.5.1 -marshmallow-sqlalchemy==0.24.1 -mccabe==0.6.1 -mistune==0.8.4 +marshmallow-sqlalchemy==0.29.0 +mccabe==0.7.0 +mistune==2.0.5 mypy-extensions==0.4.3 nodeenv==1.5.0 oauthlib==3.1.0 @@ -87,18 +87,18 @@ packaging==23.0 passlib==1.7.4 pathspec==0.11.0 pilkit==2.0 -Pillow==9.0.0 +Pillow==9.5.0 pipdeptree==2.7.0 pkginfo==1.9.6 platformdirs==3.1.0 pluggy==0.13.1 -pre-commit==2.9.3 +pre-commit==3.2.2 prompt-toolkit==3.0.38 -psycopg2-binary==2.8.6 +psycopg2-binary==2.9.6 py==1.10.0 -pycodestyle==2.6.0 +pycodestyle==2.10.0 pycparser==2.20 -pyflakes==2.2.0 +pyflakes==3.0.1 pyparsing==2.4.7 pyrsistent==0.17.3 pytest==7.3.1 @@ -110,39 +110,40 @@ pytest-split==0.8.1 python-dateutil==2.8.1 python-dotenv==0.15.0 python-editor==1.0.4 +pytoolconfig==1.2.5 pytz==2022.7.1 PyYAML==5.4.1 qrcode==6.1 -redis==4.5.1 -regex==2020.11.13 +redis==4.5.4 +regex==2023.3.23 requests==2.25.0 -requests-mock==1.9.3 +requests-mock==1.10.0 requests-oauthlib==1.3.0 -rope==0.18.0 +rope==1.7.0 six==1.15.0 soupsieve==2.1 speaklater==1.3 SQLAlchemy==2.0.9 SQLAlchemy-Utils==0.41.0 structlog==23.1.0 -swagger-spec-validator==2.7.3 +swagger-spec-validator==3.0.3 tabulate==0.9.0 -TatSu==4.4.0 +TatSu==5.8.3 toml==0.10.2 tomli==2.0.1 typed-ast==1.5.4 typing_extensions==4.5.0 urllib3==1.26.5 URLObject==2.4.3 -validators==0.18.2 +validators==0.20.0 vine==5.0.0 -virtualenv==20.2.2 +virtualenv==20.21.0 visitor==0.1.3 wcwidth==0.2.6 webargs==7.0.1 Werkzeug==2.2.3 wimpy==0.6 WTForms==3.0.1 -WTForms-SQLAlchemy==0.2 +WTForms-SQLAlchemy==0.3 yarg==0.1.9 zipp==3.4.0 diff --git a/tests/conftest.py b/tests/conftest.py index fc462ab..d5f25ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ def pytest_generate_tests(metafunc): warnings.filterwarnings("error", category=SAWarning) os.environ["DATABASE_URL"] = os.environ.get( - "TEST_DATABASE_URL", "postgresql://postgres@localhost/eventcally_tests" + "TEST_DATABASE_URL", "postgresql://user:pass@myserver/ec_tests" ) os.environ["REDIS_URL"] = os.environ.get("TEST_REDIS_URL", "redis://") os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "1" From b20e317623d0189f789bb0e9735eebd7d3fe66bc Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 18:13:47 +0200 Subject: [PATCH 06/12] Library updates #432 --- .github/workflows/test.yml | 2 +- doc/deployment.md | 12 ++++++------ tests/conftest.py | 8 ++++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 95a78b0..290e740 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,7 +63,7 @@ jobs: pip install -r requirements.txt - name: Run tests - run: pytest --cov=project --splits 4 --group ${{ matrix.group }} + run: pytest --log-cli-level=DEBUG --cov=project --splits 4 --group ${{ matrix.group }} env: TEST_DATABASE_URL: postgresql://postgres:postgres@localhost/eventcally_tests TEST_REDIS_URL: redis://localhost:6379 diff --git a/doc/deployment.md b/doc/deployment.md index b54711d..9097673 100644 --- a/doc/deployment.md +++ b/doc/deployment.md @@ -27,12 +27,12 @@ psql -c 'create database eventcally;' -U postgres ### Install and run ```sh -python3 -m venv venv -source venv/bin/activate -(venv) pip install -r requirements.txt -(venv) export DATABASE_URL='postgresql://postgres@localhost/eventcally' -(venv) flask db upgrade -(venv) gunicorn -c gunicorn.conf.py project:app +python3 -m venv env +source env/bin/activate +(env) pip install -r requirements.txt +(env) export DATABASE_URL='postgresql://postgres@localhost/eventcally' +(env) flask db upgrade +(env) gunicorn -c gunicorn.conf.py project:app ``` ## Scheduled/Cron jobs diff --git a/tests/conftest.py b/tests/conftest.py index d5f25ac..2c215c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import logging import os import warnings @@ -25,6 +26,10 @@ def pytest_generate_tests(metafunc): os.environ["GOOGLE_MAPS_API_KEY"] = "AIzaDummy" os.environ["TESTING"] = "1" + logging.getLogger().info("pytest_generate_tests") + logging.getLogger().info(os.environ["DATABASE_URL"]) + logging.getLogger().info(os.environ["TEST_DATABASE_URL"]) + @pytest.fixture def app(): @@ -48,6 +53,9 @@ def db(app): with app.app_context(): db.drop_all() db.create_all() + logging.getLogger().info("fixture-db") + logging.getLogger().info(os.environ["DATABASE_URL"]) + logging.getLogger().info(os.environ["TEST_DATABASE_URL"]) stamp() create_initial_data() From 07ce95e2963ae0e981bf8e74b1d02376899a4314 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 18:40:27 +0200 Subject: [PATCH 07/12] Library updates #432 --- migrations/README | 2 +- migrations/alembic.ini | 7 ++++++- migrations/env.py | 46 ++++++++++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/migrations/README b/migrations/README index 98e4f9c..0e04844 100644 --- a/migrations/README +++ b/migrations/README @@ -1 +1 @@ -Generic single-database configuration. \ No newline at end of file +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini index 6969e2d..ed8a140 100644 --- a/migrations/alembic.ini +++ b/migrations/alembic.ini @@ -11,7 +11,7 @@ # Logging configuration [loggers] -keys = root,sqlalchemy,alembic +keys = root,sqlalchemy,alembic,flask_migrate [handlers] keys = console @@ -34,6 +34,11 @@ level = INFO handlers = qualname = alembic +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + [handler_console] class = StreamHandler args = (sys.stderr,) diff --git a/migrations/env.py b/migrations/env.py index 22c2ca7..e6176de 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,10 +1,8 @@ -from __future__ import with_statement - import logging from logging.config import fileConfig from alembic import context -from sqlalchemy import engine_from_config, pool +from flask import current_app # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -15,17 +13,29 @@ config = context.config fileConfig(config.config_file_name) logger = logging.getLogger("alembic.env") + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions["migrate"].db.get_engine() + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions["migrate"].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace("%", "%%") + except AttributeError: + return str(get_engine().url).replace("%", "%%") + + # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata -from flask import current_app - -config.set_main_option( - "sqlalchemy.url", - str(current_app.extensions["migrate"].db.engine.url).replace("%", "%%"), -) -target_metadata = current_app.extensions["migrate"].db.metadata +config.set_main_option("sqlalchemy.url", get_engine_url()) +target_db = current_app.extensions["migrate"].db # other values from the config, defined by the needs of env.py, # can be acquired: @@ -33,6 +43,12 @@ target_metadata = current_app.extensions["migrate"].db.metadata # ... etc. +def get_metadata(): + if hasattr(target_db, "metadatas"): + return target_db.metadatas[None] + return target_db.metadata + + def exclude_tables_from_config(config_): tables_ = config_.get("tables", None) if tables_ is not None: @@ -65,7 +81,7 @@ def run_migrations_offline(): url = config.get_main_option("sqlalchemy.url") context.configure( url=url, - target_metadata=target_metadata, + target_metadata=get_metadata(), literal_binds=True, include_object=include_object, ) @@ -92,16 +108,12 @@ def run_migrations_online(): directives[:] = [] logger.info("No changes in schema detected.") - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) + connectable = get_engine() with connectable.connect() as connection: context.configure( connection=connection, - target_metadata=target_metadata, + target_metadata=get_metadata(), process_revision_directives=process_revision_directives, include_object=include_object, **current_app.extensions["migrate"].configure_args From 84b21f28bd9777a2625ee26d11d5730e2fa9b4d3 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Sun, 16 Apr 2023 23:04:39 +0200 Subject: [PATCH 08/12] Library updates #432 --- .pre-commit-config.yaml | 10 +++++----- project/api/resources.py | 4 ++-- project/models/oauth.py | 19 ++++++++++++++++--- project/oauth2.py | 28 +++++++++++++++------------- project/views/oauth.py | 2 +- project/views/oauth2_token.py | 4 ++-- tests/conftest.py | 8 -------- tests/test_models.py | 7 ++++--- tests/views/test_oauth2_token.py | 2 +- 9 files changed, 46 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 309ed3c..6c58096 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,15 @@ repos: - repo: https://github.com/pycqa/isort - rev: 5.6.3 + rev: 5.12.0 hooks: - id: isort name: isort (python) - repo: https://github.com/psf/black - rev: stable + rev: 23.3.0 hooks: - id: black - language_version: python3.7 - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + language_version: python3.9 + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 hooks: - id: flake8 \ No newline at end of file diff --git a/project/api/resources.py b/project/api/resources.py index 165b68b..273f381 100644 --- a/project/api/resources.py +++ b/project/api/resources.py @@ -22,12 +22,12 @@ def etag_cache(func): return wrapper -def require_api_access(scope=None, operator="AND", optional=False): +def require_api_access(scopes=None, optional=False): def inner_decorator(func): def wrapped(*args, **kwargs): # see authlib ResourceProtector#__call__ try: # pragma: no cover try: - require_oauth.acquire_token(scope, operator) + require_oauth.acquire_token(scopes) except MissingAuthorizationError as error: if optional: return func(*args, **kwargs) diff --git a/project/models/oauth.py b/project/models/oauth.py index 849a4c2..4a9d868 100644 --- a/project/models/oauth.py +++ b/project/models/oauth.py @@ -41,6 +41,12 @@ class OAuth2Client(db.Model, OAuth2ClientMixin): def check_token_endpoint_auth_method(self, method): return method in self.token_endpoint_auth_method + def check_endpoint_auth_method(self, method, endpoint): + if endpoint == "token": + return self.check_token_endpoint_auth_method(method) + + return True + class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): __tablename__ = "oauth2_code" @@ -66,8 +72,15 @@ class OAuth2Token(db.Model, OAuth2TokenMixin): .first() ) + @property + def expires_at(self): + return self.issued_at + self.expires_in + def is_refresh_token_active(self): - if self.revoked: + if self.is_revoked(): return False - expires_at = self.issued_at + self.expires_in * 2 - return expires_at >= time.time() + + return self.expires_at >= time.time() + + def revoke_token(self): + self.access_token_revoked_at = int(time.time()) diff --git a/project/oauth2.py b/project/oauth2.py index 5a1850a..8589eb5 100644 --- a/project/oauth2.py +++ b/project/oauth2.py @@ -108,19 +108,21 @@ class RefreshTokenGrant(grants.RefreshTokenGrant): class MyIntrospectionEndpoint(IntrospectionEndpoint): CLIENT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"] - def query_token(self, token, token_type_hint, client): + def query_token(self, token_string, token_type_hint): if token_type_hint == "access_token": - tok = OAuth2Token.query.filter_by(access_token=token).first() + tok = OAuth2Token.query.filter_by(access_token=token_string).first() elif token_type_hint == "refresh_token": - tok = OAuth2Token.query.filter_by(refresh_token=token).first() + tok = OAuth2Token.query.filter_by(refresh_token=token_string).first() else: # without token_type_hint - tok = OAuth2Token.query.filter_by(access_token=token).first() + tok = OAuth2Token.query.filter_by(access_token=token_string).first() if not tok: - tok = OAuth2Token.query.filter_by(refresh_token=token).first() - if tok: - if tok.client_id == client.client_id: - return tok + tok = OAuth2Token.query.filter_by(refresh_token=token_string).first() + + return tok + + def check_permission(self, token, client, request): + return token.client_id == client.client_id def introspect_token(self, token): return { @@ -132,7 +134,7 @@ class MyIntrospectionEndpoint(IntrospectionEndpoint): "sub": str(token.user.id), "aud": token.client_id, "iss": get_issuer(), - "exp": token.get_expires_at(), + "exp": token.expires_at, "iat": token.issued_at, } @@ -154,11 +156,11 @@ def create_revocation_endpoint(session, token_model): class _RevocationEndpoint(RevocationEndpoint): CLIENT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"] - def query_token(self, token, token_type_hint, client): - return query_token(token, token_type_hint, client) + def query_token(self, token_string, token_type_hint): + return query_token(token_string, token_type_hint) - def revoke_token(self, token): - token.revoked = True + def revoke_token(self, token, request): + token.revoke_token() session.add(token) session.commit() diff --git a/project/views/oauth.py b/project/views/oauth.py index 406de8f..46340cf 100644 --- a/project/views/oauth.py +++ b/project/views/oauth.py @@ -23,7 +23,7 @@ def authorize(): return authorization.create_authorization_response(grant_user=grant_user) else: try: - grant = authorization.validate_consent_request(end_user=user) + grant = authorization.get_consent_grant(end_user=user) except OAuth2Error as error: return error.description, error.status_code diff --git a/project/views/oauth2_token.py b/project/views/oauth2_token.py index f90393b..0763189 100644 --- a/project/views/oauth2_token.py +++ b/project/views/oauth2_token.py @@ -16,14 +16,14 @@ def oauth2_token_revoke(id): oauth2_token = OAuth2Token.query.get_or_404(id) owner_access_or_401(oauth2_token.user_id) - if oauth2_token.revoked: + if oauth2_token.is_revoked() > 0: return redirect(url_for("oauth2_tokens")) form = RevokeOAuth2TokenForm() if form.validate_on_submit(): try: - oauth2_token.revoked = True + oauth2_token.revoke_token() db.session.commit() flash(gettext("OAuth2 token successfully revoked"), "success") return redirect(url_for("oauth2_tokens")) diff --git a/tests/conftest.py b/tests/conftest.py index 2c215c8..d5f25ac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import logging import os import warnings @@ -26,10 +25,6 @@ def pytest_generate_tests(metafunc): os.environ["GOOGLE_MAPS_API_KEY"] = "AIzaDummy" os.environ["TESTING"] = "1" - logging.getLogger().info("pytest_generate_tests") - logging.getLogger().info(os.environ["DATABASE_URL"]) - logging.getLogger().info(os.environ["TEST_DATABASE_URL"]) - @pytest.fixture def app(): @@ -53,9 +48,6 @@ def db(app): with app.app_context(): db.drop_all() db.create_all() - logging.getLogger().info("fixture-db") - logging.getLogger().info(os.environ["DATABASE_URL"]) - logging.getLogger().info(os.environ["TEST_DATABASE_URL"]) stamp() create_initial_data() diff --git a/tests/test_models.py b/tests/test_models.py index 4c773f4..79a487e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -110,14 +110,15 @@ def test_event_has_multiple_dates(client, app, db, seeder): assert event_without_recc.has_multiple_dates() is False -def test_oauth2_token(client, app): +def test_oauth2_token(client, app, seeder): + import time + from project.models import OAuth2Token token = OAuth2Token() - token.revoked = True + token.access_token_revoked_at = int(time.time()) assert not token.is_refresh_token_active() - token.revoked = False token.issued_at = 0 token.expires_in = 0 assert not token.is_refresh_token_active() diff --git a/tests/views/test_oauth2_token.py b/tests/views/test_oauth2_token.py index 1432935..85ab4fd 100644 --- a/tests/views/test_oauth2_token.py +++ b/tests/views/test_oauth2_token.py @@ -42,7 +42,7 @@ def test_revoke(client, seeder, utils, app, mocker, db_error): from project.models import OAuth2Token oauth2_token = OAuth2Token.query.get(oauth2_token_id) - assert oauth2_token.revoked + assert oauth2_token.is_revoked() > 0 # Kann nicht zweimal revoked werden response = utils.get(url) From 45e6334076caa63569055da69668959a10aff9e5 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Mon, 17 Apr 2023 08:18:21 +0200 Subject: [PATCH 09/12] Library updates #432 --- .github/workflows/test.yml | 2 +- cypress/e2e/event.cy.js | 4 +-- project/api/event/schemas.py | 26 ++++++++++---------- project/api/event_date_definition/schemas.py | 2 +- project/api/schemas.py | 6 ++--- project/cli/test.py | 9 ++++--- project/forms/security.py | 4 +++ project/forms/widgets.py | 5 ++-- project/imageutils.py | 4 +-- 9 files changed, 34 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 290e740..95a78b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,7 +63,7 @@ jobs: pip install -r requirements.txt - name: Run tests - run: pytest --log-cli-level=DEBUG --cov=project --splits 4 --group ${{ matrix.group }} + run: pytest --cov=project --splits 4 --group ${{ matrix.group }} env: TEST_DATABASE_URL: postgresql://postgres:postgres@localhost/eventcally_tests TEST_REDIS_URL: redis://localhost:6379 diff --git a/cypress/e2e/event.cy.js b/cypress/e2e/event.cy.js index 479d485..792759d 100644 --- a/cypress/e2e/event.cy.js +++ b/cypress/e2e/event.cy.js @@ -1,6 +1,6 @@ describe("Event", () => { [{ recurrence: false }, { recurrence: true }].forEach(function (test) { - it("creates event with recurrence=" + test.recurrence, () => { + it.only("creates event with recurrence=" + test.recurrence, () => { cy.login(); cy.createAdminUnit().then(function (adminUnitId) { cy.visit("/admin_unit/" + adminUnitId + "/events/create"); @@ -48,7 +48,7 @@ describe("Event", () => { }); }); - it("saves draft", () => { + it.only("saves draft", () => { cy.login(); cy.createAdminUnit().then(function (adminUnitId) { cy.visit("/admin_unit/" + adminUnitId + "/events/create"); diff --git a/project/api/event/schemas.py b/project/api/event/schemas.py index e7908b0..3163254 100644 --- a/project/api/event/schemas.py +++ b/project/api/event/schemas.py @@ -88,11 +88,11 @@ class EventBaseSchemaMixin(TrackableSchemaMixin): } ) kid_friendly = marshmallow.auto_field( - missing=False, + load_default=False, metadata={"description": "If the event is particularly suitable for children."}, ) accessible_for_free = marshmallow.auto_field( - missing=False, + load_default=False, metadata={"description": "If the event is accessible for free."}, ) age_from = marshmallow.auto_field( @@ -103,19 +103,19 @@ class EventBaseSchemaMixin(TrackableSchemaMixin): ) target_group_origin = EnumField( EventTargetGroupOrigin, - missing=EventTargetGroupOrigin.both, + load_default=EventTargetGroupOrigin.both, metadata={ "description": "Whether the event is particularly suitable for tourists or residents." }, ) attendance_mode = EnumField( EventAttendanceMode, - missing=EventAttendanceMode.offline, + load_default=EventAttendanceMode.offline, metadata={"description": "Choose how people can attend the event."}, ) status = EnumField( EventStatus, - missing=EventStatus.scheduled, + load_default=EventStatus.scheduled, metadata={"description": "Select the status of the event."}, ) previous_start_date = CustomDateTimeField( @@ -124,13 +124,13 @@ class EventBaseSchemaMixin(TrackableSchemaMixin): }, ) registration_required = marshmallow.auto_field( - missing=False, + load_default=False, metadata={ "description": "If the participants needs to register for the event." }, ) booked_up = marshmallow.auto_field( - missing=False, + load_default=False, metadata={"description": "If the event is booked up or sold out."}, ) expected_participants = marshmallow.auto_field( @@ -143,7 +143,7 @@ class EventBaseSchemaMixin(TrackableSchemaMixin): ) public_status = EnumField( PublicStatus, - missing=PublicStatus.published, + load_default=PublicStatus.published, metadata={"description": "Public status of the event."}, ) @@ -296,8 +296,8 @@ class EventWriteSchemaMixin(object): metadata={"description": "Categories that fit the event."}, ) rating = marshmallow.auto_field( - missing=50, - default=50, + load_default=50, + dump_default=50, validate=validate.OneOf([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]), metadata={ "description": "How relevant the event is to your organization. 0 (Little relevant), 50 (Default), 100 (Highlight)." @@ -322,7 +322,7 @@ class EventPostRequestSchema( date_definitions = fields.List( fields.Nested(EventDateDefinitionPostRequestSchema), - default=None, + dump_default=None, required=True, validate=[validate.Length(min=1)], metadata={"description": "At least one date definition."}, @@ -339,7 +339,7 @@ class EventPatchRequestSchema( date_definitions = fields.List( fields.Nested(EventDateDefinitionPatchRequestSchema), - default=None, + dump_default=None, required=True, validate=[validate.Length(min=1)], metadata={"description": "At least one date definition."}, @@ -370,6 +370,6 @@ class EventImportRequestSchema(marshmallow.Schema): ) public_status = EnumField( PublicStatus, - missing=PublicStatus.published, + load_default=PublicStatus.published, metadata={"description": "Public status of the event."}, ) diff --git a/project/api/event_date_definition/schemas.py b/project/api/event_date_definition/schemas.py index babf320..4bb5da5 100644 --- a/project/api/event_date_definition/schemas.py +++ b/project/api/event_date_definition/schemas.py @@ -36,7 +36,7 @@ class EventDateDefinitionBaseSchemaMixin(object): }, ) allday = fields.Bool( - missing=False, + load_default=False, metadata={"description": "If the event is an all-day event."}, ) recurrence_rule = fields.Str( diff --git a/project/api/schemas.py b/project/api/schemas.py index 042860a..49ddaf3 100644 --- a/project/api/schemas.py +++ b/project/api/schemas.py @@ -25,7 +25,7 @@ class SQLAlchemyBaseSchema(marshmallow.SQLAlchemySchema): class IdSchemaMixin(object): - id = marshmallow.auto_field(dump_only=True, default=missing) + id = marshmallow.auto_field(dump_only=True, dump_default=missing) class WriteIdSchemaMixin(object): @@ -60,13 +60,13 @@ class UnprocessableEntityResponseSchema(ErrorResponseSchema): class PaginationRequestSchema(marshmallow.Schema): page = fields.Integer( required=False, - default=1, + dump_default=1, validate=validate.Range(min=1), metadata={"description": "The page number (1 indexed)."}, ) per_page = fields.Integer( required=False, - default=20, + dump_default=20, validate=validate.Range(min=1, max=50), metadata={"description": "Items per page"}, ) diff --git a/project/cli/test.py b/project/cli/test.py index efb3f42..8fe5e0b 100644 --- a/project/cli/test.py +++ b/project/cli/test.py @@ -4,7 +4,7 @@ import click from flask.cli import AppGroup from flask_migrate import stamp from flask_security.confirmable import confirm_user -from sqlalchemy import MetaData +from sqlalchemy import MetaData, text from project import app, db from project.api import scope_list @@ -75,14 +75,15 @@ def _create_user( @test_cli.command("reset") @click.option("--seed/--no-seed", default=False) def reset(seed): - meta = MetaData(bind=db.engine, reflect=True) + meta = MetaData() + meta.reflect(db.engine) con = db.engine.connect() trans = con.begin() for table in meta.sorted_tables: - con.execute(f'ALTER TABLE "{table.name}" DISABLE TRIGGER ALL;') + con.execute(text(f'ALTER TABLE "{table.name}" DISABLE TRIGGER ALL;')) con.execute(table.delete()) - con.execute(f'ALTER TABLE "{table.name}" ENABLE TRIGGER ALL;') + con.execute(text(f'ALTER TABLE "{table.name}" ENABLE TRIGGER ALL;')) trans.commit() diff --git a/project/forms/security.py b/project/forms/security.py index 7e99da0..eb16e3c 100644 --- a/project/forms/security.py +++ b/project/forms/security.py @@ -35,6 +35,10 @@ class ExtendedConfirmRegisterForm(ConfirmRegisterForm): class ExtendedLoginForm(LoginForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._fields["email"].flags.required = True + def validate(self, **kwargs): result = super().validate(**kwargs) diff --git a/project/forms/widgets.py b/project/forms/widgets.py index a82a439..4f7cb14 100644 --- a/project/forms/widgets.py +++ b/project/forms/widgets.py @@ -26,13 +26,14 @@ class CustomDateTimeWidget: time = date_value.strftime("%H:%M") kwargs_class = kwargs.pop("class", "") + required = True if field.flags.required else False date_class = kwargs_class + " datepicker" date_params = html_params( name=field.name, id=id, value=date, - required=field.flags.required, + required=required, class_=date_class, **kwargs ) @@ -42,7 +43,7 @@ class CustomDateTimeWidget: name=field.name, id=id + "-time", value=time, - required=field.flags.required, + required=required, class_=time_class, **kwargs ) diff --git a/project/imageutils.py b/project/imageutils.py index 182ccda..af98d8b 100644 --- a/project/imageutils.py +++ b/project/imageutils.py @@ -54,14 +54,14 @@ def resize_image_to_min(image: PIL.Image) -> PIL.Image: width = int(math.ceil(image.width * ratio)) height = int(math.ceil(image.height * ratio)) format = image.format - result = image.resize((width, height), PIL.Image.LANCZOS) + result = image.resize((width, height), PIL.Image.Resampling.LANCZOS) result.format = format return result def resize_image_to_max(image: PIL.Image): if image.width > max_image_size or image.height > max_image_size: - image.thumbnail((max_image_size, max_image_size), PIL.Image.ANTIALIAS) + image.thumbnail((max_image_size, max_image_size), PIL.Image.Resampling.LANCZOS) def validate_image(image: PIL.Image): From dc604949ad51c1657fb4c416f6f3807b44b729c5 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Mon, 17 Apr 2023 08:53:53 +0200 Subject: [PATCH 10/12] Library updates #432 --- migrations/env.py | 7 +------ project/api/schemas.py | 6 +++--- project/static/site.js | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/migrations/env.py b/migrations/env.py index e6176de..2615d73 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -15,12 +15,7 @@ logger = logging.getLogger("alembic.env") def get_engine(): - try: - # this works with Flask-SQLAlchemy<3 and Alchemical - return current_app.extensions["migrate"].db.get_engine() - except TypeError: - # this works with Flask-SQLAlchemy>=3 - return current_app.extensions["migrate"].db.engine + return current_app.extensions["migrate"].db.engine def get_engine_url(): diff --git a/project/api/schemas.py b/project/api/schemas.py index 49ddaf3..8766984 100644 --- a/project/api/schemas.py +++ b/project/api/schemas.py @@ -11,11 +11,11 @@ class SQLAlchemyBaseSchema(marshmallow.SQLAlchemySchema): def make_post_schema(self): for name, field in self.fields.items(): if not field.required: - if field.missing is missing: + if field.load_default is missing: if isinstance(field, fields.List): - field.missing = list() + field.load_default = list() else: - field.missing = None + field.load_default = None field.allow_none = True def make_patch_schema(self): diff --git a/project/static/site.js b/project/static/site.js index 8a0abe6..06ee2e7 100644 --- a/project/static/site.js +++ b/project/static/site.js @@ -156,7 +156,7 @@ function start_datepicker(input) { $(document).find(data_allday_attr).on('change', function() { $("#" + hidden_field_id + "-time").toggle(!this.checked); if (data_range_to_attr) { - $(data_range_to_attr + "-time").toggle(!this.checked); + $(document).find(data_range_to_attr + "-time").toggle(!this.checked); } onAlldayChecked(this, hidden_field_id) From 75b0b0ed86798da5f8acf6de679f1bb5d6ce99e1 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Mon, 17 Apr 2023 09:05:42 +0200 Subject: [PATCH 11/12] Library updates #432 --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f9d6a5..f4fb8d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -613,9 +613,9 @@ } }, "node_modules/cypress": { - "version": "12.8.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.8.1.tgz", - "integrity": "sha512-lIFbKdaSYAOarNLHNFa2aPZu6YSF+8UY4VRXMxJrFUnk6RvfG0AWsZ7/qle/aIz30TNUD4aOihz2ZgS4vuQVSA==", + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz", + "integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2512,9 +2512,9 @@ } }, "cypress": { - "version": "12.8.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.8.1.tgz", - "integrity": "sha512-lIFbKdaSYAOarNLHNFa2aPZu6YSF+8UY4VRXMxJrFUnk6RvfG0AWsZ7/qle/aIz30TNUD4aOihz2ZgS4vuQVSA==", + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz", + "integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==", "dev": true, "requires": { "@cypress/request": "^2.88.10", From b833a9756ac6b07604789a30ed70a9bd3f46d127 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Mon, 17 Apr 2023 09:09:12 +0200 Subject: [PATCH 12/12] Library updates #432 --- package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4fb8d5..8b363e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1411,9 +1411,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -1617,9 +1617,9 @@ } }, "node_modules/qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true, "engines": { "node": ">=0.6" @@ -3120,9 +3120,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -3286,9 +3286,9 @@ "dev": true }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true }, "request-progress": {