├── requirements.txt ├── mkdocs.yml ├── README.md ├── .readthedocs.yaml ├── docs ├── training-reference │ ├── contributing.md │ └── project-setup.md ├── index.md └── features │ ├── signals.md │ ├── class-based-views.md │ └── contrib.messages.md ├── LICENSE └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "Advanced Django Training" 2 | theme: readthedocs 3 | site_url: "https://django-advanced-training.readthedocs.io" 4 | repo_url: "https://github.com/chadgh/django-advanced-training" 5 | site_description: "Advanced training for the Django web framework." 6 | site_author: "Chad G. Hansen" 7 | copyright: "© 2017 Chad G. Hansen" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-advanced-training 2 | 3 | See [django-advanced-training](http://django-advanced-training.readthedocs.io). 4 | 5 | ## Contributing 6 | 7 | Please feel free to contribute PRs to add advanced training topics, correct any errors, or update according to new Django best practices or implementation changes. All contributions are welcome. 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: requirements.txt 21 | -------------------------------------------------------------------------------- /docs/training-reference/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Django Advanced Training 2 | 3 | Contributions to this project are welcomed and encouraged. If you find information that is incorrect or you want to see a feature added to the training material that isn't there yet, submit a pull request to the [github repo](https://github.com/chadgh/django-advanced-training) for django-advanced-training (the training material/documentation). 4 | 5 | You can also submit pull requests to the Django project that goes along with this training material at its [github repo](https://github.com/chadgh/django-advanced-training-project). 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chad G Hansen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Advanced Django Training 2 | 3 | This training is intended for professional developers who are already somewhat familiar with Python and Django. If you are not familiar with these, make sure to complete the [Python](https://docs.python.org/3/tutorial/) and [Django](https://docs.djangoproject.com/en/1.11/intro/tutorial01/) tutorials before using this training material. 4 | 5 | [Python](https://python.org) is a general-purpose programming language. [Django](https://djangoproject.com) is a powerful web development framework built on top of Python. 6 | 7 | This training is intended to help Django developers learn a little more deeply about some of the features Django has to offer. The format should allow you to get the information you are wanting to learn about quickly. There is also a basic Django project that you can [clone and get setup](training-reference/project-setup.md) to have a place to play around with the concepts and ideas that are taught here. 8 | 9 | After you have setup the training project, or if you have decided not to, you can jump to one of the features below to start learning more about that feature of Django. 10 | 11 | # Django Features 12 | 13 | * [Class-based Views](features/class-based-views.md) 14 | * [Messages Framework](features/contrib.messages.md) 15 | * [Signals](features/signals.md) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs 98 | site/ 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /docs/training-reference/project-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up the training project 2 | 3 | The training project can be found on github at [https://github.com/chadgh/django-advanced-training-project](https://github.com/chadgh/django-advanced-training-project). 4 | 5 | Follow the steps below to setup the project: 6 | 7 | 1. Clone the project. `git clone https://github.com/chadgh/django-advanced-training-project.git` 8 | 2. Change to the project directory. `cd django-advanced-training-project` 9 | 3. Create a Python virtual environment. `python -m venv env && source env/bin/activate` 10 | 4. Install Django. `pip install django` 11 | 5. Run Django migrations. `./manage.py migrate` 12 | 6. Create a superuser. `./manage.py createsuperuser` 13 | 14 | # Working on the project 15 | 16 | If you leave the projects virtual environment but you have already setup the project, you can get back into the project by the following steps: 17 | 18 | 1. Change to the project directory. `cd django-advanced-training-project` 19 | 2. Activate the virtual environment. `source env/bin/activate` 20 | 21 | # Running the development server 22 | 23 | After you have setup the project and are in the projects virtual environment you can run the development server from within the project directory with `./manage.py runserver`. The server will be hosted at [localhost:8000](http://localhost:8000). 24 | 25 | # Creating data 26 | 27 | With the development server running go in your browser to [localhost:8000/admin/(http://localhost:8000/admin/)], login with the superuser credentials you created at setup, and create a few todo items from the admin screens. 28 | 29 | After you have a few todo items, you can go to [localhost:8000/todos/](http://localhost:8000/todos/) to see and interact with the todo items. 30 | 31 | # Using the base training project 32 | 33 | Once you have the project setup, you can use it to try out features that you learn about from the training as well as attempt to solve the hands-on problems presented for each feature. Use the project as a sandbox to try things out with Django. 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/features/signals.md: -------------------------------------------------------------------------------- 1 | # Signals 2 | 3 | ## 1. Basics 4 | 5 | 6 | 7 | Django's signal dispatcher allows _senders_ to notify _receivers_ (or handlers) that an action has occurred. This is helpful for decoupling code and when you have different pieces of code that are interested in the same event. Signals are like event-based programming. You can hook up callback functions that get executed when specific events happen. 8 | 9 | Some of the most common built-in signals in Django are: 10 | 11 | * `django.db.models.signals.pre_save` and `django.db.models.signals.post_save` - These signals are triggered before or after a model instance `save` method is called. 12 | * `django.db.models.signals.pre_delete` and `django.db.models.signals.post_delete` - These signals are triggered before or after a model instance's `delete` method is called and before or after a queryset's `delete` method is called. 13 | * `django.core.signals.request_started` and `django.core.signals.request_finished` - These signals are triggered when Django starts or finishes an HTTP request. 14 | * `django.contrib.auth.signals.user_logged_in` and `django.contrib.auth.signals.user_logged_out` - These signals are triggered after a user successfully logs in or out of the application. 15 | 16 | The most basic use of signals is when you connect your callable with a signal to be called when that signal is triggered. 17 | 18 | For example, lets say we want to log every request that comes into out applications. We could create a function that would do that and connect it with the `request_started` signal. Assume that we have an application called `todos`: 19 | 20 | ```python 21 | # todos/tasks.py 22 | 23 | def log_request(sender, **kwargs): 24 | print('New Request!') 25 | ``` 26 | 27 | We then need to connect the function with the signal we want. This can be done in the default configuration class's `ready` method of the `todos` application's `app.py` file. 28 | 29 | ```python 30 | # todos/app.py 31 | from django.apps import AppConfig 32 | 33 | from django.core.signals import request_started 34 | 35 | from .tasks import log_request 36 | 37 | 38 | class TodosConfig(AppConfig): 39 | name = 'todos' 40 | 41 | def ready(self): 42 | request_started.connect(log_request) 43 | ``` 44 | That is all. Now every time a request comes into the application, the message "New Request!" will be logged in stdout. 45 | 46 | The `request_started` signals sends in the `kwargs` dict only one argument, `environ`, we could enhance our signal handler function to log something a little more helpful. 47 | 48 | ```python 49 | # todos/tasks.py 50 | 51 | def log_request(sender, environ, **kwargs): 52 | method = environ['REQUEST_METHOD'] 53 | host = environ['HTTP_HOST'] 54 | path = environ['PATH_INFO'] 55 | query = environ['QUERY_STRING'] 56 | query = '?' + query if query else '' 57 | print('New Request -> {method} {host}{path}{query}'.format( 58 | method=method, 59 | host=host, 60 | path=path, 61 | query=query, 62 | )) 63 | ``` 64 | Now a more helpful message will be logged. The new message will look something like this: `New Request -> GET 127.0.0.1:8000/`. Note also that it is common/good practice to identify the arguments for the signal you are writing the handler for that will be used. In our code above we do this by adding `environ` specifically to the arguments list. 65 | 66 | While the `request_started` signal handler we created might not be very useful there are more useful signals that can be used. The `django.db.models.signals.post_save` signal if useful for hooking in things that need to be done when a specific model is saved. 67 | 68 | Assume, for example, that our `todos` application has a user profile model (`UserProfile`) that it wants to make sure is created for every user created in the system. One way to do this would be to connect a handler to the `post_save` signal for the `User` model in `django.contrib.auth`. 69 | 70 | ```python 71 | # todos/tasks.py 72 | from .models import UserProfile 73 | 74 | ... 75 | 76 | def save_or_create_user_profile(sender, instance, created, **kwargs): 77 | if created: 78 | UserProfile.objects.create(user=instance) 79 | else: 80 | instance.user_profile.save() 81 | ``` 82 | 83 | Then in `todos/app.py` we connect the signal handler to the signal. 84 | 85 | ```python 86 | # todos/app.py 87 | ... 88 | from django.contrib.auth.models import User 89 | from django.db.models.signals import post_save 90 | 91 | from .tasks import log_request, save_or_create_user_profile 92 | 93 | 94 | class TodosConfig(AppConfig): 95 | ... 96 | 97 | def ready(self): 98 | ... 99 | post_save.connect(save_or_create_user_profile, sender=User) 100 | ``` 101 | 102 | Note that this time we pass the `sender` argument when we connect our function to the `post_save` signal. This means that our `save_or_create_user_profile` function will only be called if the sender of the `post_save` signal is the `User` model. This also means that we can hookup to the `post_save` signal with many different models executing different code to do different things. 103 | 104 | Finally, it is important to understand that signals are synchronous or blocking. This means that all signal handlers will have to finish executing before the code can continue after a signal has been triggered. If you have 10 signal handlers connected to the `request_started` signal, all 10 handlers would need to finish before Django even got to executing the middleware portion of the request. Keep this in mind when using signals and try not to use too many or do too much in them. 105 | 106 | ### Creating and triggering custom signals 107 | 108 | Django also allows you to create and trigger your own signals. This can be especially useful when creating a reusable package and you want developers down the road to be able to hook in bit of functionality to your existing code in a proven way. It can also be useful within your larger Django project. Lets look at an example of this. 109 | 110 | ```python 111 | # todo/tasks.py 112 | import django.dispatch 113 | 114 | todo_complete = django.dispatch.Signal(providing_args=['todo']) 115 | ``` 116 | 117 | In order to create a new signal it is as simple as instantiating a new `Signal` instance and telling it what additional arguments you will be providing handlers. 118 | 119 | Then to send a signal: 120 | 121 | ```python 122 | # todo/models.py 123 | ... 124 | from .signals import todo_complete 125 | ... 126 | class Todo(models.Model): 127 | description = models.CharField(max_length=255) 128 | done = models.BooleanField(default=False) 129 | 130 | def mark_complete(self): 131 | if not self.done: 132 | self.done = True 133 | self.save() 134 | todo_complete.send(sender=self.__class__, todo=self) 135 | ``` 136 | Note the call to `todo_complete.send`. This is calling the `send` method of the custom signal instance. This setup allows other developers, or yourself in other areas of your code, to hook into the event (signal) of completing a todo item and attach any functionality they would like to that event. 137 | 138 | ## 2. Deep Dive: Code Walk-through 139 | 140 | 141 | 142 | ### `django.core.signals` 143 | 144 | Django's signals implementation starts in the `django.core.signals` module ([code](https://github.com/django/django/blob/master/django/core/signals.py)). The first thing you may notice is that that module is very small (6 lines of code). All of the signals implementation is in the `django.dispatch` package ([code](https://github.com/django/django/blob/master/django/dispatch/dispatcher.py)). 145 | 146 | What is in the signals module are the core signals that Django provides: `request_started`, `request_finished`, `got_request_exception`, `setting_changed`. These provide good examples of what you need to do in order to create your own signals. 147 | 148 | ```python 149 | # django/core/signals.py 150 | from django.dispatch import Signal 151 | 152 | request_started = Signal(providing_args=["environ"]) 153 | ... 154 | ``` 155 | 156 | Note that the `request_started` signal that Django provides is simply an instance of the `Signal` class provided by `django.dispatch`. 157 | 158 | ### `django.dispatch.dispatcher.Signal` 159 | 160 | The `Signal` class ([implementation](https://github.com/django/django/blob/master/django/dispatch/dispatcher.py#L19)) is found in the `django.dispatch.dispatcher` module (but can be imported from `django.dispatch`). 161 | 162 | We are going to take a closer look at three methods found in the `Signal` class: `connect`, `send`, and `send_robust`. 163 | 164 | The `connect` method is called by developer code when they want to connect a callable to the signal. The `connect` method takes only one required arguments, the handler that should be called when the signals `send` method is called. 165 | 166 | Note that signal handlers are referred to as `receivers` in the code. 167 | 168 | ```python 169 | # django/dispatch/dispatcher.py 170 | ... 171 | class Signal: 172 | ... 173 | def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): 174 | ... 175 | if dispatch_uid: 176 | lookup_key = (dispatch_uid, _make_id(sender)) 177 | else: 178 | lookup_key = (_make_id(receiver), _make_id(sender)) 179 | ... 180 | with self.lock: 181 | ... 182 | for r_key, _ in self.receivers: 183 | if r_key == lookup_key: 184 | break 185 | else: 186 | self.receivers.append((lookup_key, receiver)) 187 | ... 188 | ... 189 | ... 190 | ``` 191 | All this method really accomplishes is that is adds the receiver function to the list of receivers held by the signal (instance of Signal) 192 | 193 | The `connect` method above has been simplified from the actual implementation to help us understand what `connect` is doing. First a `lookup_key` is generated for the receiver. This allows the signal to identify the individual receivers and make sure that the same receiver isn't connected more than once to the signal. 194 | 195 | Then `self.lock` is used in a context manager to lock the signal for adding the receiver to make sure the `self.receiver` list is only modified by one process at a time. 196 | 197 | Inside the lock context manager block, the list of receivers is checked to make sure we don't add a duplicate (based on the `lookup_key`) and finally added if there isn't one already. 198 | 199 | So now our receiver had been added to the signals `receiver` list. This list is then used by the `send` method. 200 | 201 | ```python 202 | # django/dispatch/dispatcher.py 203 | ... 204 | class Signal: 205 | ... 206 | def send(self, sender, **named): 207 | ... 208 | return [ 209 | (receiver, receiver(signal=self, sender=sender, **named)) 210 | for receiver in self._live_receivers(sender) 211 | ] 212 | ... 213 | ``` 214 | 215 | The `send` method returns a list of tuples where the first element is the receiver (function) and the second element is the response from calling the receiver. `Signal` has an internal method, `_live_receivers`, that returns a list of active receivers for the specified `sender`. 216 | 217 | The implementation does this with a simple list comprehension. 218 | 219 | One issue with calling the `send` method to trigger the signal is that if one of the receivers raises an exception, there is no guarantee that all of the other receivers would be executed. This is where the `send_robust` method can help. 220 | 221 | The `send_robust` method does the same thing as the `send` method except that exceptions are caught and added to the return value for later evaluation. This method guarantees that all receivers will be allowed to execute. 222 | 223 | ```python 224 | # django/dispatch/dispatcher.py 225 | ... 226 | class Signal: 227 | ... 228 | def send_robust(self, sender, **named): 229 | ... 230 | responses = [] 231 | for receiver in self._live_receivers(sender): 232 | try: 233 | response = receiver(signal=self, sender=sender, **named) 234 | except Exception as err: 235 | responses.append((receiver, err)) 236 | else: 237 | responses.append((receiver, response)) 238 | return responses 239 | ... 240 | ``` 241 | 242 | Notice here that where the `send` method had a simple list comprehension, the `send_robust` method has a for loop that uses a try-except to make sure that every receiver is called and added to the responses list along with either the response from calling the receiver or the resulting exception that was raised. 243 | 244 | Internally Django only uses the `send` method for sending signals. The `send_robust` can be used for your own custom signals. 245 | 246 | ### Where are `request_started` and `request_finished` called from? 247 | 248 | We know where the `request_started` and `request_finished` signals are created (`django.core.signals`) but where are those signals triggered (sent)? 249 | 250 | Django sends the `request_started` signal from the `django.core.handlers.wsgi.WSGIHandler` `__call__` method ([code](https://github.com/django/django/blob/master/django/core/handlers/wsgi.py#L135)). On the line 2 of the `__call__` method you can see the call to `send`. The `WSGIHandler` class is the main entry point for Django applications. After an instance is instantiated, the `__call__` method gets called for each request. The `request_started` signal is essentially the first thing to be called on each request. 251 | 252 | Django sends the `request_finished` signal from the `django.http.response.HttpResponseBase` classes `close` method ([code](https://github.com/django/django/blob/master/django/http/response.py#L27)). The `close` method ([code](https://github.com/django/django/blob/master/django/http/response.py#L238)) is called by the WSGI server on the response. When that happens the `request_finished` signal is sent. 253 | 254 | ```python 255 | # django/http/response.py 256 | ... 257 | class HttpResponseBase: 258 | ... 259 | def close(self): 260 | ... 261 | signals.request_finished.send(sender=self._handler_class) 262 | ``` 263 | We are obviously leaving a lot of things out here, but the important part is that the very last thing `close` does is send the `request_finished` signal. 264 | 265 | ## 3. Deep Dive: Language Features 266 | 267 | ### for-else block 268 | You may have noticed some strange code in the examples above, a for loop with an associated else block (in the discussion about `django.dispatch.dispatcher.Signal`). This is not a common construct, but it exists in Python and its meaning is a little tough for some to remember. 269 | 270 | ```python 271 | letters = ['a', 'b', 'c'] 272 | looking_for = 'a' 273 | 274 | for letter in letters: 275 | if letter == looking_for: 276 | print('Found the letter!') 277 | break 278 | else: 279 | print("Didn't find the letter.") 280 | ``` 281 | 282 | I think the above example illustrates its use nicely. The else block gets executed with the whole for loop completes and the break statement isn't reached. 283 | 284 | ### Context manager 285 | 286 | A lot of times in code you have to acquire a resource, do something with it, and then clean things up afterwards. This is the case when working with files (open, process, close), locks (lock, process, release), and many other resources. Because it is such a common flow (run common setup code, do something, run tear down code) Python added a statement that makes this a lot nicer. 287 | 288 | ```python 289 | # working with files 290 | 291 | # the standard method 292 | file = open('tmp.txt') 293 | ... # processing the file 294 | file.close() 295 | 296 | # context manager method 297 | with open('tmp.txt') as file: 298 | ... # processing the file 299 | ``` 300 | The above code shows two methods of working with files. Both methods work just fine in Python. However the context manager method (using the `with` statement) is safer. The `with` statement in this context guarantees that the file will be closed, even if an exception is raised in the processing code. 301 | 302 | We can see another use of context managers in the `django.dispatch.dispatcher.Signal` classes `connect` method. In there a lock is acquired and released using the `with` statement. Once again, this has the benefit of being safer because the lock is guaranteed to be released even if an exception is raised in the `with` block code. 303 | 304 | ## 4. Hands-on Exercises 305 | 306 | **Implement `todo_done` and `todo_undone` signals. Call the send method from the methods on the `Todo` model such as `mark_done` and `mark_undone`. Hook into the signal a callback that logs the item that was completed or undone.** 307 | 308 | ### Hints 309 | 310 | * We did something very similar to this in the Basics section above. 311 | 312 | ### Possible Solution 313 | 314 | ```python 315 | # todo/signals.py 316 | import django.dispatch 317 | 318 | todo_done = django.dispatch.Signal(providing_args=['item']) 319 | todo_undone = django.dispatch.Signal(providing_args=['item']) 320 | ``` 321 | 322 | ```python 323 | # todo/models.py 324 | ... 325 | from .signals import todo_done, todo_undone 326 | ... 327 | class Todo(models.Model): 328 | ... 329 | def mark_done(self): 330 | if not self.done: 331 | self.done = True 332 | self.save() 333 | todo_done.send(sender=self.__class__, item=self) 334 | 335 | def mark_undone(self): 336 | if self.done: 337 | self.done = False 338 | self.save() 339 | todo_undone.send(sender=self.__class__, item=self) 340 | ... 341 | ``` 342 | 343 | ```python 344 | # todo/tasks.py 345 | ... 346 | def log_todo_action(sender, item, **kwargs): 347 | if item.done: 348 | logger.info(f'Item complete: {item.item}') 349 | else: 350 | logger.info(f'Item undone: {item.item}') 351 | ``` 352 | 353 | ```python 354 | # todo/app.py 355 | ... 356 | from . import signals, tasks 357 | ... 358 | class TodoConfig(AppConfig): 359 | ... 360 | def ready(self): 361 | signals.todo_done.connect(tasks.log_todo_action) 362 | signals.todo_undone.connect(tasks.log_todo_action) 363 | ``` 364 | 365 | __Note__: This is just one solution for solving this problem. The problem can be solved correctly in many different ways. 366 | 367 | ## 5. Contribute 368 | 369 | ### Resources 370 | 371 | * [Django Signals documentation](https://docs.djangoproject.com/en/1.11/topics/signals/) 372 | * [Signals open tickets](https://code.djangoproject.com/query?status=assigned&status=new&summary=~signal&col=summary&col=status&col=owner&col=type&col=version&col=has_patch&col=needs_docs&col=needs_tests&col=needs_better_patch&col=easy) (refer to [Understanding Django's Ticketing System]() for more details) 373 | 374 | -------------------------------------------------------------------------------- /docs/features/class-based-views.md: -------------------------------------------------------------------------------- 1 | # Class-based Views 2 | 3 | ## 1. Basics 4 | 5 | 6 | 7 | Django's class-based views provide an object-oriented (OO) way of organizing your view code. Most Django tutorials and training materials start developers off with the simple style of function-based views (which were available in Django long before class-based views). Class-based views were introduced to help make view code more reusable and provide for better view code organization. 8 | 9 | The structure of a simple function-based view that is used to process both `GET` and `POST` requests might look like this: 10 | 11 | ```python 12 | # views.py 13 | def simple_function_based_view(request): 14 | if request.method == 'GET': 15 | ... # code to process a GET request 16 | elif request.method == 'POST': 17 | ... # code to process a POST request 18 | ``` 19 | 20 | The same thing with a class-based view could look like this: 21 | 22 | ```python 23 | # views.py 24 | from django.views import View 25 | 26 | class SimpleClassBasedView(View): 27 | def get(self, request): 28 | ... # code to process a GET request 29 | 30 | def post(self, request): 31 | ... # code to process a POST request 32 | ``` 33 | 34 | Hooking up class-based views in the `urls.py` file is a little different too: 35 | 36 | ```python 37 | # urls.py 38 | ... 39 | from . import views 40 | 41 | urlconfig = [ 42 | # the function-based view is added as the second param 43 | url(r'^function/$', views.simple_function_based_view), 44 | # the as_view method is called on the class-based view and 45 | # the result is the value of the second param 46 | url(r'^class/$', views.SimpleClassBasedView.as_view()), 47 | ] 48 | ``` 49 | 50 | To illustrate this further we will walk through converting a function-based view to a class-based view that does the same thing. 51 | 52 | ```python 53 | # views.py 54 | from datetime import datetime 55 | from django.http import HttpResponse 56 | 57 | def show_the_time(request): 58 | now = datetime.now() 59 | html = "It is now {}".format(now) 60 | return HttpResponse(html) 61 | ``` 62 | 63 | ```python 64 | # urls.py 65 | from django.conf.urls import url 66 | 67 | from . import views 68 | 69 | urlpatterns = [ 70 | url(r'^now/$', views.show_the_time), 71 | ] 72 | ``` 73 | 74 | In order to do the same thing using a class-based view the files would have to change as follows: 75 | 76 | ```python 77 | # views.py 78 | from datetime import datetime 79 | from django.http import HttpResponse 80 | from django.views import View # import the View parent class 81 | 82 | class ShowTimeView(View): # create a view class 83 | 84 | # change the function-based view to be called get and add the self param 85 | def get(self, request): 86 | now = datetime.now() 87 | html = "It is now {}".format(now) 88 | return HttpResponse(html) 89 | ``` 90 | 91 | ```python 92 | # urls.py 93 | from django.conf.urls import url 94 | 95 | from . import views 96 | 97 | urlpatterns = [ 98 | url(r'^now/$', views.ShowTimeView.as_view()), # change how we reference the new view. 99 | ] 100 | ``` 101 | Note that there are not many changes in order to change from one type of view (function-based) to the other (class-based). The benefit of going with the class-based view (even in this simple example) is that the view is going to be more robust. The class-based views (view classes that extend the `View` class) get built-in responses to unsupported methods (`POST`, `PUT`, `PATCH`, etc.) and get support for the `OPTIONS` HTTP method too. All that is required to support other HTTP methods is to implement the same named method in the view class. 102 | 103 | There are also generic class-based views. These are class-based views that provide extra common functionality. For example, a common type of view might be called a template view: a view that generates some context and sends the context to a specified template for rendering. Django provides a generic class-based view for that very purpose, `TemplateView`. 104 | 105 | To use the example above, we will assume that the html portion is in a template called `show_time.html`. If so, we can change the `ShowTimeView` class to extend from the `TemplateView` instead of `View` and get the benefits of the common code. 106 | 107 | ```python 108 | # views.py 109 | from datetime import datetime 110 | from django.http import HttpResponse 111 | from django.views.generic import TemplateView # import the TemplateView parent class 112 | 113 | class ShowTimeView(TemplateView): # extend from TemplateView 114 | template_name = 'show_time.html' # add a template_name attribute 115 | 116 | # change the get method to get_context_data 117 | def get_context_data(self, **kwargs): 118 | context = super().get_context_data(**kwargs) 119 | context['now'] = datetime.now() 120 | return context 121 | ``` 122 | The `urls.py` file shouldn't have to be modified. 123 | 124 | To learn more about using class-based views and generic class-based views see the Django documentation [ref](https://docs.djangoproject.com/en/1.11/ref/class-based-views/) and [topics](https://docs.djangoproject.com/en/1.11/topics/class-based-views/) sections. 125 | 126 | ## 2. Deep Dive: Code Walk-through 127 | 128 | 129 | 130 | ### `View` 131 | The class-based views in Django all extend from the parent class `View`. This class can be found in `django.views.generic.base` ([code here](https://github.com/django/django/blob/master/django/views/generic/base.py#L31)). 132 | 133 | The `View` class has three methods that we will take a closer look at. For convenience the important parts of these methods are included below. 134 | 135 | ```python 136 | # django/views/generic/base.py 137 | ... 138 | class View: 139 | ... 140 | def __init__(self, **kwargs): 141 | ... 142 | for key, value in kwargs.items(): 143 | setattr(self, key, value) 144 | 145 | @classonlymethod 146 | def as_view(cls, **initkwargs): 147 | ... 148 | def view(request, *args, **kwargs): 149 | self = cls(**initkwargs) 150 | if hasattr(self, 'get') and not hasattr(self, 'head'): 151 | self.head = self.get 152 | self.request = request 153 | self.args = args 154 | self.kwargs = kwargs 155 | return self.dispatch(request, *args, **kwargs) 156 | view.view_class = cls 157 | view.view_initkwargs = initkwargs 158 | ... 159 | return view 160 | 161 | def dispatch(self, request, *args, **kwargs): 162 | ... 163 | if request.method.lower() in self.http_method_names: 164 | handler = getattr(self, request.method.lower(), self.http_method_not_allowed) 165 | else: 166 | handler = self.http_method_not_allowed 167 | return handler(request, *args, **kwargs) 168 | 169 | ... 170 | ``` 171 | 172 | The `__init__` method is fairly simple. It takes keyword arguments and sets each of them as attributes on the instance with the values provided. A Django developer doesn't have a need to instantiate class-based views directly, so the constructor isn't something that you would interact with directly, but it is used by the `as_view` method to provide a chance when calling `as_view` to override attributes then. 173 | 174 | The `dispatch` method contains the actual view logic. It takes a request, finds the method that should be called for the given request (by using the HTTP method used), and returns the results of calling that method. If there isn't a method for the HTTP method used, the default view is the `http_method_not_allowed` view. It returns an HTTP 405 response. 175 | 176 | The `as_view` method creates and returns a new function that is used as a Django view. Here we can see that even class-based views are simply just function-based views when they are actually used. 177 | 178 | The `view` function that is created and returned from the `as_view` method, looks a lot like a function-based view. It takes a request and returns the results of calling the class-based views `dispatch` method. It is also interesting to see that the `view` function on each request will instantiate a new instance of the view class, set the request, args, and kwargs attributes, and then call and return the results from the view instance's `dispatch` method. It is also interesting to note that the `view` function also gets a few attributes of its own including the `view_class` and `view_initkwargs` attributes. 179 | 180 | ### `ContextMixin` and `TemplateResponseMixin` 181 | 182 | The suffix `Mixin` in Python is a convention that is often used to signify that the class provides functionality that can be used by other things. These types of classes aren't usually instantiated or used on their own other than to be extended from so that other classes can use the functionality they provide. These types of classes usually have no parent class and provide a very specific bit of functionality. 183 | 184 | The `ContextMixin` class found in the [generic views code](https://github.com/django/django/blob/master/django/views/generic/base.py#L16) provides child classes with a `get_context_data` method that can be used. 185 | 186 | ```python 187 | class ContextMixin: 188 | ... 189 | def get_context_data(self, **kwargs): 190 | if 'view' not in kwargs: 191 | kwargs['view'] = self 192 | if self.extra_context is not None: 193 | kwargs.update(self.extra_context) 194 | return kwargs 195 | ``` 196 | 197 | By itself, the `ContextMixin` isn't that helpful. The `get_context_data` method makes sure that there is a value `view` in kwargs and also makes sure that if there is anything in the `extra_context` attribute it is also added to kwargs before returning kwargs. In the next section we will see why this mixin is helpful, but first we will take a looks at the `TemplateResponseMixin`. 198 | 199 | The `TemplateResponseMixin` ([also in the same file](https://github.com/django/django/blob/master/django/views/generic/base.py#L109)) looks like this: 200 | 201 | ```python 202 | ... 203 | class TemplateResponseMixin: 204 | ... 205 | template_name = None 206 | ... 207 | def render_to_response(self, context, **response_kwargs): 208 | ... 209 | response_kwargs.setdefault('context_type', self.context_type) 210 | return self.response_class( 211 | request=self.request, 212 | template=self.get_template_names(), 213 | context=context, 214 | using=self.template_engine, 215 | **response_kwargs 216 | ) 217 | ... 218 | ``` 219 | 220 | This class provides a few more attributes and another method that are not seen here, but what is shown is the important part for understanding what this mixin provides. The `render_to_response` method takes a context dict then instantiates a response object and returns the response object. It also takes care of defining which template should be used, as denoted by the `template_name` attribute. 221 | 222 | These two mixins don't seem to provide very much, but when combined into the same view class, we can begin to see the power they offer. In the next section we look at the `TemplateView` class-based view and what it provides. 223 | 224 | ### `TemplateView` 225 | 226 | The `TemplateView` class-based view provides a simple reusable (generic) class-based view for rendering views that rely on context data and rendering a template. We look at it below ([code on github](https://github.com/django/django/blob/master/django/views/generic/base.py#L145)). 227 | 228 | ```python 229 | class TemplateView(TemplateResponseMixin, ContextMixin, View): 230 | ... 231 | def get(self, request, *args, **kwargs): 232 | context = self.get_context_data(**kwargs) 233 | return self.render_to_response(context) 234 | ``` 235 | As we can see the `TemplateView` provides a default `get` implementation that builds the context, by calling the `get_context_data` method provided by the `ContextMixin` class and then returns the results of calling the `render_to_response` method provided by the `TemplateResponseMixin` class. If you want to use the `TemplateView` all you should have to do is extend it and define the `template_name` attribute as well as an extended definition for the `get_context_data` method. For example: 236 | 237 | ```python 238 | class ShowTimeView(TemplateView): 239 | template_name = 'show_time.html' 240 | 241 | def get_context_data(self, **kwargs): 242 | context = super().get_context_data(**kwargs) 243 | context['now'] = datetime.now() 244 | return context 245 | ``` 246 | The `ShowTimeView` will take care of rendering the proper response object for us and we have no need of defining a `get` method because we extended the `TemplateView` class-based view. 247 | 248 | ## 3. Deep Dive: Language Features 249 | 250 | ### First-class functions 251 | 252 | In Python, functions are known as [first-class](https://en.wikipedia.org/wiki/First-class_function) or higher order functions. Essentially this means that functions can be passed to other function calls, returned from other functions, and be assigned to variable names. 253 | 254 | We see an [example of the use of higher order functions](https://github.com/django/django/blob/master/django/views/generic/base.py#L62) in the `View` class's `as_view` method. When that method is called, it generates (defines) a new function called `view` and returns it. This means that the urls configuration gets a function where it is expecting a function: 255 | 256 | ```python 257 | url(r'^$', views.ShowTimeView.as_view()), 258 | ``` 259 | 260 | With function-based views, the second argument is the function itself that should be executed when the url regex is matched. For class-based views we call another function, `as_view`, which returns a function (instead of naming it explicitly). 261 | 262 | Let's look at another example of using functions as first-class citizens: 263 | 264 | ```python 265 | >>> def make_power_func(power): 266 | ... def to_the_power(number): 267 | ... return number ** power 268 | ... return to_the_power 269 | >>> pow_10 = make_power_func(10) 270 | >>> pow_10(2) 271 | 1024 272 | ``` 273 | 274 | In this example the `make_power_func` function takes a number, `power`, and creates a new function that encapsulates the `power` value. The `make_power_func` function also returns the newly defined `to_the_power` function. We then assign that function to the value of `pow_10` and we can then call the function through that new name. We could call the `make_power_func` function for as many values as we wish and create a power function on the fly. 275 | 276 | ### Multiple inheritance 277 | 278 | Python's OOP model allows for multiple inheritance. This can be easily seen in the `TemplateView` class above: 279 | 280 | ```python 281 | class TemplateView(TemplateResponseMixin, ContextMixin, View): 282 | ``` 283 | 284 | When defining a class the parents of that class are defined in parentheses after the class name. The `TemplateView` class has three explicit parent classes. There would also most likely be some implicit parent classes (such as `object` which is the parent of all classes in Python, but rarely explicitly defined). The order of the parents matters here because when the `super` function is called in any method in order to call a parent method, parents are searched in the order defined. So when we call the `get_context_data` method on the `TemplateView` class instance, it first looks in `TemplateResponseMixin` and then moves to `ContextMixin` (before stopping because the method was found there). 285 | 286 | In order to see the parent classes, and the order that they will be called/searched in, you can call the `mro` method of a class (MRO stands for Method Resolution Order). 287 | 288 | ```python 289 | >>> from django.views.generic import TemplateView 290 | >>> TemplateView.mro() 291 | [, , , , ] 292 | ``` 293 | This parent list is a predictable and specifically ordered list of parents that defines the order that parents are searched in when calling a method and using `super`. 294 | 295 | ## 4. Deep Dive: Software Architecture Features 296 | 297 | ### Multiple inheritance 298 | 299 | Multiple inheritance can be a tricky problem to solve, what do you do if multiple parents define the same method, which one do you call if the method is called from a child class instance? 300 | 301 | One algorithm that solves this problem (and the algorithm that is used for MRO in Python) is the [C3 linearization]() algorithm. The C3 linearization algorithm provides a consistent order to the MRO list of parent classes because of this it is deterministic in the order of the parent classes it produces. 302 | 303 | ## 5. Hands-on Exercises 304 | 305 | **Implement a generic class-based view called `StaticView` that extends from `View` and provides a get method that will respond with a static HTML page.** 306 | 307 | ### Hints 308 | 309 | * Make sure it is reusable. The static HTML should to be specified by each child class-based view of `StaticView`. 310 | * Take a look at what the `TemplateView` does in its `get` method. 311 | 312 | ### Possible Solution 313 | 314 | There are most definitely other ways to solve this problem, but here is one simple solution. 315 | 316 | ```python 317 | from django.http import HttpResponse 318 | from django.views import View 319 | 320 | 321 | class StaticView(View): 322 | static_html = None 323 | 324 | def get(self, request): 325 | html = open(self.static_html).read() 326 | return HttpResponse(html) 327 | 328 | 329 | class MyStaticView(StaticView): 330 | static_html = 'static_file.html' 331 | ``` 332 | 333 | The `StaticView` class can be extended and the `static_html` attribute updated. The `urls.py` file would have an entry for the `MyStaticView` child class. Because of the way `View` is implemented and the `as_view` method, we can also add an entry to our urls that looks like this. 334 | 335 | ```python 336 | url(r'^test/$', views.StaticView.as_view(static_html='test.html')) 337 | ``` 338 | 339 | ## 6. Contribute 340 | 341 | ### Resources 342 | 343 | * [Django class-based views documentation](https://docs.djangoproject.com/en/1.11/ref/class-based-views/) 344 | * [Django class-based views reference](https://docs.djangoproject.com/en/1.11/topics/class-based-views/) 345 | * [Classy Class-based Views](https://ccbv.co.uk) 346 | * [Generic views open tickets](https://code.djangoproject.com/query?status=assigned&status=new&component=Generic+views&col=summary&col=status&col=owner&col=type&col=version&col=has_patch&col=needs_docs&col=needs_tests&col=needs_better_patch&col=easy) (refer to [Understanding Django's Ticketing System]() for more details) 347 | 348 | -------------------------------------------------------------------------------- /docs/features/contrib.messages.md: -------------------------------------------------------------------------------- 1 | # Messages Framework 2 | 3 | ## 1. Basics 4 | 5 | 6 | 7 | Django's messages framework makes sending one-time messages to users simple. After setting up the messages framework in your Django project (which is setup by default in the standard Django project) it is as simple as adding a message with a single call in your views. 8 | 9 | ```python 10 | # views.py 11 | from django.contrib import messages 12 | ... 13 | def some_view(request): 14 | ... 15 | # there are two ways to add a message in view code 16 | # (a) using the add_message method, 17 | messages.add_message(request, messages.INFO, 'Hello!') 18 | 19 | # (b) using one of the convenience methods for the message level (info in this case). 20 | messages.info(request, 'Hello!') 21 | ... 22 | ... 23 | ``` 24 | 25 | The above code adds the message "Hello!" to the messages storage backend to be displayed to the user later during the current request or during the next request. Messages are stored with the level information attached to them for later use. The messages above are stored with the level of 'INFO'. When a message is displayed it is removed from the storage backend. 26 | 27 | In order to display messages to the user the messages need to be looped over in a template. The following template code is one way to do this: 28 | 29 | ```djangohtml 30 | 31 | ... 32 | {% if messages %} 33 |
    34 | {% for message in messages %} 35 | {{ message }} 36 | {% endfor %} 37 |
38 | {% endif %} 39 | ... 40 | ``` 41 | 42 | Messages are added to the messages storage at specific levels. These levels are similar to the logging levels in Python, but are not the same. Each message level corresponds to an integer value. Below are the default levels and their values along with the tag that will be attached (which is also the convenience method names). 43 | 44 | | Level Constant | Tag | Value | 45 | | -------------- | ------- | ----- | 46 | | `DEBUG` | debug | 10 | 47 | | `INFO` | info | 20 | 48 | | `SUCCESS` | success | 25 | 49 | | `WARNING` | warning | 30 | 50 | | `ERROR` | error | 40 | 51 | 52 | The messages framework is setup by default in the standard Django project that you get from running `startproject` with `django-admin`. If you have setup your project using another method you might need to make the following changes in your code in order to make the messages framework available. 53 | 54 | 1. Make sure `django.contrib.messages` is in the `INSTALLED_APPS` list in your settings. 55 | 2. Make sure `django.contrib.sessions.middleware.SessionMiddleware` and `django.contrib.messages.middleware.MessageMiddleware` are both in the `MIDDLEWARE` list in your settings. Note that the session middleware needs to come before the messages middleware. 56 | 3. Make sure the `context_processors` list in the `TEMPLATES` settings contains `django.contrib.messages.context_processors.messages`. 57 | 58 | For more information about Django's messages framework see the [Django documentation](https://docs.djangoproject.com/en/1.11/ref/contrib/messages/). Also a [Google search](https://www.google.com/search?q=django+messages+framework) for `django messages framework` results in many helpful resources for learning more. 59 | 60 | ## 2. Deep Dive: Code Walk-through 61 | 62 | 63 | 64 | ### `MessageMiddleware` 65 | The messages middleware is the place to start as we take a closer look at the implementation for this feature. The middleware is one of the things that is required to make sure you are able to use the messages framework. The code can be found [on github](https://github.com/django/django/blob/master/django/contrib/messages/middleware.py). 66 | 67 | There are two methods in the `MessageMiddleware` class that are executed for each request/response. The first is the `process_request` method which is executed on each request as it comes into Django. 68 | 69 | ```python 70 | def process_request(self, request): 71 | request._messages = default_storage(request) 72 | ``` 73 | 74 | This method adds a new `_messages` attribute to the request object for the current request. The value of that attribute is the return value from calling the `default_storage` function. The `default_storage` function is also very simple, it instantiates the messages storage backend, set in the projects settings, with the request object ([`default_storage` code](https://github.com/django/django/blob/master/django/contrib/messages/storage/__init__.py)). 75 | 76 | The other method on the `MessageMiddleware` class we need to take a look at is the `process_response` method. 77 | 78 | ```python 79 | def process_response(self, request, response): 80 | ... 81 | # A higher middleware layer may return a request which does not contain 82 | # messages storage, so make no assumption that it will be there. 83 | if hasattr(request, '_messages'): 84 | unstored_messages = request._messages.update(response) 85 | ... 86 | return response 87 | ``` 88 | 89 | Before each response is sent back from Django to the user's browser the response is sent to the `update` method of the messages storage backend instance. The implementation for this method is found in the `BaseStorage` class that is extended from for each of the message storage backends ([see the code here](https://github.com/django/django/blob/master/django/contrib/messages/storage/base.py#L115)). This method makes sure that any messages that where not read for this response will be saved/stored for later requests. The actual implementation of what that means depends on the messages storage backend implementation. The `_store` method is called from the `update` method and is storage backend dependent. 90 | 91 | The end result of this means that before a response is sent back to the user's browser, all unread messages are stored for later use. 92 | 93 | ### `messages.context_processors` 94 | 95 | The messages context processor ([code](https://github.com/django/django/blob/master/django/contrib/messages/context_processors.py)) makes sure that every context that is sent to a template to be rendered gets the following context variables added: 96 | 97 |
98 |
messages
99 |
A list of the messages that haven't been displayed yet.
100 | 101 |
DEFAULT_MESSAGE_LEVELS
102 |
A dict of the default message levels where the keys are the level names and the values are the level integer values (similar to the message level table above).
103 |
104 | 105 | ### Message Storage 106 | 107 | The messages framework stores the messages in what is known as a storage backend. These backends must extend from the `BaseStorage` class found in [storage/base.py](https://github.com/django/django/blob/master/django/contrib/messages/storage/base.py#L43) in the messages framework code. The `BaseStorage` class' doc string states that all children classes that implement a new storage backend must implement two methods: `_get` and `_store`. 108 | 109 | ```python 110 | # message storage backend _get method signature. 111 | def _get(self, *args, **kwargs): 112 | """ 113 | Retrieve a list of stored messages. Return a tuple of the messages 114 | and a flag indicating whether or not all the messages originally 115 | intended to be stored in this storage were, in fact, stored and 116 | retrieved; e.g., ``(messages, all_retrieved)``. 117 | """ 118 | ... 119 | 120 | # messages storage backend _store method signature. 121 | def _store(self, messages, response, *args, **kwargs): 122 | """ 123 | Store a list of messages and return a list of any messages which could 124 | not be stored. 125 | """ 126 | ... 127 | ``` 128 | 129 | The `_get` method must return a tuple where the first element is a list of the stored messages and the second element is a flag indicating whether or not all of the messages where stored and retrieved. 130 | 131 | The `_store` method must store a list of messages and return a list of the messages that couldn't be stored. 132 | 133 | The `BaseStorage` class also provides two main methods that define how all storage backends work. First the [`add` method](https://github.com/django/django/blob/master/django/contrib/messages/storage/base.py#L129). This method is responsible for adding messages to the messages system. It takes a message level, the message and optional extra tags, instantiates a new `Message` and appends that message to an internal list of messages (this is only adding the messages in memory, actual storage in the backend happens later when the `update` method is called from the `process_response` method of the `MessagesMiddleware`, described above). 134 | 135 | ```python 136 | def add(self, level, message, extra_tags=''): 137 | ... 138 | if not message: 139 | return 140 | 141 | if level < self.level: 142 | return 143 | 144 | self.add_new = True 145 | message = Message(level, message, extra_tags=extra_tags) 146 | self._queued_messages.append(message) 147 | ``` 148 | 149 | The first thing we can see is that the `add` method returns right away and doesn't queue anything or even raise errors if the `message` argument is falsy or the `level` argument is less than the current message level set. Otherwise, a new `Message` instance is created and appended to the `_queued_messages` attribute of the storage backend. 150 | 151 | From this we can see that messages are not stored based on the message level set. Often it is assumed that messages are stored and when displaying them the message level can be set to determine which ones get shown, but that is not the case as we can see from the implementation. 152 | 153 | The `update` method is called by the `MessagesMiddleware`'s `process_response` method. It is responsible for calling the messages storage backend's `_store` method. 154 | 155 | ```python 156 | def update(self, response): 157 | ... 158 | if self.used: 159 | return self._store(self._queued_messages, response) 160 | elif self.added_new: 161 | messages = self._loaded_messages + self._queued_messages 162 | return self._store(messages, response) 163 | ``` 164 | 165 | The `self.used` is set to true on the storage backend instance if the messages have been iterated over. The `self.added_new` is set to true inside the `add` method that we saw above. So the `update` method only stores the queued messages if the backend was iterated over, otherwise it stores everything (`_loaded_messages` contains messages that were stored during a previous request in the storage backend). 166 | 167 | All messages are stored by instantiating the [`Message` class](https://github.com/django/django/blob/master/django/contrib/messages/storage/base.py#L7). This class takes in its constructor the level, message, and extra tags. The message argument is cast to a string if the `Message` instance is cast to a string (see the `__str__` method on `Message`) or when the messages are iterated over (see the `_prepare` method on `Message`). Seeing this in the implementation means that we can store in the messages framework objects other than strings as long as they can be cast to a string and that casting results in the messages that we want stored. 168 | 169 | In order to get a better understanding of how the messages storage backends work, we can take a look at one of the more simple storage backends provided by Django, the [`SessionStorage` class](https://github.com/django/django/blob/master/django/contrib/messages/storage/session.py#L10). Like we already learned, a storage backend only needs to implement the `_get` and `_store` methods, so we should be able to see how the `SessionStorage` backend is implemented by just looking at those two methods. 170 | 171 | ```python 172 | ... 173 | class SessionStorage(BaseStorage): 174 | ... 175 | session_key = '_messages' 176 | ... 177 | def _get(self, *args, **kwargs): 178 | ... 179 | return self.deserialize_messages(self.request.session.get(self.session_key)), True 180 | 181 | def _store(self, messages, response, *args, **kwargs): 182 | ... 183 | if messages: 184 | self.request.session[self.session_key] = self.serialize_messages(messages) 185 | else: 186 | self.request.session.pop(self.session_key, None) 187 | return [] 188 | ... 189 | ``` 190 | 191 | The `SessionStorage` backend does make use of a few helper methods to serialize and deserialize the messages before and after storing them in the session, but other than that the session storage backend implementation is fairly straight forward. The `_get` method pulls any messages out of the session (`self.request.session.get(self.session_key)`) and the`_store` method puts messages into the session (`self.request.session[self.session_key] = messages`). 192 | 193 | ### Messages API and Constants 194 | The final piece to the messages framework puzzle is the interface used by the Django developer. When you import `from django.contrib import messages` you are importing everything in the [`constants.py`](https://github.com/django/django/blob/master/django/contrib/messages/constants.py) file and the [`api.py`](https://github.com/django/django/blob/master/django/contrib/messages/api.py) file. 195 | 196 | The `constants.py` file contains the message level constants that are used throughout the code. 197 | 198 | The `api.py` file contains the `add_message` function, `set_level` function, and the convenience functions for adding messages at the specific levels (`debug`, `info`, `success`, `warning`, `error`). 199 | 200 | The `add_message` function is the gateway function. In most Django developer code, the `add_message` function is the only thing that is specifically interacted with in the framework. 201 | 202 | ```python 203 | ... 204 | def add_message(request, level, message, extra_tags='', fail_silently=False): 205 | ... 206 | try: 207 | messages = request._messages 208 | except AttributeError: 209 | ... # code here uses the fail_silently argument to determine if an exception is raised 210 | else: 211 | return messages.add(level, message, extra_tags) 212 | ... 213 | ``` 214 | 215 | Remember that the `_messages` attribute on the request object is an instance of the storage backend. So the `add_message` function simply delegates to the storage backend's `add` method as seen previously. 216 | 217 | The `set_level` function allows you to change the minimum message level that should be store. 218 | 219 | ```python 220 | def set_level(request, level): 221 | ... 222 | if not hasattr(request, '_messages'): 223 | return False 224 | request._messages.level = level 225 | return True 226 | ``` 227 | This function will return `True` or `False` indicating if it was able to change the message level. It simply changes the level attribute on the storage backend instance found in the `_messages` attribute of the request object. 228 | 229 | Finally, there are several convenience functions provided to make it simpler to add messages at specific levels. These simple functions all do the exact same thing. 230 | 231 | ```python 232 | def debug(request, message, extra_tags='', fail_silently=False): 233 | """Add a message with the ``DEBUG`` level.""" 234 | add_message(request, constants.DEBUG, message, extra_tags=extra_tags, 235 | fail_silently=fail_silently) 236 | ``` 237 | All of the convenience functions are the same here. The only differences being the name, the doc string, and which constant level value that is provided as the second argument of the `add_message` call. 238 | 239 | ## 3. Deep Dive: Language Features 240 | 241 | ### try-except-else 242 | Python has the concept of an `else` block in connection with a `try-except` block. This is for code that should be executed if no exceptions were raised. 243 | 244 | ```python 245 | try: 246 | ... # some code that could raise an exception 247 | except Exception as e: 248 | ... # do something when an exception is raised 249 | else: 250 | ... # code that is executed if the try block completes and no exceptions were raised 251 | ``` 252 | 253 | We saw this in the `api.py` file in the `add_message` function. The `try` block was used to get access to the current instance of the message storage backend (`request._message`) and so the `else` block is executed if that access doesn't fail. 254 | 255 | ### Custom containers 256 | 257 | Python has double underscore (dunder) methods that can be defined on classes to make them work within built-in constructs and operators. You can create your own class and if you also define the `__len__`, `__iter__`, and `__contains__` methods you have also defined a new type of container object. 258 | 259 | The `__len__` method is used to return the length of the container. This is used by the built-in `len` function. 260 | 261 | The `__iter__` method is used to return an iterator over the contents of the container. This is used by looping constructs. 262 | 263 | The `__contains__` method is used to determine if an object is in the container. This is used by the `in` operator. 264 | 265 | The `BaseStorage` class implements these methods and it is why we can iterate over the storage backend instance in the templates and determine who many messages there are. 266 | 267 | 268 | ## 4. Deep Dive: Software Architectural Features 269 | 270 | ### Inheritance 271 | 272 | In object oriented programming (OOP) inheritance is used to share code between objects and build upon the functionality provided in other classes. In Python this syntax is: 273 | 274 | ```python 275 | class MyBackend(BaseStorage): 276 | ... 277 | ``` 278 | 279 | This means that the `MyBackend` class extends the `BaseStorage` class. All of the methods and attributes defined on the `BaseStorage` class are now available to the `MyBackend` class. In this example the `BaseStorage` class is referred to as the parent class of `MyBackend`. 280 | 281 | In the messages framework we see the use of inheritance by defining the `BaseStorage` parent class that all the child classes must inherit from. There is no need to write any other code than the two methods that must get implemented (`_get` and `_store`). All other interface code is shared among all storage backends, because they must all extend from the same parent class. 282 | 283 | ### Singleton pattern 284 | 285 | In OOP it is sometimes common to make sure there is only ever one instance of a class. This is called the [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern). Code is usually written in the class constructors to make sure that it is only able to be instantiated once. 286 | 287 | In Python this can be accomplished by simply writing a module (file). Even if the module is imported more than once of under different names, there will only be one instance of it. This acts as a singleton. 288 | 289 | The messages API is implemented as a singleton. You import `from django.contrib import messages`, which pulls in the module. Functions defined in the `api.py` file are accessed as methods on an instance. 290 | 291 | To illustrate this more simply we can do the following. We will create a file called `single.py` with the following contents. 292 | ```python 293 | x = 1 294 | 295 | 296 | def calc(num): 297 | return num * x 298 | ``` 299 | Then we will start our REPL and do the following. 300 | ```python 301 | >>> import single 302 | >>> single.x 303 | 1 304 | >>> single.calc(2) 305 | 2 306 | >>> single.x = 10 307 | >>> single.x 308 | 10 309 | >>> single.calc(2) 310 | 20 311 | ``` 312 | Note that `single` here looks just like an instance of a class with attributes that can be changed and methods that can be called. 313 | 314 | Now, in that same REPL session, if we import the `single` module again but using a different name, we can see that we will always just have one instance of the module. 315 | 316 | ```python 317 | ... 318 | >>> import single as s 319 | >>> s.x 320 | 10 321 | ``` 322 | 323 | ## 5. Hands-on Exercises 324 | 325 | **Implement a new messages storage backend that uses Django's caching framework.** 326 | 327 | The hints section below is to help you come up with a solution to this problem. The possible solution section below shows one possible solution to this problem. Try out your own solution first with the help of the hints before taking a look at the possible solution section. 328 | 329 | ### Hints 330 | 331 | * You can get access to the low-level Django cache framework by importing it `from django.core.cache import cache`. See Django's [low-level cache documentation](https://docs.djangoproject.com/en/1.11/topics/cache/#the-low-level-cache-api). 332 | * You can set and get values from the cache framework using the `set` and `get` methods. 333 | * Remember that you probably want to store messages on a per user basis. 334 | * Don't forget to change the `MESSAGE_STORAGE` setting to point to your custom backend. 335 | 336 | ### Possible Solution 337 | 338 | Here is one possible solution. One major problem with this solution is that it will only store messages in the cache based on the username of the user. This is a problem in that if you are storing messages in views that don't require authentication, than any user that is not authenticated will have a blank username and all messages will be stored in the same place for those users. So, this implementation is not perfect, but it will work in authenticated views. 339 | 340 | ```python 341 | from django.contrib.messages.storage.base import BaseStorage 342 | from django.core.cache import cache 343 | 344 | 345 | class CacheStorage(BaseStorage): 346 | 347 | def cache_key(self): 348 | return f'messages-for-{self.request.user.username}' 349 | 350 | def _get(self, *args, **kwargs): 351 | return (cache.get(self.cache_key()) or [], True) 352 | 353 | def _store(self, messages, response, *args, **kwargs): 354 | if messages: 355 | cache.set(self.cache_key(), messages) 356 | else: 357 | cache.set(self.cache_key(), []) 358 | return [] 359 | ``` 360 | 361 | ## 6. Contribute 362 | 363 | ### Resources 364 | 365 | * [contrib.messages documentation](https://docs.djangoproject.com/en/1.11/ref/contrib/messages/) 366 | * [contrib.messages open tickets](https://code.djangoproject.com/query?status=assigned&status=new&component=contrib.messages&col=summary&col=status&col=owner&col=type&col=version&col=has_patch&col=needs_docs&col=needs_tests&col=needs_better_patch&col=easy) (refer to [Understanding Django's Ticketing System]() for more details) 367 | --------------------------------------------------------------------------------