Skip to content
Closed
490 changes: 0 additions & 490 deletions app/api/routes.py

This file was deleted.

14 changes: 14 additions & 0 deletions app/api/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
The `routes` module is responsible for setting up HTTP routes on the API Flask
blueprint.

Each module imported below will configure the API blueprint automatically on import.
"""

from app.api.routes import api_key
from app.api.routes import categories
from app.api.routes import languages
from app.api.routes import resource_retrieval
from app.api.routes import resource_updating
from app.api.routes import resource_voting
from app.api.routes import searching
42 changes: 42 additions & 0 deletions app/api/routes/api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from flask import request

import app.utils as utils
from app.api import bp
from app.api.auth import is_user_oc_member
from app.api.routes.utils import failures_counter, latency_summary, logger
from app.api.validations import requires_body
from app.models import Key


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/apikey', methods=['POST'], endpoint='apikey')
@requires_body
def apikey():
"""
Verify OC membership and return an API key. The API key will be
saved in the DB to verify use as well as returned upon subsequent calls
to this endpoint with the same OC credentials.
"""
json = request.get_json()
email = json.get('email')
password = json.get('password')
is_oc_member = is_user_oc_member(email, password)

if not is_oc_member:
message = "The email or password you submitted is incorrect"
payload = {'errors': {"invalid-credentials": {"message": message}}}
return utils.standardize_response(payload=payload, status_code=401)

try:
# We need to check the database for an existing key
apikey = Key.query.filter_by(email=email).first()
if not apikey:
# Since they're already authenticated by is_oc_user(), we know we
# can generate an API key for them if they don't already have one
return utils.create_new_apikey(email, logger)
logger.info(apikey.serialize)
return utils.standardize_response(payload=dict(data=apikey.serialize))
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)
51 changes: 51 additions & 0 deletions app/api/routes/categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask import request, redirect

from app import utils as utils
from app.api import bp
from app.api.routes.utils import latency_summary, failures_counter, logger
from app.models import Category
from configs import Config


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/categories', methods=['GET'])
def categories():
return get_categories()


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/categories/<int:id>', methods=['GET'], endpoint='get_category')
def category(id):
return get_category(id)


def get_categories():
try:
category_paginator = utils.Paginator(Config.CATEGORY_PAGINATOR, request)
query = Category.query

paginated_categories = category_paginator.paginated_data(query)
if not paginated_categories:
return redirect('/404')
category_list = [
category.serialize for category in paginated_categories.items
]
pagination_details = category_paginator.pagination_details(paginated_categories)
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

return utils.standardize_response(payload=dict(
data=category_list,
**pagination_details))


def get_category(id):
category = Category.query.get(id)

if category:
return utils.standardize_response(payload=dict(data=(category.serialize)))

return redirect('/404')
51 changes: 51 additions & 0 deletions app/api/routes/languages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from flask import request, redirect

from app import utils as utils
from app.api import bp
from app.api.routes.utils import latency_summary, failures_counter, logger
from app.models import Language
from configs import Config


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/languages', methods=['GET'])
def languages():
return get_languages()


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/languages/<int:id>', methods=['GET'], endpoint='get_language')
def language(id):
return get_language(id)


def get_languages():
language_paginator = utils.Paginator(Config.LANGUAGE_PAGINATOR, request)
query = Language.query

try:
paginated_languages = language_paginator.paginated_data(query)
if not paginated_languages:
return redirect('/404')
language_list = [
language.serialize for language in paginated_languages.items
]
pagination_details = language_paginator.pagination_details(paginated_languages)
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

return utils.standardize_response(payload=dict(
data=language_list,
**pagination_details))


def get_language(id):
language = Language.query.get(id)

if language:
return utils.standardize_response(payload=dict(data=(language.serialize)))

return redirect('/404')
111 changes: 111 additions & 0 deletions app/api/routes/resource_retrieval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from datetime import datetime

from dateutil import parser
from flask import request, redirect
from sqlalchemy import func, or_

from app import utils as utils
from app.api import bp
from app.api.routes.utils import latency_summary, failures_counter, logger
from app.models import Resource, Language, Category
from configs import Config


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/resources', methods=['GET'], endpoint='get_resources')
def resources():
return get_resources()


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/resources/<int:id>', methods=['GET'], endpoint='get_resource')
def resource(id):
return get_resource(id)


def get_resources():
"""
Gets a paginated list of resources.

If the URL parameters `language` or `category` are found
in the request, the list will be filtered by these parameters.

The filters are case insensitive.
"""
resource_paginator = utils.Paginator(Config.RESOURCE_PAGINATOR, request)

# Fetch the filter params from the url, if they were provided.
language = request.args.get('language')
category = request.args.get('category')
updated_after = request.args.get('updated_after')
paid = request.args.get('paid')

q = Resource.query

# Filter on language
if language:
q = q.filter(
Resource.languages.any(
Language.name.ilike(language)
)
)

# Filter on category
if category:
q = q.filter(
Resource.category.has(
func.lower(Category.name) == category.lower()
)
)

# Filter on updated_after
if updated_after:
try:
uaDate = parser.parse(updated_after)
if uaDate > datetime.now():
raise Exception("updated_after greater than today's date")
uaDate = uaDate.strftime("%Y-%m-%d")
except Exception as e:
logger.exception(e)
message = 'The value for "updated_after" is invalid'
res = {"errors": {"unprocessable-entity": {"message": message}}}
return utils.standardize_response(payload=res, status_code=422)

q = q.filter(
or_(
Resource.created_at >= uaDate,
Resource.last_updated >= uaDate
)
)

# Filter on paid
if isinstance(paid, str) and paid.lower() in ['true', 'false']:
paidAsBool = paid.lower() == 'true'
q = q.filter(Resource.paid == paidAsBool)

try:
paginated_resources = resource_paginator.paginated_data(q)
if not paginated_resources:
return redirect('/404')
resource_list = [
resource.serialize for resource in paginated_resources.items
]
pagination_details = resource_paginator.pagination_details(paginated_resources)
except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

return utils.standardize_response(payload=dict(
data=resource_list,
**pagination_details))


def get_resource(id):
resource = Resource.query.get(id)

if resource:
return utils.standardize_response(payload=dict(data=(resource.serialize)))

return redirect('/404')
124 changes: 124 additions & 0 deletions app/api/routes/resource_updating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from algoliasearch.exceptions import AlgoliaException, AlgoliaUnreachableHostException
from flask import redirect, request
from sqlalchemy.exc import IntegrityError

from app import db, index, utils as utils
from app.api import bp
from app.api.auth import authenticate
from app.api.routes.utils import (
failures_counter, get_attributes, latency_summary, logger)
from app.api.validations import requires_body, validate_resource
from app.models import Resource


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/resources', methods=['POST'], endpoint='create_resource')
@requires_body
@authenticate
def post_resources():
validation_errors = validate_resource(request)

if validation_errors:
return utils.standardize_response(payload=validation_errors, status_code=422)
return create_resource(request.get_json(), db)


@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/resources/<int:id>', methods=['PUT'], endpoint='update_resource')
@requires_body
@authenticate
def put_resource(id):
validation_errors = validate_resource(request, id)

if validation_errors:
return utils.standardize_response(payload=validation_errors, status_code=422)
return update_resource(id, request.get_json(), db)


def create_resource(json, db):
langs, categ = get_attributes(json)
new_resource = Resource(
name=json.get('name'),
url=json.get('url'),
category=categ,
languages=langs,
paid=json.get('paid'),
notes=json.get('notes'))

try:
db.session.add(new_resource)
db.session.commit()
index.save_object(new_resource.serialize_algolia_search)

except (AlgoliaUnreachableHostException, AlgoliaException) as e:
logger.exception(e)
print(f"Algolia failed to index new resource '{new_resource.name}'")

except IntegrityError as e:
logger.exception(e)
return utils.standardize_response(status_code=422)

except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)

return utils.standardize_response(payload=dict(data=new_resource.serialize))


def update_resource(id, json, db):
resource = Resource.query.get(id)

if not resource:
return redirect('/404')

langs, categ = get_attributes(json)
index_object = {'objectID': id}

try:
logger.info(f"Updating resource. Old data: {resource.serialize}")
if json.get('languages'):
resource.languages = langs
index_object['languages'] = resource.serialize['languages']
if json.get('category'):
resource.category = categ
index_object['category'] = categ.name
if json.get('name'):
resource.name = json.get('name')
index_object['name'] = json.get('name')
if json.get('url'):
resource.url = json.get('url')
index_object['url'] = json.get('url')
if 'paid' in json:
paid = json.get('paid')

# Converts "false" and "true" to their bool
if type(paid) is str and paid.lower() in ["true", "false"]:
paid = paid.lower().strip() == "true"
resource.paid = paid
index_object['paid'] = paid
if 'notes' in json:
resource.notes = json.get('notes')
index_object['notes'] = json.get('notes')

db.session.commit()

try:
index.partial_update_object(index_object)

except (AlgoliaUnreachableHostException, AlgoliaException) as e:
logger.exception(e)
print(f"Algolia failed to update index for resource '{resource.name}'")

return utils.standardize_response(
payload=dict(data=resource.serialize)
)

except IntegrityError as e:
logger.exception(e)
return utils.standardize_response(status_code=422)

except Exception as e:
logger.exception(e)
return utils.standardize_response(status_code=500)
Loading