Si vous ne connaissez pas, un favicon, c'est cette mini icône pixelisée que vous pouvez voir dans l'onglet de votre navigateur qui en général est le logo du site que vous visitez. Hé bien le dev Tim Wehrle a réussi à y faire tenir un site web entier en stockant tout son HTML dans les 212 octets de l'image.
L'astuce est toute con vous allez voir... Une image, c'est des pixels. Et chaque pixel a 3 valeurs : rouge, vert, bleu, soit 3 octets. Du coup il prend son HTML, le découpe octet par octet, et colle chaque octet dans une couleur. Premier octet dans le rouge, le suivant dans le vert, puis le bleu, et on continue ainsi de suite. Et voilà comment il arrive à stocker 208 octets de HTML dans un carré de 9 pixels sur 9. Et au final, le truc ressemble à des confettis random.
Pour relire tout ça ensuite, un peu de JavaScript suffit. Le navigateur charge l'image, la passe dans un canvas, relit chaque pixel, recolle les octets et réaffiche la page. Il n'y a donc rien à installer et pour tester ça par vous-même, il a même mis une démo ici où vous pouvez regarder l'encodage / décodage se faire sous vos yeux ébahis.
Maintenant, n'espérez pas faire tenir votre site e-commerce là dedans hein puisque la capacité plafonne à 239 octets. Et sans le bout de JavaScript qui décode, le favicon tout seul ne servira à rien. Et ce n'est pas de la stéganographie non plus puisqu'on ne planque rien dans une vraie image. La data c'est l'image...
Mais c'est le genre de hack inutile que j'adore ! Le code est sur GitHub si vous êtes curieux et il raconte toute sa démarche sur son blog .
![]()
Je ne sais pas si vous avez déjà joué à Pac-Man parce que c'est quand même un bon vieux jeu de boomer, mais si c'est le cas, vous avez peut-être déjà ressenti un peu de peine pour ces petits fantômes qui se font avaler tout cru par ce déglingot de Pac-Man.
Non ? Bah Garrit Franke, oui. Du coup il a codé pac-hunt, un petit jeu où vous incarnez ENFIN le fantôme qui traque le glouton jaune dans son labyrinthe, au lieu de vous faire bouffer bêtement.
C'est jouable directement depuis votre navigateur , sans rien installer. Vous déplacez votre fantôme avec les flèches de votre clavier ou les touches WASD, et votre boulot c'est de coincer ce "Gentil p'tit bonhomme" (vous l'avez ?) avant qu'il ne nettoie tout le labyrinthe de ses pastilles. Sauf que Garrit a gardé le pire souvenir de ses parties d'enfance, ce sale moment qui ruinait tout : Pac-Man gobe une super-pastille, et les rôles s'inversent !!! Hiiii tous aux abris, car pendant ces quelques secondes, c'est lui qui vous court après et c'est vous qui détalez.
Le tout tient dans un seul fichier HTML , sans le moindre framework, sous licence MIT et le glouton rond comme un ballon (vous l'avez ? lol oui j'aime le comique de répétition) n'est pas piloté au pif, puisqu'il a sa petite IA qui cherche les pastilles les plus proches et qui prend ses jambes à son cou dès que votre fantôme approche à moins de 7 cases. Et si vous le trouvez trop malin ou trop bête, les réglages de vitesse et de comportement sont dispo en haut du script, donc n'hésitez pas à bidouiller ça.
J'avoue que ça me donne des idées parce que des classiques à retourner comme une crêpe, y'en a un paquet. J'imagine par exemple un Mario où vous jouez le Goomba qui voulait juste traverser tranquillou le niveau pour aller faire ses courses au marché et qui se fait ratatiner le crâne par un gars amateur de champi en roues libres. On pourrait aussi jouer à un niveau de Donkey Kong où vous êtes le tonneau, avec comme seul objectif dans votre vie : rouler droit ! Et puis Space Invaders vu par l'alien du fond de la troupe qui regarde ses potes se faire dégommer rangée par rangée. Gloups !
Et Flappy Bird où vous êtes le tuyau, et vous ne faites rien... Juste vous attendez, et vous gagnez quand même ! lol... Et mon préféré, Pong où vous jouez la balle, qui rebondit d'un bord à l'autre sans fin, à vous poser de vraies questions sur le sens de la vie.
Voilà, si vous aimez le rétro-gaming autant que vous détestez prendre une douche, alors vous allez kiffer Pac-Hunt. Ça vous occupera durant cette loooongue semaine de bullshit job en attendant la mort (oui, je suis très positif en ce moment), et si l'envie vous prend de creuser le mythe, le vrai Pac-Man cache un bug bien connu des fans, et côté classiques recodés pour le navigateur, Pokémon en JavaScript vaut aussi le coup d'œil.
Bref, pac-hunt c'est 5 minutes de plaisir vengeur dans votre navigateur, et zéro install. Blinky compte sur vous les amis !
Source : Le blog de Garrit Franke
![]()
Speedtest.net c'est pratique, sauf que ça a été racheté par Accenture en 2026 et chaque test envoie votre IP, le nom de votre FAI et votre localisation à leurs serveurs...
Alors pourquoi ne pas adopter LibreSpeed , l'alternative open source que vous hébergez chez vous, et qui ne contient aucun tracker ??? L'outil est signé Federico Dossena, ça a été lancé en 2016 et c'est sous licence LGPL v3. Et ce que ça mesure c'est la vitesse du download, de l'upload, le ping, le jitter , et ça relève aussi l'adresse IP et le nom de votre FAI.
Côté déploiement, c'est du classique. Vous déposez les fichiers sur votre serveur, dont speedtest.js et speedtest_worker.js pour le frontend, vous configurez le backend via config.json, et hop c'est lancé. Un simple VPS ou n'importe quelle autre machine sur votre LAN fera l'affaire.
Et comme vous choisissez le serveur de test, y'a pas de saturation par d'autres utilisateurs, pas de route réseau bizarre vers un data center à l'autre bout de l'Europe et surtout, les résultats sont parfaitement reproductibles donc pour les "homelabers" (c'est comme ça qu'on dit ?) ou les équipes réseau, c'est assez chouette.
Il y a aussi le mode multi-serveur pour comparer des endpoints, le partage de résultats via lien unique, et un CLI librespeed avec ses flags --server et --host pour les fans du terminal. Des backends officiels en Go et Rust existent aussi sur le github de
librespeed
, signe que le projet est sérieux.
Et surtout, les résultats sont fiables, sauf si votre serveur plafonne à 20 Mbit/s et dans ce cas, vous mesurerez sa liaison et pas la vôtre. Ça coule de source, mais je tenais à le préciser quand même...
Voilà, l'interface est moche, en tout cas beaucoup plus que celle de Speed.cloudflare.com , par contre l'essentiel est là. Et surtout, les mesures restent chez vous plutôt que de partir ailleurs.
![]()
BrowserQuest est de retour les amis ! Hé ouais, el famoso MMO HTML5 que Little Workshop avait pondu pour Mozilla en 2012 vient de ressurgir sur threej.in , alors que Mozilla avait officiellement archivé le repo GitHub en janvier 2024.
En allant sur ce lien, vous choisissez le nom de votre personnage, et hop, vous vous retrouvez comme y'a plus de 10 ans, dans un monde 2D en pixel art prêt à vous taper avec des rats et des squelettes direct dans le navigateur.
Petit rappel pour les endormis du fond à côté du radiateur, quand BrowserQuest est sorti en mars 2012, c'était un événement !! Mozilla voulait avec ce jeu, montrer au monde entier que le navigateur pouvait faire tourner un MMO temps réel sans Flash, sans aucun plugin, mais juste avec du HTML5 et des WebSockets. Le studio parisien Little Workshop (les frères Guillaume et Franck Lecollinet) avait alors livré une démo incroyable qui ressemblait à un Zelda 16 bits, avec des quêtes, de l'équipement, un chat intégré et même du combat coopératif. Comme je suis viiieux de fous, je vous en avais même parlé à l'époque .
Car techniquement, c'était du sérieux... Canvas pour le rendu 2D, Web Workers pour charger la map sans freezer la page, localStorage pour sauvegarder votre perso, CSS3 Media Queries pour s'adapter au mobile, et surtout WebSockets pour la communication bi-directionnelle avec le serveur Node.js.
Du coup le jeu pouvait encaisser des CENTAINES de joueurs simultanés, avec un pic réel enregistré à plus de 1 900 connexions en même temps. C'est ouf ! Faut dire que 2 ans plus tôt, Quake 2 tournait déjà en HTML5 et on commençait alors tous à comprendre que notre navigateur allait devenir une véritable plateforme gaming.
Sauf que voilà, Mozilla n'a jamais vraiment maintenu BrowserQuest après le buzz initial. Le serveur officiel browserquest.mozilla.org a fini par mourir, le repo GitHub a basculé en mode DEPRECATED, et en janvier 2024, la fondation a appuyé sur le bouton "Archive" pour de bon. Fin du game ! Sniiiif...
Sauf que le code est sous licence MPL 2.0 et le contenu en CC-BY-SA 3.0 donc en gros, n'importe qui peut reprendre le bousin, le réhéberger et le relancer !
Hé bien c'est exactement ce que threej.in a fait et ça fonctionne parfaitement, y compris l'aspect multijoueur. L'écran de crédit affiche même encore "Made for Mozilla by Little Workshop" comme si rien n'avait bougé.
Perso, je trouve ça cool qu'un projet open source archivé aussi culte puisse continuer à vivre grâce à un inconnu qui a pris la peine de le remettre en ligne. C'est aussi à ça que servent les licences libres finalement... prolonger la vie des programmes au-delà de la motivation de leurs créateurs. Chouette hein ?
À l'époque, encaisser 1900 joueurs en simultané sur un backend Node.js relevait de la prouesse technique, alors qu'aujourd'hui, tout semble beaucoup plus facile puisqu'on trouve des dizaines de jeux .io et autres qui tournent dans un onglet de browser sans que ça nous fasse lever un sourcil. La techno derrière BrowserQuest est devenue tellement banale qu'on a tous oublié à quel point elle était impressionnante à l'époque !
Bref, c'est gratuit, c'est dans le navigateur, et ça tient toujours debout !
![]()
I like django-crispy-forms. You can use it for stylish uniform HTML forms with Bootstrap, TailwindCSS, or even your custom template pack. But when it comes to custom widgets and dynamic form handling, it was always a challenge. Recently I discovered htmx. It's a JavaScript framework that handles Ajax communication based on custom HTML attributes. In this article, I will explore how you can use django-crispy-forms with htmx to provide a form with server-side validation in a modal dialog.
For this experiment, I will be using these PyPI packages:
Also, I will use the CDN versions of Bootstrap5 and htmx.
I decided to add some crispy style to the login form by extending Django's authentication form and attaching a crispy helper to it.
from django.contrib.auth.forms import AuthenticationForm
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout
from crispy_bootstrap5 import bootstrap5
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.include_media = False
self.helper.layout = Layout(
bootstrap5.FloatingField("username", autocomplete="username"),
bootstrap5.FloatingField("password", autocomplete="current-password"),
)Here I set form_tag to False to skip the <form> tag from the rendered form elements because I want to customize it in the templates. And I set include_media to False because I want to manually handle the media inclusion, as you will see later, instead of automatically including them in the form.
I will have two views:
from django.shortcuts import render, redirect
from django.contrib.auth import login as auth_login, logout as auth_logout
from django_htmx.http import HttpResponseClientRefresh
from .forms import LoginForm
def home(request):
if request.user.is_authenticated:
return render(request, "dashboard.html")
return render(request, "home.html", context)
def login(request):
if request.method == "POST":
form = LoginForm(request=request, data=request.POST, prefix="login")
template_name = "login_form.html"
if form.is_valid():
user = form.get_user()
auth_login(request, user)
return HttpResponseClientRefresh()
else:
form = LoginForm(request=request, prefix="login")
template_name = "login_dialog.html"
context = {"form": form}
return render(request, template_name, context)
The home view just returns different templates based on whether the user is logged in or not.
The login view handles a login form and renders different templates based on whether the view was accessed by GET or POST method. If the login succeeds, a special HttpResponseClientRefresh response is returned, which tells htmx to refresh the page from where the login form was loaded and submitted. It's an empty response with the HX-Refresh: true header.
Then I plug those two views into my urls.py rules.
At the end of the base.html template, I include htmx from CDN and my custom dialog.js:
<script src="https://unpkg.com/htmx.org@1.8.2" integrity="sha384-+8ISc/waZcRdXCLxVgbsLzay31nCdyZXQxnsUy++HJzJliTzxKWr0m1cIEMyUzQu" crossorigin="anonymous"></script>
<script src="{% static 'js/dialog.js' %}"></script>In the home.html template, I add a button which will open the dialog:
<button
data-hx-get="{% url 'login' %}"
data-hx-target="main"
data-hx-swap="beforeend"
type="button"
class="btn btn-primary"
>Log in</button>Note that htmx allows either hx-* syntax or data-hx-* for its HTML attributes and I prefer the latter because data-* attributes make a valid HTML document.
In the snippet above, I tell htmx to load the login page on the button click and insert it before the end of the <main> HTML tag.
Let's have a look at my dialog.js file:
function init_widgets_for_htmx_element(target) {
// init other widgets
// init modal dialogs
if (target.tagName === 'DIALOG') {
target.showModal();
htmx.on('.close-dialog', 'click', function(event) {
var dialog = htmx.find('dialog[open]');
dialog.close();
htmx.remove(dialog);
});
}
}
htmx.onLoad(init_widgets_for_htmx_element);Here, when a page loads or a htmx inserts a snippet, the init_widgets_for_htmx_element function will be called with the <body> or the inserted element as the target. One can use this function to initialize widgets, such as rich text fields, autocompletes, tabs, custom frontend validators, etc. Also, I use this function to open the loaded modal dialogs and add event handlers to close them.
Now the login_dialog.html template looks like this (I just stripped the styling markup):
<dialog id="htmx-dialog">
<h1>Login</h1>
<button type="button" class="close-dialog btn-close" aria-label="Close"></button>
{% include "login_form.html" %}
<button type="submit" form="htmx-dialog-form">Log in</button>
</dialog>And the login_form.html looks like this:
<form
id="htmx-dialog-form"
novalidate
data-hx-post="{% url 'login' %}"
data-hx-swap="outerHTML"
>
{% load crispy_forms_tags %}
{% crispy form %}
</form>The htmx attributes tell htmx to submit the form data to the login view by Ajax and replace the <form> HTML tag with the response received. That is used for form validation. If the response has the HX-Refresh: true header, as mentioned before, then the home page is refreshed.
Here is what the result looks like:
If you noticed before, we excluded the media from the form. Otherwise, the home page would load the media of its own forms (for example, a search form) and the media of the dialog forms. And that would cause double executions of shared scripts, for example, the ones for autocompletes and rich text fields.
You can nicely combine media from different forms by concatenating the media instances. This way, each CSS and Javascript file is included just once.
As the login form isn't part of the home page but instead included on demand, I need to have its media in the home view or any other view where the login button is shown.
Here comes this custom context processor for help:
def login_dialog(request):
from .forms import LoginForm
if not request.user.is_authenticated:
form = LoginForm(request=request)
if hasattr(request, "combined_media"):
request.combined_media += form.media
else:
request.combined_media = form.media
return {}It checks for any request.combined_media and attaches the form media from the login form.
Lastly, I attach this context processor to the template settings and render the value of combined media before </body>:
{{ request.combined_media }}Get the code to play with from Github. As you can see, htmx makes Ajax communications pretty straightforward, even when it's about dynamically loading and reloading parts of the content or initializing custom widgets.
Cover photo by Pixabay
![]()
For quite some time, I have been building a SaaS product - strategic prioritizer 1st things 1st. It's using Django in the backend and ReactJS in the frontend and communicating between those ends by REST API. Every week I try to make progress with this project, be it a more prominent feature, some content changes, or small styling tweaks. In the past week, I implemented frontend testing with Selenium, and I want to share my journey with you.
1st things 1st allows you to evaluate a list of items by multiple criteria and calculates priorities for you to follow and take action. The service has 4 main steps:
Selenium is a testing tool that mimics user interaction in the browser: you can fill in fields, trigger events, or read out information from the HTML tags. To test the frontend of 1st things 1st with Selenium, I had to
Let's see how I did it.
In 2020, Chrome is the most popular browser, and it's my default browser, so I decided to develop tests using it.
I had to install Selenium with pip into my virtual environment:
(venv)$ pip install seleniumAlso, I needed a binary chromedriver, which makes Selenium talk to your Chrome browser. I downloaded it and placed it under myproject/drivers/chromedriver.
In the Django project configuration, I needed a couple of settings. I usually have separate settings-file for each of the environments, such as:
myproject.settings.local for the local development,myproject.settings.staging for the staging server,myproject.settings.test for testing, andmyproject.settings.production for production.All of them import defaults from a common base, and I have to set only the differences for each environment.
In the myproject.settings.test I added these settings:
WEBSITE_URL = 'http://my.1st-things-1st.127.0.0.1.xip.io:8080' # no trailing slash
TESTS_SHOW_BROWSER = TrueHere for the WEBSITE_URL, I was using the xip.io service. It allows you to create domains dynamically pointing to the localhost or any other IP. The Selenium tests will use this URL.
The TEST_SHOW_BROWSER was my custom setting, telling whether to show a browser while testing the frontend or just to run the tests in the background.
In one of my apps, myproject.apps.evaluations, I created a tests package, and there I placed a test case test_evaluations_frontend.py with the following content:
import os
from time import sleep
from datetime import timedelta
from django.conf import settings
from django.test import LiveServerTestCase
from django.test import override_settings
from django.contrib.auth import get_user_model
from django.utils import timezone
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
User = get_user_model()
SHOW_BROWSER = getattr(settings, "TESTS_SHOW_BROWSER", False)
@override_settings(DEBUG=True)
class EvaluationTest(LiveServerTestCase):
host = settings.WEBSITE_URL.rsplit(":", 1)[0].replace(
"http://", ""
) # domain before port
port = int(settings.WEBSITE_URL.rsplit(":", 1)[1]) # port
USER1_USERNAME = "user1"
USER1_FIRST_NAME = "user1"
USER1_LAST_NAME = "user1"
USER1_EMAIL = "user1@example.com"
USER1_PASSWORD = "change-me"
@classmethod
def setUpClass(cls):
# …
@classmethod
def tearDownClass(cls):
# …
def wait_until_element_found(self, xpath):
# …
def wait_a_little(self, seconds=2):
# …
def test_evaluations(self):
# …It's a live-server test case, which runs a Django development server under the specified IP and port and then runs the Chrome browser via Selenium and navigates through the DOM and fills in forms.
By default, the LiveServerTestCase runs in non-debug mode, but I want to have the debug mode on so that I could see any causes of server errors. With the @override_settings decorator, I could change the DEBUG setting to True.
The host and port attributes define on which host and port the test server will be running (instead of a 127.0.0.1 and a random port). I extracted those values from the WEBSITE_URL setting.
The test case also had some attributes for the user who will be navigating through the web app.
Let's dig deeper into the code for each method.
Django test cases can have class-level setup and teardown, which run before and after all methods whose names start with test_:
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user1 = User.objects.create_user(
cls.USER1_USERNAME, cls.USER1_EMAIL, cls.USER1_PASSWORD
)
# … add subscription for this new user …
driver_path = os.path.join(settings.BASE_DIR, "drivers", "chromedriver")
chrome_options = Options()
if not SHOW_BROWSER:
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=1200,800")
cls.browser = webdriver.Chrome(
executable_path=driver_path, options=chrome_options
)
cls.browser.delete_all_cookies()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
cls.browser.quit()
# … delete subscription for the user …
cls.user1.delete()In the setup, I created a new user, added a subscription to them, and prepared the Chrome browser to use.
If the TEST_SHOW_BROWSER setting was False, Chrome was running headless, that is, in the background without displaying a browser window.
When the tests were over, the browser closed, and the subscription, as well as the user, were deleted.
I created two utility methods for my Selenium test: wait_until_element_found() and wait_a_little():
def wait_until_element_found(self, xpath):
WebDriverWait(self.browser, timeout=10).until(
lambda x: self.browser.find_element_by_xpath(xpath)
)
def wait_a_little(self, seconds=2):
if SHOW_BROWSER:
sleep(seconds)I used the wait_until_element_found(xpath) method to keep the test running while pages switched.
I used the wait_a_little(seconds) method to stop the execution for 2 or more seconds so that I could follow what's on the screen, make some screenshots, or even inspect the DOM in the Web Developer Inspector.
Selenium allows to select DOM elements by ID, name, CSS class, tag name, and other ways, but the most flexible approach, in my opinion, is selecting elements by XPath (XML Path Language).
Contrary to jQuery, ReactJS doesn't use IDs or CSS classes in the markup to update the contents of specific widgets. So the straightforward Selenium's methods for finding elements by IDs or classes won't always work.
XPath is a very flexible and powerful tool. For example, you can:
"//input[@id='id_title']""//div[@aria-label='Blank']""//button[.='Save']""//button[contains(@class,'btn-primary')][.='Save']""(//button[.='yes'])[1]"You can try out XPath syntax and capabilities in Web Developer Console in Chrome and Firefox, using the $x() function, for example:
» $x("//h1[.='Projects']")
← Array [ h1.display-4.mb-4 ]I started with opening a login page, dismissing cookie consent notification, filling in user credentials into the login form, creating a new project from a blank template, setting title and description, etc.
def test_evaluations(self):
self.browser.get(f"{self.live_server_url}/")
self.wait_until_element_found("//h1[.='Log in or Sign up']")
# Accept Cookie Consent
self.wait_until_element_found("//a[.='Got it!']")
self.browser.find_element_by_xpath("//a[.='Got it!']").click()
# Log in
self.browser.find_element_by_id("id_email").send_keys(self.USER1_EMAIL)
self.browser.find_element_by_id("id_password").send_keys(self.USER1_PASSWORD)
self.browser.find_element_by_xpath('//button[text()="Log in"]').send_keys(
"\n"
) # submit the form
self.wait_until_element_found("//h1[.='Projects']")
# Click on "Add new project"
self.wait_until_element_found("//a[.='Add new project']")
self.wait_a_little()
self.browser.find_element_by_xpath("//a[.='Add new project']").send_keys("\n")
self.wait_until_element_found("//div[@aria-label='Blank']")
# Create a project from the project template "Blank"
self.wait_a_little()
self.browser.find_element_by_xpath("//div[@aria-label='Blank']").send_keys("\n")
# Enter project title and description
self.wait_until_element_found("//input[@id='id_title']")
self.browser.find_element_by_xpath("//input[@id='id_title']").send_keys(
"Urgent and Important Activities"
)
self.browser.find_element_by_xpath(
"//textarea[@id='id_description']"
).send_keys("I want to find which things to do and which to skip.")
self.browser.find_element_by_xpath("//button[.='Next']").send_keys("\n")
# Keep the default verbose names for the criteria and initiatives
self.wait_until_element_found("//input[@id='id_initiative_verbose_name_plural']")
self.wait_a_little()
self.browser.find_element_by_xpath("//button[.='Next']").send_keys("\n")If TESTS_SHOW_BROWSER was set to True, we would see all this workflow in an opened browser window.
I was creating the test by carefully inspecting the markup in Web Developer Inspector and creating appropriate DOM navigation with XPath. For most of the navigation, I was using send_keys() method, which triggers keyboard events. During the testing, I also noticed that my cookie consent only worked with a mouse click, and I couldn't approve it by the keyboard. That's some room for improving accessibility.
I ran the test with the following command each time I added some more lines:
(venv)$ python manage.py test myproject.apps.evaluations --settings=myproject.settings.testThe test case failed if any command in the test failed. I didn't even need asserts.
Now it was time to add some criteria:
self.wait_until_element_found("//h2[.='Criteria']")
# Add new criterion "Urgent" with the evaluation type Yes/No/Maybe
self.wait_until_element_found("//a[.='Add new criterion']")
self.browser.find_element_by_xpath("//a[.='Add new criterion']").send_keys("\n")
self.wait_until_element_found("//input[@id='id_title']")
self.browser.find_element_by_xpath("//input[@id='id_title']").send_keys(
"Urgent"
)
self.browser.find_element_by_xpath("//input[@id='widget_y']").send_keys(" ")
self.browser.find_element_by_xpath("//button[.='Save']").send_keys("\n")
# Add new criterion "Important" with the evaluation type Yes/No/Maybe
self.wait_until_element_found("//a[.='Add new criterion']")
self.browser.find_element_by_xpath("//a[.='Add new criterion']").send_keys("\n")
self.wait_until_element_found("//input[@id='id_title']")
self.browser.find_element_by_xpath("//input[@id='id_title']").send_keys(
"Important"
)
self.browser.find_element_by_xpath("//input[@id='widget_y']").send_keys(" ")
self.browser.find_element_by_xpath("//button[.='Save']").send_keys("\n")
# Click on the button "Done"
self.wait_until_element_found("//a[.='Done']")
self.browser.find_element_by_xpath("//a[.='Done']").send_keys("\n")I added two criteria, "Urgent" and "Important", with evaluation type "Yes/No/Maybe".
![]()
Then I created some activities to evaluate:
self.wait_until_element_found("//h2[.='Things']")
# Add new thing "Write a blog post"
self.wait_until_element_found("//a[.='Add new thing']")
self.browser.find_element_by_xpath("//a[.='Add new thing']").send_keys("\n")
self.wait_until_element_found("//input[@id='id_title']")
self.browser.find_element_by_xpath("//input[@id='id_title']").send_keys(
"Write a blog post"
)
self.browser.find_element_by_xpath("//textarea[@id='id_description']").send_keys(
"I have an idea of a blog post that I want to write."
)
self.browser.find_element_by_xpath("//button[.='Save']").send_keys("\n")
# Add new thing "Fix a bug"
self.wait_until_element_found("//a[.='Add new thing']")
self.browser.find_element_by_xpath("//a[.='Add new thing']").send_keys("\n")
self.wait_until_element_found("//input[@id='id_title']")
self.browser.find_element_by_xpath("//input[@id='id_title']").send_keys(
"Fix a bug"
)
self.browser.find_element_by_xpath("//textarea[@id='id_description']").send_keys(
"There is a critical bug that bothers our clients."
)
self.browser.find_element_by_xpath("//button[.='Save']").send_keys("\n")
# Add new thing "Binge-watch a series"
self.wait_until_element_found("//a[.='Add new thing']")
self.browser.find_element_by_xpath("//a[.='Add new thing']").send_keys("\n")
self.wait_until_element_found("//input[@id='id_title']")
self.browser.find_element_by_xpath("//input[@id='id_title']").send_keys(
"Binge-watch a series"
)
self.browser.find_element_by_xpath("//textarea[@id='id_description']").send_keys(
"There is an exciting series that I would like to watch."
)
self.browser.find_element_by_xpath("//button[.='Save']").send_keys("\n")
# Click on the button "Done"
self.wait_until_element_found("//a[.='Done']")
self.browser.find_element_by_xpath("//a[.='Done']").send_keys("\n")
These were three activities: "Write a blog post", "Fix a bug", and "Binge-watch a series" with their descriptions:
![]()
In this step, there was a list of widgets to evaluate each thing by each criterion with answers "No", "Maybe", or "Yes". The buttons for those answers had no specific id or CSS class, but I could target them by the text on the button using XPath like "//button[.='maybe']":
self.wait_until_element_found("//h2[.='Evaluations']")
self.wait_until_element_found("//button[.='maybe']")
# Evaluate all things by Urgency
self.browser.find_element_by_xpath("(//button[.='no'])[1]").send_keys("\n")
self.wait_until_element_found("//footer[.='Evaluation saved.']")
self.browser.find_element_by_xpath("(//button[.='yes'])[2]").send_keys("\n")
self.wait_until_element_found("//footer[.='Evaluation saved.']")
self.browser.find_element_by_xpath("(//button[.='no'])[3]").send_keys("\n")
self.wait_until_element_found("//footer[.='Evaluation saved.']")
# Evaluate all things by Importance
self.browser.find_element_by_xpath("(//button[.='yes'])[4]").send_keys("\n")
self.wait_until_element_found("//footer[.='Evaluation saved.']")
self.browser.find_element_by_xpath("(//button[.='yes'])[5]").send_keys("\n")
self.wait_until_element_found("//footer[.='Evaluation saved.']")
self.browser.find_element_by_xpath("(//button[.='maybe'])[6]").send_keys("\n")
self.wait_until_element_found("//footer[.='Evaluation saved.']")
# Click on the button "Done"
self.browser.find_element_by_xpath("//a[.='Done']").send_keys("\n")
![]()
These were my evaluations:
So in the last step, I got the calculated priorities:
self.wait_until_element_found("//h2[.='Priorities']")
self.wait_until_element_found("//h5[.='1. Fix a bug (100%)']")
self.wait_until_element_found("//h5[.='2. Write a blog post (50%)']")
self.wait_until_element_found("//h5[.='3. Binge-watch a series (25%)']")
self.wait_a_little()![]()
The results looked correct:
LiveServerTestCase.I hope that my journey was useful to you too.
Happy coding!
Thanks a lot to Adam Johnson for the review.
Cover photo by Science in HD.
HTML5 comes with a bunch of new types for the input fields that are rendered as rich native widgets. Browsers even restrict invalid values and validate the input immediately. Let's explore how we could make use of them in Django forms.
We will be using an Exhibition model with models.DateField, models.TimeField, and models.DateTimeField:
# exhibitions/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
class Exhibition(models.Model):
title = models.CharField(_("Title"), max_length=200)
start = models.DateField(_("Start"))
end = models.DateField(_("End"), blank=True, null=True)
opening = models.TimeField(_("Opening every day"))
closing = models.TimeField(_("Closing every day"))
vernissage = models.DateTimeField(_("Vernissage"), blank=True, null=True)
finissage = models.DateTimeField(_("Finissage"), blank=True, null=True)
class Meta:
verbose_name = _("Exhibition")
verbose_name_plural = _("Exhibitions")
def __str__(self):
return self.titleHere is a quick model form for the Exhibition model:
# exhibitions/forms.py
from django import forms
from .models import Exhibition
class ExhibitionForm(forms.ModelForm):
class Meta:
model = Exhibition
fields = "__all__"If we now open a Django shell and create an instance of the model form with some initial values, then print the form as HTML to the console, we will notice, that all date and time fields are rendered as <input type="text" /> and the values for the dates are in a local format, not the ISO standard YYYY-MM-DD:
(venv)$ python manage.py shell
>>> from exhibitions.forms import ExhibitionForm
>>> from datetime import datetime, date, time
>>> form = ExhibitionForm(initial={
... "start": date(2020, 1, 1),
... "end": date(2020, 3, 31),
... "opening": time(11, 0),
... "closing": time(20, 0),
... "vernissage": datetime(2019, 12, 27, 19, 0),
... "finissage": datetime(2020, 4, 1, 19, 0),
>>> })
>>> print(form.as_p())
<p><label for="id_title">Title:</label> <input type="text" name="title" maxlength="200" required id="id_title"></p>
<p><label for="id_start">Start:</label> <input type="text" name="start" value="01.01.2020" required id="id_start"></p>
<p><label for="id_end">End:</label> <input type="text" name="end" value="31.03.2020" id="id_end"></p>
<p><label for="id_opening">Opening every day:</label> <input type="text" name="opening" value="11:00:00" required id="id_opening"></p>
<p><label for="id_closing">Closing every day:</label> <input type="text" name="closing" value="20:00:00" required id="id_closing"></p>
<p><label for="id_vernissage">Vernissage:</label> <input type="text" name="vernissage" value="27.12.2019 19:00:00" id="id_vernissage"></p>
<p><label for="id_finissage">Finissage:</label> <input type="text" name="finissage" value="01.04.2020 19:00:00" id="id_finissage"></p>
Let's modify the model form and customize the date and time inputs. We will extend and use forms.DateInput, forms.TimeInput, and forms.DateTimeInput widgets. We want to show date inputs as <input type="date" />, time inputs as <input type="time" />, and date-time inputs as <input type="datetime-local" />. In addition, the format for the dates should be based on ISO standard.
# exhibitions/forms.py
from django import forms
from .models import Exhibition
class DateInput(forms.DateInput):
input_type = "date"
def __init__(self, **kwargs):
kwargs["format"] = "%Y-%m-%d"
super().__init__(**kwargs)
class TimeInput(forms.TimeInput):
input_type = "time"
class DateTimeInput(forms.DateTimeInput):
input_type = "datetime-local"
def __init__(self, **kwargs):
kwargs["format"] = "%Y-%m-%dT%H:%M"
super().__init__(**kwargs)
class ExhibitionForm(forms.ModelForm):
class Meta:
model = Exhibition
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["start"].widget = DateInput()
self.fields["end"].widget = DateInput()
self.fields["opening"].widget = TimeInput()
self.fields["closing"].widget = TimeInput()
self.fields["vernissage"].widget = DateTimeInput()
self.fields["vernissage"].input_formats = ["%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"]
self.fields["finissage"].widget = DateTimeInput()
self.fields["finissage"].input_formats = ["%Y-%m-%dT%H:%M", "%Y-%m-%d %H:%M"]Let's see now in the Django shell if that worked as expected:
(venv)$ python manage.py shell
>>> from exhibitions.forms import ExhibitionForm
>>> from datetime import datetime, date, time
>>> form = ExhibitionForm(initial={
... "start": date(2020, 1, 1),
... "end": date(2020, 3, 31),
... "opening": time(11, 0),
... "closing": time(20, 0),
... "vernissage": datetime(2019, 12, 27, 19, 0),
... "finissage": datetime(2020, 4, 1, 19, 0),
>>> })
>>> print(form.as_p())
<p><label for="id_title">Title:</label> <input type="text" name="title" maxlength="200" required id="id_title"></p>
<p><label for="id_start">Start:</label> <input type="date" name="start" value="2020-01-01" required id="id_start"></p>
<p><label for="id_end">End:</label> <input type="date" name="end" value="2020-03-31" id="id_end"></p>
<p><label for="id_opening">Opening every day:</label> <input type="time" name="opening" value="11:00:00" required id="id_opening"></p>
<p><label for="id_closing">Closing every day:</label> <input type="time" name="closing" value="20:00:00" required id="id_closing"></p>
<p><label for="id_vernissage">Vernissage:</label> <input type="datetime-local" name="vernissage" value="2019-12-27T19:00" id="id_vernissage"></p>
<p><label for="id_finissage">Finissage:</label> <input type="datetime-local" name="finissage" value="2020-04-01T19:00" id="id_finissage"></p>
The same way you can also create widgets for other HTML5 input types: color, email, month, number, range, tel, url, week, and alike.
Happy coding!
Cover photo by Eric Rothermel.