Merge pull request #56 from DanielGrams/issue/55-dump

Dump data to file #55
This commit is contained in:
Daniel Grams 2021-01-18 08:16:42 +01:00 committed by GitHub
commit 1dce5c4f23
47 changed files with 590 additions and 195 deletions

View File

@ -23,3 +23,4 @@
**/secrets.dev.yaml
**/values.dev.yaml
README.md
tmp

View File

@ -47,6 +47,7 @@ Jobs that should run on a regular basis.
```sh
flask event update-recurring-dates
flask dump all
```
## Administration

View File

@ -15,6 +15,11 @@ from flask_restful import Api
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask_apispec.extension import FlaskApiSpec
import pathlib
import logging
logging.basicConfig()
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
# Create app
app = Flask(__name__)
@ -39,6 +44,11 @@ app.config["SECURITY_PASSWORD_SALT"] = os.environ.get(
"SECURITY_PASSWORD_SALT", "146585145368132386173505678016728509634"
)
# Temporary pathes
temp_path = os.path.join(app.root_path, "tmp")
dump_path = os.path.join(temp_path, "dump")
pathlib.Path(dump_path).mkdir(parents=True, exist_ok=True)
# i18n
app.config["BABEL_DEFAULT_LOCALE"] = "de"
app.config["BABEL_DEFAULT_TIMEZONE"] = "Europe/Berlin"
@ -55,9 +65,12 @@ app.config.update(
{
"APISPEC_SPEC": APISpec(
title="Oveda API",
version="1.0.0",
version="0.1.0",
plugins=[marshmallow_plugin],
openapi_version="2.0",
info=dict(
description="This API provides endpoints to interact with the Oveda data. At the moment, there is no authorization neeeded."
),
),
}
)
@ -123,6 +136,7 @@ from project.views import (
event_date,
event_place,
event_suggestion,
dump,
image,
manage,
organizer,
@ -140,6 +154,7 @@ import project.api
# Command line
import project.cli.event
import project.cli.dump
import project.cli.user
if __name__ == "__main__": # pragma: no cover

View File

@ -1,3 +1,6 @@
from project import rest_api, api_docs
def enum_to_properties(self, field, **kwargs):
"""
Add an OpenAPI extension for marshmallow_enum.EnumField instances
@ -9,6 +12,11 @@ def enum_to_properties(self, field, **kwargs):
return {}
def add_api_resource(resource, url, endpoint):
rest_api.add_resource(resource, url, endpoint=endpoint)
api_docs.register(resource, endpoint=endpoint)
from project import marshmallow_plugin
marshmallow_plugin.converter.add_attribute_function(enum_to_properties)
@ -16,6 +24,8 @@ marshmallow_plugin.converter.add_attribute_function(enum_to_properties)
import project.api.event.resources
import project.api.event_category.resources
import project.api.event_date.resources
import project.api.event_reference.resources
import project.api.dump.resources
import project.api.image.resources
import project.api.location.resources
import project.api.organization.resources

View File

View File

@ -0,0 +1,20 @@
from project.api import add_api_resource
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.schemas import NoneSchema
from project.api.dump.schemas import DumpResponseSchema
class DumpResource(MethodResource):
@doc(
summary="Dump model definition",
description="Always returns 404 because the endpoint is just for response definition of the dump data file. Go to the developers page to learn how to download the dump data file.",
tags=["Dump"],
)
@marshal_with(NoneSchema, 404)
@marshal_with(DumpResponseSchema, 200)
def get(self, **kwargs):
return None, 404
add_api_resource(DumpResource, "/dump", "api_v1_dump")

View File

@ -0,0 +1,21 @@
from project import marshmallow
from marshmallow import fields
from project.api.event.schemas import EventDumpSchema
from project.api.place.schemas import PlaceDumpSchema
from project.api.location.schemas import LocationDumpSchema
from project.api.event_category.schemas import EventCategoryDumpSchema
from project.api.organizer.schemas import OrganizerDumpSchema
from project.api.image.schemas import ImageDumpSchema
from project.api.organization.schemas import OrganizationDumpSchema
from project.api.event_reference.schemas import EventReferenceDumpSchema
class DumpResponseSchema(marshmallow.Schema):
events = fields.List(fields.Nested(EventDumpSchema))
places = fields.List(fields.Nested(PlaceDumpSchema))
locations = fields.List(fields.Nested(LocationDumpSchema))
event_categories = fields.List(fields.Nested(EventCategoryDumpSchema))
organizers = fields.List(fields.Nested(OrganizerDumpSchema))
images = fields.List(fields.Nested(ImageDumpSchema))
organizations = fields.List(fields.Nested(OrganizationDumpSchema))
event_references = fields.List(fields.Nested(EventReferenceDumpSchema))

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc, use_kwargs
from flask_apispec.views import MethodResource
from project.api.event.schemas import (
@ -18,7 +18,7 @@ from project.services.event_search import EventSearchParams
class EventListResource(MethodResource):
@doc(tags=["Events"])
@doc(summary="List events", tags=["Events"])
@use_kwargs(EventListRequestSchema, location=("query"))
@marshal_with(EventListResponseSchema)
def get(self, **kwargs):
@ -27,14 +27,14 @@ class EventListResource(MethodResource):
class EventResource(MethodResource):
@doc(tags=["Events"])
@doc(summary="Get event", tags=["Events"])
@marshal_with(EventSchema)
def get(self, id):
return Event.query.get_or_404(id)
class EventDatesResource(MethodResource):
@doc(tags=["Events", "Event Dates"])
@doc(summary="List dates for event", tags=["Events", "Event Dates"])
@use_kwargs(EventDateListRequestSchema, location=("query"))
@marshal_with(EventDateListResponseSchema)
def get(self, id):
@ -43,7 +43,7 @@ class EventDatesResource(MethodResource):
class EventSearchResource(MethodResource):
@doc(tags=["Events"])
@doc(summary="Search for events", tags=["Events"])
@use_kwargs(EventSearchRequestSchema, location=("query"))
@marshal_with(EventSearchResponseSchema)
def get(self, **kwargs):
@ -53,14 +53,7 @@ class EventSearchResource(MethodResource):
return pagination
rest_api.add_resource(EventListResource, "/events")
api_docs.register(EventListResource)
rest_api.add_resource(EventResource, "/events/<int:id>")
api_docs.register(EventResource)
rest_api.add_resource(EventDatesResource, "/events/<int:id>/dates")
api_docs.register(EventDatesResource)
rest_api.add_resource(EventSearchResource, "/events/search")
api_docs.register(EventSearchResource)
add_api_resource(EventListResource, "/events", "api_v1_event_list")
add_api_resource(EventResource, "/events/<int:id>", "api_v1_event")
add_api_resource(EventDatesResource, "/events/<int:id>/dates", "api_v1_event_dates")
add_api_resource(EventSearchResource, "/events/search", "api_v1_event_search")

View File

@ -12,26 +12,25 @@ from project.api.organization.schemas import OrganizationRefSchema
from project.api.organizer.schemas import OrganizerRefSchema
from project.api.image.schemas import ImageRefSchema
from project.api.place.schemas import PlaceRefSchema, PlaceSearchItemSchema
from project.api.event_category.schemas import EventCategoryRefSchema
from project.api.event_category.schemas import (
EventCategoryRefSchema,
EventCategoryIdSchema,
)
class EventSchema(marshmallow.SQLAlchemySchema):
class EventBaseSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Event
id = marshmallow.auto_field()
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
organizer = fields.Nested(OrganizerRefSchema)
place = fields.Nested(PlaceRefSchema, attribute="event_place")
name = marshmallow.auto_field()
description = marshmallow.auto_field()
external_link = marshmallow.auto_field()
ticket_link = marshmallow.auto_field()
photo = fields.Nested(ImageRefSchema)
categories = fields.List(fields.Nested(EventCategoryRefSchema))
tags = marshmallow.auto_field()
kid_friendly = marshmallow.auto_field()
accessible_for_free = marshmallow.auto_field()
@ -52,12 +51,29 @@ class EventSchema(marshmallow.SQLAlchemySchema):
end = marshmallow.auto_field()
class EventSchema(EventBaseSchema):
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
organizer = fields.Nested(OrganizerRefSchema)
place = fields.Nested(PlaceRefSchema, attribute="event_place")
photo = fields.Nested(ImageRefSchema)
categories = fields.List(fields.Nested(EventCategoryRefSchema))
class EventDumpSchema(EventBaseSchema):
organization_id = fields.Int(attribute="admin_unit_id")
organizer_id = fields.Int()
place_id = fields.Int(attribute="event_place_id")
photo_id = fields.Int()
category_ids = fields.Pluck(
EventCategoryIdSchema, "id", many=True, attribute="categories"
)
class EventRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Event
id = marshmallow.auto_field()
href = marshmallow.URLFor("eventresource", values=dict(id="<id>"))
name = marshmallow.auto_field()
@ -73,6 +89,7 @@ class EventSearchItemSchema(EventRefSchema):
place = fields.Nested(PlaceSearchItemSchema, attribute="event_place")
status = EnumField(EventStatus)
organizer = fields.Nested(OrganizerRefSchema)
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
categories = fields.List(fields.Nested(EventCategoryRefSchema))

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc, use_kwargs
from flask_apispec.views import MethodResource
from project.api.event_category.schemas import (
@ -9,7 +9,7 @@ from project.models import EventCategory
class EventCategoryListResource(MethodResource):
@doc(tags=["Event Categories"])
@doc(summary="List event categories", tags=["Event Categories"])
@use_kwargs(EventCategoryListRequestSchema, location=("query"))
@marshal_with(EventCategoryListResponseSchema)
def get(self, **kwargs):
@ -17,5 +17,6 @@ class EventCategoryListResource(MethodResource):
return pagination
rest_api.add_resource(EventCategoryListResource, "/event_categories")
api_docs.register(EventCategoryListResource)
add_api_resource(
EventCategoryListResource, "/event-categories", "api_v1_event_category_list"
)

View File

@ -4,14 +4,21 @@ from project.models import EventCategory
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
class EventCategoryRefSchema(marshmallow.SQLAlchemySchema):
class EventCategoryIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventCategory
id = marshmallow.auto_field()
class EventCategoryRefSchema(EventCategoryIdSchema):
name = marshmallow.auto_field()
class EventCategoryDumpSchema(EventCategoryRefSchema):
pass
class EventCategoryListRequestSchema(PaginationRequestSchema):
pass

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc, use_kwargs
from flask_apispec.views import MethodResource
from project.api.event_date.schemas import (
@ -14,7 +14,7 @@ from project.services.event_search import EventSearchParams
class EventDateListResource(MethodResource):
@doc(tags=["Event Dates"])
@doc(summary="List event dates", tags=["Event Dates"])
@use_kwargs(EventDateListRequestSchema, location=("query"))
@marshal_with(EventDateListResponseSchema)
def get(self, **kwargs):
@ -23,14 +23,14 @@ class EventDateListResource(MethodResource):
class EventDateResource(MethodResource):
@doc(tags=["Event Dates"])
@doc(summary="Get event date", tags=["Event Dates"])
@marshal_with(EventDateSchema)
def get(self, id):
return EventDate.query.get_or_404(id)
class EventDateSearchResource(MethodResource):
@doc(tags=["Event Dates"])
@doc(summary="Search for event dates", tags=["Event Dates"])
@use_kwargs(EventDateSearchRequestSchema, location=("query"))
@marshal_with(EventDateSearchResponseSchema)
def get(self, **kwargs):
@ -40,11 +40,8 @@ class EventDateSearchResource(MethodResource):
return pagination
rest_api.add_resource(EventDateListResource, "/event_dates")
api_docs.register(EventDateListResource)
rest_api.add_resource(EventDateResource, "/event_dates/<int:id>")
api_docs.register(EventDateResource)
rest_api.add_resource(EventDateSearchResource, "/event_dates/search")
api_docs.register(EventDateSearchResource)
add_api_resource(EventDateListResource, "/event-dates", "api_v1_event_date_list")
add_api_resource(EventDateResource, "/event-dates/<int:id>", "api_v1_event_date")
add_api_resource(
EventDateSearchResource, "/event-dates/search", "api_v1_event_date_search"
)

View File

@ -24,7 +24,6 @@ class EventDateRefSchema(marshmallow.SQLAlchemySchema):
model = EventDate
id = marshmallow.auto_field()
href = marshmallow.URLFor("eventdateresource", values=dict(id="<id>"))
start = marshmallow.auto_field()

View File

View File

@ -0,0 +1,17 @@
from project.api import add_api_resource
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.event_reference.schemas import EventReferenceSchema
from project.models import EventReference
class EventReferenceResource(MethodResource):
@doc(summary="Get event reference", tags=["Event References"])
@marshal_with(EventReferenceSchema)
def get(self, id):
return EventReference.query.get_or_404(id)
add_api_resource(
EventReferenceResource, "/event-references/<int:id>", "api_v1_event_reference"
)

View File

@ -0,0 +1,38 @@
from marshmallow import fields
from project import marshmallow
from project.models import EventReference
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
from project.api.event.schemas import EventRefSchema
from project.api.organization.schemas import OrganizationRefSchema
class EventReferenceIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventReference
id = marshmallow.auto_field()
class EventReferenceRefSchema(EventReferenceIdSchema):
event = fields.Nested(EventRefSchema)
class EventReferenceSchema(EventReferenceIdSchema):
event = fields.Nested(EventRefSchema)
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
class EventReferenceDumpSchema(EventReferenceIdSchema):
event_id = marshmallow.auto_field()
organization_id = fields.Int(attribute="admin_unit_id")
class EventReferenceListRequestSchema(PaginationRequestSchema):
pass
class EventReferenceListResponseSchema(PaginationResponseSchema):
items = fields.List(
fields.Nested(EventReferenceRefSchema),
metadata={"description": "Event references"},
)

View File

@ -1,16 +1,15 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.image.schemas import ImageSchema
from project.models import AdminUnit
from project.models import Image
class ImageResource(MethodResource):
@doc(tags=["Images"])
@doc(summary="Get image", tags=["Images"])
@marshal_with(ImageSchema)
def get(self, id):
return AdminUnit.query.get_or_404(id)
return Image.query.get_or_404(id)
rest_api.add_resource(ImageResource, "/images/<int:id>")
api_docs.register(ImageResource)
add_api_resource(ImageResource, "/images/<int:id>", "api_v1_image")

View File

@ -2,24 +2,26 @@ from project import marshmallow
from project.models import Image
class ImageSchema(marshmallow.SQLAlchemySchema):
class ImageIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Image
id = marshmallow.auto_field()
class ImageBaseSchema(ImageIdSchema):
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
copyright_text = marshmallow.auto_field()
class ImageRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Image
id = marshmallow.auto_field()
class ImageSchema(ImageBaseSchema):
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
class ImageDumpSchema(ImageBaseSchema):
pass
class ImageRefSchema(ImageIdSchema):
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
href = marshmallow.URLFor(
"imageresource",
values=dict(id="<id>"),
)

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.location.schemas import LocationSchema
@ -6,11 +6,10 @@ from project.models import Location
class LocationResource(MethodResource):
@doc(tags=["Locations"])
@doc(summary="Get location", tags=["Locations"])
@marshal_with(LocationSchema)
def get(self, id):
return Location.query.get_or_404(id)
rest_api.add_resource(LocationResource, "/locations/<int:id>")
api_docs.register(LocationResource)
add_api_resource(LocationResource, "/locations/<int:id>", "api_v1_location")

View File

@ -3,11 +3,14 @@ from project import marshmallow
from project.models import Location
class LocationSchema(marshmallow.SQLAlchemySchema):
class LocationIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Location
id = marshmallow.auto_field()
class LocationSchema(LocationIdSchema):
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
street = marshmallow.auto_field()
@ -19,15 +22,12 @@ class LocationSchema(marshmallow.SQLAlchemySchema):
latitude = fields.Str()
class LocationRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = Location
class LocationDumpSchema(LocationSchema):
pass
id = marshmallow.auto_field()
href = marshmallow.URLFor(
"locationresource",
values=dict(id="<id>"),
)
class LocationRefSchema(LocationIdSchema):
pass
class LocationSearchItemSchema(LocationRefSchema):

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc, use_kwargs
from flask_apispec.views import MethodResource
from project.api.organization.schemas import (
@ -19,6 +19,14 @@ from project.api.organizer.schemas import (
OrganizerListRequestSchema,
OrganizerListResponseSchema,
)
from project.api.event_reference.schemas import (
EventReferenceListRequestSchema,
EventReferenceListResponseSchema,
)
from project.services.reference import (
get_reference_incoming_query,
get_reference_outgoing_query,
)
from project.api.place.schemas import PlaceListRequestSchema, PlaceListResponseSchema
from project.services.event import get_event_dates_query, get_events_query
from project.services.event_search import EventSearchParams
@ -30,21 +38,18 @@ from project.services.admin_unit import (
class OrganizationResource(MethodResource):
@doc(tags=["Organizations"])
@doc(summary="Get organization", tags=["Organizations"])
@marshal_with(OrganizationSchema)
def get(self, id):
return AdminUnit.query.get_or_404(id)
class OrganizationByShortNameResource(MethodResource):
@doc(tags=["Organizations"])
@marshal_with(OrganizationSchema)
def get(self, short_name):
return AdminUnit.query.filter(AdminUnit.short_name == short_name).first_or_404()
class OrganizationEventDateSearchResource(MethodResource):
@doc(tags=["Organizations", "Event Dates"])
@doc(
summary="Search for event dates of organization",
description="Includes events that organization is referencing.",
tags=["Organizations", "Event Dates"],
)
@use_kwargs(EventDateSearchRequestSchema, location=("query"))
@marshal_with(EventDateSearchResponseSchema)
def get(self, id, **kwargs):
@ -59,7 +64,7 @@ class OrganizationEventDateSearchResource(MethodResource):
class OrganizationEventSearchResource(MethodResource):
@doc(tags=["Organizations", "Events"])
@doc(summary="Search for events of organization", tags=["Organizations", "Events"])
@use_kwargs(EventSearchRequestSchema, location=("query"))
@marshal_with(EventSearchResponseSchema)
def get(self, id, **kwargs):
@ -74,7 +79,7 @@ class OrganizationEventSearchResource(MethodResource):
class OrganizationListResource(MethodResource):
@doc(tags=["Organizations"])
@doc(summary="List organizations", tags=["Organizations"])
@use_kwargs(OrganizationListRequestSchema, location=("query"))
@marshal_with(OrganizationListResponseSchema)
def get(self, **kwargs):
@ -84,7 +89,9 @@ class OrganizationListResource(MethodResource):
class OrganizationOrganizerListResource(MethodResource):
@doc(tags=["Organizations", "Organizers"])
@doc(
summary="List organizers of organization", tags=["Organizations", "Organizers"]
)
@use_kwargs(OrganizerListRequestSchema, location=("query"))
@marshal_with(OrganizerListResponseSchema)
def get(self, id, **kwargs):
@ -96,7 +103,7 @@ class OrganizationOrganizerListResource(MethodResource):
class OrganizationPlaceListResource(MethodResource):
@doc(tags=["Organizations", "Places"])
@doc(summary="List places of organization", tags=["Organizations", "Places"])
@use_kwargs(PlaceListRequestSchema, location=("query"))
@marshal_with(PlaceListResponseSchema)
def get(self, id, **kwargs):
@ -107,35 +114,63 @@ class OrganizationPlaceListResource(MethodResource):
return pagination
rest_api.add_resource(OrganizationResource, "/organizations/<int:id>")
api_docs.register(OrganizationResource)
class OrganizationIncomingEventReferenceListResource(MethodResource):
@doc(
summary="List incoming event references of organization",
tags=["Organizations", "Event References"],
)
@use_kwargs(EventReferenceListRequestSchema, location=("query"))
@marshal_with(EventReferenceListResponseSchema)
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
rest_api.add_resource(
OrganizationByShortNameResource, "/organizations/<string:short_name>"
)
api_docs.register(OrganizationByShortNameResource)
pagination = get_reference_incoming_query(admin_unit).paginate()
return pagination
rest_api.add_resource(
class OrganizationOutgoingEventReferenceListResource(MethodResource):
@doc(
summary="List outgoing event references of organization",
tags=["Organizations", "Event References"],
)
@use_kwargs(EventReferenceListRequestSchema, location=("query"))
@marshal_with(EventReferenceListResponseSchema)
def get(self, id, **kwargs):
admin_unit = AdminUnit.query.get_or_404(id)
pagination = get_reference_outgoing_query(admin_unit).paginate()
return pagination
add_api_resource(OrganizationResource, "/organizations/<int:id>", "api_v1_organization")
add_api_resource(
OrganizationEventDateSearchResource,
"/organizations/<int:id>/event_dates/search",
"/organizations/<int:id>/event-dates/search",
"api_v1_organization_event_date_search",
)
api_docs.register(OrganizationEventDateSearchResource)
rest_api.add_resource(
OrganizationEventSearchResource, "/organizations/<int:id>/events/search"
add_api_resource(
OrganizationEventSearchResource,
"/organizations/<int:id>/events/search",
"api_v1_organization_event_search",
)
api_docs.register(OrganizationEventSearchResource)
rest_api.add_resource(OrganizationListResource, "/organizations")
api_docs.register(OrganizationListResource)
rest_api.add_resource(
OrganizationOrganizerListResource, "/organizations/<int:id>/organizers"
add_api_resource(OrganizationListResource, "/organizations", "api_v1_organization_list")
add_api_resource(
OrganizationOrganizerListResource,
"/organizations/<int:id>/organizers",
"api_v1_organization_organizer_list",
)
api_docs.register(OrganizationOrganizerListResource)
rest_api.add_resource(
add_api_resource(
OrganizationPlaceListResource,
"/organizations/<int:id>/places",
"api_v1_organization_place_list",
)
add_api_resource(
OrganizationIncomingEventReferenceListResource,
"/organizations/<int:id>/event-references/incoming",
"api_v1_organization_incoming_event_reference_list",
)
add_api_resource(
OrganizationOutgoingEventReferenceListResource,
"/organizations/<int:id>/event-references/outgoing",
"api_v1_organization_outgoing_event_reference_list",
)
api_docs.register(OrganizationPlaceListResource)

View File

@ -6,30 +6,36 @@ from project.api.image.schemas import ImageRefSchema
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
class OrganizationSchema(marshmallow.SQLAlchemySchema):
class OrganizationIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = AdminUnit
id = marshmallow.auto_field()
class OrganizationBaseSchema(OrganizationIdSchema):
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
name = marshmallow.auto_field()
short_name = marshmallow.auto_field()
location = fields.Nested(LocationRefSchema)
logo = fields.Nested(ImageRefSchema)
url = marshmallow.auto_field()
email = marshmallow.auto_field()
phone = marshmallow.auto_field()
fax = marshmallow.auto_field()
class OrganizationRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = AdminUnit
class OrganizationSchema(OrganizationBaseSchema):
location = fields.Nested(LocationRefSchema)
logo = fields.Nested(ImageRefSchema)
id = marshmallow.auto_field()
class OrganizationDumpSchema(OrganizationBaseSchema):
location_id = fields.Int()
logo_id = fields.Int()
class OrganizationRefSchema(OrganizationIdSchema):
name = marshmallow.auto_field()
href = marshmallow.URLFor("organizationresource", values=dict(id="<id>"))
class OrganizationListRefSchema(OrganizationRefSchema):

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.organizer.schemas import OrganizerSchema
@ -6,14 +6,14 @@ from project.models import EventOrganizer
class OrganizerResource(MethodResource):
@doc(tags=["Organizers"])
@doc(summary="Get organizer", tags=["Organizers"])
@marshal_with(OrganizerSchema)
def get(self, id):
return EventOrganizer.query.get_or_404(id)
rest_api.add_resource(
add_api_resource(
OrganizerResource,
"/organizers/<int:id>",
"api_v1_organizer",
)
api_docs.register(OrganizerResource)

View File

@ -7,11 +7,14 @@ from project.api.organization.schemas import OrganizationRefSchema
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
class OrganizerSchema(marshmallow.SQLAlchemySchema):
class OrganizerIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventOrganizer
id = marshmallow.auto_field()
class OrganizerBaseSchema(OrganizerIdSchema):
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
name = marshmallow.auto_field()
@ -19,21 +22,22 @@ class OrganizerSchema(marshmallow.SQLAlchemySchema):
email = marshmallow.auto_field()
phone = marshmallow.auto_field()
fax = marshmallow.auto_field()
class OrganizerSchema(OrganizerBaseSchema):
location = fields.Nested(LocationRefSchema)
logo = fields.Nested(ImageRefSchema)
organization = fields.Nested(OrganizationRefSchema, attribute="adminunit")
class OrganizerRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventOrganizer
class OrganizerDumpSchema(OrganizerBaseSchema):
location_id = fields.Int()
logo_id = fields.Int()
organization_id = fields.Int(attribute="admin_unit_id")
id = marshmallow.auto_field()
class OrganizerRefSchema(OrganizerIdSchema):
name = marshmallow.auto_field()
href = marshmallow.URLFor(
"organizerresource",
values=dict(id="<id>"),
)
class OrganizerListRequestSchema(PaginationRequestSchema):

View File

@ -1,4 +1,4 @@
from project import rest_api, api_docs
from project.api import add_api_resource
from flask_apispec import marshal_with, doc
from flask_apispec.views import MethodResource
from project.api.place.schemas import PlaceSchema
@ -6,14 +6,10 @@ from project.models import EventPlace
class PlaceResource(MethodResource):
@doc(tags=["Places"])
@doc(summary="Get place", tags=["Places"])
@marshal_with(PlaceSchema)
def get(self, id):
return EventPlace.query.get_or_404(id)
rest_api.add_resource(
PlaceResource,
"/places/<int:id>",
)
api_docs.register(PlaceResource)
add_api_resource(PlaceResource, "/places/<int:id>", "api_v1_place")

View File

@ -7,31 +7,35 @@ from project.api.organization.schemas import OrganizationRefSchema
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
class PlaceSchema(marshmallow.SQLAlchemySchema):
class PlaceIdSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventPlace
id = marshmallow.auto_field()
class PlaceBaseSchema(PlaceIdSchema):
created_at = marshmallow.auto_field()
updated_at = marshmallow.auto_field()
name = marshmallow.auto_field()
location = fields.Nested(LocationRefSchema)
photo = fields.Nested(ImageRefSchema)
url = marshmallow.auto_field()
description = marshmallow.auto_field()
class PlaceSchema(PlaceBaseSchema):
location = fields.Nested(LocationRefSchema)
photo = fields.Nested(ImageRefSchema)
organization = fields.Nested(OrganizationRefSchema, attribute="adminunit")
class PlaceRefSchema(marshmallow.SQLAlchemySchema):
class Meta:
model = EventPlace
class PlaceDumpSchema(PlaceBaseSchema):
location_id = fields.Int()
photo_id = fields.Int()
organization_id = fields.Int(attribute="admin_unit_id")
id = marshmallow.auto_field()
class PlaceRefSchema(PlaceIdSchema):
name = marshmallow.auto_field()
href = marshmallow.URLFor(
"placeresource",
values=dict(id="<id>"),
)
class PlaceSearchItemSchema(PlaceRefSchema):

View File

@ -41,3 +41,7 @@ class PaginationResponseSchema(marshmallow.Schema):
required=True,
metadata={"description": "The total number of items matching the query"},
)
class NoneSchema(marshmallow.Schema):
pass

100
project/cli/dump.py Normal file
View File

@ -0,0 +1,100 @@
import click
from flask.cli import AppGroup
from project import app, dump_path
from project.models import (
Event,
EventPlace,
EventReference,
Location,
EventCategory,
EventOrganizer,
Image,
AdminUnit,
)
from sqlalchemy.orm import joinedload
import json
from project.api.event.schemas import EventDumpSchema
from project.api.place.schemas import PlaceDumpSchema
from project.api.location.schemas import LocationDumpSchema
from project.api.event_category.schemas import EventCategoryDumpSchema
from project.api.organizer.schemas import OrganizerDumpSchema
from project.api.image.schemas import ImageDumpSchema
from project.api.organization.schemas import OrganizationDumpSchema
from project.api.event_reference.schemas import EventReferenceDumpSchema
import os.path
import shutil
import pathlib
dump_cli = AppGroup("dump")
def dump_items(items, schema, file_base_name, dump_path):
result = schema.dump(items)
path = os.path.join(dump_path, file_base_name + ".json")
with open(path, "w") as outfile:
json.dump(result, outfile, ensure_ascii=False)
click.echo(f"{len(items)} item(s) dumped to {path}.")
@dump_cli.command("all")
def dump_all():
# Setup temp dir
tmp_path = os.path.join(dump_path, "tmp")
pathlib.Path(tmp_path).mkdir(parents=True, exist_ok=True)
# Events
events = Event.query.options(joinedload(Event.categories)).all()
dump_items(events, EventDumpSchema(many=True), "events", tmp_path)
# Places
places = EventPlace.query.all()
dump_items(places, PlaceDumpSchema(many=True), "places", tmp_path)
# Locations
locations = Location.query.all()
dump_items(locations, LocationDumpSchema(many=True), "locations", tmp_path)
# Event categories
event_categories = EventCategory.query.all()
dump_items(
event_categories,
EventCategoryDumpSchema(many=True),
"event_categories",
tmp_path,
)
# Organizers
organizers = EventOrganizer.query.all()
dump_items(organizers, OrganizerDumpSchema(many=True), "organizers", tmp_path)
# Images
images = Image.query.all()
dump_items(images, ImageDumpSchema(many=True), "images", tmp_path)
# Organizations
organizations = AdminUnit.query.all()
dump_items(
organizations, OrganizationDumpSchema(many=True), "organizations", tmp_path
)
# Event references
event_references = EventReference.query.all()
dump_items(
event_references,
EventReferenceDumpSchema(many=True),
"event_references",
tmp_path,
)
# Zip
zip_base_name = os.path.join(dump_path, "all")
zip_path = shutil.make_archive(zip_base_name, "zip", tmp_path)
click.echo(f"Zipped all up to {zip_path}.")
# Clean up temp dir
shutil.rmtree(tmp_path, ignore_errors=True)
app.cli.add_command(dump_cli)

View File

@ -1,10 +1,13 @@
from project import db
from project.models import (
AdminUnit,
EventCategory,
Event,
EventDate,
EventOrganizer,
EventReference,
EventPlace,
Image,
Location,
)
from project.dateutils import (
@ -14,6 +17,7 @@ from project.dateutils import (
)
from sqlalchemy import and_, or_, func
from sqlalchemy.sql import extract
from sqlalchemy.orm import joinedload, contains_eager
from dateutil.relativedelta import relativedelta
@ -89,9 +93,24 @@ def get_event_dates_query(params):
date_filter = and_(date_filter, extract("dow", EventDate.start).in_(weekdays))
return (
EventDate.query.join(Event)
.join(EventPlace, isouter=True)
.join(Location, isouter=True)
EventDate.query.join(EventDate.event)
.join(Event.event_place, isouter=True)
.join(EventPlace.location, isouter=True)
.options(
contains_eager(EventDate.event)
.contains_eager(Event.event_place)
.contains_eager(EventPlace.location),
joinedload(EventDate.event)
.joinedload(Event.categories)
.load_only(EventCategory.id, EventCategory.name),
joinedload(EventDate.event)
.joinedload(Event.organizer)
.load_only(EventOrganizer.id, EventOrganizer.name),
joinedload(EventDate.event).joinedload(Event.photo).load_only(Image.id),
joinedload(EventDate.event)
.joinedload(Event.admin_unit)
.load_only(AdminUnit.id, AdminUnit.name),
)
.filter(date_filter)
.filter(event_filter)
.order_by(EventDate.start)
@ -117,6 +136,13 @@ def get_events_query(params):
return (
Event.query.join(EventPlace, isouter=True)
.join(Location, isouter=True)
.options(
contains_eager(Event.event_place).contains_eager(EventPlace.location),
joinedload(Event.categories),
joinedload(Event.organizer),
joinedload(Event.photo),
joinedload(Event.admin_unit),
)
.filter(event_filter)
.order_by(Event.start)
)

View File

@ -1,5 +1,6 @@
from project import db
from project.models import (
Event,
EventReference,
EventReferenceRequest,
EventReferenceRequestReviewStatus,
@ -24,6 +25,14 @@ def create_event_reference_for_request(request):
return result
def get_reference_incoming_query(admin_unit):
return EventReference.query.filter(EventReference.admin_unit_id == admin_unit.id)
def get_reference_outgoing_query(admin_unit):
return EventReference.query.join(Event).filter(Event.admin_unit_id == admin_unit.id)
def get_reference_requests_incoming_query(admin_unit):
return EventReferenceRequest.query.filter(
and_(

View File

@ -7,18 +7,21 @@
<h1>Developer</h1>
<h2>Endpoint for all events</h2>
<input class="form-control" value="{{ url_for('api_events', _external=True) }}" />
<p><a class="btn btn-outline-info my-2" href="{{ url_for('api_events', _external=True) }}" target="_blank">Open <i class="fa fa-external-link-alt"></i></a></p>
<h2>Endpoint to search events</h2>
{% set search_url = url_for('api_event_dates', page='1', per_page='10', coordinate='51.9077888,10.4333312', distance='1000', date_from='2020-10-03', date_to='2021-10-03', keyword='stadtrundgang', _external=True) %}
<input class="form-control" value="{{ search_url }}" />
<p><a class="btn btn-outline-info my-2" href="{{ search_url }}" target="_blank">Open <i class="fa fa-external-link-alt"></i></a></p>
<h2>Documentation</h2>
<h2>API</h2>
<ul>
<li>Format: <a href="https://schema.org/Project" target="_blank">https://schema.org/Project</a></li>
<li>Documentation: <a href="/swagger-ui" target="_blank">Swagger/OpenAPI</a></li>
</ul>
<h2>Data download</h2>
<ul>
<li>
{% if dump_file %}
<a href="{{ dump_file.url }}">Dump of all data</a> <span class="badge badge-pill badge-light">{{ dump_file.ctime | datetimeformat }}</span> <span class="badge badge-pill badge-light">{{ dump_file.size }} Bytes</span>
{% else %}
No files available
{% endif %}
</li>
<li>The data file format is part of the <a href="/swagger-ui" target="_blank">API spec</a>. Watch for the <code>*Dump</code> models.</li>
</ul>

View File

@ -130,7 +130,7 @@
req_data += '&page=' + page + '&per_page=' + per_page;
$.ajax({
url: "{{ url_for('eventdatesearchresource') }}",
url: "{{ url_for('api_v1_event_date_search') }}",
type: "get",
dataType: "json",
data: req_data,

View File

@ -19,7 +19,7 @@ $( function() {
tbody.empty();
$.ajax({
url: "{{ url_for('eventdatesearchresource', per_page=max_events) }}",
url: "{{ url_for('api_v1_event_date_search', per_page=max_events) }}",
type: "get",
dataType: "json",
data: $(this).serialize(),

7
project/views/dump.py Normal file
View File

@ -0,0 +1,7 @@
from project import app, dump_path
from flask import send_from_directory
@app.route("/dump/<path:path>")
def dump_files(path):
return send_from_directory(dump_path, path)

View File

@ -9,6 +9,10 @@ from project.forms.reference import (
UpdateEventReferenceForm,
DeleteReferenceForm,
)
from project.services.reference import (
get_reference_incoming_query,
get_reference_outgoing_query,
)
from flask import render_template, flash, redirect, url_for, abort
from flask_babelex import gettext
from flask_security import auth_required
@ -86,7 +90,7 @@ def event_reference_update(id):
def manage_admin_unit_references_incoming(id):
admin_unit = get_admin_unit_for_manage_or_404(id)
references = (
EventReference.query.filter(EventReference.admin_unit_id == admin_unit.id)
get_reference_incoming_query(admin_unit)
.order_by(desc(EventReference.created_at))
.paginate()
)
@ -104,8 +108,7 @@ def manage_admin_unit_references_incoming(id):
def manage_admin_unit_references_outgoing(id):
admin_unit = get_admin_unit_for_manage_or_404(id)
references = (
EventReference.query.join(Event)
.filter(Event.admin_unit_id == admin_unit.id)
get_reference_outgoing_query(admin_unit)
.order_by(desc(EventReference.created_at))
.paginate()
)

View File

@ -1,9 +1,10 @@
from project import app
from project import app, dump_path
from project.services.admin import upsert_settings
from project.views.utils import track_analytics
from flask import url_for, render_template, request, redirect
from flask_babelex import gettext
from markupsafe import Markup
import os.path
@app.route("/")
@ -54,4 +55,15 @@ def privacy():
@app.route("/developer")
def developer():
return render_template("developer/read.html")
file_name = "all.zip"
all_path = os.path.join(dump_path, file_name)
dump_file = None
if os.path.exists(all_path):
dump_file = {
"url": url_for("dump_files", path=file_name),
"size": os.path.getsize(all_path),
"ctime": os.path.getctime(all_path),
}
return render_template("developer/read.html", dump_file=dump_file)

3
tests/api/test_dump.py Normal file
View File

@ -0,0 +1,3 @@
def test_read(client, seeder, utils):
response = utils.get_endpoint("api_v1_dump")
utils.assert_response_notFound(response)

View File

@ -12,7 +12,7 @@ def test_read(client, app, db, seeder, utils):
update_event(event)
db.session.commit()
url = utils.get_url("eventresource", id=event_id)
url = utils.get_url("api_v1_event", id=event_id)
response = utils.get_ok(url)
assert response.json["status"] == "scheduled"
@ -21,7 +21,7 @@ def test_list(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventlistresource")
url = utils.get_url("api_v1_event_list")
utils.get_ok(url)
@ -29,7 +29,7 @@ def test_search(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventsearchresource")
url = utils.get_url("api_v1_event_search")
utils.get_ok(url)
@ -37,5 +37,5 @@ def test_dates(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
event_id = seeder.create_event(admin_unit_id)
url = utils.get_url("eventdatesresource", id=event_id)
url = utils.get_url("api_v1_event_dates", id=event_id)
utils.get_ok(url)

View File

@ -2,5 +2,5 @@ def test_list(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventcategorylistresource")
url = utils.get_url("api_v1_event_category_list")
utils.get_ok(url)

View File

@ -2,7 +2,7 @@ def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventdateresource", id=1)
url = utils.get_url("api_v1_event_date", id=1)
utils.get_ok(url)
@ -10,7 +10,7 @@ def test_list(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventdatelistresource")
url = utils.get_url("api_v1_event_date_list")
utils.get_ok(url)
@ -18,5 +18,5 @@ def test_search(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("eventdatesearchresource")
url = utils.get_url("api_v1_event_date_search")
utils.get_ok(url)

View File

@ -0,0 +1,11 @@
def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
(
other_user_id,
other_admin_unit_id,
event_id,
reference_id,
) = seeder.create_any_reference(admin_unit_id)
url = utils.get_url("api_v1_event_reference", id=reference_id)
utils.get_ok(url)

View File

@ -2,5 +2,5 @@ def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
image_id = seeder.upsert_default_image()
url = utils.get_url("imageresource", id=image_id)
url = utils.get_url("api_v1_image", id=image_id)
utils.get_ok(url)

View File

@ -15,6 +15,6 @@ def test_read(client, app, db, seeder, utils):
db.session.commit()
location_id = location.id
url = utils.get_url("locationresource", id=location_id)
url = utils.get_url("api_v1_location", id=location_id)
response = utils.get_ok(url)
assert response.json["latitude"] == "51.9077888000000000"

View File

@ -1,21 +1,14 @@
def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
url = utils.get_url("organizationresource", id=admin_unit_id)
url = utils.get_url("api_v1_organization", id=admin_unit_id)
utils.get_ok(url)
def test_list(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
url = utils.get_url("organizationlistresource", keyword="crew")
utils.get_ok(url)
def test_read_by_short_name(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
url = utils.get_url("organizationbyshortnameresource", short_name="meinecrew")
url = utils.get_url("api_v1_organization_list", keyword="crew")
utils.get_ok(url)
@ -23,7 +16,7 @@ def test_event_date_search(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("organizationeventdatesearchresource", id=admin_unit_id)
url = utils.get_url("api_v1_organization_event_date_search", id=admin_unit_id)
utils.get_ok(url)
@ -31,7 +24,7 @@ def test_event_search(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id)
url = utils.get_url("organizationeventsearchresource", id=admin_unit_id)
url = utils.get_url("api_v1_organization_event_search", id=admin_unit_id)
utils.get_ok(url)
@ -40,7 +33,7 @@ def test_organizers(client, seeder, utils):
seeder.upsert_default_event_organizer(admin_unit_id)
url = utils.get_url(
"organizationorganizerlistresource", id=admin_unit_id, name="crew"
"api_v1_organization_organizer_list", id=admin_unit_id, name="crew"
)
utils.get_ok(url)
@ -49,5 +42,38 @@ def test_places(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.upsert_default_event_place(admin_unit_id)
url = utils.get_url("organizationplacelistresource", id=admin_unit_id, name="crew")
url = utils.get_url("api_v1_organization_place_list", id=admin_unit_id, name="crew")
utils.get_ok(url)
def test_references_incoming(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
(
other_user_id,
other_admin_unit_id,
event_id,
reference_id,
) = seeder.create_any_reference(admin_unit_id)
url = utils.get_url(
"api_v1_organization_incoming_event_reference_list",
id=admin_unit_id,
name="crew",
)
utils.get_ok(url)
def test_references_outgoing(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
event_id = seeder.create_event(admin_unit_id)
other_user_id = seeder.create_user("other@test.de")
other_admin_unit_id = seeder.create_admin_unit(other_user_id, "Other Crew")
seeder.create_reference(event_id, other_admin_unit_id)
url = utils.get_url(
"api_v1_organization_outgoing_event_reference_list",
id=admin_unit_id,
name="crew",
)
utils.get_ok(url)

View File

@ -2,5 +2,5 @@ def test_read(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
organizer_id = seeder.upsert_default_event_organizer(admin_unit_id)
url = utils.get_url("organizerresource", id=organizer_id)
url = utils.get_url("api_v1_organizer", id=organizer_id)
utils.get_ok(url)

View File

@ -2,5 +2,5 @@ def test_read(client, app, db, seeder, utils):
user_id, admin_unit_id = seeder.setup_base()
place_id = seeder.upsert_default_event_place(admin_unit_id)
url = utils.get_url("placeresource", id=place_id)
url = utils.get_url("api_v1_place", id=place_id)
utils.get_ok(url)

9
tests/cli/test_dump.py Normal file
View File

@ -0,0 +1,9 @@
def test_all(client, seeder, app, utils):
user_id, admin_unit_id = seeder.setup_base()
seeder.create_event(admin_unit_id, "RRULE:FREQ=DAILY;COUNT=7")
runner = app.test_cli_runner()
result = runner.invoke(args=["dump", "all"])
assert "Zipped all up" in result.output
utils.get_endpoint_ok("dump_files", path="all.zip")