From a3a50bf892257fd88d284a6354d4809c962189bb Mon Sep 17 00:00:00 2001 From: "shoden@gmail.com" Date: Wed, 14 Aug 2024 12:07:28 -0700 Subject: [PATCH] Edit tutorial for clarity --- docs/source/tutorials/intro.md | 6 +- docs/source/tutorials/mealplan/django-app.md | 26 +- .../source/tutorials/mealplan/finished-app.md | 47 + .../tutorials/mealplan/full-text-tutorial.md | 960 ------------------ .../tutorials/mealplan/img/mealplan-form.png | Bin 0 -> 24081 bytes .../tutorials/mealplan/img/mealplan-home.png | Bin 0 -> 13176 bytes .../tutorials/mealplan/img/mealplan-saved.png | Bin 0 -> 14479 bytes docs/source/tutorials/mealplan/summary.md | 2 +- .../tutorials/mealplan/unicorn-advanced.md | 4 +- .../mealplan/unicorn-functionality.md | 14 +- 10 files changed, 80 insertions(+), 979 deletions(-) create mode 100644 docs/source/tutorials/mealplan/finished-app.md delete mode 100644 docs/source/tutorials/mealplan/full-text-tutorial.md create mode 100644 docs/source/tutorials/mealplan/img/mealplan-form.png create mode 100644 docs/source/tutorials/mealplan/img/mealplan-home.png create mode 100644 docs/source/tutorials/mealplan/img/mealplan-saved.png diff --git a/docs/source/tutorials/intro.md b/docs/source/tutorials/intro.md index 68bc3cc2..9e2556db 100644 --- a/docs/source/tutorials/intro.md +++ b/docs/source/tutorials/intro.md @@ -4,12 +4,14 @@ This tutorial is intended to familiarize you with some basic concepts around Django Unicorn. If you have little to no prior experience with Django or other web frameworks, this is a good place to start. However, having basic knowledge of Python and web development concepts will prove helpful. ``` -The goal of this tutorial is to develop a Meal Plan application. With Django Unicorn, you will be able to create "Meals" and add them without refreshing the page. You will also be able to search dynamically for saved Meals. +The goal of this tutorial is to develop a Meal Plan application. With Django Unicorn, you will be able to create "Meals" and add them without refreshing the page. This will include basic form validation, as well as a way to clear your list automatically. + +Let's dive in! ```{toctree} mealplan/django-app mealplan/unicorn-functionality mealplan/unicorn-advanced +mealplan/finished-app mealplan/summary -mealplan/full-text-tutorial.md ``` \ No newline at end of file diff --git a/docs/source/tutorials/mealplan/django-app.md b/docs/source/tutorials/mealplan/django-app.md index a2ec80e4..2da6be06 100644 --- a/docs/source/tutorials/mealplan/django-app.md +++ b/docs/source/tutorials/mealplan/django-app.md @@ -2,7 +2,9 @@ ## Installation -Note: You must have Python installed before starting. +```{note} +You must have Python installed before starting. +``` To start with, create a new directory in your terminal where you will build your project. Once you are in this new directory, create a virtual environment and then activate it. @@ -24,6 +26,10 @@ Now install Django and Django Unicorn. > python -m pip install django-unicorn ``` +```{note} +You can [use other package managers](../../installation.md) for installation as well. +``` + Now you're ready to create a new Django project. This command will populate your directory with your Django project files and directories. ```shell @@ -129,7 +135,7 @@ Running migrations: Applying sessions.0001_initial... OK ``` -So let's create a model for our meals. +Next, let's navigate to the `mealplan/models.py` file and create a model. ```python # mealplan/models.py @@ -165,13 +171,15 @@ And then, to apply those changes to the database: python manage.py migrate mealplan ``` -Note: Any time you add or make changes to your models, you need to run these commands to make sure the changes apply to the database. +```{note} +Any time you add or make changes to your models, you need to run these commands to make sure the changes apply to the database. +``` ## URLs Before we create a page that we can actually _see_, we need to configure the URLs that will lead to our (eventual) content. -Django automatically creates a URL leading to an "Admin" section (you can read more about that in the official Django tutorial). +Django automatically creates a URL leading to an "Admin" section (you can [read more about that in the official Django tutorial](https://docs.djangoproject.com/en/5.1/intro/tutorial02/#introducing-the-django-admin)). ```python # app/urls.py @@ -184,7 +192,7 @@ urlpatterns = [ ] ``` -We want to go ahead and add a pattern that will lead to any URLs defined in your `mealplan` app. Also, Django Unicorn utilizes its own pattern which needs to be added also. +We want to go ahead and add a pattern that will lead to any URLs defined in your `mealplan` app. Also, Django Unicorn utilizes its own pattern which needs to be added. ```python # app/urls.py @@ -249,7 +257,7 @@ Here, we're trying to render a template called `meals.html` (it doesn't exist ye ## Templates -In Django, there is a naming convention that organizes how _views_ link up to templates, and it relies on a certain directory structure. To create this first template, we need to add two directories and a file like so: +Django organizes how _views_ link up to templates based on a certain directory structure and naming convention. To accommodate for this, we need to add two directories and a file like so: ``` mealplan/ @@ -266,7 +274,7 @@ mealplan/ ┗ __init__.py ``` -Django templates allow you to insert Python into your HTML. Let's build a quick template that will show us a list of all our meals (hint: we don't have any saved yet!) +Django templates allow you to insert Python into your HTML. Let's build a quick HTML file that will show us a list of all our meals (hint: we don't have any saved yet!) :::{code} html :force: true @@ -313,7 +321,7 @@ Change the `
` section to look like this: ... -
+
{% if not meals %}

No meals have been prepared yet!

{% else %} @@ -324,7 +332,7 @@ Change the `
` section to look like this: {% endif %} -
+
... diff --git a/docs/source/tutorials/mealplan/finished-app.md b/docs/source/tutorials/mealplan/finished-app.md new file mode 100644 index 00000000..2b90871f --- /dev/null +++ b/docs/source/tutorials/mealplan/finished-app.md @@ -0,0 +1,47 @@ +# Finished App + +If you have followed along, your app should be ready to run! + +However, in case you missed something, you can reference the [final code for this tutorial](https://github.com/tataraba/django-unicorn-tutorial-app) on GitHub. + +```{note} +The example on GitHub also registered the `Meal` in the `mealplan/admin.py` module, making it available in the Django Admin section. This allows you to create/delete Meals by creating and logging in to your site's admin section. You can read more about the [admin section on the Django documentation](https://docs.djangoproject.com/en/5.0/intro/tutorial02/#introducing-the-django-admin). +``` + +By this point, you should be able to run your app with the Django command. + +```shell +python manage.py runserver +``` + +You can now go to [http://127.0.0.1:8000](http://127.0.0.1:8000) on your browser to see your app in action. + +When you first open the app, it should look something like this: + +```{image} img/mealplan-home.png +:alt: Browser displaying Meal Plan app with Add a Meal button +:class: bg-primary +:width: 400px +:align: center +``` + +When you click to "Add a Meal," you will then see the form populate below. + +```{image} img/mealplan-form.png +:alt: Browser displaying Meal Plan app with input elements for Meal +:class: bg-primary +:width: 400px +:align: center +``` + + +And finally, after adding a few meals, you should see a list of them populate, as well as a button to _clear_ the list and start over. + +```{image} img/mealplan-saved.png +:alt: Browser displaying Meal Plan app with two listed items +:class: bg-primary +:width: 400px +:align: center +``` + +Give it a try! diff --git a/docs/source/tutorials/mealplan/full-text-tutorial.md b/docs/source/tutorials/mealplan/full-text-tutorial.md deleted file mode 100644 index 5d6b1867..00000000 --- a/docs/source/tutorials/mealplan/full-text-tutorial.md +++ /dev/null @@ -1,960 +0,0 @@ -(beginner-tutorial)= -# Beginner Tutorial - -Note: Who is this for? -This tutorial is intended to familiarize you with some basic concepts around Django Unicorn. If you have little to no prior experience with Django or other web frameworks, this is a good place to start. However, having basic knowledge of Python and web development concepts will prove helpful. - -The goal of this tutorial is to develop a Meal Plan application. With Django Unicorn, you will be able to create "Meals" and add them without refreshing the page. You will also be able to search dynamically for saved Meals. - - -## Installation - -Note: You must have Python installed before starting. - -To start with, create a new directory in your terminal where you will build your project. Once you are in this new directory, create a virtual environment and then activate it. - -```shell -# Inside your project directory -> python -m venv .venv - -# activate environment (MacOs or Linux) -> source .venv/bin/activate -``` - -Now install Django and Django Unicorn. - -```shell -# This installs the Django web framework -> python -m pip install Django - -# This installs Django Unicorn -> python -m pip install django-unicorn -``` - -Now you're ready to create a new Django project. This command will populate your directory with your Django project files and directories. - -```shell -django-admin startproject app . -``` - -(Typing the period `.` in the end will ensure Django is created in your current directory. You can substitute `app` with any other name.) - -Once you have created your project, let's build your "Meal Plan" application. - -## Create Meal Plan App - -You should now have a basic project structure that looks a little like this: - -``` -my-project/ -┣ .venv/ -┣ mealplan/ -┃ ┣ asgi.py -┃ ┣ settings.py -┃ ┣ urls.py -┃ ┣ wsgi.py -┃ ┗ __init__.py -┗ manage.py -``` - -In order to create your Meal Plan application, make sure that you are in the `my-project` directory and type the following command. - -```shell -python manage.py startapp mealplan -``` - -In this case, `mealplan` is the name of your app. You can choose to name it something different if you prefer. - -After running the command, your file structure should look like this. - -``` -my-project/ -┣ .venv/ -┣ app/ -┃ ┣ asgi.py -┃ ┣ settings.py -┃ ┣ urls.py -┃ ┣ wsgi.py -┃ ┗ __init__.py -┣ mealplan/ -┃ ┣ migrations/ -┃ ┣ admin.py -┃ ┣ apps.py -┃ ┣ models.py -┃ ┣ tests.py -┃ ┣ views.py -┃ ┗ __init__.py -┗ manage.py -``` - -Next, we need to register your `mealplan` app, as well as `Django Unicorn` (which we installed earlier) into the `my-project/app/settings.py` file. - -Find the `INSTALLED_APPS` and include them like this: - -```python -# app/settings.py - -INSTALLED_APPS = [ -    'django.contrib.admin', -    'django.contrib.auth', -    'django.contrib.contenttypes', -    'django.contrib.sessions', -    'django.contrib.messages', -    'django.contrib.staticfiles', -    'django_unicorn', -    'mealplan', -] -``` - -## Creating Models - -We will use SQLite as a database for our meal plans. This setting already comes configured with Django, but we must first _create a migration_ which will allow us to use Models to represent the data in the database. - -Run the command `python manage.py migrate` and you should see something like this: - -```shell -Operations to perform: - Apply all migrations: admin, auth, contenttypes, sessions -Running migrations: - Applying contenttypes.0001_initial... OK - Applying auth.0001_initial... OK - Applying admin.0001_initial... OK - Applying admin.0002_logentry_remove_auto_add... OK - Applying admin.0003_logentry_add_action_flag_choices... OK - Applying contenttypes.0002_remove_content_type_name... OK - Applying auth.0002_alter_permission_name_max_length... OK - Applying auth.0003_alter_user_email_max_length... OK - Applying auth.0004_alter_user_username_opts... OK - Applying auth.0005_alter_user_last_login_null... OK - Applying auth.0006_require_contenttypes_0002... OK - Applying auth.0007_alter_validators_add_error_messages... OK - Applying auth.0008_alter_user_username_max_length... OK - Applying auth.0009_alter_user_last_name_max_length... OK - Applying auth.0010_alter_group_name_max_length... OK - Applying auth.0011_update_proxy_permissions... OK - Applying auth.0012_alter_user_first_name_max_length... OK - Applying sessions.0001_initial... OK -``` - -So let's create a model for our meals. - -```python -# mealplan/models.py - -from django.db import models - -class Meal(models.Model): - TYPE_OF_MEAL = { - "B": "Breakfast", - "L": "Lunch", - "D": "Dinner", - "S": "Snack", - } - name = models.CharField(max_length=100) - main_dish = models.CharField(max_length=50) - side_dish = models.CharField(max_length=50, blank=True) - desert = models.CharField(max_length=50, blank=True) - type_of_meal = models.CharField(max_length=1, choices=TYPE_OF_MEAL.items(), blank=True) - - def __str__(self): - return self.name -``` - -Now our model is in our codebase, but we also need to add this table to our database. First, we prepare a migration file with instructions on how to do that with the following Django command. - -```shell -python manage.py makemigrations mealplan -``` - -And then, to apply those changes to the database: - -```shell -python manage.py migrate mealplan -``` - -Note: Any time you add or make changes to your models, you need to run these commands to make sure the changes apply to the database. - -## URLs - -Before we create a page that we can actually _see_, we need to configure the URLs that will lead to our (eventual) content. - -Django automatically creates a URL leading to an "Admin" section (you can read more about that in the official Django tutorial). - -```python -# app/urls.py - -from django.contrib import admin -from django.urls import path - -urlpatterns = [ -    path('admin/', admin.site.urls), -] -``` - -We want to go ahead and add a pattern that will lead to any URLs defined in your `mealplan` app. Also, Django Unicorn utilizes its own pattern which needs to be added also. - -```python -# app/urls.py - -from django.contrib import admin -from django.urls import path, include # Added include - -urlpatterns = [ - path('admin/', admin.site.urls), - path("", include("mealplan.urls")), # Added for your app - path("unicorn/", include("django_unicorn.urls")), # Added for Django Unicorn -] -``` - -Django will now redirect everything that goes to your index page to `mealplan.urls`, but you'll note that there is currently not a module (a `.py` file) by that name in your `mealplan` directory. You will need to create it! - -``` -mealplan/ -┣ migrations/ -┣ admin.py -┣ apps.py -┣ models.py -┣ tests.py -┣ urls.py # New! -┣ views.py -┗ __init__.py -``` - -In that file, you can define the URLs and what "Views" will be rendered. - -```python -# mealplan/urls.py -from django.urls import path -from . import views - -urlpatterns = [ - path('', views.index, name='index'), -] - -``` - -We have not yet created a View called `index` yet, but we're getting there! - -## Views - -In Django, a _view_ is often where you might include the application logic. It also specifies what will be rendered on the browser, typically HTML contained in a _template_ file. - -You should already have a `view.py` file created for your `mealplan` app. Let's open it up and try to render a page. - -```python -# mealplan/views.py - -from django.shortcuts import render - - -def index(request) : - return render(request, "mealplan/meals.html") - -``` - -Here, we're trying to render a template called `meals.html` (it doesn't exist yet). So let's go ahead and build that template, shall we? - -## Templates - -In Django, there is a naming convention that organizes how _views_ link up to templates, and it relies on a certain directory structure. To create this first template, we need to add two directories and a file like so: - -``` -mealplan/ -┣ migrations/ -┣ templates/ # New! -┃ ┗ mealplan/ # New! -┃ ┗ meals.html # New! -┣ admin.py -┣ apps.py -┣ models.py -┣ tests.py -┣ urls.py -┣ views.py -┗ __init__.py -``` - -Django templates allow you to insert Python into your HTML. Let's build a quick template that will show us a list of all our meals (hint: we don't have any saved yet!) - -:::{code} html -:force: true - - - - - - Meal Plan - - - -
-
-

Meal Plan

-
-
-

No meals have been prepared yet!

- -
-
-
- 2024 © -
-
- - - -::: - -A couple things to note. - -We are using a CDN to link to a [SimpleCSS stylesheet](https://https://simplecss.org). This applies specific styling to our webpage. Ordinarily, you would store your CSS files in a Django `static` directory. You can read more about that in the [official Django documentation](https://docs.djangoproject.com/en/5.0/howto/static-files/). - -So far, we have a message stating that no meals have been created, as well as a button that is supposed to let us add a meal to our database. - -Let's add some logic in the template that will look for a `meals` object (which could be a list of meals saved in our database). If it finds an object (a list of meals), we will _do something_, otherwise, we'll display the message that no meals have been created. - -Change the `
` section to look like this: - -:::{code} html -:force: true - - -... - -
- {% if not meals %} -

No meals have been prepared yet!

- {% else %} -
    - {% for meal in meals %} -
  • {{ meal.name }}
  • - {% endfor %} -
- {% endif %} - -
- -... - -::: - -The _something_ we are doing is iterating over the `meals` object and creating a new list item (`
  • `) for every meal that we find, and then displaying the name of that meal. - -We'll see that in action eventually, but first, we need to provide an ability to create a meal item. - -## Creating a Form - -In order to allow a user to input the data that we need for our `Meal` table in the database, we have to create an HTML form that allows a user to do that. - -Django allows us to create a `Form` object similar to how we created a `Model`, which we could then feed directly to our `template`, which in turn would render the appropriate HTML form in the browser. - -However, when using a Django Unicorn "component", you _cannot_ send the `Form` object to the template and render it the same way. But, _we can still use_ a Django `Form` object in order to _validate_ the input data. - -This might make more sense when we see it in action. So for now, let's go ahead and create a `Form` object. - -Similar to the `url.py` module, we will need to create a `forms.py` file for our `mealplan` app. - -``` -mealplan/ -┣ migrations/ -┣ admin.py -┣ apps.py -┣ forms.py # New! -┣ models.py -┣ tests.py -┣ urls.py -┣ views.py -┗ __init__.py -``` -The `MealForm` model below is linked to the `Meal` object that links to our database. This `ModelForm` now has the _constraints_ we defined in the model (such as the `max_length` of any given field, or whether a field is required or not). - -```python -# mealplan/forms.py - -from django.forms import ModelForm -from .models import Meal - -class MealForm(ModelForm): - class Meta: - model = Meal - fields = '__all__' - labels = { - "name": "Name", - "main_dish": "Main dish", - "side_dish": "Side dish", - "desert": "Desert", - "type_of_meal": "Type of meal", - } - -``` - - -## Django Unicorn Setup - -Django Unicorn uses the term "[Component](https://www.django-unicorn.com/docs/components/)" to refer to a set (or a block) of interactive functionality. Similar to how your Django _views_ connect to your _templates_, `Unicorn` uses a special [view class](https://www.django-unicorn.com/docs/views/) (`UnicornView`) residing within a special `components` directory linking to a specific _template_ with the _same name_ as the `component` module. - -Phew. That's a lot of words. Perhaps it's easier if you see it. - -While you can create the directory structure manually, Django Unicorn also includes a special command that will do this for you. - -In your terminal, type the following command: - -```shell -python manage.py startunicorn mealplan create-meal -``` - -This command tells unicorn to create a component/template combo in your `mealplan` app, with the name of `create-meal.py` and `create-meal.html` respectively. - -After you successfully run the command, your file structure will look like this: - -``` -mealplan/ -┣ components/ # New! -┃ ┣ create_meal.py # New! -┃ ┗ __init__.py # New! -┣ migrations/ -┣ templates/ -┃ ┣ mealplan/ -┃ ┃ ┗ meals.html -┃ ┗ unicorn/ # New! -┃ ┗ create-meal.html # New! -┣ admin.py -┣ apps.py -┣ forms.py -┣ models.py -┣ tests.py -┣ urls.py -┣ views.py -┗ __init__.py -``` - -You could create those files marked as `#New!` manually, but the command makes it easy for you. - -If you build additional components, you would create new files in the `components` directory as well as in the `template/unicorn`directory with the same name (note the distinction between the `.py` and `.html` extensions, as well as the hyphen and underscore). - -In order to use components within your regular Django templates, you need to "include" them within your HTML file. - -Let's go back to `index.html` and add that. - -:::{code} html -:force: true - - -{% load unicorn %} - - -... -::: - -There are two more items that you need to add to your templates to ensure Django Unicorn functions properly. The first is a `{% unicorn_scripts %}` tag. You can place that in the `` element in your HTML. - -:::{code} html -:force: true - - -... - - Meal Plan - - {% unicorn_scripts %} - -... -::: - -And secondly, a `{% csrf_token %}` tag within the body of your HTML. We can include it near the end. - -:::{code} html -:force: true - - -... - - {% csrf_token %} - - -::: - -Note: In case you missed it earlier and if you haven't already done so, make sure that `django_unicorn` is listed within your `INSTALLED_APPS` in your `settings.py` file. - -## Components - -Components are where much of the Django Unicorn heavy lifting occurs. Here, we will define a `UnicornView`, which in turn contains the back end logic which will be passed to the corresponding `template`. - -The interaction between the component and the tutorial is unique to this pairing, and it is "included" in your Django templates with a special template tag. The tag contains the name `unicorn`, followed by the name of the template, which in turn corresponds to the component. - -For example, to load the component we created earlier, we would add this template tag to our `meal.html` template. (Here, it is included within then `
    ` element). - -:::{code} html -:force: true - - -... - -
    -
    -

    Meal Plan

    -
    -
    - {% unicorn "create-meal" %} -
    -
    - - {% csrf_token %} -
    - -... -::: - -Now we can define what actually goes in the `create-meal.html` template. - -:::{code} html -:force: true - - -{% load unicorn %} -
    -
    - {% if not meals %} -

    No meals yet

    - {% else %} - {% for meal in meals %} -

    {{ meal.name }}

    - {% endfor %} - {% endif %} -
    -
    -::: - -Notice the `unicorn:model` attribute on the `
    ` element. This is what "binds" this element to the logic we will write next in the `create_meal.py` component. - -Note: The term `model` in this particular context _does not_ correlate to a Django `Model`. In other words, `unicorn:model` is what enables reactivity. Django Unicorn holds the fields from the component (`create_model.py`) in a special context. Then, when the element with `unicorn:model` triggers a change (whether on load, click, submit, blur, etc...), then it sends an AJAX request to a specific Unicorn endpoint, and the response is rendered in place. You don't necessarily have to understand all of that, but it's worth noting that `unicorn:model` is _not_ referring to your Django `Model` directly. - -Our last piece here is to actually write some logic in the component. - -In our `create_meal.py` file, we will create a `UnicornView` which will handle our backend logic. - -If you noticed in our template code above, we want to check to see if we have any "Meals" in the database. If none are found, we want to display a message stating as so. However, if we do find any meals, we want to iterate through them and list the `name` of that meal. - -So in our component, we will defined a "field" corresponding to a list of Meals in our database. - -```python -# mealplan/components/create_meal.py - -from django_unicorn.components import UnicornView -from mealplan.models import Meal - -class CreateMealView(UnicornView): - meals: list[Meal] = None - - def mount(self): - self.meals = Meal.objects.all() - -``` - -The name of your view should match the name of your component (but in CamelCase). We're defining a field called `meals` which will correspond to any meals we are able to load from the database. Lastly, we define the `mount` method. This method will get called when the component gets initialized or reset. - -Since we don't currently have any Meals in the database, this may not seem like it is doing much. - -Let's add some cool functionality. - - -## Adding Meals - -We have everything set in order to start using our component in very powerful ways. We can now interact with our `CreateMealView` from within any element in the `create-meal.html` template, which enables us to make powerful, reactive functionality _without the need_ of writing any JavaScript. - -Next, what we want to do is provide a user with a form so that they can create a new meal to save into the database. But we want to keep that form hidden, unless the user clicks on a button to "Add a Meal." - -In our `CreateMealView`, we are going to create a field called `state` which will determine whether a user sees a form or not. - -```python -# mealplan/components/create_meal.py - -... -class CreateMealView(UnicornView): - meals: list[Meal] = None - - def mount(self): - state: str = "Add" - self.meals = Meal.objects.all() -``` - -The value of `state` will be sent over to our template. So now we can create a condition in our template to look for that value. - -:::{code} html -:force: true - - -
    - {% if state == "Add" %} - - {% if meals %} - - {% endif %} -
    - {% else %} - -
    -
    -::: - -The line `{% if state == "Add" %}` compares the `state` field in the component to its value. - -Since that is the value that is initially set, we are rendering an "Add a Meal" button. Additionally, if we find any value in the `meals` field (again, in the component), then we will also render a "Clear Meals" button. For now, it doesn't do anything. - -But what happens when we _click_ the "Add a Meal" button? - -In this case, `unicorn:click` checks our component for a method called `add`, but we have not created it yet, so let's go ahead and do that (along with some logic for the `cancel` action). - -```python -# mealplan/components/create_meal.py - -... -class CreateMealView(UnicornView): - state: str = "Add" - meals: list[Meal] = None - - def mount(self): - self.meals = Meal.objects.all() - - def add(self): - self.state = "Cancel" - - def cancel(self): - self.reset() - self.state = "Add" -``` - -Now, when a user clicks on the "Add a Meal" button, the _value_ of the `state` field is going to change to "Cancel". - -Our template logic will no longer see "Add" as the value to `state`, so the button that will render next is the "Cancel" button. - -Similarly, to change the button back, you follow a similar logic. The only difference we see here is the addition of the `discard` directive. This is to prevent any model updates from being saved (we're getting there). - -However, we still need to display the form to users so that they can input a meal. I'm going to provide the entire `create-meal.html` file here and then we can build out the component in the next section. - -:::{code} html -:force: true - - -{% load unicorn %} -
    -
    - {% if not meals %} -

    No meals yet

    - {% else %} - {% for meal in meals %} -

    {{ meal.name }}

    - {% endfor %} - {% endif %} -
    -
    - {% if state == "Add" %} - - {% if meals %} - - {% endif %} -
    - {% else %} - -
    -
    -
    - - -  {{ unicorn.errors.name.0.message }} -
    -
    - - -  {{ unicorn.errors.main_dish.0.message }} -
    -
    - - -
    -
    - - -
    -
    - - -
    -

    - -

    - {% endif %} -
    -::: - -You'll notice that after the `{% else %}` statement that includes the "Cancel" button, we are also including `` fields that are linked to the `unicorn:model`. Remember, this is _not_ directly the `Meal` model that is connected to our database. Rather, it is what is _binding_ the template to our component. - -Each input is linked to a field in the component that matches the assignment. In other words, the `` line is looking for a field called `name` in our component. (Hint: we haven't created that field yet.) - -Also, you'll notice the `defer` directive on these inputs. This is is done to store and save model changes until the next action gets triggered (in our case, clicking the "Save" button). - -## Backend Logic - -We've provided the _ability_ for users to enter meals to the form, but so far, that won't do anything. - -As I mentioned in the previous section, the fields referenced in the `` elements haven't been defined. Let's go ahead and add those fields to our `CreateMealView`. - -```python -# mealplan/components/create_meal.py - -... -class CreateMealView(UnicornView): - state: str = "Add" - meals: list[Meal] = Nonename = None - main_dish = None - side_dish = None - desert = None - type_of_meal = None - -``` - -Now take a look at the "submit" button in the `create-meal.html` file. It is looking for a method called `create`, which should handle saving the data from the form to our database. - -Django allows us to create a new `Meal` in the database by passing values to the corresponding fields. For the sake of convenience, here is what our model looks like. - -```python -# mealplan/models.py - -from django.db import models - -class Meal(models.Model): - TYPE_OF_MEAL = { - "B": "Breakfast", - "L": "Lunch", - "D": "Dinner", - "S": "Snack", - } - name = models.CharField(max_length=100) - main_dish = models.CharField(max_length=50) - side_dish = models.CharField(max_length=50, blank=True) - desert = models.CharField(max_length=50, blank=True) - type_of_meal = models.CharField(max_length=1, choices=TYPE_OF_MEAL.items(), blank=True) - - def __str__(self): - return self.name -``` - -In order to create a record in our database, we can use the `Model.objects.create()` method, where the `Model` in question is an instance of `Meal` we have defined above. - -So now, we can introduce a new method in our `CreateMealView` class like this: - -```python -# mealplan/components/create_meal.py - -... -class CreateMealView(UnicornView): - state: str = "Add" - meals: list[Meal] = None - - def mount(self): - self.meals = Meal.objects.all() - - def add(self): - self.state = "Cancel" - - def cancel(self): - self.reset() - self.state = "Add" - - def create(self): - _new_meal = Meal.objects.create( - name=self.name, - main_dish=self.main_dish, - side_dish=self.side_dish, - desert=self.desert, - type_of_meal=self.type_of_meal - ) - self.state = "Add" -``` - -This will create a `_new_meal` when a user clicks on the "Save" button, and it will also turn the `state` field into "Add", which means that our template will hide the form automatically! - -## A Few More Things - -We've mostly got things working, but there are still some oddities to work out. - -For example, we can currently save Meals to our database, but we don't have a way to remove them. Also, when we try to add _new_ Meals by clicking on "Add a Meal," the values of the previous Meal are pre-populated in the `` elements. Additionally, the list of Meals does not refresh after clicking the "Save" button. - -First, let's add a method that will remove all the meals we have previously saved. Remember in our template, we have a `unicorn:click.discard="cancel"...` attribute, which means we can create a `cancel` method in our component. - -```python -def clear(self): - _remove_all_meals = Meal.objects.all().delete() - self.mount() -``` - -The `self.mount()` method is called after we delete all the meals, which refreshes the component back to the initial state. (Without it, the list of Meals won't disappear until you refresh the component or page.) - -Secondly, let's remove the values that appear on the `` elements after saving a Meal. What we can do is add a _special_ method to the `add` method in our component. It will refresh the data so that when the template loads, the items disappear. Django Unicorn provides us with a special method within the `UnicornView` called `reset()`. - -```python -def add(self): - self.reset() - self.name = None - self.main_dish = None - self.side_dish = None - self.desert = None - self.type_of_meal = None - self.state = "Cancel" -``` - -And finally, to refresh the list of Meals after saving an object, we just need to refresh the `meals` field in the component. We can do that by making a call to the database after saving. - -```python -def create(self): - _new_meal = Meal.objects.create( - name=self.name, - main_dish=self.main_dish, - side_dish=self.side_dish, - desert=self.desert, - type_of_meal=self.type_of_meal - ) - - self.meals = Meal.objects.all() - self.state = "Add" -``` - -There is still one last feature we will examine in this tutorial. - - -## Validation - -Remember how we created a `MealForm` in the `forms.py` module? - -What can we do with that? - -With Django Unicorn, we can use it to validate the data a user might type into the form. Currently, a user could theoretically save an empty form, or include more than the allowed characters on each field. - -Let's import our `MealForm` into the `create_meal.py` component, and then add a `form_class` field that references this form. - -```python -# mealplan/components/create_meal.py - -from django_unicorn.components import UnicornView - -from mealplan.forms import MealForm -from mealplan.models import Meal - -class CreateMealView(UnicornView): - state: str = "Add" - meals: list[Meal] = None - - form_class = MealForm - name = None - main_dish = None - side_dish = None - desert = None - type_of_meal = None -... -``` - -Now, Django Unicorn knows that we are using the `MealForm` to validate the data we are receiving from the related inputs. - -We've included a `{{ unicorn.errors.name.0.message }}` template tag to display any validation errors that occur for the `name` field. - -If you look at the model definition, you'll note that the field cannot be longer than 100 characters. And since the `blank=True` is not part of its definition, it means that it is also a _required_ field. - -Although our form will tell us of these validation errors, we also don't want to allow users to be able to _save_ the models if they fail validation. - -Django Unicorn provides another special method within a `UnicornView` class to check for validation. We can use that to update our `create` method within the component. - -```python -def create(self): - if not self.is_valid(): - return - - _new_meal = Meal.objects.create( - name=self.name, - main_dish=self.main_dish, - side_dish=self.side_dish, - desert=self.desert, - type_of_meal=self.type_of_meal - ) - - self.meals = Meal.objects.all() - self.state = "Add" -``` - -If the data is not valid in the form, the user will see the errors displayed, but they will not be able to `create` the `Meal` object. - -And, putting it all together, your `create_meal.py` component should look something like this: - -```python -# mealplan/components/create_meal.py - -from django_unicorn.components import UnicornView - -from mealplan.forms import MealForm -from mealplan.models import Meal - -class CreateMealView(UnicornView): - state: str = "Add" - meals: list[Meal] = None - - form_class = MealForm - name = None - main_dish = None - side_dish = None - desert = None - type_of_meal = None - - def mount(self): - self.meals = Meal.objects.all() - - def add(self): - self.reset() - self.name = None - self.main_dish = None - self.side_dish = None - self.desert = None - self.type_of_meal = None - self.state = "Cancel" - - def cancel(self): - self.reset() - self.state = "Add" - - def create(self): - if not self.is_valid(): - return - - _new_meal = Meal.objects.create( - name=self.name, - main_dish=self.main_dish, - side_dish=self.side_dish, - desert=self.desert, - type_of_meal=self.type_of_meal - ) - - self.meals = Meal.objects.all() - self.state = "Add" - - def clear(self): - _remove_all_meals = Meal.objects.all().delete() - self.mount() -``` - -Summary >> - -## Summary - -Congratulations! - -By the end of this tutorial, not only were you able to build a small Django application, but you were also able to incorporate Django Unicorn to allow for some awesome, reactive, and dynamic data manipulation without the need to rely on any external frontend frameworks. - -You learned how to create a component/template combo that allows for highly reactive functionality. You were able to change field values dynamically, as well as conduct database operations (creating a record or deleting records) with minimal setup. - -And there's much more to uncover with Django Unicorn. Take a look at the documentation to see what else you can discover! \ No newline at end of file diff --git a/docs/source/tutorials/mealplan/img/mealplan-form.png b/docs/source/tutorials/mealplan/img/mealplan-form.png new file mode 100644 index 0000000000000000000000000000000000000000..7b8e8c67c1fe342892059372c705c500f4a6f182 GIT binary patch literal 24081 zcmeFZcT`jVwl5kGrHLR#lq!k^kS4u`B1NRP2uPD21XOwrQWa@R?@hXZ2%&|5fE1Wj`JUqZnqL!5Xijf@gqe;9}DyxO{Sq|9&uk#>A8LC>ZbjRZ_>*bsAX*32OfNr-D+vhn`zCkp z`t~%uHwP3p+VkNy-sz@fPDf09aDMd+ZkN;Ttb-W()u#Ij;R}e%w!LkAxB#EyCUwlu zvV>o2KY7M>k*53#nmmT7M&bCKg?kk9s&p)yhK%M^P+)lsH{rLc#2O%CoDo)3%w$qn{ml}Q@ChHX( zjwR?!i*@Xl6#VpUhT*p?(fV~C3RI!(VH#Q`vI7}+9nZ*i*Ip$BMV4x1NW)Lkv^~A#3$*P2 zBzvQO$1!LOLWo$2!#0t@+9y-=G*ps&Sp}p~{ zjV!em17}#hG_oCcp=u6+(W%yf{<4YN*pFcy*>cy!FM1%){yaIIXoYoDN(lv={^0{g z0QL-QcErHnievUd?@2CwO7Qr}Q@mMPoIO%NTUGBI8Od|(5Oi2@y7olifN;H8MaHYS zvJ#B{K~4P&?}AteFaS4WaHjZgZii`u<69kT0gqB(n+Zq57Yx?peul8NlndK*Il8ai z6(p1Wb&+(pn3-b!)%UAGR=$J&T~04L{hM=khmX)R9&=)l88yGuvV7vsdwlovW5&yry~{4A zyT^l`4m0dtTBa?S8_eSt+vM+_*ucHCHgKAAYoax)1*a==*!82F2XN;|)Nhe8k7wNZ z4*EP5MCFmnIc$pu26FhuX*BU!y|y=TS=-aEide*a`AXo-j0Z3#{={p~{jihOezthO zEOFnj#MCyz%>6^PJJ`5iisbB{gd;gCZ^ufG`1g4cpFRxoQ_C2@>n?$lA!*oyHrmau z-2gLMue13|w2}LKcQZC1X6JNQ9DcS&i#D|)@+QR#LC5aT1#Hn8`Od!b(yA)X@zb}= z#;G758+3lj(`l!pP*xDq6cQq7BfH3OJ))y_!{L-M*U7yic?hRaAH zpw?k9%)(hwq^81TioN+BW#HzB&epFr%UpN+>~fuFhdPA8=(!6Gd_7Yl_Uk>jK!b2w1&b9e;Qz=t?7=fir4<@mH%>!>%gz#b}xNelIs?R@^CAHS5|{!)*dd7ako zH{VLgAYjTZ5AGQ_M#NCkTQ<4|4GO3KV5^Zj3beafC6yb$_p?_J2cy&S6!r_}ziE3J zqU(czyiKno99a@95k-Qd_r3*acXAVIayAQ@Ef3cOaeYkE`=1idPNL1-T1vBIyvyu< zz^276BnZ4LA4A$B%oBz`O%7vAzb^a0pr!^zI<8O4{AxC))3Y&z!x;Eb2=9e2&Zjx3!8ajT9R7FTVZL`zN)pXrc&}A+%dg$j zDRi%2>?qqoG;K~zHym_fc)?je;Y*|dV!0+rL5Q8PgOTo`rmoLvY4I6@k0pM7K=K!o zFj$-u^pm*pXSQbW6i@rQc*)k4D1BYV0%p!nJh=dy7v&Q@n}OiHMxx#A2M$BSHZ;Yq z^XCLqL4;6_N#;A_C21lo=;rzbJZo)W-|%ZA-Jq?HrKj%Aszhjt|EzAI`lh@&lxSZU z@JGI|OLHW8mL>SjjzjDZ>Z=Lh#&H4cL&$L&AMur1%y11Wp*tsqpcd!GQE+>~W;G6l z(}^d%bla^v@|+QK9Z_pOQJRs}nJaShS+X7u-B8)K9CVv21LHS*5sB!NHDXC+r2s-y zw9a*InJ7yj+vR-x?K{J$HGU8l)JAmr4)a4K&()35QrOFLGH{5yZPq>+k!x)*m^(V7 zvCvwxP|0A4>|;7gf|!ppJm@>^mB*P7U!`FhAuF8H-30%m)E1L8OlWDR(CrS*yufY5 z0vgin2E9nDBc9wq*vb~{pluAj^37{6H7zK};hyKdM5%d< z9HaeCq(s0LYAtv>BwbHjDYHfLyh){cn+rt-dZyl)(E6+#Oc0h4a`oMj>c{;Q3v&Oa zdphR^+ahM7hDvxW-nd9BWCST3u#PI8zIoJ6OnWsGN6={twrX!HsgEA{Of69B?3YG* zSip%(h3O?VMrEJ2(H=4OtR@fJ2lmpAGSfH=9>)_$@5^jD&UrdyxFS#N*~RUn1)59h z)_iwbcjb}Ry%EY+e9L2{H%qcH_G9B#>2!VjyR-07&eb_i2D(@Qa2MF(z|SnFmynf) z5`*`yN(KeO?ta2D1!zI(kA}W-5}#tLo^25I1x^lEyj*-T{iZjheMfjU16I=lukB>R ze*|P!5N0ejb0`x0Vs@DQsvbUULJ+r!4+I5K1#z;=gS6jiTUvVt3dyfO9=OX0J#;&Gsu=W?CCo6S8MR`oIOD_RgX>B=Q zW7ktdW_yqy!I(pjwB#G&u2kJ9q&u#}o1;)`eqkVIj=%sz`o@C2?T9YEJ5&7x`3N3O z;-8;A8HebMmNjM;l&_7Ayp*FMB<*GLRnPVM(gS@aS7=K`8Qitie<&dQ-bT)%M1E5I+5wL}k9k zq_qZ4H8R?Sf79=Wqn!7Hov5?h$OyB1uxaLNOKEzc)}u7@rC5^$JD1_Mzgu~Xb`#s^ zGHU^UQf$kF5MN`A{8p0Wh27NCUc$EW%{)4b1&2@8#@C-%9yjka;+RiSTb1MPgL36P z``ufYb_3JPaBZeRSo7(VxGWa8@?FbggA`7VkOUreErshMt9#j=I#_oJvzpx&E2xa- z8y6_osR;D<{nl&W?c5!TuSgxz5d$&zj?=@!xag8|g)&LF3it``a+AY(O{Y^$XHn9~ zPF!S?^{N&Qa^T%k?(RG=YDOEFG57GWrN-*b6}fGpK-Ymac;JlJocAsj{z!rnc{IIn z_6k+>Ysou(RJr8RuW)9IAzIf)lbJe~(GlWk**$UBTC<3u?ZR~bpO^T58a%NIDBqMU z{3xYGN6_s$q*HCZ@cj2|c3u<)46d&a&A*}Qq$YIPqgBaIT0kD-Ur3O)xM!z&$46e) zshNLU%i+;RbllMqq-6i$^QM~Tc`lQz3yu5!r=~Uq99TkD`o?KhkdZ_{RsGcG$+T4( z_in>c&gZiYuTD=f_yy>+#}KvGh{)1S18Qs6QP+@&a)un9LlO9e)t}R~n6-oL)qX-2 zm&l+^`lG&~U8G&M+fUs5!f4oDS%p^5p^nf@oG+vi?3ONXf!`|kTOcpaQ*SZ*0r~JNw`&dqH{L+q%g%&zb2*S z3O5c@*LN@blY#aetlo~$($hS-V)QMBdz)Z4q>&rvfmh(P%-vCv{d-Sf+w}zzZ%V}+MU^>3ZnJ1E zv2DpDb1{J}MDW^C_L9qun*>#DkuXY1OBqwF@#OH8wza7iW34n(%9@td!KvO0Q-dfh zTuc3U@qPVt5GCjEEbHKC!{OcC*LLBRIE$h$68%>po0rY_&b9@shVyZw2jTT5_V<4? z$Byk*o@7U0GjyXy=lFKUne9>76L0yte5D2AU}w`}a~!}^m4Ad>oN_9^9sUl8xu8k@Rj|#X^d5HWT3?PrLcA2sC=+=QvkKTB7$W?&vK3fs2 zT3<=A_#+st*?NiLW?z8np6|md>(toz-rYt-F)_p!c!(LNhPaH_k7=mh9=0b%mT#=7 z_BTrn@V*Ar&&hHIWnp(EQ_)3fcYK=Deu);;Xj;A_Skx>C`jnkaKFE^p5%lrf9#(7q z{@N<2ZL~Az%)#X#=my_zKg>NZu+6X)xx(+ZU&V2h(#I?tgQX|PAQ=u$Dqx2UM9z=2 zzA!qf`eKZO5X>;2^YjZqC~kq9&z1*>#>*&1;roFE?5E#1`VAdtTDq8Jnd(EhNGPZY zC7{lC@w&6?3Bmc+t}n=PeHf)3r2Z}=DBh53s-L^8aGd5o!F1{0Gz2~2(Ka_8BtnL? zuqU%T?LeG)&3zwGHXNH*7VD3ZFKOitb=NpKK)4(!iGC^QfRX#aY6qZRtT(*Pw{dl4 zY;L<6{PM>b<7Kin_lB$VAW$$vKMe@I^#ox3^m?^FY;K4Z z1PWvM=e_>n3;+M(-v;jgd>ovZ zAjV*u6vUQW@)&qL+Do4w-}hn4u-S&2wrsvn7ab9xov5TaB}AJvS(lp&+%H=dtB6XP z>C_khEJisOg)%DVvSuc??t7JB_-g&BkxKCf(m~tkF#2uR<@E(pF-p)-&T`e`NfyZS zf~*AwScBR7ON?}MsxvuLxioR}#WSuaO_j0zW50jh+ZtK*ueCt?!{4Pg_sa%F|8@;*Eh(Ysk{K%e)n-b;mb;FnE3Wr>d z`njCig9~tFXV_x{Wj)DS%>?bNM6o1xXg!9nv}(a8Zap6a3WeBmapk)YX4$UQppPFT zK88BIp8Q~lV-LKoq$0|T<+Ez_CBmUd|HfqUqS$Wf@slA9xP^$3&9sol-CPZs+>aKsv1}G8``;v%_O_?2siSRaE=ct17Lvn(X_2g7Vis4yAK@ zQi6n@cA{p5HW*dVFY4Hv&?(O3lL23pe^_?1Dj94G#aB|_O<4(5xbA` zIYF0=_rt@YejUzN180hrnNDI;r_3oXRkSH~(qDQq(l%4A@}urrAfCEL)Oq@G+2_x- zqqhCci~8nx&iajPwR*?y?C1d@FAqMrK;H54UGqTS9p}tXn{k69g(&fjx)s*iLfg?n zN3AU1v@GwL{rbSeRqXKE!kjU)Pj+UhK;`GQEJPC$W+QQ2Wf`bp4?dWlZ>?herQymy zWhomR_<3DP3EDfH?q`u*-i2C*!oCo}Z)GN$)Wo9Y-O6|gXAX$klsY%vvvPQPz{pft z)*2H%)@3N7a!jd=2d>-fz3 zKIWHnmJuqS1}~4g;^J5_TDY*YyKoI;%hwXWcmvDUThg`sXG1FoEV1(%bWbz_#D;3a^&8x4s;|5{Q zW%Z5ivyRZyld>uqOM^I8g;0%r^YR7G)g`fuUL10=C*0NtsEMGpNn^6)*Yq zp%5Ang&&TlB1G)u(*(AW)kxr~=-ZPH!<&$-lJ>Z3_50 z;QHVh+Cq5kwMt}>vXUU~#9MZ@dO@Bjyk=gt;w)Vou%QJ#vy^a}B7R1l5?hRoF7B$P z4Mp5L`f};QH<@TP(Q5MgvnP1Np8O0`k3B({@8)?K5JYvcohGRLVa*z8dWpbQr z1#n&P^z`ERvWC6ajpVz&kn~p11?Xw~yBMfh&9nX#Rl)B9`p^Hp_5{J}5Q+_#^my!Png8$F@Hl(BqIdS&~Z(DtUZzi?KGT@N3}!oBT3MwsDgiEK`J=UaEwc_Se2<4l7u*$-!7Yl(4U5vms-Ej z98N%=acl9r6onvvvt2V!DA*HHG51ev1gPOk-d{b|#icRZ@zZ$&se<3rXz z{YVh=?RsGD*VtG+h{fRTuudHXZ5dl^l3kju#=XYyjrYYnGr?bgxnXdd#cZ#Fy|N?j zYYpfNbCfK%`iG6`X2>RXwthlf>IGz3-r75NI-@f#&CS;gW32Ji4+GDjIc7{}ggsoqylADonQ!Y(c8?fPO4|lGYHQlL(kRfy&_IXgGXGr4m9at2ZzSbpN+FO*m}D|`s{rMy?$p7#>?KvA zR9%CuD{Z@8P_W?QU_KzZ(7HTV;}mi0P4_Zsb@f&o33;);l2f`j96gzYmlrP}yjyr|gq0(F`?XGp_thL!8Ym|urox<{D#Ky_eA%haFXF5p<-3|P1n?aCVW zp2_6c_`WygfFF^PWqX|rGC%K*J*&SH z#Gi+NDcwUL&(97re8{D!_6YON#H$5d+$AcuKofoR$Tvp*Rd$xQx`k!A;Dec5^o8U0 zB6hQcriyBnK;Akvj^_?+$^7GUm34p2%@~P)m_1mPxv?BM;NT0o8)0NI!(;A)^#|cv zO^#)^yn-B~hZ`YdYe<#a#+3Fz%dEmpk*!0F9o7-ONi+TDr{2#d{G`Mo2 zkTdOb!a&(L3byLlA;4Px*w;-kQ@7({N?EB(g}(WQ4Drr%y2l1?(72ilHDfIH>K^A`gNBXwbIAK3F1yASee({BHJbf2 z7-`rkx7JS@nj;y?B8#WHF!HuPMsei_dE(}d6376D4R;iXrZjH@&!Zact-paJI$k)iXA0uY!eJPp{rqoxB4AeND2^BE1wH31f7hB_$ zWu64l*ILEvzP$j7(v@_kzlOS!3z{dIy2ki_;A-8eQ-M|=fIy5HbP-{GI6Q zgq91N2Xw>H2l*q*UTQIF^~?2j>h4g{j>-+bQA;inDX8{@*u}cy;^&SLB%s8Iml0$- zCTLQz3_eU(JDL!|Gg-c-GBz<)sx4CCl)P$$B?~Td-z=OhdQ-u3r=@$9Jy`q?-FD%)gSDtFcBfXO$zoAy3jgMV9{vq}Qq@1*BqtSh9bjNJW^A>F(< zMqkeQ@+B$A$}LQs+ehWgbFKcUyDXxux;3GYMS+JG{hRgVR9@?7egEi@CsWwP*T(?0 zWPf`BTYq1=c;26oSU2fuirQg1!IY&vPr(@#PPL(`KZ2pUw@eFetaz!!9v<2Am+;p( zH#genK z)m+68~`jm@5Z z%V%RDy!+A3Km6A4qOfV53HRKr?iTVRwo;s#LJI`C{NZIJna(N|LR<&@hQBln@>RS< zddPKGr+T|BGAKd0hzOizuj+7gDQ_jHW+GNq(oHqHU5=L3_F)GX-a0OJXr!Q*T3|jd z`RTyEv2l4@E$r}1;I$%nxxH4=kG6noz$xN}BFPM>hptOpY{tz~GOhlZ;Y*Yub-^MH z%Acq4601LC^98k*wndPDWo47}>(OeuB7wSB+fXxWT%~soPAOSduBJ)Ho;GaxAWOOC zzQw<&#B`{t?a6^aLeWt-xqXU6%lccr+XLV;MmK_Ll-h7iPl00kYvtY8s!vlX7sDk@ zAE7wQEZ)hu++3a-ymm4=W_9&3CY^QNc?i;E!e#C+tR9pD2vZSiufoW5nx?q&9vLS! zcV8Q#8GRi==hZo$IKEWf$N4q&>2Rf?(BV3a*5)*I+LL$lbOot{+t3$6_m@dB8&zWYSU>>c3*ot!Qb_s9;ZFof`u))ew_{90uJ} zHi&l!Ct>Ai#(dcKzjif%KoLdS!4X;{dUSd8^3jv0jA?TWLO-EqNUUwUj=etudM=z@ zSH11!4=4yAg}7T~=Soe-|JyT|>h{s(iEe|<_}w|5ZX3VlDVOQSEiJ^+H(}^+Md??w zOHHfG1|Iv-lwSP6-3S_TSf@AKT4*-#p@WTfZ9lxsrt|Mi&Gfrp0O`Jn;^Ph|)V#M&vMym@WJH*}}7j{X0WjVQDGO*&LLIE|O0j27lamvxp6UyB@}0I=}X8 zwom{e#h@Vo&M&X8`u@{};LgAZ0(BNi$_QK;^WHU9F|PT}!XJ1{v+=g$HIzZtqK$Lb zdk1MgUDRPTXVBnJviR{eyDMq?wwPDe+CY+@XT>QyPE{7q`3NCe9xh9!!u<61)@FRLVrK5-`oq) zV!!7+dv!i8N?qsX530LB4NP06qV#qhKZO)BXm8FG=f=Iahh{@0!Hpx-wtWnyZ zTRDE=v#wqjI++A?C-3-_I{@q2`r}*l^&WP=R1R~iNP%hji*2Tng8Y(N4xIMPmt11z z>tAPF0S9UvZCQvK&`z(It%8g1c{T_PvfG~|QD2_}K##P_)T{mE6L7+ffyou7$tg6^ zz@yG$LZkIpE~G?oXQWPnlSJhEt2i9@bqfVyWe%o*IVrWW^4lW3?f(7go~JQCG5vjB zB3d{^au8~V6r}LsFNlTZ8+E0XU=RhO%2mbWT$n$SQv&AW%Uj&Q^1c6LkxEPby4c6& z<#r(0CM}f=NV6n&Udlw?{I_uH?<{ugn>pHJWqFeL4>9=?i1hPlrl3kbVe4D72KEOX zMGghD8FaCii*IzvaEvkxx{eN*yG_H%uZ)tni3+E(kdXp)8AfG(9*;iH#% z>Pfd5Go&7XWr-2G;bA*0|Tl?Qy8F1*T0(7PayN12*^+mqNZ^gHM-?>yCNo&9_QlD1bQ&?w zy3u7}{7%gdHK)YxY@p#5q93s><@mr&s<2N>e&E13nFAJ3@W*HM~!E!U18V zYRg9uA9Mv2{G4GRoy;=r?|jq$md&3I#B@M%b;5me{Vc!0`80rJ=T`^)(C18I^UI4$eUgQk{;{gQ+J zm`=3+yrO%t)ktvb$-einB(5|Om- z%Xt+%tyV8mlPS5+9UK_Lbfz0%WM=B5M`NBNFQN<~(^M#eK`by96d7_afm zo;hT3zF7l4NYm$OC}9J*{+cb8u-hBmvVIslOcp-hUDTRmO*HyN`y2mB&5A4*p=YX) zLk5zxiWd5H6a8PqH6D)wR&n`i6I$?}+U#9FJ%kt12}ODxdRvX-lTB~j$VM@HRe}zi z8;Vq+3W#bWT5h=A7InI5VAS1c8RG)gm|8Q?WjN3_=`>n1=^1b$w%JLihfHMT$ z95%<*y=-bEu0j29a0_Yw+`>~^(j@=rfV6vjs_67lXqnbG5XkC!l9hUizKye{o!5_& zrhF5kwZ zzSUSTaWY(fbNrfY9Z$_TTaxJxEg5K6($u*o)}jU(oL_vN6F?D08%K{R#F>2{xOY}0 zmK=KD%?2vXU-m#*N{rsu^TMaDjhmN`Wyjz86?)OnROLJ0nEC3M?L}G!)mscm&REc* zgw7-(Pfc$aq$mc93yew2^($yy~b2sj)F>Xmr^Rbi$%A z&Muwz;h>l$^-(^pViHS#b$Z@MHemm^v}DM zuh1HPXoyR^Aw9LQ_XGrLkBk4ptl8b!M-%6F_mm3+(jfbn2*bcpslDd|xzpN!xfkWP zmk-ynrN~Kc-WP>2#qxtSGO`2b1xT)Iug;3B-2{<^-{sB>n%*~^*}o2_{3ax_mMMkp zlcE4A=AGHjkh_Rk}O>d|{ zVb_2*d?I;j#oCa=pd335bW1!0fw-=<$J6Qc`|90U<^O;fK9?=%%fMA#f8{l<#D1C& z8)5)jLdVwO0PSr)*>7C4p6~VHq4Rp$=IeBU)1$|GoCW|~o0~Y8RP>PU3!H1JEy6`bJo5eq5|^dv;F}**CB_b^&SBJlq8iyiZ`jM;*Y3y?zEL}dOm3mktj>wI5}l7 zEUgwRS~Mp5ChC_h@a>kQqePD)Xju*E`HWY{lGuXo{mkt4Q=@ZsF<4lZS4;7JHos+$ z%;AX%#69S9>fKcS5Tvt{4hTdM8ji{#KOIW$ZknqsEchccn_D~igw}YCT_L|~Dog93 zL8hVf^DqExnaK~c^!NDAN4@n1y-Q@HA(tD;x{s5e@UQLbpyrLczszn1gOg&`evf4w z$0g`z;Hp-Dw5$^@!JS&Z&xds0?|a$P$An|riMW*Ga!?grTEDp<$~xJPYfm<8b)mYL z`r#ToG`6l}B7$Wn4+NrkPZvoxn8-5j51F=?wEd$-<6Ai?(!}I;c$7A}8XjsjPHb60 ziElHFihj5v%1&JnL=45hE&zt@b(jnw9^zkAEoy#F-Cd9^CH&kbT)MC%K|XmBnD-*! zkw&evk!wyQ0NOZzQ>TpAmGvKnsxv_sfy?y?ljTkgaj)EDD1s;=p;Yr>C7K`J&Bt}3 zk!YOxtNxsN!P_AYKVOKcx#O57O=%E`Dc%8ECh$v-;r`b5SxUbEy2Lg(kCw7XJg*DU zGyvYNh8QV(;ezZ6YOOf2kJTlZe>c0X1eE^xT!UbYPlvd0+7unW?7_d50EpG==t=as zHVl%B%DoE^m~GnsL>ggi;)-X@nfx5d>1#UZjVv)o%-1@TT!w$RO*>M(qPB538jJdI zFSg#91fZu%?PcU2r@vw@YE{UqFaR`9uRwL09~3b?kcFt-m|BC^{Sr2cmQxiOV5xMI zCDDCwuI13QH~s$A#4Voi5_I`C!!_5qsn`QCx@!>Rq408o| zR_csST1v=iX_@h~v}FwRol#k3#f&Q_OVG{I_s)(vK-1B(F3{L!z%nkbrt$fy%9|^o zZ*)9q@{~qvWBxo`h5TMOfFaKMh;Epy-&M+5HutiL`;`9ms5+3fX}RoRwS4VP^WtzW z6lS)!AIJ%r%B-CMQ~;Vg4D=td_3Gzp?~}j6{!I|Ii#t6ub>g?n521^J=eXwUsrd0v z8z?4O=9(&U?M2<-XxUR=KX>n$TRii)*(~RDCi7=9zXO3hIhmtzi#ydcbh=YdOrq0p zwW<`rF!u5lK-AxaXq0NYD5Oa2R+Of3Ogd{6uoO?8kOFdK4fa5(^V#UVTPil>pj;0| zCv`_Xm)x>eKZ{2$Z}{*WTj_oo7mCF0FX2jQhzo&dito0%kTP=Gzs1yei#~ z0eW+PBtBk*>}!plk=OgdBc77zQg84mx0OV(QE7FJa{uuBH{l_Fe(sEZ!Z;LMRbSd3 zT?qo^CMH^StL##>Rx7@fatZ8E=Cq4~7g76;60H=lrcY6cuG3FPODCqSLN9?pg@t6w zA!gW3uRGh#?>8J47o4r*PMNsJUDJWS!~0x!uG9ZyOk2l4ot>XSaY*h|{P*H`kC zPuw&gQoFsee^qtr_-zWyG@Mt`?PcPbvaLb;W0Xkt?AlVU!95AeOp4ei`V1a{wj zO6TPpuN@E|1<{c%QB#_~8uz_uZ#Oaao2lT&1}PkfOo7PwKw8FAfYx6tiyxXZF8+Cr zYexYQq;D*5wd7pBy_`W89`-4@@5%k9mv5f0cqLV7R!;yD$sJ$!7z-cA3jUg2uJh9G z9sRe`uX10OE03=*5qtZg{P%D$2t*#g7Z&#CM%gQ7$g;I*=&gN)9CF_(so-3|4l%?uzny+qwIo+(uTsH6?-kjssn8?W z@{h&5PKr`YlaI~|i1tXO@mIog$vHbEj#PaCkeSAFd+NEinHM;mCuZ|$>ddcdvwbt< z;v#xx#xhqT^8wGHsRf-$tt8Ow*+#fp)cPpn^cq&`tSGSrTI9fKnIlgvwVSs|wc9Y3 zf7C{HPEXVultKm>n>Wf2HJ{#>ofR%U>eRLe4ERlPumCDwR{#z$Gw@2qz7k?*PS~Q@A?-9=pwc4ah~$=g-bu4(d971;5bp zRe|$-_QzfPRBXEIhy21{8Z^U5C3O3t=DL$!MWEJSH5@?~8CH8P*I=oRj>df}i_qgo z!O=1IH}R-UPtJ(>(xERuqk_t*PZLVH9+T$~oPi=Z*{Z}z|L_lOj3xwIKf6$W3+xNi z#s^m%4YEp7?PUniqfL{{3YH|SCr?Ot%Uz6S*mgJcV?SI441q^JN+CD$h8)7)oLGdJ z!XIUb$?V3wYvw!T+bZmYPeE3Jxr>+_z?O`>Xg$f7E^b=z&sq&qmC#J9Sao!5CEa}$ zLu0YM9*Lb#{|coU!QapSK!vyrQaBDPzuR^NwvFb~t%!K%viTy$S1&i1Cx#HOWN?g? zJtDBI2`RfXxlK5z>eyP`^V9}cElDujV);rwX9|iH$~--~$0RE*!}f5@I|dL;_Rdoe zAfN_wsQfM22>xdx?;KrEjLwURjyTzhHH5`j<+&;g6mEcJYydgNo-I93>-AX z2N*a1nl-opd}R7?`v9OCsk~hr1veZp_`60R@;X3JFbMr3aMB>~uAiF?)X@_bKJ$xD ztZ7F=Gv5bL?8#3?XBWdH zbKr9Pfwzd1AR0WoYPJC;e&@wsq?3vM9Q^XaXQhJ&BDT_BpFS`wbIO`C4qnLofN7BK zL~M5j&*IOUm;q}nG2)i9>wSaIr6$9G?0^iBg5@n#99QU@>EwiKdp?LRyR#~==YI!o zs}dtk8t43%*0HVq!QSu@rdfUQM8>ct-36Cta86Q=Vr7dP=Li&<(eSlOcwX`036V;9_2i|48VM{oP}nK4D1nqztn(`She03248I!l&zFs@lFTx3O&SVUw7tKET*~+NM9`mu_f1JB!~5+N)F1^) z9-#7nxI$gT!zJt%l*_|adbc&0_DT}lx1^xEhTloGn4C4Wl&bi7rUAo=JmMBDrBhl_ z*wOfgX~@6n3H>*CP1Ng`V`wz`oDSL*2}_AghJcfBh3?K(`iE3kVxFOPBqS~;FEm@N z#Q^>}&v2ztBl}H+acP#urSmJO1iD!+oCcsmP6om}`rEd7HpB?yLB@*Nh6bNo-|80i zJlgGEKAQhk8=$m7B1&&D0!#XT>n-boy< zo6dWm7C3Dr5)9xxnxfmb@1Y`tbNBgsHWK1@$UoFr0@(YVneqjYM(^7>4w+U<{^nO{ ztLE;d=A=TilH?@6c}wL1fNHf)_t)DK|6K3?*c4UmM=C(W`4>G9@w&$G9Z+%oV$hsxl!PvY2XyG8Mp*~}a0?k`0q z>5wcY#u4^g7jPb4CBFG{j({h!#&lR3&hF)3JiS_5Qm3Q4XMvlLhUeJi6z6Fe(b%_W zVdm>jeRlmopeXZK;bG4#mRcOEc`{kY4IZR)*IHQ{zVm(JM|BKzaflWdd2=T5Owb(= za+1&VLg|Nrrj!29A0WpWA+0L!UqkQEGpv;XeguG|!T*y6Mq9&3FB}Lu z#Tfg^4BitlvSEAA$sn>RgMt2(GwVcqB)dl1hMN^m>hSPhPu996V5=!G>8L+N{a%O> zo_w$(6aR;Hq?91$cL4)`g4{?mik5 zsH2v5cV3&nN{e^Frdh|2vNZ1v@il!>u8_OU(HBS$Eu78$SPf_tf{(!6Ku!~;F#5}! z)EiU2Ndo%TqI|yg|7b2?{5eb`u@F$R)3hbvGTWf%*1R8a4xiZq_S&RRekXV?g)?uK zl~;asCOMbZ+KaR@JWf_}hHc{HpeWEJN?U5hkl*~&Mk`gQt&$|T7O(sM?;MDmS(rG*Y~geFW)|eF>PZz$9Z$Ja??z}U zG+^ibJ?DMbuF?eue$O;pWfK89L)ZSHum;jyHV9Ra1Vuzjq57m8Y=MH?L!sr~A>7UO z%Ul0!G=~64co83*{CrS|`4w;b0N@U`O+gfTT1xhe7f491Tco+ies~0W{R*%{{)5Q> zUrK+59)A<@lC(6YSC$fk&_6RvwE>Iqyn=zw}#7F9T)DutNZ~j6CUX;G3x@{b1>4@UBOTX-QN)JSJYwnjybN;Bm z^mBE@s)Pph8EV5NO6a2BIfmcf@~%8@Xj>x-|CIdu(BJW|^4-!6{2LrYoD_19NlVl* zC<`k!%d+7c(A{2A&&ke50`f09Rt{kXtqwjpCdmVWdX1;tWt%@2HkSKHJ3-M%Ib#z^ z@ulaLSSy5HU&}zdKh!(=y=Kg#W`J=g{j@P%WLgFi2PsLeMvN4mqpWg|G+19_<=Ub9 zwBJG%_L=*dk02W9<-Q2-g|7MS3Imd;@)-vSh&(KxmU6Cq2KoGph1i?mcz(+S#qe8t zRaRO+icr3judc18O1zGYe<-tdy|*i4`idHeLi~p20ckAO5?VfIKb|wnAQ8Z{+AnFr z3JR7pI^?o`@)HpdtJ-`E94AmkcNCPmd17T2XlDm!X^y-mD!y#qiRI6we$DnC7X9v$ zkpr?tqSJJn!?;LLS<|GSzUUZm5*q$jGuIu}0i+620!Wpj0-;Gap-3RKAe{uroZ#H|?!4=Iv*ylQYv!-4 zm3;ZyIeVY|D`y`;Xla?LaqBOdn7~y6caD^xL+8bo#c6&pCB)|)xcA&yDo;iSV)1Qs zs`SoLz~wyv*e9t%vP&GqEAkdi#VpYSzOv1_N28WxJSU^gm&8J7pJ9xieL@Q55_*@A-h;t6lDYmR81d}oe0S?KYk;W zx3?tNrDnCBgx~&If@=ODCp3c8*?5!i*tX}MC0gY{1PPh&NOcGAav?mLIq?T7$^{J52|3NxCSP#sp zleCKJk9$!6mwvgXGcnr4b{Xt#8=I`f%E<%O7i!win|gF>fPMEc;F_opID1%doP?rQ z7uJ#;>mVKS2@AG$mezEQb}AGj#Iz#*rk!23WbLLUD5w0FST(eK=I-O)B)L?5lv3?@ z_h?G{J>@H@Uk^p@P4%>`9xCG58RtZS;v?bgjb!@)2;?AAR{rnfcrG!ztGj#Lds}9K zf;lQ_JlJd3@Stv~k!>f<7Ex>#wn`yf`?aht7{$boYIU_%Tu4% za`h1B^01Ax1*a6NT%E1g6v)W7jR;h0_+@>21-mg|w@#^NQhtp^EV}!$Ewiuug6`r} z?qAKS+#)2wSw}(BsLS3s-)!nIZCa>vDt(hmTHC7GoOoIV8ezzJFB(%X>efZ@?=FQQ zTU)_gSgYyJH!78N#nt3b^3G5&)LqKX*o&c7-ij^urxtyT5cAE$00lDUiEitp9}{0P zPNCCPgMSo?%c-gwE_|-?+t^APwT8OcF2Z79o)ow{$xp4Ud(fV$G(vS??Rnfj*|8Ge zsG@L7@X(XZ(>yu>b5)=Cb7&I@B{})7_r?m_uJcIw8gG(?BVAQ>HE2DhHzY@oQX#|# zr9JbN;JT#4ybmIfkwZR{{IKPi5o3mEK5C;HzcnuC091+o}5uD`Btxa&~d2MI#MF7 zp_YGTh%rf4$GLe0tAgix4OX)^(dMBViDFauZG!ynqx8lq6{+{DFZdO7(vLq#N|SYX zv51*ISsj<;o1a_2{Ia2jzn9mtAGH+3Kkx7eS|(1bmJGF^ur~BTSy(&a(~xmN)Cu`c zPEPt5%qbL@s6?~xOr++6$vSY9GT|*DTt#O;8L4B&3Qj{7bTbf!s0wNTIG}?MDsvhj zGg`W=Raq1+yXR44bh*&8xcRi)P;8$zd#Gr%W(3dpgIiEtK|(lE&ryE81y_@%W55W3 zr~%uf4xYZ(3ia`YkYB4J?)ipfsO>AY;*~LpiqVNENhsfLgib-Q!&B_S7b+Mwg*eoLC|re(#bbNeyBQz?IWw07Em-{w?z>Gq5xx}0 zm57GOe5$|w)^3>45mVxj#Ca5l4nx@vS{CzByVN;7m0hFf-pbf@u9|tH_Iw=HSIy)7 zUk>+6r$=mui=>&+F#>A+TF(W+>3 zM^mS9>QgAs#bp7$ z`^Z-}R2FWV)E_4>={>Cmw%<@A|&i z@6I!TNBOsS-VO-hDYy;5?GBL`ApUAv3jU9`KAZI$!c1)9MlUHUF7QIeR5i5}>WvX= zUyyg*1O*_FKxP&u0oXITePDF*K(Q=VRP);a|AN=$Pav0On*^h}FpBvsDLDU+SBsj8`dy>{KGnx7$!pQCw@~6_#?_OV zT&;YCPLj$R+}L>Y0Xm5OV-P%qoiVkqNUF^6ZiW1O;)EQ*nQ88Ydt`5;R1LOpA8p$U z8UvJ!SK6x3b_$f#S|bHu_e~HE4wfP@{?Qh=C|>|c4`zz9bf$1oh1?g=aiH*3HZT?(?<}}SedE#iE`j{QBxV36xkRC3$Y!du ztRPxTxUTd|B-qB$Z@2E0#+q5jDIvKu$9&7h6^20P)7KtHjNF^9c2vE;K5{^8K)#f9 z$Jms4=@T_e32`)b7Jm|{F5KRJc=trva)NwjxU|Ib2B~Z&vndN^)~`L1{MP>C6q!Io zkr0K=COt$0W>#SQgn96BdroT`4n~H9{}u?g&5US{$8KSX*ZA_6zk6XOZJc|@-*o!T z%NoM8^a_n~@3&8A$an@AnI7|zB&~7#1xzmWMv=In;CY3bxeq-)%di^h2(@VNwQ&vA zUYF&%;t^Q!W}Bxv4K|2>FO*H35VOH4kMw>2x;hc}CaQ0|$jc$uq_c7=qz+C+<+U&- zg0fjePUH9?=^l#)Ss`8|*&zVQ(7XDYWxtWSkbz&1OGi9q^8&H>v|KML+2J<@d0R%E`X71m z3yX+R4J)}VnnfNyF!-$JrBt2Z7WO5}@rK;AY1%%km@8UsA_}rSuG(#7SFh_ze0XS8 z*5-O|W}(=Avx8MhjspVWy$gK*@yh$1F-`m(ZY_XjL6iK==%WoQ^?HMR6PdRUM1;jq zbG6102BhNt9%Qs=w!(Q8o1Ehy?8I*{f@^@3TItimVDhN* zp%B;MKgMkBnCN7rVUourUlj3)U4_GRq5Q(|gar*I><6rdDhT7!t`M+LA3Wgkp6Mri z&40^L{9hdKBlnjrwb*z8%^_ILevfM9rfJ&#f6|EmnKSgA9bD5)o zot>RuIE`nl($k0TOW(5}?A)`&s5LMI-xGD&_>SI_KIeo*@@b~)6n^Ll@#9&H309wU zoKD^Q783HI8q)+BX{#g2}C=T>#2v zsFjm=h?)70#+liyU|_qnDR6>4h4l?Eu%a?%ll@%lyb5M>g>sftv7PHV`edSCszg$* zhJNYrQ9nfveeaRSWoZN5Tvs0@shvyeea2zP0SsK{QIHOh?Ss|3J&kp6Bom_MY14;k zR=}e`Q*h##By!R5+$x0;Yly`for0@Z+?{&Yn9!I^Kbkny!NCW!PAvdp^YJq3uO~s$ zojAB0-Bn;HcBm&g5r<~9vixwffdA%)PQsTm?FC#fU~DQ{P7N97#a?ta>fqX$)fSDp zSnjF&#>BX&Jo#)`$#VURi!Ir=jC45lmd;B)2<9u zMf>xAn6tieD!`8k#-SO@D%Hl2cmpT}Z=*wlSG$^NPLv`R4}D($woPSEb|iC7e!k)L zcSd{|wR-mZvK{OVknU!-6B@z0fgpUx4}!iYc&GZKGbyyDL~Ck==imlw zu69A&k-?_Sze>)#MJUhQsX4E=(5M$D0Gn15whNj&#?m9Sa9vl0onDQ2d_ila@u2?}DD6 zHf6!+=mv40IdySEb;h01?(&MMFNcwJgDLOz_Z51b+-Zd9LdH*ocE$j#ca#`dT zIhNhFpqxZXbr0BQP>6YWOWdv6s%|oUV-SrS4g)?~Z6G_t^%DQKzqwDqL*$WD);agY z>o1i!?!sAVsB06%eA*i4Y(_fY23Ck)|NMLx9jkdP}Gx zNDoCy0*G`-fDl4w=lSms`#s;zJH{So>~Y5akdb?4jWsjZeb05R-@NAfU}B`lNY70V z000=DJk~Y^04ON|0E$GqE9771spxKyzbGK4dRl<0VcrdLgW5&YP!j;CNnkj8MMG|1 z^?Ga#0RWh~|NT((flD0$06E4d+M4D8_B$AcV7AGn!#!G^X9&%SJ06-WQw#-Cw>;>_ zxj#m^@|f^A(CkGPjhe~qBAS1^$pvvC#ImeduGri+xi z{qZ06I3=udtz5rjZRWO?G|w=8%Vj=&W(L!b`?i)2N#8k0sfO=dAJtMn-Ijw=svxMC zRY$Y`bneAFCjIp0`5+k1QPX#f|B|5Ljba0VK*v7XF^8Kenwx*mI2rpM ze16i_=OIZO(Ac3a3;b9wFzHUhVS>d9y6^Q>DMt_0youjZ)-V0V+w|EUz1YseYr;EceWSe7rYQU?Avz2i=RK-+VXb} zJSU2KV9kpW2*V(Z!SKFI?mo@j^|w!gRIT95K|iiK(hU)_vx)1`IU!9YURJG z2FKplq6+n8kUwF*!5Js7vy@L2i;Id}A(LB1ny7xUgKXQ9QH=r^R4JJk`T|Vc_RE66 zvfSPMH=@hT+Vlzvxz334cFNubFx4RxUcJ(e5D7Itdzxci_UU?e`)!CNp-f5iy!Zgj zhy&@vSgBihecp?_P<{ry{GsIQ>yE%8sw`(TEJ2-TfjEbVsi^|Zm)_56F3b9FJmOPkhsEjCKHbt2{&J>rZ$MyPv zJ+@k%2Z$#>M)SpiwYvHjXK9G6>C1Vsv)0fh29=$wPw<9T_$&G_R!Re|yZL!i*Z2+%m8j3WJMr=` z%Ls8L&kBhEXxMH9mRmr(S`yfdW+bscj@uIv9L~dp(IdpeIq=pyR^#!&HQ=I0`Wf<&T`%!nZf7oxKhrA- z(yuI^yRO&d5{ER(W*WVYiQoPDhp^Ts00A39y17IjYX=tsuJpINOkHu8dTs4Z}^KXIyh0O$wFE>UG(3zo2M)1U^ ziXxIP!n))9PF+Q6N1PXI^q$4m-Oal$F$|EMX!=P zc@O+;<1-Dgp#wY)+h)x7WUQNcW2X`8v(^XnZ#Kd$EM${H4v*JP4$#IH_b)@ihQVD? zn}QbAh`p(!{gjYFAL5Q*`ww{GMdy8OcmGn?z^wJ%oU+RtjNAnrTlIF%U|Bef9=JoyZ> zJ)Q;ODo60flWRxI7+bU^RC6g9ioHv~qZXQ)JK63ps7lz8#d>xJ;skHZTGmjg_vnJ% zQb(MdFl#h5d1?%)GYfe8mvbcU>}9}z{A$zPm6JUkZ6;`9IZlTUq?!Y^Z@2Uu$H!(3 zp5XNOS3G9!9sc5u*h>X2MFY@cchj}PN8UwDD|-5(-q~!ashuCl&D#=sd4KWPHZ+}6 z+$w2sl%C9ZKbxR#x^zxzA*@!&kzACTcfKQQ_2PZIBRBF=-gm_gd(kgkt33F+wc_@7 zR~3w%!EMk_7V#FkrW7~E^A+&v+&C#~eBp8^7$u$x$|AJ`1IBzT;MYcXf65&PG|HX+ zrH+c=^dNlQN|y_~yoMx=iOrhsaYF4+Vin&QjV1M^AG011zZ7xiB2d+ihH*j6cP&l~ zF;;j6yVlKun&pGmKth`W(I6}$R^~?aBPQh@TXg6}tw-j?>B41zKI8eC2H5@da0hGp z>vw}kISkhIWH^({>rekuM~g$><-Bd-dOmG^z?ha2b=vWBgNMJ*f>IiVrOpWsh{JgN zqP6$mnQvvvyCari!8`SSOPv)0VZH3Q zza-R)=IHLVPaH3LsqB1y7?TwLdIflih#O8X9b1kCPMj@Vj-B_lzg(i)^w)-}jrXK( zsy{g9DmMSBB)u&vJ)zuJVL2ubRu(LALAd+B+5h^<5K;By*=9esKoq<4r+%gKCB+3F z5pPzQ4Nf~&SJ~gqJ^=~}Szdrke9f7oGMZ%Woc^g1w{5op2hji%tw|$cXWcW-S&i`X ziHV8zAGrqTOt9iLL)(RbN9gL@O-oE;5$bXWjN3VwSB1()Z6t$ED(BNUnZAsqd~In& z>VzGIe7V^{9X?F!XSte2XeZY-!fzOg+RAx;VT%FJK)Y5O9X8jl zV49$=o1kG?0mHSK#S|Bz)dmmZqOy9@H9R zZuShxMy<)J#ru(_+prBv+@a2^euh$rnFN&j4(;R*8-fCiv{6V}fGtQJImkwIej3;Z ziAUz@w+*gWl~_qa6BhC^>{<^vT|)E|)MsQs)Et+43B#F9UFV5?_C95eih)^&Db<_F z6eWlO4+zLj)N{^D z%hJ(qarUztxBGj-A%3jdQ)<*f|1vk!`k7Y0A*w>F4`LRYKRemN(M8}xWze>0^{JHVNhd13^ok+ygId6QFHrnX?aU^YPFo!*vXZ5 z9<$eH@QY9Rpr4d@Sn&RPSD|dY<+N=Lp(Q3wJs`#3O?Jb|p^y6b#2U+O{;d+~^JmfA z(yj7W^Iqp{=e{S#ZMU0A=^lf`2mg2{Lk^We!#gKKJJ2TJxRg@~fzSV~*%7KOv>k2- zt>j^{dApH&ZzZGO=H$o$M^qWy^NeQ1Le}_jdx7KqqM%ie#=dPXu3wQ|fw-CC>lH}c z)DGRs*}8b6;JwDxa{|NWOQNr%nNH%3C|1e&*B=_g8SkCCxyu{eoMJPRc+~*g+BtC$ zFLfEQXY6`vW^<}wX4(=6S<(ufszmvNuBo33tNxvw*F`o2h7Fx&m10^odWRIh}HWuqR>n+oK=94WO@U0QO*6IS(`0X#Z;dX*QxXLM=<*qfU z6d$f%n6no%cUUOTNnOOtTEVehGLg(}5o4>w{T|xtoPER?#k5d?g4I)vgMxOOhg8ml zUD{iNi{gnYOx*bn*)nB~jj(Ly!UdP$nPVS$wFQBomr1SL1U*LTbdT{7^|{KO4>4jr zo~;CFl6WMlSpf*awY2JVhb!Yk@2%Lh6O0N=(FFz_?JhXGmb1?_+##OPRn41vHZ6Zg zPBs|P2S0dh*;JOOhx{*$%!YDGo%H#k3>oU!nU%|nRtzIuXFuBu5$ZI`UqF(LoRtD2 zho@BkgiTu4)P#t92_YgIGyBO^(aK^Yu_~r1%nCx5tj0Ik?ciL#DMtlV;LcAQR>DQb z-4>-m=wF`_-qYP~=5_@sFp7y%_xrnuhHDhaKLvURTGr_ze+?#zQ;kP8hbcDY2=JZq z#l}WA^B>JBbz7WmFdHn`W!QYWn+curCA28RPQm74;>*cgZL!hpc?TcVL*?|mK~S|# z{Q4!tbiW;nDYI+!cU!`9t}j%#{lTCkMWiSGnn}nj{g^>|Z{>v|Bokbvy<#Y$V=kMs zFSH6bqmNfq?;((8c{d{0@h) zgejVVPAF0M`L3SdwHlT`XAL6ZDsLz$h0QpkvAo+{E|lTG%$L&r9$nhIp}ArKXSKnQ z=r^;S4zf|18^t({gfg6B_eWAnAhHv98e}(ldWyCF4%G3)X&lBbCAuQU42j3n$Rtav zIoXpohRs!t~y{LA} z+tAK!$YmBU(|m9*%1HGYX#tY(C5?Id_ON=oQ5du2S>c>#=C#3@u$&vx>j`XkX}tWG z!*)}2L|u#KWJj;9@Dy*_-paUUz;e_p8-z$z`Y3p$s!*PSpO#J|-gv+*-B+7kz6Z(_ z^jftg1)NhO3;Q9%7WiGeVX@&aElW(O475Do+4B;Z!h~&38lu@aX3YUJ-8!;|v%Yk4 z2xEb=7a#v)e>5tT;g3=9q~Q8R7Qb-|RtPsAts0CtjvzQ+1W?%{^WaGVU#4 zLtC*SB)i7+0x`Hr%kH08j0m9nofP_b5L~Mm_36fdb11s|cm0-~+R5l6KR!S!?|5&> zU@IO;rWw>u4sTlM6%paIK;UlR+4Nll(MBiz)QNg%W}heaEU0kP$87z$dW}Hp1w)h! zG_w^Q-#-Am%Y9lTq_2^>BbpcFKTwmV7mVe^e;(MqUsEu=iBpP8BhQ-#&y(6C{Ht5l zZaBHfu$b$|&D6l!!$O-z2lyp_Llj4Q&$r{T{7-o2%{uBZ-}M#ZrO#*c{qyzFD9-bT zty#>_Ub7mRZ)k1z3Zs{olr7vd=;itW4ua9lk^eZQ;sK zd($pUjf^dO+Iuh zX<6|fvP%ev9g_zD!h7k+p5pUQg8zmpAal6(S( z5Z(U`|GRnEs_R=CDtuceos(C$QH(Y1D0*$N!nDuv`C0u(ks>jrfvPn&3o-DwMslwG zb~vbCbG+76D{-5FSb@SDxvKXgF9tHKwxoI!52Bn1HZpWQ(xru41rugk^x5*--7(j! zPTt?7_aosxek7N$|5kJ6l?L{*19q^weQrwm-w0;3wYI2skH3$Hh)g4!0e5bX6eeqCrh-xf@CMYN0(`H91&w}r_{>-$Ir3_fs zbR}~V%L@=p7q(_8O^)a1UumHjQexHyag171{e!$hao8#LR(>Ee&fKPXGLs4*{N?YK zBo}wlryOkXL-c=oE!q=`mS0G(RIazRsCkp*{34|+$>qRe0l8FmIcnf!uka|UcV#x? zv2hb-n4pF-k6j%cwQ%58cUdq)Co8Xt4?S-)XP;AfNot{TP++Xfnw4U=*$L*a1-A?Y zR_nUwU1Dj}zv%oD|8rz8s1rwt4|zENT=w`=I%AILa$8PTu0uh?!iqz{S@1y#i`(b? zfNk2(GIYNDc?S%I=697QRayM%IszC4zBALHT@(;^hAmcaxQG!{O1!M?^9MTaHqGv^ zBc3B4AERr1r*5PVNL9W0?h?Es5i7rPN|oJ(4Mggfxc;zH|TI~OG&>6|(neFhQNzoU|vb|lpmMF8Dn zq~Ib~Vh&$%rLsTa;e>O9cX$1?@=ItjhaE}E$w2=|o$a3rI_$M_+wce90I1{@U87WyvYYW%V(#(e_+8Gsn01vy*(F&n z$Dj?{=!3m)M-2Go|FJwo#L7&d^x*5~G%+@Cyq|2z9`~%Y#+FI>_=Z`%>z78Zl7VBn z)&gX+<6DF%WwEymz*O8XyLjt)GbV=!xgX9TK%)9Dy7gL zkJ0i)W6WFkX+{ho^)3CTH+Ax#E&CDpV(SC?Ylk7}YqlldQIeLO?=+uv>mP$T6bM;6 zuF!h%T(ODdZEx{U&lV3eGAa*3T#McEV2=xbU@GP~rKi59ERlxKRXI5BxOh!KjgOgy z{WS~6P4Ux-E$}HjRk3lV%WH3htUI6O8=6x z>3v^(Hr?@CV4hAUjl20pqpL;-KC3L3y}U3JB66t%R$Na{9b&(pVQE{)EyspCHf0~m ztein*6nHz|e|7k&v|ISW?EN$8634AWQI0#Ujg19SlT#C}TPKq?JJg+B%C#xr=%ctB zWfhr)NEl*%b?C><^OzZK7O8JU{sP%J=?Ff{<`f1IiuJV5_vzAV9o86TGOMi?R7bxT z(ufoH<*;w4%2Q4&A6f+TBrA-}NxDKmx$kE@O&QKCkdzZ?zkJ9j|7b}%J8wR+)sGZe zY{RFV^-X9~{qeGkl-bN@$~ey@78ZVVZgHL2DdMstLIpHfvzS74~C$`CDa{p)9F_eD_WrdcTyPi5AxaR>WqjT`c3 zL`^xVsU3}6cvgik<2r^L9}@Q!s9|cB!Hr%}`y2iF=GxkQ&AaIfYJkV;Pxz^e?7M$x zZ{%4ZQmv9f7LH^VexZE_F8x&P_P%6O^9C2bCVqA@LoLK@S*v18&`vEZYCzGv4B?(_ znWp8pv@>w}D*7Zavm@{{C%01U7q?!x^xNvGNK~?0H)!~@(LajkDaXh#`tP$g7$EU| z5i{jxc9rb2=yy^m)VDD_Jx;$iTv-CoUN7|g@fSQ@4n?2b%0x^mQG$o$;C1M8Hs70} zc&ljQCm+_jlDayq78pBqR#>&{hZlg0wF40?+VYy)rK=dKaBbl*7899b;AoT4%dLi= z_6Mi4k}Tulr3uw0OJBx<3i?WdAax6ZTm=-OdJEG7oR$cOIA&MFNyI!?vF73ymAv3= zV$X=U#f3bcA0S*nMNJ_}|LLb?i;{pDgFM4EuCILcy05mf%7!k(4RbRVdqL)CV6;A^C$53meS(PPs*I+l!Sx;sq5TnC}){X`h#q{1@uON^8N2 zU*C9FbljrvK4OTidZ2$EV>J3HP)9r@wB8bi9`CJHSVkmq|Gt!T!k8EFUGM(c;v46G zJ)x2^{!Ft<u3?dEu+RX8}xZrV7&CX))O`NJda}6pEF|%YVEZ)QE1V38Zle@?LhzS z0uv1^c`j+wUM1@+m$&^V%E{vslw{$GF2lj#RC`#HN)XQhc3u4`YxUJYhN1P0s`SRy zyY7O5PJgxS_||yFoq7z|&k~``fPcnbMTWPOY-K^(kFT`cfoUHAcjj#Et{9`gmvEBF z%DKuzAYVVhH;UUXxhnRG-RT2N=A;4F`WkiOXBuw49VNy$Q%;HS^9y??B{O8qciiO1 zQ(d!6O2F04`%XXk0-dEW-8S&)U!sq^#WFwwDlb#W#k^`PR ziozdTn?Nki-U8f>;F_M&_?D_WWRw5BfYL7HBC&%|bVvS8T~&@n#doq;+@jW=h9uKJ z9WYnja*yNZ@WCn=lME1DPAwxtP0a3ap#UhlHwq@Ii|K6?H@75-`;bA{@N|;yorwFsaGCE(a0;v(0C+g(K0O@y z)+aWlP|JzEhld7XA>Vl*LucTv-q6-DH?R> zLEmu#kBB+T*(*R4i@k62R@HC9&#-o$6`{1uxdOO_$AS01M9#1O$RTT*7W0-aczV`Q!F<&dx6g)R)pO0-=1z zCz@kefxdXXYS$wh@10sw(|znCK*vRi9ffUxKm4VFDye?$z3xfGGnf(yk@xCzF!3YV$p>A3Iq!3ubqe zt8*3LMW4Ur7G+$CW9q`?&B}AnvVNA6iyt=vc4I`?MS!$ovWjFkqw#HQdI*tl zTikGD)C=V-g#EX7Ddos;Aq9|gQ_F+ddYZP{-~P)}y!TzE%iFrtUfV2ME?bq~837?n z2SCpWEHRqRSOWXX{fIl2$3}_vth!Ai!t;`ep}5@>=MPPXHvlhNi>^|39&YjAKd4@k z5F>>am#r3yvKfpvA9=K&TI>h^ncTpolOHRooq^(+D`z!45`;uPQNDq*u?6 z^BOX7;7-4d001!IXDnVvhSYE>AeH^&v{ZI)_=~Dw^p%CYM=wBJMISuTIvM*szTe9& zpG5%QCLu;Y3>&`nKfQ%+S9<6dfx4?9{wv)?c!_r07M3B}>IEJ zn*$%H-O{^yZ9vLQGbsQ2*(sqYedzGIm;6!Ycrxtu-}U#;+q$qBj+z^lDYM=u=RQ+6~3;1wn`fTHRHZ=v{F+ekqs$4p! zTuaCIA8LRg1$6||&r7wpbsIO?@x3NG06_4;b+UlIp7=k-%&9Sg@nVBB1ASMvG6?Ix zPDo$vP7Z2E+Up;4WHYNi6eZ!{Wt=$(h8O{SB@JLZVOEvS07arvJ30`V4BjQb+0`$; z!axh!o;jD_LR1HW0D#j>MGx+~3KZ|cpgjC*YbL^0X|JDjbBs^btnD zw=j3K;L8*0yyYbbRrtA8<`oK+EB*$-o`7$cuaC%X%74r6nnS(qAUc?v?Ndp$cG!A8 zy)}oUT^&Z5EA$Ha#Wh|7d^4k^qbzZ?t}%1-??4-5oZtlb-*#VHKBBB>~JlB&ak~i5c(^^z=l~%WyVnvw_GsG<#6f zLfo2J8cBq5?ttm#KXA^heijomPvlWAfKS?S{CntQt@h z>Au|1T$NqB)-3n(HI0mZW90d1>=*ZosfMe8hv_M5$PZEzfhUr7~rcUGgE= zwjj5CqA6P~$)m7&>qiVx!R*lz>`m0yG`TaOgk8&?PT1g+M{jA4#B3Dhxq@#UldP`T zU$Q%?bKMZP+yGfi_@*{GHg!IVK`7Nz0>bH@@KDb;_@Jyi))blV)3{!~vY81(;+HI zRUNyyvn(t}zx0y0zKo#-go{3T+abn1GvfPo3$B0OHJE*6tD-w=vOT^WOh7K)CGI+B zru=M>ZQ}gnedw?VKY>R@4bOyhdBO}wuAHd81BqizRvmD!9YUD=7zni@Gz-jfQCS^q zi=>-eVp0d0v<8GIDB14S?7u-hUkh9tRQt`bkY~d1!mQ(gP`v)FIo;(fC*bTaWr^Iz zZ?Di@_E!%Az3B0>vhL!WZ!SiUTf3_<>uTQc-HWUOCx>oB0kIqUujOR{023Lpx8bmz z@Sn~UxeFq|>8%Sp5AehbmQ{bnH-bqX%j}+>zj>gTBi(>IOV8KvAz`f?x~H%Z&-XK} zvEoq+#sC1fewQE%zZPdJ`i0}^iNwH*6-m9rdFTkX{aEG08=febCHC9hoCg{O?&>$o z>voG1CU?H=CU(B0Mg3^=K&d>Jkzbb#{P=Z<^nedpAceoL)S1N2`DwzJ5K0MP(b=b= zfYRB?P5y1Pc;GS~0ivI8&=!;MZ;o=bn+v>jhuR5||6`+ueUp9fcO4^aMz4dF|LRyz zVkbcEb?5fl6QTBV|B}wln*7k@j=vv32ivC+NOp%}>wh)2XVfjBI}_{&m*V03a+Mtr zes26Zo|{aq9uZ`9`F}X?|HXv=FX5~_ov(W0{MhxqW{QrBazoUTF>c6s!B_h~8bwyG zyTmTDlzo~|K#7oSZtC$oS^7#887VoLH0&KlaC%(pf`e<0>N1383c#VU$4TKMq@ESB zN;v^p5eA+qHr6#P)*G|@wlBIGa*6BqCOnI_Fn9eSwB86=)yhAXvShngMl!t-wrJhv!D zh|8sv3AUS9$Nl>bg*76}!V;D3lfrTRia~zXp=0#GyhoOsMa1};lY-e@tMc_6EwDO; zeR%UTx%Lo5?tm5~`{7+pow;@o3cxyGHeXk;sK1Yu`|oi)x#AN}^@fMyD~>6dJ+ zFinj5r2df2B6Vaxg7+yG0N~5(AwV5;Z9K1qWpU%W&z}hM<{6FY>Zt)y{gi(Jj}p5s z`{IVt4d9V)$n{*60F!PH-m;K+7c-{`O63%ykJ8@R^Q)rn@_5Y`>CSxZ@7O# zyZZ%KDhEuS*dK^o0{}i7`uKCqBTa7)-++}G7v8eZXU)!xOM%&qT5>LwzF>F#^AfD1 zYQWg_;Y+*CHMC^WvSVCa1009C4{u@a+ll{VmcQMT9|S4(8D^TMN#=Ac^D_0;COkx# zQdK*-3vYtR^7+_)M}XR34%Wa*mH$co}ZsXtKrk;Nl@CaftyMC?p#>Pzr>5ttzhi+*76f?H%Ph6Jv ziK~iNC!p#E+Qs6Hfwl4DtMtbnDMtRTB9i1gmU>o-E}8Qv%0C6+(>;k}Oi{cO9AYa` z=tp91oLl?4TH1to8kn?m3EFp<_@}4OoOsH|Mr+5rcs1^nd=ZJk z|E@Sk<}s0h7Rg$3uJRu{tSSfMhKD}npHO1>=5kk<6eP>{VMJQx>NR`0JoDy9Z`IDT z`_k&s=&WPs=rhKg(#SYS_uRqHsYt}@sQJF*kreoGsH^NbOU~fq3b|VVz)lgGKSpUm zaghD|yF@n2Xw!-M&j~I^4_|uHq5|c7%~>-&`y}oA(z)K^i%1=B1^uRKk18P+sW(T1 zm?c+wXbtqwyD*=TokY$%N(h@_pm? zWmliGkIy5=Wb}EF+jnmQ0HZZ^fpOumD|j~SSi0gx$BjlSDwAo6j;j=mr>yTrHXZCG zo5l_`uTIfXH>E1%@Q+#L_fJy-1eNPU&}0dE%)JxFN7l(v&;PBe_kTP4pZn1NU*$pn aM5G8xDAGZYUL^Dm0zrBSp%;-R(xgiXi1g4y2?Rp# z0YVKOq!U7s-owl9`LFk^bI$wWUFUo_ZJiIZ_S&;&-?P`gXYOmR>z)b!s3uQI&OitO5HA~_9aBTK!m*wy)i1Mr4NYb5FZ|SOzZ>rJ; zGP(C0ZdAY3xmWde5MJ;q=JALDvqR?N&-ZyfgA!_?H>02^ir3;U(&A4#B)rZ zuBiY7HFsa$vUglv_(SyeylJ%9-Yc)QEwIfLN;~6H9=17CH=>^>AH%#iR_3og=ew_J zEuzm7a8c24Jm&-Eobf;3VefhZ{1{FDPJyzk`-{aWR+}5=C zdse9~OqQ1&PG|fgv#7FN2D2>$0YR3o=D70%&ZohgM*XNJh0`*Bn>-XQD~y?wl-cuF z3b}$?1Ia9hxnAYojMH651?38H%H$aRqf%|l* zy=Nl_@};WA|HbsxJ`3#>N&5JaFR^aM=h%3!Z$!PPBl!^~CA*1Tlc%A%K~y+g%>=SK z%Slg9kI3DH(srh4zbVS({7Fq1>8f*nceYL(<~{#>xc}M4W|D2(7}(Z8ga0%k1X&p_ z)PybB455#EA&eOUWiQIkdZ2LutFM5$%B7j9Z(}9LF9bHLOGh)xx4j0XL2h`h(^X_6 zo4h5sce=kHbbNe>@Em%-hL*Bce2l|z7A?mlQe(2UV}1(X*j=q3;CeStcAyw?3#_(Q zSGx|Pj}J%^Yy1=<=6v-4h_s^=XOq8u>Wi4%Nshvfr`5rBdg;e|k#dng&L|lzPcxYl zUdf}0LeLWVaIFbdjRT zwtX9F%^^XS9C5Vme^M3IlSaOM>lqhPR;Er>a+xz9$+Y${zlCawY#=A zAiC3C(Z$5OmhO|GKkT3H2h=S^ts3Ohmxk2dE+;6>?!uU!6_#&jtNmh?i{6Zj z_ea>1+3M<&9tA_NiF8`wUaE&QV702)d1CYvf7!g04I?zQXdj1Po%g{>+!NgS8(?C( zr1(+6zNT)8`bTpXd~QJFpNnF@PJT>IjtY(+{W6Ns+vg^=+lE!l&e*v-<{FX0yAK!N zmuc$^vq<31=ZTGKu@cL?&oQ}@(CjVFj>$`<*N9FG7s4i+VyW*o{d6l%VR2p^+w$qp z<^AsOYf~{(Zx+~xhBgA%ca`(iASUO}MT{?fhF23Vm53&WTr3S&jm2e`X)_`o(ZDNF zuRw;c5`Jgzs>$XSe#lTkO(Jq+AUI0Nct*#I5a9aLiYTfByRv$T+Gy`VLh(4d(yhw_ z_53)T+5XOPPOJA?G}8bXNH>3~x#pKURNIUN-l~GUr7KCi>P!EO>eqA1cLSXc2JH<@ zJBcpE^ES$=Jrbx4|rgyH?dH zuF`D}IB>lEebfUUp=smF5%x*!JSQdl1NWf!|3*9;c7{Y^@5Hyda`xpb2c7h%>-om> z#A(jyU(a4h6oXDrv^iZ*`_jy~W1R19qkEkS%j}Vz35^!V?wIZsTN%1W@fs?!27Yx3 zIe3p1eyC_6W6yYnVF`H7`S|eH8X+Kz3D0GE3Ev;Nnz9>EPoDO$ZhqVKP7<0w&_NrK z?xGsgbmi|hhN22_%8X?vWEc?+c81!fH%_p9o2e9=TE3%aSz-sxh(F#NP-f`rx*BE0 z_KBJ?B&VsZUj0C1v+3#vI2@gSE@Jd<_j#QHZ0R|vc$4~kTYucbaH*yEX&j5Fbctx( z!7nfoKX*rjfb01oVu2oxVE$_?oEeK^rzpO_4ix& z5Wn&(h>8z_!Zslt;;$~&=C8r1>FP4ie!g*51sdrvotmE6#at%%yUT_BA0EzmZ$i_6 zddXf3spaG_o7zXhuBY2KColAnN_g?fNcOOOw38E_52014{Kv6yXRoM~{B}cz)siiwwSU?CY6-G8XcM2O6c|2IEK0H_3SCE$P9SYnanrn-*R79yR&0mlV5dZg7Mii ziHs0ZJw7~rI$&p`iy-=9*Y-}=GW9S2d{vh?)jN7IqA;y?&hognmq0%x3^py_<_(Bd zh0Eq!`K@pv);$88i8pvmT$>Igietpm|Fk2=)!u>lE)ItR#h~Ho&}Hw!belZ!@8?!Z z7{j#wgc|)+L%tXL{{U@X)Egmc8n-AXcY6NZc#T-16qk@XV@Amk33bu?R~}*A3;K)7 ze^YfALTGXwFTK4+g5#4jj<3h8@oQjDA70}1>({QASA7XW^LLxdZM>H#Q=nS{Y64LC zvuV?-jf-?`(8fXBIFm3`@;gD9gw9ueeDs~9v2wsweXw+ijQ17EQL+oofB@vZTS5Ba z<&G%bm1Cjn2|k}RNJG*ege|KTa(40QEZ7l|tnOotPUvDTiL2kKm7(hC{sF;q-SHoo z8Y^0{fNYDc{|Z|9#u|NRVBEtc=Yg2_rR3$N=Gswz&H5KnKEf+&zr$k(A6DN?{LWZ) zthYE3fkXd{#HGwn1~db_uF%IY=Vs_B?WyFE8sNHz6d$eg;l5zbq*4DqF zp8R?~p*N`mJ~pj0Sj($4eEyzd7Zcw^q!f1PRva#H_p~;iu@}%?yi~0=^cZqjJ>%51 zBRUmmE!aBxV@^{N6WMr)`d)|KSt)%WLpLgV{vscux^c2+tIt{1WuC~w> zP~|tyZUyLii1ix)G<-LuNn$H9;9w{W;b{D)?%}{1(tn-5f=xN#SiXLmcwE3McE;>q zD|UX2GAjv0-d^~_d+eG@a&RjIDa)vA9gyGHF`9dvKkZ(kyn>Qj38o{em zGC9$pkvwr3OX;J+7Tb*>*lM(i)fBN z*W)Y3>Y1sxyw!3e&LNqMj)fR6NUl;)(;&8@B5 zcZxTicdv&$-+CoFdWsjJ9ppuy{O)Tw<796Z4cg{?yVQ$EELOFV`|d!YbjKJW#h>07$+Z17}JiX`ak(CVSZQml;E1Tc2uB% zGT!~I%oD2_MhS$7;DRvQ$ud;pS}#>IHlH+)sf%sT;C@eU6Ze)4)*kWsQRrfWs9lp( zJAV7@Q2A^weHC>CS}-Y-9=g>T-lj@)l4wBsdhb6d50rKmmgk z#@9N9?+m`)6VmK`LF(w?e@y= zzW4E0(UrJ}m%S&I`zyT|e@?EXD@Dgz$GnE!PMuvQpW;5vruwi_13WCZg60~}zE+4I2)kDIR!~b`4EBZdIy>nHsKe!~V3teM}Imj9l!fu>Lf3Ln{uPoVAu*~gOp|a(fA|>Lf zTA_YDoAr+q0xP@|W>|g`X(G(j;cF3u)T5OY6C90InI~F#`?A*?J#V1`rxAIG$G5x9 z)OojoVRzpI7K=uOr@QJDh>MyU7u^y`Wb;-{Jj_>(zYaf<@b|cwm?&SZVw2SvYRR0r zeHtKp#e83rvA>1^XIa5zL0Fn&gU0GCv+cHmhquo<`vLxFk9c{G*-V@A4~9El9k}NW zKif*`JzVmF{_!#Oz&krDV^BGT(^gAEH*$Pf>NB&8+z(&Fe3S#`%10XMi^2Z$34*g5 zW|wCgresFlcuosrZQm)RYf$y=C9E*-J7N!eW;acYyoo4qaV8<19lKA@5eRd{T1{amNE6i+!ubcAN#PL!*MdHdH&*G^_ORLiZMb>M3qf#$ig*!$&9sMpC z^vea`Fz-|oNuphpuypDd8fjxLJbu58_Wrc-?Qd|wetfq+g|jas3Oh8qq?j;e@ zd<&bEed(oR^w`_uTjNc2TY_H4vmP+cQ&~6X^nisBH7GKV_nTW789v$(**6(a5Fzqxf88s8!otT!rXI3ts9EQ*nn%_7rgUn^$hlzZi z1p|x?aJeS4O|7$`g}(Xzs{2G{gJu6+|9oQo8VZT3hKExw?BOB%rdMD&*G+VZLI zC1j&LzSbfCbzyqLUZYPQB%rkM+x6-+AR)Es+L*TVIiB5l)=1~kCU+vY@m8^NA8JdG z*-xy!UiA5p9V|+6InXWcP4NvilL_7XI0bf_S#VV8?gzhy7e~h7(KgW@6W{)$GPRFY zS{KaTnV?&@5Yc%BX=u|47I-ks&MW@Jv$^rOir@Dw-eOX`l-1?mCsS_Cfu!%yd_IR8 zwEO>%=8*VpZdmBCV9tICWv~DMkd)2v z1OUL?#{mF((dD-R&935&IIMiy^`l3Giu`)!J_-OJ?!Caf>!z&5Hvr{N%qan1R_@pV z0NnC=BmgtvR)+uW`~O<~|C|2%2aICGzthX7elF#Vo6#2l_gWYv@+U3-Bx4j%w$dm~ zXxBSj?3LqS0>1y(NQ{05${h|;99PGSkDXq{MOf`+}>YFc^hL1Z?(`4JQmgV#&`n* z)v8Tv9-0UkBl<00T&4Ac?*ax*l7g#Em%nMe-7a#h=M>YStxbRXV#TMbf=;b3wgSu# zifZbKyaAZPWKe+{pY-flcZxsI4e5g4q)YK#{aYg{Rh?U{$e_cMIGG$3|2Cg0A&`Wm z?a%!s%NeW6+~07MR1jXg+R$$f$YqJYm~@>C{)>I$2`Y_Db57|qsd>*(lWU$_e7UF{ zezWu~P99}s*$3&%+Dsa;(gN9gYVQa2 z6N1=|N#iX$TN!posHlHMOw!=asbd@VEwu7p!pUPMDSQ!;42^UPyZRCQT8Z7Yn%EyM zx!nuIl^3Y2Y+`TE9+`B>7 zCaLKzq}&!!WQC6cTF!Wc_SO#54j-g=y=JQ7#~6b7hyUJ40T+uK8rQ^T5np>|9HWlY7SJ&D8U_-2Ysc$Tz=GvtD!YZaDIdp4#ZjAbiJ}*~q zgk|5in{|W^{)U2^KWa9~?5q?_%p+^CC)x{di<;S^;@qtd9h~ zQi6o-A|tuVVi+f0^OnWaV)H={N2njAJhSjOND&O2J;NI4J9M&A;#tZBMCmtL@jbMl z%-+i>?@S|cx$p3)`xBW37F+oi%ZMQioiQ3Gr1VS2nkKi#xF zFF60YTUB#vXv5LBq-Sv+=5g8!Oh&i1o32tP*f&gs zL1IXy3~$G5d*4zu9Cs>5TQ*TIgglIquKO(5di(Bf6pUMFwoxVNb%2sz60so)bi8H- zGD)D>znM)OSDrrUbF8Y@zfjldj%-x~-DiJTS;mpjfc_kNXQYeWf_?S{^0od;uJKzn zuu93K1m)fGpN8ta+v;0c+yVltypadJcO?KXD|O9uxK)pJm1RT(bO8#6Ng>tW3Bb4_ z@|{7>zc4N>eEFP;=Rt4$-UcVf>+aZ13HrDpc1>SvlV=!Cr?`TfOWU~!OVvn_mwM*I z`9_dnV~qxG1`@BrEMyd#nO-8H9AFY^NyWeuB3TxfSZVQ4+C#S&Mc3~sRFsm)h@)5b z_|6EdR?%sA!cIj>i@8ufZd4~qchO=TFhWX85WD3JQ?;aB)-cQuVxYs?W=w1d=jhXv6Z@`lqM z$qsu+f89dfPr~z{m))Z-;2DdaLM9)T<9?e zAtL=ds7LW5r>a_BBriweE<17v86cT?mxX1i)eLERQE1;N4YVR+_g{?Em+^&mFJ~6b zcc_Hg{dk!XK=q4af)2-t=La1Shk|`IC3;;EUW1=Fsel0OO-f9 zP34@;f%=^H^Cq+|T)IA|i*sO{p*ORm9P1F6@)w03{WY_6u}H8?g%eL>d~aAw^LEGAK#1BbH2q_C~R^4P3<6~EwB8p}H^RPnw zi?mN#{sEA0cf+Bg6W0EPrWhPa`3~Y;v9?jB{;RjdUT7^6FpGx6M+Gkg(maR%&Z~z@cn=P9o)v5GiJ} zOISw6;(T;(F%Lp4D-t>jT*r}6Z492HaFsBhr$n?su3EVgoJ_p%@Kr|~((@R#K* zI@jToWwrMmG>azt2^HY{Zhe~ppVjDO_%JHlJJ1@lJdI)F9iTxPAji9h_a1Ph@+rPd zG4G7>=ouc{-P!4XOf;sq>ujs;_SSxI*B5tpE$PK2y*TM<@iygLHJ|0l`h=mnduj4_ z{SSec&d`+82(WmE(6@g*;H6hoE3u#cE-PPch;Gfpi1)95h%0ud?Az?iDQ3UU-cc?; z@NY`=p5?&nj)U9B-H(CVr3$V=2V#)ugsb3xP59|4XNn0E?19XA`42Pb&&#x{b+P6^ ztCcA$K8PhF_ILCkkW1ZHgSu&hLs<7xXPe_QYdL9QMDRujp9Bur-PG;9+TAxl%NpWf zSzh*sSs$|FY0C!K9NDC{>-o`8@wOL-CTF{Cjsfe46RaYeeP#jKV|8xNW%(D1j z>5~BdcF6sIj)S1y6ccjo|44DbZ3RaFfT#2c|2x`ct*=KqRZZ$0w&%TXpa9X5{LH*t!f`l}wusSMX=8OdJgS@B} zRc9NhV3o};n3`2%wD;&yFjQ&eMw%#wU3;4^3JUU1_tKaySAb(nDg5Lt5+9ByC^*V~ z-uM%Bk$$0l3k0%wbVrkwjrF$D;rbwA#fuhn&*lD)zW>Ag=}1PmV@+-?xllVEjMPwVpWMB%i+U&Uqie=CWZ_| z;Rx;Wv_b_h2tkaHmxMNYt*`l-pcOuy_+MR6Oc%EmRZu*F>u3DxKPvpA`nzhQ1Tg^1 zz1i5w>u4z`saew7E3s>pLN7h}TT;RxV>#x)1n}z8*VZxW1et>W)lQ%WNWqKA2~y3g zq1qO@g>i++JZM3kN6P)hGg43Gv_FteWl`jCp3p;4mRk!61N*+5siHc3|Gu_KMxeBU zKZFqD^lb01VP5N0X62~*VfH77&*7_ZLbi`5bsFc27b*;ST$!4SsFL;55oN0*0suL1 zOq<9AZj%wN0L)@|KkJPv z>yNT3j5XY60|84oFIujttXn1TX93S&C{e^xioX*5*gOC*JqZ4fZ1B1AKMKSDI}`#Q zbep$N?nBBaudEpRsz9a^A8#j(r*Ta4D_h0(6OhO0Tc%ot{+a+Od3!ERnlc#Vky=N5gPr8eOy03qHZ*c9A+>S7+DNRPSlJ}&E%9B_2 zaRZv^bcD2mV&X1ro(mM=IzB^6l0Q`1zS!B+SOh{ErpCccw)=VT`i0zPIh?6OMDd7%<^asZY^dq+A@}<=K1HB zH|tFAIg$}2(uACnP<37)ji0h%(tu_Yd{TK*@%xlgrEyvhpElXBK=MMH(@)PWdA)*A zRsGmLZoMbWt6(O(i=o^FF z${FD&tp%AmD2RBJFJo#_*SBl*VkKc6dMK#7en@>OA?|(faoQebkSBM&Kvw}s<*#Ry z>$u9v-<}aEEY-N-q1Yu0_4C@3X8KJcq19nn{;_{*`Pk^=XRkO(zTeC9*!=r*iu zQS3|<0AMPbZZ!y1P26i3bTCrGJ5C)$<3CoHHLcaj19n8Z ztVltiINOD}Ro!H=K)BcObJjNx-$^s{2t8eVZ&7c0u_6D#eEIyaL(MD}aGiPkZLnuI z3;!!Gl9dMl+$%h9HAq~NgE=@Zxx%BgYooV+f~rd&P*vr>oV;NxlS1xc?7&12EF2X! zuczaaUoDFB4$)$-+*T`;DBs@x5bE)H|M)H2-%n0HXpc8rQ->>@7yG@rr7my9M{oR! zfLiDkeRQ1?mt)TZQBl`8aD_QPc1Aebw#GKLEEHM@qX(=a@pyk9M{L1PKIHcRSy`uz;pNP}B+XCWDP7)TMUFyLM+bt_&N`0mI|YqpVkjX` z{t;;&IqdP--ryN2d=*XiG|;Ez1^`x;)@JY_L~~_27HJnUqAm_05#44P{Kz)hk=UX3 zr_Q0~>?rT3Mql(s?F6MElJdkmmQNhZJ^b>LK5AGGAK@6otnqxYu6BryEz0(?Z5jsY zkQC|3Pp9O9I%?BguNT8^H5RrAUNwirARQVE_PjPZ0B+&TJ`aIZ*ShWK7TdLa`{VKd zgO%LUF6NF%50pF(&9l$qrAVxXCi|XEu7P>%E3W)a<7m!~lk5g06$e~L+ zaaq)M!3;7NrG8_g5Yy_76UX+F*a<0PNLkW^%e6a`sT0DpNqc56YCy>E3ds>=zVnA) z(5M^+qvYmy75UWY%md=^`RhkV)@L?$nK#P~lY^^EIak^68x=eZc0tp9j_Yi@BID3Ln>|1m2{Ru3%9S_Z$V_wSA5xG z@{kzF$M0eK%7N*Xb$a5vpO4ZL+0gwbjqM!$y63Vu?$}6QY`Js&1ksu;nkDw672hi? zkJ$x30f*ucYUWfvv@pi$>;Ix&aXDD(Hr6V5w z->s)rhW#qz*>*2{Y)ewge@xNUQ{lNR7IELwUs>E{s>N;r0Y?@5#q!+=PETHyHG;T2 za9f|3f>T}!?)=czuJd}IE~uX$d_;7gtlgIaTC+IMJx<9zUP@izo9$EL67}frJf<1_ zX5>?x?$XUOSv#q6t@cy^i_7{GmaLLT@o^K`(+@J=WKX@Psbw`3 zhHEO{t9>_F?UiOP0Qp_JHMxB++gNn)eov)}5b{{Gnswu*rJ`lH2wTr)WzBI^rNe}>yhs%ka)*sVvXxL+HI1fPR8&|;TlhZ!;mOQq$W z38QrGCZZLP^{d2Nbxhn=%g+2$b;YC$Q$4eyW|k@$|JBk=h9v&Nh;*A-=mTo4`vGpem;X_ zA#^;DSB)mHz7K11q8H6F(isvg!QAzU{#7KV^v{(P4)2|hQaDqvhntmn%}b31uQf`=?KBx#L5X{f zQByix%A;?G&l$Tyji3I5a|kOAr#a~U_=lJo86a``)!HxOvU7v3-jWB zwyA;w0EO>RbEb7;J9x9PnszODUCSK2fBmD!?CCsp|4*(&D;l!d;p{Mu4PY+z83)uq zjo8aW%a9>>BB=b?#U$XsZ?e?il8IJhabG6}KJ2+8sCIG0cU~;WXEdL@o_PL z)jx6bOAxGC+#=P(XZ@Koh}u>C#6oNgKI?5aia|j0&texlRDMiacg2KXp!+NS#2z$y zUCC(MI)r^=xQ_&y&D5OajL%lts1vvtgS4km2eYAu$)rVRB$>)S0_A0uT*WX@4ri?F zTuc!uyQkQ(QCgf82&6CgNZSAwSE7X62xicUn&Zh3^rS7PaUDx?~pYI0iS zYr+hM2ZB>;VKlN&8@d(*ZhomZQJUr zEV8V#-hsoh-Y(rQk6Ul~w3upMX!jqS{7$88*JmkL0&ycchSio?uDeok2IjnyPPUNy znf;Gs(w1Q#{};b#adQWyhh#;;P7dx$_gZ{Pq0_Sr(3{%QkL>B5%xv~W#PuR#-4ZR@ zeITZj_0v{2VM$Y*S+~|R*1g5srlrCZaa>tcDkJ%*v8+|7UjztQqhD{kQswKXJlt~s z^4{pLuN=EXs;d6)9+`#atqiiGK*Q8;` z=FW~ww({B{U#f;IVm@_>Mmgrocn_Q3>p8ldl%a(Af6TAYy9Ur%ZUQCMrubKe=El%} zv&a84ZUTy2Hw7~y3SBu6M5zTB-pE1#{T>-8T$X(KmK1O^Z&1IT31R**h&QPJO8?Sm&yv{ zWJ0dXYc;p>oilf`VR6hzd-;@GAfby`^@g!G-6{h9$?wxAjrrtFkDGvUIuuVa^kYmx z(#4dVjs2($EMZMsL0Zd<@Qdf()eLIT74*FB{`JuUpoezjs4%ABzeKC5`x*TpFOLv< z^}xFBT9ehEP;yFW<1Nmr|EOO2j{sk&a3O4VnJQ(*+#&j45MOS&%B^B8g~QuGWt&oi zr?g%xeECmD`rcy361tybUHQyVqpx|i${6%R_^cf>x?4Bi?5PT1nt6e;%mp0x@&a@e!YcFKIFX~ zhJo#rGmHxt`sp1+Pwj+HX%(1%ikfy+ov(|J9mpySpva2JR#W&-XA~M(81f0O=7k29}()O4y^`l21iQ9&M@2g z$JaNm8D#b~twY|TgBN6oc8x^OhB7qqrwl2e+Vopm8y*K17a^2oL8lo{O(#NRGI*yW zJBG0Ai!sR1>+!6P46cSyjDCAi7|vN(^9;*AhegLM;+L1U%Wq;#vrB3b8qoB=z z^I~xxESH#C^VKuWbj61;kP0C6{d%ti0W)jnkkAj*wT(JY)uCayUdV=$-Ul5{M0km=GBfb z{#LUeJbw$&JV!v;Is*WMJhL8|*IR)U$p6oCMS<8WYS6M=_r$D!`}HA0fZ_)=+0yrB GU;i7W!EVR^ literal 0 HcmV?d00001 diff --git a/docs/source/tutorials/mealplan/summary.md b/docs/source/tutorials/mealplan/summary.md index aad38b85..c6d1249b 100644 --- a/docs/source/tutorials/mealplan/summary.md +++ b/docs/source/tutorials/mealplan/summary.md @@ -8,4 +8,4 @@ You learned how to create a component/template combo that allows for highly reac And there's much more to uncover with Django Unicorn. Take a look at the documentation to see what else you can discover! -You can see the [finished code for this tutorial](https://github.com/tataraba/django-unicorn-tutorial-app) on Github. \ No newline at end of file +You can see the [finished code for this tutorial](https://github.com/tataraba/django-unicorn-tutorial-app) on GitHub. \ No newline at end of file diff --git a/docs/source/tutorials/mealplan/unicorn-advanced.md b/docs/source/tutorials/mealplan/unicorn-advanced.md index a23b0b98..59f59503 100644 --- a/docs/source/tutorials/mealplan/unicorn-advanced.md +++ b/docs/source/tutorials/mealplan/unicorn-advanced.md @@ -13,10 +13,10 @@ In our `CreateMealView`, we are going to create a field called `state` which wil ... class CreateMealView(UnicornView): + state: str = "Add" meals: list[Meal] = None def mount(self): - state: str = "Add" self.meals = Meal.objects.all() ``` @@ -139,7 +139,7 @@ You'll notice that after the `{% else %}` statement that includes the "Cancel" b Each input is linked to a field in the component that matches the assignment. In other words, the `` line is looking for a field called `name` in our component. (Hint: we haven't created that field yet.) -Also, you'll notice the `defer` directive on these inputs. This is is done to store and save model changes until the next action gets triggered (in our case, clicking the "Save" button). +Also, you'll notice the `defer` directive on these inputs. This is is done in order to store and save model changes, but not until the next action gets triggered (in our case, clicking the "Save" button). ## Backend Logic diff --git a/docs/source/tutorials/mealplan/unicorn-functionality.md b/docs/source/tutorials/mealplan/unicorn-functionality.md index ed028e6e..1f7b915d 100644 --- a/docs/source/tutorials/mealplan/unicorn-functionality.md +++ b/docs/source/tutorials/mealplan/unicorn-functionality.md @@ -2,7 +2,7 @@ ## Setup -Django Unicorn uses the term "[Component](https://www.django-unicorn.com/docs/components/)" to refer to a set (or a block) of interactive functionality. Similar to how your Django _views_ connect to your _templates_, `Unicorn` uses a special [view class](https://www.django-unicorn.com/docs/views/) (`UnicornView`) residing within a special `components` directory linking to a specific _template_ with the _same name_ as the `component` module. +Django Unicorn uses the term "[Component](../../components.md)" to refer to a set (or a block) of interactive functionality. Similar to how your Django _views_ connect to your _templates_, `Unicorn` uses a special [view class](../../views.md) (`UnicornView`) residing within a special `components` directory linking to a specific _template_ with the _same name_ as the `component` module. Phew. That's a lot of words. Perhaps it's easier if you see it. @@ -45,7 +45,7 @@ If you build additional components, you would create new files in the `component In order to use components within your regular Django templates, you need to "include" them within your HTML file. -Let's go back to `index.html` and add that. +Let's go back to `index.html` and add it at the very top of the file. :::{code} html :force: true @@ -85,13 +85,15 @@ And secondly, a `{% csrf_token %}` tag within the body of your HTML. We can incl ::: -Note: In case you missed it earlier and if you haven't already done so, make sure that `django_unicorn` is listed within your `INSTALLED_APPS` in your `settings.py` file. +```{note} +In case you missed it earlier and if you haven't already done so, make sure that `django_unicorn` is listed within your `INSTALLED_APPS` in your `settings.py` file. +``` ## Components Components are where much of the Django Unicorn heavy lifting occurs. Here, we will define a `UnicornView`, which in turn contains the back end logic which will be passed to the corresponding `template`. -The interaction between the component and the tutorial is unique to this pairing, and it is "included" in your Django templates with a special template tag. The tag contains the name `unicorn`, followed by the name of the template, which in turn corresponds to the component. +The interaction between the component and the template is unique to this pairing, and it is "included" in your Django templates with a special template tag. The tag contains the name `unicorn`, followed by the name of the template, which in turn corresponds to the matching component. For example, to load the component we created earlier, we would add this template tag to our `meal.html` template. (Here, it is included within then `
    ` element). @@ -141,7 +143,9 @@ Now we can define what actually goes in the `create-meal.html` template. Notice the `unicorn:model` attribute on the `
    ` element. This is what "binds" this element to the logic we will write next in the `create_meal.py` component. -Note: The term `model` in this particular context _does not_ correlate to a Django `Model`. In other words, `unicorn:model` is what enables reactivity. Django Unicorn holds the fields from the component (`create_model.py`) in a special context. Then, when the element with `unicorn:model` triggers a change (whether on load, click, submit, blur, etc...), then it sends an AJAX request to a specific Unicorn endpoint, and the response is rendered in place. You don't necessarily have to understand all of that, but it's worth noting that `unicorn:model` is _not_ referring to your Django `Model` directly. +```{warning} +The term `model` in this particular context _does not_ correlate to a Django `Model`. In other words, `unicorn:model` is what enables reactivity. Django Unicorn holds the fields from the component (`create_model.py`) in a special context. Then, when the element with `unicorn:model` triggers a change (whether on load, click, submit, blur, etc...), then it sends an AJAX request to a specific Unicorn endpoint, and the response is rendered in place. You don't necessarily have to understand all of that, but it's worth noting that `unicorn:model` is _not_ referring to your Django `Model` directly. +``` Our last piece here is to actually write some logic in the component.