From 889a5e2767fbf5c27a2d85316b17a52db9072c99 Mon Sep 17 00:00:00 2001 From: Andros Fenollosa Date: Thu, 14 Mar 2024 11:02:04 +0100 Subject: [PATCH] Add tutorials --- assets/css/main.css | 31 +- one.org | 669 ++++++++++++++++++++++++++------------------ onerc.el | 28 +- 3 files changed, 448 insertions(+), 280 deletions(-) diff --git a/assets/css/main.css b/assets/css/main.css index b5a0cb2..10d40fe 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -90,6 +90,12 @@ a:hover, grid-gap: 1rem; } +@media (width < 600px) { + .nav-main__list > li:first-child { + margin-right: 6rem; + } +} + .nav-main__logo { width: 80px; margin-bottom: calc(var(--gap-m) * -1); @@ -101,12 +107,6 @@ a:hover, bottom: 0; } -@media (width < 600px) { - .nav-main__link--logo { - z-index: -1; - } -} - .center-block { display: block; margin-inline: auto; @@ -152,9 +152,11 @@ a:hover, } .button:hover { + --translate: 3px; border-color: var(--color-brown); - box-shadow: 0 5px 0 var(--color-brown); + box-shadow: var(--translate) var(--translate) 0 var(--color-brown); text-decoration: none; + transform: translate(calc(-1 * var(--translate)), calc(-1 * var(--translate))); } .header { @@ -178,7 +180,7 @@ a:hover, .header .nav__list { display: grid; - grid-template-columns: 1fr repeat(4, auto); + grid-template-columns: 1fr repeat(5, auto); } @media (width < 600px) { @@ -288,6 +290,11 @@ a:hover, } } +.nav-docs__list { + list-style: "➔ "; + list-style-position: inside; +} + /* Home */ .nav-home__list { @@ -333,7 +340,7 @@ a:hover, /* Docs */ .docs { display: grid; - grid-template-columns: 8rem auto; + grid-template-columns: 10rem auto; gap: var(--gap-l); } @@ -348,6 +355,12 @@ a:hover, } } +/* Tutorials */ +.tutorials ul { + list-style: "📚 "; + list-position: inside; +} + /* Footer */ .footer { text-align: center; diff --git a/one.org b/one.org index 396a03c..6019183 100644 --- a/one.org +++ b/one.org @@ -56,35 +56,23 @@ The same process is repeated for each action, such as clicking a button, submitt - Everything is asynchronous by default. - Don't learn anything new. If you know Python, you know how to use Django LiveView. -Are you ready to create your first Realtime SPA? Let's go to the [[#/docs/quickstart/][Quickstart]]. +Are you ready to create your first Realtime SPA? Let's go to the [[#/tutorials/quickstart/][Quickstart]]. -* Quickstart +* Install :PROPERTIES: :ONE: one-custom-default-doc -:CUSTOM_ID: /docs/quickstart/ -:TITLE: Quickstart -:DESCRIPTION: Get started with Django LiveView the easy way. +:CUSTOM_ID: /docs/install/ +:TITLE: Install +:DESCRIPTION: Install Django LiveView. :END: -Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python. - -All the steps are applied in a [[https://github.com/Django-LiveView/minimal-template][minimalist template]]. - -** 1. Install Django - -Install Django, create a project and an app. - -** 2. Install LiveView - -Install django-liveview with ~pip~. +You can install Django LiveView with ~pip~. #+BEGIN_SRC sh pip install django-liveview #+END_SRC -** 3. Modify the configuration - -Add ~liveview~ to your installed ~INSTALLED_APPS~. +Then add ~liveview~ to your installed ~INSTALLED_APPS~ in your ~settings.py~. #+BEGIN_SRC python INSTALLED_APPS = [ @@ -94,264 +82,21 @@ INSTALLED_APPS = [ ] #+END_SRC -Then indicate in which previously created App you want to implement LiveView. +You need to previously have installed ~channels~ and ~daphne~. + +Now, in ~settings.py~, indicate in which previously created App you want to integrate LiveView. #+BEGIN_SRC python LIVEVIEW_APPS = ["website"] #+END_SRC -** 4. Migration - -Execute the migrations so that the LiveView tables are generated. +Finally, execute the migrations so that the LiveView tables are generated. #+BEGIN_SRC python python manage.py migrate #+END_SRC -** 5. ASGI - -Modify the ASGI file, ~asgi.py~ to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different. - -#+BEGIN_SRC python -import os -import django - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") -django.setup() - -from channels.auth import AuthMiddlewareStack -from django.core.asgi import get_asgi_application -from channels.security.websocket import AllowedHostsOriginValidator -from channels.routing import ProtocolTypeRouter, URLRouter -from django.urls import re_path -from liveview.consumers import LiveViewConsumer - - -application = ProtocolTypeRouter( - { - # Django's ASGI application to handle traditional HTTP requests - "http": get_asgi_application(), - # WebSocket handler - "websocket": AuthMiddlewareStack( - AllowedHostsOriginValidator( - URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())]) - ) - ), - } -) -#+END_SRC - -** 6. Create your first Action - -Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it. - -Create inside your App a folder called ~actions~, here will go all the actions for each page. Now we will create inside the folder a file named ~home.py~. - -#+BEGIN_SRC python -# my-app/actions/home.py -from liveview.context_processors import get_global_context -from core import settings -from liveview.utils import ( - get_html, - update_active_nav, - enable_lang, - loading, -) -from channels.db import database_sync_to_async -from django.templatetags.static import static -from django.urls import reverse -from django.utils.translation import gettext as _ -from random import randint - -template = "pages/home.html" - -# Database - -# Functions - -async def get_context(consumer=None): - context = get_global_context(consumer=consumer) - # Update context - context.update( - { - "url": settings.DOMAIN_URL + reverse("home"), - "title": _("Home") + " | Home", - "meta": { - "description": _("Home page of the website"), - "image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}", - }, - "active_nav": "home", - "page": template, - } - ) - return context - - -@enable_lang -@loading -async def send_page(consumer, client_data, lang=None): - # Nav - await update_active_nav(consumer, "home") - # Main - my_context = await get_context(consumer=consumer) - html = await get_html(template, my_context) - data = { - "action": client_data["action"], - "selector": "#main", - "html": html, - } - data.update(my_context) - await consumer.send_html(data) - -async def random_number(consumer, client_data, lang=None): - my_context = await get_context(consumer=consumer) - data = { - "action": client_data["action"], - "selector": "#output-random-number", - "html": randint(0, 10), - } - data.update(my_context) - await consumer.send_html(data) -#+END_SRC - -There are several points in the above code to keep in mind. - -- ~template~ is the name of the template that will be rendered. -- ~get_context()~ is a function that returns a dictionary with the context of the page. -- ~send_page()~ is the function that will be executed when the page is loaded. -- ~random_number()~ is the function that will be executed when the button is clicked. - -** 7. Create the base template - -Now we will create the base template, which will be the one that will be rendered when the page is loaded. - -Create a folder called ~templates~, or use your template folder, inside your App and inside it create another folder called ~layouts~. Now create a file called ~base.html~. - -#+BEGIN_SRC html -{# my-app/templates/layouts/base.html #} -{% load static i18n %} -{% get_current_language as CURRENT_LANGUAGE %} - - - - {{ title }} - - - - - - -
-
-
-
-
- {% include 'components/header.html' %} -
-
{% include page %}
-
- {% include 'components/footer.html' %} -
-
- - -#+END_SRC - -In the future we will define ~main.js~, a minimal JavaScript to connect the events and the WebSockets client. - -** 8. Create the page template - -We will create the home page template, which will be the one that will be rendered when the page is loaded. - -Create a folder called ~pages~ in your template folder and inside it create a file called ~home.html~. - -#+BEGIN_SRC html -{# my-app/templates/pages/home.html #} -{% load static %} - -
-

- -

-

-
-#+END_SRC - -As you can see, we have defined a button to launch the action of generating the random number (~button~) and the place where we will print the result (~output-random-number~). - -** 9. Create frontend - -Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions. - -Download [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][assets]] and unzip it in your static folder. You will be left with the following route: ~/static/js/~. - -** 10. Create View - -We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application). - -In a normal Django application we would create a view, ~views.py~, similar to the following: - -#+BEGIN_SRC python -# my-app/views.py -from django.shortcuts import render - -# Create your views here. -def home(request): - return render(request, "pages/home.html") -#+END_SRC - -With LiveView, on the other hand, you will have the following structure. - -#+BEGIN_SRC python -# my-app/views.py -from django.shortcuts import render -from .actions.home import get_context as get_home_context - -from liveview.utils import get_html - -async def home(request): - return render(request, "layouts/base.html", await get_home_context()) -#+END_SRC - -** 11. Create URL - -Finally, we will create the URL that will render the page. - -#+BEGIN_SRC python -# my-app/urls.py -from django.urls import path - -from .views import home - -urlpatterns = [ - path("", home, name="home"), -] -#+END_SRC - -** 12. Run the server - -Run the server. - -#+BEGIN_SRC sh -python manage.py runserver -#+END_SRC - -And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number. - -#+ATTR_HTML: :class center-block image image--responsive -[[#/img/quickstart/minimal-template.webp][Random number]] +We strongly recommend that you follow the [[#/tutorials/quickstart/][Quickstart]] to see the installation in action. * Actions :PROPERTIES: @@ -689,6 +434,54 @@ You can customize the history management system by editing the ~history~ control If you want to disable it, remove `startHistory();` from ~assets/js/main.js~. +* Internationalization +:PROPERTIES: +:ONE: one-custom-default-doc +:CUSTOM_ID: /docs/internationalization/ +:TITLE: Internationalization +:DESCRIPTION: Internationalization of Django LiveView. +:END: + +Django LiveView uses the same internationalization system as Django. You can read more about it in the [[https://docs.djangoproject.com/en/dev/topics/i18n/][Django documentation]]. However, let's go deeper. + +Every time that the client sends a request for action, it sends the language that the user has set in the browser. + +#+BEGIN_SRC json + { + "action": "blog_list->send_page", + "data": { + "lang": "es" + } +} +#+END_SRC + +The ~lang~ attribute is extracted directly from the ~~ tag. + +#+BEGIN_SRC html + {% load static i18n %} + {% get_current_language as CURRENT_LANGUAGE %} + +#+END_SRC + +You can access the language with the ~lang~ parameter of the action using the ~@enable_lang~ decorator. + +#+BEGIN_SRC python + @enable_lang + @loading + async def send_page(consumer, client_data, lang=None): + print(lang) +#+END_SRC + +Or you could read it with the ~lang~ key of the ~client_data~ parameter. + + #+BEGIN_SRC python + @loading + async def send_page(consumer, client_data): + print(client_data["data"]["lang"]) +#+END_SRC + +You can read the tutorial [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]] to see how to create a multilingual website with subdomains. + * Deploy :PROPERTIES: :ONE: one-custom-default-doc @@ -767,6 +560,348 @@ Yes, you can. Only me and my free time. + + +* Tutorials +:PROPERTIES: +:ONE: one-custom-default-tutorials +:CUSTOM_ID: /tutorials/ +:TITLE: Tutorials +:DESCRIPTION: Tutorials about Django LiveView. +:END: + +** Tutorials + +Learn how to create a Django LiveView application step by step. + +- [[#/tutorials/quickstart/][Quickstart]] +- [[#/tutorials/internationalize-with-subdomains/][Internationalize with subdomains]] + +* Quickstart +:PROPERTIES: +:ONE: one-custom-default-page +:CUSTOM_ID: /tutorials/quickstart/ +:TITLE: Quickstart +:DESCRIPTION: Get started with Django LiveView the easy way. +:END: + +Welcome to the Quickstart guide. Here you will learn how to create your first Realtime SPA using Django LiveView. I assume you have a basic understanding of Django and Python. + +All the steps are applied in a [[https://github.com/Django-LiveView/minimal-template][minimalist template]]. + +** 1. Install Django + +Install Django, create a project and an app. + +** 2. Install LiveView + +Install django-liveview with ~pip~. + +#+BEGIN_SRC sh +pip install django-liveview +#+END_SRC + +** 3. Modify the configuration + +Add ~liveview~ to your installed ~INSTALLED_APPS~. + +#+BEGIN_SRC python +INSTALLED_APPS = [ + "daphne", + "channels", + "liveview", +] +#+END_SRC + +Then indicate in which previously created App you want to implement LiveView. + +#+BEGIN_SRC python +LIVEVIEW_APPS = ["website"] +#+END_SRC + +** 4. Migration + +Execute the migrations so that the LiveView tables are generated. + +#+BEGIN_SRC python +python manage.py migrate +#+END_SRC + +** 5. ASGI + +Modify the ASGI file, ~asgi.py~ to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different. + +#+BEGIN_SRC python +import os +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") +django.setup() + +from channels.auth import AuthMiddlewareStack +from django.core.asgi import get_asgi_application +from channels.security.websocket import AllowedHostsOriginValidator +from channels.routing import ProtocolTypeRouter, URLRouter +from django.urls import re_path +from liveview.consumers import LiveViewConsumer + + +application = ProtocolTypeRouter( + { + # Django's ASGI application to handle traditional HTTP requests + "http": get_asgi_application(), + # WebSocket handler + "websocket": AuthMiddlewareStack( + AllowedHostsOriginValidator( + URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())]) + ) + ), + } +) +#+END_SRC + +** 6. Create your first Action + +Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it. + +Create inside your App a folder called ~actions~, here will go all the actions for each page. Now we will create inside the folder a file named ~home.py~. + +#+BEGIN_SRC python +# my-app/actions/home.py +from liveview.context_processors import get_global_context +from core import settings +from liveview.utils import ( + get_html, + update_active_nav, + enable_lang, + loading, +) +from channels.db import database_sync_to_async +from django.templatetags.static import static +from django.urls import reverse +from django.utils.translation import gettext as _ +from random import randint + +template = "pages/home.html" + +# Database + +# Functions + +async def get_context(consumer=None): + context = get_global_context(consumer=consumer) + # Update context + context.update( + { + "url": settings.DOMAIN_URL + reverse("home"), + "title": _("Home") + " | Home", + "meta": { + "description": _("Home page of the website"), + "image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}", + }, + "active_nav": "home", + "page": template, + } + ) + return context + + +@enable_lang +@loading +async def send_page(consumer, client_data, lang=None): + # Nav + await update_active_nav(consumer, "home") + # Main + my_context = await get_context(consumer=consumer) + html = await get_html(template, my_context) + data = { + "action": client_data["action"], + "selector": "#main", + "html": html, + } + data.update(my_context) + await consumer.send_html(data) + +async def random_number(consumer, client_data, lang=None): + my_context = await get_context(consumer=consumer) + data = { + "action": client_data["action"], + "selector": "#output-random-number", + "html": randint(0, 10), + } + data.update(my_context) + await consumer.send_html(data) +#+END_SRC + +There are several points in the above code to keep in mind. + +- ~template~ is the name of the template that will be rendered. +- ~get_context()~ is a function that returns a dictionary with the context of the page. +- ~send_page()~ is the function that will be executed when the page is loaded. +- ~random_number()~ is the function that will be executed when the button is clicked. + +** 7. Create the base template + +Now we will create the base template, which will be the one that will be rendered when the page is loaded. + +Create a folder called ~templates~, or use your template folder, inside your App and inside it create another folder called ~layouts~. Now create a file called ~base.html~. + +#+BEGIN_SRC html +{# my-app/templates/layouts/base.html #} +{% load static i18n %} +{% get_current_language as CURRENT_LANGUAGE %} + + + + {{ title }} + + + + + + +
+
+
+
+
+ {% include 'components/header.html' %} +
+
{% include page %}
+
+ {% include 'components/footer.html' %} +
+
+ + +#+END_SRC + +In the future we will define ~main.js~, a minimal JavaScript to connect the events and the WebSockets client. + +** 8. Create the page template + +We will create the home page template, which will be the one that will be rendered when the page is loaded. + +Create a folder called ~pages~ in your template folder and inside it create a file called ~home.html~. + +#+BEGIN_SRC html +{# my-app/templates/pages/home.html #} +{% load static %} + +
+

+ +

+

+
+#+END_SRC + +As you can see, we have defined a button to launch the action of generating the random number (~button~) and the place where we will print the result (~output-random-number~). + +** 9. Create frontend + +Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions. + +Download [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][assets]] and unzip it in your static folder. You will be left with the following route: ~/static/js/~. + +** 10. Create View + +We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application). + +In a normal Django application we would create a view, ~views.py~, similar to the following: + +#+BEGIN_SRC python +# my-app/views.py +from django.shortcuts import render + +# Create your views here. +def home(request): + return render(request, "pages/home.html") +#+END_SRC + +With LiveView, on the other hand, you will have the following structure. + +#+BEGIN_SRC python +# my-app/views.py +from django.shortcuts import render +from .actions.home import get_context as get_home_context + +from liveview.utils import get_html + +async def home(request): + return render(request, "layouts/base.html", await get_home_context()) +#+END_SRC + +** 11. Create URL + +Finally, we will create the URL that will render the page. + +#+BEGIN_SRC python +# my-app/urls.py +from django.urls import path + +from .views import home + +urlpatterns = [ + path("", home, name="home"), +] +#+END_SRC + +** 12. Run the server + +Run the server. + +#+BEGIN_SRC sh +python manage.py runserver +#+END_SRC + +And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number when clicked. + +#+ATTR_HTML: :class center-block image image--responsive +[[#/img/quickstart/minimal-template.webp][Random number]] + +Congratulations! You have created your first Realtime SPA using Django LiveView. + +The next step is to create a more complex application. You can read other [[#/tutorials/][tutorials]] or go to the [[#/docs/install/][documentation]]. + +* Internationalize with subdomains +:PROPERTIES: +:ONE: one-custom-default-page +:CUSTOM_ID: /tutorials/internationalize-with-subdomains/ +:TITLE: Internationalize with subdomains +:DESCRIPTION: Create a multilingual website with subdomains. +:END: + +Here you will learn how to create a multilingual website using Django LiveView. We recommend using subdomains to define the language (~en.example.com~, ~es.example.com~...), instead of using prefixes in addresses (~example.com/en/blog/~, ~example.com/es/blog/~). They simplify SEO, maintain consistency in the Sitemap and are easy to test. + +** 1. Configure the subdomains + +** 2. Create the subdomains + +** 3. Configure the languages + +** 4. Redirection with Middleware + +** 5. First text + +** 6. Make messages + +** 7. Compile messages + +** 8. Selector of languages + * Source code :PROPERTIES: :ONE: one-custom-default-page diff --git a/onerc.el b/onerc.el index b59eafc..f72c819 100644 --- a/onerc.el +++ b/onerc.el @@ -97,13 +97,15 @@ (:li.nav-main__item (:a.nav-main__link.nav-main__link--logo (@ :href "/") (:img.nav-main__logo (@ :alt "Django LiveView" :src "/img/logo.webp")))) (:li.nav-main__item - (:a.button.nav-main__link (@ :href "/docs/quickstart/") "Docs")) + (:a.button.nav-main__link (@ :href "/docs/install/") "Docs")) (:li.nav-main__item - (:a.button.nav-main__link (@ :href "/source-code/") "Source code")) + (:a.button.nav-main__link (@ :href "/tutorials/") "Tutorials")) (:li.nav-main__item (:a.button.nav-main__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo")) (:li.nav-main__item - (:a.button.nav-main__link (@ :href "/books/") "Books")))))) + (:a.button.nav-main__link (@ :href "/books/") "Books")) + (:li.nav-main__item + (:a.button.nav-main__link (@ :href "/source-code/") "Source code")))))) ,tree-content (:footer.footer (:p "Created with " (:i (@ :aria-label "love") "❤️") " by " (:a.link (@ :href "https://andros.dev/" :target "_blank") "Andros Fenollosa") " with " (:a.link (@ :href "https://one.tonyaldon.com/" :target "_blank") "one.el")) @@ -168,7 +170,7 @@ (:nav.nav-docs (:ul.nav__list.nav__list--docs.nav-docs__list (:li.nav-docs__item - (:a.nav-docs__link (@ :href "/docs/quickstart/") "Quickstart")) + (:a.nav-docs__link (@ :href "/docs/install/") "Install")) (:li.nav-docs__item (:a.nav-docs__link (@ :href "/docs/actions/") "Actions")) (:li.nav-docs__item @@ -177,6 +179,8 @@ (:a.nav-docs__link (@ :href "/docs/routing/") "Routing")) (:li.nav-docs__item (:a.nav-docs__link (@ :href "/docs/history/") "History")) + (:li.nav-docs__item + (:a.nav-docs__link (@ :href "/docs/internationalization/") "Internationalization")) (:li.nav-docs__item (:a.nav-docs__link (@ :href "/docs/deploy/") "Deploy")) (:li.nav-docs__item @@ -185,6 +189,22 @@ (:main.main.main--docs ,content)))))) +(defun one-custom-default-tutorials (page-tree pages _global) + "Default render function by tutorials page." + (let* ((title (org-element-property :raw-value page-tree)) + (description (org-element-property :DESCRIPTION page-tree)) + (path (org-element-property :CUSTOM_ID page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages)) + (nav (one-default-nav path pages))) + (render-layout-html + title + description + (jack-html `(:main.main + (:section.tutorials + (:div.container ,content))))))) ;; Sitemap (defun make-sitemap (pages tree global)