Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB-427: Support entity endpoint in the API including average rating #401

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
54 changes: 54 additions & 0 deletions critiquebrainz/db/rating_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import sqlalchemy

from critiquebrainz import db

def get_stats(entity_id, entity_type):
"""Gets the average rating and the rating statistics of the entity

It is done by selecting ratings from the latest revisions of all reviews
for a given entity.

Args:
entity_id (uuid): ID of the entity
entity_type (str): Type of the entity
"""
with db.engine.connect() as connection:
result = connection.execute(sqlalchemy.text("""
WITH LatestRevisions AS (
SELECT review_id,
MAX("timestamp") created_at
FROM revision
WHERE review_id in (
SELECT id
FROM review
WHERE entity_id = :entity_id
AND entity_type = :entity_type
AND is_hidden = 'f')
GROUP BY review_id
)
SELECT rating
FROM revision
INNER JOIN LatestRevisions
ON revision.review_id = LatestRevisions.review_id
AND revision.timestamp = LatestRevisions.created_at
"""), {
"entity_id": entity_id,
"entity_type": entity_type,
})
row = result.fetchall()

ratings_stats = {1: 0, 2: 0, 3: 0, 4:0, 5:0}
if row == []:
return ratings_stats, 0

ratings = [r[0]/20 for r in row if r[0] is not None]

for rating in ratings:
ratings_stats[rating] += 1

if ratings:
average_rating = sum(ratings)/len(ratings)
else:
average_rating = 0

return ratings_stats, average_rating
24 changes: 20 additions & 4 deletions critiquebrainz/db/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,14 +300,16 @@ def update(review_id, *, drafted, text=None, rating=None, license_id=None, langu

with db.engine.begin() as connection:
result = connection.execute(sqlalchemy.text("""
SELECT review.entity_id
SELECT review.entity_id,
review.entity_type,
review.user_id
FROM review
WHERE review.id = :review_id
"""), {
"review_id": review_id,
})
review = dict(result.mappings().first())
invalidate_ws_entity_cache(review["entity_id"])
invalidate_ws_entity_cache(review["entity_id"], review["entity_type"], review["user_id"])


def create(*, entity_id, entity_type, user_id, is_draft, text=None, rating=None,
Expand Down Expand Up @@ -389,11 +391,11 @@ def create(*, entity_id, entity_type, user_id, is_draft, text=None, rating=None,
if rating:
db_revision.update_rating(review_id)

invalidate_ws_entity_cache(entity_id)
invalidate_ws_entity_cache(entity_id, entity_type, user_id)
return get_by_id(review_id)


def invalidate_ws_entity_cache(entity_id):
def invalidate_ws_entity_cache(entity_id, entity_type, user_id):
cache_keys_for_entity_id_key = cache.gen_key('ws_cache', entity_id)
cache_keys_to_delete = cache.smembers(cache_keys_for_entity_id_key, namespace=REVIEW_CACHE_NAMESPACE)
if cache_keys_to_delete:
Expand All @@ -406,6 +408,20 @@ def invalidate_ws_entity_cache(entity_id):
cache.delete_many(cache_keys_to_delete, namespace=REVIEW_CACHE_NAMESPACE)
cache.delete(cache_keys_for_no_entity_id_key, namespace=REVIEW_CACHE_NAMESPACE)

# Invalidate top and latest reviews caches
cache_keys_to_delete = [
cache.gen_key(f'entity_api_{entity_type}', entity_id, review_type, f"{sort_type}_reviews")
for sort_type in ('popularity', 'published_on')
for review_type in ('review', 'rating', None)
]
cache.delete_many(cache_keys_to_delete, namespace=REVIEW_CACHE_NAMESPACE)

user = db_users.get_by_id(user_id)
if user and 'musicbrainz_username' in user.keys() and user['musicbrainz_username']:
username = user["musicbrainz_username"]
cache_key = cache.gen_key('entity_api', entity_id, entity_type, username, "user_review")
cache.delete(cache_key, namespace=REVIEW_CACHE_NAMESPACE)


# pylint: disable=too-many-branches
def get_reviews_list(connection, *, inc_drafts=False, inc_hidden=False, entity_id=None,
Expand Down
11 changes: 11 additions & 0 deletions critiquebrainz/ws/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

deploy_env = os.environ.get('DEPLOY_ENV', '')
CONSUL_CONFIG_FILE_RETRY_COUNT = 10
REVIEWS_LIMIT = 5
REVIEW_CACHE_NAMESPACE = "Review"
REVIEW_CACHE_TIMEOUT = 30 * 60 # 30 minutes


def create_app(debug=None, config_path=None):
Expand Down Expand Up @@ -95,6 +98,7 @@ def create_app(debug=None, config_path=None):

app.config["JSONIFY_PRETTYPRINT_REGULAR"] = False

_register_converters(app)
_register_blueprints(app)

return app
Expand All @@ -111,7 +115,14 @@ def _register_blueprints(app):
from critiquebrainz.ws.review.views import review_bp
from critiquebrainz.ws.user.views import user_bp
from critiquebrainz.ws.review.bulk import bulk_review_bp
from critiquebrainz.ws.entity.views import entity_bp
app.register_blueprint(oauth_bp, url_prefix="/oauth")
app.register_blueprint(review_bp, url_prefix="/review")
app.register_blueprint(user_bp, url_prefix="/user")
app.register_blueprint(bulk_review_bp, url_prefix="/reviews")
app.register_blueprint(entity_bp)


def _register_converters(app):
from critiquebrainz.ws.entity.views import EntityNameConverter
app.url_map.converters['entity_name'] = EntityNameConverter
2 changes: 2 additions & 0 deletions critiquebrainz/ws/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
'user',
'review',
'vote',
'artist',
'release_group',
)
Empty file.
Empty file.
89 changes: 89 additions & 0 deletions critiquebrainz/ws/entity/test/views_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from brainzutils import cache

import critiquebrainz.db.license as db_license
import critiquebrainz.db.review as db_review
import critiquebrainz.db.users as db_users
from critiquebrainz.db.user import User
from critiquebrainz.ws.testing import WebServiceTestCase


class EntityViewsTestCase(WebServiceTestCase):

def setUp(self):
super(EntityViewsTestCase, self).setUp()

self.artist_id1 = "f59c5520-5f46-4d2c-b2c4-822eabf53419"
self.artist_id2 = "83d91898-7763-47d7-b03b-b92132375c47"

self.user = User(db_users.get_or_create(1, "Tester", new_user_data={
"display_name": "test user",
}))
self.another_user = User(db_users.get_or_create(2, "Hacker!", new_user_data={
"display_name": "test hacker",
}))
self.license = db_license.create(
id="CC BY-SA 3.0",
full_name="Created so we can fill the form correctly.",
)
self.review = dict(
entity_id=self.artist_id1,
entity_type='artist',
user_id=self.user.id,
text="Testing! This text should be on the page.",
rating=5,
is_draft=False,
license_id=self.license["id"],
)
self.review2 = dict(
entity_id=self.artist_id2,
entity_type='artist',
user_id=self.user.id,
text="Testing! This text should be on the page.",
rating=5,
is_draft=False,
license_id=self.license["id"],
)

def create_dummy_review(self):
return db_review.create(**self.review)

def create_dummy_review2(self):
return db_review.create(**self.review2)

def test_artist_endpoint(self):
review = self.create_dummy_review()
response = self.client.get('/artist/f59c5520-5f46-4d2c-b2c4-822eabf53419')

self.assert200(response)
self.assertIn(review['text'], response.json['top_reviews'][0]['text'])

self.assertEqual(5, response.json['average_rating'])
self.assertEqual(1, response.json['reviews_count'])

# Test for an artist which does not exist
response = self.client.get('/artist/f59c5520-5f46-4d2c-b2c4-822eabf53417')
self.assert404(response)

def test_artist_user_reviews(self):
review = self.create_dummy_review()
response = self.client.get('/artist/f59c5520-5f46-4d2c-b2c4-822eabf53419?username=%s' % self.user.musicbrainz_username)

self.assert200(response)
self.assertIn(review['text'], response.json['user_review']['text'])

def test_user_cache_tracking(self):
track_key = cache.gen_key("entity_api", self.artist_id2, "artist",self.user.musicbrainz_username, "user_review")

# Make sure the cache is empty
self.client.get('/artist/83d91898-7763-47d7-b03b-b92132375c47?username=%s' % self.user.musicbrainz_username)
cache_value = cache.get(track_key, namespace="Review")
self.assertEqual([], cache_value)

review = self.create_dummy_review2()

# Check if the cache is populated after the request
self.client.get('/artist/83d91898-7763-47d7-b03b-b92132375c47?username=%s' % self.user.musicbrainz_username)
cache_value = cache.get(track_key, namespace="Review")
self.assertTrue(cache_value is not None)

self.assertIn(review['text'], cache_value['text'])
Loading
Loading