Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import uuid
import os
from enum import Enum
import functools

import requests
from app import db
Expand Down Expand Up @@ -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:
Expand All @@ -136,10 +141,12 @@ 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)
if key:
log_request(request, key)

g.auth_key = key

return func(*args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion app/api/routes/resource_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

# Take all the created resources and save them in Algolia with one API call
try:
Expand Down
41 changes: 26 additions & 15 deletions app/api/routes/resource_modification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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:
Expand Down Expand Up @@ -99,7 +101,9 @@ def get_unique_resource_languages_as_strings():
db.session.commit()

return utils.standardize_response(
payload=dict(data=resource.serialize),
payload=dict(
data=resource.serialize(api_key)
),
datatype="resource"
)

Expand All @@ -124,19 +128,24 @@ def change_votes(id, vote_direction):
@latency_summary.time()
@failures_counter.count_exceptions()
@bp.route('/resources/<int:id>/click', methods=['PUT'])
@authenticate(allow_no_auth_key=True)
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(
Expand All @@ -152,25 +161,27 @@ 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),
datatype="resource")
payload=dict(data=resource.serialize(api_key)),
datatype="resource"
)


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')
Expand All @@ -180,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")
20 changes: 13 additions & 7 deletions app/api/routes/resource_retrieval.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from datetime import datetime

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

from app import utils as utils
from app.api import bp
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
Expand All @@ -25,6 +26,7 @@ def resource(id):
return get_resource(id)


@authenticate(allow_no_auth_key=True)
def get_resources():
"""
Gets a paginated list of resources.
Expand All @@ -41,6 +43,7 @@ def get_resources():
category = request.args.get('category')
updated_after = request.args.get('updated_after')
free = request.args.get('free')
api_key = g.auth_key.apikey if g.auth_key else None

q = Resource.query

Expand Down Expand Up @@ -102,25 +105,28 @@ def get_resources():
if not paginated_resources:
return redirect('/404')
resource_list = [
resource.serialize for resource in paginated_resources.items
item.serialize(api_key)
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"
)


@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

if resource:
return utils.standardize_response(
payload=dict(data=(resource.serialize)),
payload=dict(data=(resource.serialize(api_key))),
datatype="resource")

return redirect('/404')
13 changes: 8 additions & 5 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -60,12 +59,16 @@ def serialize(self):
'downvotes': self.downvotes,
'times_clicked': self.times_clicked,
'created_at': created,
'last_updated': updated
'last_updated': updated,
'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
result = self.serialize()
result['objectID'] = self.id
return result

Expand Down Expand Up @@ -200,6 +203,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')
10 changes: 10 additions & 0 deletions app/static/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ paths:
times_clicked: 0
upvotes: 0
url: 'http://www.freetechbooks.com/'
user_vote_direction: null
status: 'ok'
status_code: 200
404:
Expand Down Expand Up @@ -447,6 +448,7 @@ paths:
times_clicked: 3
upvotes: 1
url: 'http://www.test.com'
user_vote_direction: null
status: 'ok'
status_code: 200
404:
Expand Down Expand Up @@ -554,6 +556,7 @@ paths:
times_clicked: 0
upvotes: 1
url: 'http://teachyourkidstocode.com/'
user_vote_direction: 'upvote'
status: 'ok'
status_code: 200
404:
Expand Down Expand Up @@ -598,6 +601,7 @@ paths:
times_clicked: 0
upvotes: 0
url: 'http://teachyourkidstocode.com/'
user_vote_direction: 'downvote'
status: 'ok'
status_code: 200
404:
Expand Down Expand Up @@ -658,6 +662,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
Expand Down Expand Up @@ -722,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:
Expand Down Expand Up @@ -1092,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
Expand Down
33 changes: 33 additions & 0 deletions migrations/versions/e6ac83ef4570_include_vote_direction.py
Original file line number Diff line number Diff line change
@@ -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 ###
5 changes: 3 additions & 2 deletions tests/unit/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_resource():
"\tCategory: <Category Category>\n"
"\tURL: https://resource.url\n>")

assert (resource.serialize == {
assert (resource.serialize() == {
'id': None,
'name': 'name',
'url': 'https://resource.url',
Expand All @@ -33,7 +33,8 @@ def test_resource():
'downvotes': None,
'times_clicked': None,
'created_at': '',
'last_updated': ''
'last_updated': '',
'user_vote_direction': None
})

# Test equality
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_routes/test_resource_retreival.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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)

Expand Down
Loading