From f2bdf63cba0f99ebab74cf6b2fa3fdecf511976b Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Sun, 25 Oct 2020 19:44:00 -0400 Subject: [PATCH 01/11] include user's current vote information --- app/api/routes/resource_retrieval.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index d6153ff2..90553700 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -1,13 +1,13 @@ from datetime import datetime from dateutil import parser -from flask import redirect, request -from sqlalchemy import func, or_, text +from flask import redirect, request, g +from sqlalchemy import func, or_, and_, text -from app import utils as utils +from app import db, utils as utils from app.api import bp from app.api.routes.helpers import failures_counter, latency_summary, logger -from app.models import Category, Language, Resource +from app.models import Category, Language, Resource, VoteInformation, Key from configs import Config @@ -42,7 +42,19 @@ def get_resources(): updated_after = request.args.get('updated_after') free = request.args.get('free') + apikey = request.headers.get('x-apikey') + filters = {'apikey': apikey, 'denied': False} + key = Key.query.filter_by(**filters).first() if apikey else g.get('auth_key') + q = Resource.query + if key: + q = db.session.query(Resource, VoteInformation.current_direction).outerjoin( + VoteInformation, + and_( + Resource.id == VoteInformation.resource_id, + VoteInformation.voter_apikey == key.apikey + ) + ) # Filter on languages if languages: @@ -102,7 +114,8 @@ def get_resources(): if not paginated_resources: return redirect('/404') resource_list = [ - resource.serialize for resource in paginated_resources.items + {**item.Resource.serialize, 'vote_direction': item[1]} + if key else item.serialize for item in paginated_resources.items ] details = resource_paginator.details(paginated_resources) except Exception as e: From 2b175cb40f910d85b25085c7ff28a1319ff3b67e Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Thu, 29 Oct 2020 20:03:18 -0400 Subject: [PATCH 02/11] minor fix --- app/api/routes/resource_modification.py | 7 +++++-- app/api/routes/resource_retrieval.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 0f2f7a59..275c33dc 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -165,8 +165,11 @@ def update_votes(id, vote_direction): db.session.commit() return utils.standardize_response( - payload=dict(data=resource.serialize), - datatype="resource") + payload=dict( + data={**resource.serialize, 'user_vote_direction': vote_info.current_direction} + ), + datatype="resource" + ) def add_click(id): diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index 90553700..cbc5da14 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -1,11 +1,12 @@ from datetime import datetime from dateutil import parser -from flask import redirect, request, g +from flask import redirect, request from sqlalchemy import func, or_, and_, text from app import db, utils as utils from app.api import bp +from app.api.auth import jwt_to_key from app.api.routes.helpers import failures_counter, latency_summary, logger from app.models import Category, Language, Resource, VoteInformation, Key from configs import Config @@ -44,7 +45,7 @@ def get_resources(): apikey = request.headers.get('x-apikey') filters = {'apikey': apikey, 'denied': False} - key = Key.query.filter_by(**filters).first() if apikey else g.get('auth_key') + key = Key.query.filter_by(**filters).first() if apikey else jwt_to_key() q = Resource.query if key: @@ -114,18 +115,19 @@ def get_resources(): if not paginated_resources: return redirect('/404') resource_list = [ - {**item.Resource.serialize, 'vote_direction': item[1]} - if key else item.serialize for item in paginated_resources.items + {**item.Resource.serialize, 'user_vote_direction': item[1]} if key + else item.serialize + for item in paginated_resources.items ] details = resource_paginator.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, - **details), - datatype="resources") + return utils.standardize_response( + payload=dict(data=resource_list, **details), + datatype="resources" + ) def get_resource(id): From 1bf9fc83b5e51f599852683a9b9855517fb77891 Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Sun, 1 Nov 2020 17:49:44 -0500 Subject: [PATCH 03/11] add test, update api doc, make adjustments to model, etc --- app/api/auth.py | 15 ++++++++++ app/api/routes/resource_modification.py | 25 ++++++++-------- app/api/routes/resource_retrieval.py | 30 +++++++------------ app/models.py | 10 ++++++- app/static/openapi.yaml | 7 +++++ .../test_routes/test_resource_retreival.py | 2 ++ .../unit/test_routes/test_resource_update.py | 8 ++++- 7 files changed, 63 insertions(+), 34 deletions(-) diff --git a/app/api/auth.py b/app/api/auth.py index 8a54e376..d2299d54 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -146,6 +146,21 @@ def wrapper(*args, **kwargs): return wrapper +def set_api_key(func): + def wrapper(*args, **kwargs): + apikey = request.headers.get('x-apikey') + try: + filters = {'apikey': apikey, 'denied': False} + key = Key.query.filter_by(**filters).first() if apikey else jwt_to_key() + except Exception: + return standardize_response(status_code=500) + + g.auth_key = key + + return func(*args, **kwargs) + return wrapper + + def is_user_oc_member(email, password): response = requests.post( 'https://api.operationcode.org/auth/login/', diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 275c33dc..19e6e204 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -128,15 +128,18 @@ def update_resource_click(id): return add_click(id) -def update_votes(id, vote_direction): +def update_votes(id, vote_direction_attribute): resource = Resource.query.get(id) if not resource: return redirect('/404') - initial_count = getattr(resource, vote_direction) - opposite_direction = 'downvotes' if vote_direction == 'upvotes' else 'upvotes' - opposite_count = getattr(resource, opposite_direction) + initial_count = getattr(resource, vote_direction_attribute) + vote_direction = vote_direction_attribute[:-1] + + opposite_direction_attribute = 'downvotes' if vote_direction_attribute == 'upvotes' else 'upvotes' + opposite_direction = opposite_direction_attribute[:-1] + opposite_count = getattr(resource, opposite_direction_attribute) api_key = g.auth_key.apikey vote_info = VoteInformation.query.get( @@ -152,22 +155,20 @@ def update_votes(id, vote_direction): ) new_vote_info.voter = voter resource.voters.append(new_vote_info) - setattr(resource, vote_direction, initial_count + 1) + setattr(resource, vote_direction_attribute, initial_count + 1) else: if vote_info.current_direction == vote_direction: - setattr(resource, vote_direction, initial_count - 1) - setattr(vote_info, 'current_direction', 'None') + setattr(resource, vote_direction_attribute, initial_count - 1) + setattr(vote_info, 'current_direction', None) else: - setattr(resource, opposite_direction, opposite_count - 1) \ + setattr(resource, opposite_direction_attribute, opposite_count - 1) \ if vote_info.current_direction == opposite_direction else None - setattr(resource, vote_direction, initial_count + 1) + setattr(resource, vote_direction_attribute, initial_count + 1) setattr(vote_info, 'current_direction', vote_direction) db.session.commit() return utils.standardize_response( - payload=dict( - data={**resource.serialize, 'user_vote_direction': vote_info.current_direction} - ), + payload=dict(data=resource.serialize_with_vote_direction(api_key)), datatype="resource" ) diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index cbc5da14..5dd37e2a 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -1,12 +1,12 @@ from datetime import datetime from dateutil import parser -from flask import redirect, request -from sqlalchemy import func, or_, and_, text +from flask import redirect, request, g +from sqlalchemy import func, or_, text -from app import db, utils as utils +from app import utils as utils from app.api import bp -from app.api.auth import jwt_to_key +from app.api.auth import set_api_key from app.api.routes.helpers import failures_counter, latency_summary, logger from app.models import Category, Language, Resource, VoteInformation, Key from configs import Config @@ -26,6 +26,7 @@ def resource(id): return get_resource(id) +@set_api_key def get_resources(): """ Gets a paginated list of resources. @@ -42,20 +43,9 @@ def get_resources(): category = request.args.get('category') updated_after = request.args.get('updated_after') free = request.args.get('free') - - apikey = request.headers.get('x-apikey') - filters = {'apikey': apikey, 'denied': False} - key = Key.query.filter_by(**filters).first() if apikey else jwt_to_key() + api_key = g.auth_key.apikey if g.auth_key else None q = Resource.query - if key: - q = db.session.query(Resource, VoteInformation.current_direction).outerjoin( - VoteInformation, - and_( - Resource.id == VoteInformation.resource_id, - VoteInformation.voter_apikey == key.apikey - ) - ) # Filter on languages if languages: @@ -115,9 +105,7 @@ def get_resources(): if not paginated_resources: return redirect('/404') resource_list = [ - {**item.Resource.serialize, 'user_vote_direction': item[1]} if key - else item.serialize - for item in paginated_resources.items + item.serialize_with_vote_direction(api_key) for item in paginated_resources.items ] details = resource_paginator.details(paginated_resources) except Exception as e: @@ -130,12 +118,14 @@ def get_resources(): ) +@set_api_key def get_resource(id): resource = Resource.query.get(id) + api_key = g.auth_key.apikey if g.auth_key else None if resource: return utils.standardize_response( - payload=dict(data=(resource.serialize)), + payload=dict(data=(resource.serialize_with_vote_direction(api_key))), datatype="resource") return redirect('/404') diff --git a/app/models.py b/app/models.py index 85e11bcb..f215b60d 100644 --- a/app/models.py +++ b/app/models.py @@ -63,6 +63,14 @@ def serialize(self): 'last_updated': updated } + def serialize_with_vote_direction(self, apikey=None): + return { + **self.serialize, + 'user_vote_direction': next( + (voter.current_direction for voter in self.voters if voter.voter_apikey == apikey), None + ) if apikey else None + } + @property def serialize_algolia_search(self): result = self.serialize @@ -200,6 +208,6 @@ def __repr__(self): class VoteInformation(db.Model): voter_apikey = db.Column(db.String, db.ForeignKey('key.apikey'), primary_key=True) resource_id = db.Column(db.Integer, db.ForeignKey('resource.id'), primary_key=True) - current_direction = db.Column(db.String, nullable=False) + current_direction = db.Column(db.String, nullable=True) resource = db.relationship('Resource', back_populates='voters') voter = db.relationship('Key', back_populates='voted_resources') diff --git a/app/static/openapi.yaml b/app/static/openapi.yaml index bde51a36..b8426998 100644 --- a/app/static/openapi.yaml +++ b/app/static/openapi.yaml @@ -156,6 +156,7 @@ paths: times_clicked: 0 upvotes: 0 url: 'https://www.cprogramming.com/tutorial/c-tutorial.html' + user_vote_direction: null number_of_pages: 1 page: 0 records_per_page: 20 @@ -398,6 +399,7 @@ paths: times_clicked: 0 upvotes: 0 url: 'http://www.freetechbooks.com/' + user_vote_direction: null status: 'ok' status_code: 200 404: @@ -447,6 +449,7 @@ paths: times_clicked: 3 upvotes: 1 url: 'http://www.test.com' + user_vote_direction: null status: 'ok' status_code: 200 404: @@ -510,6 +513,7 @@ paths: times_clicked: 2 upvotes: 0 url: 'http://thinking-forth.sourceforge.net/' + user_vote_direction: null status: 'ok' status_code: 200 404: @@ -554,6 +558,7 @@ paths: times_clicked: 0 upvotes: 1 url: 'http://teachyourkidstocode.com/' + user_vote_direction: 'upvote' status: 'ok' status_code: 200 404: @@ -598,6 +603,7 @@ paths: times_clicked: 0 upvotes: 0 url: 'http://teachyourkidstocode.com/' + user_vote_direction: 'downvote' status: 'ok' status_code: 200 404: @@ -658,6 +664,7 @@ paths: times_clicked: 3 upvotes: 1 url: 'http://www.test.com' + user_vote_direction: null has_next: false has_prev: false number_of_pages: 1 diff --git a/tests/unit/test_routes/test_resource_retreival.py b/tests/unit/test_routes/test_resource_retreival.py index 5b6b7ee3..1e1f62f6 100644 --- a/tests/unit/test_routes/test_resource_retreival.py +++ b/tests/unit/test_routes/test_resource_retreival.py @@ -20,6 +20,7 @@ def test_get_resources(module_client, module_db): assert (isinstance(resource.get('category'), str)) assert (resource.get('category')) assert (isinstance(resource.get('languages'), list)) + assert ('user_vote_direction' in resource) assert (response.json['number_of_pages'] is not None) @@ -62,6 +63,7 @@ def test_get_single_resource(module_client, module_db): assert (isinstance(resource.get('category'), str)) assert (resource.get('category')) assert (isinstance(resource.get('languages'), list)) + assert ('user_vote_direction' in resource) assert (resource.get('id') == 5) diff --git a/tests/unit/test_routes/test_resource_update.py b/tests/unit/test_routes/test_resource_update.py index ef980b06..6719c556 100644 --- a/tests/unit/test_routes/test_resource_update.py +++ b/tests/unit/test_routes/test_resource_update.py @@ -10,6 +10,7 @@ def test_update_votes(module_client, module_db, fake_auth_from_oc, fake_algolia_ client = module_client UPVOTE = 'upvote' DOWNVOTE = 'downvote' + USER_VOTE_DIRECTION = 'user_vote_direction' id = 1 apikey = get_api_key(client) @@ -23,6 +24,7 @@ def test_update_votes(module_client, module_db, fake_auth_from_oc, fake_algolia_ assert (response.status_code == 200) assert (response.json['resource'].get(f"{UPVOTE}s") == initial_upvotes + 1) + assert (response.json['resource'].get(USER_VOTE_DIRECTION) == UPVOTE) response = client.put( f"/api/v1/resources/{id}/{DOWNVOTE}", @@ -32,6 +34,7 @@ def test_update_votes(module_client, module_db, fake_auth_from_oc, fake_algolia_ assert (response.status_code == 200) assert (response.json['resource'].get(f"{UPVOTE}s") == initial_upvotes) assert (response.json['resource'].get(f"{DOWNVOTE}s") == initial_downvotes + 1) + assert (response.json['resource'].get(USER_VOTE_DIRECTION) == DOWNVOTE) response = client.put( f"/api/v1/resources/{id}/{DOWNVOTE}", @@ -39,7 +42,7 @@ def test_update_votes(module_client, module_db, fake_auth_from_oc, fake_algolia_ headers={'x-apikey': apikey}) assert (response.status_code == 200) assert (response.json['resource'].get(f"{DOWNVOTE}s") == initial_downvotes) - + assert (response.json['resource'].get(USER_VOTE_DIRECTION) == None) def test_update_votes_invalid( module_client, module_db, fake_auth_from_oc, fake_algolia_save): @@ -262,6 +265,7 @@ def test_update_votes_authorization_header( id = 1 UPVOTE = 'upvote' DOWNVOTE = 'downvote' + USER_VOTE_DIRECTION = 'user_vote_direction' data = client.get(f"api/v1/resources/{id}").json['resource'] response = client.put( @@ -273,6 +277,7 @@ def test_update_votes_authorization_header( assert (response.status_code == 200) assert (response.json['resource'].get(f"{UPVOTE}s") == initial_upvotes + 1) + assert (response.json['resource'].get(USER_VOTE_DIRECTION) == UPVOTE) response = client.put( f"/api/v1/resources/{id}/downvote", @@ -282,6 +287,7 @@ def test_update_votes_authorization_header( assert (response.status_code == 200) assert (response.json['resource'].get(f"{UPVOTE}s") == initial_upvotes) assert (response.json['resource'].get(f"{DOWNVOTE}s") == initial_downvotes + 1) + assert (response.json['resource'].get(USER_VOTE_DIRECTION) == DOWNVOTE) def test_delete_unused_languages(module_client, module_db, From b2149912b71c6fe04daa4901d75c2a825899c0f6 Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Sun, 1 Nov 2020 18:35:35 -0500 Subject: [PATCH 04/11] some adjustments --- app/api/routes/resource_creation.py | 2 +- app/api/routes/resource_modification.py | 2 +- app/static/openapi.yaml | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/api/routes/resource_creation.py b/app/api/routes/resource_creation.py index 5ecbbebd..2fe9a8b6 100644 --- a/app/api/routes/resource_creation.py +++ b/app/api/routes/resource_creation.py @@ -67,7 +67,7 @@ def create_resources(json, db): logger.exception(e) return utils.standardize_response(status_code=500) - created_resources.append(new_resource.serialize) + created_resources.append(new_resource.serialize_with_vote_direction()) # Take all the created resources and save them in Algolia with one API call try: diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 19e6e204..797690d3 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -99,7 +99,7 @@ def get_unique_resource_languages_as_strings(): db.session.commit() return utils.standardize_response( - payload=dict(data=resource.serialize), + payload=dict(data=resource.serialize_with_vote_direction(g.auth_key.apikey)), datatype="resource" ) diff --git a/app/static/openapi.yaml b/app/static/openapi.yaml index b8426998..6fd84f0e 100644 --- a/app/static/openapi.yaml +++ b/app/static/openapi.yaml @@ -156,7 +156,6 @@ paths: times_clicked: 0 upvotes: 0 url: 'https://www.cprogramming.com/tutorial/c-tutorial.html' - user_vote_direction: null number_of_pages: 1 page: 0 records_per_page: 20 @@ -513,7 +512,6 @@ paths: times_clicked: 2 upvotes: 0 url: 'http://thinking-forth.sourceforge.net/' - user_vote_direction: null status: 'ok' status_code: 200 404: @@ -729,6 +727,7 @@ paths: times_clicked: 3 upvotes: 1 url: 'https://www.w3schools.com/css/' + user_vote_direction: null status: 'ok' status_code: 200 400: From 68babeb5470f833f6a672a5d416d6ca543bea8a3 Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Sun, 1 Nov 2020 19:24:31 -0500 Subject: [PATCH 05/11] fix lint error --- app/api/routes/resource_modification.py | 7 +++++-- app/api/routes/resource_retrieval.py | 5 +++-- app/models.py | 3 ++- tests/unit/test_routes/test_resource_update.py | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 797690d3..0ff591f5 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -99,7 +99,9 @@ def get_unique_resource_languages_as_strings(): db.session.commit() return utils.standardize_response( - payload=dict(data=resource.serialize_with_vote_direction(g.auth_key.apikey)), + payload=dict( + data=resource.serialize_with_vote_direction(g.auth_key.apikey) + ), datatype="resource" ) @@ -137,7 +139,8 @@ def update_votes(id, vote_direction_attribute): initial_count = getattr(resource, vote_direction_attribute) vote_direction = vote_direction_attribute[:-1] - opposite_direction_attribute = 'downvotes' if vote_direction_attribute == 'upvotes' else 'upvotes' + opposite_direction_attribute = 'downvotes' \ + if vote_direction_attribute == 'upvotes' else 'upvotes' opposite_direction = opposite_direction_attribute[:-1] opposite_count = getattr(resource, opposite_direction_attribute) diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index 5dd37e2a..941f3aa3 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -8,7 +8,7 @@ from app.api import bp from app.api.auth import set_api_key from app.api.routes.helpers import failures_counter, latency_summary, logger -from app.models import Category, Language, Resource, VoteInformation, Key +from app.models import Category, Language, Resource from configs import Config @@ -105,7 +105,8 @@ def get_resources(): if not paginated_resources: return redirect('/404') resource_list = [ - item.serialize_with_vote_direction(api_key) for item in paginated_resources.items + item.serialize_with_vote_direction(api_key) + for item in paginated_resources.items ] details = resource_paginator.details(paginated_resources) except Exception as e: diff --git a/app/models.py b/app/models.py index f215b60d..f955e90b 100644 --- a/app/models.py +++ b/app/models.py @@ -67,7 +67,8 @@ def serialize_with_vote_direction(self, apikey=None): return { **self.serialize, 'user_vote_direction': next( - (voter.current_direction for voter in self.voters if voter.voter_apikey == apikey), None + (voter.current_direction for voter in self.voters + if voter.voter_apikey == apikey), None ) if apikey else None } diff --git a/tests/unit/test_routes/test_resource_update.py b/tests/unit/test_routes/test_resource_update.py index 6719c556..c7ced481 100644 --- a/tests/unit/test_routes/test_resource_update.py +++ b/tests/unit/test_routes/test_resource_update.py @@ -42,7 +42,8 @@ def test_update_votes(module_client, module_db, fake_auth_from_oc, fake_algolia_ headers={'x-apikey': apikey}) assert (response.status_code == 200) assert (response.json['resource'].get(f"{DOWNVOTE}s") == initial_downvotes) - assert (response.json['resource'].get(USER_VOTE_DIRECTION) == None) + assert (response.json['resource'].get(USER_VOTE_DIRECTION) is None) + def test_update_votes_invalid( module_client, module_db, fake_auth_from_oc, fake_algolia_save): From 6f728f5d6825ad3c1bd689e8cd0c9b811c10ab56 Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Tue, 3 Nov 2020 19:42:21 -0500 Subject: [PATCH 06/11] add schema and modify decorator --- app/api/auth.py | 26 +++++++++---------------- app/api/routes/resource_modification.py | 12 ++++++++++++ app/api/routes/resource_retrieval.py | 6 +++--- app/static/openapi.yaml | 4 ++++ 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/api/auth.py b/app/api/auth.py index d2299d54..af75cc9d 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -1,6 +1,7 @@ import uuid import os from enum import Enum +import functools import requests from app import db @@ -127,7 +128,11 @@ def get_api_key_from_authenticated_email(email): return apikey -def authenticate(func): +def authenticate(func=None, allow_no_auth_key=False): + if not func: + return functools.partial(authenticate, allow_no_auth_key=allow_no_auth_key) + + @functools.wraps(func) def wrapper(*args, **kwargs): apikey = request.headers.get('x-apikey') try: @@ -136,24 +141,11 @@ def wrapper(*args, **kwargs): except Exception: return standardize_response(status_code=500) - if not key: + if not key and not allow_no_auth_key: return standardize_response(status_code=401) - log_request(request, key) - g.auth_key = key - - return func(*args, **kwargs) - return wrapper - - -def set_api_key(func): - def wrapper(*args, **kwargs): - apikey = request.headers.get('x-apikey') - try: - filters = {'apikey': apikey, 'denied': False} - key = Key.query.filter_by(**filters).first() if apikey else jwt_to_key() - except Exception: - return standardize_response(status_code=500) + if key: + log_request(request, key) g.auth_key = key diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 0ff591f5..a328c5ab 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -14,6 +14,18 @@ import json as json_module +# def decorator_function_with_arguments(arg1, arg2, arg3): +# def wrap(f): +# print("Inside wrap()") +# +# def wrapped_f(*args, **kwargs): +# print("Inside wrapped_f()") +# print("Decorator arguments:", arg1, arg2, arg3) +# return f(*args, **kwargs) +# return wrapped_f +# return wrap + + @latency_summary.time() @failures_counter.count_exceptions() @bp.route('/resources/', methods=['PUT'], endpoint='update_resource') diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index 941f3aa3..6dfa1b67 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -6,7 +6,7 @@ from app import utils as utils from app.api import bp -from app.api.auth import set_api_key +from app.api.auth import authenticate from app.api.routes.helpers import failures_counter, latency_summary, logger from app.models import Category, Language, Resource from configs import Config @@ -26,7 +26,7 @@ def resource(id): return get_resource(id) -@set_api_key +@authenticate(allow_no_auth_key=True) def get_resources(): """ Gets a paginated list of resources. @@ -119,7 +119,7 @@ def get_resources(): ) -@set_api_key +@authenticate(allow_no_auth_key=True) def get_resource(id): resource = Resource.query.get(id) api_key = g.auth_key.apikey if g.auth_key else None diff --git a/app/static/openapi.yaml b/app/static/openapi.yaml index 6fd84f0e..a59908dd 100644 --- a/app/static/openapi.yaml +++ b/app/static/openapi.yaml @@ -1098,6 +1098,10 @@ components: url: type: string description: Resource url + user_vote_direction: + type: string + nullable: true + description: Vote direction of a logged in user ResourceList: type: array From d40d585409116f4e74c92c1c062149182a338bf0 Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Tue, 3 Nov 2020 19:46:48 -0500 Subject: [PATCH 07/11] remove unused code --- app/api/routes/resource_modification.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index a328c5ab..0ff591f5 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -14,18 +14,6 @@ import json as json_module -# def decorator_function_with_arguments(arg1, arg2, arg3): -# def wrap(f): -# print("Inside wrap()") -# -# def wrapped_f(*args, **kwargs): -# print("Inside wrapped_f()") -# print("Decorator arguments:", arg1, arg2, arg3) -# return f(*args, **kwargs) -# return wrapped_f -# return wrap - - @latency_summary.time() @failures_counter.count_exceptions() @bp.route('/resources/', methods=['PUT'], endpoint='update_resource') From 5a8c66454fef2c0ceb1c2e99d351bff3809ad66f Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Wed, 4 Nov 2020 18:09:58 -0500 Subject: [PATCH 08/11] have one serialize method for "Resource" --- app/api/routes/resource_creation.py | 2 +- app/api/routes/resource_modification.py | 12 +++++++----- app/api/routes/resource_retrieval.py | 4 ++-- app/models.py | 12 +++--------- tests/unit/test_models.py | 5 +++-- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/app/api/routes/resource_creation.py b/app/api/routes/resource_creation.py index 2fe9a8b6..84171c98 100644 --- a/app/api/routes/resource_creation.py +++ b/app/api/routes/resource_creation.py @@ -67,7 +67,7 @@ def create_resources(json, db): logger.exception(e) return utils.standardize_response(status_code=500) - created_resources.append(new_resource.serialize_with_vote_direction()) + created_resources.append(new_resource.serialize()) # Take all the created resources and save them in Algolia with one API call try: diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 0ff591f5..50ea2f20 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -35,6 +35,7 @@ def put_resource(id): def update_resource(id, json, db): resource = Resource.query.get(id) + api_key = g.auth_key.apikey if not resource: return redirect('/404') @@ -54,11 +55,12 @@ def get_unique_resource_languages_as_strings(): try: logger.info( - f"Updating resource. Old data: {json_module.dumps(resource.serialize)}") + f"Updating resource. Old data: " + f"{json_module.dumps(resource.serialize(api_key))}") if json.get('languages') is not None: old_languages = resource.languages[:] resource.languages = langs - index_object['languages'] = resource.serialize['languages'] + index_object['languages'] = resource.serialize(api_key)['languages'] resource_languages = get_unique_resource_languages_as_strings() for language in old_languages: if language.name not in resource_languages: @@ -100,7 +102,7 @@ def get_unique_resource_languages_as_strings(): return utils.standardize_response( payload=dict( - data=resource.serialize_with_vote_direction(g.auth_key.apikey) + data=resource.serialize(g.auth_key.apikey) ), datatype="resource" ) @@ -171,7 +173,7 @@ def update_votes(id, vote_direction_attribute): db.session.commit() return utils.standardize_response( - payload=dict(data=resource.serialize_with_vote_direction(api_key)), + payload=dict(data=resource.serialize(api_key)), datatype="resource" ) @@ -187,5 +189,5 @@ def add_click(id): db.session.commit() return utils.standardize_response( - payload=dict(data=resource.serialize), + payload=dict(data=resource.serialize()), datatype="resource") diff --git a/app/api/routes/resource_retrieval.py b/app/api/routes/resource_retrieval.py index 6dfa1b67..ffb0c760 100644 --- a/app/api/routes/resource_retrieval.py +++ b/app/api/routes/resource_retrieval.py @@ -105,7 +105,7 @@ def get_resources(): if not paginated_resources: return redirect('/404') resource_list = [ - item.serialize_with_vote_direction(api_key) + item.serialize(api_key) for item in paginated_resources.items ] details = resource_paginator.details(paginated_resources) @@ -126,7 +126,7 @@ def get_resource(id): if resource: return utils.standardize_response( - payload=dict(data=(resource.serialize_with_vote_direction(api_key))), + payload=dict(data=(resource.serialize(api_key))), datatype="resource") return redirect('/404') diff --git a/app/models.py b/app/models.py index f955e90b..21522506 100644 --- a/app/models.py +++ b/app/models.py @@ -36,8 +36,7 @@ class Resource(TimestampMixin, db.Model): times_clicked = db.Column(db.INTEGER, default=0) voters = db.relationship('VoteInformation', back_populates='resource') - @property - def serialize(self): + def serialize(self, apikey=None): """Return object data in easily serializeable format""" if self.created_at: created = self.created_at.strftime("%Y-%m-%d %H:%M:%S") @@ -60,12 +59,7 @@ def serialize(self): 'downvotes': self.downvotes, 'times_clicked': self.times_clicked, 'created_at': created, - 'last_updated': updated - } - - def serialize_with_vote_direction(self, apikey=None): - return { - **self.serialize, + 'last_updated': updated, 'user_vote_direction': next( (voter.current_direction for voter in self.voters if voter.voter_apikey == apikey), None @@ -74,7 +68,7 @@ def serialize_with_vote_direction(self, apikey=None): @property def serialize_algolia_search(self): - result = self.serialize + result = self.serialize() result['objectID'] = self.id return result diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 5c7ae9e5..f4394fc0 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -21,7 +21,7 @@ def test_resource(): "\tCategory: \n" "\tURL: https://resource.url\n>") - assert (resource.serialize == { + assert (resource.serialize() == { 'id': None, 'name': 'name', 'url': 'https://resource.url', @@ -33,7 +33,8 @@ def test_resource(): 'downvotes': None, 'times_clicked': None, 'created_at': '', - 'last_updated': '' + 'last_updated': '', + 'user_vote_direction': None }) # Test equality From d0f7ee53405755f798c3bd531879389fd450ee87 Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Thu, 5 Nov 2020 21:17:19 -0500 Subject: [PATCH 09/11] nit fix --- app/api/routes/resource_modification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index 50ea2f20..fcdef6c9 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -102,7 +102,7 @@ def get_unique_resource_languages_as_strings(): return utils.standardize_response( payload=dict( - data=resource.serialize(g.auth_key.apikey) + data=resource.serialize(api_key) ), datatype="resource" ) From c71387cf512d9d9721983d4323af8d4ddc4f3f1f Mon Sep 17 00:00:00 2001 From: Aaron Suarez Date: Fri, 6 Nov 2020 08:52:48 -0600 Subject: [PATCH 10/11] Add migration for include vote direction --- .../e6ac83ef4570_include_vote_direction.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 migrations/versions/e6ac83ef4570_include_vote_direction.py diff --git a/migrations/versions/e6ac83ef4570_include_vote_direction.py b/migrations/versions/e6ac83ef4570_include_vote_direction.py new file mode 100644 index 00000000..b5037dbc --- /dev/null +++ b/migrations/versions/e6ac83ef4570_include_vote_direction.py @@ -0,0 +1,33 @@ +"""include vote direction + +Revision ID: e6ac83ef4570 +Revises: fc34137ad3ba +Create Date: 2020-11-05 20:04:51.029258 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision = 'e6ac83ef4570' +down_revision = 'fc34137ad3ba' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('vote_information', 'current_direction', + existing_type=sa.VARCHAR(), + nullable=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('vote_information', 'current_direction', + existing_type=sa.VARCHAR(), + nullable=False) + # ### end Alembic commands ### From 541450052d96da120f45355af3d6b7daee38a98d Mon Sep 17 00:00:00 2001 From: Chongyoon Nah Date: Fri, 6 Nov 2020 15:58:59 -0500 Subject: [PATCH 11/11] fix "add_click" route --- app/api/routes/resource_modification.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/routes/resource_modification.py b/app/api/routes/resource_modification.py index fcdef6c9..763bdbd3 100644 --- a/app/api/routes/resource_modification.py +++ b/app/api/routes/resource_modification.py @@ -128,6 +128,7 @@ def change_votes(id, vote_direction): @latency_summary.time() @failures_counter.count_exceptions() @bp.route('/resources//click', methods=['PUT']) +@authenticate(allow_no_auth_key=True) def update_resource_click(id): return add_click(id) @@ -180,6 +181,7 @@ def update_votes(id, vote_direction_attribute): def add_click(id): resource = Resource.query.get(id) + api_key = g.auth_key.apikey if g.auth_key else None if not resource: return redirect('/404') @@ -189,5 +191,5 @@ def add_click(id): db.session.commit() return utils.standardize_response( - payload=dict(data=resource.serialize()), + payload=dict(data=resource.serialize(api_key)), datatype="resource")