Skip to content

Commit

Permalink
Merge branch rec-component-feedback to master (#1197)
Browse files Browse the repository at this point in the history
* 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
vansika committed Jan 20, 2021
1 parent ed250a3 commit 0ceadc4
Show file tree
Hide file tree
Showing 39 changed files with 4,983 additions and 438 deletions.
6 changes: 6 additions & 0 deletions admin/sql/create_foreign_keys.sql
Expand Up @@ -78,4 +78,10 @@ ALTER TABLE user_relationship
REFERENCES "user" (id)
ON DELETE CASCADE;

ALTER TABLE recommendation_feedback
ADD CONSTRAINT recommendation_feedback_user_id_foreign_key
FOREIGN KEY (user_id)
REFERENCES "user" (id)
ON DELETE CASCADE;

COMMIT;
4 changes: 4 additions & 0 deletions admin/sql/create_indexes.sql
Expand Up @@ -25,4 +25,8 @@ CREATE UNIQUE INDEX user_id_rec_msid_ndx_feedback ON recording_feedback (user_id
CREATE INDEX user_0_user_relationship_ndx ON user_relationship (user_0);
CREATE INDEX user_1_user_relationship_ndx ON user_relationship (user_1);

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;
3 changes: 3 additions & 0 deletions admin/sql/create_primary_keys.sql
Expand Up @@ -19,4 +19,7 @@ ALTER TABLE recording_feedback ADD CONSTRAINT recording_feedback_pkey PRIMARY KE
ALTER TABLE missing_musicbrainz_data ADD CONSTRAINT missing_mb_data_pkey PRIMARY KEY (id);

ALTER TABLE user_relationship ADD CONSTRAINT user_relationship_pkey PRIMARY KEY (user_0, user_1, relationship_type);

ALTER TABLE recommendation_feedback ADD CONSTRAINT recommendation_feedback_pkey PRIMARY KEY (id);

COMMIT;
8 changes: 8 additions & 0 deletions admin/sql/create_tables.sql
Expand Up @@ -158,6 +158,14 @@ CREATE TABLE statistics.sitewide (

ALTER TABLE statistics.sitewide ADD CONSTRAINT stats_range_uniq UNIQUE (stats_range);

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 TABLE recording_feedback (
id SERIAL, -- PK
user_id INTEGER NOT NULL, -- FK to "user".id
Expand Down
2 changes: 2 additions & 0 deletions admin/sql/create_types.sql
Expand Up @@ -3,3 +3,5 @@ CREATE TYPE cf_recording_type AS ENUM('top_artist', 'similar_artist');
CREATE TYPE mb_missing_data_source_enum AS ENUM('cf', 'artist_map');

CREATE TYPE user_relationship_enum AS ENUM('follow');

CREATE TYPE recommendation_feedback_type_enum AS ENUM('like', 'love', 'dislike', 'hate', 'bad_recommendation');
1 change: 1 addition & 0 deletions admin/sql/drop_tables.sql
Expand Up @@ -7,5 +7,6 @@ DROP TABLE IF EXISTS follow_list CASCADE;
DROP TABLE IF EXISTS recording_feedback CASCADE;
DROP TABLE IF EXISTS missing_musicbrainz_data CASCADE;
DROP TABLE IF EXISTS user_relationship CASCADE;
DROP TABLE IF EXISTS recommendation_feedback CASCADE;

COMMIT;
27 changes: 27 additions & 0 deletions admin/sql/updates/2020-10-03-add-recommendation-feedback-table.sql
@@ -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;
57 changes: 57 additions & 0 deletions listenbrainz/db/model/recommendation_feedback.py
@@ -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 listenbrainz/db/recommendations_cf_recording_feedback.py
@@ -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()]

0 comments on commit 0ceadc4

Please sign in to comment.