Linux prompt how-to customisé + explications

Je vais faire un résumé de ce que j’ai trouvé un peu partout, et pourquoi j’ai choisi mon prompt tel que je l’ai fait.

  1. Visuellement, avoir quelque chose de coloré est extrêmement parlant. Il me fallait absolument utiliser les couleurs possibles du prompt pour rapidement mettre en évidence ce qui m’intéresse ;
  2. Le bash a sa syntaxe qui est très efficace mais pas forcément facile à appréhender ;
  3. Comme pour tout ce qu’on cherche sur Internet en termes de développement, il y a à boire et à manger… et cela semble devenir vrai aussi pour Linux. J’espère vraiment (x 450e+6654) me tromper.

Ce que j’ai trouvé et qui fonctionne selon mes tests. Ici, bien sûr, peut être qu’il y a d’autres solutions, mais celle que je donne ici a fonctionné sur tous les shells que j’ai testé (Ubuntu, Mint mais aussi CentOS).

Séquence invisible

Toute séquence qu’on ne veut pas visible doit être entourée par \[ et \].
Fin de l’histoire. Je vais pas pleurer en disant que j’ai passé un sacré paquet de temps à trouver ça alors que c’est très simple. Ah si je pleure ? Bon ok.

Changer de couleur

Pour changer de couleur, il faut imaginer qu’on change simplement de stylo. Simple, me direz-vous ? Mais après avoir lu et travaillé dans le Web où on ouvre puis on ferme une balise, ici, non. Il faut envoyer une séquence spéciale : \033 puis ouvrir un crochet qu’on ne va pas fermer, c’est codé en dur, il faut l’accepter tel quel : [. Et ensuite, un chiffre : 0 pour dire qu’on veut une couleur foncée, ou 1 pour dire qu’on la veut un peu plus claire (ex : le noir devient gris) puis un point virgule ;. Ensuite, il faut donner un chiffre compris entre 30 et 36 pour préciser la couleur. Et vous croyez que c’est terminé ? Non ! Il faut terminer par la lettre m. Si vous avez bien lu, vous devriez instantanément comprendre l’exemple suivant : \[\033[1;31m\]. Oui oui, le crochet au milieu ne se ferme jamais. Respirez, calmez-vous. C’est la vie. Il faut faire avec.

Pour toutes les couleurs, j’ai tout mis dans mon mémo qui remplace ma mémoire trop remplie (je préfère dire « trop remplie » que « défaillante », mais parfois j’hésite entre les deux…).

Restorer la couleur d’origine

Oui c’est en h1 parce que c’est aussi important que les deux paragraphes précédents : pour restaurer la couleur d’origine, cela ressemble un peu à l’ordre précédent : entourer par des crochets échappés, une séquence de couleur qui n’a pas deux valeurs mais une seule : 0. Cela donne : \[\033[0m\].
Donc votre code qui change votre prompt devrait toujours se terminer par « remettre la couleur d’origine », soit \[\033[0m\]. Ce n’est que mon point de vue…

Mon prompt

Vous allez me dire « mais pourquoi il fait tout un article sur un prompt ? ». Réponse : c’est comme mon clavier : j’ai passé beaucoup de temps à l’apprendre et le configurer, parce que je sais que sur du long terme ça va m’être très rentable. Passer plusieurs semaines pour gagner plusieurs mois, ça vaut le coup, fin de l’histoire. Donc, voici ma réflexion qui explique mon choix de prompt. J’aurais tout autant pu vous répondre un grand classique indétrônable : le diable est dans les détails.

  1. J’ai très souvent besoin de savoir globalement l’heure qu’il est. J’aimerais bien, dans mon prompt, je sache l’heure qu’il est. Si je tape quelque chose, je valide, hop j’ai la nouvelle heure. Pratique, pas « mortellement pratique », mais sympa. De plus, 6 caractères utilisés pour HH:MM[espace] c’est négligeable. Par contre, j’aimerais que visuellement, ça ne soit pas polluant. C’est pratique, mais pas important. La couleur la plus sombre possible dans le prompt c’est « noir éclairci », soit premier chiffre = éclairci = 1 et second chiffre = noir = 30. Donc mon premier ordre c’est « noir éclairci + heure », donc : \[\033[1;30m\]\t 
  2. La seconde chose, qu’on retrouve partout, c’est bien sûr, qui est connecté, donc l’utilisateur. Là, ça devient plus important. Mais pas le plus important pour moi (surtout sur la fin du prompt où on voit en méga surbrillance, si on est superuser ou utilisateur normal) : c’est une couleur foncée 0 et qui peut avoir un pendant « éclairci » plus agréable pour préciser l’hôte (j’en parle juste après) : j’ai choisi purple = 35. La séquence échappée du user en cours est \u. Ici aussi, si vous avez suivi, ça donne, couleur + user en cours : \[\033[0;35m\]\u
  3. Ensuite, séparateur entre l’utilisateur et l’hôte. Le classique « @ » qui ne doit pas être gênant, même couleur que l’heure : « noir éclairci », soit \[\033[1;30m\]@
  4. L’hôte. Oui, le grand classique c’est «[userconnecté]@[hôte]», et si des millions de geeks linux ont choisi cela c’est que c’est parlant. Mais ici, comme je gère pas mal de machines, selon moi, il faudrait que l’hôte soit un peu plus visible que le user en cours. Donc même couleur que le user, mais avec «1» au lieu de «0». Sachant que la séquence échappée de l’hôte est \h on obtient : \[\033[1;35m\]\h
  5. Avant dernière chose (pour moi) : le path. Là, bon sang, il faut que ça pète. Il faut que le yeux le voient même si on commence à s’endormir (petit clin d’œil à ceux qui mangent kebab-frites avant de commencer les cours avec moi). Un bon rouge, bien éclairci. Allez on le fait rapide : pour exécuter un ordre à chaque appel de prompt, l’astuce est d’échapper le $ et de mettre entre parenthèses l’ordre shell qu’on veut lancer : ici, on est sur du Linux, c’est instantané. La solution, donc : rouge vif + $(pwd) version « échappée » :
    \[\033[1;31m\]\$(pwd)
  6. Dernière information : la plus cruciale, toujours présente sur tous les paths : quel type d’utilisateur est connecté : \$. Il est substitué par # si vous êtes root, sinon $. Là, il faut qu’on ait les yeux qui brûlent. Lunettes de soleil. Du jaune, le plus visible possible (= « 1« ). Un bon jaune qui fait pleurer les yeux, mais ça ne prend qu’un caractère, qui est IMHO, d’une importance cruciale :
    \[\033[1;33m\]\$

Voici donc mon bash résumé en une ligne, que je mets dans tous mes .bashrc. Si vous ne savez pas ce qu’est un .bashrc, je vous laisse chercher, ce n’est pas le but de l’article.

Voici ce que donne mon prompt, et même si ici il n’est pas super vendeur, en pratique, je n’ai pas trouvé quelque chose de plus efficace visuellement parlant, si vous avez des idées / suggestions, améliorations, commme d’habitude, je suis preneur :

Mon prompt Linux shell

Le code de mon prompt, volontairement en minuscules juste pour vous faire râler :

export PS1="\[\033[1;30m\]\t \[\033[0;35m\]\u\[\033[1;30m\]@\[\033[1;35m\]\h \[\033[1;31m\]\$(pwd) \[\033[1;33m\]\$\[\033[0m\] "

PS : pour ceux qui ne savent pas ce que « IMHO » signifie, ne le dites à personne, et allez vite chercher sur Internet. Promis on ne le dira à personne. C’est comme « RTFM ». Promis. Chut. [silence gêné].

Django 2.1 : les autorisations de l’administration grandement facilitées

Voici le (petit) souci que tout le monde a forcément dès qu’on commence à vouloir un peu pousser la customization de l’administration Django : on aimerait avoir la possibilité d’accéder à certains modèles, mais uniquement en lecture seule. J’ai des tonnes d’exemples simples, mais très souvent, ce sont des modèles avec des choix « fixes » qui ne risquent pas d’évoluer dans le temps (exemple : sexe « féminin »/ »masculin »). Seulement (1) on ne veut pas le voir apparaître dans l’admin (2) si on ne veut pas le voir apparaître, il ne faut pas le déclarer, et Django ne veut pas accéder à un modèle qui n’est pas déclaré (= erreur), donc on en arrive toujours à (3) on déclare le modèle, on applique une solution « bidouille » que je ne décris pas ici mais même si le modèle n’apparaît pas dans l’interface, en tapant à la main la bonne URL (facile à deviner qui plus est), on peut tout de même modifier ce modèle. Heureusement la version 2.1 de Django pallie à ce manque !

Eh oui, dans la classe interne «Meta», ils ont fait évoluer default_permissions, qui est écrit en base. Donc en passant à Django 2.1 il vous faudra faire le fameux makemigrations migrate.

Là ou cela devient intéressant, c’est que dans les options vous avez default_permissions et elles sont ('add', 'change', 'delete', 'view'). Le plus important et le nouveau est donc 'view' qui pallie parfaitement au problème expliqué en haut !

Il vous suffit de déclarer votre modèle, de faire la classe dérivée de ModelAdmin et de préciser uniquement ('view',).
Attention, je n’ai pas testé, si quelqu’un veut tester et confirmer que ce code fonctionne (ou pas), je suis preneur !

class SexeAdmin(models.ModelAdmin):
    class Meta:
        default_permissions = ('view',)

Plein de code en moins, plus aucune bidouille ici, plus de lisibilité, bref, de mieux en mieux !

Django Async Roadmap

Petite parenthèse sur l’évolution de Django. Aujourd’hui, ce qui commence à importer plus que la vitesse, c’est la parallélisation des choses.
Donc avoir un serveur Web asynchrone, c’est censé être mieux qu’un serveur Web synchrone (= bloquant).

  • Django 2.1 : rien d’asynchrone. Aucun travail dessus.
  • Django 2.2 : travail initial pour ajouter la possibilité de faire de l’asynchrone sur l’ORM et les vues, sachant que c’est une option et que tout sera synchrone par défaut. Le support async sera principalement basé sur des pools de threads.
  • Django 3.0 : Ré-écriture de la gestion interne des requêtes pour être 100% asynchrone, ajout de la gestion asynchrone des middleware, forms, cache, sessions, auth. Début du process de dépréciation pour toutes les APIs prévues pour être uniquement asynchrones.
  • Django 3.1 : amélioration du support async, peut-être changement du templating
  • Django 3.2 : Fin du process de dépréciation et Django sera presque entièrement asynchrone.

Django et git (git « pur », pas « github »)

Si jamais vous décidez d’être un vrai guerrier, un de ceux qui veulent tout maîtriser, un de ceux qui, quand on leur donne des ordres, peuvent les refuser (oui c’est rare), et si vous voulez prendre des responsabilités, bref, si vous en avez : faites vous votre propre serveur git !

Je ne décrirai pas comment installer un serveur git sous Linux tellement c’est simple. Et je ne décrirai pas non plus comment faire ça sous Windows : pour des raisons de sécurité, de fiabilité et d’éthique, évitez Windows un point c’est tout.

Donc, sous Linux :

  • Créez un dossier dans lequel vous mettez toutes vos sources (scp est votre ami si vous êtes sur un PC et que vous voulez faire cela sur un autre PC)
  • Supposons que l’on est dans /web/htdocs/interro/htdocs
  • Créez un fichier .gitignore
  • Dans ce fichier, mettez ceci :
    *.pyc
    __pycache__
    myvenv
    db.sqlite3
    /static
    .DS_Store

    Cela signifie « ignore tous ces fichiers (.DS_Store est pour les personnes sous Mac (non je ne vous en veux pas, je suppose que vous ne connaissez pas bien l’histoire de l’informatique pour avoir acheté Apple)) » pour toutes les personnes qui vont cloner ce dossier
  • Ensuite, ajoutez tous les fichiers ainsi : git add --all .
  • Et commitez (oui je sais ce n’est pas français) ou « appliquez » les modifications c’est à dire l’ajout de tous les fichiers ainsi : git commit -m "My application, first commit"
  • Vous devriez avoir un paquet de fichiers ajoutés, vérifiez que tout est bien enregistré en demandant l’état via git status. Vous devez avoir un messsage ressemblant fortement à
    nothing to commit (working directory clean)
  • Vous êtes prêts ! Vous avez crée un repository (j’ai la flemme de traduire en français), et vous y avez appliqué tous vos fichiers. Il ne vous reste plus qu’à le cloner à distance
  • (Update) : par défaut, le serveur travaille sur la branche d’origine (« master »). Seul problème : si vous essayez de faire un « push » à partir de votre PC de développement, git ne sera pas d’accord pour faire un « push » sur la branche « master ». Je pense que c’est pour des raisons de sécurité ou autre, mais c’est plutôt récent, avant cela fonctionnait. La solution : dites sur le serveur de travailler sur une autre branche c’est via l’ordre checkout. Notez bien : c’est sur le serveur. Je ne trouve pas ça très cohérent, j’aurais dit que c’était au client de dire sur quelle branche il veut « push », mais je dois manquer quelque chose… j’ai vraiment beaucoup de lacunes git. Bref. Exemple : git checkout -b development.
  • Ensuite il vous suffira de faire un git clone (dans PyCharm, c’est le menu VCS » git » clone)

Bien sûr, comme tous les gestionnaires de version délocalisés, il vous faudra toujours trois étapes pour valider vos modifications :

  1. Appliquer les modifications en local (git commit -m "mon message expliquant les modifs")
  2. Essayer de récupérer toutes les dernières modifications du repository (git pull) vers votre code « local », au cas où d’autres personnes auraient « pushé » des modifications
  3. Envoyer vos modifications sur le serveur pour qu’elles soient accessibles à tout le monde (git push)

Django : autorisations sur mesure – custom authorizations

Voici comment créer manuellement des groupes + des autorisations personnalisées :

from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
new_group, created = Group.objects.get_or_create(
    name ='co_branding_trainer')
ct = ContentType.objects.get_for_model(User)
permission_co_branding_trainer = Permission.objects.create(
    codename='co_branding_trainer',
    name='Co-branding trainer', content_type=ct)
new_group.permissions.add(permission_co_branding_trainer)

Une fois cela fait, on a deux possibilités :

  • aller dans le template et se servir de
    {% if perms.app_label.can_do_something %}
    {% endif %}
  • ou modifier le code vue dans le fichier Python. Dans ce cas, on n’a rien dans le template et si la personne connectée n’a pas les droits, la vue fait un forbidden

Comme je préfère la seconde (qui évite d’avoir dans le template des « if » un peu partout), voici comment faire : on crée un mixin qui se chargera de vérifier si, dans la vue, on a les droits : il suffit de chercher PermissionRequiredMixin dans le code de Django, et vous verrez que tout est simple, à partir de là vous pourrez faire descendre vos vues génériques de PermissionRequiredMixin.

Django Channels : astuces

Vous êtes sous Windows ?
Vous avez Python36 ?
Vous voulez faire tourner Django Channels ?
Installez Python 3.5 dans C:\Python35, puis à partir de là il faut tout faire sous Python 3.5.
Vous verrez que lorsque vous aurez tout installé, tout configuré, eh bien… daphne (le serveur Web asynchrone pour Django Channels) ne fonctionnera pas.
En fait, il vous faut modifier le path Windows pour être sûr qu’il pointe d’abord sur Python35 puis sur Python35/Scripts

Sous PyCharm, j’ai (enfin !) trouvé l’astuce : allez dans :
Project » Settings » Tools » Terminal

De là, modifiez Shell path par :
cmd.exe "/k set PATH=C:\Python35;C:\Python35\Scripts;%PATH%"
ou
"cmd.exe" /k ""set PATH=C:\Python35;C:\Python35\Scripts;%PATH%""
(une des deux solutions va fonctionner)

Et vous aurez accès à daphne en ligne de commande !

Django Channels sous Windows : attention !

Bah oui !

Sous Linux, Django Channels est compatible Python 3.5 et plus : c’est vrai.
Pof, comme d’habitude, sudo apt-get install blabla, pip3 install blabla et tout fonctionne.
Sous Windows, ce n’est pas le cas (cela changera sûrement dans les semaines à venir et cet article va rapidement ne plus être vrai), mais pour l’instant, Django Channels ne fonctionne pas sous Windows avec Python 3.6, mais uniquement avec Python 3.5.

De plus, vous devrez télécharger des Gigas et des Gigas de données Microsoft pour avoir le compilateur C++ 14 (si ma mémoire est bonne).

Parce que mon ami Microsoft © a retiré les liens (et si vous les avez je suis preneur) du compilateur en ligne C++, et vous devez télécharger Visual Studio et toutes ses immondices qui vont autour, « téléchargez la version pro », « payez un abonnement », ou le pire : « vous devez être enregistré pour pouvoir lancer Visual Studio » (ce qui, à mon sens, est totalement inadmissible et fait partie des wagons (trains entiers ?) de choses qui font que je prône Linux).

2,8 Go chez moi pour Visual Studio Community ainsi que 5,3 Go pour Microsoft SDKs.

Bref, sous Windows :

  • Installez Python 3.5 (toujour à la racine « C:\Python35« ) pour éviter les problèmes de droits quand il est dans « Program Files« 
  • Installez via la ligne de commande classique « channels » (sans passer par PyCharm) : python.exe -m pip install -U channels

Windows : le shell Linux / Ubuntu enfin acceptable !

Merci à l’un de mes étudiants pour l’astuce :

Sous Windows 10 :

  • Activez le mode développeur ;
  • Cochez l’option « Linux » (il faut la chercher) ;
  • Tentez de lancer un shell via le menu « Démarrer » et tapez « Ubuntu ».

Vous aurez un Shell Linux, mais totalement pourri.

Faites néanmoins le classique « sudo apt-get update && sudo apt-get upgrade« .

Une fois cela terminé, allez récupérer ce qui va vous changer la vie : wsltty.
C’est un outil qui remplace le shell Ubuntu de Windows, mais qui est mieux sur tous les plans.
Et là vous aurez un shell qui ressemble au mintty de Cygwin (mon shell préféré).

JavaScript hacks

/*--------------------------------------*/
var loadjQuery = function(cb){
    var scr = document.createElement('script');
    scr.setAttribute('type', 'text/javascript');
    scr.setAttribute('src', 'https://code.jquery.com/jquery-3.3.1.min.js');
    if(scr.readyState){
       scr.onreadystatechange = function(){
           if(scr.readyState === 'complete' || scr.readyState === 'loaded'){
              scr.onreadystatechange = null;
              if(cb === 'function'){
                 args = [].slice.call(arguments, 1);
                 cb.apply(this, args);
              }
           }
       };
    } else {
        scr.onload = function(){
           if(typeof(cb) === 'function'){
              args = [].slice.call(arguments, 1);
              cb.apply(this, args);
           }
        };
    }

    var head = document.getElementsByTagName('head')[0];
    head.insertBefore(scr, head.firstChild);
}

loadjQuery(function() {
    $('body').empty();
    let getRandomInt = function (max) {
        return Math.floor(Math.random() * Math.floor(max));
    };
    let curr=getRandomInt(5000);
    let addNewAvatar=function() {
        $('body').append(
            $('<img />').attr(
                'src', 'https://avatars.githubusercontent.com/u/'+curr
            ).css({
                'max-width': '50px',
                'display': 'inline-block',
                'float': 'left',
                'margin': '0',
                'padding': '0'
            })
        );
        curr += (1+getRandomInt(3));
        setTimeout(addNewAvatar, 100);
    };
    setTimeout(addNewAvatar, 100);
});

/*--------------------------------------*/
var loadjQuery = function(cb){
    var scr = document.createElement('script');
    scr.setAttribute('type', 'text/javascript');
    scr.setAttribute('src', 'https://code.jquery.com/jquery-3.3.1.min.js');
    if(scr.readyState){
       scr.onreadystatechange = function(){
           if(scr.readyState === 'complete' || scr.readyState === 'loaded'){
              scr.onreadystatechange = null;
              if(cb === 'function'){
                 args = [].slice.call(arguments, 1);
                 cb.apply(this, args);
              }
           }
       };
    } else {
        scr.onload = function(){
           if(typeof(cb) === 'function'){
              args = [].slice.call(arguments, 1);
              cb.apply(this, args);
           }
        };
    }
    var head = document.getElementsByTagName('head')[0];
    head.insertBefore(scr, head.firstChild);
}

loadjQuery(function() {
    $('body').empty();
    $.ajax({
        type: 'get',
        url: 'https://api.magicthegathering.io/v1/cards?page=311'
    }).done(function(data) {
        for (var i=0; i<data.cards.length; i++) {
            $('body').append(
                $('<img />').attr('src', data.cards[i].imageUrl)
            );
        }
    });
});

IUT : jQuery à rendre

A rendre pour le 18 mars au plus tard


Faites un fichier README.txt et déposez-le ici


Sujet

Ce que vous voulez tant que c’est dans le cadre de ce que l’on a vu. Vous avez tout le Web comme inspiration !
N’oubliez pas de me donner le nom et le mot de passe pour se connecter !

Voici les fonctionnalités obligatoires :

  • Connexion + déconnexion (vu en cours)
  • Effets jQuery sur les éléments
  • Appels JSON : au moins deux appels en plus de ceux vus en cours

Sujets possibles

  1. Site de partage de photos
  2. Site de cocktails (cf ci-dessus)
  3. e-rated : site d’appréciations (selon des sujets, à définir)
  4. Ask-a-question : site où l’on pose des questions sur des sujets divers, et des gens répondent
  5. Write-a-book-together : site où l’on se connecte et où on peut écrire un livre à plusieurs
  6. A-maze-ing : site où l’on peut se ballader dans un labyrinthe et essayer d’en trouver la sortie
  7. Wedding-couple-site : site où l’on uploade + partage des photos de mariage + livre de commandes
  8. Playing-cards-collection : site où on scanne + échange des cartes (Magic the gathering)
  9. Polls-and-surveys : site de création de sondages (= QCM)
  10. Poems-generator : faire un cadavre exquis qui génère des poèmes + possibilité pour les utilisateurs de les noter / d’ajouter des mots
  11. Turn-by-turn : faire un jeu multijoueurs en tour par tour (jeu de cartes, de poker, ou de plateau etc)
  12. The-future-of-post-it : faire un carnet de choses à faire pour les utilisateurs, qui envoie des mails de rappels de ces choses à des dates données
  13. Gift-ideas : un site où l’on va faire des idées de cadeaux / suggérer des idées de cadeaux + les noter (les meilleurs ressortent en premier)
  14. Le-bon-recoin : refaire le bon coin en plus simple
  15. Chat-with-someone : site de chat/discussion
  16. Suggest-crawlers : site de suggestions : on clique sur un mot, il en suggère plein d’autres avec + définitions / liens de sites pour chacuns
  17. Tv-fans : site de présentations + notes d’émissions télé

Sujet imposé si vous n’avez pas d’idée

Cocktails : on se connecte, on a une liste d’éléments (récupérés en JSON) disponibles, on coche ceux qui nous intéressent, on valide, c’est envoyé, et le retour en JSON affiche les cocktails qu’il est possible de faire avec ce que l’on a coché.

Ce que vous devez rendre

Idéalement

Une Url vers un site Web (utilisez Alwaysdata par exemple)

Si vous n’avez pas le choix

Les fichiers source de votre projet

Pour favoriser votre organisation

  • Utilisez ce que l’on a vu en cours (Google boilerplate)
  • Librairies autorisées

    Interdiction d’utiliser une librairie JavaScript qui ne vienne pas des sites autorisés précédemment

    Retard

    Après le dimanche 18 mars minuit

    Fin de semaine de notre dernier cours, soit dimanche à minuit. Passé ce délai ce sera 1 pt par 2 heures de retard (je prendrai en compte la date de réception du mail).
    Pour ceux qui essaient vraiment d’aller jusqu’à la dernière minute, toute heure entamée est comptée comme une heure complète. Exemple : un point en moins si je le reçois le lundi 3 avril à 00:01.
    N’oubliez pas de me donner le nom et le mot de passe pour se connecter !

    Avertissements

    • Copie sur une autre personne (« je se savais pas comment implémenter telle ou telle fonctionnalité dont j’avais besoin pour aller plus loin, je l’ai copiée sur un autre »), deux cas se distinguent :
      • si la personne est clairement nommée, cette fonctionnalité ne sera pas prise en compte dans la notation (= 0 pour cette fonctionnalité) ;
      • si la personne est clairement nommée, et qu’il y a une amélioration de la fonctionnalité : note pour la fonctionnalité divisée par 2 (uniquement la moitié du travail a été faite) ;
      • 0 aux deux personnes sinon ;
    • Si je m’aperçois que vous avez bêtement copié collé des sources Internet, je vous convoquerai pour vous demander de m’expliquer la fonctionnalité, et deux cas se distinguent :
      • si vous ne savez pas m’expliquer le code alors 0 ;
      • si vous savez m’expliquer tout le code alors votre note totale sera divisée par vous + le nombre de contributeurs à ce projet, ce qui se rapprochera certainement de 0 aussi.

    Voici un exemple de ce que vous pouvez faire, si vous choisissez le projet cocktails.