mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
Setup Rest API with Swagger #50
This commit is contained in:
parent
8fb3b83530
commit
1a5f33c9de
@ -1,6 +1,6 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
|
||||
@ -50,12 +50,13 @@ cors = CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
# API
|
||||
rest_api = Api(app, "/api/v1")
|
||||
marshmallow = Marshmallow(app)
|
||||
marshmallow_plugin = MarshmallowPlugin()
|
||||
app.config.update(
|
||||
{
|
||||
"APISPEC_SPEC": APISpec(
|
||||
title="Oveda API",
|
||||
version="1.0.0",
|
||||
plugins=[MarshmallowPlugin()],
|
||||
plugins=[marshmallow_plugin],
|
||||
openapi_version="2.0",
|
||||
),
|
||||
}
|
||||
|
||||
@ -1,3 +1,22 @@
|
||||
def enum_to_properties(self, field, **kwargs):
|
||||
"""
|
||||
Add an OpenAPI extension for marshmallow_enum.EnumField instances
|
||||
"""
|
||||
import marshmallow_enum
|
||||
|
||||
if isinstance(field, marshmallow_enum.EnumField):
|
||||
return {"type": "string", "enum": [m.name for m in field.enum]}
|
||||
return {}
|
||||
|
||||
|
||||
from project import marshmallow_plugin
|
||||
|
||||
marshmallow_plugin.converter.add_attribute_function(enum_to_properties)
|
||||
|
||||
import project.api.event.resources
|
||||
import project.api.event_date.resources
|
||||
import project.api.image.resources
|
||||
import project.api.location.resources
|
||||
import project.api.organization.resources
|
||||
import project.api.organizer.resources
|
||||
import project.api.place.resources
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
from project import marshmallow
|
||||
from marshmallow import fields
|
||||
from project.models import Event
|
||||
from marshmallow_enum import EnumField
|
||||
from project.models import Event, EventStatus
|
||||
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
|
||||
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
|
||||
|
||||
|
||||
class EventSchema(marshmallow.SQLAlchemySchema):
|
||||
@ -8,7 +14,11 @@ class EventSchema(marshmallow.SQLAlchemySchema):
|
||||
model = Event
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
href = marshmallow.URLFor("eventresource", values=dict(id="<id>"))
|
||||
name = marshmallow.auto_field()
|
||||
start = marshmallow.auto_field()
|
||||
end = marshmallow.auto_field()
|
||||
recurrence_rule = marshmallow.auto_field()
|
||||
description = marshmallow.auto_field()
|
||||
external_link = marshmallow.auto_field()
|
||||
ticket_link = marshmallow.auto_field()
|
||||
@ -17,14 +27,28 @@ class EventSchema(marshmallow.SQLAlchemySchema):
|
||||
accessible_for_free = marshmallow.auto_field()
|
||||
age_from = marshmallow.auto_field()
|
||||
age_to = marshmallow.auto_field()
|
||||
status = EnumField(EventStatus)
|
||||
|
||||
organization = marshmallow.HyperlinkRelated(
|
||||
"organizationresource", attribute="admin_unit"
|
||||
)
|
||||
organizer = marshmallow.URLFor(
|
||||
"organizerresource",
|
||||
values=dict(organization_id="<admin_unit_id>", organizer_id="<organizer_id>"),
|
||||
)
|
||||
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
|
||||
organizer = fields.Nested(OrganizerRefSchema)
|
||||
photo = fields.Nested(ImageRefSchema)
|
||||
place = fields.Nested(PlaceRefSchema, attribute="event_place")
|
||||
|
||||
|
||||
class EventRefSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = Event
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
href = marshmallow.URLFor("eventresource", values=dict(id="<id>"))
|
||||
name = marshmallow.auto_field()
|
||||
description = marshmallow.auto_field()
|
||||
status = EnumField(EventStatus)
|
||||
|
||||
organization = fields.Nested(OrganizationRefSchema, attribute="admin_unit")
|
||||
organizer = fields.Nested(OrganizerRefSchema)
|
||||
photo = fields.Nested(ImageRefSchema)
|
||||
place = fields.Nested(PlaceRefSchema, attribute="event_place")
|
||||
|
||||
|
||||
class EventListItemSchema(marshmallow.SQLAlchemySchema):
|
||||
@ -39,41 +63,11 @@ class EventListItemSchema(marshmallow.SQLAlchemySchema):
|
||||
recurrence_rule = marshmallow.auto_field()
|
||||
|
||||
|
||||
class EventListRequestSchema(marshmallow.Schema):
|
||||
page = fields.Integer(
|
||||
required=False,
|
||||
default=1,
|
||||
metadata={"description": "The page number (1 indexed)."},
|
||||
)
|
||||
per_page = fields.Integer(
|
||||
required=False, default=20, metadata={"description": "Items per page"}
|
||||
)
|
||||
class EventListRequestSchema(PaginationRequestSchema):
|
||||
pass
|
||||
|
||||
|
||||
class EventListResponseSchema(marshmallow.Schema):
|
||||
has_next = fields.Boolean(
|
||||
required=True, metadata={"description": "True if a next page exists."}
|
||||
)
|
||||
has_prev = fields.Boolean(
|
||||
required=True, metadata={"description": "True if a previous page exists."}
|
||||
)
|
||||
next_num = fields.Integer(
|
||||
required=False, metadata={"description": "Number of the next page."}
|
||||
)
|
||||
prev_num = fields.Integer(
|
||||
required=False, metadata={"description": "Number of the previous page."}
|
||||
)
|
||||
page = fields.Integer(
|
||||
required=True, metadata={"description": "The current page number (1 indexed)."}
|
||||
)
|
||||
pages = fields.Integer(
|
||||
required=True, metadata={"description": "The total number of pages."}
|
||||
)
|
||||
per_page = fields.Integer(required=True, metadata={"description": "Items per page"})
|
||||
total = fields.Integer(
|
||||
required=True,
|
||||
metadata={"description": "The total number of items matching the query"},
|
||||
)
|
||||
class EventListResponseSchema(PaginationResponseSchema):
|
||||
items = fields.List(
|
||||
fields.Nested(EventListItemSchema), metadata={"description": "Events"}
|
||||
)
|
||||
|
||||
0
project/api/event_date/__init__.py
Normal file
0
project/api/event_date/__init__.py
Normal file
32
project/api/event_date/resources.py
Normal file
32
project/api/event_date/resources.py
Normal file
@ -0,0 +1,32 @@
|
||||
from project import rest_api, api_docs
|
||||
from flask_apispec import marshal_with, doc, use_kwargs
|
||||
from flask_apispec.views import MethodResource
|
||||
from project.api.event_date.schemas import (
|
||||
EventDateSchema,
|
||||
EventDateListRequestSchema,
|
||||
EventDateListResponseSchema,
|
||||
)
|
||||
from project.models import EventDate
|
||||
|
||||
|
||||
class EventDateListResource(MethodResource):
|
||||
@doc(tags=["Event Dates"])
|
||||
@use_kwargs(EventDateListRequestSchema, location=("query"))
|
||||
@marshal_with(EventDateListResponseSchema)
|
||||
def get(self, **kwargs):
|
||||
pagination = EventDate.query.paginate()
|
||||
return pagination
|
||||
|
||||
|
||||
class EventDateResource(MethodResource):
|
||||
@doc(tags=["Event Dates"])
|
||||
@marshal_with(EventDateSchema)
|
||||
def get(self, id):
|
||||
return EventDate.query.get_or_404(id)
|
||||
|
||||
|
||||
rest_api.add_resource(EventDateListResource, "/event_dates")
|
||||
api_docs.register(EventDateListResource)
|
||||
|
||||
rest_api.add_resource(EventDateResource, "/event_dates/<int:id>")
|
||||
api_docs.register(EventDateResource)
|
||||
36
project/api/event_date/schemas.py
Normal file
36
project/api/event_date/schemas.py
Normal file
@ -0,0 +1,36 @@
|
||||
from project import marshmallow
|
||||
from marshmallow import fields
|
||||
from project.models import EventDate
|
||||
from project.api.event.schemas import EventSchema, EventRefSchema
|
||||
from project.api.schemas import PaginationRequestSchema, PaginationResponseSchema
|
||||
|
||||
|
||||
class EventDateSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = EventDate
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
start = marshmallow.auto_field()
|
||||
end = marshmallow.auto_field()
|
||||
event = fields.Nested(EventSchema)
|
||||
|
||||
|
||||
class EventDateListItemSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = EventDate
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
href = marshmallow.URLFor("eventdateresource", values=dict(id="<id>"))
|
||||
start = marshmallow.auto_field()
|
||||
end = marshmallow.auto_field()
|
||||
event = fields.Nested(EventRefSchema)
|
||||
|
||||
|
||||
class EventDateListRequestSchema(PaginationRequestSchema):
|
||||
pass
|
||||
|
||||
|
||||
class EventDateListResponseSchema(PaginationResponseSchema):
|
||||
items = fields.List(
|
||||
fields.Nested(EventDateListItemSchema), metadata={"description": "Dates"}
|
||||
)
|
||||
0
project/api/image/__init__.py
Normal file
0
project/api/image/__init__.py
Normal file
16
project/api/image/resources.py
Normal file
16
project/api/image/resources.py
Normal file
@ -0,0 +1,16 @@
|
||||
from project import rest_api, api_docs
|
||||
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
|
||||
|
||||
|
||||
class ImageResource(MethodResource):
|
||||
@doc(tags=["Images"])
|
||||
@marshal_with(ImageSchema)
|
||||
def get(self, id):
|
||||
return AdminUnit.query.get_or_404(id)
|
||||
|
||||
|
||||
rest_api.add_resource(ImageResource, "/images/<int:id>")
|
||||
api_docs.register(ImageResource)
|
||||
24
project/api/image/schemas.py
Normal file
24
project/api/image/schemas.py
Normal file
@ -0,0 +1,24 @@
|
||||
from project import marshmallow
|
||||
from project.models import Image
|
||||
|
||||
|
||||
class ImageSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = Image
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
copyright_text = marshmallow.auto_field()
|
||||
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
|
||||
|
||||
|
||||
class ImageRefSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = Image
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
copyright_text = marshmallow.auto_field()
|
||||
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
|
||||
href = marshmallow.URLFor(
|
||||
"imageresource",
|
||||
values=dict(id="<id>"),
|
||||
)
|
||||
0
project/api/location/__init__.py
Normal file
0
project/api/location/__init__.py
Normal file
16
project/api/location/resources.py
Normal file
16
project/api/location/resources.py
Normal file
@ -0,0 +1,16 @@
|
||||
from project import rest_api, api_docs
|
||||
from flask_apispec import marshal_with, doc
|
||||
from flask_apispec.views import MethodResource
|
||||
from project.api.location.schemas import LocationSchema
|
||||
from project.models import Location
|
||||
|
||||
|
||||
class LocationResource(MethodResource):
|
||||
@doc(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)
|
||||
30
project/api/location/schemas.py
Normal file
30
project/api/location/schemas.py
Normal file
@ -0,0 +1,30 @@
|
||||
from marshmallow import fields
|
||||
from project import marshmallow
|
||||
from project.models import Location
|
||||
|
||||
|
||||
class LocationSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = Location
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
street = marshmallow.auto_field()
|
||||
postalCode = marshmallow.auto_field()
|
||||
city = marshmallow.auto_field()
|
||||
state = marshmallow.auto_field()
|
||||
country = marshmallow.auto_field()
|
||||
longitude = fields.Str()
|
||||
latitude = fields.Str()
|
||||
|
||||
|
||||
class LocationRefSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = Location
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
longitude = fields.Str()
|
||||
latitude = fields.Str()
|
||||
href = marshmallow.URLFor(
|
||||
"locationresource",
|
||||
values=dict(id="<id>"),
|
||||
)
|
||||
@ -12,3 +12,12 @@ class OrganizationSchema(marshmallow.SQLAlchemySchema):
|
||||
email = marshmallow.auto_field()
|
||||
phone = marshmallow.auto_field()
|
||||
fax = marshmallow.auto_field()
|
||||
|
||||
|
||||
class OrganizationRefSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = AdminUnit
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
name = marshmallow.auto_field()
|
||||
href = marshmallow.URLFor("organizationresource", values=dict(id="<id>"))
|
||||
|
||||
@ -8,12 +8,12 @@ from project.models import EventOrganizer
|
||||
class OrganizerResource(MethodResource):
|
||||
@doc(tags=["Organizers"])
|
||||
@marshal_with(OrganizerSchema)
|
||||
def get(self, organization_id, organizer_id):
|
||||
return EventOrganizer.query.get_or_404(organizer_id)
|
||||
def get(self, id):
|
||||
return EventOrganizer.query.get_or_404(id)
|
||||
|
||||
|
||||
rest_api.add_resource(
|
||||
OrganizerResource,
|
||||
"/organizations/<int:organization_id>/organizers/<int:organizer_id>",
|
||||
"/organizers/<int:id>",
|
||||
)
|
||||
api_docs.register(OrganizerResource)
|
||||
|
||||
@ -8,3 +8,15 @@ class OrganizerSchema(marshmallow.SQLAlchemySchema):
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
name = marshmallow.auto_field()
|
||||
|
||||
|
||||
class OrganizerRefSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = EventOrganizer
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
name = marshmallow.auto_field()
|
||||
href = marshmallow.URLFor(
|
||||
"organizerresource",
|
||||
values=dict(id="<id>"),
|
||||
)
|
||||
|
||||
0
project/api/place/__init__.py
Normal file
0
project/api/place/__init__.py
Normal file
19
project/api/place/resources.py
Normal file
19
project/api/place/resources.py
Normal file
@ -0,0 +1,19 @@
|
||||
from project import rest_api, api_docs
|
||||
from flask_apispec import marshal_with, doc
|
||||
from flask_apispec.views import MethodResource
|
||||
from project.api.place.schemas import PlaceSchema
|
||||
from project.models import EventPlace
|
||||
|
||||
|
||||
class PlaceResource(MethodResource):
|
||||
@doc(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)
|
||||
30
project/api/place/schemas.py
Normal file
30
project/api/place/schemas.py
Normal file
@ -0,0 +1,30 @@
|
||||
from marshmallow import fields
|
||||
from project import marshmallow
|
||||
from project.models import EventPlace
|
||||
from project.api.image.schemas import ImageRefSchema
|
||||
from project.api.location.schemas import LocationRefSchema
|
||||
|
||||
|
||||
class PlaceSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = EventPlace
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
name = marshmallow.auto_field()
|
||||
url = marshmallow.auto_field()
|
||||
description = marshmallow.auto_field()
|
||||
photo = fields.Nested(ImageRefSchema)
|
||||
location = fields.Nested(LocationRefSchema)
|
||||
|
||||
|
||||
class PlaceRefSchema(marshmallow.SQLAlchemySchema):
|
||||
class Meta:
|
||||
model = EventPlace
|
||||
|
||||
id = marshmallow.auto_field()
|
||||
name = marshmallow.auto_field()
|
||||
href = marshmallow.URLFor(
|
||||
"placeresource",
|
||||
values=dict(id="<id>"),
|
||||
)
|
||||
location = fields.Nested(LocationRefSchema)
|
||||
39
project/api/schemas.py
Normal file
39
project/api/schemas.py
Normal file
@ -0,0 +1,39 @@
|
||||
from project import marshmallow
|
||||
from marshmallow import fields
|
||||
|
||||
|
||||
class PaginationRequestSchema(marshmallow.Schema):
|
||||
page = fields.Integer(
|
||||
required=False,
|
||||
default=1,
|
||||
metadata={"description": "The page number (1 indexed)."},
|
||||
)
|
||||
per_page = fields.Integer(
|
||||
required=False, default=20, metadata={"description": "Items per page"}
|
||||
)
|
||||
|
||||
|
||||
class PaginationResponseSchema(marshmallow.Schema):
|
||||
has_next = fields.Boolean(
|
||||
required=True, metadata={"description": "True if a next page exists."}
|
||||
)
|
||||
has_prev = fields.Boolean(
|
||||
required=True, metadata={"description": "True if a previous page exists."}
|
||||
)
|
||||
next_num = fields.Integer(
|
||||
required=False, metadata={"description": "Number of the next page."}
|
||||
)
|
||||
prev_num = fields.Integer(
|
||||
required=False, metadata={"description": "Number of the previous page."}
|
||||
)
|
||||
page = fields.Integer(
|
||||
required=True, metadata={"description": "The current page number (1 indexed)."}
|
||||
)
|
||||
pages = fields.Integer(
|
||||
required=True, metadata={"description": "The total number of pages."}
|
||||
)
|
||||
per_page = fields.Integer(required=True, metadata={"description": "Items per page"})
|
||||
total = fields.Integer(
|
||||
required=True,
|
||||
metadata={"description": "The total number of items matching the query"},
|
||||
)
|
||||
@ -52,6 +52,7 @@ jsonschema==3.2.0
|
||||
Mako==1.1.3
|
||||
MarkupSafe==1.1.1
|
||||
marshmallow==3.10.0
|
||||
marshmallow-enum==1.5.1
|
||||
marshmallow-sqlalchemy==0.24.1
|
||||
mccabe==0.6.1
|
||||
mistune==0.8.4
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
def test_read(client, seeder, utils):
|
||||
def test_read(client, app, db, seeder, utils):
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
event_id = seeder.create_event(admin_unit_id)
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Event, EventStatus
|
||||
from project.services.event import update_event
|
||||
|
||||
event = Event.query.get(event_id)
|
||||
event.status = EventStatus.scheduled
|
||||
|
||||
update_event(event)
|
||||
db.session.commit()
|
||||
|
||||
url = utils.get_url("eventresource", id=event_id)
|
||||
utils.get_ok(url)
|
||||
response = utils.get_ok(url)
|
||||
assert response.json["status"] == "scheduled"
|
||||
|
||||
|
||||
def test_list(client, seeder, utils):
|
||||
|
||||
14
tests/api/test_event_date.py
Normal file
14
tests/api/test_event_date.py
Normal file
@ -0,0 +1,14 @@
|
||||
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)
|
||||
utils.get_ok(url)
|
||||
|
||||
|
||||
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")
|
||||
utils.get_ok(url)
|
||||
6
tests/api/test_image.py
Normal file
6
tests/api/test_image.py
Normal file
@ -0,0 +1,6 @@
|
||||
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)
|
||||
utils.get_ok(url)
|
||||
20
tests/api/test_location.py
Normal file
20
tests/api/test_location.py
Normal file
@ -0,0 +1,20 @@
|
||||
def test_read(client, app, db, seeder, utils):
|
||||
user_id, admin_unit_id = seeder.setup_base()
|
||||
|
||||
with app.app_context():
|
||||
from project.models import Location
|
||||
|
||||
location = Location()
|
||||
location.street = "Markt 7"
|
||||
location.postalCode = "38640"
|
||||
location.city = "Goslar"
|
||||
location.latitude = 51.9077888
|
||||
location.longitude = 10.4333312
|
||||
|
||||
db.session.add(location)
|
||||
db.session.commit()
|
||||
location_id = location.id
|
||||
|
||||
url = utils.get_url("locationresource", id=location_id)
|
||||
response = utils.get_ok(url)
|
||||
assert response.json["latitude"] == "51.9077888000000000"
|
||||
@ -2,7 +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", organization_id=admin_unit_id, organizer_id=organizer_id
|
||||
)
|
||||
url = utils.get_url("organizerresource", id=organizer_id)
|
||||
utils.get_ok(url)
|
||||
|
||||
6
tests/api/test_place.py
Normal file
6
tests/api/test_place.py
Normal file
@ -0,0 +1,6 @@
|
||||
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)
|
||||
utils.get_ok(url)
|
||||
Loading…
x
Reference in New Issue
Block a user