Commit ef44d9eea5e4f884f15fa26315a2f62d2c39b8f5

Authored by hitier
2 parents 2b3ef82e 3bf6504d

Update deploy pattterns

CHANGELOG.md
... ... @@ -24,6 +24,12 @@ or major refactoring improvments.
24 24  
25 25 ## Unreleased
26 26  
  27 +## [0.2.pre-5] - 2021-03-19 - More Deploy facilities
  28 +### Changed
  29 +More documentation
  30 +Configuration pattern changed
  31 +
  32 +
27 33 ## [0.2.pre-4] - 2021-03-15 - First Graphs
28 34 ### New
29 35 D3js imports
... ...
INSTALL.md
... ... @@ -3,6 +3,9 @@
3 3 ## Prérequis
4 4  
5 5 - python3
  6 +- sqlite ( pour le développement et les tests unitaires )
  7 +- chrome-driver et chromium ( pour les tests unitaires)
  8 +- postgresql ou mariadb (pour la production)
6 9  
7 10 ## Obtenir un répertoire fonctionnel
8 11  
... ... @@ -13,14 +16,36 @@
13 16  
14 17 python3 -m venv venv
15 18 source venv/bin/activate
  19 + pip install --upgrade pip
16 20 pip install -r requirements.txt
17 21  
18 22 ### Configurer l'application
19 23  
20   - cp resources/pdc_config.py .
21   - $(EDITOR) pdc_config.py
  24 + # D'abord les accés base de donnée
  25 + cp resources/db_config.py .
  26 + $(EDITOR) db_config.py
22 27  
  28 + # Ensuite l'appli elle même (ce fichier est utilisable tel quel)
23 29 cp resources/flaskenv .flaskenv
  30 + $(EDITOR) .flaskenv
  31 +
  32 +### Créer la base de données
  33 +
  34 +Utiliser l'outil de ligne de commande fourni avec l'application.
  35 +
  36 + flask pdc_db --help
  37 +
  38 + # créer la structure de la base
  39 + flask pdc_db create_db
  40 +
  41 + # renseigner des agent depuis une base type lesia
  42 + flask pdc_db feed_from_lesia
  43 +
  44 + # entrer des périodes
  45 + flask pdc_db feed_periods
  46 +
  47 + # entrer des charges aléatoires (une centaine)
  48 + flask pdc_db feed_random_charges
24 49  
25 50  
26 51 ### Jouer les tests et exécuter un serveur local
... ... @@ -34,19 +59,34 @@
34 59 # ouvrir un serveur sur localhost:5000
35 60 flask run
36 61  
37   -## Configurer apache
  62 +## Configurer l'appli web avec apache
38 63  
39 64 Les fichiers concernés:
40 65  
41 66 - pdc_web.wsgi
42   -- pdc_config.py
  67 +- db_config.py
43 68  
44 69 La procédure:
45 70  
46   - mkdir /var/www/html/pdc-web
  71 + # créer le répertoire pour le web
  72 + export WEB_DIR=/var/www/html/pdc-web
  73 + mkdir $WEB_DIR
  74 +
  75 + # le peupler avec le code
  76 + export GIT_DIR=/path/to/pdc_web/.git
  77 + export GIT_BRANCH=master
  78 + git --work-tree=$WEB_DIR --git-dir=$GIT_DIR checkout -f $GIT_BRANCH
47 79  
  80 + # le configurer : cf plus haut "Obtenir un répertoire fonctionnel"
  81 + cd $WEB_DIR
  82 + python -m venv venv
  83 + source venv/bin/activate
  84 + pip install -r requirements.txt
  85 + $(EDITOR) db_config.py .flaskenv
  86 +
  87 + # configurer le serveur web
48 88 cp ./resources/apache2-virtual-host.conf /etc/apache2/sites-available/pdc-web.conf
49   - $(EDITOR) /etc/apache2/sites-available/pdc-web.conf # éditer les paramètres
  89 + $(EDITOR) /etc/apache2/sites-available/pdc-web.conf
50 90 a2ensite pdc-web
51 91 apachectl restart
52 92  
... ...
VERSION.txt
1   -0.2.pre-4
  1 +0.2.pre-5
... ...
app/__init__.py
... ... @@ -18,12 +18,12 @@ def load_user(user_id):
18 18  
19 19  
20 20 # Please, set a config file on top project dir
21   -# find example in ressources/pdc_config.py
  21 +# see in ../pdc_config.py
22 22 try:
23 23 from pdc_config import Config, ProdConfig, DevConfig, TestConfig
24 24 except ImportError:
25 25 # TODO: use logging system
26   - print("Please set an pdc_config.py file in you PYTHON_PATH")
  26 + print("Please set a pdc_config.py file in you PYTHON_PATH")
27 27 print("See INSTALL.md for more info")
28 28 sys.exit(-1)
29 29  
... ...
app/commands/__init__.py
... ... @@ -2,4 +2,6 @@ from flask import Blueprint
2 2  
3 3 bp = Blueprint('pdc_db', __name__)
4 4  
  5 +bp.cli.short_help ="Database utilities for pdc app"
  6 +
5 7 from . import commands
6 8 \ No newline at end of file
... ...
app/commands/commands.py
... ... @@ -2,6 +2,7 @@ import sys
2 2 import click
3 3 import random
4 4  
  5 +from sqlalchemy.exc import OperationalError
5 6 from sqlalchemy.sql import func
6 7 from sqlalchemy.ext.automap import automap_base
7 8 from sqlalchemy.orm import Session
... ... @@ -9,19 +10,29 @@ from sqlalchemy import create_engine
9 10  
10 11 from app.models import db, User, Agent, Service, Project, Capacity, Period, Charge
11 12  
12   -from db_config import mysql_uri
  13 +from db_config import mysql_lesia_uri
13 14  
14 15 from . import bp
15 16  
16 17  
17 18 @bp.cli.command("feed_from_lesia")
18 19 def feed_from_lesia():
  20 + """ Feed db with agents from a lesia like mysql database.
  21 +
  22 + configure that database uri in the db_config.py file.
  23 + """
19 24 Base = automap_base()
20 25  
21   - engine = create_engine(mysql_uri)
  26 + engine = create_engine(mysql_lesia_uri)
22 27  
23 28 # reflect the tables
24   - Base.prepare(engine, reflect=True)
  29 + try:
  30 + Base.prepare(engine, reflect=True)
  31 + except OperationalError:
  32 + # TODO: use logging facility instead
  33 + print("Please, configure the mysql database (see db_config.py)")
  34 + sys.exit(-1)
  35 +
25 36  
26 37 # mapped classes are now created with names by default
27 38 # matching that of the table name.
... ... @@ -58,6 +69,7 @@ def feed_from_lesia():
58 69  
59 70 @bp.cli.command("feed_periods")
60 71 def feed_periods():
  72 + """ Fill in the periods name in the database. """
61 73 for y in range(2014, 2023):
62 74 for s in ['S1', 'S2']:
63 75 period_name = "{}_{}".format(y, s)
... ... @@ -66,9 +78,10 @@ def feed_periods():
66 78 db.session.commit()
67 79  
68 80  
69   -@bp.cli.command("random_charges")
70   -@click.option('--agent', '-a', 'agent', default=None)
71   -def random_charges(agent):
  81 +@bp.cli.command("feed_random_charges")
  82 +@click.option('--agent', '-a', 'agent', default=None, help="the agent id you want to charge")
  83 +def feed_random_charges(agent):
  84 + """ Randomly fill in the agents charges. """
72 85 for i in range(0, 100):
73 86 if agent is None:
74 87 agent_id = random.choice([i for (i,) in db.session.query(Agent.id).all()])
... ... @@ -98,9 +111,10 @@ def random_charges(agent):
98 111 db.session.commit()
99 112  
100 113  
101   -@bp.cli.command('delete_user')
  114 +@bp.cli.command('user_delete')
102 115 @click.argument('user_id')
103   -def delete_user(user_id):
  116 +def user_delete(user_id):
  117 + """Delete the user by given id (see user_show_all")."""
104 118 user = User.query.get(user_id)
105 119 db.session.delete(user)
106 120 db.session.commit()
... ... @@ -108,26 +122,29 @@ def delete_user(user_id):
108 122  
109 123 @bp.cli.command('create_db')
110 124 def create_db():
  125 + """ Create the database structure."""
111 126 db.create_all()
112 127 admin = User(email='admin@nowhere.org', name='admin', login='admin', password='admin')
113 128 db.session.add(admin)
114 129 db.session.commit()
115 130  
116 131  
117   -@bp.cli.command('add_user')
  132 +@bp.cli.command('user_add')
118 133 @click.argument('email')
119 134 @click.argument('name')
120 135 @click.argument('login')
121 136 @click.argument('password')
122   -def add_user(email, name, login, password):
  137 +def user_add(email, name, login, password):
  138 + """ Add a new user in db."""
123 139 user = User(email=email, name=name, login=login, password=password)
124 140 db.session.add(user)
125 141 db.session.commit()
126 142 print("added ", name)
127 143  
128 144  
129   -@bp.cli.command('show_all')
130   -def show_all():
  145 +@bp.cli.command('user_show_all')
  146 +def user_show_all():
  147 + """ Show all users in db."""
131 148 print("{:<5} {:<15} {:<15} {:<15} {:<15}".format('id', 'name', 'login', 'passwd', 'email'))
132 149 print("{:<5} {:<15} {:<15} {:<15} {:<15}".format('-' * 5, '-' * 15, '-' * 15, '-' * 15, '-' * 15))
133 150 for user in User.query.all():
... ...
app/main/templates/agent.html
... ... @@ -74,7 +74,7 @@
74 74 // Pour l'axe X, c'est la liste des pays
75 75 // Pour l'axe Y, c'est le max des charge
76 76 x.domain(data.map(d => d.periode));
77   - y.domain([0, d3.max(data, d => d.charge)-1]);
  77 + y.domain([0, 100]);
78 78  
79 79 // Ajout de l'axe X au SVG
80 80 // Déplacement de l'axe horizontal et du futur texte (via la fonction translate) au bas du SVG
... ...
resources/pdc_config.py renamed to pdc_config.py
1 1 import os
  2 +from db_config import *
2 3  
3 4 root_dir = os.path.abspath(os.path.dirname(__file__))
4 5  
5 6  
  7 +#
  8 +# SQLALCHEMY_DATABASE_URI will default to 'sqlite:///:memory:' if not set
  9 +#
  10 +
6 11 class Config(object):
7 12 SECRET_KEY = 'dev'
8 13 SQLALCHEMY_TRACK_MODIFICATIONS = False
9   - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
10   - 'sqlite:///' + os.path.join(root_dir, 'pdc_app.db')
  14 +
  15 + # Trying to set specific db uri from ./db_config.py
  16 + try:
  17 + SQLALCHEMY_DATABASE_URI = sqlalchemy_database_uri
  18 + except NameError:
  19 + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
  20 + 'sqlite:///' + os.path.join(root_dir, 'pdc_app.db')
11 21  
12 22 with open(os.path.join(root_dir, 'VERSION.txt')) as version_file:
13 23 VERSION = version_file.read().strip()
... ... @@ -20,11 +30,23 @@ class ProdConfig(Config):
20 30  
21 31 class DevConfig(Config):
22 32 DEBUG = True
  33 + # Trying to set specific db uri from ./db_config.py
  34 + try:
  35 + SQLALCHEMY_DATABASE_URI = sqlalchemy_devdb_uri
  36 + except NameError:
  37 + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
  38 + 'sqlite:///' + os.path.join(root_dir, 'pdc_app_dev.db')
23 39  
24 40  
25 41 class TestConfig(Config):
26 42 TESTING = True
27 43 DEBUG = True
  44 + # Trying to set specific db uri from ./db_config.py
  45 + try:
  46 + SQLALCHEMY_DATABASE_URI = sqlalchemy_testdb_uri
  47 + except NameError:
  48 + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
  49 + 'sqlite:///' + os.path.join(root_dir, 'pdc_app_test.db')
28 50 # ignores @login_required decorator
29 51 # LOGIN_DISABLED = True
30 52  
... ...
resources/db_config.py
  1 +SQLITE = {
  2 + 'file': '/home/user/tmp/pdc.db'
  3 +}
  4 +sqlite_uri = 'sqlite:///%(file)s' % SQLITE
  5 +
  6 +MYSQL = {
  7 + 'user': 'mysql',
  8 + 'pw': 'mysql',
  9 + 'db': 'pdc_dev',
  10 + 'host': '127.0.0.1',
  11 + 'port': '3306',
  12 +}
  13 +mysql_uri = 'mysql+pymysql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % MYSQL
  14 +
1 15 POSTGRES = {
2   - 'user': 'postgres',
3   - 'pw': 'postgres',
4   - 'db': 'climso',
  16 + 'user': 'aroma-user',
  17 + 'pw': 'aroma-pwd',
  18 + 'db': 'aroma-db',
5 19 'host': '127.0.0.1',
6   - 'port': '5434',
  20 + 'port': '5432',
7 21 }
8 22 postgres_uri = 'postgresql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % POSTGRES
9 23  
10   -SQLITE = {
11   - 'file': '/home/richard/tmp/climso.db'
  24 +POSTGRESDEV = {
  25 + 'user': 'aroma-user',
  26 + 'pw': 'aroma-pwd',
  27 + 'db': 'aroma-dev-db',
  28 + 'host': '127.0.0.1',
  29 + 'port': '5433',
12 30 }
13   -sqlite_uri = 'sqlite:///%(file)s' % SQLITE
14   -
15   -database_uri = sqlite_uri
16   -database_uri = postgres_uri
  31 +postgres_dev_uri = 'postgresql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % POSTGRESDEV
17 32  
18   -data_basedir = '/data/CLIMSO'
  33 +POSTGRESTEST = {
  34 + 'user': 'aroma-user',
  35 + 'pw': 'aroma-pwd',
  36 + 'db': 'aroma-test-db',
  37 + 'host': '127.0.0.1',
  38 + 'port': '5434',
  39 +}
  40 +postgres_test_uri = 'postgresql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % POSTGRESTEST
19 41  
20   -MYSQL = {
  42 +MYSQL_LESIA = {
21 43 'user': 'mysql',
22 44 'pw': 'mysql',
23 45 'db': 'pdc_dev',
24 46 'host': '127.0.0.1',
25 47 'port': '3306',
26 48 }
27   -mysql_uri = 'mysql+pymysql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % MYSQL
  49 +mysql_lesia_uri = 'mysql+pymysql://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s' % MYSQL
  50 +
  51 +# Here, set you database uri
  52 +# will be imported in ./pdc_config.py
  53 +#
  54 +# sqlalchemy_database_uri = 'sqlite:///:memory:'
  55 +# sqlalchemy_database_uri = 'sqlite:///another-app.db'
  56 +# sqlalchemy_database_uri = postgres_uri
  57 +# sqlalchemy_testdb_uri = postgres_test_uri
  58 +# sqlalchemy_devdb_uri = postgres_dev_uri
  59 +sqlalchemy_devdb_uri = 'sqlite:///another-app.db'
... ...
resources/farm-index.html 0 → 100644
... ... @@ -0,0 +1,95 @@
  1 +<!doctype html>
  2 +<html lang="fr">
  3 +<head>
  4 + <meta charset="utf-8">
  5 + <meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
  6 + <link href="pdc-ico.svg" rel="icon"/>
  7 + <link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
  8 + integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" rel="stylesheet">
  9 + <title> PDC Farm </title>
  10 + <style>
  11 +
  12 + /*
  13 + * Globals
  14 + */
  15 +
  16 + html,
  17 + body {
  18 + height: 100%;
  19 + /*background-color: #333;*/
  20 + }
  21 +
  22 + body {
  23 +
  24 + display: flex;
  25 + align-items: center;
  26 + background-color: #f5f5f5;
  27 + }
  28 +
  29 + .cover-container {
  30 + width: 100%;
  31 + max-width: 330px;
  32 + padding: 15px;
  33 + margin-top: 10em;
  34 + }
  35 +
  36 +
  37 + /*.cover {*/
  38 + /* padding: 0 1.5rem;*/
  39 + /*}*/
  40 +
  41 +
  42 + .cover .btn-lg {
  43 + padding: .75rem 1.25rem;
  44 + font-weight: 700;
  45 + }
  46 +
  47 + .btn-lg {
  48 + display: block;
  49 + width: 100%;
  50 + margin: 10px;
  51 + color: #fff;
  52 + background-color: #343a40;
  53 + border-color: #343a40;
  54 +
  55 + padding: .5rem 1rem;
  56 + font-size: 1.25rem;
  57 + line-height: 1.5;
  58 + border-radius: .3rem;
  59 + }
  60 +
  61 + /*
  62 + * Footer
  63 + */
  64 + /*.mastfoot {*/
  65 + /* color: rgba(255, 255, 255, .5);*/
  66 + /*}*/
  67 + </style>
  68 +</head>
  69 +<body class="text-center">
  70 +
  71 + <div class="cover-container d-flex h-100 p-3 mx-auto flex-column">
  72 +
  73 + <main class="inner cover" role="main">
  74 + <img class="mb-4" height="72" src="pdc-farm.svg" width="72">
  75 + <h1 class="h3 font-weight-normal my-5">Plan de Charge</h1>
  76 +
  77 + <p class="lead">
  78 + Place centrale de redirection vers les différents sites de la ferme d'applications sur le site de l'Irap.
  79 + </p>
  80 + <p class="lead">
  81 + <a class="btn btn-lg btn-secondary" href="https://plan-de-charges.irap.omp.eu/public/">Pour tous</a>
  82 + <a class="btn btn-lg btn-secondary" href="https://plan-de-charges.irap.omp.eu/irap/">Irap</a>
  83 + </p>
  84 + </main>
  85 +
  86 + <footer class="mt-5 mb-3 text-muted">
  87 + <p class="text-muted text-center">
  88 + Plan de Charge<br/>
  89 + © 2021 IRAP
  90 + </p>
  91 + </footer>
  92 +
  93 + </div>
  94 +</body>
  95 +</html>
0 96 \ No newline at end of file
... ...
resources/flaskenv
1 1 FLASK_ENV=development
2   -FLASK_APP=pdc
  2 +FLASK_APP=pdc_web
... ...
resources/pdc-farm.svg 0 → 100644
... ... @@ -0,0 +1,101 @@
  1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2 +<!-- Created with Inkscape (http://www.inkscape.org/) -->
  3 +
  4 +<svg
  5 + xmlns:dc="http://purl.org/dc/elements/1.1/"
  6 + xmlns:cc="http://creativecommons.org/ns#"
  7 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  8 + xmlns:svg="http://www.w3.org/2000/svg"
  9 + xmlns="http://www.w3.org/2000/svg"
  10 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
  11 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
  12 + width="620"
  13 + height="620"
  14 + viewBox="0 0 164.04166 164.04166"
  15 + version="1.1"
  16 + id="svg8"
  17 + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
  18 + sodipodi:docname="pdc-farm.svg"
  19 + inkscape:export-filename="/home/rhitier/00DEV/pdc-web/app/static/img/pdc.png"
  20 + inkscape:export-xdpi="96"
  21 + inkscape:export-ydpi="96">
  22 + <defs
  23 + id="defs2" />
  24 + <sodipodi:namedview
  25 + id="base"
  26 + pagecolor="#ffffff"
  27 + bordercolor="#666666"
  28 + borderopacity="1.0"
  29 + inkscape:pageopacity="0.0"
  30 + inkscape:pageshadow="2"
  31 + inkscape:zoom="0.7"
  32 + inkscape:cx="266.06923"
  33 + inkscape:cy="292.4403"
  34 + inkscape:document-units="mm"
  35 + inkscape:current-layer="layer1"
  36 + showgrid="false"
  37 + inkscape:window-width="1595"
  38 + inkscape:window-height="893"
  39 + inkscape:window-x="192"
  40 + inkscape:window-y="103"
  41 + inkscape:window-maximized="0"
  42 + units="px"
  43 + fit-margin-top="0"
  44 + fit-margin-left="0"
  45 + fit-margin-right="0"
  46 + fit-margin-bottom="0" />
  47 + <metadata
  48 + id="metadata5">
  49 + <rdf:RDF>
  50 + <cc:Work
  51 + rdf:about="">
  52 + <dc:format>image/svg+xml</dc:format>
  53 + <dc:type
  54 + rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
  55 + <dc:title />
  56 + </cc:Work>
  57 + </rdf:RDF>
  58 + </metadata>
  59 + <g
  60 + inkscape:label="Layer 1"
  61 + inkscape:groupmode="layer"
  62 + id="layer1"
  63 + transform="translate(-35.439223,-79.805708)">
  64 + <g
  65 + id="g820"
  66 + transform="matrix(1.0412558,0,0,0.99961298,-6.221947,0.06279446)">
  67 + <rect
  68 + style="fill:#7f7f9f;fill-opacity:1;stroke:#ffffff;stroke-width:2.66268826;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  69 + id="rect836"
  70 + width="151.87685"
  71 + height="156.32327"
  72 + x="43.024651"
  73 + y="83.286613"
  74 + ry="42.783207" />
  75 + </g>
  76 + <text
  77 + xml:space="preserve"
  78 + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:65.38256836px;line-height:24.25;font-family:aakar;-inkscape-font-specification:'aakar Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.62687314;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  79 + x="45.287548"
  80 + y="165.87547"
  81 + id="text3721"
  82 + transform="scale(1.025263,0.97535948)"><tspan
  83 + sodipodi:role="line"
  84 + id="tspan3719"
  85 + x="45.287548"
  86 + y="165.87547"
  87 + style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:65.38256836px;line-height:24.25;font-family:FreeSans;-inkscape-font-specification:'FreeSans Semi-Bold';text-align:start;text-anchor:start;fill:#ffffff;stroke:#000000;stroke-width:0.62687314;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">PDC</tspan></text>
  88 + <text
  89 + xml:space="preserve"
  90 + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:65.38256836px;line-height:24.25;font-family:aakar;-inkscape-font-specification:'aakar Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.62687314;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
  91 + x="45.876003"
  92 + y="214.90958"
  93 + id="text3721-3"
  94 + transform="scale(1.025263,0.97535947)"><tspan
  95 + sodipodi:role="line"
  96 + id="tspan3719-6"
  97 + x="45.876003"
  98 + y="214.90958"
  99 + style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:65.38256836px;line-height:24.25;font-family:FreeSans;-inkscape-font-specification:'FreeSans Semi-Bold';text-align:start;text-anchor:start;fill:#ffffff;stroke:#000000;stroke-width:0.62687314;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">farm</tspan></text>
  100 + </g>
  101 +</svg>
... ...