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-230: Migrate review related code off the ORM #91

Merged
merged 8 commits into from Apr 17, 2017

Conversation

ferbncode
Copy link
Member

@ferbncode ferbncode commented Mar 19, 2017

Functions and tests for migrating reviews off the ORM. Changes have been made to the frontend component to use the migrated code. I have added the following functions in db/review.py and the corresponding tests (db/review_test.py):

  • get_by_id for getting review by id.
  • hide and unhide 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.

Copy link
Contributor

@gentlecat gentlecat left a 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.

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'])
Copy link
Contributor

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.

{
"id": uuid,
"entity_id": uuid,
"entity_type": 'release_group', 'event', 'place',
Copy link
Contributor

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"}?

"last_revision: dict,
"votes": dict,
"rating": int,
"text": str, "created": datetime,
Copy link
Contributor

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.

is_blocked,
created_time.created,
license.full_name,
license.info_url
Copy link
Contributor

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.


review = result.fetchone()
if not review:
raise db_exceptions.NoDataFoundException("No review found with review ID: {}".format(review_id))
Copy link
Contributor

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}?

WHEN vote='t' THEN 1
ELSE 0
END
) AS votes_positive_count,
Copy link
Contributor

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,

Copy link
Contributor

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.

JOIN "user"
ON review.user_id = "user".id
JOIN license
ON license.id = license_id
Copy link
Contributor

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

AND latest.latest_timestamp = revision.timestamp
) AS latest_revision
ON review.id = latest_revision.review_id
{}
Copy link
Contributor

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.

@@ -56,6 +56,7 @@ def submit(user_id, revision_id, vote):
"vote": vote,
"rated_at": datetime.utcnow(),
})
return get(user_id, revision_id)
Copy link
Contributor

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?

Copy link
Contributor

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))
Copy link
Contributor

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 =.

@ferbncode ferbncode force-pushed the migrate-reviews branch 2 times, most recently from 887259c to 9032a75 Compare March 23, 2017 16:52
Copy link
Contributor

@gentlecat gentlecat left a 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.

WHERE id = :review_id
"""), {
"review_id": review_id,
})
Copy link
Contributor

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).



def hide(review_id):
"""Hide review given the review id.
Copy link
Contributor

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.

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
Copy link
Contributor

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.



def to_dict(review, confidential=False):
review["user"] = User(db_users.get_by_id(review.pop("user_id")))
Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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.


def update(*, review_id, drafted, text, license_id=None,
language=None, is_draft=None):
"""Update contents of this review.
Copy link
Contributor

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,
Copy link
Contributor

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.


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,
Copy link
Contributor

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.

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.
Copy link
Contributor

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.


Returns:
Dictionary of reviews.
Total number of reviews.
Copy link
Contributor

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,
Copy link
Contributor

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?

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))
Copy link
Contributor

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")
Copy link
Contributor

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")
Copy link
Contributor

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.

Copy link
Contributor

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)

Copy link
Contributor

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.


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."))
Copy link
Contributor

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.

if 'iso639_1_code' in dir(lang):
supported_languages.append(lang.iso639_1_code)

def get_or_404(review_id):
Copy link
Contributor

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.

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.
@gentlecat
Copy link
Contributor

Looks like there's a small conflict with another change. Would you mind making an update please?

@gentlecat gentlecat merged commit e11662b into metabrainz:master Apr 17, 2017
@gentlecat
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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?

@gentlecat
Copy link
Contributor

Next time make sure that your PR is small and easy to review. Otherwise, as evidenced here, this causes many issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants