├── .env.dist
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── bin
├── git
└── test
├── docs
├── lab1.md
├── lab2.md
└── project_overview.md
├── manage.py
├── project
├── __init__.py
├── config
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── data
│ ├── __init__.py
│ ├── apps.py
│ ├── author.py
│ ├── category.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ └── fake_data.py
│ ├── markdown.py
│ ├── subscribers.py
│ └── subscription_notifications.py
├── newsletter
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ ├── __init__.py
│ │ │ ├── send_notifications.py
│ │ │ └── test_send_notifications.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_remove_post_is_draft_post_is_published_and_more.py
│ │ ├── 0003_post_is_public_post_summary.py
│ │ ├── 0004_alter_subscription_options_and_more.py
│ │ ├── 0005_auto_20220809_0012.py
│ │ ├── 0006_alter_category_options_alter_subscription_categories.py
│ │ ├── 0007_alter_subscriptionnotification_options_and_more.py
│ │ ├── 0008_alter_post_categories.py
│ │ ├── 0009_post_open_graph_description_post_open_graph_image.py
│ │ ├── 0010_subscriptionnotification_read.py
│ │ ├── 0011_alter_post_options_remove_category_category_unq_slug_and_more.py
│ │ ├── 0012_alter_category_created_alter_post_created_and_more.py
│ │ └── __init__.py
│ ├── models.py
│ ├── operations.py
│ ├── receivers.py
│ ├── syndication.py
│ ├── templatetags
│ │ ├── __init__.py
│ │ └── newsletter_utils.py
│ ├── test.py
│ ├── test_admin.py
│ ├── test_forms.py
│ ├── test_models.py
│ ├── test_operations.py
│ ├── test_receivers.py
│ ├── test_syndication.py
│ ├── test_templatetags.py
│ ├── test_views.py
│ ├── urls.py
│ └── views.py
├── templates
│ ├── 403.html
│ ├── 404.html
│ ├── 500.html
│ ├── base.html
│ ├── django
│ │ └── forms
│ │ │ ├── p.html
│ │ │ └── widgets
│ │ │ └── select.html
│ ├── inclusion_tags
│ │ └── nice_datetime.html
│ ├── landing.html
│ ├── posts
│ │ ├── detail.html
│ │ ├── includes
│ │ │ └── list_item.html
│ │ └── list.html
│ ├── registration
│ │ ├── activate.html
│ │ ├── activation_complete.html
│ │ ├── activation_complete_admin_pending.html
│ │ ├── activation_email.html
│ │ ├── activation_email.txt
│ │ ├── activation_email_subject.txt
│ │ ├── admin_approve.html
│ │ ├── admin_approve_complete.html
│ │ ├── admin_approve_complete_email.html
│ │ ├── admin_approve_complete_email.txt
│ │ ├── admin_approve_complete_email_subject.txt
│ │ ├── admin_approve_email.html
│ │ ├── admin_approve_email.txt
│ │ ├── admin_approve_email_subject.txt
│ │ ├── login.html
│ │ ├── logout.html
│ │ ├── password_change_done.html
│ │ ├── password_change_form.html
│ │ ├── password_reset_complete.html
│ │ ├── password_reset_confirm.html
│ │ ├── password_reset_done.html
│ │ ├── password_reset_email.html
│ │ ├── password_reset_form.html
│ │ ├── registration_base.html
│ │ ├── registration_closed.html
│ │ ├── registration_complete.html
│ │ ├── registration_form.html
│ │ ├── resend_activation_complete.html
│ │ └── resend_activation_form.html
│ ├── staff
│ │ ├── analytics.html
│ │ └── post_form.html
│ └── subscription
│ │ └── update.html
└── tests
│ ├── __init__.py
│ └── runner.py
├── requirements.in
├── requirements.txt
├── setup.cfg
└── static
├── .gitkeep
└── fomantic
├── fomantic-ui-2.8.8.semantic.min.css
├── fomantic-ui-2.8.8.semantic.min.js
├── jquery.min.js
└── themes
├── basic
└── assets
│ └── fonts
│ ├── icons.eot
│ ├── icons.svg
│ ├── icons.ttf
│ └── icons.woff
├── default
└── assets
│ ├── fonts
│ ├── brand-icons.eot
│ ├── brand-icons.svg
│ ├── brand-icons.ttf
│ ├── brand-icons.woff
│ ├── brand-icons.woff2
│ ├── icons.eot
│ ├── icons.svg
│ ├── icons.ttf
│ ├── icons.woff
│ ├── icons.woff2
│ ├── outline-icons.eot
│ ├── outline-icons.svg
│ ├── outline-icons.ttf
│ ├── outline-icons.woff
│ └── outline-icons.woff2
│ └── images
│ └── flags.png
├── github
└── assets
│ └── fonts
│ ├── octicons-local.ttf
│ ├── octicons.svg
│ ├── octicons.ttf
│ └── octicons.woff
└── material
└── assets
└── fonts
├── icons.eot
├── icons.svg
├── icons.ttf
├── icons.woff
└── icons.woff2
/.env.dist:
--------------------------------------------------------------------------------
1 | DEBUG=True
2 | SECRET_KEY='django-insecure-#3*_ltg+zl_f!q=t^+x#n-inv7d1@e#i9b-nb328&a0u52z5xp'
3 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - lab-1.1
8 | - lab-1.2
9 | - lab-1.3
10 | - lab-2.1
11 | - lab-2.2
12 | - lab-2.3
13 | - lab-2.4
14 | pull_request:
15 |
16 | jobs:
17 | tests:
18 | name: Tests Python ${{ matrix.python-version }} on ${{ matrix.os }}
19 | runs-on: ${{ matrix.os }}
20 |
21 | strategy:
22 | matrix:
23 | os: [ubuntu-latest, windows-latest, macos-latest]
24 | python-version: ['3.11', '3.12', '3.13']
25 |
26 | env:
27 | SECRET_KEY: very-unstrustworthy
28 |
29 | steps:
30 | - uses: actions/checkout@v4
31 |
32 | - uses: actions/setup-python@v5
33 | with:
34 | python-version: ${{ matrix.python-version }}
35 | cache: pip
36 | cache-dependency-path: 'requirements.txt'
37 |
38 | - name: Install dependencies
39 | run: |
40 | python -m pip install --upgrade pip setuptools wheel
41 | python -m pip install -r requirements.txt
42 |
43 | - name: Run tests
44 | run: |
45 | python -m manage test
46 |
47 | - name: Run lab tests
48 | run: |
49 | python -m manage test --tag lab_test
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Django static and media directories
2 | staticfiles/
3 | media/
4 |
5 | # PyCharm project files
6 | .idea/
7 |
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | wheels/
30 | pip-wheel-metadata/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .nox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *.cover
57 | *.py,cover
58 | .hypothesis/
59 | .pytest_cache/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Django stuff:
66 | *.log
67 | local_settings.py
68 | db.sqlite3
69 | db.sqlite3-journal
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 |
120 | # Spyder project settings
121 | .spyderproject
122 | .spyproject
123 |
124 | # Rope project settings
125 | .ropeproject
126 |
127 | # mkdocs documentation
128 | /site
129 |
130 | # mypy
131 | .mypy_cache/
132 | .dmypy.json
133 | dmypy.json
134 |
135 | # Pyre type checker
136 | .pyre/
137 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v5.0.0
4 | hooks:
5 | - id: check-yaml
6 | - id: end-of-file-fixer
7 | - id: trailing-whitespace
8 | - id: mixed-line-ending
9 | - repo: https://github.com/pycqa/flake8
10 | rev: 7.2.0
11 | hooks:
12 | - id: flake8
13 | - repo: https://github.com/pycqa/doc8
14 | rev: v1.1.2
15 | hooks:
16 | - id: doc8
17 | - repo: https://github.com/asottile/pyupgrade
18 | rev: v3.20.0
19 | hooks:
20 | - id: pyupgrade
21 | args: [--py38-plus]
22 | - repo: https://github.com/adamchainz/django-upgrade
23 | rev: 1.25.0
24 | hooks:
25 | - id: django-upgrade
26 | args: [--target-version, "3.2"]
27 | - repo: https://github.com/pycqa/isort
28 | rev: 6.0.1
29 | hooks:
30 | - id: isort
31 | - repo: https://github.com/pre-commit/pygrep-hooks
32 | rev: v1.10.0
33 | hooks:
34 | - id: python-check-blanket-noqa
35 | - id: python-check-mock-methods
36 | - id: python-no-eval
37 | - id: python-no-log-warn
38 | - repo: https://github.com/psf/black
39 | rev: 25.1.0
40 | hooks:
41 | - id: black
42 | language_version: python3
43 | entry: black --target-version=py38
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # "It doesn't work" - A Djangonaut's Debugging Toolkit
2 |
3 | Welcome to the tutorial! I really appreciate the time you've spent so far
4 | and the time you spend in the future on this tutorial.
5 |
6 | The purpose of this tutorial is to expose you to some of the tools
7 | available to you as a Djangonaut to aid you in your debugging adventures.
8 |
9 | If you didn't attend the talk or would like to view the slides, [you
10 | can find them here](https://docs.google.com/presentation/d/1dmeFD5kGsukQaMinU0BJQyyCeu5ZGR4drc0pJilT10Y/edit?usp=share_link).
11 |
12 | ## Preamble to the setup
13 |
14 | This tutorial is written to give you the experience of working on a real
15 | project. There are tests and adjacent functionality. The tests are written
16 | so that they all pass for each lab, which means they confirm the bugs you
17 | will be finding.
18 |
19 | I suggest reviewing the [Project Overview page](docs/project_overview.md)
20 | which has high level information on the models. It should make it easier
21 | to understand the project.
22 |
23 | Regarding debugging methods, a very useful one is to bisect commits or
24 | find the last known good version, then compare what doesn't work. That
25 | is very easy to do in this environment, but I strongly suggest that you
26 | don't do that unless you're stuck.
27 |
28 | ## How the Labs will work
29 |
30 | There are a total of 7 labs, numbered 1.1-1.3, 2.1-2.4 (it made sense at
31 | the time). Each is structured with a Report, a set of Facts, an
32 | Investigation section, a Conclusion and finally Further Consideration.
33 |
34 | I suggest using the Investigation section, but if you'd like to avoid any
35 | hints in your debugging effort then stop reading after the Report and Facts
36 | sections.
37 |
38 | The Conclusion section will have the solution(s) while Further Consideration
39 | is reserved for asking you to reflect on your own projects.
40 |
41 |
42 | ## Getting Setup
43 |
44 | This section will provide you with the minimal setup for this tutorial.
45 | If you have a preference on setting up a project differently and know that
46 | it works, go for it! Do what you're most comfortable.
47 |
48 | 1. Have Python 3.11+ installed. My assumption is that you have your own
49 | setup decided upon. However, if you're unsure where to go for
50 | this, I'd recommend using https://www.python.org/downloads/ for today.
51 | 1. From the terminal or command prompt, clone the project.
52 | ```shell
53 | git clone git@github.com:tim-schilling/debug-tutorial.git
54 | ```
55 | If you don't have a SSH key setup, you can use:
56 | ```shell
57 | git clone https://github.com/tim-schilling/debug-tutorial.git
58 | ```
59 | If you don't have git installed, you can
60 | [download the zip file](https://github.com/tim-schilling/debug-tutorial/archive/refs/heads/main.zip)
61 | and unpack it. Be aware, though, that you won't be able to complete some
62 | of the instructions in the lab without access to the git history. Cloning
63 | the repo is recommended.
64 | 1. Change into project directory
65 | ```shell
66 | cd debug-tutorial
67 | ```
68 | If you downloaded the zip file, you'll need to ``cd`` to the location where
69 | you unpacked the archive.
70 |
71 |
72 | ---
73 |
74 | ### Windows warning
75 |
76 | If you are comfortable on Windows with Python and have setup multiple projects, ignore this
77 | warning. Do what's comfortable.
78 |
79 | If not, I highly recommend using the Windows Subsystem for Linux
80 | ([docs](https://learn.microsoft.com/en-us/windows/wsl/about)). If you do, the
81 | rest of the instructions will work for you. If you don't have access to that
82 | please scroll down to [Windows non-WSL Setup](#windows-non-wsl-setup).
83 |
84 | ---
85 |
86 | ## MacOS, Linux, Windows WSL Setup
87 |
88 | 1. From the terminal, create a virtual environment. ([venv docs](https://docs.python.org/3/library/venv.html#venv-def))
89 | ```shell
90 | python -m venv venv
91 | ```
92 | 1. Active the virtual environment.
93 | ```shell
94 | source venv/bin/activate
95 | ```
96 | 1. Install the project dependencies.
97 | ```shell
98 | pip install -r requirements.txt
99 | ```
100 | 1. Create a new .env file.
101 | ```shell
102 | cp .env.dist .env
103 | ```
104 | If you use this for literally anything but this tutorial, please
105 | change the ``SECRET_KEY`` value in your ``.env`` file.
106 | 1. Create the database for the project.
107 | ```shell
108 | python manage.py migrate
109 | ```
110 | 1. Verify the tests currently pass, if they don't and you're not sure why,
111 | please ask.
112 | ```shell
113 | python manage.py test
114 | ```
115 | 1. Create the fake data. This will take a few minutes.
116 | ```shell
117 | python manage.py fake_data
118 | ```
119 | 1. Create your own super user account. Follow the prompts.
120 | ```shell
121 | python manage.py createsuperuser
122 | ```
123 | 1. Run the development web server.
124 | ```shell
125 | python manage.py runserver
126 | ```
127 | 1. Verify the following pages load:
128 | * http://127.0.0.1:8000/
129 | * http://127.0.0.1:8000/p/
130 | * http://127.0.0.1:8000/p/author-up-language-push-162/
131 | 1. Log into the admin ([link](http://127.0.0.1:8000/admin/)) with your super user.
132 | 1. Verify the following pages load:
133 | * http://127.0.0.1:8000/post/create/
134 | * http://127.0.0.1:8000/analytics/
135 |
136 | **BOOM!** You're done with the setup. Now that we've accomplished
137 | the boring stuff, let's get to the dopamine rushes. I mean the bugs.
138 |
139 | Proceed to [Lab 1](docs/lab1.md).
140 |
141 |
142 | ## Windows non-WSL Setup
143 |
144 | 1. From the command prompt, create a virtual environment.
145 | ```shell
146 | python3 -m venv venv
147 | ```
148 | 1. Activate the project
149 | ```shell
150 | .\venv\Scripts\activate
151 | ```
152 | 1. Install the project dependencies.
153 | ```shell
154 | pip install -r requirements.txt
155 | ```
156 | 1. Create a new .env file.
157 | ```shell
158 | copy .env.dist .env
159 | ```
160 | If you use this for literally anything but this tutorial, please
161 | change the ``SECRET_KEY`` value in your ``.env`` file.
162 | 1. Create the database for the project.
163 | ```shell
164 | python -m manage migrate
165 | ```
166 | 1. Verify the tests currently pass, if they don't and you're not sure why,
167 | please ask.
168 | ```shell
169 | python -m manage test
170 | ```
171 | 1. Create the fake data. This will take a few minutes.
172 | ```shell
173 | python -m manage fake_data
174 | ```
175 | 1. Create your own super user account. Follow the prompts.
176 | ```shell
177 | python -m manage createsuperuser
178 | ```
179 | 1. Run the development web server.
180 | ```shell
181 | python -m manage runserver
182 | ```
183 | 1. Verify the following pages load:
184 | * http://127.0.0.1:8000/
185 | * http://127.0.0.1:8000/p/
186 | * http://127.0.0.1:8000/p/author-up-language-push-162/
187 | 1. Log into the admin ([link](http://127.0.0.1:8000/admin/)) with your super user.
188 | 1. Verify the following pages load:
189 | * http://127.0.0.1:8000/post/create/
190 | * http://127.0.0.1:8000/analytics/
191 |
192 | Proceed to [Lab 1](docs/lab1.md).
193 |
--------------------------------------------------------------------------------
/bin/git:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # To rebase on main, run:
4 | # ./bin/git 'rebase main'
5 |
6 | branches=$(eval "git branch | grep 'lab' | xargs")
7 |
8 | git checkout main
9 | for branch in $branches
10 | do
11 | eval "git $1 ${branch}"
12 | done
13 | git checkout main
14 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | branches=$(eval "git branch | grep 'lab' | xargs")
4 |
5 | git checkout main
6 | for branch in $branches
7 | do
8 | echo "Testing branch ${branch}"
9 | eval "git checkout ${branch}"
10 | python manage.py test
11 | python manage.py test --tag lab_test
12 | done
13 | git checkout main
14 |
--------------------------------------------------------------------------------
/docs/lab1.md:
--------------------------------------------------------------------------------
1 |
2 | # Lab 1
3 |
4 | Let's put those basics to the test.
5 |
6 | ## Lab 1.1
7 |
8 | Change to the correct branch:
9 |
10 | ```shell
11 | git checkout lab-1.1
12 | ```
13 |
14 | ### Report
15 |
16 | The detail view for the post "Author up language push." is broken.
17 |
18 | To reproduce:
19 | 1. Browse to the [posts page](http://127.0.0.1:8000/p/).
20 | 1. Click on "Read" for the post with the title "Author up language push."
21 | 1. "It doesn't work!"
22 |
23 | ### Facts
24 |
25 | Let's consider what we know:
26 |
27 | - The error message is ``Post matching query does not exist.``, implying the
28 | QuerySet does not contain a Post matching the filters.
29 | - The line that causes the error is on line 80:``post = posts.get(title=lookup)``
30 | - We know the post exists, we can find it in the
31 | [admin](http://127.0.0.1:8000/admin/newsletter/post/?q=Author+up+language+push.)
32 | - This impacts more than just the post in the report. The detail
33 | view is broken for all posts.
34 |
35 |
36 | ### Investigation
37 |
38 | - What is being used to look up the Post instance?
39 | - Where is that lookup value coming from?
40 | - How does the link generate the URL to supply that lookup value?
41 |
42 |
43 | ### Conclusion
44 |
45 | In this scenario the Post's slug field is being used to generate the
46 | URL, while the view expects the title to be passed in.
47 |
48 | This is an example of a bug in which the error report provides the majority
49 | of the information we need, but we have to read closely and correctly
50 | interpret the information. Try to avoid making assumptions about what you
51 | expect the error to be. Sometimes we'll see an exception type and think,
52 | "Oh, that obviously has to be in XYZ." But upon reading the actual error
53 | message and looking at the stacktrace we discover it's from ABC.
54 |
55 | To solve this, we should pass ``post.slug`` into the calls for generating
56 | ``newsletter:view_post``. I'd also argue that the URL parameter should be
57 | renamed to ``slug`` to be more explicit about what is expected.
58 |
59 | For example, it's easier to spot the bug in this code during a code review:
60 |
61 | ```python
62 | post = posts.get(title=slug)
63 | ```
64 |
65 | than
66 |
67 | ```python
68 | post = posts.get(title=lookup)
69 | ```
70 |
71 | ### Further consideration
72 |
73 | This view uses ``post = posts.get(...)`` to fetch a single instance. When
74 | that doesn't exist, it's resulting in an uncaught exception which causes a 500
75 | error. Instead we should use ``post = get_object_or_404(posts, ...)`` to get a
76 | more friendly and less noisy 404 response. But consider what this error report
77 | would look like if that's how the code worked; we wouldn't have an error message
78 | or a stacktrace. We simply would see that the view is returning our HTTP 404 template
79 | implying the Post doesn't exist when we know it does.
80 |
81 | What I'd like you to think about for a minute is how would you approach this problem
82 | in that scenario (the view is returning a 404 response when it shouldn't)?
83 |
84 |
85 | ## Lab 1.2
86 |
87 | This lab covers a very common error, but hard to diagnosis when it's an
88 | [unknown unknown](https://www.techtarget.com/whatis/definition/unknown-unknown).
89 |
90 | Change to the correct branch:
91 |
92 | ```shell
93 | git checkout lab-1.2
94 | ```
95 |
96 | ### Report
97 |
98 | Creating posts is broken. The open graph image doesn't get uploaded!
99 |
100 |
101 | To reproduce:
102 | 1. Browse to the [create post page](http://127.0.0.1:8000/post/create/).
103 | 1. Fill out the form fields with some filler information.
104 | 1. Select an image for "Open graph image"
105 | 1. Click save.
106 | 1. The update form does not contain the file for the Open graph image field.
107 | 1. "It doesn't work!"
108 |
109 | ### Facts
110 |
111 | Let's consider what we know:
112 |
113 | - The form is hitting the correct view and is using the form class we
114 | expect since the post is created with the non-file fields.
115 |
116 |
117 | ### Investigation
118 |
119 | - Does the file show up on the server side?
120 | - Using the IDE's debugger, a ``breakpoint()`` line or a print statement, inspect
121 | [``request.FILES``](https://docs.djangoproject.com/en/4.1/ref/request-response/#django.http.HttpRequest.FILES).
122 | - Are there any files included? Is ``"open_graph_image"`` a key?
123 | - Is the file being sent from the browser to the server?
124 | - We can use the browser's Developer Tool's Network panel to inspect the request.
125 | - Browse to the [create view](http://127.0.0.1:8000/post/create/).
126 | - Open the developer tools, click on the networking panel.
127 | - Populate and submit the form.
128 | - Look for the image content in the request.
129 | - What value is being sent? Does it look like a file or the name of the file?
130 | - Can we create a Post with an Open graph image [in the admin](http://127.0.0.1:8000/admin/newsletter/post/add/)?
131 | - What is different between creating a Post in the admin versus
132 | creating a Post in our own application?
133 | - Compare the rendered ```` elements for ``open_graph_image``.
134 | - Compare the containing ``form`` element on the for those inputs.
135 |
136 | ### Conclusion
137 |
138 | In this scenario the ``