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-230: Migrate review related code off the ORM #91
Conversation
9f12391
to
abde3cb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite large, so I didn't have enough time to look at all of the changes yet.
critiquebrainz/db/review.py
Outdated
review["id"] = str(review["id"]) | ||
review["entity_id"] = str(review["entity_id"]) | ||
review["last_updated"] = review["last_revision"]["timestamp"] | ||
review['last_revision']['review_id'] = str(review['last_revision']['review_id']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor thing, but you should stick to one type of quotation marks within a file.
critiquebrainz/db/review.py
Outdated
{ | ||
"id": uuid, | ||
"entity_id": uuid, | ||
"entity_type": 'release_group', 'event', 'place', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be consistent with the way output values are documented. All the other ones have "name": type
structure, but this one is different. How about something like "entity_type": str {"release_group", "event", "place"}
?
critiquebrainz/db/review.py
Outdated
"last_revision: dict, | ||
"votes": dict, | ||
"rating": int, | ||
"text": str, "created": datetime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this needs to be split up into multiple lines.
critiquebrainz/db/review.py
Outdated
is_blocked, | ||
created_time.created, | ||
license.full_name, | ||
license.info_url |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to reference each table explicitly to make it easier to understand. For example, "user".display_name
instead of just display_name
.
critiquebrainz/db/review.py
Outdated
|
||
review = result.fetchone() | ||
if not review: | ||
raise db_exceptions.NoDataFoundException("No review found with review ID: {}".format(review_id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about Can't find review with ID: {id}
?
critiquebrainz/db/review.py
Outdated
WHEN vote='t' THEN 1 | ||
ELSE 0 | ||
END | ||
) AS votes_positive_count, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of problems here. The goal of splitting parts of a query into multiple rows is to make it more readable. It doesn't mean that everything needs to be like that. This query doesn't even fit on my screen.
Statement like this is still readable:
SUM(
CASE WHEN vote='t' THEN 1 ELSE 0 END
) AS votes_positive_count,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And make sure that you use 4 spaces for indentation, not 1.
critiquebrainz/db/review.py
Outdated
JOIN "user" | ||
ON review.user_id = "user".id | ||
JOIN license | ||
ON license.id = license_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with these JOINs. This is fine:
JOIN license ON license.id = license_id
critiquebrainz/db/review.py
Outdated
AND latest.latest_timestamp = revision.timestamp | ||
) AS latest_revision | ||
ON review.id = latest_revision.review_id | ||
{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Placeholder names should be explicitly specified: {where_clause}
instead of {}
, etc.
critiquebrainz/db/vote.py
Outdated
@@ -56,6 +56,7 @@ def submit(user_id, revision_id, vote): | |||
"vote": vote, | |||
"rated_at": datetime.utcnow(), | |||
}) | |||
return get(user_id, revision_id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is data that returned from submit
used somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also don't forget to document the return value.
|
||
# Statistics | ||
review_count = format_number(Review.get_count(is_draft = False)) | ||
review_count = format_number(db_review.get_count(is_draft = False)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should've been no spaces around =
.
887259c
to
9032a75
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, finally looked over most things. Looks great.
I pointed out several other problems that need to be fixed.
critiquebrainz/db/review.py
Outdated
WHERE id = :review_id | ||
"""), { | ||
"review_id": review_id, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can hide
and unhide
be combined into one function? Something like set_hidden_state(review_id, is_hidden)
.
critiquebrainz/db/review.py
Outdated
|
||
|
||
def hide(review_id): | ||
"""Hide review given the review id. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hide review with a given ID.
would be a better description.
critiquebrainz/db/review.py
Outdated
import critiquebrainz.db.revision as db_revision | ||
from critiquebrainz.db.user import User | ||
import critiquebrainz.db.users as db_users | ||
from critiquebrainz.db.user import User |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see some duplicate imports there.
critiquebrainz/db/review.py
Outdated
|
||
|
||
def to_dict(review, confidential=False): | ||
review["user"] = User(db_users.get_by_id(review.pop("user_id"))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to use a user class instead of providing a dictionary with user data? If it's not necessary, maybe it's better to be consistent with the way the rest of data is returned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also the case in other places. Can you help me understand how it's used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In review_credit
macro present in macros.html
, review.user.avatar
is used to show user avatar which is a property in the User
class. The macro is used in popular reviews
section and browse reviews
pages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Can improve that part later, if necessary.
critiquebrainz/db/review.py
Outdated
|
||
def update(*, review_id, drafted, text, license_id=None, | ||
language=None, is_draft=None): | ||
"""Update contents of this review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update contents of a review.
"edits": 0, | ||
"is_draft": is_draft, | ||
"is_hidden": False, | ||
"language": language, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be a good idea to check that provided language is one of the supported ones.
critiquebrainz/db/review.py
Outdated
|
||
def list_reviews(*, inc_drafts=False, inc_hidden=False, entity_id=None, | ||
entity_type=None, license_id=None, user_id=None, | ||
language=None, exclude=None, sort=None, limit=None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be a good idea to provide a reasonable default limit
to avoid issues in case it's not provided. 20 sounds like a good number.
critiquebrainz/db/review.py
Outdated
license_id (str): License of the returned reviews. | ||
inc_drafts (bool): True if reviews marked as drafts should be included. False if not. | ||
inc_hidden (bool): True if reviews marked as hidden should be included. False if not. | ||
exclude (list): List of id of reviews to exclude. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List of reviews (their IDs) to exclude from results.
critiquebrainz/db/review.py
Outdated
|
||
Returns: | ||
Dictionary of reviews. | ||
Total number of reviews. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Total number of reviews that match specified filters.
drafted=review["is_draft"], | ||
text=form.text.data, | ||
is_draft=(form.state.data == 'draft'), | ||
license_id=license_choice, language=form.language.data, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put language
argument on a new line?
9032a75
to
04d56a8
Compare
critiquebrainz/db/review.py
Outdated
if not entity_type or not entity_id: | ||
raise TypeError("Entity Type or ID not defined") | ||
if language not in supported_languages: | ||
raise TypeError("Language: {} is not supported".format(language)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look like a TypeError
. Take a look its description: https://docs.python.org/3.6/library/exceptions.html#TypeError. Someone might pass an invalid language as a string.
ValueError
would be the right one to use here.
@@ -365,7 +366,7 @@ def hide(id): | |||
|
|||
form = AdminActionForm() | |||
if form.validate_on_submit(): | |||
db_review.hide(review["id"]) | |||
db_review.set_hidden_state(review["id"], is_hidden="True") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The is_hidden
argument should be a bool
. Pretty sure you can pass it straight to psycopg too.
@@ -382,6 +383,6 @@ def hide(id): | |||
@admin_view | |||
def unhide(id): | |||
review = db_review.get_or_404(id) | |||
db_review.unhide(review["id"]) | |||
db_review.set_hidden_state(review["id"], is_hidden="False") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue with is_hidden
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have to accept boolean arguments as strings in an interface, it's a good sign that something is not right. :)
for lang in list(pycountry.languages): | ||
if 'iso639_1_code' in dir(lang): | ||
supported_languages.append(lang.iso639_1_code) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor thing: there should be two spaces between functions and other code.
critiquebrainz/db/review.py
Outdated
|
||
if license_id is not None: | ||
if not drafted: # If trying to convert published review into draft. | ||
raise BadRequest(lazy_gettext("Changing license of a published review is not allowed.")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One other important thing that I missed. We should try to avoid making Flask views and database access code closely tied. That means not using exceptions and functions from Flask.
Modules in the critiquebrainz.db
package should raise exceptions from critiquebrianz.db.exceptions
. I understand that this is not the old implementation, but that's the perfect time to make this improvement. This would mean we could easily use DB access code without Flask.
critiquebrainz/db/review.py
Outdated
if 'iso639_1_code' in dir(lang): | ||
supported_languages.append(lang.iso639_1_code) | ||
|
||
def get_or_404(review_id): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably shouldn't have this function here. As I described in another comment, this makes db
package closely tied to Flask itself. This function can be moved into packages with views if it's used more than once.
9bbfcea
to
7cc6b7c
Compare
Functions and tests for directly accessing review information.
Corrections in list reviews, popular reviews, submit votes and used keyword only arguments for functions, also made syntax corrections.
Refactor create_review function, corrections in update function, combine hide and unhide functions.
7cc6b7c
to
848b1a5
Compare
Looks like there's a small conflict with another change. Would you mind making an update please? |
3410d1a
to
285278a
Compare
Great. 👍 |
result = connection.execute(sqlalchemy.text(""" | ||
INSERT INTO review | ||
VALUES (:id, :entity_id, :entity_type, :user_id, :edits, :is_draft, | ||
:is_hidden, :license_id, :language, :source, :source_url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way of insertion is unsafe and caused CB-258. Order of columns needs to be explicitly specified. Here you are making an assumption that entity_type
is third, which it might be when a new instance of CB database is created, but it isn't in the production database.
""" | ||
cache_key = cache.gen_key("popular_reviews", limit) | ||
reviews = cache.get(cache_key, REVIEW_CACHE_NAMESPACE) | ||
reviews = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the point of retrieving reviews from cache and then getting rid of them in the next statement?
Next time make sure that your PR is small and easy to review. Otherwise, as evidenced here, this causes many issues. |
Functions and tests for migrating
reviews
off the ORM. Changes have been made to thefrontend
component to use the migrated code. I have added the following functions indb/review.py
and the corresponding tests (db/review_test.py
):get_by_id
for getting review by id.hide
andunhide
for hiding and unhiding reviews.get_count
for review count.update
for updating a reviews info.create
for creating a review.delete
for deleting a review.list
for list reviews and filter them on various filters.get_popular
for getting popular reviews.revision.create
for creating a revision.votes
for getting votes of a revision.