Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch rec-component-feedback to master (#1197)
* schema for storing recommendation feedback (#1126) * schema for recommendation feedback * rename enum strings and enum name * Separate component for Recommendations (#1133) * recommendation componeent and recommendation card * CSS for recommendation component * shift recommendationPaginationControl to main method don't modify bootstrap classes in recommendation-page.less * fix utils.tsx error * html emoticons for rec feddback * update func name * revamp listens and recommendations type * tests for recommendation component * tests for rec component * Refactor RecentListens - Remove recommendations code (#1146) * refactor recentListens * fix indentation * remove unused const * Recommendation feedback [DB + API] (#1143) * recommendation feedback model * use enum vals as feeeback to store in db * db and api script for recommendation feedback * PEP-8 fixes * tests feedback (db) * tests feedback (API) * tests update * don't create two objects * Sync recommendation feedback with emoticons (#1149) * tests feedback (db) * tests update * sync feedback * add missing props to test file * simplify get feedback for multiple recording query * keep recommendationFeedback type same as recommendationFeedback enum don't mutate the previous state * add event.stopPagination to stop closing of dropdown when submitting feedback * use Icon + text for feedback * use regular font awsome icons in place of stroke * if feedback given render corresponding feedback solid * remove redundant log statements * don't use prevState value in currRecPage * remove componentDidUpdate * adjust alignement of feedback for mobile view * capitalize feedback text * don't show feedbacks options for user != currentUser * define mediaquery after regular padding * tests for feedback * don't overwrite afterRecommendationDisplay in tests * add comment to instruct eslint to ignore import issue * tests for db * check for html elements before and after updating feedback * test interdependency of button and dropdown * format tests - nitpicks * remove bad_recommendation feddback type * adjust spacing between emoticons
- Loading branch information
Showing
39 changed files
with
4,983 additions
and
438 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
admin/sql/updates/2020-10-03-add-recommendation-feedback-table.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
BEGIN; | ||
|
||
-- Create new table | ||
CREATE TABLE recommendation_feedback ( | ||
id SERIAL, -- PK | ||
user_id INTEGER NOT NULL, -- FK to "user".id | ||
recording_mbid UUID NOT NULL, | ||
rating recommendation_feedback_type_enum NOT NULL, | ||
created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() | ||
); | ||
|
||
-- Create primary key | ||
ALTER TABLE recommendation_feedback ADD CONSTRAINT recommendation_feedback_pkey PRIMARY KEY (id); | ||
|
||
-- Create foreign key | ||
ALTER TABLE recommendation_feedback | ||
ADD CONSTRAINT recommendation_feedback_user_id_foreign_key | ||
FOREIGN KEY (user_id) | ||
REFERENCES "user" (id) | ||
ON DELETE CASCADE; | ||
|
||
-- Create unique index | ||
CREATE UNIQUE INDEX user_id_rec_mbid_ndx_feedback ON recommendation_feedback (user_id, recording_mbid); | ||
|
||
CREATE INDEX rating_recommendation_feedback ON recommendation_feedback (rating); | ||
|
||
COMMIT; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import uuid | ||
|
||
from datetime import datetime | ||
from pydantic import BaseModel, ValidationError, validator | ||
|
||
|
||
def get_allowed_ratings(): | ||
""" Get rating values that can be submitted corresponding to a recommendation. | ||
""" | ||
return ['like', 'love', 'dislike', 'hate', 'bad_recommendation'] | ||
|
||
|
||
def check_recording_mbid_is_valid_uuid(rec_mbid): | ||
try: | ||
rec_mbid = uuid.UUID(rec_mbid) | ||
return str(rec_mbid) | ||
except (AttributeError, ValueError): | ||
raise ValueError('Recording MBID must be a valid UUID.') | ||
|
||
|
||
class RecommendationFeedbackSubmit(BaseModel): | ||
""" Represents a recommendation feedback submit object. | ||
Args: | ||
user_id: the row id of the user in the DB | ||
recording_mbid: the MusicBrainz ID of the recording | ||
rating: the feedback associated with the recommendation. | ||
Refer to "recommendation_feedback_type_enum" in admin/sql/create_types.py | ||
for allowed rating values. | ||
created: (Optional)the timestamp when the feedback record was inserted into DB | ||
""" | ||
|
||
user_id: int | ||
recording_mbid: str | ||
rating: str | ||
created: datetime = None | ||
|
||
@validator('rating') | ||
def check_feedback_is_valid(cls, rating): | ||
expected_rating = get_allowed_ratings() | ||
if rating not in expected_rating: | ||
raise ValueError('Feedback can only have a value in {}'.format(expected_rating)) | ||
return rating | ||
|
||
_is_recording_mbid_valid: classmethod = validator("recording_mbid", allow_reuse=True)(check_recording_mbid_is_valid_uuid) | ||
|
||
|
||
class RecommendationFeedbackDelete(BaseModel): | ||
""" Represents a recommendation feedback delete object. | ||
Args: | ||
user_id: the row id of the user in the DB | ||
recording_mbid: the MusicBrainz ID of the recommendation | ||
""" | ||
|
||
user_id: int | ||
recording_mbid: str | ||
|
||
_is_recording_mbid_valid: classmethod = validator("recording_mbid", allow_reuse=True)(check_recording_mbid_is_valid_uuid) |
138 changes: 138 additions & 0 deletions
138
listenbrainz/db/recommendations_cf_recording_feedback.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import sqlalchemy | ||
|
||
from listenbrainz import db | ||
from listenbrainz.db.model.recommendation_feedback import (RecommendationFeedbackSubmit, | ||
RecommendationFeedbackDelete) | ||
from typing import List | ||
from flask import current_app | ||
|
||
|
||
def insert(feedback_submit: RecommendationFeedbackSubmit): | ||
""" Inserts a feedback record for a user's rated recommendation into the database. | ||
If the record is already present for the user, the rating is updated to the new | ||
value passed. | ||
Args: | ||
feedback_submit: An object of class RecommendationFeedbackSubmit | ||
""" | ||
|
||
with db.engine.connect() as connection: | ||
connection.execute(sqlalchemy.text(""" | ||
INSERT INTO recommendation_feedback (user_id, recording_mbid, rating) | ||
VALUES (:user_id, :recording_mbid, :rating) | ||
ON CONFLICT (user_id, recording_mbid) | ||
DO UPDATE SET rating = :rating, | ||
created = NOW() | ||
"""), { | ||
'user_id': feedback_submit.user_id, | ||
'recording_mbid': feedback_submit.recording_mbid, | ||
'rating': feedback_submit.rating, | ||
} | ||
) | ||
|
||
|
||
def delete(feedback_delete: RecommendationFeedbackDelete): | ||
""" Deletes the feedback record for a given recommendation for the user from the database | ||
Args: | ||
feedback_delete: An object of class RecommendationFeedbackDelete | ||
""" | ||
|
||
with db.engine.connect() as connection: | ||
connection.execute(sqlalchemy.text(""" | ||
DELETE FROM recommendation_feedback | ||
WHERE user_id = :user_id | ||
AND recording_mbid = :recording_mbid | ||
"""), { | ||
'user_id': feedback_delete.user_id, | ||
'recording_mbid': feedback_delete.recording_mbid, | ||
} | ||
) | ||
|
||
|
||
def get_feedback_for_user(user_id: int, limit: int, offset: int, rating: str = None) -> List[RecommendationFeedbackSubmit]: | ||
""" Get a list of recommendation feedback given by the user in descending order of their creation. | ||
Feedback will be filtered based on limit, offset and rating, if passed. | ||
Args: | ||
user_id: the row ID of the user in the DB | ||
rating: the rating value by which the results are to be filtered. | ||
limit: number of rows to be returned | ||
offset: number of feedback to skip from the beginning | ||
Returns: | ||
A list of Feedback objects | ||
""" | ||
|
||
args = {"user_id": user_id, "limit": limit, "offset": offset} | ||
query = """ SELECT user_id, | ||
recording_mbid::text, | ||
rating, | ||
created | ||
FROM recommendation_feedback | ||
WHERE user_id = :user_id """ | ||
|
||
if rating: | ||
query += " AND rating = :rating" | ||
args["rating"] = rating | ||
|
||
query += """ ORDER BY created DESC | ||
LIMIT :limit | ||
OFFSET :offset """ | ||
|
||
with db.engine.connect() as connection: | ||
result = connection.execute(sqlalchemy.text(query), args) | ||
return [RecommendationFeedbackSubmit(**dict(row)) for row in result.fetchall()] | ||
|
||
|
||
def get_feedback_count_for_user(user_id: int) -> int: | ||
""" Get total number of recommendation feedback given by the user | ||
Args: | ||
user_id: the row ID of the user in the DB | ||
Returns: | ||
The total number of recommendation feedback given by the user | ||
""" | ||
with db.engine.connect() as connection: | ||
result = connection.execute(sqlalchemy.text(""" | ||
SELECT count(*) AS count | ||
FROM recommendation_feedback | ||
WHERE user_id = :user_id | ||
"""), { | ||
'user_id': user_id, | ||
} | ||
) | ||
count = int(result.fetchone()["count"]) | ||
|
||
return count | ||
|
||
|
||
def get_feedback_for_multiple_recordings_for_user(user_id: int, recording_list: List[str]): | ||
""" Get a list of recording feedback given by the user for given recordings | ||
Args: | ||
user_id: the row ID of the user in the DB | ||
recording_list: list of recording_mbid for which feedback records are to be obtained | ||
- if record is present then return it | ||
- if record is not present then return rating = None | ||
Returns: | ||
A list of Feedback objects | ||
""" | ||
|
||
args = {"user_id": user_id, "recording_list": tuple(recording_list)} | ||
query = """ SELECT user_id, | ||
recording_mbid::text, | ||
rating, | ||
created | ||
FROM recommendation_feedback | ||
WHERE user_id = :user_id | ||
AND recording_mbid | ||
IN :recording_list | ||
ORDER BY created DESC | ||
""" | ||
|
||
with db.engine.connect() as connection: | ||
result = connection.execute(sqlalchemy.text(query), args) | ||
return [RecommendationFeedbackSubmit(**dict(row)) for row in result.fetchall()] |
Oops, something went wrong.