Skip to content

Commit

Permalink
Add full user reposts feed (#882)
Browse files Browse the repository at this point in the history
* Add full user reposts feed

* Clean up

* Fix ns references

* Add 'is' field to full track and playlist

* Use handle.lower() and handle_lc

* Move to item type in activity
  • Loading branch information
raymondjacobson committed Oct 5, 2020
1 parent 2896103 commit bd8d544
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 8 deletions.
15 changes: 15 additions & 0 deletions discovery-provider/src/api/v1/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ def extend_playlist(playlist):
playlist["favorite_count"] = playlist["save_count"]
return playlist

def extend_activity(item):
if item.get("track_id"):
return {
"item_type": 'track',
"timestamp": item["activity_timestamp"],
"item": extend_track(item)
}
if item.get("playlist_id"):
return {
"item_type": 'playlist',
"timestamp": item["activity_timestamp"],
"item": extend_playlist(item)
}
return None

def abort_bad_request_param(param, namespace):
namespace.abort(400, "Oh no! Bad request parameter {}.".format(param))

Expand Down
49 changes: 49 additions & 0 deletions discovery-provider/src/api/v1/models/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from flask_restx import fields
from flask_restx.fields import MarshallingError
from flask_restx.marshalling import marshal
from .common import ns
from .tracks import track, track_full
from .playlists import playlist_model, full_playlist_model

class ItemType(fields.Raw):
def format(self, value):
if value == "track":
return "track"
if value == "playlist":
return "playlist"
raise MarshallingError("Unable to marshal as activity type")


class ActivityItem(fields.Raw):
def format(self, value):
try:
if value.get("track_id"):
return marshal(value, track)
if value.get("playlist_id"):
return marshal(value, playlist_model)
except:
raise MarshallingError("Unable to marshal as activity item")


class FullActivityItem(fields.Raw):
def format(self, value):
try:
if value.get("track_id"):
return marshal(value, track_full)
if value.get("playlist_id"):
return marshal(value, full_playlist_model)
except:
raise MarshallingError("Unable to marshal as activity item")


activity_model = ns.model("activity", {
"timestamp": fields.String(allow_null=True),
"item_type": ItemType,
"item": ActivityItem
})

activity_model_full = ns.model("activity", {
"timestamp": fields.String(allow_null=True),
"item_type": ItemType,
"item": FullActivityItem
})
3 changes: 2 additions & 1 deletion discovery-provider/src/api/v1/models/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from flask_restx import Namespace, fields
from flask_restx.fields import MarshallingError

# Make a common namespace for all the models
ns = Namespace("Models")
Expand Down Expand Up @@ -26,6 +27,6 @@
"owner_wallet": fields.Integer(required=True),
"signature": fields.String(required=True),
"success": fields.Boolean(required=True),
"timestamp": fields.String(required=True) ,
"timestamp": fields.String(required=True),
"version": fields.Nested(version_metadata, required=True),
})
146 changes: 141 additions & 5 deletions discovery-provider/src/api/v1/users.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from src.queries.get_repost_feed_for_user import get_repost_feed_for_user
from flask_restx import Resource, Namespace, fields, reqparse
from src.api.v1.models.common import favorite
from src.api.v1.models.users import user_model, user_model_full
Expand All @@ -10,10 +11,11 @@
from src.queries.get_followees_for_user import get_followees_for_user
from src.queries.get_followers_for_user import get_followers_for_user

from src.api.v1.helpers import abort_not_found, decode_with_abort, extend_favorite, extend_track, \
from src.api.v1.helpers import abort_not_found, decode_with_abort, extend_activity, extend_favorite, extend_track, \
extend_user, format_limit, format_offset, make_response, search_parser, success_response, abort_bad_request_param, \
get_default_max, decode_string_id
from .models.tracks import track, track_full
from .models.activities import activity_model, activity_model_full
from src.utils.redis_cache import cache
from src.utils.redis_metrics import record_metrics

Expand Down Expand Up @@ -156,7 +158,7 @@ def get(self, user_id):
@full_ns.route(USER_TRACKS_ROUTE)
class FullTrackList(Resource):
@record_metrics
@ns.doc(
@full_ns.doc(
id="""Get User's Tracks""",
params={
'user_id': 'A User ID',
Expand All @@ -170,7 +172,7 @@ class FullTrackList(Resource):
500: 'Server error'
}
)
@ns.marshal_with(full_tracks_response)
@full_ns.marshal_with(full_tracks_response)
@cache(ttl_sec=5)
def get(self, user_id):
"""Fetch a list of tracks for a user."""
Expand Down Expand Up @@ -202,7 +204,7 @@ def get(self, user_id):
@full_ns.route("/handle/<string:handle>/tracks")
class HandleFullTrackList(Resource):
@record_metrics
@ns.doc(
@full_ns.doc(
id="""Get User's Tracks""",
params={
'user_id': 'A User ID',
Expand All @@ -216,7 +218,7 @@ class HandleFullTrackList(Resource):
500: 'Server error'
}
)
@ns.marshal_with(full_tracks_response)
@full_ns.marshal_with(full_tracks_response)
@cache(ttl_sec=5)
def get(self, handle):
"""Fetch a list of tracks for a user."""
Expand All @@ -243,6 +245,140 @@ def get(self, handle):
tracks = list(map(extend_track, tracks))
return success_response(tracks)

USER_REPOSTS_ROUTE = "/<string:user_id>/reposts"

user_reposts_route_parser = reqparse.RequestParser()
user_reposts_route_parser.add_argument('user_id', required=False)
user_reposts_route_parser.add_argument('limit', required=False, type=int)
user_reposts_route_parser.add_argument('offset', required=False, type=int)

reposts_response = make_response("reposts", full_ns, fields.List(fields.Nested(activity_model)))
@ns.route(USER_REPOSTS_ROUTE)
class RepostList(Resource):
@record_metrics
@ns.doc(
id="""Get User's Reposts""",
params={
'user_id': 'A User ID',
'limit': 'Limit',
'offset': 'Offset',
},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(reposts_response)
@cache(ttl_sec=5)
def get(self, user_id):
decoded_id = decode_with_abort(user_id, ns)
args = user_reposts_route_parser.parse_args()

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

offset = format_offset(args)
limit = format_limit(args)

args = {
"user_id": decoded_id,
"current_user_id": current_user_id,
"with_users": True,
"filter_deleted": True,
"limit": limit,
"offset": offset
}
reposts = get_repost_feed_for_user(decoded_id, args)
activities = list(map(extend_activity, reposts))

return success_response(activities)

full_reposts_response = make_response("full_reposts", full_ns, fields.List(fields.Nested(activity_model_full)))
@full_ns.route(USER_REPOSTS_ROUTE)
class FullRepostList(Resource):
@record_metrics
@full_ns.doc(
id="""Get User's Reposts""",
params={
'user_id': 'A User ID',
'limit': 'Limit',
'offset': 'Offset',
},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@full_ns.marshal_with(full_reposts_response)
@cache(ttl_sec=5)
def get(self, user_id):
decoded_id = decode_with_abort(user_id, ns)
args = user_reposts_route_parser.parse_args()

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

offset = format_offset(args)
limit = format_limit(args)

args = {
"current_user_id": current_user_id,
"with_users": True,
"filter_deleted": True,
"limit": limit,
"offset": offset
}
reposts = get_repost_feed_for_user(decoded_id, args)
activities = list(map(extend_activity, reposts))

return success_response(activities)


@full_ns.route("/handle/<string:handle>/reposts")
class HandleFullRepostList(Resource):
@record_metrics
@full_ns.doc(
id="""Get User's Reposts""",
params={
'user_id': 'A User ID',
'limit': 'Limit',
'offset': 'Offset',
},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@full_ns.marshal_with(full_reposts_response)
@cache(ttl_sec=5)
def get(self, handle):
args = user_reposts_route_parser.parse_args()

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

offset = format_offset(args)
limit = format_limit(args)

args = {
"handle": handle,
"current_user_id": current_user_id,
"with_users": True,
"filter_deleted": True,
"limit": limit,
"offset": offset
}
reposts = get_repost_feed_for_user(None, args)
activities = list(map(extend_activity, reposts))

return success_response(activities)


favorites_response = make_response("favorites_response", ns, fields.List(fields.Nested(favorite)))
@ns.route("/<string:user_id>/favorites")
Expand Down
8 changes: 7 additions & 1 deletion discovery-provider/src/queries/get_repost_feed_for_user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sqlalchemy import desc, or_

from src.models import Track, Repost, RepostType, Follow, Playlist, Save, SaveType
from src.models import Track, Repost, RepostType, Follow, Playlist, Save, SaveType, User
from src.utils import helpers
from src.utils.db_session import get_db_read_replica
from src.queries import response_name_constants
Expand All @@ -12,6 +12,10 @@ def get_repost_feed_for_user(user_id, args):
feed_results = {}
db = get_db_read_replica()
with db.scoped_session() as session:
if "handle" in args:
handle = args.get("handle")
user_id = session.query(User.user_id).filter(User.handle_lc == handle.lower()).first()

# query all reposts by user
repost_query = (
session.query(Repost)
Expand Down Expand Up @@ -233,6 +237,7 @@ def get_repost_feed_for_user(user_id, args):
track["track_id"], False)
track[response_name_constants.followee_reposts] = followees_track_repost_dict.get(
track["track_id"], [])
track[response_name_constants.followee_saves] = []
track[response_name_constants.activity_timestamp] = track_repost_dict[track["track_id"]]["created_at"]

for playlist in playlists:
Expand All @@ -248,6 +253,7 @@ def get_repost_feed_for_user(user_id, args):
user_saved_playlist_dict.get(playlist["playlist_id"], False)
playlist[response_name_constants.followee_reposts] = \
followees_playlist_repost_dict.get(playlist["playlist_id"], [])
playlist[response_name_constants.followee_saves] = []
playlist[response_name_constants.activity_timestamp] = \
playlist_repost_dict[playlist["playlist_id"]]["created_at"]

Expand Down
2 changes: 1 addition & 1 deletion discovery-provider/src/queries/get_tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_tracks(args):
with db.scoped_session() as session:
if "handle" in args:
handle = args.get("handle")
user_id = session.query(User.user_id).filter(User.handle == handle).first()
user_id = session.query(User.user_id).filter(User.handle_lc == handle.lower()).first()
args["user_id"] = user_id

# Create initial query
Expand Down
1 change: 1 addition & 0 deletions discovery-provider/src/queries/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def get_repost_feed_for_user_route(user_id):
args = to_dict(request.args)
if "with_users" in request.args:
args["with_users"] = parse_bool_param(request.args.get("with_users"))
args["current_user_id"] = get_current_user_id(required=False)
feed_results = get_repost_feed_for_user(user_id, args)
return api_helpers.success_response(feed_results)

Expand Down

0 comments on commit bd8d544

Please sign in to comment.