Afficheur de jour férié

This page is a translated version of the page API:Holidays viewer and the translation is 100% complete.

Présentation

Ce tutoriel permet de créer une application de démonstration qui récupère les jours fériés et les observations pour une date donnée dans Wikipedia, avec une option qui permet de se connecter pour ajouter de nouveaux jours.

Les outils et les technologies utilisées pour créer l'application de démonstration sont :

Processus pas à pas pour construire cette application

Etape 1 : installer l'environnement de développement Python et Flask

Python est fourni préinstallé avec la plupart des distributions Linux. Pour les autres systèmes d'exploitation, voir le guide des débutants Python pour les instructions concernant l'installation.

Installez Flask en exécutant pip install flask. Si vous ne disposez pas de pip, récupérez-le à partir du site web Pip officiel


Etape 2: créer une application Flask simple

Dans votre répertoire racine (home), créez un répertoire appelé holidays-viewer et qui contiendra tous les fichiers des applications. Dans le répertoire, créez un fichier app.py et insérez-y le code suivant :

#!/usr/bin/python3

from flask import Flask

APP = Flask(__name__)

@APP.route("/")
def list_holidays():
  return "Holidays and observances"

if __name__ == "__main__":
  APP.run()

Exécutez l'application en utilisant la commande python app.py et ouvrez http://127.0.0.1:5000/ dans votre navigateur. Vous devriez voir « Holidays and observances » affiché.

Etape 3 : créer l'architecture de base

L'application aura quatre pages : la page d'accueil, une page de recherche, une page pour la connexion et une page pour les annonces. Toutes les pages auront certains éléments en commun, il nous faut donc créer un fichier de configuration de base appelé layout.html qui contiendra ces éléments.

Notez-bien que nous utilisons les classes de Bootstrap pour appliquer un style CSS spécifique à un élément, Materialiser les icônes pour les icônes concernant l'addition, la recherche et la flèche arrière, et Jinja pour étendre l'affichage de base à d'autres pages, et pour passer des variables de Python à HTML.

$HOME/holidays-viewer/templates/layout.html
<title>Holidays</title>

<link rel="stylesheet" href="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css">
<link rel="stylesheet" href="//tools-static.wmflabs.org/fontcdn/css?family=Material+Icons">

<div class="content bg-secondary rounded m-auto">
  <div class="title-bar bg-primary-dark text-white pl-2">
    <small>Holidays and observances</small>
  </div>
  <div class="header-bar bg-primary text-white shadow p-2">
    {% if request.path != url_for('list_holidays') %}
    <a class=" btn text-white" href="{{ url_for('list_holidays') }}">
      <i class="material-icons">arrow_back</i>
    </a>
    {% endif %}
    <h5>{{header}}</h5>
    <div class="filler"></div>
    <a class="btn text-white" href="{{ url_for('add') }}">
      <i class="material-icons">add</i>
    </a>
    <a class="btn text-white" href="{{ url_for('search') }}">
      <i class="material-icons">search</i>
    </a>
  </div>
  {% with messages = get_flashed_messages() %}
    {% if messages %}
    <div class="alert alert-primary mb-0" role="alert">
      {% for message in messages %}
        {{ message }}
      {% endfor %}
      <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">×</span>
      </button>
    </div>
    {% endif %}
  {% endwith %}
  {% block content %}{% endblock %}
</div>
<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"></script>

Les autres pages vont étendre layout.html en utilisant le code suivant :

{% extends "layout.html" %}
{% block content %}
  <!--content for other pages-->
{% endblock %}

Etape 4 : lister les jours de vacance

L'URL de base de l'application va activer la fonction list_holidays(...), qui liste les vacances à une date donnée.

Dans la fonction et dans toute l'application, holidays_date se rapporte à la date de vacance à afficher, header concerne le titre de la page, et holidays_html se rapporte au html qui contient les vacances à afficher. Nous utiliserons également la fonction render_template(...) pour générer un fichier html spécifique à partir du répertoire des modèles. Les autres arguments ajoutés à la fonction sont les variables qui seront passées au fichier HTML.

Dans app.py, mettre à jour list_holidays() avec le code ci-dessous :

@APP.route('/', methods=['GET', 'POST'])
@APP.route('/<holidays_date>', methods=['GET', 'POST'])
def list_holidays(holidays_date=None):
    holidays_html = ""

    return render_template("index.html", header=holidays_date.replace('_', ' '),
                           holidays_html=holidays_html)
$HOME/holidays-viewer/templates/index.html
{% extends "layout.html" %}
{% block content %}
  <div class="holidays-html">
    {{holidays_html|safe}}
  </div>
{% endblock %}

Obtenir la date du jour

Si aucune date n'est précisée, les vacances seront affichées pour la date du jour. Pour utiliser le module Python datetime afin d'obtenir la date du jour, importez le module avec from datetime import datetime puis créez la fonction suivante :

def get_todays_date():
    current_month = datetime.now().strftime('%B')
    current_day = datetime.now().strftime('%d')
    if current_day.startswith('0'):
        current_day = current_day.replace('0', '')

    return current_month + "_" + current_day

Appeler la fonction dans list_holidays(...) :

if holidays_date is None:
        holidays_date = get_todays_date()

Récupérer la liste des vacances

Une fois la date obtenue, nous obtenons les vacances pour cette date. Wikipedia possède une page par date et les vacances figurent dans une section intitulée Holidays and observances. Pour obtenir les vacances, il nous faut connaître leur numéro de section ainsi que le contenu de ces sections.

Créez une fonction pour obtenir le numéro de section en utilisant API:Parse  :

def get_holidays_section(url, page, date_to_get):
    params = {
        "format":"json",
        "action":"parse",
        "prop":"sections",
        "page":page
    }

    response = S.get(url=url, params=params)
    data = response.json()
    sections = data['parse']['sections']
    section_number = "0"

    for index, value in enumerate(sections):
        if value['anchor'] == "Holidays_and_observances":
            section_number = index + 1

        if url == TEST_URL:
            if value['anchor'] == date_to_get:
                section_number = index + 1

    return section_number

Créez une fonction get_holidays(...) pour obtenir les vacances dans cette section en utilisant également API:Parse , puis appelez la fonction dans list_holidays(...) :

section_number = get_holidays_section(URL, holidays_date, None)
holidays = get_holidays(URL, holidays_date, section_number)
holidays_html = holidays

Mettre à jour les liens de vacance

Le code HTML des vacances qui a été renvoyé contient des liens internes qui pointent vers ces vacances, par exemple /wiki/New_Years_Day. Il faut préfixer ces liens avec //en.wikipedia.org en utilisant jQuery pour en faire des liens externes dans notre application, et qu'ils puissent s'ouvrir dans un nouvel onglet. Pour faire cela, ajoutez le code suivant à $HOME/holidays-viewer/static/update-links.js :

$( document ).ready( function() {
    $( ".holidays-html a" ).attr( "target", "_blank" );

    $( ".holidays-html a" ).attr( "href", function( i, href ) {
      return "//en.wikipedia.org" + href;
    });
});

Ensuite ajoutez jQuery à layout.html en utilisant :

<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="static/update-links.js"></script>


Etape 5 : recherche des vacances à d'autres dates

Pour obtenir les vacances à d'autres dates, créez une route de recherche en affichant un formulaire de saisie du mois et du jour concerné :

@APP.route("/search")
def search():

    return render_template("search.html", header="Search date")
$HOME/holidays-viewer/templates/search.html
{% extends "layout.html" %}
{% block content %}
  <div class="instructions m-3">
    Search for holidays by date
  </div>
  <div class="base rounded shadow bg-white m-3">
    <form class="m-auto" action="/" method="POST">
      <fieldset>
        <div class="label-field">Select Month</div>
        <select class="bg-secondary mb-5 border-0" name="monthList">
          <option value="January">January
          <option value="February">February
          <option value="March">March
          <option value="April">April
          <option value="May">May
          <option value="June">June
          <option value="July">July
          <option value="August">August
          <option value="September">September
          <option value="October">October
          <option value="November">November
          <option value="December">December
        </select>
      </fieldset>
      <fieldset>
        <div class="label-field">Select Day</div>
        <select class="bg-secondary mb-5 border-0" name="dayList">
          <option value="1">1
          <option value="2">2
          <option value="3">3
          <option value="4">4
          <option value="5">5
          <option value="6">6
          <option value="7">7
          <option value="8">8
          <option value="9">9
          <option value="10">10
          <option value="11">11
          <option value="12">12
          <option value="13">13
          <option value="14">14
          <option value="15">15
          <option value="16">16
          <option value="17">17
          <option value="18">18
          <option value="19">19
          <option value="20">20
          <option value="21">21
          <option value="22">22
          <option value="23">23
          <option value="24">24
          <option value="25">25
          <option value="26">26
          <option value="27">27
          <option value="28">28
          <option value="29">29
          <option value="30">30
          <option value="31">31
        </select>
      </fieldset>
      <button type="submit" name="search" class="bg-primary btn btn-submit text-white">Submit</button>
    </form>
  </div>
{% endblock %}

Une fois que le formulaire de recherche a été envoyé, mettez à jour holidays_date pour qu'il corresponde à la date qui a été saisie. Pour faire cela, ajoutez le code suivant à list_holidays(...) :

if request.method == 'POST' and 'search' in request.form:
        search_month = str(request.form.get('monthList'))
        search_day = str(request.form.get('dayList'))
        holidays_date = search_month +"_"+search_day

Etape 6 : lister un jour de vacance

La page à laquelle nous allons déclarer une nouvelle vacance est protégée des modifications issues des utilisateurs anonymes; il faut donc se connecter d'abord en utilisant clientlogin.

Pour ajouter une vacance, envoyez une requête à API:Edit avec la date et la descritption de la vacance. La modification ajoute de nouvelles vacances à la page suivante de la Wikipedia de test : Sandbox/Holidays_and_observances. Ceci est fait pour empêcher d'ajouter des vacances de test sur la Wikipedia anglophone.

Une fois les informations de vacance ajoutées, redirigez vers la page d'accueil où les vacances ajoutées seront également affichées et formatées en gras pour les distinguer des vacances réelles. Pour récupérer les vacances de test en même temps que les vacances réelles, mettez à jour list_holidays(...) :

test_section_number = get_holidays_section(TEST_URL, TEST_PAGE, holidays_date)
test_holidays = get_holidays(TEST_URL, TEST_PAGE, test_section_number)

holidays_html = test_holidays + holidays
flash("Holidays added through this app are in bold")
$HOME/holidays-viewer/templates/login.html
{% extends "layout.html" %}
{% block content %}
  <div class="instructions m-3">
    <p>You need to login to Wikipedia in order to add a new holiday
  </div>
  <div class="base rounded shadow bg-white m-3">
    <form class="m-auto" action="/login" method="POST">
      <div class="form-group">
        <div class="form-field">
          <div class="label-field">Username</div>
          <input class="bg-secondary mb-5 border-0" name="username">
        </div>
        <div class="form-field">
          <div class="label-field">Password</div>
          <input class="bg-secondary mb-5 border-0" type="password" name="password">
        </div>
      </div>
      <button type="submit" name="login" class="bg-primary btn btn-submit text-white">Login</button>
    </form>
  </div>
{% endblock %}
$HOME/holidays-viewer/templates/add.html
{% extends "layout.html" %}
{% block content %}
  <div class="instructions m-3">
    <p>Add a new test holiday
  </div>
  <div class="base rounded shadow bg-white m-3">
    <form class="m-auto" action="" method="POST">
      <div class="form-group">
        <div class="form-field">
          <div class="label-field">Date [MMMM dd]</div>
          <input class="bg-secondary border-0 mb-5" name="date" placeholder="e.g April 1">
        </div>
        <div class="form-field">
          <div class="label-field">Description</div>
          <input class="bg-secondary border-0 mb-5" name="description" placeholder="e.g April fools' day">
        </div>
      </div>
      <button type="submit" name="add" class="bg-primary btn btn-submit text-white">Add</button>
    </form>
  </div>
{% endblock %}

Etape 7 : habiller l'application

Pour ajouter davantage de style à votre application, créez une feuille de style appelée style.css et créez un lien dessus à partir de layout.html en ajoutant <link rel="stylesheet" href="static/style.css">.

$HOME/holidays-viewer/static/style.css
.content {
    width: 420px;
    min-height: 100vh;
}

.holidays-html{
    overflow-y: auto;
    overflow-x: hidden;
    max-height: 88vh;
    scrollbar-width: thin;
}

.base {
    height: 400px;
    display: flex;
}

input, select {
    width: 300px;
    height: 40px;
}

.btn-submit {
    width: 300px;
}

.btn {
    cursor: pointer;
    align-content: center;
    background-color: transparent;
}

.bg-primary {
    background-color: #36c !important;
}

.bg-primary-dark {
    background-color: #2a4b8d !important;
}

.bg-secondary {
    background-color: #eaecf0 !important;
}

.header-bar {
    height: 48px;
    display: flex;
    flex: 1;
    align-items: center;
}

.filler {
    flex-grow: 1;
    text-align: center
}

h2 {
    display: none;
}

ul {
    margin: 5px;
    padding: 0;
}

li  {
    list-style-type: none;
    margin-bottom: 4px;
    background-color: white;
    padding: 8px;
    border-radius: 5px;
}

ul li li {
    box-shadow: 0 .5rem 1rem rgba(0,0,0,.15);
}

Affichage des applications

A cet instant, la structure de votre application devrait être :

$HOME/holidays-viewer
├── templates/
│   └── add.html
    └── index.html
    └── layout.html
    └── login.html
    └── search.html
├── static/
│   └── style.css
    └── update-links.js
├── app.py

Avec app.py et layout.html étant :

$HOME/holidays-viewer/app.py
#!/usr/bin/python3

"""
    app.py

    Démonstrations de l'API MediaWiki

    Holidays viewer: une application de démonstration qui récupère les vacances de Wikipedia pour un jour donné avec des options de recherche de vacances à d'autres dates, et de connexion pour ajouter de nouvelles vacances.

    Licence MIT
"""

from datetime import datetime
from flask import Flask, render_template, flash, request, url_for, redirect
import requests


APP = Flask(__name__)
APP.secret_key = 'your_secret_key'

URL = "https://en.wikipedia.org/w/api.php"
TEST_URL = "https://test.wikipedia.org/w/api.php"
TEST_PAGE = "Sandbox/Holidays_and_observances"
S = requests.Session()
IS_LOGGED_IN = False

@APP.route('/', methods=['GET', 'POST'])
@APP.route('/<holidays_date>', methods=['GET', 'POST'])
def list_holidays(holidays_date=None):
    """ Affiche les vacances pour la date courante ou une date personnalisée
    """

    if holidays_date is None:
        holidays_date = get_todays_date()

    # Update date to a custom date
    if request.method == 'POST' and 'search' in request.form:
        search_month = str(request.form.get('monthList'))
        search_day = str(request.form.get('dayList'))
        holidays_date = search_month +"_"+search_day

    # Get the section numbers for the holidays on Wikipedia and for those on the test page
    section_number = get_holidays_section(URL, holidays_date, None)
    test_section_number = get_holidays_section(TEST_URL, TEST_PAGE, holidays_date)

    holidays = get_holidays(URL, holidays_date, section_number)
    test_holidays = get_holidays(TEST_URL, TEST_PAGE, test_section_number)

    holidays_html = test_holidays + holidays
    flash('Holidays added through this app are in bold')

    return render_template("index.html", header=holidays_date.replace('_', ' '),
                           holidays_html=holidays_html)


def get_todays_date():
    """ Récupère le mois actuel sous forme de texte et le jour courant en tant que nombre
    """

    current_month = datetime.now().strftime('%B')
    current_day = datetime.now().strftime('%d')
    if current_day.startswith('0'):
        current_day = current_day.replace('0', '')

    return current_month + "_" + current_day

def get_holidays_section(url, page, date_to_get):
    """ Obtenir le numéro de section des vacances sur Wikipedia et celles de la page de test
    """

    params = {
        "format":"json",
        "action":"parse",
        "prop":"sections",
        "page":page
    }

    response = S.get(url=url, params=params)
    data = response.json()
    sections = data['parse']['sections']
    section_number = "0"

    for index, value in enumerate(sections):
        if value['anchor'] == "Holidays_and_observances":
            section_number = index + 1

        if url == TEST_URL:
            if value['anchor'] == date_to_get:
                section_number = index + 1

    return section_number

def get_holidays(url, page, section_number):
    """ Obtenir le HTML contenant les vacances
    """

    params = {
        "format":"json",
        "action":"parse",
        "prop":"text",
        "page": page,
        "section": section_number,
        "disableeditsection":1
    }

    response = S.get(url=url, params=params)
    data = response.json()
    text = data['parse']['text']['*']

    return text

@APP.route("/search")
def search():
    """ Recherche des vacances à des dates prédéfinies
    """

    return render_template("search.html", header="Search date")

@APP.route("/login", methods=['GET', 'POST'])
def login():
    """ Connexion à Wikipedia
    """

    if request.method == 'POST' and 'login' in request.form:
        params_0 = {
            "action": "query",
            "meta": "tokens",
            "type": "login",
            "format": "json"
        }

        response = S.get(url=URL, params=params_0)
        data = response.json()

        login_token = data['query']['tokens']['logintoken']

        params_1 = {
            "action": "clientlogin",
            "username": str(request.form.get('username')),
            "password": str(request.form.get('password')),
            "loginreturnurl": "http://127.0.0.1:5000/login",
            "logintoken": login_token,
            "format": "json"
        }

        response = S.post(url=URL, data=params_1)
        data = response.json()

        if data['clientlogin']['status'] != 'PASS':
            flash('Oops! Something went wrong -- ' + data['clientlogin']['messagecode'])
        else:
            global IS_LOGGED_IN
            IS_LOGGED_IN = True
            flash('Login success! Welcome, ' + data['clientlogin']['username'] + '!')
            return redirect(url_for('add'))

    return render_template("login.html", header="Login")

@APP.route("/add", methods=['GET', 'POST'])
def add():
    """ Ajouter une nouvelle vacance sur une page de test et rediriger vers les vacances de cette date pour montrer celles qui ont été ajoutées.
    """

    if not IS_LOGGED_IN:
        return redirect(url_for('login'))

    if request.method == 'POST' and 'add' in request.form:

        # Wiki markup to format the added holiday's text as a list item and in bold
        holiday_text = "* '''" + str(request.form.get('description')) + "'''"
        date = str(request.form.get('date'))

        params_2 = {
            "action": "query",
            "meta": "tokens",
            "format": "json"
        }

        response = S.get(url=TEST_URL, params=params_2)
        data = response.json()

        csrf_token = data['query']['tokens']['csrftoken']

        params_4 = {
            "action": "edit",
            "title": TEST_PAGE,
            "token": csrf_token,
            "format": "json",
            "section": "new",
            "sectiontitle": date,
            "text": holiday_text,
        }

        response = S.post(url=TEST_URL, data=params_4)
        data = response.json()

        if data['edit']['result'] != 'Success':
            flash('Oops! Something went wrong -- ' + data['clientlogin']['messagecode'])
        else:
            flash('New holiday added successfully!')
            return redirect(url_for('list_holidays', holidays_date=date.replace(' ', '_')))

    return render_template("add.html", header="Add holiday")

if __name__ == "__main__":
    APP.run()
$HOME/holidays-viewer/templates/layout.html
<title>Holidays</title>

<link rel="stylesheet" href="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.min.css">
<link rel="stylesheet" href="//tools-static.wmflabs.org/fontcdn/css?family=Material+Icons">
<link rel="stylesheet" href="static/style.css">

<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="static/update-links.js"></script>

<div class="content bg-secondary rounded m-auto">
  <div class="title-bar bg-primary-dark text-white pl-2">
    <small>Holidays and observances</small>
  </div>
  <div class="header-bar bg-primary text-white shadow p-2">
    {% if request.path != url_for('list_holidays') %}
    <a class=" btn text-white" href="{{ url_for('list_holidays') }}">
      <i class="material-icons">arrow_back</i>
    </a>
    {% endif %}
    <h5>{{header}}</h5>
    <div class="filler"></div>
    <a class="btn text-white" href="{{ url_for('add') }}">
      <i class="material-icons">add</i>
    </a>
    <a class="btn text-white" href="{{ url_for('search') }}">
      <i class="material-icons">search</i>
    </a>
  </div>
  {% with messages = get_flashed_messages() %}
    {% if messages %}
    <div class="alert alert-primary mb-0" role="alert">
      {% for message in messages %}
        {{ message }}
      {% endfor %}
      <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">×</span>
      </button>
    </div>
    {% endif %}
  {% endwith %}
  {% block content %}{% endblock %}
</div>
<script src="//tools-static.wmflabs.org/cdnjs/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"></script>
 
Ecran de la page d'accueil de l'afficheur de vacances

Etapes suivantes

  • Contribuez en enregistrant l'application de démonstration que vous avez réalisée utilisant l'API MediaWiki, dans ce dépôt d'exemples de code.

Voir aussi