Skip to content

Latest commit

 

History

History
326 lines (230 loc) · 14.1 KB

File metadata and controls

326 lines (230 loc) · 14.1 KB

Lab 1

Let's put those basics to the test.

Lab 1.1

Change to the correct branch:

git checkout lab-1.1

Report

The detail view for the post "Welcome to Our Newsletter" is broken.

To reproduce:

  1. Browse to the posts page.
  2. Click on "Read" for the post with the title "Welcome to Our Newsletter"
  3. "It doesn't work!"

Facts

Let's consider what we know:

  • The error message is Post matching query does not exist., implying the QuerySet does not contain a Post matching the filters.
  • The line that causes the error is on line 80:post = posts.get(title=lookup)
  • We know the post exists, we can find it in the admin
  • This impacts more than just the post in the report. The detail view is broken for all posts.

Investigation

  • What is being used to look up the Post instance?
  • Where is that lookup value coming from?
  • How does the link generate the URL to supply that lookup value?
Hints

We know from our facts that the view, view_post is trying to look up the post based on the title. The parameter, lookup that's being compared to title is coming from a URL keyword argument as seen in the function definition def view_post(request, lookup):.

From here we can do one of two things. We can see how the URLs get generated for this view, or we can see what value is passed in.

We can find how the URLs get generated by searching for newsletter:view_post (the name of the path for this view in our urls.py). From here we see that Post.get_absolute_url generates this URL by passing in the slug field as the lookup argument. This means that Post.get_absolute_url would generate links that would result in requests that would cause the view, view_post to try to find a Post where the title also matched its slug. This would be very unlikely and hence why it appears that every post's page is broken.

If we decided to go the route of inspecting the lookup parameter, we could have printed the value in the view print(lookup). From there, we would have had to recognize that lookup wasn't the actual title. This may have required us to compare the lookup value to instance's values in the admin. We can view these values by clicking on the instance from our admin search from earlier.

Conclusion

In this scenario, the Post's slug field is being used to generate the URL, while the view expects the title to be passed in.

This is an example of a bug in which the error report provides the majority of the information we need, but we have to read closely and correctly interpret the information. Try to avoid making assumptions about what you expect the error to be. Sometimes we'll see an exception type and think, "Oh, that obviously has to be in XYZ." But upon reading the actual error message and looking at the stacktrace, we discover it's from ABC.

To solve this, we should pass post.slug into the calls for generating newsletter:view_post. I'd also argue that the URL parameter should be renamed to slug to be more explicit about what is expected.

For example, it's easier to spot the bug in this code during a code review:

post = posts.get(title=slug)

than

post = posts.get(title=lookup)

Further consideration

This view uses post = posts.get(...) to fetch a single instance. When that doesn't exist, it's resulting in an uncaught exception which causes a 500 error. Instead, we should use post = get_object_or_404(posts, ...) to get a more friendly and less noisy 404 response. But consider what this error report would look like if that's how the code worked; we wouldn't have an error message or a stacktrace. We simply would see that the view is returning our HTTP 404 template implying the Post doesn't exist when we know it does.

What I'd like you to think about for a minute is how you would approach this problem in that scenario (the view is returning a 404 response when it shouldn't)?

Lab 1.2

This lab covers a very common error, but hard to diagnosis when it's an unknown unknown. An unknown unknown is something that you don't know, that you don't know.

Change to the correct branch:

git checkout lab-1.2

Report

Creating posts is broken. The open graph image doesn't get uploaded!

To reproduce:

  1. Browse to the create post page.
  2. Fill out the form fields with some filler information.
  3. Select an image for "Open graph image"
  4. Click save.
  5. The update form does not contain the file for the Open graph image field.
  6. "It doesn't work!"

Facts

Let's consider what we know:

  • The form is hitting the correct view and is using the form class we expect since the post is created with the non-file fields.

Investigation

  • Does the file show up on the server side?
    • Using the IDE's debugger, a breakpoint() line or a print statement, inspect request.FILES.
    • Are there any files included? Is "open_graph_image" a key?
  • Is the file being sent from the browser to the server?
    • We can use the browser's Developer Tool's Network panel to inspect the request.
    • Browse to the create view.
    • Open the developer tools, click on the networking panel.
    • Populate and submit the form.
    • Look for the image content in the request.
    • What value is being sent? Does it look like a file or the name of the file?
  • Can we create a Post with an Open graph image in the admin?
  • What is different between creating a Post in the admin versus creating a Post in our own application?
    • Compare the rendered <input> elements for open_graph_image.
    • Compare the containing form elements for those inputs.
Hints

If you've gone through all the steps of the investigation, and you're still unsure of why the file isn't being shared, you should read about the enctype attribute of forms.

You'll see that the

By default, a <form> element will use enctype="application/x-www-form-urlencoded". However, as we found, that will not submit any <input type="file" /> (or other file-based types) in the request. In order to include a file in the body of the request, the form must be encoded with multipart/form-data. This Stack Overflow answer has a bunch of good information.

Conclusion

In this scenario the <form> element on that's used for creating/updating a Post is missing the proper enctype="multipart/form-data" attribute. This is 100% one of those, need to remember parts of web development.

However, by asking the above questions, we can determine that this attribute is what's needed without knowing about it in the first place. You're never going to know everything about everything, so you will benefit from developing skills that help you reveal those unknown unknowns.

By starting with the fact that the server is not getting a file, we know the browser isn't doing what we expected. From there, we can compare a broken case to a working case, the application's create view to the admin's add Post view. Comparing those requests, we saw that the admin request was including binary data while the application view simply sent the string of the file name.

The next step is the most difficult jump in this lab. We either need to understand how HTML encodes forms/data or have a discerning eye to compare the two forms and spot the differences then experiment to identify which of those differences is the cause of the problem.

Knowing what the enctype attribute does will always be faster than investigative trial and error, but the skills used in that investigative trial and error can be reused on other problems.

Further consideration

How can you prevent this type of error from occurring in your applications? What can you do during development, testing or code reviews to catch this before it makes it way into production?

Lab 1.3

Change to the correct branch:

git checkout lab-1.3

Report

The list of posts is broken! The datetime strings should generally be in order of most recent to oldest, but they appear jumbled.

To reproduce:

  1. Browse to the list posts view.
  2. The dates are ordered from most recent to oldest, but the post "Getting Started with Python" appears out of order in comparison to "Welcome to Our Newsletter"
  3. "It doesn't work!"

Facts

Let's consider what we know:

  • Either the posts are being returned in a jumbled order or the datetime string is being rendered incorrectly.

Investigation

  • How are the posts being ordered for newsletter.views.list_posts?
  • What is rendering the datetime string on the page?
Hints

Reviewing newsletter.views.list_posts, we see that the QuerySet is being ordered by recent_first() which orders the QuerySet with Coalesce("publish_at", "created").desc(). This means that the posts will show up in descending order based on their publish_at value or created value if publish_at is set to None. If we needed to, we could check the query that is run to confirm this ordering is used, but it's a fair assumption considering the size of the view and template.

The next piece of the puzzle is determining how the timestamp for the post is rendered. There are two general ways to approach this. One is to look at the rendered HTML for something to search our templates for. The other is to review the templates top down, starting with the template that's used to render the view and look through them until we find the relevant part.

Let's start with searching the code base for existing piece of HTML. Using the Browser Developer tools and the Inspector (these names may be different with your browser), select the timestamp for one of the posts on the list view. Clicking that should then highlight some HTML that looks similar to:

<div class="left floated six wide column">
  <span class="" title="April 21, 2021, 5:51 a.m.">
    4&nbsp;years, 5&nbsp;months ago
  </span>
</div>

We can assume that the date specific information is going to be in some type of template filter or template tag. So let's try searching our codebase for left floated six wide column. This should have two code matches. One of which will have a child element with the text "Read More", and since that doesn't appear in our HTML, we can exclude it. This leaves us with the template projects/templates/posts/includes/list_item.html. From our search result, we can see that the template tag {% nice_datetime ... %} is rendering the timestamp.

If we wanted to try from the other approach, we'd start by looking at project/templates/posts/list.html. We can see a loop over to render individual elements for posts on the page, {% for post in page %}. This brings us to the template projects/templates/posts/includes/list_item.html. From there, we'd need to compare the rendered HTML to the template to narrow it down to the template tag {% nice_datetime ... %}.

Inspecting the function nice_datetime, we see that it's template tag makes use of an inclusion_tag decorator (read more about it here in the docs). When we review the template projects/templates/inclusion_tags/nice_datetime.html, we see that the timestamp is rendered via the timestamp context variable. Backtracking to nice_datetime, timestamp is declared by timestamp = post.created.

This is root problem since the collection of posts are actually ordered based on published_at and then created if published_at wasn't set.

Conclusion

The posts are being ordered correctly, publish_at first, falling back to created when unset. Therefore the template must be rendering incorrectly. This can be confirmed by comparing the fields of the posts that render correctly and incorrectly. From the admin, we can see the correctly rendering Post does not have a value for publish_at, while the incorrectly rendering Post does have a value for publish_at. We can see that the publish_at value is significantly more recent than the created value which explains why it appears in the list of posts at the top.

We can also infer that since the publish_at is the order_by value, that it should also be the value used when rendering the datetime string.

Now we know that the template is likely using the created field to render the datetime string when it shouldn't be. However, the template that's used to render the individual posts doesn't contain post.created explicitly. But we do see that there's a custom template tag that's rendering the datetime called nice_datetime. Looking at that code, we indeed find the timestamp variable being set to the value of post.created when it should be post.publish_date.

Further consideration

This is a straightforward example with an underlying concept. You must be able to switch between assuming that some part of the code works and knowing that some part of the code must contain an error. Was there a time when you incorrectly assumed a bug could not be in a component only to find that your assumption was wrong? Why were you so confident in that assumption? How can you learn to hold these opinions more loosely in the future?


Good work!

I hope you were able to find something to take away from this lab. Proceed to Lab 2.