Skip to content

Commit 202b27d

Browse files
chynhAaron Suarez
and
Aaron Suarez
authored
Include vote direction (#408)
* include user's current vote information * minor fix * add test, update api doc, make adjustments to model, etc * some adjustments * fix lint error * add schema and modify decorator * remove unused code * have one serialize method for "Resource" * nit fix * Add migration for include vote direction * fix "add_click" route Co-authored-by: Aaron Suarez <[email protected]>
1 parent f27184a commit 202b27d

10 files changed

+113
-33
lines changed

app/api/auth.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import uuid
22
import os
33
from enum import Enum
4+
import functools
45

56
import requests
67
from app import db
@@ -127,7 +128,11 @@ def get_api_key_from_authenticated_email(email):
127128
return apikey
128129

129130

130-
def authenticate(func):
131+
def authenticate(func=None, allow_no_auth_key=False):
132+
if not func:
133+
return functools.partial(authenticate, allow_no_auth_key=allow_no_auth_key)
134+
135+
@functools.wraps(func)
131136
def wrapper(*args, **kwargs):
132137
apikey = request.headers.get('x-apikey')
133138
try:
@@ -136,10 +141,12 @@ def wrapper(*args, **kwargs):
136141
except Exception:
137142
return standardize_response(status_code=500)
138143

139-
if not key:
144+
if not key and not allow_no_auth_key:
140145
return standardize_response(status_code=401)
141146

142-
log_request(request, key)
147+
if key:
148+
log_request(request, key)
149+
143150
g.auth_key = key
144151

145152
return func(*args, **kwargs)

app/api/routes/resource_creation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def create_resources(json, db):
6767
logger.exception(e)
6868
return utils.standardize_response(status_code=500)
6969

70-
created_resources.append(new_resource.serialize)
70+
created_resources.append(new_resource.serialize())
7171

7272
# Take all the created resources and save them in Algolia with one API call
7373
try:

app/api/routes/resource_modification.py

+26-15
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def put_resource(id):
3535

3636
def update_resource(id, json, db):
3737
resource = Resource.query.get(id)
38+
api_key = g.auth_key.apikey
3839

3940
if not resource:
4041
return redirect('/404')
@@ -54,11 +55,12 @@ def get_unique_resource_languages_as_strings():
5455

5556
try:
5657
logger.info(
57-
f"Updating resource. Old data: {json_module.dumps(resource.serialize)}")
58+
f"Updating resource. Old data: "
59+
f"{json_module.dumps(resource.serialize(api_key))}")
5860
if json.get('languages') is not None:
5961
old_languages = resource.languages[:]
6062
resource.languages = langs
61-
index_object['languages'] = resource.serialize['languages']
63+
index_object['languages'] = resource.serialize(api_key)['languages']
6264
resource_languages = get_unique_resource_languages_as_strings()
6365
for language in old_languages:
6466
if language.name not in resource_languages:
@@ -99,7 +101,9 @@ def get_unique_resource_languages_as_strings():
99101
db.session.commit()
100102

101103
return utils.standardize_response(
102-
payload=dict(data=resource.serialize),
104+
payload=dict(
105+
data=resource.serialize(api_key)
106+
),
103107
datatype="resource"
104108
)
105109

@@ -124,19 +128,24 @@ def change_votes(id, vote_direction):
124128
@latency_summary.time()
125129
@failures_counter.count_exceptions()
126130
@bp.route('/resources/<int:id>/click', methods=['PUT'])
131+
@authenticate(allow_no_auth_key=True)
127132
def update_resource_click(id):
128133
return add_click(id)
129134

130135

131-
def update_votes(id, vote_direction):
136+
def update_votes(id, vote_direction_attribute):
132137
resource = Resource.query.get(id)
133138

134139
if not resource:
135140
return redirect('/404')
136141

137-
initial_count = getattr(resource, vote_direction)
138-
opposite_direction = 'downvotes' if vote_direction == 'upvotes' else 'upvotes'
139-
opposite_count = getattr(resource, opposite_direction)
142+
initial_count = getattr(resource, vote_direction_attribute)
143+
vote_direction = vote_direction_attribute[:-1]
144+
145+
opposite_direction_attribute = 'downvotes' \
146+
if vote_direction_attribute == 'upvotes' else 'upvotes'
147+
opposite_direction = opposite_direction_attribute[:-1]
148+
opposite_count = getattr(resource, opposite_direction_attribute)
140149

141150
api_key = g.auth_key.apikey
142151
vote_info = VoteInformation.query.get(
@@ -152,25 +161,27 @@ def update_votes(id, vote_direction):
152161
)
153162
new_vote_info.voter = voter
154163
resource.voters.append(new_vote_info)
155-
setattr(resource, vote_direction, initial_count + 1)
164+
setattr(resource, vote_direction_attribute, initial_count + 1)
156165
else:
157166
if vote_info.current_direction == vote_direction:
158-
setattr(resource, vote_direction, initial_count - 1)
159-
setattr(vote_info, 'current_direction', 'None')
167+
setattr(resource, vote_direction_attribute, initial_count - 1)
168+
setattr(vote_info, 'current_direction', None)
160169
else:
161-
setattr(resource, opposite_direction, opposite_count - 1) \
170+
setattr(resource, opposite_direction_attribute, opposite_count - 1) \
162171
if vote_info.current_direction == opposite_direction else None
163-
setattr(resource, vote_direction, initial_count + 1)
172+
setattr(resource, vote_direction_attribute, initial_count + 1)
164173
setattr(vote_info, 'current_direction', vote_direction)
165174
db.session.commit()
166175

167176
return utils.standardize_response(
168-
payload=dict(data=resource.serialize),
169-
datatype="resource")
177+
payload=dict(data=resource.serialize(api_key)),
178+
datatype="resource"
179+
)
170180

171181

172182
def add_click(id):
173183
resource = Resource.query.get(id)
184+
api_key = g.auth_key.apikey if g.auth_key else None
174185

175186
if not resource:
176187
return redirect('/404')
@@ -180,5 +191,5 @@ def add_click(id):
180191
db.session.commit()
181192

182193
return utils.standardize_response(
183-
payload=dict(data=resource.serialize),
194+
payload=dict(data=resource.serialize(api_key)),
184195
datatype="resource")

app/api/routes/resource_retrieval.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from datetime import datetime
22

33
from dateutil import parser
4-
from flask import redirect, request
4+
from flask import redirect, request, g
55
from sqlalchemy import func, or_, text
66

77
from app import utils as utils
88
from app.api import bp
9+
from app.api.auth import authenticate
910
from app.api.routes.helpers import failures_counter, latency_summary, logger
1011
from app.models import Category, Language, Resource
1112
from configs import Config
@@ -25,6 +26,7 @@ def resource(id):
2526
return get_resource(id)
2627

2728

29+
@authenticate(allow_no_auth_key=True)
2830
def get_resources():
2931
"""
3032
Gets a paginated list of resources.
@@ -41,6 +43,7 @@ def get_resources():
4143
category = request.args.get('category')
4244
updated_after = request.args.get('updated_after')
4345
free = request.args.get('free')
46+
api_key = g.auth_key.apikey if g.auth_key else None
4447

4548
q = Resource.query
4649

@@ -102,25 +105,28 @@ def get_resources():
102105
if not paginated_resources:
103106
return redirect('/404')
104107
resource_list = [
105-
resource.serialize for resource in paginated_resources.items
108+
item.serialize(api_key)
109+
for item in paginated_resources.items
106110
]
107111
details = resource_paginator.details(paginated_resources)
108112
except Exception as e:
109113
logger.exception(e)
110114
return utils.standardize_response(status_code=500)
111115

112-
return utils.standardize_response(payload=dict(
113-
data=resource_list,
114-
**details),
115-
datatype="resources")
116+
return utils.standardize_response(
117+
payload=dict(data=resource_list, **details),
118+
datatype="resources"
119+
)
116120

117121

122+
@authenticate(allow_no_auth_key=True)
118123
def get_resource(id):
119124
resource = Resource.query.get(id)
125+
api_key = g.auth_key.apikey if g.auth_key else None
120126

121127
if resource:
122128
return utils.standardize_response(
123-
payload=dict(data=(resource.serialize)),
129+
payload=dict(data=(resource.serialize(api_key))),
124130
datatype="resource")
125131

126132
return redirect('/404')

app/models.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ class Resource(TimestampMixin, db.Model):
3636
times_clicked = db.Column(db.INTEGER, default=0)
3737
voters = db.relationship('VoteInformation', back_populates='resource')
3838

39-
@property
40-
def serialize(self):
39+
def serialize(self, apikey=None):
4140
"""Return object data in easily serializeable format"""
4241
if self.created_at:
4342
created = self.created_at.strftime("%Y-%m-%d %H:%M:%S")
@@ -60,12 +59,16 @@ def serialize(self):
6059
'downvotes': self.downvotes,
6160
'times_clicked': self.times_clicked,
6261
'created_at': created,
63-
'last_updated': updated
62+
'last_updated': updated,
63+
'user_vote_direction': next(
64+
(voter.current_direction for voter in self.voters
65+
if voter.voter_apikey == apikey), None
66+
) if apikey else None
6467
}
6568

6669
@property
6770
def serialize_algolia_search(self):
68-
result = self.serialize
71+
result = self.serialize()
6972
result['objectID'] = self.id
7073
return result
7174

@@ -200,6 +203,6 @@ def __repr__(self):
200203
class VoteInformation(db.Model):
201204
voter_apikey = db.Column(db.String, db.ForeignKey('key.apikey'), primary_key=True)
202205
resource_id = db.Column(db.Integer, db.ForeignKey('resource.id'), primary_key=True)
203-
current_direction = db.Column(db.String, nullable=False)
206+
current_direction = db.Column(db.String, nullable=True)
204207
resource = db.relationship('Resource', back_populates='voters')
205208
voter = db.relationship('Key', back_populates='voted_resources')

app/static/openapi.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ paths:
398398
times_clicked: 0
399399
upvotes: 0
400400
url: 'http://www.freetechbooks.com/'
401+
user_vote_direction: null
401402
status: 'ok'
402403
status_code: 200
403404
404:
@@ -447,6 +448,7 @@ paths:
447448
times_clicked: 3
448449
upvotes: 1
449450
url: 'http://www.test.com'
451+
user_vote_direction: null
450452
status: 'ok'
451453
status_code: 200
452454
404:
@@ -554,6 +556,7 @@ paths:
554556
times_clicked: 0
555557
upvotes: 1
556558
url: 'http://teachyourkidstocode.com/'
559+
user_vote_direction: 'upvote'
557560
status: 'ok'
558561
status_code: 200
559562
404:
@@ -598,6 +601,7 @@ paths:
598601
times_clicked: 0
599602
upvotes: 0
600603
url: 'http://teachyourkidstocode.com/'
604+
user_vote_direction: 'downvote'
601605
status: 'ok'
602606
status_code: 200
603607
404:
@@ -658,6 +662,7 @@ paths:
658662
times_clicked: 3
659663
upvotes: 1
660664
url: 'http://www.test.com'
665+
user_vote_direction: null
661666
has_next: false
662667
has_prev: false
663668
number_of_pages: 1
@@ -722,6 +727,7 @@ paths:
722727
times_clicked: 3
723728
upvotes: 1
724729
url: 'https://www.w3schools.com/css/'
730+
user_vote_direction: null
725731
status: 'ok'
726732
status_code: 200
727733
400:
@@ -1092,6 +1098,10 @@ components:
10921098
url:
10931099
type: string
10941100
description: Resource url
1101+
user_vote_direction:
1102+
type: string
1103+
nullable: true
1104+
description: Vote direction of a logged in user
10951105

10961106
ResourceList:
10971107
type: array
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""include vote direction
2+
3+
Revision ID: e6ac83ef4570
4+
Revises: fc34137ad3ba
5+
Create Date: 2020-11-05 20:04:51.029258
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlalchemy_utils
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'e6ac83ef4570'
15+
down_revision = 'fc34137ad3ba'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.alter_column('vote_information', 'current_direction',
23+
existing_type=sa.VARCHAR(),
24+
nullable=True)
25+
# ### end Alembic commands ###
26+
27+
28+
def downgrade():
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
op.alter_column('vote_information', 'current_direction',
31+
existing_type=sa.VARCHAR(),
32+
nullable=False)
33+
# ### end Alembic commands ###

tests/unit/test_models.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_resource():
2121
"\tCategory: <Category Category>\n"
2222
"\tURL: https://resource.url\n>")
2323

24-
assert (resource.serialize == {
24+
assert (resource.serialize() == {
2525
'id': None,
2626
'name': 'name',
2727
'url': 'https://resource.url',
@@ -33,7 +33,8 @@ def test_resource():
3333
'downvotes': None,
3434
'times_clicked': None,
3535
'created_at': '',
36-
'last_updated': ''
36+
'last_updated': '',
37+
'user_vote_direction': None
3738
})
3839

3940
# Test equality

tests/unit/test_routes/test_resource_retreival.py

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def test_get_resources(module_client, module_db):
2020
assert (isinstance(resource.get('category'), str))
2121
assert (resource.get('category'))
2222
assert (isinstance(resource.get('languages'), list))
23+
assert ('user_vote_direction' in resource)
2324
assert (response.json['number_of_pages'] is not None)
2425

2526

@@ -62,6 +63,7 @@ def test_get_single_resource(module_client, module_db):
6263
assert (isinstance(resource.get('category'), str))
6364
assert (resource.get('category'))
6465
assert (isinstance(resource.get('languages'), list))
66+
assert ('user_vote_direction' in resource)
6567

6668
assert (resource.get('id') == 5)
6769

0 commit comments

Comments
 (0)