<?xml version="1.0" encoding="UTF-8"?>
<!--Generated by Site-Server v@build.version@ (http://www.squarespace.com) on Tue, 07 Apr 2026 02:36:02 GMT
--><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://www.rssboard.org/media-rss" version="2.0"><channel><title>Blog - Lacey Henschel</title><link>https://www.laceyhenschel.com/blog/</link><lastBuildDate>Mon, 18 Mar 2024 23:43:59 +0000</lastBuildDate><language>en-US</language><generator>Site-Server v@build.version@ (http://www.squarespace.com)</generator><description><![CDATA[<p>Read articles on Django, Python, tech, gardening, and other fun topics on Lacey's blog</p>]]></description><item><title>Weeknotes -- critiquing my own pull request </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 10 Feb 2023 22:15:03 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2023/2/10/weeknotes-critiquing-my-own-pull-request</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:63e6b6032c669a0a19833ba2</guid><description><![CDATA[<p class="">This week, <a href="https://github.com/revsys/boost.org/pull/108">I let a PR get too big</a>. Everything in the PR relates to one another, but it could have been broken up into more manageable pieces. I just kept having one little thing to fix! And now, it’s at 400+ lines of code changes across 16 files. Yikes! </p><p class="">Instead of beating myself up about it, I thought I would talk about what makes this PR so unwieldy.  </p><h2>What’s wrong with it?</h2><p class="">In short: it does too much. </p><ul data-rte-list="default"><li><p class="">It adds several new URLs and changes the behavior of existing URLs across two apps. </p></li><li><p class="">It adds a manager method to a totally different app. </p></li><li><p class="">It adds a new field and overrides some default model behavior. </p></li><li><p class="">It makes minor changes to several template files. </p></li></ul><p class="">In a code review, all of this is cognitive overhead. For whoever reviews this PR, they have to jump around a lot to check my work. It’s extra work to make sure they are reviewing the right view for the right URL for the right test. It’s extra work to make sure everything has tests. It’s difficult for me to even explain my own changes, because there are so many.  </p><p class="">This would work better as several PRs. It would be easier for me to explain my changes, and easier for a reviewer to understand them. </p><p class="">It got this way because I was adding a ManyToMany relationship where one didn’t exist before, and trying to change the existing functionality to accommodate this all in one PR. </p><p class="">When I began, I didn’t think it would get so hairy. But along the way requirements got clarified that added some work, and my own focus was disrupted by household illness and then jury duty. It wasn’t even a case of scope creep. All of this would have needed to be done for the feature to have been considered “complete.” But it didn’t need to be done in one PR. </p><p class="">It was just one of those weeks where I felt behind all week, and by the time things felt clearer, I’d dug myself into a little hole. </p><h2>The PRs I should have opened </h2><ul data-rte-list="default"><li><p class="">Adding the new manager method. It’s a perfect little PR — a few lines of code in <code>managers.py</code>, then a test, and you’re good to go. </p></li><li><p class="">Adding the SlugField to a model, and then using the slug in the URL instead of the PK. This could have been two PRs, but I probably would have done it in one. This would have kept a database change and its side effects in their own PR. It’s a very straightforward enhancement, and is perfect for its own PR (or two). </p></li><li><p class="">Adding each new URL and its corresponding view in their own PRs. Each view had to override some default behavior. I think if I had done them to completeness, including tests, one at a time, it would have made the opportunities to make the code DRY-er more apparent. As it was, I was jumping around a lot and I think the code is a bit repetitive.  </p></li><li><p class="">Making the changes to the behavior of the existing views in their own PRs, for similar reasons as above. Since I changed existing behavior, having those changes in small PRs makes it very clear what changed. </p></li><li><p class="">Cleaning up the breadcrumbs and links in the templates. I tried to do this as I went, but it involved a lot of context switching. Especially since this project is still in dev, merging in changes that meant breadcrumbs needed some TLC wouldn’t have been the end of the world. And it would have been faster to get the other changes done, so I wouldn’t have had to do so much back-and-forth. </p></li></ul><p class="">Each of these would be easier for me to explain in a pull request description, and easier for a reviewer to review accurately. </p><p class="">… I might just re-do them. </p><p class="">Learn from my mistakes, friends. </p><h2>In other news…</h2><ul data-rte-list="default"><li><p class="">My cat is doing awesome at going in his crate.  </p></li><li><p class="">My current professional development project is learning about docs in a more official way. I’ve bought some books, I’m helping a friend revamp the docs for a project he maintains, and I’m planning to write about it! Let’s learn what we’re supposed to be doing with our docs together. </p></li><li><p class="">I’m finishing up another quilt. It’s all sandwiched and basted. I’m planning to free-motion quilt it. It already has a recipient and I’m excited to complete it! </p></li><li><p class="">I’m trying to write more. It’s hard! So I’m going to try to write more about why it’s hard. </p></li><li><p class="">Despite feeling a bit behind all week, it’s been a good week! I feel generally energized at work. We have some new projects coming up that I’m excited about. I think I’m going to get to stretch my wings in new directions a bit this year, and I am ready for it. </p></li></ul>]]></description></item><item><title>Weeknotes - Daily art, cat saga, and a redesigned office</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 13 Jan 2023 19:16:18 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2023/1/13/weeknotes-daily-art-cat-saga-and-a-redesigned-office</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:63c1ad93ce69d7285115965c</guid><description><![CDATA[<h2>Daily Art&nbsp;</h2><p class="">Thanks to the recommendation of a friend who knew I was trying to do more art, I’ve been following <a href="https://www.instagram.com/andreanelsonart/"><span>@andrea.nelson.art</span></a> on Instagram and trying out some of her art projects for myself. I took two weeks off at the end of 2022, and I spent most of it making art, and it was delightful.&nbsp;</p><p class="">I’ve never really done much art. I’m “not good at it.” But then eight years ago I became a Big Sister to a seven-year-old girl, and since then I have been collecting art supplies. Today, I am still a Big Sister (though my Little Sis is now a teenager), and I’m a mom to a three-year-old. Art is a huge part of my life, but I’d still mentally shunted it to the side as a “kids” activity.&nbsp;</p><p class="">And I’m just… not doing that anymore. Making art is fun! It’s relaxing. I’m giving myself permission to have fun and have no expectations of myself. I’m feeling the same joy my little sister and my daughter do when they create art and not judging myself.&nbsp;</p><p class="">So most days, I start off with 15-30 minutes of watercolor, or collage, or painting on a paper plate (I’ll write more on this another time), or doodling, or just fussy cutting pretty pictures out of magazines. It relaxes my mind and gives me a chance to get into a more focused, “work” mindset. A nice bonus is the gift I am giving myself of permission to use the many art supplies I’ve hoarded over the years!&nbsp;</p><h2>Cat Saga&nbsp;</h2><p class="">(I’m going to jump into the middle of the story without providing background. Sorry about that.)</p><p class="">My 14-year-old cat, Baxter, has been confined to one of the rooms in my house for most of the last year, for his own health and for a lot of reasons that I won’t go into. It’s been wonderful! He’s very happy having his own space, and the rest of us (including the 17-year-old cat) are happy that things are generally calmer.&nbsp;</p><p class="">But we’re not thrilled with losing access to what is, frankly, the best room in the house because the senior cat needs an excessive level of alone time. So the cat is moving in with me, into my home office!&nbsp;</p><p class="">This will be a long transition, measured in months and not weeks, because this is a very tough cat. (This is also where I just skip the backstory and ask you to trust me.) I’ve officially crossed over into cat-lady-dome, but whatever keeps the peace.&nbsp;</p><h2>New Office&nbsp;</h2><p class="">Well, same office, new layout.&nbsp;</p><p class="">I’ve been working from home almost exclusively for nearly nine years, and I’ve never really loved my office. I love working from home — the benefits of this arrangement are so numerous for me and I joke a lot about how I could never go back to an office — and my office has always been fine, but it’s never <em>really </em>worked the way I wanted it to.&nbsp;</p><p class="">Deciding to move my cat into my office with me has prompted a redesign of basically the rest of the house. Long story short, if the cat lives in here, then things that the rest of the family needs to access cannot live in here. So the gift wrapping supplies can no longer be stored in mom’s office, the basket of laundry that no one will fold can’t go in there with this particular cat, etc.&nbsp;&nbsp;</p><p class="">Also, my office needs to be very cat-friendly, so it needs a comfortable place to nap, and a place to look out the window, and space for a litter box, and an eating space, etc. There wasn’t the space for this with my old layout, or with all the extra stuff I’d been storing in my office.&nbsp;</p><p class="">This is an amazing opportunity, so on my two-week vacation, I really went through my office. My focus was on storing things more securely (Baxter can be destructive in some specific ways that I know how to cat-proof for), identifying things that needed a new home, and making zones for his various needs while still providing a place for my desk. An unexpected bonus was that I love how I reoriented my desk! I redesigned my bookshelves to exactly my taste, and my desk now faces them. My webcam no longer faces the door, which means that if my toddler comes running into a meeting covered in syrup, my colleagues won’t immediately know! I hadn’t identified this as a thing that made me anxious, but I actually felt relieved when I realized that someone being able to crack open my office door and know they won’t be on camera is a huge benefit.&nbsp;</p><h2>Journaling&nbsp;</h2><p class="">I’m a chronic <a href="https://www.laceyhenschel.com/blog/2018/6/5/keeping-a-work-notebook">journaler</a>, and I go through different journal phases where I write more or less. I am in a very, very strong journaling place right now. It’s overlapping with the art practice in fun ways, too – I watercolored on the pages in my work journal and now I am writing over them. I’m taking notes in my pretty felt pens in forest-y tones, using pastel highlighters, it’s all very soothing and nice.&nbsp;</p><h2>Reading&nbsp;</h2><p class="">- <a href="https://www.engmanagement.dev/"><span>Engineering Management for the Rest of Us</span></a>. I saw people on Twitter raving about this and picked up a copy for myself, even though I am not an engineering manager and don’t know if I plan to become one. I’m only a few chapters in, but I really like it so far. I’m keeping a list of takeaways as a non-manager in my work journal as I read.&nbsp;</p><p class="">- <a href="https://www.grammarly.com/blog/call-to-action/"><span>Writing calls to action</span></a> from Grammarly&nbsp;</p><p class="">- This helpful issue thread for <strong>django-allauth</strong>: <a href="https://github.com/python-social-auth/social-app-django/issues/174">GitHub The redirect_uri MUST match the registered callback URL for this application</a></p><p class="">- This article on <a href="https://www.additudemag.com/slideshows/adhd-in-the-workplace/"><span>productivity strategies for people with ADHD</span></a>. I don’t have ADHD, but I did find it helpful.</p>]]></description></item><item><title>Weeknotes: Week ending 11-18-2022: Ficography progress and Django Admin TILs</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 18 Nov 2022 18:44:16 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2022/11/18/weeknotes-week-ending-11-18-2022-ficography-progress-and-django-admin-tils</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:6377d208e3e3ba29c2c6360d</guid><description><![CDATA[<h2 id="tils">TILs</h2>
<ul>
<li><a href="https://github.com/williln/til/blob/main/django_admin/add_fields_to_list_view.md">Adding extra fields to the list view in the Django Admin</a> </li>
<li><a href="https://github.com/williln/til/blob/main/django_admin/add_search.md">Adding ability to search in the Django Admin</a> </li>
<li><a href="https://github.com/williln/til/blob/main/django_admin/add_filtering.md">Adding filtering functionality to the Django Admin</a></li>
<li><a href="https://github.com/williln/til/blob/main/django_admin/custom_fields.md">Adding a custom field to the Django admin list display</a> </li>
</ul>
<h2 id="ficography">ficography</h2>
<p><a href="https://github.com/williln/ficography">Ficography</a> is my attempt to build a better system for tracking the fanfiction I want to read. I spent some time this week creating sample data, making the Django admin work, and getting it set up to work with <a href="https://htmx.org">HTMX</a> using <a href="https://github.com/adamchainz/django-htmx"><code>django-htmx</code></a> and <a href="https://tailwindcss.com">Tailwind</a> using <a href="https://pypi.org/project/pytailwindcss/"><code>pytailwindcss</code></a>.</p>
<p>I'm trying to use <a href="https://simonwillison.net/2022/Oct/29/the-perfect-commit/">Simon's tips on documenting stuff</a> in issues, but I find it easier to document things in PRs and just commit/push frequently. That may change as I write up more issues; we'll see. I love adding a ton of screenshots and context to my work, though, and I tend to write TILs on a lot of the things I do lately. </p>
<p>I'm also experimenting with using a <a href="https://github.com/williln/ficography/issues?q=is%3Aopen+is%3Aissue+label%3A30-minutes"><code>30-minutes</code> label</a> for my issues. I'm a busy person -- I have a job, I help with a conference, I'm a mentor, I have a small child, I have other hobbies that I like to spend time on, and it's coming up on peak baking season. I want to split my side-project work into very easily-managed chunks, something I can fit in before my first meeting or right before going to pick up the kiddo from daycare. It's worked well so far. </p>
<p>It's freeing to have a side project where I am letting myself be a little messy. There is test coverage, for sure! But I don't have <code>coverage</code> installed yet, and I know that stuff like the sample data command is a bit brittle. But at the moment, the audience for this project is just me. So I'm doing what I want. YOLO and all that. </p>
<ul>
<li><a href="https://github.com/williln/ficography/pull/7">Generate fake data</a></li>
<li><a href="https://github.com/williln/ficography/pull/15">Add Author model to Admin</a></li>
<li><a href="https://github.com/williln/ficography/pull/16">Add Character model to Admin and beef up sample data</a></li>
<li><a href="https://github.com/williln/ficography/pull/17">Add Fandom model to Admin</a></li>
<li><a href="https://github.com/williln/ficography/pull/19">Add Tag model to Admin</a></li>
<li><a href="https://github.com/williln/ficography/pull/20">Add Ship model to Admin</a></li>
<li><a href="https://github.com/williln/ficography/pull/21">Add Fic model to Admin and beef up sample data</a></li>
<li><a href="https://github.com/williln/ficography/pull/22">Add django-htmx and Tailwind</a></li>
</ul>
<h2 id="client-work">Client work</h2>
<p>My work with the client I have had for the last two-plus years is coming to a close, and I'm embarking on a new adventure with a new client this week. I'm sad to leave my old client and I am really proud of the work I did there. As I prepped to separate from them, I wrote up transition notes based on Jacob's post on <a href="https://jacobian.org/2022/nov/9/transition-files/">maintaining a transition file</a>. His post is specific to keeping the file personal for yourself, but I adpated it for public (within the organization) sharing. I included: </p>
<ul>
<li>My main development responsibilities for the last year </li>
<li>Anything I was currently working on </li>
<li>An intro to the services that I was more of a "core" maintainer on that other engineers might not have a lot of context on </li>
<li>A list of the people I worked with on those services, including people from other areas of the company </li>
<li>Any errors that I'd been trying to debug </li>
<li>A short explanation for where I felt the code could use improvement, or areas I was watching, especially since I'm leaving right after having shipped a couple brand-new features that haven't gotten a ton of use yet</li>
</ul>
<h2 id="reading">Reading</h2>
<ul>
<li>My colleague Jacob Kaplan-Moss wrote up a post on <a href="https://jacobian.org/2022/nov/9/transition-files/">maintaining a transition file</a> that I found helpful. </li>
<li>I really loved hearing from <a href="https://www.askamanager.org">Ask A Manager</a> about Twitter: <a href="https://t.co/y9LSzHmszx">I work at Twitter … what do I do?</a> I don't envy anyone who work(s)(ed) at Twitter right now. Can't imagine how it feels to have your company torn apart like this. </li>
<li>Huge TW for this one for child abuse, but this was just enraging and heartbreaking: <a href="https://propub.li/3DYijij">A Custody Evaluator Who Disbelieves 90% of Abuse Allegations Recommended a Teen Stay Under Her Abusive Father’s Control</a>. Also: weird Val Kilmer connection.</li>
<li>I learned about <a href="https://medium.com/be-loved/lets-talk-about-walkaway-wife-syndrome-ba9c293bca8d">walkway wife syndrome</a>. </li>
<li>In case I needed another reason to be anti-death penalty (spoiler: I didn't), this NPR article gave me one: <a href="https://www.npr.org/2022/11/16/1136796857/death-penalty-executions-prison">Carrying out executions took a secret toll on workers — then changed their politics</a></li>
</ul>
<h2 id="personal-accomplishments">Personal accomplishments</h2>
<ul>
<li>✅ Went to the dentist for a cleaning </li>
<li>✅ Cleared off the sewing table to make room to actually sew </li>
<li>✅ Dragged out a WIP quilt, got it <a href="https://www.youtube.com/watch?v=v67D0LKpN6s">sandwiched</a>, and quilted about 20% of it.</li>
<li>✅ Made the first batch of <a href="https://www.thekitchn.com/best-cutout-sugar-cookies-recipe-recipes-from-the-kitchn-38629">Christmas cookies</a> and <a href="https://sallysbakingaddiction.com/royal-icing/">royal icing</a> for decorating. I'm happy with my icing consistency this year, which I put down to <a href="https://youtu.be/EMRWGjQ2Xzc">Quick and Easy Royal Icing Recipe</a> on YouTube.   </li>
<li>✅ Put away Halloween decorations </li>
<li>✅ Called the cat behaviorist back to give an update on my cat with complex behavioral issues </li>
<li>✅ Clipped about 5 of said cat's nails, which might potentially be my biggest accomplishment of the week </li>
<li>✅ Booked hotels for travel </li>
</ul>
<h2 id="miscellaneous">Miscellaneous</h2>
<ul>
<li>Watching Gilmore Girls, Season 2 -- comfort re-watch </li>
<li>Looked up lots of quilted coasters I could make as gifts: <ul>
<li><a href="https://quilterscandy.com/diy-coaster/amp/">Modern quilted coasters</a></li>
<li><a href="https://blog.spoonflower.com/2022/05/09/quilted-coasters/">Hexagonal coasters</a></li>
<li><a href="https://www.diaryofaquilter.com/quilted-leaf-coaster-tutorial/">Leaf coasters</a></li>
</ul>
</li>
<li>Looked up patterns for quilted Christmas stockings: <ul>
<li><a href="https://www.etsy.com/listing/713500847/">All HSTs</a>: Uses a bunch of <a href="https://sarahmaker.com/half-square-triangle-charts/">half-square triangles</a>, which are a pain, but there would be no end of patterns I could make with that block. </li>
<li><a href="https://www.diaryofaquilter.com/quilted-christmas-stocking-tutorial/">Quilt-as-you-go log cabin variation</a>: I really like the <a href="https://www.google.com/url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;cd=&amp;cad=rja&amp;uact=8&amp;ved=2ahUKEwjAzNaXrLj7AhWYMjQIHWuQDkAQFnoECBcQAw&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DgauFew1yrFo&amp;usg=AOvVaw2vk-5G9kygm8oHEqQqvZgT">quilt-as-you-go</a> method for making coasters, and it would be an awesome way to approach other small quilt projects.</li>
<li><a href="https://www.canuckquilter.com/2017/12/twice-turned-christmas-stocking-tutorial.html?m=1">Center star, log cabin</a>: I love quilted stars, and this one seems like another QAYG-possible option. </li>
<li><a href="https://suzyquilts.com/free-quilted-christmas-stocking-pattern/">Simple Suzy Quilts stocking</a>: Really simple pattern, if I could select the right fabrics I think I would love it. </li>
</ul>
</li>
</ul>]]></description></item><item><title>Weeknotes: GitHub Actions, switching gears on my fanfiction tracker </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Mon, 07 Nov 2022 21:46:11 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2022/11/7/weeknotes-github-actions-switching-gears-on-my-fanfiction-tracker</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:63697c3533241a02e37a4b01</guid><description><![CDATA[<h2 id="week-ending-11-4-22">Week ending 11-4-22</h2>
<h3 id="new-tils-">New TILs:</h3>
<p>I'm trying to work in public more, and break up the things I learn into smaller chunks, and practice more writing, so this week I distilled wha I learned from GH Actions into some TILs. </p>
<ul>
<li><a href="https://github.com/williln/til/blob/main/github/gh-action-run-job-conditionally.md">Running an action conditionally</a></li>
<li><a href="https://github.com/williln/til/blob/main/github/gh-action-set-output.md">Setting output for a step in a job, so a different job can use it</a></li>
<li><a href="https://github.com/williln/til/blob/main/github/gh-actions-set-job-dependency.md">Making one job in a workflow depend on another job</a></li>
<li><a href="https://github.com/williln/til/blob/main/github/gh-actions-parse-json.md">Parsing JSON output from a GitHub Issue template in a GitHub Action</a></li>
<li><a href="https://github.com/williln/til/blob/main/github/gh-actions-step-to-create-and-commt-a-file.md">Creating a new file and committing it using a GitHub Action</a></li>
</ul>
<h3 id="ao3-tracker"><code>ao3-tracker</code></h3>
<p>I'm going in a different direction with this. I honestly don't want to build a UI. I like using GH on my phone, but you can't directly edit files on mobile. </p>
<p>Current plan is this workflow to manage TBRs: </p>
<ul>
<li>Submit a new issue with a template </li>
<li>GH action will auto-tag the issue with <code>tbr</code> </li>
<li>GH action will look at issues tagged <code>tbr</code> and find the link</li>
<li>Link gets passed to Python script that: <ul>
<li>Goes to AO3 to get data </li>
<li>Parses data into JSON </li>
<li>Uses JSON to generate YAML </li>
<li>Writes the YAML to a new file </li>
</ul>
</li>
<li>GH action that commits the resulting new file to <code>main</code> </li>
<li>GH action that re-writes a TBR table in the style of my TIL readmes </li>
<li>Will prove the concept in a public repo, then fork it to a private one to keep my TBRs private </li>
</ul>
<p>Since I was doing a lot of testing of GH actions, my commits and PRs are all over the place. But here is what I worked on, broadly speaking. (Though almost every PR had a follow-up series of commits directly to <code>main</code> as I debugged what was wrong in the action.) </p>
<p>I really need a CI step that validates my YAML. So many syntax errors. </p>
<ul>
<li><a href="https://github.com/williln/ao3-tracker/pull/49">#49 Add conditional logic to an action to only run on certain labels</a> </li>
<li><a href="https://github.com/williln/ao3-tracker/pull/60">#60 Action to close an issue after it's been commented on</a> </li>
<li><a href="https://github.com/williln/ao3-tracker/pull/64">#64 Add action to parse the JSON of an issue</a></li>
<li><a href="https://github.com/williln/ao3-tracker/pull/66">#66 Hide Django</a> since I'm not planning to use it and it distracts me. </li>
</ul>
<h2 id="media">media</h2>
<ul>
<li>📺 Severance season 1: Too dark for me. Didn't make it past the pilot. </li>
<li>📺 Jane the Virgin season 1: Rewatch and loving it. </li>
</ul>]]></description></item><item><title>7 things I learned about GitHub Actions</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Mon, 07 Nov 2022 21:31:34 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2022/11/7/7-things-i-learned-about-github-actions</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:636975cc05b9fe46d41838f7</guid><description><![CDATA[<p class="">I’m working on a way to track my fanfiction reading, and using that as an excuse to learn GitHub Actions. I’ve posted 7 new TILs on small things you can do with GitHub Actions and wanted to share those. </p><p class="">My personal experience with learning GitHub Actions has been… interesting. The syntax is very different than what I am expecting, and I don’t always predict correctly whether to use the <code>${{ }}</code> braces around a variable. I bet if I learned some <code>bash</code> that it would be a bit easier? Maybe? Not sure. But once I muddled through a few of these, I got the hang of the syntax and things are going a little faster now. I still have a ton of questions, and none of it is easy for me, but I also feel like I’ve been bitten by the “automate all the things!” bug, and that’s fun. </p><ul data-rte-list="default"><li><p class=""><a href="https://github.com/williln/til/blob/main/github/action_pr_comment.md">Leaving a comment on a new issue or new PR</a> — could combine this one with labels to respond to first-time contributors, or set an out-of-office response, or just say, “Hi there!” </p></li><li><p class=""><a href="https://github.com/williln/til/blob/main/github/gh-action-run-job-conditionally.md">Running an action conditionally</a> — AKA where do I put the <code>if</code> statement? Bonus points for a little intro into traversing the API response within the variable syntax. </p></li><li><p class=""><a href="https://github.com/williln/til/blob/main/github/gh-actions-set-job-dependency.md">Making one job in a workflow depend on another job</a> — Or, how do I make sure this job runs before that job runs? </p></li><li><p class=""><a href="https://github.com/williln/til/blob/main/github/gh-actions-parse-json.md">Parsing JSON output from a GitHub issue template in a GitHub action</a> — AKA, take an issue template and turn it into JSON! </p></li><li><p class=""><a href="https://github.com/williln/til/blob/main/github/gh-action-set-output.md">Setting output from one step in a job, so a different job can use it</a> — This goes hand-in-hand with the previous TIL about using an issue template to parse data about an issue. I needed to be able to pass the data in an issue into another process. </p></li><li><p class=""><a href="https://github.com/williln/til/blob/main/github/gh-actions-step-to-create-and-commt-a-file.md">Creating a new file and committing is as part of a GitHub action</a> — In this case, I took the data I got from the parsed issue, then wrote some of it to a new <code>txt</code> file, just to prove that I could. In the future, I plan to use this auto-generate <code>yaml</code> files from AO3 links. The dream workflow is opening an issue with the title “TBR” and a link to AO3, then the GitHub actions workflow takes care of tagging the issue, calling AO3, creating the new file, updating the status to TBR, creating the commit, pushing to <code>main</code>, and closing the PR. Not there yet, but soonish! </p></li><li><p class=""><a href="https://github.com/williln/til/blob/main/github/gh_actions_temporary_disable.md">Temporarily disabling a GitHub action without touching the workflow file</a> — Some of my workflows are real works-in-progress. I’m flying by the seat of my pants here a little bit, so things don’t always wind up in a “working” state before I need to stop, and I didn’t want to keep merging other work and then getting emails that the workflow failed. So I learned how to disable it without just commenting out the file. </p></li></ul><p class=""><strong>Side note</strong>: Thanks to Simon Willison’s excellent post about <a href="https://simonwillison.net/2022/Nov/6/what-to-blog-about/">What to blog about</a>, which inspired me to write this! </p>]]></description></item><item><title>Weeknotes: ao3-tracker and DjangoCon US </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Mon, 24 Oct 2022 16:26:44 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2022/10/24/weeknotes-ao3-tracker-and-djangocon-us</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:6356b886f4abcb7927a6f20f</guid><description><![CDATA[<h1>DjangoCon US 2022 </h1><p class="">I just got back from my first  in-person conference in more than three years! DjangoCon US 2022 in San Diego, CA accomplished everything a conference is supposed to — I feel inspired, energized, and motivated. I’m planning on doing a longer DjangoCon US post once the talks come out on YouTube so I can link to the ones that spoke the loudest to me, but suffice it to say, the talks were top-tier this year and it was incredible. </p><h2>Pull Requests: </h2><ul data-rte-list="default"><li><p class=""><a href="https://github.com/djangocon/2022.djangocon.us/pull/229">Update the location of some conference amenities</a></p></li><li><p class=""><a href="https://github.com/djangocon/2022.djangocon.us/pull/230">Add daily breakfast and lunch menus</a></p></li><li><p class=""><a href="https://github.com/djangocon/2022.djangocon.us/pull/231">Add Code of Conduct team members</a> </p></li><li><p class=""><a href="https://github.com/djangocon/2022.djangocon.us/pull/236">Suspend ticket sales</a></p></li></ul><h1>REVSYS offsite</h1><p class="">Or hackathon or get-together or whatever we call it — most of REVSYS attended DjangoCon US and got there a couple of days early to code together! It was mostly on an internal project in a private repo, but it’s been a while since I worked on anything “from scratch,” and I really enjoyed the feeling of making a brand-new set of endpoints work for the first time. </p><h2>Pull Requests: </h2><ul data-rte-list="default"><li><p class="">Add users with a custom user model </p></li><li><p class="">Add basic Django models, serializers, and endpoints </p></li><li><p class="">Add some placeholder JSON in the serializer so that the frontend could start working with it </p></li><li><p class="">Re-learn how to use <code>django-filters</code> and allow an endpoint to do some filtering </p></li></ul><h1><code>ao3-tracker</code></h1><p class="">During the pandemic, I got into fanfiction. Like, <em>really</em> into fanfiction. Specifially on the site Archive of Our Own, abbreviated as AO3. </p><p class="">AO3 has an awesome filtering system, making it easy to find fics that fit your preferred fandom, ships, tropes, etc. And they provide a way to save fics to read later, and to recommend fics to friends. But they don’t have the same filtering options on the works you’ve saved to read later, or that you’ve recommended to others, as they do for the more general search. </p><p class="">They also don’t have a REST API (or any other kind of API). </p><p class="">And there is a LOT of fanfiction, y’all. My TBR (“to be read”) is hundreds of works long. And without a good way to filter them, it’s really hard to find the next thing I want to read. I’m not the only one with this problem. In the fandom communities I’m part of, people maintain complex Notion board and Google spreadsheets, or they comment in Reddit threads or keep notebooks, all so they can track their reading.  </p><p class="">Enter <code><a href="https://github.com/williln/ao3-tracker">ao3-tracker</a></code>, my first idea for a personal project that I need. It’s really simple so far. It uses the Python library <code><a href="https://github.com/ArmindoFlores/ao3_api">ao3_api</a></code>, and my hope is to turn it into something I can use to track my reading, which will pull data from AO3. No idea where it’s going to go. Maybe I’ll abandon it after this week and no one will hear about it again. Who knows.</p><h2>Pull Requests:</h2><ul data-rte-list="default"><li><p class=""><a href="https://github.com/williln/ao3-tracker/pull/18">Set up the Django project</a></p></li><li><p class=""><a href="https://github.com/williln/ao3-tracker/pull/23">Add some basic models</a></p></li><li><p class=""><a href="https://github.com/williln/ao3-tracker/pull/32">Import my bookmarks from AO3</a></p></li></ul><p data-rte-preserve-empty="true" class=""></p>]]></description></item><item><title>Weeknotes: WYSKADRF and why is my API slow today? </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 26 Feb 2021 17:10:14 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2021/2/26/weeknotes-wyskadrf-and-why-is-my-api-slow-today</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:60392872f3eca176e415b621</guid><description><![CDATA[In which I talk about super-slow API endpoints and how I fixed them, why 
it’s okay that a pie chart can add up to more than 100% (spoiler: it’s not 
really a pie chart and dealing with money is hard), and link you to my 
3-part series on What You Should Know About DRF.]]></description><content:encoded><![CDATA[<h2 id="client-work">Client Work</h2>
<h3 id="why-is-this-endpoint-so-slow-">Why is this endpoint so slow?</h3>
<p>At the end of the last sprint, my client shipped a new feature that had the side effect of slowing down one of the endpoints significantly (think 25-30 seconds). This endpoint was returning a list of something — let’s say it was Games — and prior to this release we’d only had 3 Games in the system. The endpoint had never been “snappy,” but once we added 7 more Games (for a total of 10 Games), depending on how many games you had played, the endpoint was taking forever. </p>
<p>As I have mentioned before, this client uses a microservice architecture. This means that, for many endpoints, we need to make at least one call to another microservice to get some data. I realized that we were calling the microservice that held the info we needed once <em>per game</em>. 3 calls to that microservice was one thing, but 10 calls was quite another and resulted in significant slowdown. </p>
<p>The calls being made to the other service were also inefficient. What we needed was some summary data: your total score for all the games you’ve played in the last 3 months, plus the average number of games you play per week. What we were doing was requesting the record of every single game you played for that time period, then doing the math ourselves to add up the score and the number of games played. Yikes! </p>
<p>I spent about a week on a pretty significant refactor of this endpoint. Since this Games endpoint initially shipped — well over a year ago — we’d added some features to the other microservice that enabled us to get this summary data without returning all the objects. </p>
<p>I’ve simplified this explanation a lot — we actually need make 3 separate API calls to other services, and we were doing 2 of those calls inefficiently, resulting in at least 1 API call per Game object. Basically, I replaced 10 calls (1 call per type of Game to get all instances of the user playing that game) with a single call that returns the list of Game types with the summary data needed. </p>
<h3 id="how-can-this-pie-chart-add-up-to-more-than-100-">How can this pie chart add up to more than 100%?</h3>
<p>I wrote a feature that looks at all your spending for a period of time, divides it up into categories, and tells you how much you spent weekly on average in each category. Theoretically, the percentages should add up to 100% (or close to it — some rounding means it might be off a bit). </p>
<p>But in dev, a tester was getting some very strange results, like 414% for one category. WTF? </p>
<p>I am still working on this one, but I actually don’t think it’s a bug (although it did result in some clarification discussions with the product team). First, I wasn’t filtering the transactions the way Product was expecting, so some transactions (like your monthly paycheck) were getting included in “spending.” This can significantly throw off the numbers. If you spent $400 last month on ice cream (represented with a negative number -400) but you also made $2,000 via a paycheck and that $2,000 is counted with your “spending,” that’s going to throw your numbers off. </p>
<p>But you can’t just look at transactions that have a negative amount. People return things, and the refund they get shouldn’t be treated as income in this case. It should be treated as spending. And returning very expensive items (like cancelling an order for a fridge) or returning things well after the initial purchase date (like you can with REI) can throw off your numbers as well.  
So I changed the queryset for the Transactions to filter the way that product was expecting, and we also talked about some edge cases where the percent you spent in a specific category might be negative (like if you returned something that got you a refund larger than everything else you spent) or even over 100% (if the total amount you spent was technically $100 because of a large return, but you spent $200 dining out, it looks like the Dining Out category represented 200% of your spending). </p>
<p>I am reasonably sure that there is not actually a bug in my math, but if it turns out there is, I will update next week. </p>
<h2 id="pycascades">PyCascades</h2>
<p>I presented “What You Should Know About Django REST Framework” last Saturday at PyCascades Remote (<a href="http://youtu.be/06DJBu1zwoY">link to video</a>)! PyCascades was an awesome conference and I’m so glad they allowed me to present. You can access all the videos from the conference on their <a href="https://www.youtube.com/watch?v=BLcea-tIxjw&amp;list=PLcNrB7gPa-NeSmRMdvEsJWE3iVUm3bbW4">YouTube channel</a>. </p>
<h2 id="this-website">This website</h2>
<p>I added a <a href="https://www.laceyhenschel.com/colophon">Colophon</a> to my site. I also changed some of the fonts I was using. </p>
<h2 id="writing">Writing</h2>
<p>I wrote a 3-part series, “<a href="https://www.laceyhenschel.com/blog/tag/whatyoushouldknowaboutdrf">What You Should Know About DRF</a>,” based on the talk I gave at PyCascades.  </p>
<p>I didn’t write any new TILs this week. </p>]]></content:encoded></item><item><title>What You Should Know About DRF, Part 3: Adding custom endpoints </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Wed, 24 Feb 2021 16:37:43 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-django-rest-framework-part-3-adding-custom-endpoints</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:603581382157a83e8e84c5de</guid><description><![CDATA[Sometimes the endpoints you get when you use a ModelViewSet aren't enough 
and you need to add extra endpoints for custom functions. To do this, you 
could use the APIView class and add a custom route to your `urls.py` file, 
and that would work fine.

But if you have a viewset already, and you feel like this new endpoint 
belongs with the other endpoints in your viewset, you can use DRF's @action 
decorator to add a custom endpoint. This means you don't have to change 
your urls.py -- the method you decorate with your @action decorator will 
automatically be rendered along with the other enpdoints.]]></description><content:encoded><![CDATA[<p><em>This is Part 3 of a 3-part series on Django REST Framework viewsets. Read <a href="https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods">Part 1: ModelViewSet attributes and methods</a> and <a href="https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-drf-part-2-customizing-built-in-methods">Part 2: Customizing built-in methods</a>.</em> </p>
<p><em>I gave this talk at <a href="">PyCascades 2021</a> and decided to turn it into a series of blog posts so it's available to folks who didn't attend the conference or don't like to watch videos. Here are the <a href="">slides</a> and the <a href="">video</a> if you want to see them.</em> </p>
<hr>
<p>Sometimes the endpoints you get when you use a <code>ModelViewSet</code> aren't enough and you need to add extra endpoints for custom functions. To do this, you could use the <a href="https://www.django-rest-framework.org/api-guide/views/"><code>APIView</code></a> class and add a custom route to your <code>urls.py</code> file, and that would work fine. </p>
<p>But if you have a viewset already, and you feel like this new endpoint belongs with the other endpoints in your viewset, you can use DRF's <code>@action</code> decorator to add a custom endpoint. This means you don't have to change your <code>urls.py</code> -- the method you decorate with your <code>@action</code> decorator will automatically be rendered along with the other enpdoints. </p>
<p>Let's continue with the library example from Part 1. Now we need a new endpoint just for featured books, books with <code>featured = True</code> on the <code>Book</code> model. To do this, we'll add a <code>featured()</code> method to our <code>BookViewSet</code> and decorate with DRF's <code>@action</code> decorator. </p>
<pre><code class="lang-py">from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from .models import Book
from .serializers import BookDetailSerializer, BookListSerializer


class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer

    def get_serializer_class(self):
        if self.action in ["list", "featured"]:
            return BookListSerializer
        return super().get_serializer_class()

    @action(detail=False, methods=["get"])
    def featured(self, request):
        books = self.get_queryset().filter(featured=True)
        serializer = self.get_serializer(books, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)</code></pre>
<p>This code will result in this endpoint: </p>
<pre><code>GET /books/featured/</code></pre><p>Let's start with the <a href="https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing"><code>@action</code> decorator</a>.</p>
<p>The <code>@action</code> decorator takes a couple of required arguments: </p>
<ul>
<li><code>detail</code>: <code>True</code> or <code>False</code>, depending on whether this endpoint is expected to deal with a single object or a group of objects. Since we want to return a group of featured books, we have set <code>detail=False</code>. </li>
<li><code>methods</code>: A list of the HTTP methods that are valid to call this endpoint. We set ours to <code>["get"]</code>, so if someone tries to call our endpoint with a <code>POST</code> request, they will receive an error. This argument is actually optional and will default to <code>["get"]</code>, but actions are frequently used for <code>POST</code> requests so I wanted to make sure to mention it. </li>
</ul>
<p>Once we are in our <code>featured()</code> method, we create the queryset by calling <code>get_queryset()</code> and then filtering for <code>featured=True</code> books. Then we get the right serializer from <code>get_serializer()</code>, which will call <code>get_serializer_class()</code>. </p>
<p>Notice that I added <code>"featured"</code> to the list of actions that will return <code>BookListSerializer</code> in <code>get_serializer_class()</code>. The name of the action will share the name of the method. </p>
<p>I pass the <code>books</code> queryset into the serializer, then return the data in the <code>Response</code> object along with the correct HTTP status code. (The status will default to <code>HTTP_200_OK</code> if you don't set it, but I set it explicitly to show you that you can.)</p>
<hr>
<p>In <a href="https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods">Part 1: ModelViewSet attributes and methods</a>, I covered the attributes and methods that ship with <code>ModelViewSet</code>, what they do, and why you need to know about them. </p>
<p>In <a href="https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-drf-part-2-customizing-built-in-methods">Part 2: Customizing built-in methods</a>, I went through some real-world examples for when you might want to override some of <code>ModelViewSet</code>'s built-in methods. </p>]]></content:encoded></item><item><title>What You Should Know About DRF, Part 2: Customizing built-in methods </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Tue, 23 Feb 2021 18:13:42 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-drf-part-2-customizing-built-in-methods</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:603545828644cd5acf6e5831</guid><description><![CDATA[If you came here from Part 1 of What You Should Know About Django REST 
Framework, you may be wondering why I just walked you through a bunch of 
source code. We stepped through that code because if you know what the main 
methods of the ModelViewSet do and how they work, you know where to go when 
you want to tweak the behavior of your viewset. You can pull out the method 
that contains what you want to change, override it with your own custom 
behavior, and put it back in. In Part 1, we were writing a BookViewSet. So 
let's go through a few cases where we might want to customize the behavior 
of our endpoints and walk through how we would do that.]]></description><content:encoded><![CDATA[<p><em>This is Part 2 of a 3-part series on Django REST Framework viewsets. Read <a href="https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods">Part 1: ModelViewSet attributes and methods</a> and <a href="https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-django-rest-framework-part-3-adding-custom-endpoints">Part 3: Adding custom endpoints</a>.</em> </p>
<p><em>I gave this talk at <a href="https://2021.pycascades.com/program/talks/what-you-should-know-about-django-rest-framework/">PyCascades 2021</a> and decided to turn it into a series of blog posts so it's available to folks who didn't attend the conference or don't like to watch videos. Here are the <a href="https://2021.pycascades.com/program/talks/what-you-should-know-about-django-rest-framework/">slides</a> and the <a href="http://youtu.be/06DJBu1zwoY">video</a> if you want to see them.</em> </p>
<hr>
<p>If you came here from <a href="https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods">Part 1</a> of What You Should Know About Django REST Framework, you may be wondering why I just walked you through a bunch of source code. </p>
<p>We stepped through that code because if you know what the main methods of the <code>ModelViewSet</code> do and how they work, you know where to go when you want to tweak the behavior of your viewset. You can pull out the method that contains what you want to change, override it with your own custom behavior, and put it back in.  </p>
<p>In Part 1, we were writing a <code>BookViewSet</code>. So let's go through a few cases where we might want to customize the behavior of our endpoints and walk through how we would do that. </p>
<h2 id="how-do-i-return-different-serializers-for-list-and-detail-endpoints-">How do I return different serializers for list and detail endpoints?</h2>
<p>When I hit <code>GET /books/</code> (so I'm seeing a list of books), I only want some of the book data. Maybe I want the cover image, the title, the author, and whether there are books available. For this, I want to use my <code>BookListSerializer</code>. </p>
<p>But when I hit <code>GET /books/{id}/</code> (so I'm on the page for a specific book), I want all that data and more. I want links to other books the author has written, reviews for the book, the number of copies, and the year it was published. For this data, I want to use my <code>BookDetailSerializer</code>. </p>
<p>Remember the method that DRF uses to return the serializer class, <code>get_serializer_class()</code>? That's the method we want to override. </p>
<pre><code class="lang-py">from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet 

from .models import Book
from .serializers import BookDetailSerializer, BookListSerializer

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookDetailSerializer
    permission_classes = [AllowAny]

    def get_serializer_class(self):
        if self.action in ["list"]:
            return BookListSerializer
        return super().get_serializer_class()</code></pre>
<p>In line 5, we import both serializers. </p>
<p>Then on line 9, we set the <code>serializer_class</code> attribute to whichever serializer we want to be the default. I'm going to set the default serializer to <code>BookDetailSerializer</code>, because there is only one case where I want to use the list serializer.</p>
<p>Then, I create my own <code>get_serializer_class()</code> method.  </p>
<p>The <code>self.action</code> attribute is set by the the DRF <code>ModelViewSet</code> and is set to the name of the request method (see the <a href="https://github.com/encode/django-rest-framework/blob/3.9.0/rest_framework/viewsets.py#L145">source code</a>). Remember that <code>ModelViewSet</code> doesn't use HTTP methods like <code>get()</code> and <code>post()</code>. It uses action-based method names, so the <code>/books/</code> list endpoint uses the <code>list()</code> method.  </p>
<p>Since we know this, we can check the value of <code>self.action</code> to decide which serializer to return. </p>
<pre><code class="lang-py">if self.action in ["list"]:
    return BookListSerializer</code></pre>
<p>If <code>action</code> is "something from this list", then we return the list serializer. The only thing in the list is <code>"list"</code>, but I like the <code>if value in [list of things]</code> syntax so I can add more actions later if I need to. </p>
<p>If the <code>action</code> is not in the list of actions we define, then we want DRF to return whatever it was going to if we hadn't done anything. To do that, we call <code>super()</code>:</p>
<pre><code class="lang-py">if self.action in ["list"]:
    return BookListSerializer
return super().get_serializer_class()</code></pre>
<h2 id="how-do-i-create-things-with-one-serializer-but-return-them-with-another-serializer-">How do I create things with one serializer, but return them with another serializer?</h2>
<p>Our API includes an endpoint that allows the creation of new books. Maybe our serializer performs some post-processing and we want to be able to return the results of that post-processing in the response using a different serializer than the one we use to create the book. </p>
<p>This is another common use case where you want to use more than one serializer, but in this case, it would be much harder to accomplish this by just overriding <code>get_serializer_class()</code> because you want to change serializers while you're performing your action.</p>
<p>The easiest way to do this is by overriding the action method itself. First, let's review the <code>create()</code> and <code>perform_create()</code> methods from <code>CreateModelMixin</code>: </p>
<pre><code class="lang-py">class CreateModelMixin:
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(
            serializer.data, status=status.HTTP_201_CREATED, headers=headers
        )

    def perform_create(self, serializer):
        serializer.save()</code></pre>
<p>The <code>create()</code> method of the <code>ModelViewSet</code> does a few things: </p>
<ol>
<li>Retrieves the serializer using <code>self.get_serializer()</code> and passes in the data from the request </li>
<li>Checks that the serializer is valid, and raises an error if it isn't </li>
<li>Calls <code>perform_create()</code> with the serializer, which calls <code>save()</code> on the serializer but doesn't return anything </li>
<li>Calls <code>get_success_headers()</code> </li>
<li>Returns the serializer data with the <code>Response</code> object </li>
</ol>
<p>What we want to do is start with one serializer (first line of the <code>create()</code> method), but after we've created our instance (by calling <code>perform_create()</code>), we want to switch to a different serializer. </p>
<p>For this to work, we need to be able to access the instance we just created. Luckily, the instance is returned from the serializer's <code>save()</code> method -- it's just that RF's <code>perform_create()</code> method doesn't use it. </p>
<p>We can override the <code>create()</code> method and replace the line that calls <code>perform_create()</code>. </p>
<pre><code class="lang-py">from .serializers import BookSerializer, BookCreatedSerializer

class BookViewSet(ModelViewSet):
    ...

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()
        return_serializer = BookCreatedSerializer(instance)
        headers = self.get_success_headers(return_serializer.data)
        return Response(
            return_serializer.data, status=status.HTTP_201_CREATED, headers=headers
        )</code></pre>
<p>In the code above, I've basically copied DRF's <code>create()</code> method into my own <code>BookViewSet</code>. My <code>create()</code> method and theirs are almost identical. But I've replaced where DRF calls <code>perform_create()</code> with my own call to <code>serializer.save()</code> so I can save the instance that method returns in my own variable. </p>
<p>Then, I can instantiate my <code>BookCreatedSerializer</code> with the new book <code>instance</code> (and give this serializer the variable <code>return_serializer</code>), call <code>get_success_headers()</code>, and return the <code>return_serializer</code> data in the <code>Response</code>. </p>
<h2 id="how-can-i-remove-endpoints-from-modelviewset-">How can I remove endpoints from <code>ModelViewSet</code>?</h2>
<p>Like we talked about in <a href="https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods">Part 1</a>, using <code>ModelViewSet</code> gives you 6 endpoints from 5 mixins: </p>
<ul>
<li><code>CreateModelMixin</code> gives you <code>POST /books/</code> </li>
<li><code>RetrieveModelMixin</code> gives you <code>GET /books/{id}/</code></li>
<li><code>UpdateModelMixin</code> gives you <code>PUT /books/{id}/</code> and <code>PATCH /books/{id}/</code> (full update and partial update)</li>
<li><code>ListModelMixin</code> gives you <code>GET /books/</code></li>
<li><code>DestroyModelMixin</code> gives you <code>DELETE /books/{id}/</code></li>
</ul>
<p>But what if you don't need all those endpoints? Maybe you want your API to include the ability to perform all these actions except deleting books. </p>
<p>In that case, you can create your own <code>ModelViewSet</code> using only the mixins that give you the endpoints you want. To do everything but delete, your viewset would look like this: </p>
<pre><code class="lang-py">from rest_framework.generics import GenericAPIView 
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin,
    UpdateModelMixin, ListModelMixin

# import models and serializers...

class BookViewSet(
    CreateModelMixin,
    RetrieveModelMixin,
    UpdateModelMixin,
    ListModelMixin,
    GenericAPIView
):
    ...</code></pre>
<p>But DRF is smart! It knows that you might want to use only a few endpoints at a time, so DRF includes several convenience classes for you with different combinations of the <code>GenericAPIView</code> and the action mixins. You can see them on the <a href="http://www.cdrf.co/">ClassyDRF</a> website under the <strong>Generics</strong> heading. </p>
<ul>
<li><code>CreateAPIView</code> = <code>GenericAPIView</code> + <code>CreateModelMixin</code> </li>
<li><code>ListAPIView</code> = <code>GenericAPIView</code> + <code>ListModelMixin</code> </li>
<li><code>DestroyAPIView</code> = <code>GenericAPIView</code> + <code>DestroyModelMixin</code>  </li>
<li><code>UpdateAPIView</code> = <code>GenericAPIView</code> + <code>UpdateModelMixin</code>  </li>
<li><code>RetrieveAPIView</code> = <code>GenericAPIView</code> + <code>RetrieveModelMixin</code>  </li>
<li><code>ListCreateAPIView</code> </li>
<li><code>RetrieveDestoryAPIView</code></li>
<li><code>RetrieveUpdateAPIView</code> </li>
<li><code>RetrieveUpdateDestroyAPIView</code> </li>
</ul>
<p>You can use any of these convenience view classes to create the set of API endpoints you need for your project, or use the <code>GenericAPIView</code> class plus the mixins you need to create your own. </p>
<hr>
<p>In <a href="https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods">Part 1: ModelViewSet attributes and methods</a>, I covered the attributes and methods that ship with <code>ModelViewSet</code>, what they do, and why you need to know about them. </p>
<p>In <a href="https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-django-rest-framework-part-3-adding-custom-endpoints">Part 3: Adding custom endpoints</a>, I tell you how to add your own custom endpoints to your viewset without having to write a whole new view or add anything new to your <code>urls.py</code>. </p>]]></content:encoded></item><item><title>What You Should Know About DRF, Part 1: ModelViewSet attributes and methods</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Mon, 22 Feb 2021 16:59:59 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2021/2/22/what-you-should-know-about-drf-part-1-modelviewset-attributes-and-methods</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:6033e2c57838b72471625c31</guid><description><![CDATA[One of the things I hear people say about Django is that it's a "batteries 
included" framework, and Django REST Framework is no different. One of the 
most powerful of these "batteries" is the ModelViewSet class, which is more 
of a "battery pack," in that it contains several different batteries. If 
you have any experience with Django's class-based views, then DRF's 
viewsets will hopefully look familiar to you.]]></description><content:encoded><![CDATA[<p><em>I gave this talk at <a href="https://2021.pycascades.com/program/talks/what-you-should-know-about-django-rest-framework/">PyCascades 2021</a> and decided to turn it into a series of blog posts so it's available to folks who didn't attend the conference or don't like to watch videos. Here are the <a href="https://2021.pycascades.com/program/talks/what-you-should-know-about-django-rest-framework/">slides</a> and the <a href="http://youtu.be/06DJBu1zwoY">video</a> if you want to see them.</em> </p>
<hr>
<p>One of the things I hear people say about Django is that it's a "batteries included" framework, and Django REST Framework is no different. One of the most powerful of these "batteries" is the <a href="http://www.cdrf.co/3.9/rest_framework.viewsets/ModelViewSet.html"><code>ModelViewSet</code></a> class, which is more of a "battery pack," in that it contains several different batteries. If you have any experience with Django's class-based views, then DRF's viewsets will hopefully look familiar to you. </p>
<p>The <a href="http://www.cdrf.co/3.9/rest_framework.viewsets/ModelViewSet.html"><code>ModelViewSet</code></a> is what it sounds like: a set of views that lets you take a series of actions on model objects. The <a href="https://www.django-rest-framework.org/api-guide/viewsets/">DRF docs</a> define it as "a type of class-based View, that does not provide any method handlers such as <code>.get()</code> or <code>.post()</code>, and instead provides actions such as <code>.list()</code> and <code>.create()</code>."</p>
<pre><code class="lang-py">class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, 
    `retrieve()`, `update()`, `partial_update()`, 
    `destroy()` and `list()` actions.
    """
    pass</code></pre>
<p>You can see how the <code>ModelViewSet</code> is constructed: it includes a class called <code>GenericViewSet</code>, and then 5 mixins with names like <code>CreateModelMixin</code>. Each <code>*ModelMixin</code> class has its own methods that perform actions related to the name of the mixin. For example, <code>CreateModelMixin</code> has a <code>create()</code> method. It does not, however, have a <code>post()</code> method. This is what DRF means when the docs said above that it "does not provide method handlers such as <code>.get()</code> or <code>.post()</code>." If you've used Django's CBVs, you have probably dealt with the <code>.get()</code> and <code>.post()</code> methods there. But DRF's <code>ModelViewSet</code> skips these methods and replaces them with more specific methods related to actions. </p>
<p>For the <a href="http://www.cdrf.co/3.9/rest_framework.mixins/CreateModelMixin.html"><code>CreateModelMixin</code></a>, which is a set of methods that helps you create new objects, you would expect to deal with a <code>.post()</code> method since creating new stuff for your database is generally dealt with in an HTTP POST request. But <code>CreateModelMixin</code> instead gives you a <code>.create()</code> method. This comes in handy later on, because handling cases where you're adding new objects versus cases updating existing objects is easier. You don't need any conditional logic in a <code>.post()</code> method to tell the difference -- they are already in their respective <code>.create()</code> and <code>.update()</code> methods. </p>
<h2 id="example">Example</h2>
<p>Let's say you're building a library app, and you want to create a set of endpoints to deal with books. Using a <code>ModelViewSet</code> means that creating endpoints to add, update, delete, retrieve, and list all books requires just 6 lines of code in your <code>views.py</code>. </p>
<pre><code class="lang-py"># views.py 
from rest_framework.viewsets import ModelViewSet 

from .models import Book
from .serializers import BookSerializer

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer</code></pre>
<p>This gets you these endpoints: </p>
<ul>
<li>List all books: GET <code>/books/</code></li>
<li>Retrieve a specific book: GET <code>/books/{id}/</code></li>
<li>Add a new book: POST <code>/books/</code></li>
<li>Update an existing books: PUT <code>/books/{id}/</code></li>
<li>Update part of an existing book: PATCH <code>/books/{id}/</code></li>
<li>Remove a book: DELETE <code>/books/{id}/</code></li>
</ul>
<p>You would also need to write the <code>BookSerializer</code> and hook these endpoints up in your <code>urls.py</code>, but you can see how to do that in the docs. </p>
<p>Six lines of code and you're done! </p>
<p>Except that most of the time, your project requirements are a little more complex than "write 6 lines of code and let DRF take it from there." That's where this talk (and set of blog posts) comes in. You can do a lot to customize DRF's functionality while still using the convenience methods that DRF includes for you. This can save you time, lines of code, testing, and headaches. </p>
<h2 id="modelviewset-attributes"><code>ModelViewSet</code> Attributes</h2>
<p>There are three attributes on your <code>ModelViewSet</code> that you should set.  </p>
<p>The <code>queryset</code> attribute answers the question, "What objects are you working with?" It takes a (you guessed it) queryset. Below, I've set mine to <code>Book.objects.all()</code>, but you can set yours to a model manager or a queryset with some filtering. </p>
<p>The <code>serializer_class</code> attribute addresses the question, "How should the data you are dealing with be serialized?" I've set mine to <code>BookSerializer</code>. A serializer is the class that defines how the data should be formatted. If you're not super familiar with APIs at this point, the basic idea is that an API sends data back as JSON blobs. Your serializer defines how you want to transform your model objects into JSON and which fields you want to include. </p>
<p>The <code>permission_classes</code> attribute defines who is allowed to access the endpoints created by this viewset, and it takes a list or tuple of permission classes. I've set mine to <code>[AllowAny]</code> using a built-in permission class from DRF. If you don't set this attribute, DRF provides a default or you can define your own default in settings. I always prefer to set mine explicitly, though. </p>
<pre><code class="lang-py">from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet 

from .models import Book
from .serializers import BookSerializer

class BookViewSet(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = [AllowAny]</code></pre>
<h2 id="modelviewset-methods-that-come-from-genericviewset"><code>ModelViewSet</code> methods that come from <code>GenericViewSet</code></h2>
<h3 id="get_queryset-"><code>get_queryset()</code></h3>
<p>The <a href="http://www.cdrf.co/3.9/rest_framework.viewsets/GenericViewSet.html#get_queryset"><code>get_queryset()</code></a> method mostly just returns whatever you set in your <code>queryset</code> attribute. </p>
<pre><code class="lang-py">def get_queryset(self):
    assert self.queryset is not None, (
        "'%s' should either include a `queryset` attribute, "
        "or override the `get_queryset()` method."
        % self.__class__.__name__
    )

    queryset = self.queryset
    if isinstance(queryset, QuerySet):
        queryset = queryset.all()
    return queryset</code></pre>
<p><strong>Why it's useful</strong>: Knowing about this method is useful for when you want to make some changes to your queryset using some data you don't have until the time of the request to your API. I often override this method in my own viewsets so I can filter the queryset based on the user. </p>
<pre><code class="lang-py">class BookViewSet(ModelViewSet):
    def get_queryset(self):
        queryset = super().get_queryset() 
        return queryset.filter(owner=self.request.user)</code></pre>
<h3 id="get_object-"><code>get_object()</code></h3>
<p>The <a href="http://www.cdrf.co/3.9/rest_framework.viewsets/GenericViewSet.html#get_object"><code>get_object()</code></a> method is used in endpoints that deal with a specific object, so any endpoint that uses an identifier (PUT <code>/books/{id}/</code>, for example). </p>
<pre><code class="lang-py">def get_object(self):
    queryset = self.filter_queryset(self.get_queryset())

    lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

    assert lookup_url_kwarg in self.kwargs, (
        'Expected view %s to be called with a URL keyword argument '
        'named "%s". Fix your URL conf, or set the `.lookup_field` '
        'attribute on the view correctly.' %
        (self.__class__.__name__, lookup_url_kwarg)
    )
    # Uses the lookup_field attribute, which defaults to `pk`
    filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
    obj = get_object_or_404(queryset, **filter_kwargs)

    # May raise a permission denied
    self.check_object_permissions(self.request, obj)
    return obj</code></pre>
<p><strong>Why it's useful</strong>: I don't need to override this method very often, but it's really useful to know about because of all the steps it takes for you. </p>
<ul>
<li>First, it filters the queryset for you. </li>
<li>Then, it makes sure it's able to look up your object with the <code>lookup_url_kwarg</code>. (This will default to <code>id</code> or <code>pk</code> but you can set it to something else if you need to.)</li>
<li>Then, it tries to retrieve your object for you and will raise a 404 error on your behalf if it can't find it in your queryset using <code>get_object_or_404()</code>. </li>
<li>Finally, before it returns the object, it checks to make sure that the user who made this request has adequate permissions for this object. </li>
</ul>
<p>This is a lot of tedious work. If you write custom endpoints for your viewset, or you're in the <code>update()</code> method doing some special work for your project's requirements, you will probably need the object itself at some point. If you grab the <code>id</code> from the request and try to get the object from there, you then have to worry about permissions, what to do if the object doesn't exist, etc.  </p>
<p>Instead, you can run </p>
<pre><code class="lang-py">obj = self.get_object()</code></pre>
<p>from inside your method and let DRF take care of those important steps for you! </p>
<h3 id="the-serializer-methods">The serializer methods</h3>
<p>There are three methods that deal with the serializer: </p>
<ul>
<li><code>get_serializer_class()</code></li>
<li><code>get_serializer_context()</code></li>
<li><code>get_serializer()</code></li>
</ul>
<p>These three methods work together to return a serializer that's ready for you to work with. </p>
<p><a href="http://www.cdrf.co/3.9/rest_framework.viewsets/GenericViewSet.html#get_serializer_class"><code>get_serializer_class</code></a> returns whatever you set in your <code>serializer_class</code> attribute. </p>
<pre><code class="lang-py">def get_serializer_class(self):
    assert self.serializer_class is not None, (
        "'%s' should either include a `serializer_class` attribute, "
        "or override the `get_serializer_class()` method."
        % self.__class__.__name__
    )

    return self.serializer_class</code></pre>
<p><strong>Why it's useful</strong>: If you want to use a different serializer in different situations, you can override <code>get_serializer_class()</code> to add that logic. You might want to use different serializers for list requests and detail requests, for example. We'll go over that in the next post. </p>
<p><a href="http://www.cdrf.co/3.9/rest_framework.viewsets/GenericViewSet.html#get_serializer"><code>get_serializer()</code></a> calls <code>get_serializer_class()</code> and returns it. </p>
<pre><code class="lang-py">def get_serializer(self, *args, **kwargs):
    serializer_class = self.get_serializer_class()

    # The context is where the request is added 
    # to the serializer
    kwargs['context'] = self.get_serializer_context()

    return serializer_class(*args, **kwargs)</code></pre>
<p>But first, it calls <a href="http://www.cdrf.co/3.9/rest_framework.viewsets/GenericViewSet.html#get_serializer_context"><code>get_serializer_context()</code></a> and adds what that returns to the serializer, before getting it back to you. </p>
<pre><code class="lang-py">def get_serializer_context(self):
    return {
        'request': self.request,
        'format': self.format_kwarg,
        'view': self
    }</code></pre>
<p><strong>Why it's useful</strong>: You can override <code>get_serializer_context()</code> to add more information to your serializer if you need to. If you've ever been in one of your serializer methods and used <code>self.context["request"].user</code>, the reason you're able to access the user from the request in your serializer is because of <code>get_serializer_context()</code>. </p>
<p>I recently had a situation where I needed to do a lot of math calculations in my serializer for each object. It was more effecient to get some of the values I needed up front and pass them into the serializer context by overriding this method, rather than getting those values new for each object I was dealing with. </p>
<p>I don't often need to override <code>get_serializer()</code>, but knowing what it does (get your serializer class and pass your serializer context into it, before giving your serializer to you) means that you can run </p>
<pre><code class="lang-py">serializer = self.get_serializer()</code></pre>
<p>in your viewset methods as a shortcut. Like with <code>get_object()</code>, this ensures that you're getting the serializer you want, with the data you want in it, without having to do any extra or duplicate work. If you construct your serializer manually in your methods, like <code>serializer = BookSerializer(instance=obj)</code>, then you skip that context and lose the chance to have access to the request (and therefore the user) in your serializer. </p>
<h2 id="modelviewset-methods-that-come-from-the-action-mixins"><code>ModelViewSet</code> methods that come from the action mixins</h2>
<p>I'm not going to go into the methods that come with all five of the mixins that are included with <code>ModelViewSet</code>, but I'll go through the ones that come with <code>CreateModelMixin</code> as an example, and hopefully you can extrapolate from there. </p>
<p><code>CreateModelMixin</code> comes with three methods: <a href="http://www.cdrf.co/3.9/rest_framework.mixins/CreateModelMixin.html#create"><code>create()</code></a>, <a href="http://www.cdrf.co/3.9/rest_framework.mixins/CreateModelMixin.html#perform_create"><code>perform_create()</code></a>, and <a href="http://www.cdrf.co/3.9/rest_framework.mixins/CreateModelMixin.html#get_success_headers"><code>get_success_headers()</code></a>. I won't go over <code>get_success_headers()</code> because I never need to mess with it, but you can explore what it does on your own. </p>
<p>The <code>create()</code> method does several things: </p>
<ul>
<li>Gets the serializer from <code>get_serializer()</code> and passes the data from the request into it </li>
<li>Checks that the serializer is valid, and raises an exception for you if it isn't </li>
<li>Calls <code>perform_create()</code> </li>
<li>Gets the success headers from <code>get_success_headers()</code></li>
<li>Returns the serializer data in the response with those headers and an HTTP status code </li>
</ul>
<pre><code class="lang-py">class CreateModelMixin:
    def create(self, request, *args, **kwargs):
      serializer = self.get_serializer(data=request.data)
      serializer.is_valid(raise_exception=True)
      self.perform_create(serializer)
      headers = self.get_success_headers(serializer.data)
      return Response(
        serializer.data, status=status.HTTP_201_CREATED, headers=headers
      )</code></pre>
<p>The <code>perform_create()</code> method calls the <code>save()</code> method from the serializer, but doesn't return anything. </p>
<pre><code class="lang-py">class CreateModelMixin:
    def perform_create(self, serializer):
        serializer.save()</code></pre>
<p><strong>Why it's useful</strong>: It's useful to know about these because sometimes, you need to do some custom processing either before or after you have performed the action in the request. For creating a new object, maybe you need to call a task that does some other processing, or you need to send a message to a message or event bus so another system can take some action. </p>
<p>Knowing where the action is happening, so to speak, lets you override the method you want to inject your custom behavior. For example, to fire off a special task after you've created a new object, you could override the <code>perform_create()</code> method: </p>
<pre><code class="lang-py">from .tasks import special_new_book_task 

class BookViewSet(ModelViewSet):
    def perform_create(self, serializer):
        super().perform_create(serializer)
        special_new_book_task.delay(serializer.instance.id)</code></pre>
<p>This lets you fire off the task after the new obejct has been saved without having to manually retool the whole <code>create()</code> method to make it happen. </p>
<hr>
<p>In <a href="https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-drf-part-2-customizing-built-in-methods">Part 2: Customizing built-in methods</a>, I'll go through some real-world examples for when you might want to override some of these built-in methods. </p>
<p>In <a href="https://www.laceyhenschel.com/blog/2021/2/23/what-you-should-know-about-django-rest-framework-part-3-adding-custom-endpoints">Part 3: Adding custom endpoints</a>, I tell you how to add your own custom endpoints to your viewset without having to write a whole new view or add anything new to your <code>urls.py</code>. </p>]]></content:encoded></item><item><title>Weeknotes: Squish all the bugs </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 19 Feb 2021 18:26:40 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2021/2/19/weeknotes-squish-all-the-bugs</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:602ff0ce32b4c41bbd19eb1d</guid><description><![CDATA[In looking back over the PRs I submitted this week for my main client, who 
has a microservice architecture, I see that I was fixing a lot of small, 
annoying bugs.

These bugs were introduced as part of a rewrite I’m working on to improve 
the performance of a few API endpoints in one of the microservices. These 
endpoints depend on getting a lot of data from another microservice and we 
made some other changes recently that had the unexpected side effect of 
slowing these endpoints down quite a bit. I wound up making some pretty 
significant changes to how these endpoints get their data that involved 
also making some changes to the endpoints in the microservice it was 
calling, so it’s not surprising that there were some bugs to work out.]]></description><content:encoded><![CDATA[<h2 id="client-work">Client Work</h2>
<p>In looking back over the PRs I submitted this week for my main client, who has a microservice architecture, I see that I was fixing a lot of small, annoying bugs. </p>
<p>These bugs were introduced as part of a rewrite I’m working on to improve the performance of a few API endpoints in one of the microservices. These endpoints depend on getting a lot of data from another microservice and we made some other changes recently that had the unexpected side effect of slowing these endpoints down quite a bit. I wound up making some pretty significant changes to how these endpoints get their data that involved also making some changes to the endpoints in the microservice it was calling, so it’s not surprising that there were some bugs to work out. </p>
<p>What was surprising was how much I appreciated having a user in the dev environment that had lots of weird data in it. </p>
<p>I have a useful habit: Every time a bug pops up in production or staging that’s due to some unexpected or weird data in someone’s account, I create a record attached to my dev user that shares those qualities. You never know when something like a field being <code>null</code> and not 0 will trigger a bug in the system. It’s hard to account for everything, and it’s easy to make assumptions about how you think a service you’re calling will return null data to you. What this meant was that I caught a lot of bugs before they even hit the staging environment! It also meant I spent the week playing whack-a-mole with bugs. Here are some of the bugs I fixed. </p>
<h3 id="arrays-in-query-parameters">Arrays in query parameters</h3>
<p>These were some bugs that I have encountered before, but I always forget. </p>
<p>When you’re calling an API and passing an array as one of the query parameters, you need to stringify it. </p>
<pre><code>$ ids = [123, 456, 789]
$ ids_query_param = “,”.join([str(id) for id in ids])
$ print(ids_query_param)
“123,456,789”</code></pre><p>This means you don’t wind up accidentally constructing an API call that looks like this: </p>
<pre><code>localhost:8000/api/my-stuff/?ids=[123, 456, 789]</code></pre><p>Which won’t work. Instead, your call will look like this: </p>
<pre><code>localhost:8000/api/my-stuff/?ids=123,456,789

# or 

localhost:8000/api/my-stuff/?ids=123%2C456%2C789</code></pre><p>Depending on whether you’re endcoding the commas in your URLs. </p>
<p>I also remembered that retrieving an array query parameter in your view isn’t as simple as <code>request.query_params.get()</code>. That will get you only the first item in your array. To retrieve an array query parameter, you must use <code>.getlist</code>. </p>
<pre><code>ids = request.query_params.getlist(“ids”)</code></pre><h3 id="null-values-where-you-expect-zeros">Null values where you expect zeros</h3>
<p>The microservice I was working in was making a call to another microservice that returned a numeric total. It did this in this format: </p>
<pre><code class="lang-json">{“total”: {“amount”: 100}}</code></pre>
<p>There were some other fields in the <code>total</code> dictionary, but the one I cared about was <code>amount</code>. I knew that the service I was calling was getting this <code>total</code> by actually performing an aggregate query using <code>Sum</code>, so I assumed that if the fields it was totaling were null, it would return <code>{“total”: {“amount”: 0}}</code>. </p>
<p>This was a bad assumption. </p>
<p>I found this out when this code resulted in an <code>AttributeError</code>: </p>
<pre><code class="lang-python">return response.json()[“total”].get(“amount”, 0)</code></pre>
<p>I thought I was being so clever, including a default value for <code>amount</code>! Alas, the AttributeError was due to the response actually looking like this: </p>
<pre><code class="lang-json">{“total”: null}</code></pre>
<p>So I changed my code to this instead: </p>
<pre><code class="lang-python">return response.json()[“total”].get(“amount”, 0) if response.json()[“total”] else 0</code></pre>
<p>It’s maybe not the most elegant solution, but there you go. </p>
<h3 id="why-is-my-custom-drf-serializer-context-not-in-my-serializer-">Why is my custom DRF serializer context not in my serializer?</h3>
<p>I am giving a talk tomorrow about Django REST Framework that includes a section on customizing your serializer context and I still introduced this bug this week. (This code is paraphrased.) </p>
<pre><code class="lang-python"># views.py 
context = self.get_serializer_context()
context[“my_field”] = value 

serializer = self.get_serializer(data, context=context)
return serializer.data

# serializers.py 
my_field = self.context[“my_field”] # Error!!</code></pre>
<p>This is because <code>get_serializer()</code> <a href="http://www.cdrf.co/3.9/rest_framework.generics/GenericAPIView.html#get_serializer">already calls</a> <code>get_serializer_context()</code> and passes it into the serializer. Me passing another kwarg called <code>context</code> didn’t override this behavior. </p>
<p>The better practice is to call <code>get_serializer_class</code> instead. </p>
<pre><code class="lang-python"># views.py 
context = self.get_serializer_context()
context[“my_field”] = value 

serializer = self.get_serializer_class()(data, context=context)
return serializer.data

# serializers.py 
my_field = self.context[“my_field”] # No error!</code></pre>
<h2 id="pycascades">PyCascades</h2>
<p>I’m presenting tomorrow at PyCascades! My talk, <a href="https://2021.pycascades.com/program/talks/what-you-should-know-about-django-rest-framework/">What You Should Know About Django REST Framework</a>, is tomorrow at 11:05 AM. Next week, I’ll be posting a couple blog posts that summarize the talk. I’ll also make my slides available once the talk is over. I recorded the talk a couple of weeks ago and it’s my first virtual conference talk (and first conference talk in some time, and first talk on DRF), so I’m excited! </p>
<h2 id="til">TIL</h2>
<p>I posted a new TIL to that repo, <a href="https://github.com/williln/til/blob/main/django/aggregation_coalesce.md">Using Coalesce to provide a default value for aggregate queries</a>. It doesn’t have a ton of data in it that isn’t in the Django docs, but the <code>Coalesce</code> function was entirely new to me and I wanted to make sure I remember it! </p>
<h2 id="this-website">This website</h2>
<p>I redesigned my website this week. Simplified it, made the home page more of a “resume” that includes my articles and talks, and removed those as individual pages. It just felt like it made more sense.  </p>
<p>I still like Squarespace for my main site, but I do think I would like to move the blog to something like Jekyll or <a href="https://datasette.io/">Datasette</a> with GitHub actions. I am finding it so much easier to push a markdown file to my TIL repo than to create a post on Squarespace. In fact, Squarespace froze in a way that made me lose all my work on this blog post, so I’m actually writing this in Apple Notes so I can copy and paste it.  </p>
<h2 id="github-profile">GitHub Profile</h2>
<p>I finally added a README to my <a href="https://github.com/williln/">GitHub profile</a>! I feel like I am the last one to do this, but it was time. </p>]]></content:encoded></item><item><title>Weeknotes: Dipping a toe into Amazon S3</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 08 Jan 2021 19:22:08 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2021/1/8/weeknotes-dipping-a-toe-into-amazon-s3</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5ff8ae1715099227a91b1493</guid><description><![CDATA[<p class="">I’ll be honest: I was incredibly distracted at work this week. I had a lot of goals for this week that just did not happen. But I was able to solve a couple problems and make some progress! </p><p class="">One of my goals for 2021 is to share what I’m working on more frequently, and I really like seeing Simon Willison’s <a href="https://simonwillison.net/tags/weeknotes/">Weeknotes</a> posts as well as his <a href="https://github.com/simonw/til/">TILs</a>, so I’m going to try to copycat those this year. </p><h2>Amazon S3 and Django</h2><p class="">This week (really should be “for the last month” but this is a weeknote and not a monthnote) I made my first foray into configuring Amazon S3 for file storage in a Django project, and it brought me a lot of headaches! I wrote about my problem (unsigned URLs that I expected to be signed) in my <a href="https://github.com/williln/til/blob/main/django/aws_signed_urls.md">first TIL post of 2021</a>. Part of the problem was probably that I was adding to existing S3 configuration — I was adding the capability to do private storage as well as public storage — and since I don’t have prior S3 experience <em>and </em>I hadn’t been the one to set it up in the first place, I just wasn’t very familiar with the settings so even diagnosing the problem was a challenge for me. Luckily, a colleague with more S3 experience knew what was wrong immediately and everything is working now. </p><h2>Pumpkin Py</h2><p class=""><a href="https://pumpkinpy.substack.com/p/my-favorite-weeknight-meals-in-2020">This week’s Pumpkin Py newsletter</a> was all about easy meals. If you like food newsletters, check out Pumpkin Py. </p>]]></description></item><item><title>Django Tips: Custom Model Managers </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Tue, 18 Dec 2018 17:01:41 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2018/12/18/django-tips-custom-model-managers</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5c1928040e2e724de170c1b4</guid><description><![CDATA[Every Django model comes with its own model manager. The manager is how you 
interact with your Django models to do database queries. In this post, 
learn how to override the built-in model manager to create your own custom 
manager.]]></description><content:encoded><![CDATA[<p>Every Django model comes with its own <a href="https://docs.djangoproject.com/en/2.1/topics/db/managers/">model manager</a>. The manager is how you interact with your  Django models to do database queries. Whenever you call your model with <code>Model.objects.all()</code>, for example, you're using the built-in manager for your model. </p>
<p>You might have a query that you run in more than one place in your codebase. If you run a pizza parlor, for example, you might want to get the "special" pizzas: </p>
<pre><code>return Pizza.objects.filter(special=True)</code></pre><p>But at some point your models might change. Maybe you add an <code>active</code> field to the model so you can store seasonal pizzas and mark them as "inactive" when it's not the right season for them anymore. Then your query needs to change: </p>
<pre><code>return Pizza.objects.filter(special=True, active=True)</code></pre><p>You have to make this change every place you make this query for pizza specials, which might be in several places. </p>
<p>Enter custom model managers! You can create your own custom model managers to deal with queries that occur frequently in your code. Then you can change the query in one place when your conditions change. You do this by overriding the built-in model manager and adding methods to it. </p>
<pre><code>from django.db import models 

class PizzaManager(models.Manager):
    def specials(self):
        return self.get_queryset().filter(special=True, active=True)</code></pre><p>Then tell your Django model to use this new, custom manager instead of the default manager. </p>
<pre><code>from django.db import models

from .managers import PizzaManager 

class Pizza(models.Model):
    ….
    objects = PizzaManager()</code></pre><p>Now you can call <code>Pizza.objects.specials()</code> to get your specials! </p>
<p><em>This article uses Django 2.1. Thanks <a href="https://twitter.com/webology">Jeff</a> for proofreading this article.</em>  </p>]]></content:encoded><media:content type="image/jpeg" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1545152653955-YHV71VZUP92RBT1BV8E4/dayne-topkin-81631-unsplash.jpg?format=1500w" medium="image" isDefault="true" width="1500" height="2250"><media:title type="plain">Django Tips: Custom Model Managers</media:title></media:content></item><item><title>Search History: Adding Page Privacy in Wagtail Programmatically</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Wed, 01 Aug 2018 19:34:06 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2018/8/1/search-history-adding-page-privacy-in-wagtail-programmatically</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5b620a281ae6cfe2d5e344ba</guid><description><![CDATA[This week I needed to manipulate the privacy of a Wagtail page 
programmatically. The Wagtail docs show you how to edit page privacy in the 
admin UI, but it doesn't peek behind the curtain to show you the code. The 
example here is a result of my Google searching.]]></description><content:encoded><![CDATA[<p>Versions: </p><ul>
<li>Wagtail 2.1.1</li>
<li>Django 1.11</li>
<li>Python 3.6</li>
</ul><p>This week I needed to manipulate the privacy of a Wagtail page programmatically. The Wagtail docs show you how to edit <a href="http://docs.wagtail.io/en/v2.1.1/advanced_topics/privacy.html#private-pages">page privacy</a> in the admin UI, but it doesn't peek behind the curtain to show you the code. The example here is a result of my Google searching. </p>











































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg" data-image-dimensions="1350x1002" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=1000w" width="1350" height="1002" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152102187-8HOBQAH4KJ3BBCP3YYXV/page_privacy.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  


<p>For this example, assume we have a group called "members," and we need to restrict a page to be seen only by members of that group. This code assumes you have a page instance from a model that inherits from Wagtail's <code>Page</code> model, and that you have already defined your group. </p><pre><code>from django.contrib.auth.models import Group
from wagtail.core.models import PageViewRestriction
from app.models import MyPage

members_group = Group.objects.get(name='members')
my_page = MyPage.objects.get(slug='my-slug')</code></pre><p>Once you have retrieved the group and the page you want to restrict, create a new <code>PageViewRestriction</code> object. Because the <code>groups</code> attribute on the <code>PageViewRestriction</code> model is a <code>ManyToMany</code> field, you must create the restriction instance before you add the group.</p><pre><code>restriction = PageViewRestriction.objects.create(
    page=my_page, 
    restriction_type=PageViewRestriction.GROUPS,
)</code></pre><p>Now add the group that is allowed to see the page. </p><pre><code>restriction.groups.add(members_group)</code></pre><p>See the code for the <a href="https://github.com/wagtail/wagtail/blob/stable/2.1.x/wagtail/core/models.py#L1918"><code>PageViewRestriction</code></a> class, and the available <code>RESTRICTION_CHOICES</code> (where I got <code>PageViewRestriction.GROUPS</code>) in the <a href="https://github.com/wagtail/wagtail/blob/stable/2.1.x/wagtail/core/models.py#L1866"><code>BaseViewRestriction</code></a> class. </p><p><em>Thanks to <a href="https://twitter.com/webology">Jeff Triplett</a> for proofreading a draft of this article.</em></p>]]></content:encoded><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1533152002963-2NJXD5TI3PL86KJY2CP1/Screen+Shot+2018-08-01+at+12.33.00+PM.png?format=1500w" medium="image" isDefault="true" width="1350" height="1002"><media:title type="plain">Search History: Adding Page Privacy in Wagtail Programmatically</media:title></media:content></item><item><title>5 Reasons to Keep a Work Notebook</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Wed, 06 Jun 2018 15:30:36 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2018/6/5/keeping-a-work-notebook</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5b1743070e2e7265c1833e77</guid><description><![CDATA[Notebooks aren’t just for journaling and to-do lists. They can also be 
valuable tools for learning, personal organization, and professional 
development.]]></description><content:encoded><![CDATA[<p>Earlier this week I posted on Twitter about filling up my most recent work notebook.&nbsp;</p>























<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Took six months to fill my most recent work notebook. Learned about celery, async, wagtail, AWS S3, and django-rest-auth in these pages. <br><br>The notebook is dead. Long live the notebook. 📒 <a href="https://t.co/MOR9gDWL8W">pic.twitter.com/MOR9gDWL8W</a></p>— Lacey Williams Henschel (@laceynwilliams) <a href="https://twitter.com/laceynwilliams/status/1003785553756614656?ref_src=twsrc%5Etfw">June 4, 2018</a></blockquote>
<p>Someone asked to learn more about this practice, so here you go! I've kept a work notebook for years. I stole the strategy from my friend and former colleague <a href="https://twitter.com/rebeccakindschi">Rebecca</a>, who attended all of our team meetings with a small notebook that proved invaluable for reminding us all what we'd decided on the week before, or how to solve a particularly weird bug that only came up once a year. My notebook started out primarily as a place to keep track of personal meeting notes, but over time it has grown to serve several other purposes. </p>
<h2 id="1-personal-meeting-notes">1. Personal meeting notes</h2>
<p>These notes aren't shared — I'm not taking minutes. Taking notes in meetings helps me identify questions I have and clarify priorities. Many meetings aren't super formal and so might not include a follow-up email of what was discussed or decided on; because I take my own notes, I can be pretty confident of action items and relevant decisions. </p>
<h2 id="2-retain-new-information">2. Retain new information</h2>
<p>When I write things down I tend to remember them better. I take notes when reading docs on topics that are new to me, focusing on new terms or clarifying a process that was hard to make sense of. </p>
<h2 id="3-draw-pictures">3. Draw pictures</h2>
<p>I'm a big fan of flowcharting for helping me understand model relationships (like an informal <a href="https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model">Entity Relationship Diagram</a>) and complex processes. A good flowchart can pinpoint parts of lengthy functions that can be broken out into smaller, easier-to-understand pieces. But it's generally not worth my time to use a tool to create a formal flowchart. Most of the time, I can get what I need with a few minutes and a couple of pages in my notebook. </p>
<h2 id="4-make-to-do-lists">4. Make to-do lists</h2>
<p>I like to spend a few minutes at the end of my day thinking about what I need to get done the next day. By writing a to-do list in the notebook I use for work, it will be the first thing I see the next morning. </p>
<h2 id="5-remember-questions">5. Remember questions</h2>
<p>I work remotely full-time and in a slightly different time zone than most of my colleagues. Sometimes they're all gone but I'm confused about something. My work journal helps me keep track of questions to ask the next morning, without needing to Slack people in their off-hours, send unnecessary email, or otherwise do something to remember what it was I was confused about. </p>
<h2 id="is-this-just-bullet-journaling-">Is this just bullet journaling?</h2>
<p>Not really. I've tried <a href="http://bulletjournal.com/">bullet journaling</a> in a more formal way in the past, and I know it works for some people. I also bought a <a href="http://www.passionplanner.com/">Passion Planner</a> at the beginning of this year in the hopes of taking my work notebook habit to new and more impressive heights. But I've discovered that the formality of a planner or a specific journal method doesn't suit me. I don't really keep track of my to-do lists from day to day; I might not even cross things off. I generally need my work notebook to help me keep track of things I've learned and things I need to do, and to organize my thoughts, so keeping things simple works better for me. </p>
<p>My work notebook also isn't a journal. I don't tend to write personal thoughts in my work notebook — I have a separate journal for that. (I do a lot of journaling about work, and I highly recommend developing a journaling habit, especially if you tend to be a little anxious.) </p>
<h2 id="tools">Tools</h2>
<p>Use what works for you; these are the tools that have worked for me. </p>
<ul>
<li><a href="https://smile.amazon.com/Moleskine-Classic-Colored-Notebook-Underwater/dp/8867323679/ref=sr_1_9?ie=UTF8&amp;qid=1528252190&amp;sr=8-9&amp;keywords=moleskine+soft+cover+notebook">Moleskine Large Dotted Soft Cover Notebook</a> — I like this notebook because of the soft cover, thin pages, and dots. The soft cover makes it easy to open, and it has an elastic strap to keep it closed. The thin pages still don't bleed with most pens. The dots, as opposed to lines or just blank pages, give me flexibility: I have dots to keep my writing lined up, but they are faint enough that I can still draw diagrams without getting distracted by lines. The notebook lays flat when open. </li>
<li><a href="https://smile.amazon.com/SAN1742659-Sharpie-Fine-Point-Pen/dp/B004WAIRRM/ref=sr_1_8?s=office-products&amp;ie=UTF8&amp;qid=1528252345&amp;sr=1-8&amp;keywords=sharpie+pens&amp;dpID=41ZV4Uzq13L&amp;preST=_SY300_QL70_&amp;dpSrc=srch">Sharpie Fine-Point Pens</a>  — Honestly, I could use a pen recommendation. I like felt-tip pens because they're easy to write with and they treat the paper better. I write sort of "hard," so ballpoint pens tend to dig into the paper and leave little depressions, but felt-tip pens don't do that. These Sharpie pens also don't bleed. But they don't last as long as I want them to, so if you have a felt-tip pen you like, let me know! </li>
</ul>
<p>I don't really use rulers or anything else. If I need a straight edge, for example, I generally just grab a book or a piece of mail and use that. I also don't index, but I do dog-ear pages that I think I'll need to refer to. I keep my notebooks and still refer back to them for how to do things. </p>]]></content:encoded><media:content type="image/jpeg" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1528251150469-99KA1PHLEREWTQG3CLOH/De4ponsVQAAK91Z.jpg?format=1500w" medium="image" isDefault="true" width="900" height="1200"><media:title type="plain">5 Reasons to Keep a Work Notebook</media:title></media:content></item><item><title>Free Talk Ideas for DjangoCon US 2018 </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Thu, 19 Apr 2018 22:45:29 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2018/4/19/2018-djangocon-us-talks-id-like-to-see</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5ad91901575d1f4a47b95500</guid><description><![CDATA[Every year for DjangoCon US, the organizers get asked about what kinds of 
talks we're looking for. I've got a list of free ideas for you! ]]></description><content:encoded><![CDATA[<p>Every year for <a href="https://2018.djangocon.us/">DjangoCon US</a>, the organizers get asked about what kinds of talks we're looking for. I can't speak for the rest of the organizing team, but for me it's been a wild year of learning new things. I've got a long list of topics I'd like to learn more about. </p>
<p>The wish list below is heavily influenced by the work I've been doing at my new job. What topics would you like to see onstage at DjangoCon US this year? Tweet us <a href="https://twitter.com/djangocon">@djangocon</a>! Then go <a href="https://www.papercall.io/djangocon-us-2018">submit your proposal</a>. </p>
<h2 id="django">Django</h2>
<p>I can always use a refresher on things like Django's built-in security features and other security stuff I should be enabling. Reminders about how to customize the admin are also helpful. Also, tell me what style rules I'm breaking! Is there an order my model methods should be in? Is it bad to name things <code>utils.py</code>? Help me organize my code, people. </p>
<h2 id="channels">Channels</h2>
<p>I'd love to see a Channels tutorial, as well as some talks on Channels. A beginners' talk would be fantastic, and I would welcome talks on Channels and testing or Channels with an app that scrapes an external API, or really any other "Channels and [topic]" talk.  </p>
<h2 id="django-rest-framework">Django REST Framework</h2>
<p>Intermediate/advanced talks, especially that go into Viewsets and actions (formerly <code>list_route</code> and <code>detail_route</code>). Using DRF with a frontend like React. Testing DRF. Serializers for users with different permissions and why you might want that, securing your serializer, etc. </p>
<h2 id="testing">Testing</h2>
<p>All the testing talks you can think of! But especially a talk that goes into deciding what to test, and a talk to teach me about mocking. </p>
<h2 id="containers">Containers</h2>
<p>I'm using a lot of Docker these days, so talks that get me beyond-the-basics would be welcome. I'd also like to learn some Kubernetes, and if someone could give a talk that got me started with that, I'd be grateful! Talks that touch on using other things — Celery, Pipenv, etc. — with Docker would be rad too. </p>
<h2 id="professional-skills">Professional Skills</h2>
<p>Topics like tips on debugging, how to write technical documentation (especially blog posts and tutorials), onboarding new team members, and code review tips for both the coder and the reviewer. </p>
<h2 id="wagtail">Wagtail</h2>
<p>I've just started using Wagtail, and I wound up in the not-shallow end. A talk that covered the basics (and the basic "gotchas"), and a talk that went into more advanced topics, would be the bee's knees. </p>
<hr>
<p>For other talk ideas, see:</p>
<ul>
<li><a href="https://twitter.com/webology">Jeff Triplett</a>'s <a href="https://jefftriplett.com/2017/django-talks-id-like-to-see/">2017</a> and <a href="https://jefftriplett.com/2016/djangocon-us-talks-id-like-to-see/">2016</a> posts</li>
<li><a href="https://twitter.com/saradgore">Sara Gore</a>'s 2018 <a href="https://gist.github.com/SaraDGore/3b998f94681c7f569491fd781dd59d98">Talks I want to see at DjangoCon US</a> gist</li>
</ul>]]></content:encoded><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1524177539443-8KSA40JPGQDVMPVSQMIB/avatar-main.png?format=1500w" medium="image" isDefault="true" width="1500" height="1500"><media:title type="plain">Free Talk Ideas for DjangoCon US 2018</media:title></media:content></item><item><title>Five for Friyay: Useful Python and Django Libraries </title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 19 Jan 2018 15:05:00 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2018/1/19/five-for-friyay-useful-python-and-django-libraries18</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5a4fe881419202379417a734</guid><description><![CDATA[Every day is a new adventure in a new job. I came into my job at REVSYS 
with not much production Python experience and my colleagues have been kind 
enough to share some time-saving and frustration-reducing libraries with me 
as I've been learning. This Friday, I'm sharing five libraries 
(technically, four libraries and a repo) that I've learned about in the 
last three months and fallen pretty much in love with. Enjoy!]]></description><content:encoded><![CDATA[<p>Every day is a new adventure in a new job. I came into my job at REVSYS with not much production Python experience and my colleagues have been kind enough to share some time-saving and frustration-reducing libraries with me as I've been learning. This Friday, I'm sharing five libraries (technically, four libraries and a repo) that I've learned about in the last three months and fallen pretty much in love with. Enjoy!</p>























<h2 id="1-python-dateutil-">1. <code>python-dateutil</code></h2>
<p>I shared <a href="https://dateutil.readthedocs.io/en/stable/"><code>python-dateutil</code></a> with a Slack channel of software engineers whose first programming language is not Python and the response was 🙌 and exclamations of "You mean I didn't need to spend hours fussing with <code>strptime</code>?!?"</p>
<p>This library does a lot of handy things, but the most important thing it does it take a string that contains some sort of date/time data and just <em>poof</em> make it into a DateTime object.</p>
<p>Using <code>strptime</code> to parse a DateTime string from an API, I had to slice the string because there was data I couldn't figure out how to get strptime to account for. Just getting this far took me more than an hour, and it still wasn't perfect:</p>
<pre><code>&gt;&gt; from datetime import datetime
&gt;&gt; date = datetime.strptime('2013-08-28T23:59:00-06:00'[:19], '%Y-%m-%dT%H:%M:%S')
&gt;&gt; date
datetime.datetime(2013, 8, 28, 23, 59)</code></pre><p>Using <code>python-dateutil</code> for the same thing:</p>
<pre><code>&gt;&gt; from dateutil.parser import parse
&gt;&gt; date = parse('2013-08-28T23:59:00-06:00')
&gt;&gt; date
datetime.datetime(2013, 8, 28, 23, 59, tzinfo=tzoffset(None, -21600))</code></pre><p>I get a much more accurate DateTime object! I wish I'd known about it a month ago, because <code>strptime</code> isn't that fun to use.</p>
<h2 id="2-django-test-plus-">2. <code>django-test-plus</code></h2>
<p>I'm pitching a <a href="https://www.revsys.com/">REVSYS</a> product here, but I really like it. <a href="https://github.com/revsys/django-test-plus"><code>django-test-plus</code></a> makes writing Django REST Framework tests a little bit easier. I turned this:</p>
<pre><code>class MyModelTestCase(TestCase):
   def test_list(self):        
       url = reverse('mymodel-list')

       # Non-logged-in users should not be able to see models
       response = self.client.get(url)
       self.assertEqual(response.status_code, 401)

       # Superusers can view models
       superuser = SuperUserFactory()
       with self.login(superuser):
           response = self.client.get(url)
           self.assertEqual(response.status_code, 200)</code></pre><p>into this:</p>
<pre><code>class MyModelTestCase(TestCase):
   def test_list(self):
       # Non-logged-in users should not be able to see models
       self.get('my-model-list')
       self.response_401()

       # Superusers can view models
       superuser = SuperUserFactory()
       with self.login(superuser):
           self.get_check_200('my-model-list')</code></pre><p>The library contains built-in methods for checking the major HTTP status codes using the standard HTTP methods (GET, POST, PUT, DELETE, etc.) and can save you a lot of keystrokes. <a href="https://twitter.com/fwiles">Frank Wiles</a>'s <a href="https://www.revsys.com/blog/2015/may/29/django-test-plus/">blog post</a> about using <code>django-test-plus</code> is pretty helpful, too.</p>
<h2 id="3-django-rest-swagger-">3. <code>django-rest-swagger</code></h2>
<p><a href="https://github.com/marcgibbons/django-rest-swagger"><code>django-rest-swagger</code></a> puts a prettier UI on your Django REST Framework APIs. The project ships with an example based on the Django REST Framework tutorial so you can see it in action right out of the box. It integrates your docstrings into the UI so your API's documentation is right there in the browser.</p>
<p><img src="https://marcgibbons.com/django-rest-swagger/img/ui-screenshot.png" alt="Screeshot of django-rest-swagger in the browser"></p>
<h2 id="4-django-click-">4. <code>django-click</code></h2>
<p>Write management commands for fun and profit with <a href="https://github.com/GaretJax/django-click"><code>django-click</code></a>! The documentation for this library is solid and it makes writing management commands really easy. I wind up using it a lot to generate and mess with test data in development. Here's a silly example management command that takes in your name and greets you:</p>
<pre><code># greeting.py
import djclick as click


@click.command()
@click.option('--name', help="Pass in your name", default='')
def command(name):
    print('Hi there', name)</code></pre><p>Now I can run <code>python manage.py greeting</code> and see "Hi there" in my console. Or, I can run <code>python manage.py greeting --name=Lacey</code> and see "Hi there Lacey." Let your imagination run wild with possibilities!</p>
<p>Thanks to <a href="https://twitter.com/webology">Jeff Triplett</a> for letting me know this library existed!</p>
<h2 id="5-styleguide-git-commit-message-">5. <code>styleguide-git-commit-message</code></h2>
<p>I'm cheating. The <a href="https://github.com/slashsBin/styleguide-git-commit-message">Git Commit Message StyleGuide</a> isn't a library and it isn't Django. It IS a style guide for writing commit messages that use semantic emoji. I've been integrating this style guide into my own Git workflow and not only do my commits feel more whimsical, I can also tell at a glance what I was doing in my commit history.</p>
<p><em>Thanks to <a href="https://twitter.com/webology">Jeff Triplett</a> for his advice on this post.</em></p>]]></content:encoded><media:content type="image/jpeg" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1516301969309-OLD5AII7HRXNBHSSH7HH/rawpixel-com-395556.jpg?format=1500w" medium="image" isDefault="true" width="1500" height="1001"><media:title type="plain">Five for Friyay: Useful Python and Django Libraries</media:title></media:content></item><item><title>2017 Reviewed</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Tue, 02 Jan 2018 19:13:56 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2018/1/2/2017-reviewed</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:5a4bcf0e71c10b82b8b555cf</guid><description><![CDATA[In the spirit of starting 2018 off with a more confident step, however, I 
kept the focus on the results of the year, which were often beautiful, 
inspiring, funny, and joyful. Here's to a new year and a new 365 days of 
memories. ]]></description><content:encoded><![CDATA[<p>When writing this post, it was a challenge to focus on the things I accomplished and not the stresses and anxieties I felt along the way. In the spirit of starting 2018 off with a more confident step, however, I kept the focus on the results of the year, which were often beautiful, inspiring, funny, and joyful. Here's to a new year and a new 365 days of memories.&nbsp;</p>


































































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg" data-image-dimensions="640x427" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=1000w" width="640" height="427" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514917690761-AWHMS2W5G4GJTIF5TPYE/36294609743_da3f0b97f4_z.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p>Photo by Adam Gregory, <a target="_blank" href="https://www.instagram.com/atomimages/">Atom Images</a>.&nbsp;</p>
          </figcaption>
        
      
        </figure>
      

    
  


  





  <h2>Chaired DjangoCon US&nbsp;</h2><p>Last year, I served as conference chair of <a target="_blank" href="https://2017.djangocon.us/">DjangoCon US</a>. I and more than a dozen other people spent months prepping, fundraising, reviewing proposals, making schedules, approving menus, and doing a million other small and large tasks so that more than 300 people could come together in Spokane to talk about Django for a week. Every conference has its fires that must be put out, but I'm so proud of this team and this conference. I know 2018 will be even better!&nbsp;</p>


































































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg" data-image-dimensions="640x828" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=1000w" width="640" height="828" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920196976-XCSFR3M9DLCFUUE8IN5F/20292857_10213939256223802_9213509342548768622_n.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p>Photo by my aunt, Elaina.</p>
          </figcaption>
        
      
        </figure>
      

    
  


  





  <h2>Rode in a bike race</h2><p>In July, I joined my mom, my sister, and my cousins in Buffalo Gap, Texas for the Tour de Gap. All I can say about my performance is that I finished those 27 miles. Feel free to google my race results; I'm not ashamed!&nbsp;</p><p>In all seriousness, this was an awesome experience. I prepped by attending a spinning class a couple times a week for a few weeks, I borrowed my cousin's old bike that came with the festive pink water bottle holder you see here, and I had a blast with my family. Hope I get to do it again this year!&nbsp;</p>


































































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg" data-image-dimensions="640x638" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=1000w" width="640" height="638" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920222938-KCRUJFA6CPXCR0Q2B5SM/21122364_10105200722018251_7170840859459418474_o.jpg?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
          
          <figcaption class="image-caption-wrapper">
            <p>Photo by me.&nbsp;</p>
          </figcaption>
        
      
        </figure>
      

    
  


  





  <h2>Started a new job</h2><p>While at DjangoCon US, I accepted a job offer from <a target="_blank" href="https://www.revsys.com/">REVSYS</a>.&nbsp;I started there in October and have absorbed so much new information in the last three months that every day it feels like there is not room in my brain to learn more, and yet somehow I do. Leaving the University of Texas after six years was incredibly hard; my colleagues there are amazing people and very talented developers. Luckily, I recruited several of them to work on DjangoCon US, so I still get to work with them! This photo is the celebration selfie I took with my husband after I accepted the offer.&nbsp;</p><h2>By some numbers&nbsp;</h2><ul><li>32: Outings with my Little through <a target="_blank" href="https://itsbigtime.org/">Big Brothers Big Sisters</a>&nbsp;</li><li>6: Blog posts. Four published here, and two on the <a target="_blank" href="https://www.revsys.com/tidbits/">REVSYS blog</a>.&nbsp;</li><li>2: Conference presentations. I spoke about <a target="_blank" href="https://speakerdeck.com/williln/with-a-little-help-from-my-network">networking</a> at ACT-W Portland and gave the <a target="_blank" href="https://www.youtube.com/watch?v=Bd52gyJaHzY&amp;index=43&amp;list=PL2NFhrDSOxgXmA215-fo02djziShwLa6T">closing remarks</a> at DjangoCon US.&nbsp;</li><li>8: Round-trip flights taken. I went to New York for a wedding;&nbsp;Texas for the bike race;&nbsp;Spokane, WA for DjangoCon US; Bismarck, ND for a family reunion; Texas to see family; Oklahoma for Thanksgiving; Lawrence, KS to visit REVSYS; and back to Texas for Christmas.&nbsp;</li><li>39: Books read. Favorites include <a target="_blank" href="https://smile.amazon.com/Kindred-Octavia-Butler/dp/0807083690/ref=sr_1_1?ie=UTF8&amp;qid=1514918930&amp;sr=8-1&amp;keywords=kindred+octavia+butler"><em>Kindred</em></a>&nbsp;by Octavia Butler,&nbsp;<a target="_blank" href="https://smile.amazon.com/Nightingale-Novel-Kristin-Hannah/dp/1250080401/ref=sr_1_1?s=books&amp;ie=UTF8&amp;qid=1514918943&amp;sr=1-1&amp;keywords=the+nightingale+kristin+hannah"><em>The Nightingale</em></a>&nbsp;by Kristin Hannah, and <a target="_blank" href="https://smile.amazon.com/Heart-Malice-Alice-Worth-1/dp/1944728341/ref=sr_1_1?s=books&amp;ie=UTF8&amp;qid=1514918960&amp;sr=1-1&amp;keywords=heart+of+malice"><em>Heart of Malice</em></a>&nbsp;by Lisa Edmonds.&nbsp;</li><li>1: Homes purchased. We bought a house! It's very cute and has a large yard and will feature heavily in my 2018 goals.&nbsp;</li><li>2: Jobs held. I started the year only a month into a new job at the University of Texas, and ended the year three months into my new job at REVSYS.&nbsp;</li><li>1,407: Tweets sent. Busiest month was August with 182 sent tweets, unsurprising since that was the month DjangoCon US happened. Slowest month was September with 26, when I took a post-conference social media break.&nbsp;</li></ul><h2>2018 Goals&nbsp;</h2><p>I hate to call these "resolutions," but I do have some personal and professional goals for this year.&nbsp;</p><p><em>Speaking</em>: Give a Docker talk at a conference. I've submitted this talk to a couple of conferences already, and have a couple more conferences I will submit it to once their CFPs open.&nbsp;</p><p><em>Growing</em>: Vegetables, that is. Plant a vegetable, grow it, and eat it. Now that I have a house with a yard, I want to garden again.&nbsp;</p><p><em>Writing</em>: I wrote and published 6 blog posts last year, so let's make it 12 this year. I'm part of a couple of different writing projects this year, so hopefully I will be able to keep pace. I also developed a journaling habit in 2017, and I'd like to keep that going.&nbsp;</p><p><em>Reading</em>: Read more. My goal for 2017 was a book a week, which I did not hit. Mostly this year I would like to be reading consistently, so my Goodreads goal is 40 books for 2018.&nbsp;</p>]]></content:encoded><media:content type="image/jpeg" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1514920487800-8BF8GGJMH4M9ASS0VNUH/36294609743_da3f0b97f4_z.jpg?format=1500w" medium="image" isDefault="true" width="640" height="427"><media:title type="plain">2017 Reviewed</media:title></media:content></item><item><title>TIL: How to configure SublimeText for prettier code</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Fri, 20 Oct 2017 15:21:03 +0000</pubDate><link>https://www.laceyhenschel.com/blog/2017/10/20/til-how-to-configure-sublimetext-for-prettier-code</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:59ea129edc2b4a46cf949b67</guid><description><![CDATA[This week I added a Python linter and an automatic editor configurator 
(configurer?) to SublimeText and I'm a lot happier.]]></description><content:encoded><![CDATA[<p>This week I added a Python linter and an automatic editor configurator (configurer?) to SublimeText and I'm a lot happier.</p><h1>Step 1: Install Package Control</h1><p>You will need to install <a href="https://packagecontrol.io/installation">Package Control</a>. The instructions in that link are very clear.</p><p>Once Package Control is installed, you can get to it by typing <strong>cmd</strong> + <strong>shift</strong> + <strong>P</strong> when in SublimeText.</p>


































































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png" data-image-dimensions="594x281" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=1000w" width="594" height="281" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512483564-EFAU2ZJ97FSVHN7ZZPXV/packagecontrol.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  





  <h1>Step 2: Add a linter</h1><p>A linter is a tool that checks for small mistakes in your code. <a href="http://flake8.pycqa.org/en/latest/">Flake8</a>, a Python linter, checks for PEP 8 violations (like trailing whitespace), unused imports, syntax errors, and other helpful stuff. (You might find <a href="http://eldarion.com/blog/2017/10/17/how-we-maintain-high-levels-code-quality/">Eldarion's post about clean code</a> helpful.)</p><p>Follow the steps in <a href="https://janikarhunen.fi/three-steps-to-lint-python-3-6-in-sublime-text.html">Three steps to lint Python 3.6 in Sublime Text</a> to get started. The linked post will walk you through the following:</p><ul><li><p>Installing Flake8 (<strong>pip install flake8</strong>)</p></li><li><p>Installing the SublimeLinter plugin (more detailed instructions <a href="https://sublimelinter.readthedocs.io/en/latest/about.html">in the docs</a>)</p></li><li><p>Installing the SublimeLinter-flake8 plugin</p></li><li><p>Configuring the linter</p></li></ul><p>Once you have done that, open a Python file and make a mistake on purpose. Save the file, and you will see your linter yell at you helpfully.</p>


































































  

    
  
    

      

      
        <figure class="
              sqs-block-image-figure
              intrinsic
            "
        >
          
        
        

        
          
            
          
            
                
                
                
                
                
                
                
                <img data-stretch="false" data-image="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png" data-image-dimensions="518x236" data-image-focal-point="0.5,0.5" alt="" data-load="false" elementtiming="system-image-block" src="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=1000w" width="518" height="236" sizes="(max-width: 640px) 100vw, (max-width: 767px) 100vw, 100vw" onload="this.classList.add(&quot;loaded&quot;)" srcset="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=100w 100w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=300w 300w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=500w 500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=750w 750w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=1000w 1000w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=1500w 1500w, https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512576899-C6KQQ944JDYPOTGO125Y/linter_example.png?format=2500w 2500w" loading="lazy" decoding="async" data-loader="sqs">

            
          
        
          
        

        
      
        </figure>
      

    
  


  





  <p>You can customize the ways your linter alerts you to code violations. See <a href="https://sublimelinter.readthedocs.io/en/latest/lint_modes.html">the docs</a> if you would prefer your linter to be a little less obnoxious than mine is.</p><h1>Step 3: Install EditorConfig</h1><p>My Python sin is that I'm overly attached to whitespace at the end of lines. I blame the English degrees. It's ingrained in me to type the spacebar at the end of anything I do, and it's a habit that's so hard to break.</p><p>Within a few days of installing a linter plugin for SublimeText, I was getting pretty sick of the linter pointing out my love of trailing whitespace and I joked about it to my colleague Jeff. That's when he told me about <a href="http://editorconfig.org/">EditorConfig</a>. You install the EditorConfig plugin for SublimeText, create an <strong>.editorconfig</strong> file, and your editor will automatically correct certain kinds of errors for you.</p><ol><li><p>In SublimeText, use Package Control to search for "Install" and select "Package Control: Install Package."</p></li><li><p>In the next modal that opens, search for "EditorConfig." Click on it.</p></li><li><p>Nothing will happen, but EditorConfig is now installed.</p></li><li><p>At the top level of a project, create a file called <strong>.editorconfig</strong>.</p></li><li><p>Tell the file what you want your editor to do. There are <a href="https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties">a ton of properties</a> you can set. Here's the <a href="https://github.com/djangocon/2017.djangocon.us/blob/master/.editorconfig">DjangoCon 2017 .editorconfig</a> for reference:</p></li></ol><pre># http://editorconfig.org
root = true

[*]
indent_style = space // Converts tabs to spaces
indent_size = 4 // 4 spaces per tab 
end_of_line = lf // Helps keep Windows, Mac, Linux on the same page since they handle end of line differently
charset = utf-8
trim_trailing_whitespace = true // Auto-trims trailing whitespace
insert_final_newline = true // Auto-adds a blank newline to the end of a file

[*.scss]
indent_size = 2 // More common to see 2 spaces in SCSS, HTML, and JS 

[*.html]
indent_size = 2

[*.js]
indent_size = 2</pre><p>As soon as you save that file, test it out. Add a whole bunch of trailing whitespace to a line of code, save the file, and watch your whitespace disappear without your linter ever knowing it was there.</p><h1>Final notes</h1><p>There are other linters out there. My colleagues mentioned <a href="https://damnwidget.github.io/anaconda/">Anaconda</a> (turns SublimeText into a Python IDE and comes with linting), <a href="https://sublimelinter.readthedocs.io/en/latest/index.html">SublimeLinter</a> without Flake8, and <a href="https://github.com/biermeester/Pylinter">PyLinter</a>. I'm not overly attached to the one I'm using because it's about a week old for me. Feel free to experiment.</p><p><em>Huge thanks to <a href="https://twitter.com/webology">Jeff Triplett</a> for proofreading this post, introducing me to these tools, and making my day job a little easier.</em></p>]]></content:encoded><media:content type="image/png" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1508512898322-VYC5LFOZP72L26DWMAGJ/Screen+Shot+2017-10-19+at+9.47.50+AM.png?format=1500w" medium="image" isDefault="true" width="518" height="236"><media:title type="plain">TIL: How to configure SublimeText for prettier code</media:title></media:content></item><item><title>How does working remotely work?</title><dc:creator>Lacey Willliams Henschel</dc:creator><pubDate>Thu, 31 Aug 2017 00:08:22 +0000</pubDate><link>https://www.laceyhenschel.com/blog/remote-work</link><guid isPermaLink="false">5897f186d482e95fd8760b8b:58a21ab31b10e3be5b09e8b3:59a7507803596eb951d22f71</guid><description><![CDATA[Because I have worked remotely full-time for the past three years and it 
suits me pretty well, people ask me a lot about how working remotely works 
in practice. How did you get your boss on board? Do you really wear pajamas 
all day? What about loneliness? Read on.]]></description><content:encoded><![CDATA[<p>Because I have worked remotely full-time for the past three years and it suits me pretty well, people ask me a lot about how working remotely works in practice. All of the advice here is just that: advice. These things have worked for me or been true for me, but they might not work for you. Take what works and leave what doesn't, just like in the cafeteria.</p><h2>How did you get your boss on board?</h2><p>When I first started telecommuting full-time, I was in a situation where I was moving no matter what (so my spouse could go to grad school), so my company was faced with either letting me quit or letting me be remote.&nbsp;</p><p>A couple of things helped:</p><ul><li>I gave my organization a lot of notice that I would be moving and would like to work remotely. Several months of lead time helped us all get answers to questions and make a solid plan.</li><li>I agreed to a trial period of six months. If after six months things weren't working out, then we'd go from there. (But I was pretty sure things would work out.)</li><li>While I was the first person on my team to go remote, I was not the first person in my whole company. I was able to give my supervisor the names of some other people who worked remotely so he could do his own research.</li></ul><h2>Has working remotely stalled your career?</h2><p>Not at all. I've been promoted, transitioned to a new department, and accepted a job with a new company all while working remotely. &nbsp;</p><p>I was initially concerned that working remotely, especially as the only person on my team who was remote full-time, would mean I would get left behind professionally because I would be out of sight and thus out of mind. I assumed that I would only work remotely for a year or two, and then I would need to get an in-person job in order to progress professionally. None of that happened, and now I'm so attached to remote work that I don't want to be back in an office full-time again.</p><h2>Do you wear pajamas and watch Netflix all day?</h2><p>Pajamas: Yes. Netflix: No.</p><p>Truth be told, the first couple of weeks that I was remote I had also just moved, so I was unpacking boxes and settling into a new routine in a new part of the country. So yes, there was more Netflix during the workday than I generally recommend. "My office is my sofa and I won't be distracted with a little Grey's Anatomy in the background!" I thought. But I quickly, quickly discovered that I need more structure than that.</p><p>So work happens in my "office," which is really just a little space off the dining room, and Netflix stays off until after work. But I don't have to wear headphones when I listen to Spotify because the cats don't care!</p><p>But there is no reason to wear pants that are not pajamas ever again if you work from home. Embrace the comfort.</p><h2>Don't you get lonely?</h2><p>Not really. I'm in a lot of Slacks, I schedule weekly Skype sessions or Hangouts with my colleagues, and I pair program pretty regularly. My non-work social life is healthier, too; since I'm not commuting, I'm not too tired in the evenings to catch a movie with a friend or go to happy hour!</p><p>I've always been the only person who was remote full-time on my team, but most of my teammates have worked from home at least one or two days a week. Still, being the only remote person on the team can result in some FOMO. Your colleagues can Skype you in to happy hour, for example, but it's not the same. Especially in those moments where work is emotionally difficult (layoffs, an emergency, bad news), it can feel extra isolating to not be able to have those water-cooler conversations.</p><p>But active Slack channels (with coworkers, people in your organization but on other teams, people in your profession, people who love cats as much as you do) can help you fill the gap left by the absence of the work water cooler.</p><h2>What do you do for lunch?</h2><p>I cook for myself or pour a bowl of cereal or walk to Subway. I generally spend less money on eating out for lunch than I did when I was in the office.</p><h2>What is your day like?</h2><p>I wake up around 7, have coffee, and take a shower. I'm usually online by 8:30, but I check Slack and email before then to make sure there are no fires to put out. If I have any meetings, they are generally between 10-1 my time (but I do have a rule that if it isn't at least 9 a.m. in Oregon, I don't have to turn my video camera on).</p><p>I take about a half hour for lunch sometime between 12 and 2, and then keep working until 5 or 6 in the evening. I generally hit my most productive "stride" from 2-5. Since I'm in Oregon and my colleagues are on central time, by 2 p.m. most of the emails have stopped and the meetings are over, so I'm no longer getting interrupted.</p><p>Depending on the day's schedule, I might take small breaks to pick up the house, make more coffee, or check the mail. When I was in an office, I took breaks to take walks, hit the vending machine, or see if a colleague wanted to get a Frosty with me, so this works out to about the same amount of time. I might also take a longer lunch to work out.</p><h2>What about traveling or when someone else is home?</h2><p>I am generally a fan of "vacation time is vacation time and work time is work time," but I've worked from Texas, New Mexico, Mississippi, Oklahoma, and California before! Once, for example, I was giving two conference talks in Texas a week apart, so flying back to Oregon in between didn't make much sense. But I also didn't want to take extra vacation time, so I worked from my mom's dining room for the week in between. I do know people who combine remote work with vacation time and that works really well for them. My one caveat is to make sure your team knows where you are (specifically what timezone you're in) and whether you'll be working half-time that week. Setting expectations is important.</p><p>Speaking of setting expectations, if you share your house with roommates, family members, or a significant other, it's important to let them know that your work time needs to be respected. What that means will depend on the situation, but it's always good to have an extra pair of headphones.</p><h2>What about when your internet goes down?</h2><p>Have a backup plan--a nearby coffee shop or library--if you lose your connection at home.</p><h2>What else should I know?</h2><p>There have been times I have felt pressure to be available on Slack, email, Skype, or phone. I have felt guilty for turning off those services so I could focus, and then having missed a call or message from a colleague with a question. I don't have a great answer for that except… don't feel that way!</p><p>One of the most common pieces of productivity advice is to limit the amount of time per day you spend in your email and Slack, and then <a href="https://www.entrepreneur.com/article/254432">close your email entirely</a> outside of those times. Being constantly available can have a very negative effect on what you can accomplish, and in turn negatively impact how you feel about yourself. So if you missed a phone call because you silenced your phone so you could finally squish that bug, don't worry about it.</p><p>Some colleagues of mine also follow the <a href="https://en.wikipedia.org/wiki/Pomodoro_Technique">pomodoro technique</a> (25 minutes on, 5 minutes off) and let their teams know when they're starting a pomodoro and closing all their chat windows. That way everyone knows they'll be slower to respond to things.</p><p>Working remotely is also great for your wallet and the environment!&nbsp;</p><p><em>Thank you to <a href="https://twitter.com/rebeccakindschi">Rebecca Kindschi</a> for her help with this post! Photo from <a target="_blank" href="https://unsplash.com/collections/829879/computers-remote-work?photo=3iT3dnmblGE">Unsplash</a>.</em></p>]]></content:encoded><media:content type="image/jpeg" url="https://images.squarespace-cdn.com/content/v1/5897f186d482e95fd8760b8b/1504138199601-XKR2YSFI7IWW2YT0SEXN/mikayla-mallek-219946.jpg?format=1500w" medium="image" isDefault="true" width="1500" height="1000"><media:title type="plain">How does working remotely work?</media:title></media:content></item></channel></rss>