API Write Access with OAuth2 #104

This commit is contained in:
Daniel Grams 2021-02-09 11:12:15 +01:00
parent 041571cd28
commit c5c14f9675
29 changed files with 937 additions and 173 deletions

View File

@ -1,8 +1,8 @@
"""empty message
Revision ID: ddb85cb1c21e
Revision ID: 1fb9f679defb
Revises: b1a6e7630185
Create Date: 2021-02-02 15:17:21.988363
Create Date: 2021-02-07 17:54:44.257540
"""
from alembic import op
@ -12,7 +12,7 @@ from project import dbtypes
# revision identifiers, used by Alembic.
revision = "ddb85cb1c21e"
revision = "1fb9f679defb"
down_revision = "b1a6e7630185"
branch_labels = None
depends_on = None
@ -74,6 +74,10 @@ def upgrade():
["refresh_token"],
unique=False,
)
op.alter_column(
"event", "event_place_id", existing_type=sa.INTEGER(), nullable=False
)
op.alter_column("event", "organizer_id", existing_type=sa.INTEGER(), nullable=False)
op.create_unique_constraint(
"eventplace_name_admin_unit_id", "eventplace", ["name", "admin_unit_id"]
)
@ -83,6 +87,10 @@ def upgrade():
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("eventplace_name_admin_unit_id", "eventplace", type_="unique")
op.alter_column("event", "organizer_id", existing_type=sa.INTEGER(), nullable=True)
op.alter_column(
"event", "event_place_id", existing_type=sa.INTEGER(), nullable=True
)
op.drop_index(op.f("ix_oauth2_token_refresh_token"), table_name="oauth2_token")
op.drop_table("oauth2_token")
op.drop_table("oauth2_code")

View File

@ -1,6 +1,6 @@
from flask_restful import Api
from sqlalchemy.exc import IntegrityError
from psycopg2.errorcodes import UNIQUE_VIOLATION
from psycopg2.errorcodes import UNIQUE_VIOLATION, CHECK_VIOLATION
from werkzeug.exceptions import HTTPException, UnprocessableEntity
from marshmallow import ValidationError
from project.utils import get_localized_scope
@ -22,15 +22,23 @@ class RestApi(Api):
data = {}
code = 500
if (
isinstance(err, IntegrityError)
and err.orig
and err.orig.pgcode == UNIQUE_VIOLATION
):
data["name"] = "Unique Violation"
data[
"message"
] = "An entry with the entered values already exists. Duplicate entries are not allowed."
if isinstance(err, IntegrityError) and err.orig:
if err.orig.pgcode == UNIQUE_VIOLATION:
data["name"] = "Unique Violation"
data[
"message"
] = "An entry with the entered values already exists. Duplicate entries are not allowed."
elif err.orig.pgcode == CHECK_VIOLATION:
data["name"] = "Check Violation"
if hasattr(err.orig, "message") and getattr(err.orig, "message", None):
data["message"] = err.orig.message
else:
data["message"] = "Action violates database constraint."
else:
data["name"] = "Integrity Error"
data["message"] = "Action violates database integrity."
code = 400
schema = ErrorResponseSchema()
elif isinstance(err, HTTPException):
@ -47,25 +55,18 @@ class RestApi(Api):
data["message"] = err.description
code = err.code
schema = UnprocessableEntityResponseSchema()
self.fill_validation_data(err.exc, data)
if (
getattr(err.exc, "args", None)
and isinstance(err.exc.args, tuple)
and len(err.exc.args) > 0
):
arg = err.exc.args[0]
if isinstance(arg, dict):
errors = []
for field, messages in arg.items():
if isinstance(messages, list):
for message in messages:
error = {"field": field, "message": message}
errors.append(error)
if len(errors) > 0:
data["errors"] = errors
else:
schema = ErrorResponseSchema()
elif isinstance(err, ValidationError):
data["name"] = "Unprocessable Entity"
data[
"message"
] = "The request was well-formed but was unable to be followed due to semantic errors."
code = 422
schema = UnprocessableEntityResponseSchema()
self.fill_validation_data(err, data)
# Call default error handler that propagates error further
try:
@ -76,6 +77,24 @@ class RestApi(Api):
return schema.dump(data), code
def fill_validation_data(self, err: ValidationError, data: dict):
if (
getattr(err, "args", None)
and isinstance(err.args, tuple)
and len(err.args) > 0
):
arg = err.args[0]
if isinstance(arg, dict):
errors = []
for field, messages in arg.items():
if isinstance(messages, list):
for message in messages:
error = {"field": field, "message": message}
errors.append(error)
if len(errors) > 0:
data["errors"] = errors
scope_list = [
"organizer:write",

View File

@ -1,4 +1,5 @@
from project.api import add_api_resource
from flask import make_response
from flask_apispec import marshal_with, doc, use_kwargs
from project.api.resources import BaseResource
from project.api.event.schemas import (
@ -7,15 +8,26 @@ from project.api.event.schemas import (
EventListResponseSchema,
EventSearchRequestSchema,
EventSearchResponseSchema,
EventPostRequestSchema,
EventPatchRequestSchema,
)
from project.api.event_date.schemas import (
EventDateListRequestSchema,
EventDateListResponseSchema,
)
from project.models import Event, EventDate
from project.services.event import get_events_query, get_event_with_details_or_404
from project.services.event import (
get_events_query,
get_event_with_details_or_404,
update_event,
)
from project.services.event_search import EventSearchParams
from sqlalchemy.orm import lazyload, load_only
from project.oauth2 import require_oauth
from authlib.integrations.flask_oauth2 import current_token
from project import db
from project.access import access_or_401, login_api_user_or_401
from project.views.event import send_referenced_event_changed_mails
class EventListResource(BaseResource):
@ -33,6 +45,55 @@ class EventResource(BaseResource):
def get(self, id):
return get_event_with_details_or_404(id)
@doc(
summary="Update event", tags=["Events"], security=[{"oauth2": ["event:write"]}]
)
@use_kwargs(EventPostRequestSchema, location="json", apply=False)
@marshal_with(None, 204)
@require_oauth("event:write")
def put(self, id):
login_api_user_or_401(current_token.user)
event = Event.query.get_or_404(id)
access_or_401(event.admin_unit_id, "event:update")
event = self.update_instance(EventPostRequestSchema, instance=event)
update_event(event)
db.session.commit()
send_referenced_event_changed_mails(event)
return make_response("", 204)
@doc(summary="Patch event", tags=["Events"], security=[{"oauth2": ["event:write"]}])
@use_kwargs(EventPatchRequestSchema, location="json", apply=False)
@marshal_with(None, 204)
@require_oauth("event:write")
def patch(self, id):
login_api_user_or_401(current_token.user)
event = Event.query.get_or_404(id)
access_or_401(event.admin_unit_id, "event:update")
event = self.update_instance(EventPatchRequestSchema, instance=event)
update_event(event)
db.session.commit()
send_referenced_event_changed_mails(event)
return make_response("", 204)
@doc(
summary="Delete event", tags=["Events"], security=[{"oauth2": ["event:write"]}]
)
@marshal_with(None, 204)
@require_oauth("event:write")
def delete(self, id):
login_api_user_or_401(current_token.user)
event = Event.query.get_or_404(id)
access_or_401(event.admin_unit_id, "event:delete")
db.session.delete(event)
db.session.commit()
return make_response("", 204)
class EventDatesResource(BaseResource):
@doc(summary="List dates for event", tags=["Events", "Event Dates"])

View File

@ -1,5 +1,5 @@
from project.api import marshmallow
from marshmallow import fields, validate
from marshmallow import fields, validate, ValidationError
from marshmallow_enum import EnumField
from project.models import (
Event,
@ -7,51 +7,145 @@ from project.models import (
EventTargetGroupOrigin,
EventAttendanceMode,
)
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
from project.api.schemas import (
SQLAlchemyBaseSchema,
IdSchemaMixin,
TrackableSchemaMixin,
PaginationRequestSchema,
PaginationResponseSchema,
)
from project.api.organization.schemas import OrganizationRefSchema
from project.api.organizer.schemas import OrganizerRefSchema
from project.api.organizer.schemas import OrganizerRefSchema, OrganizerWriteIdSchema
from project.api.image.schemas import ImageSchema
from project.api.place.schemas import PlaceRefSchema, PlaceSearchItemSchema
from project.api.place.schemas import (
PlaceRefSchema,
PlaceSearchItemSchema,
PlaceWriteIdSchema,
)
from project.api.event_category.schemas import (
EventCategoryRefSchema,
EventCategoryIdSchema,
EventCategoryWriteIdSchema,
)
from project.api.fields import CustomDateTimeField
from dateutil.rrule import rrulestr
class EventBaseSchema(marshmallow.SQLAlchemySchema):
class EventModelSchema(SQLAlchemyBaseSchema):
class Meta:
model = Event
id = marshmallow.auto_field()
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
name = marshmallow.auto_field()
description = marshmallow.auto_field()
external_link = marshmallow.auto_field()
ticket_link = marshmallow.auto_field()
tags = marshmallow.auto_field()
kid_friendly = marshmallow.auto_field()
accessible_for_free = marshmallow.auto_field()
age_from = marshmallow.auto_field()
age_to = marshmallow.auto_field()
target_group_origin = EnumField(EventTargetGroupOrigin)
attendance_mode = EnumField(EventAttendanceMode)
status = EnumField(EventStatus)
previous_start_date = marshmallow.auto_field()
registration_required = marshmallow.auto_field()
booked_up = marshmallow.auto_field()
expected_participants = marshmallow.auto_field()
price_info = marshmallow.auto_field()
recurrence_rule = marshmallow.auto_field()
start = marshmallow.auto_field()
end = marshmallow.auto_field()
load_instance = True
class EventSchema(EventBaseSchema):
class EventIdSchema(EventModelSchema, IdSchemaMixin):
pass
def validate_recurrence_rule(recurrence_rule):
try:
rrulestr(recurrence_rule, forceset=True)
except Exception as e:
raise ValidationError(str(e))
class EventBaseSchemaMixin(TrackableSchemaMixin):
name = marshmallow.auto_field(
required=True,
validate=validate.Length(min=3, max=255),
metadata={"description": "A short, meaningful name for the event."},
)
description = marshmallow.auto_field(
metadata={"description": "Description of the event"},
)
external_link = marshmallow.auto_field(
validate=[validate.URL(), validate.Length(max=255)],
metadata={
"description": "A link to an external website containing more information about the event."
},
)
ticket_link = marshmallow.auto_field(
validate=[validate.URL(), validate.Length(max=255)],
metadata={"description": "A link where tickets can be purchased."},
)
tags = marshmallow.auto_field(
metadata={
"description": "Comma separated keywords with which the event should be found. Words do not need to be entered if they are already in the name or description."
}
)
kid_friendly = marshmallow.auto_field(
missing=False,
metadata={"description": "If the event is particularly suitable for children."},
)
accessible_for_free = marshmallow.auto_field(
missing=False,
metadata={"description": "If the event is accessible for free."},
)
age_from = marshmallow.auto_field(
metadata={"description": "The minimum age that participants should be."},
)
age_to = marshmallow.auto_field(
metadata={"description": "The maximum age that participants should be."},
)
target_group_origin = EnumField(
EventTargetGroupOrigin,
missing=EventTargetGroupOrigin.both,
metadata={
"description": "Whether the event is particularly suitable for tourists or residents."
},
)
attendance_mode = EnumField(
EventAttendanceMode,
missing=EventAttendanceMode.offline,
metadata={"description": "Choose how people can attend the event."},
)
status = EnumField(
EventStatus,
missing=EventStatus.scheduled,
metadata={"description": "Select the status of the event."},
)
previous_start_date = CustomDateTimeField(
metadata={
"description": "When the event should have taken place before it was postponed."
},
)
registration_required = marshmallow.auto_field(
missing=False,
metadata={
"description": "If the participants needs to register for the event."
},
)
booked_up = marshmallow.auto_field(
missing=False,
metadata={"description": "If the event is booked up or sold out."},
)
expected_participants = marshmallow.auto_field(
metadata={"description": "The estimated expected attendance."},
)
price_info = marshmallow.auto_field(
metadata={
"description": "Price information in textual form. E.g., different prices for adults and children."
},
)
recurrence_rule = marshmallow.auto_field(
validate=validate_recurrence_rule,
metadata={
"description": "If the event takes place regularly. Format: RFC 5545."
},
)
start = CustomDateTimeField(
required=True,
metadata={
"description": "When the event will take place. If the event takes place regularly, enter when the first date will begin."
},
)
end = CustomDateTimeField(
metadata={
"description": "When the event will end. An event can last a maximum of 24 hours. If the event takes place regularly, enter when the first date will end."
},
)
class EventSchema(EventIdSchema, EventBaseSchemaMixin):
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
organizer = fields.Nested(OrganizerRefSchema)
place = fields.Nested(PlaceRefSchema, attribute="event_place")
@ -59,7 +153,7 @@ class EventSchema(EventBaseSchema):
categories = fields.List(fields.Nested(EventCategoryRefSchema))
class EventDumpSchema(EventBaseSchema):
class EventDumpSchema(EventIdSchema, EventBaseSchemaMixin):
organization_id = fields.Int(attribute="admin_unit_id")
organizer_id = fields.Int()
place_id = fields.Int(attribute="event_place_id")
@ -69,18 +163,11 @@ class EventDumpSchema(EventBaseSchema):
)
class EventRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Event
id = marshmallow.auto_field()
class EventRefSchema(EventIdSchema):
name = marshmallow.auto_field()
class EventSearchItemSchema(EventRefSchema):
class Meta:
model = Event
description = marshmallow.auto_field()
start = marshmallow.auto_field()
end = marshmallow.auto_field()
@ -145,3 +232,45 @@ class EventSearchResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventSearchItemSchema), metadata={"description": "Events"}
)
class EventWriteSchemaMixin(object):
organizer = fields.Nested(
OrganizerWriteIdSchema,
required=True,
metadata={"description": "Who is organizing the event."},
)
place = fields.Nested(
PlaceWriteIdSchema,
required=True,
attribute="event_place",
metadata={"description": "Where the event takes place."},
)
categories = fields.List(
fields.Nested(EventCategoryWriteIdSchema),
metadata={"description": "Categories that fit the event."},
)
rating = marshmallow.auto_field(
missing=50,
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), 5 (Default), 10 (Highlight)."
},
)
class EventPostRequestSchema(
EventModelSchema, EventBaseSchemaMixin, EventWriteSchemaMixin
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_post_schema()
class EventPatchRequestSchema(
EventModelSchema, EventBaseSchemaMixin, EventWriteSchemaMixin
):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_patch_schema()

View File

@ -1,20 +1,33 @@
from marshmallow import fields
from project.api import marshmallow
from project.models import EventCategory
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
from project.api.schemas import (
SQLAlchemyBaseSchema,
IdSchemaMixin,
PaginationRequestSchema,
PaginationResponseSchema,
WriteIdSchemaMixin,
)
class EventCategoryIdSchema(marshmallow.SQLAlchemySchema):
class EventCategoryModelSchema(SQLAlchemyBaseSchema):
class Meta:
model = EventCategory
load_instance = True
id = marshmallow.auto_field()
class EventCategoryIdSchema(EventCategoryModelSchema, IdSchemaMixin):
pass
class EventCategoryRefSchema(EventCategoryIdSchema):
name = marshmallow.auto_field()
class EventCategoryWriteIdSchema(EventCategoryModelSchema, WriteIdSchemaMixin):
pass
class EventCategoryDumpSchema(EventCategoryRefSchema):
pass

View File

@ -12,6 +12,7 @@ from project.api.schemas import PaginationRequestSchema, PaginationResponseSchem
class EventDateSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventDate
load_instance = True
id = marshmallow.auto_field()
start = marshmallow.auto_field()
@ -22,6 +23,7 @@ class EventDateSchema(marshmallow.SQLAlchemySchema):
class EventDateRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventDate
load_instance = True
id = marshmallow.auto_field()
start = marshmallow.auto_field()
@ -44,6 +46,7 @@ class EventDateSearchRequestSchema(EventSearchRequestSchema):
class EventDateSearchItemSchema(EventDateRefSchema):
class Meta:
model = EventDate
load_instance = True
end = marshmallow.auto_field()
event = fields.Nested(EventSearchItemSchema)

View File

@ -9,6 +9,7 @@ from project.api.organization.schemas import OrganizationRefSchema
class EventReferenceIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventReference
load_instance = True
id = marshmallow.auto_field()

View File

@ -1,4 +1,5 @@
from marshmallow import fields, ValidationError
from project.dateutils import berlin_tz
class NumericStr(fields.String):
@ -13,3 +14,13 @@ class NumericStr(fields.String):
return float(value)
except ValueError as error:
raise ValidationError("Must be a numeric value.") from error
class CustomDateTimeField(fields.DateTime):
def deserialize(self, value, attr, data, **kwargs):
result = super().deserialize(value, attr, data, **kwargs)
if result and result.tzinfo is None:
result = berlin_tz.localize(result)
return result

View File

@ -5,6 +5,7 @@ from project.models import Image
class ImageIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Image
load_instance = True
id = marshmallow.auto_field()

View File

@ -2,16 +2,13 @@ from marshmallow import validate, validates_schema, ValidationError
from project.api import marshmallow
from project.models import Location
from project.api.fields import NumericStr
from project.api.schemas import (
SQLAlchemyBaseSchema,
PostSchema,
PatchSchema,
)
from project.api.schemas import SQLAlchemyBaseSchema
class LocationModelSchema(SQLAlchemyBaseSchema):
class Meta:
model = Location
load_instance = True
class LocationBaseSchemaMixin(object):
@ -55,13 +52,13 @@ class LocationSearchItemSchema(LocationSchema):
pass
class LocationPostRequestSchema(
PostSchema, LocationModelSchema, LocationBaseSchemaMixin
):
pass
class LocationPostRequestSchema(LocationModelSchema, LocationBaseSchemaMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_post_schema()
class LocationPatchRequestSchema(
PatchSchema, LocationModelSchema, LocationBaseSchemaMixin
):
pass
class LocationPatchRequestSchema(LocationModelSchema, LocationBaseSchemaMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_patch_schema()

View File

@ -6,7 +6,7 @@ from project.api.organization.schemas import (
OrganizationListRequestSchema,
OrganizationListResponseSchema,
)
from project.models import AdminUnit
from project.models import AdminUnit, Event
from project.api.event_date.schemas import (
EventDateSearchRequestSchema,
EventDateSearchResponseSchema,
@ -14,6 +14,10 @@ from project.api.event_date.schemas import (
from project.api.event.schemas import (
EventSearchRequestSchema,
EventSearchResponseSchema,
EventListRequestSchema,
EventListResponseSchema,
EventPostRequestSchema,
EventIdSchema,
)
from project.api.organizer.schemas import (
OrganizerListRequestSchema,
@ -35,7 +39,7 @@ from project.api.place.schemas import (
PlaceIdSchema,
PlacePostRequestSchema,
)
from project.services.event import get_event_dates_query, get_events_query
from project.services.event import get_event_dates_query, get_events_query, insert_event
from project.services.event_search import EventSearchParams
from project.services.admin_unit import (
get_admin_unit_query,
@ -93,6 +97,37 @@ class OrganizationEventSearchResource(BaseResource):
return pagination
class OrganizationEventListResource(BaseResource):
@doc(summary="List events of organization", tags=["Organizations", "Events"])
@use_kwargs(EventListRequestSchema, location=("query"))
@marshal_with(EventListResponseSchema)
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
pagination = Event.query.filter(Event.admin_unit_id == admin_unit.id).paginate()
return pagination
@doc(
summary="Add new event",
tags=["Organizations", "Events"],
security=[{"oauth2": ["event:write"]}],
)
@use_kwargs(EventPostRequestSchema, location="json", apply=False)
@marshal_with(EventIdSchema, 201)
@require_oauth("event:write")
def post(self, id):
login_api_user_or_401(current_token.user)
admin_unit = get_admin_unit_for_manage_or_404(id)
access_or_401(admin_unit, "event:create")
event = self.create_instance(
EventPostRequestSchema, admin_unit_id=admin_unit.id
)
insert_event(event)
db.session.commit()
return event, 201
class OrganizationListResource(BaseResource):
@doc(summary="List organizations", tags=["Organizations"])
@use_kwargs(OrganizationListRequestSchema, location=("query"))
@ -121,18 +156,17 @@ class OrganizationOrganizerListResource(BaseResource):
tags=["Organizations", "Organizers"],
security=[{"oauth2": ["organizer:write"]}],
)
@use_kwargs(OrganizerPostRequestSchema, location="json")
@use_kwargs(OrganizerPostRequestSchema, location="json", apply=False)
@marshal_with(OrganizerIdSchema, 201)
@require_oauth("organizer:write")
def post(self, id, **kwargs):
def post(self, id):
login_api_user_or_401(current_token.user)
admin_unit = get_admin_unit_for_manage_or_404(id)
access_or_401(admin_unit, "organizer:create")
organizer = OrganizerPostRequestSchema(load_instance=True).load(
kwargs, session=db.session
organizer = self.create_instance(
OrganizerPostRequestSchema, admin_unit_id=admin_unit.id
)
organizer.admin_unit_id = admin_unit.id
db.session.add(organizer)
db.session.commit()
@ -155,18 +189,17 @@ class OrganizationPlaceListResource(BaseResource):
tags=["Organizations", "Places"],
security=[{"oauth2": ["place:write"]}],
)
@use_kwargs(PlacePostRequestSchema, location="json")
@use_kwargs(PlacePostRequestSchema, location="json", apply=False)
@marshal_with(PlaceIdSchema, 201)
@require_oauth("place:write")
def post(self, id, **kwargs):
def post(self, id):
login_api_user_or_401(current_token.user)
admin_unit = get_admin_unit_for_manage_or_404(id)
access_or_401(admin_unit, "place:create")
place = PlacePostRequestSchema(load_instance=True).load(
kwargs, session=db.session
place = self.create_instance(
PlacePostRequestSchema, admin_unit_id=admin_unit.id
)
place.admin_unit_id = admin_unit.id
db.session.add(place)
db.session.commit()
@ -212,6 +245,11 @@ add_api_resource(
"/organizations/<int:id>/events/search",
"api_v1_organization_event_search",
)
add_api_resource(
OrganizationEventListResource,
"/organizations/<int:id>/events",
"api_v1_organization_event_list",
)
add_api_resource(OrganizationListResource, "/organizations", "api_v1_organization_list")
add_api_resource(
OrganizationOrganizerListResource,

View File

@ -9,6 +9,7 @@ from project.api.schemas import PaginationRequestSchema, PaginationResponseSchem
class OrganizationIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = AdminUnit
load_instance = True
id = marshmallow.auto_field()

View File

@ -25,17 +25,15 @@ class OrganizerResource(BaseResource):
tags=["Organizers"],
security=[{"oauth2": ["organizer:write"]}],
)
@use_kwargs(OrganizerPostRequestSchema, location="json")
@use_kwargs(OrganizerPostRequestSchema, location="json", apply=False)
@marshal_with(None, 204)
@require_oauth("organizer:write")
def put(self, id, **kwargs):
def put(self, id):
login_api_user_or_401(current_token.user)
organizer = EventOrganizer.query.get_or_404(id)
access_or_401(organizer.adminunit, "organizer:update")
organizer = OrganizerPostRequestSchema(load_instance=True).load(
kwargs, session=db.session, instance=organizer
)
organizer = self.update_instance(OrganizerPostRequestSchema, instance=organizer)
db.session.commit()
return make_response("", 204)
@ -45,16 +43,16 @@ class OrganizerResource(BaseResource):
tags=["Organizers"],
security=[{"oauth2": ["organizer:write"]}],
)
@use_kwargs(OrganizerPatchRequestSchema, location="json")
@use_kwargs(OrganizerPatchRequestSchema, location="json", apply=False)
@marshal_with(None, 204)
@require_oauth("organizer:write")
def patch(self, id, **kwargs):
def patch(self, id):
login_api_user_or_401(current_token.user)
organizer = EventOrganizer.query.get_or_404(id)
access_or_401(organizer.adminunit, "organizer:update")
organizer = OrganizerPatchRequestSchema(load_instance=True).load(
kwargs, session=db.session, instance=organizer
organizer = self.update_instance(
OrganizerPatchRequestSchema, instance=organizer
)
db.session.commit()

View File

@ -11,9 +11,8 @@ from project.api.organization.schemas import OrganizationRefSchema
from project.api.schemas import (
SQLAlchemyBaseSchema,
IdSchemaMixin,
WriteIdSchemaMixin,
TrackableSchemaMixin,
PostSchema,
PatchSchema,
PaginationRequestSchema,
PaginationResponseSchema,
)
@ -22,12 +21,17 @@ from project.api.schemas import (
class OrganizerModelSchema(SQLAlchemyBaseSchema):
class Meta:
model = EventOrganizer
load_instance = True
class OrganizerIdSchema(OrganizerModelSchema, IdSchemaMixin):
pass
class OrganizerWriteIdSchema(OrganizerModelSchema, WriteIdSchemaMixin):
pass
class OrganizerBaseSchemaMixin(TrackableSchemaMixin):
name = marshmallow.auto_field(
required=True, validate=validate.Length(min=3, max=255)
@ -68,13 +72,17 @@ class OrganizerListResponseSchema(PaginationResponseSchema):
)
class OrganizerPostRequestSchema(
PostSchema, OrganizerModelSchema, OrganizerBaseSchemaMixin
):
location = fields.Nested(LocationPostRequestSchema, missing=None)
class OrganizerPostRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_post_schema()
location = fields.Nested(LocationPostRequestSchema)
class OrganizerPatchRequestSchema(
PatchSchema, OrganizerModelSchema, OrganizerBaseSchemaMixin
):
location = fields.Nested(LocationPatchRequestSchema, allow_none=True)
class OrganizerPatchRequestSchema(OrganizerModelSchema, OrganizerBaseSchemaMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_patch_schema()
location = fields.Nested(LocationPatchRequestSchema)

View File

@ -23,33 +23,29 @@ class PlaceResource(BaseResource):
@doc(
summary="Update place", tags=["Places"], security=[{"oauth2": ["place:write"]}]
)
@use_kwargs(PlacePostRequestSchema, location="json")
@use_kwargs(PlacePostRequestSchema, location="json", apply=False)
@marshal_with(None, 204)
@require_oauth("place:write")
def put(self, id, **kwargs):
def put(self, id):
login_api_user_or_401(current_token.user)
place = EventPlace.query.get_or_404(id)
access_or_401(place.adminunit, "place:update")
place = PlacePostRequestSchema(load_instance=True).load(
kwargs, session=db.session, instance=place
)
place = self.update_instance(PlacePostRequestSchema, instance=place)
db.session.commit()
return make_response("", 204)
@doc(summary="Patch place", tags=["Places"], security=[{"oauth2": ["place:write"]}])
@use_kwargs(PlacePatchRequestSchema, location="json")
@use_kwargs(PlacePatchRequestSchema, location="json", apply=False)
@marshal_with(None, 204)
@require_oauth("place:write")
def patch(self, id, **kwargs):
def patch(self, id):
login_api_user_or_401(current_token.user)
place = EventPlace.query.get_or_404(id)
access_or_401(place.adminunit, "place:update")
place = PlacePatchRequestSchema(load_instance=True).load(
kwargs, session=db.session, instance=place
)
place = self.update_instance(PlacePatchRequestSchema, instance=place)
db.session.commit()
return make_response("", 204)

View File

@ -12,9 +12,8 @@ from project.api.organization.schemas import OrganizationRefSchema
from project.api.schemas import (
SQLAlchemyBaseSchema,
IdSchemaMixin,
WriteIdSchemaMixin,
TrackableSchemaMixin,
PostSchema,
PatchSchema,
PaginationRequestSchema,
PaginationResponseSchema,
)
@ -23,12 +22,17 @@ from project.api.schemas import (
class PlaceModelSchema(SQLAlchemyBaseSchema):
class Meta:
model = EventPlace
load_instance = True
class PlaceIdSchema(PlaceModelSchema, IdSchemaMixin):
pass
class PlaceWriteIdSchema(PlaceModelSchema, WriteIdSchemaMixin):
pass
class PlaceBaseSchemaMixin(TrackableSchemaMixin):
name = marshmallow.auto_field(
required=True, validate=validate.Length(min=3, max=255)
@ -54,9 +58,6 @@ class PlaceRefSchema(PlaceIdSchema):
class PlaceSearchItemSchema(PlaceRefSchema):
class Meta:
model = EventPlace
location = fields.Nested(LocationSearchItemSchema)
@ -72,9 +73,17 @@ class PlaceListResponseSchema(PaginationResponseSchema):
)
class PlacePostRequestSchema(PostSchema, PlaceModelSchema, PlaceBaseSchemaMixin):
location = fields.Nested(LocationPostRequestSchema, missing=None)
class PlacePostRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_post_schema()
location = fields.Nested(LocationPostRequestSchema)
class PlacePatchRequestSchema(PatchSchema, PlaceModelSchema, PlaceBaseSchemaMixin):
location = fields.Nested(LocationPatchRequestSchema, allow_none=True)
class PlacePatchRequestSchema(PlaceModelSchema, PlaceBaseSchemaMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.make_patch_schema()
location = fields.Nested(LocationPatchRequestSchema)

View File

@ -2,6 +2,7 @@ from flask import request
from flask_apispec import marshal_with
from flask_apispec.views import MethodResource
from functools import wraps
from project import db
from project.api.schemas import ErrorResponseSchema, UnprocessableEntityResponseSchema
@ -19,3 +20,27 @@ def etag_cache(func):
@marshal_with(UnprocessableEntityResponseSchema, 422, "Unprocessable Entity")
class BaseResource(MethodResource):
decorators = [etag_cache]
def create_instance(self, schema_cls, **kwargs):
instance = schema_cls().load(request.json, session=db.session)
for key, value in kwargs.items():
if hasattr(instance, key):
setattr(instance, key, value)
validate = getattr(instance, "validate", None)
if callable(validate):
validate()
return instance
def update_instance(self, schema_cls, instance):
instance = schema_cls().load(
request.json, session=db.session, instance=instance
)
validate = getattr(instance, "validate", None)
if callable(validate):
validate()
return instance

View File

@ -4,23 +4,20 @@ from marshmallow import fields, validate, missing
class SQLAlchemyBaseSchema(marshmallow.SQLAlchemySchema):
def __init__(self, *args, **kwargs):
load_instance = kwargs.pop("load_instance", False)
super().__init__(*args, **kwargs)
self.opts.load_instance = load_instance
class PostSchema(object):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self._declared_fields.items():
def make_post_schema(self):
for name, field in self.fields.items():
if not field.required:
field.missing = None
if field.missing is missing:
if isinstance(field, fields.List):
field.missing = list()
else:
field.missing = None
field.allow_none = True
class PatchSchema(object):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self._declared_fields.items():
def make_patch_schema(self):
for name, field in self.fields.items():
field.required = False
field.allow_none = True
@ -29,6 +26,10 @@ class IdSchemaMixin(object):
id = marshmallow.auto_field(dump_only=True, default=missing)
class WriteIdSchemaMixin(object):
id = marshmallow.auto_field(required=True)
class TrackableSchemaMixin(object):
created_at = marshmallow.auto_field(dump_only=True)
updated_at = marshmallow.auto_field(dump_only=True)

View File

@ -2,9 +2,7 @@ import datetime
from json import JSONEncoder
from flask import url_for
from project.models import EventAttendanceMode, EventStatus
import pytz
berlin_tz = pytz.timezone("Europe/Berlin")
from project.dateutils import berlin_tz
class DateTimeEncoder(JSONEncoder):

View File

@ -1,4 +1,5 @@
from project import db
from project.utils import make_check_violation
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, backref, deferred, object_session
@ -30,6 +31,7 @@ from authlib.integrations.sqla_oauth2 import (
OAuth2TokenMixin,
)
import time
from dateutil.relativedelta import relativedelta
# Base
@ -585,11 +587,11 @@ class Event(db.Model, TrackableMixin, EventMixin):
admin_unit_id = db.Column(db.Integer, db.ForeignKey("adminunit.id"), nullable=False)
admin_unit = db.relationship("AdminUnit", backref=db.backref("events", lazy=True))
organizer_id = db.Column(
db.Integer, db.ForeignKey("eventorganizer.id"), nullable=True
db.Integer, db.ForeignKey("eventorganizer.id"), nullable=False
)
organizer = db.relationship("EventOrganizer", uselist=False)
event_place_id = db.Column(
db.Integer, db.ForeignKey("eventplace.id"), nullable=True
db.Integer, db.ForeignKey("eventplace.id"), nullable=False
)
event_place = db.relationship("EventPlace", uselist=False)
@ -621,6 +623,21 @@ class Event(db.Model, TrackableMixin, EventMixin):
else:
return None
def validate(self):
if self.organizer and self.organizer.admin_unit_id != self.admin_unit_id:
raise make_check_violation("Invalid organizer")
if self.event_place and self.event_place.admin_unit_id != self.admin_unit_id:
raise make_check_violation("Invalid place")
if self.start and self.end:
if self.start > self.end:
raise make_check_violation("The start must be before the end.")
max_end = self.start + relativedelta(days=1)
if self.end > max_end:
raise make_check_violation("An event can last a maximum of 24 hours.")
@listens_for(Event, "before_insert")
@listens_for(Event, "before_update")

View File

@ -21,8 +21,12 @@ from sqlalchemy.orm import joinedload, contains_eager, defaultload
from dateutil.relativedelta import relativedelta
def get_event_category(category_name):
return EventCategory.query.filter_by(name=category_name).first()
def upsert_event_category(category_name):
result = EventCategory.query.filter_by(name=category_name).first()
result = get_event_category(category_name)
if result is None:
result = EventCategory(name=category_name)
db.session.add(result)

View File

@ -1,6 +1,8 @@
from flask_babelex import lazy_gettext
import pathlib
import os
from sqlalchemy.exc import IntegrityError
from psycopg2.errorcodes import UNIQUE_VIOLATION, CHECK_VIOLATION
def get_event_category_name(category):
@ -28,3 +30,23 @@ def make_dir(path):
def split_by_crlf(s):
return [v for v in s.splitlines() if v]
def make_integrity_error(
pgcode: str, message: str = "", statement: str = None
) -> IntegrityError:
class Psycog2Error(object):
def __init__(self, pgcode, message):
self.pgcode = pgcode
self.message = message
orig = Psycog2Error(pgcode, message)
return IntegrityError(statement, list(), orig)
def make_check_violation(message: str = None, statement: str = "") -> IntegrityError:
return make_integrity_error(CHECK_VIOLATION, message, statement)
def make_unique_violation(message: str = None, statement: str = "") -> IntegrityError:
return make_integrity_error(UNIQUE_VIOLATION, message, statement)

View File

@ -2,17 +2,10 @@ import pytest
from project.api import RestApi
class Psycog2Error(object):
def __init__(self, pgcode):
self.pgcode = pgcode
def test_handle_error_unique(app):
from sqlalchemy.exc import IntegrityError
from psycopg2.errorcodes import UNIQUE_VIOLATION
from project.utils import make_unique_violation
orig = Psycog2Error(UNIQUE_VIOLATION)
error = IntegrityError("Select", list(), orig)
error = make_unique_violation()
api = RestApi(app)
(data, code) = api.handle_error(error)
@ -20,6 +13,28 @@ def test_handle_error_unique(app):
assert data["name"] == "Unique Violation"
def test_handle_error_checkViolation(app):
from project.utils import make_check_violation
error = make_check_violation()
api = RestApi(app)
(data, code) = api.handle_error(error)
assert code == 400
assert data["name"] == "Check Violation"
def test_handle_error_integrity(app):
from project.utils import make_integrity_error
error = make_integrity_error("custom")
api = RestApi(app)
(data, code) = api.handle_error(error)
assert code == 400
assert data["name"] == "Integrity Error"
def test_handle_error_httpException(app):
from werkzeug.exceptions import InternalServerError
@ -47,6 +62,19 @@ def test_handle_error_unprocessableEntity(app):
assert data["errors"][0]["message"] == "Required"
def test_handle_error_validationError(app):
from marshmallow import ValidationError
args = {"name": ["Required"]}
validation_error = ValidationError(args)
api = RestApi(app)
(data, code) = api.handle_error(validation_error)
assert code == 422
assert data["errors"][0]["field"] == "name"
assert data["errors"][0]["message"] == "Required"
def test_handle_error_unspecificRaises(app):
error = Exception()
api = RestApi(app)

View File

@ -39,3 +39,317 @@ def test_dates(client, seeder, utils):
url = utils.get_url("api_v1_event_dates", id=event_id)
utils.get_ok(url)
def create_put(
place_id, organizer_id, name="Neuer Name", start="2021-02-07T11:00:00.000Z"
):
return {
"name": name,
"start": start,
"place": {"id": place_id},
"organizer": {"id": organizer_id},
}
def test_put(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
put["rating"] = 10
put["description"] = "Neue Beschreibung"
put["external_link"] = "http://www.google.de"
put["ticket_link"] = "http://www.yahoo.de"
put["tags"] = "Freizeit, Klönen"
put["kid_friendly"] = True
put["accessible_for_free"] = True
put["age_from"] = 9
put["age_to"] = 99
put["target_group_origin"] = "tourist"
put["attendance_mode"] = "online"
put["status"] = "movedOnline"
put["previous_start_date"] = "2021-02-07T10:00:00+01:00"
put["registration_required"] = True
put["booked_up"] = True
put["expected_participants"] = 500
put["price_info"] = "Erwachsene 5€, Kinder 2€."
put["recurrence_rule"] = "RRULE:FREQ=DAILY;COUNT=7"
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import (
Event,
EventStatus,
EventTargetGroupOrigin,
EventAttendanceMode,
)
from project.dateutils import create_berlin_date
event = Event.query.get(event_id)
assert event.name == "Neuer Name"
assert event.event_place_id == place_id
assert event.organizer_id == organizer_id
assert event.rating == put["rating"]
assert event.description == put["description"]
assert event.external_link == put["external_link"]
assert event.ticket_link == put["ticket_link"]
assert event.tags == put["tags"]
assert event.kid_friendly == put["kid_friendly"]
assert event.accessible_for_free == put["accessible_for_free"]
assert event.age_from == put["age_from"]
assert event.age_to == put["age_to"]
assert event.target_group_origin == EventTargetGroupOrigin.tourist
assert event.attendance_mode == EventAttendanceMode.online
assert event.status == EventStatus.movedOnline
assert event.previous_start_date == create_berlin_date(2021, 2, 7, 10, 0)
assert event.registration_required == put["registration_required"]
assert event.booked_up == put["booked_up"]
assert event.expected_participants == put["expected_participants"]
assert event.price_info == put["price_info"]
assert event.recurrence_rule == put["recurrence_rule"]
len_dates = len(event.dates)
assert len_dates == 7
def test_put_invalidRecurrenceRule(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
put["recurrence_rule"] = "RRULE:FREQ=SCHMAILY;COUNT=7"
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_unprocessable_entity(response)
def test_put_missingName(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
del put["name"]
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_unprocessable_entity(response)
def test_put_missingPlace(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
del put["place"]
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_unprocessable_entity(response)
def test_put_placeFromAnotherAdminUnit(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
other_admin_unit_id = seeder.create_admin_unit(user_id, "Other Crew")
place_id = seeder.upsert_default_event_place(other_admin_unit_id)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, create_put(place_id, organizer_id))
utils.assert_response_bad_request(response)
utils.assert_response_api_error(response, "Check Violation")
def test_put_missingOrganizer(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
del put["organizer"]
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_unprocessable_entity(response)
def test_put_organizerFromAnotherAdminUnit(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
other_admin_unit_id = seeder.create_admin_unit(user_id, "Other Crew")
organizer_id = seeder.upsert_default_event_organizer(other_admin_unit_id)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, create_put(place_id, organizer_id))
utils.assert_response_bad_request(response)
utils.assert_response_api_error(response, "Check Violation")
def test_put_invalidDateFormat(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id, start="07.02.2021T11:00:00.000Z")
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_unprocessable_entity(response)
def test_put_startAfterEnd(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
put["start"] = "2021-02-07T11:00:00.000Z"
put["end"] = "2021-02-07T10:59:00.000Z"
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_bad_request(response)
def test_put_durationMoreThan24Hours(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id)
put["start"] = "2021-02-07T11:00:00.000Z"
put["end"] = "2021-02-08T11:01:00.000Z"
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_bad_request(response)
def test_put_categories(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
category_id = seeder.get_event_category_id("Art")
put = create_put(place_id, organizer_id)
put["categories"] = [{"id": category_id}]
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
assert event.category.name == "Art"
def test_put_dateWithTimezone(client, seeder, utils, app):
from project.dateutils import create_berlin_date
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id, start="2030-12-31T14:30:00+01:00")
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
expected = create_berlin_date(2030, 12, 31, 14, 30)
event = Event.query.get(event_id)
assert event.start == expected
def test_put_dateWithoutTimezone(client, seeder, utils, app):
from project.dateutils import create_berlin_date
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
put = create_put(place_id, organizer_id, start="2030-12-31T14:30:00")
url = utils.get_url("api_v1_event", id=event_id)
response = utils.put_json(url, put)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
expected = create_berlin_date(2030, 12, 31, 14, 30)
event = Event.query.get(event_id)
assert event.start == expected
def test_patch(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.patch_json(url, {"description": "Neu"})
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
assert event.name == "Name"
assert event.description == "Neu"
def test_patch_startAfterEnd(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.patch_json(url, {"start": "2021-02-07T11:00:00.000Z"})
utils.assert_response_no_content(response)
response = utils.patch_json(url, {"end": "2021-02-07T10:59:00.000Z"})
utils.assert_response_bad_request(response)
def test_delete(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
event_id = seeder.create_event(admin_unit_id)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.delete(url)
utils.assert_response_no_content(response)
with app.app_context():
from project.models import Event
event = Event.query.get(event_id)
assert event is None

View File

@ -41,7 +41,7 @@ def test_numeric_str_deserialize(latitude, longitude, valid):
"longitude": longitude,
}
schema = LocationPostRequestSchema(load_instance=True)
schema = LocationPostRequestSchema()
if valid:
location = schema.load(data)

View File

@ -41,9 +41,7 @@ def test_organizers(client, seeder, utils):
def test_organizers_post(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
url = utils.get_url(
"api_v1_organization_organizer_list", id=admin_unit_id, name="crew"
)
url = utils.get_url("api_v1_organization_organizer_list", id=admin_unit_id)
response = utils.post_json(url, {"name": "Neuer Organisator"})
utils.assert_response_created(response)
assert "id" in response.json
@ -59,6 +57,45 @@ def test_organizers_post(client, seeder, utils, app):
assert organizer is not None
def test_events(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("api_v1_organization_event_list", id=admin_unit_id)
utils.get_ok(url)
def test_events_post(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
place_id = seeder.upsert_default_event_place(admin_unit_id)
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
url = utils.get_url("api_v1_organization_event_list", id=admin_unit_id)
response = utils.post_json(
url,
{
"name": "Fest",
"start": "2021-02-07T11:00:00.000Z",
"place": {"id": place_id},
"organizer": {"id": organizer_id},
},
)
utils.assert_response_created(response)
assert "id" in response.json
with app.app_context():
from project.models import Event
event = (
Event.query.filter(Event.admin_unit_id == admin_unit_id)
.filter(Event.name == "Fest")
.first()
)
assert event is not None
assert event.event_place_id == place_id
assert event.organizer_id == organizer_id
def test_places(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.upsert_default_event_place(admin_unit_id)
@ -71,7 +108,13 @@ def test_places_post(client, seeder, utils, app):
user_id, admin_unit_id = seeder.setup_api_access()
url = utils.get_url("api_v1_organization_place_list", id=admin_unit_id, name="crew")
response = utils.post_json(url, {"name": "Neuer Ort"})
response = utils.post_json(
url,
{
"name": "Neuer Ort",
"location": {"street": "Straße 1", "postalCode": "38640", "city": "Goslar"},
},
)
utils.assert_response_created(response)
assert "id" in response.json
@ -84,6 +127,10 @@ def test_places_post(client, seeder, utils, app):
.first()
)
assert place is not None
assert place.name == "Neuer Ort"
assert place.location.street == "Straße 1"
assert place.location.postalCode == "38640"
assert place.location.city == "Goslar"
def test_references_incoming(client, seeder, utils):

View File

@ -166,6 +166,12 @@ class Seeder(object):
self._utils.authorize(client_id, client_secret, scope)
return (user_id, admin_unit_id)
def get_event_category_id(self, category_name):
from project.services.event import get_event_category
category = get_event_category(category_name)
return category.id
def create_event(self, admin_unit_id, recurrence_rule=None):
from project.models import Event
from project.services.event import insert_event, upsert_event_category

View File

@ -169,6 +169,15 @@ class UtilActions(object):
def assert_response_no_content(self, response):
assert response.status_code == 204
def assert_response_unprocessable_entity(self, response):
assert response.status_code == 422
def assert_response_bad_request(self, response):
assert response.status_code == 400
def assert_response_api_error(self, response, message):
assert response.json["name"] == message
def get_unauthorized(self, url):
response = self._client.get(url)
self.assert_response_unauthorized(response)

View File

@ -186,7 +186,7 @@ def test_create_startAfterEnd(client, app, utils, seeder, mocker):
)
def test_create_DurationMoreThan24Hours(client, app, utils, seeder, mocker):
def test_create_durationMoreThan24Hours(client, app, utils, seeder, mocker):
user_id, admin_unit_id = seeder.setup_base()
place_id = seeder.upsert_default_event_place(admin_unit_id)