Merge pull request #433 from eventcally/issues/432

Library updates #432
This commit is contained in:
Daniel Grams 2023-04-17 09:21:20 +02:00 committed by GitHub
commit b5c78617d0
60 changed files with 464 additions and 358 deletions

View File

@ -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: |

View File

@ -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

View File

@ -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: |

View File

@ -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

View File

@ -1,4 +1,4 @@
FROM python:3.7
FROM python:3.9
# Add rsync
RUN apt update -qq && apt upgrade -y && apt autoremove -y

View File

@ -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");

View File

@ -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
@ -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

View File

@ -1 +1 @@
Generic single-database configuration.
Single-database configuration for Flask.

View File

@ -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,)

View File

@ -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,24 @@ config = context.config
fileConfig(config.config_file_name)
logger = logging.getLogger("alembic.env")
def get_engine():
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 +38,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 +76,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 +103,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

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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
@ -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 ###

View File

@ -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 ###

View File

@ -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

36
package-lock.json generated
View File

@ -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": {
@ -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"
@ -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",
@ -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": {

View File

@ -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."},
)

View File

@ -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(

View File

@ -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)

View File

@ -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):
@ -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"},
)

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -35,8 +35,12 @@ class ExtendedConfirmRegisterForm(ConfirmRegisterForm):
class ExtendedLoginForm(LoginForm):
def validate(self):
result = super().validate()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._fields["email"].flags.required = True
def validate(self, **kwargs):
result = super().validate(**kwargs)
if not result and self.requires_confirmation:
flash_message(

View File

@ -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
@ -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
)
@ -164,14 +165,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 +195,8 @@ class HTML5StringField(StringField):
default,
widget,
render_kw,
name,
_form,
_name,
_prefix,
_translations,
_meta,

View File

@ -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):

View File

@ -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
)

View File

@ -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(

View File

@ -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

View File

@ -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())

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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"))

View File

@ -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()

View File

@ -1,21 +1,23 @@
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
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
celery==5.2.7
certifi==2020.12.5
cffi==1.14.4
@ -27,109 +29,121 @@ 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
filelock==3.0.12
flake8==3.8.4
Flask==1.1.2
flask-apispec==0.11.0
exceptiongroup==1.1.1
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.5.0
Flask-Login==0.6.2
Flask-Mail==0.9.1
flask-marshmallow==0.14.0
Flask-Migrate==2.5.3
flask-marshmallow==0.15.0
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
googlemaps==4.4.7
gunicorn==20.0.4
icalendar==4.0.7
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.10.0
greenlet==2.0.2
gunicorn==20.1.0
icalendar==5.0.5
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
isort==5.12.0
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
marshmallow==3.10.0
MarkupSafe==2.1.2
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
oyaml==1.0
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==6.1.2
pytest-cov==2.10.1
pytest-datadir==1.3.1
pytest-mock==3.3.1
pytest-split==0.6.0
pytest==7.3.1
pytest-celery==0.0.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
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==1.3.20
SQLAlchemy-Utils==0.36.8
swagger-spec-validator==2.7.3
TatSu==4.4.0
SQLAlchemy==2.0.9
SQLAlchemy-Utils==0.41.0
structlog==23.1.0
swagger-spec-validator==3.0.3
tabulate==0.9.0
TatSu==5.8.3
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
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==1.0.1
WTForms==2.3.3
WTForms-SQLAlchemy==0.2
Werkzeug==2.2.3
wimpy==0.6
WTForms==3.0.1
WTForms-SQLAlchemy==0.3
yarg==0.1.9
zipp==3.4.0

View File

@ -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"

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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."

View File

@ -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)

View File

@ -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."