Catégorie : développement

Tout ce qui concerne le développement en général, que ce soit des choses générales, ou des choses bien précises. Cela va de la gestion de projet à la recherche du fonctionnement de pointeurs en C.

Django : favicon.ico : comment le gérer facilement

En mode débug, un serveur Django renvoie tous les fichiers, y compris les fichiers statique. Seul problème : tous les fichiers statique on leur URL qui commence par /static/.

Mais on rencontre un problème : le favicon.ico n’est pas un fichier statique « normal » = qui commence par /static/. C’est un nom « en dur », qui ressemble à http://monsite/favicon.ico.

Il vous faut donc la coder en dur dans les URLs : ainsi : url(r'^favicon.ico/$',...)
Et pour renvoyer directement le contenu, il suffit de faire une fonction immédiate (= lambda) qui renvoie directement le contenu :
urlpatterns = [
    # ...blabla... all path and now:
    url(r'^public/(?P.*)$', static.serve, {
        'document_root': settings.MEDIA_ROOT
    }, name='url_public'),
    url(r'^favicon.ico/$', # google chrome favicon fix :
        lambda x: HttpResponseRedirect(settings.STATIC_URL+'favicon.ico')),

]

Django : comment passer une constante à tous vos templates ?

Django, nativement, a la possibilité de passer des dictionnaires clé-valeurs à tous les templates, et dans les templates on accède aux valeurs via la clé.

Exemple concret : je voulais passer le nom de mon site à tous les templates, mais sous forme de constante.

Dans settings.py j’ai défini ma constante : WEBSITE_NAME = 'mywebsite'

Il suffit de créer une fonction qui renvoie un dictionnaire avec une clé nommée « correctement », par exemple :

def context_processor_website_name(request):
    return {'website_name': WEBSITE_NAME}

Et ensuite, dans les context_processors, juste ajouter le nom de la fonction par rapport au package où elle est.
J’ai mis cette fonction dans settings.py.
Je ne sais pas si c’est la meilleure place mais l’idée c’est que comme c’est en rapport avec la configuration de mon site, c’est le meilleur endroit…

Donc pour préciser où est la fonction c’est 'myproject.settings.context_processor_website_name' :

On retrouve donc le code final comme ceci :

TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [os.path.join(BASE_DIR, 'templates')],
    'APP_DIRS': True,
    'OPTIONS': {
        'context_processors': [
            'django.template.context_processors.debug',
            # ... and:
            'myproject.settings.context_processor_website_name'
    ],
    },
}, ]

Et voilà, à partir de maintenant, avec exactement 3 lignes de code, j’ai accès à ma variable website_name, que je peux utiliser ainsi :

<title>{% block title %}{% if title %}{{ title|safe }} - {% endif %}{{ website_name }}{% endblock %}</title>

Petite parenthèse : grâce à ce code, si la page qu’on affiche a un titre, elle aura pour titre : [titre] - [nom du site] et si elle n’a aucun titre, elle affichera : [nom du site]

Django : un modèle sur mesure : DaysField code source complet

Code source complet de DaysOfWeek

Après avoir écrit en trois parties (1, 2 et 3) comment faire un modèle sur mesure, voici le code source au complet.

from django.core.exceptions import ValidationError
from django import forms
from django.db import models
from django.utils.translation import ugettext_lazy as _
class DaysOfWeek:
    DAYS = {1: _("Monday"),
            2: _("Tuesday"),
            3: _("Wednesday"),
            4: _("Thursday"),
            5: _("Friday"),
            6: _("Saturday"),
            7: _("Sunday"), }
    DAYS_SHORT = {1: _("Mo"),
                  2: _("Tu"),
                  3: _("We"),
                  4: _("Th"),
                  5: _("Fr"),
                  6: _("Sa"),
                  7: _("Su"), }
    CHOICES = [(idx, value) for idx, value in DAYS.items()]
    @staticmethod
    def summary_from_list(tab, empty=' '):
        if tab is None:
            return '-'.join([empty for a in range(len(DaysOfWeek.DAYS))])
        return '-'.join([str(DaysOfWeek.DAYS_SHORT.get(i, empty)) for i in tab])
class DaysFormField(forms.TypedMultipleChoiceField):
    # different widget, comment to change interface:
    widget = forms.CheckboxSelectMultiple
    def __init__(self, *args, **kwargs):
        if 'max_length' in kwargs:
            kwargs.pop('max_length')
        kwargs['choices'] = DaysOfWeek.CHOICES
        super().__init__(*args, **kwargs)
class DaysField(models.CharField):
    description = _("Comma-separated integers between 1 and 7")
    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 13 # max. len = all days = "1,2,3,4,5,6,7" = 13
        super().__init__(*args, **kwargs)
    @staticmethod
    def value_to_array(value):
        if value is None:
            return None
        try:
            if isinstance(value, list):
                return [int(a) for a in value]
            elif isinstance(value, str):
                return [int(a) for a in value.split(',')]
        except (TypeError, ValueError):
            raise ValidationError(_("Unexpected value"))
        raise ValidationError(_("Unexpected value"))
    @staticmethod
    def from_db_value(value, expression, connection):
        return DaysField.value_to_array(value)
    def to_python(self, value):
        return DaysField.value_to_array(value)
    def get_prep_value(self, value):
        return ','.join([str(a) for a in value]) if value is not None else None
    def formfield(self, **kwargs):
        # ignore the admin directives: directly override with our custom form:
        return super().formfield(form_class=DaysFormField,
                                 initial=[])
    # region - Validator -
    class ListBetween1And7Validator:
        def __call__(self, value):
            try:
                if isinstance(value, list):
                    loop = [int(a) for a in value]
                elif isinstance(value, str):
                    loop = [int(a) for a in value.strip('[]').split(',')]
                else:
                    raise ValueError()
                for v in loop:
                    if not (7 >= v >= 1):
                        raise ValueError()
            except (TypeError, ValueError):
                raise ValidationError(
                    _("Enter a list if coma-separated values between 1 and 7."),
                    code='invalid')
    # endregion - Validator -
    default_validators = [ListBetween1And7Validator()]

Django : un modèle sur mesure : DaysField étape 3/3

Formulaire spécifique pour un modèle sur-mesure

Après les conversions de notre modèle dans les deux sens :

Il ne nous reste qu’à faire le validateur = s’assurer que ce qui arrive (peu importe la source : base de données ou code Python) :

  • est bien un tableau :
  • que ce tableau n’est rempli que d’entiers ;
  • que chacun de ces entiers est compris entre 1 et 7.

Il faut créer une classe, qui doit implémenter __call__. Vous ne savez pas ce que c’est ? L’explication sur SO ici est excellente.
En une phrase : __init__() est appelé lorsque la classe est instanciée, __call__ est appelé lorsque l’instance est appelée (avec des ())

class ListBetween1And7Validator:
    def __call__(self, value):
        try:
            if isinstance(value, list):
                loop = [int(a) for a in value]
            elif isinstance(value, str):
                loop = [int(a) for a in value.strip('[]').split(',')]
            else:
                raise ValueError()
            for v in loop:
                if not (7 >= v >= 1):
                    raise ValueError()
        except (TypeError, ValueError):
            raise ValidationError(
                _("Enter a list if coma-separated values between 1 and 7."),
                  code='invalid')

Puis il faut le déclarer dans la liste des validateurs de notre classe :

    default_validators = [ListBetween1And7Validator()]

Dans le prochain et dernier article, je mets le code source au complet.

Django : un modèle sur mesure : DaysField étape 2/3

Formulaire spécifique pour un modèle sur-mesure

Après les conversions (base / et / ou / Python) vers données Python (dans les deux sens), il nous faut écrire notre formulaire spécifique pour notre modèle DaysField sur-mesure.

Ici, l’utilisateur doit pouvoir cocher / ou pas / des valeurs qui correspondent aux jours de la semaine.

Au jour de l’écriture de ce document, la documentation n’est pas complète.
Officiellement, il « suffirait » de surcharger def formfield(self, **kwargs) en y ajoutant sa propre classe de formulaire via l’index « form_class » de kwargs.

L’exemple proposé ne fonctionne pas !

En effet, lorsqu’on affiche le modèle dans l’interface d’administration, kwargs est déjà pré-rempli avec l’indice « widget » qui est le composant spécifique pour l’administration. Pour que leur exemple fonctionne, il faudrait supprimer « widget » avant d’y ajouter « form_class ».

J’ai opté pour la méthode directe qui ignore « kwargs » :
    def formfield(self, **kwargs):
        # ignore the admin directives: directly override with our custom form:
        return super().formfield(form_class=DaysFormField,
                                 initial=[])

Enfin, la classe DaysFormField elle-même :
class DaysFormField(forms.TypedMultipleChoiceField):
    def __init__(self, *args, **kwargs):
        if 'max_length' in kwargs:
            kwargs.pop('max_length')
        kwargs['choices'] = DaysOfWeek.CHOICES
        super().__init__(*args, **kwargs)

Cela affiche maintenant un choix ainsi :
Custom field DaysField choice 1.jpg

On peut facilement le modifier en y précisant un autre widget, par exemple :
class DaysFormField(forms.TypedMultipleChoiceField):
    widget = forms.CheckboxSelectMultiple
    # (le reste du code avec __init_())

Cela affiche maintenant un choix ainsi :
Custom field DaysField choice 2.jpg

Django : un modèle sur mesure : DaysField étape 1/3

Le principe

Je voulais un modèle de données qui stocke les jours d’une semaine que l’utilisateur désire, et qui les stocke sous forme de chaîne simple, avec des numéros qui correspondent aux jours de la semaine.
J’ai appelé ce modèle DaysField.
Les valeurs sont stockées par rapport aux jours choisis. Par exemple, 1,3 signifie que l’utilisateur a choisi lundi et mercredi parmi les jours. 6,7 correspondent à samedi et dimanche. Pour finir, tous les jours de la semaine cochés serait donc : 1,2,3,4,5,6,7

Toujours descendre d’un modèle classique

J’ai retenu ces principes et cela semble suffire pour fonctionner : on descend du modèle qu’on veut écrire en base de données. Ici, je voulais écrire une chaîne de caractères dans la base ⇒ on descend de CharField(), en modifiant la description au passage :
class DaysField(models.CharField):
    description = _("Comma-separated integers between 1 and 7")

Conversion de « sources » différentes en données Python

Pour résumer : les modèles ont deux « arrivées » de données :

  • quand on les données viennent de la base = from_db_value() ;
  • quand on les données viennent d’un formulaire (ou autre, mais en gros quand ça vient pas de la base de données mais du code Python lui-même) = to_python().

Il faut donc gérer ces deux cas, qui, dans les deux cas, doivent renvoyer quelque chose au format Python qui nous intéresse.
Ici, c’est un tableau d’entiers compris entre 1 et 7 qui correspond aux jours de la semaine.

J’ai centralisé la gestion des deux « arrivées » (base et Python) dans une seule fonction : value_to_array() :

    @staticmethod
    def value_to_array(value):
        if value is None:
            return None
        try:
            if isinstance(value, list):
                return [int(a) for a in value]
            elif isinstance(value, str):
                return [int(a) for a in value.split(',')]
        except (TypeError, ValueError):
            raise ValidationError(_("Unexpected value"))
        raise ValidationError(_("Unexpected value"))
    @staticmethod
    def from_db_value(value, expression, connection):
        return DaysField.value_to_array(value)
    def to_python(self, value):
        return DaysField.value_to_array(value)

Peut-être existe-t-il des cas où la provenance du code Python doit être gérée différemment de la provenance de la base de données… ici ce n’est pas le cas – et si vous avez des exemples de gestion différentes pour des entrées de base et de code, laissez-moi un commentaire, car je n’en vois pas…

Conversion de données Python ⇒ données pour la base

C’est la fonction get_prep_value().

Dans le cas DaysField, c’est très simple :
    def get_prep_value(self, value):
        return ','.join([str(a) for a in value]) \
            if value is not None else None

On prend toutes les valeurs du tableau, on les concatène avec la virgule , comme séparateur. C’est plus long de l’expliquer que de le coder !

Gitlab Continuous Integration

gitlab CI

Gitlab CI (Continuous Integration)

Layer A set of read-only files or commands that describe how to set up the underlying system beneath the container. Layers are built on top of each other, and each one represents a change to the filesystem.
Image An immutable layer that forms the base of the container.
Container An instance of the image that can be executed as an independent application. The container has a mutable layer that lies on top of the image and that is separate from the underlying layers.
Registry A storage and content delivery system used for distributing Docker images.
Repository A collection of related Docker images, often different versions of the same application.

Docker memo

Docker memo

docker images List all images present on host.
docker rmi To remove an image you no longer plan to use.
docker run [image] Start a container of the image [image]. If the image is not present on the host, it will go out to docker hub and pull the image down, it's only done the first time. For the subsequent executions, the same image will be re-used.
docker run [image] -d Start a container of the image docker run [image] in detached mode = in the background.
docker run -p [src]:[dst] [image] -d Like rStart a container of the image docker run [image] in detached mode = in the background.
docker pull [image] Like docker run but only pull the [image], not run it.
docker exec [container name] [command] Runs the command [command] on a running container.
docker ps List all running containers with informations such as container id, the image used for it.
docker ps -a To see all containers running or not.
docker ps A collection of related Docker images, often different versions of the same application.
docker stop [name] To stop a running container.
docker rm [name] rm = remove container. To remove a stopped or exited container permanently = stop using space.
docker rmi rmi = "remove images". To remove an image you no longer plan to use.
docker rmi To remove an image you no longer plan to use.

Django rest framework : Got AttributeError when attempting to get a value for field `my_field` on serializer `UserSerializer`.

Si vous voulez faire un mapping avec un modèle c’est ultra simple. Si vous voulez ajouter un champ custom, là ça devient compliqué…

Aucune réponse viable sur stackoverflow, vous risquez de chercher longtemps.

Si vous avez fait un code comme cela, pour rajouter un champ « custom » :

class UserSerializer(serializers.ModelSerializer):
    my_field = serializers.CharField(allow_blank=True)
    class Meta:
        model = User
        fields = ['url', 'email', 'password', 'my_field']

Eh bien cela ne marchera jamais, avec une erreur qui ne vous met absolument pas sur la voie :

Got AttributeError when attempting to get a value for field `my_field` on serializer `UserSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `User` instance.
Original exception text was: 'User' object has no attribute 'my_field'.

Voici la solution : autoriser le null :

class UserSerializer(serializers.ModelSerializer):
    my_field = serializers.CharField(allow_blank=True, allow_null=True)
    class Meta:
        model = User
        fields = ['url', 'email', 'password', 'my_field']

git : exemple de fichier

Voici un exemple de fichier de configuration qui ignore la plupart des fichiers qui posent problème :

# --------------------------------------
# git cleanup if too many files / or / problems:
# git reflog expire --expire=now --all && git repack -ad && git prune
# --------------------------------------
# PyCharm
.idea/
# --------------------------------------
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# --------------------------------------
# C extensions
*.so
# Distribution / packaging
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# --------------------------------------
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# --------------------------------------
# Unit test / coverage reports
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# --------------------------------------
# Translations
*.mo
# --------------------------------------
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# --------------------------------------
# Rope
.ropeproject
# --------------------------------------
# Django stuff:
*.log
*.pot
# --------------------------------------
# Sphinx documentation
docs/_build/
# --------------------------------------
# git
objects/
# --------------------------------------
# swap files
*.swp
*.swo

Bootstrap : classes utilitaires

Vous allez peut-être trouver du code qui ressemble à cela :
<p class="mb-0">Texte dans un paragraphe</p>

Bootstrap >= 4 a mis en place plein de possibilités pour faire du padding et des marges « responsives ».
Elles fonctionnent pour tous les breakpoints :

  • xs (<=576px)
  • sm (>=576px)
  • md (>=768px)
  • lg (>=992px)
  • xl (>=1200px)

<p class="{propriété}{côtés}-{taille}">texte</p>

propriété :

  • m – les marges
  • p – le padding

côtés :

  • tmargin-top or padding-top
  • bmargin-bottom or padding-bottom
  • lmargin-left or padding-left
  • rmargin-right or padding-right
  • x*-left and *-right
  • y*-top and *-bottom
  • blank – les 4 côtés à la fois

taille :

  • 0 – supprimer les marges ou mettre le padding à 0
  • 1 – $spacer *0.25
  • 2 – $spacer *0.5
  • 3 – $spacer
  • 4 – $spacer *1.5
  • 5 – $spacer *3
  • auto – margin à auto