Last active
August 29, 2015 14:23
-
-
Save bchirico/ec02f196513f8b7325f9 to your computer and use it in GitHub Desktop.
Django
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Django</title> | |
<link rel="stylesheet" href="https://stackedit.io/res-min/themes/base.css" /> | |
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script> | |
</head> | |
<body><div class="container"><p><div class="toc"> | |
<ul> | |
<li><ul> | |
<li><a href="#mvc-in-django">MVC in Django</a><ul> | |
<li><a href="#model">Model</a></li> | |
<li><a href="#view">View</a></li> | |
<li><a href="#controller">Controller</a></li> | |
<li><a href="#filosofia-di-progetto">Filosofia di progetto</a><ul> | |
<li><a href="#api-per-il-db">API per il DB</a></li> | |
<li><a href="#url-nel-controller">URL nel controller</a></li> | |
<li><a href="#utilizzo-di-template-per-le-view">Utilizzo di template per le view</a></li> | |
<li><a href="#view-1">View</a></li> | |
</ul> | |
</li> | |
<li><a href="#struttura-di-un-progetto-django">Struttura di un progetto Django</a></li> | |
</ul> | |
</li> | |
<li><a href="#tutorial-base">Tutorial base</a></li> | |
<li><a href="#interfaccia-di-amministrazione">Interfaccia di amministrazione</a><ul> | |
<li><a href="#customizzare-linterfaccia-admin">Customizzare l’interfaccia admin</a></li> | |
</ul> | |
</li> | |
<li><a href="#template-tutorial">Template tutorial</a><ul> | |
<li><a href="#template-filtri">Template - filtri</a></li> | |
<li><a href="#template-controllo-di-flusso">Template: controllo di flusso</a></li> | |
</ul> | |
</li> | |
<li><a href="#view-2">View</a><ul> | |
<li><a href="#regular-expression-in-pythondjango">* Regular expression in Python/Django*</a></li> | |
</ul> | |
</li> | |
</ul> | |
</li> | |
</ul> | |
</div> | |
</p> | |
<h2 id="mvc-in-django"><strong>MVC in Django</strong></h2> | |
<p>Ci sono un elevato numero di funzioni che possono essere usate per facilitare e modularizzare compiti “standard”.</p> | |
<ul> | |
<li>interazione con db</li> | |
<li>autenticazione</li> | |
<li>etc</li> | |
</ul> | |
<p>Gle elementi MVVC sono gestiti in 3 file separati:</p> | |
<ul> | |
<li><code>models.py</code> -> i modelli</li> | |
<li><code>views.py</code> -> le viste</li> | |
<li><code>urls.py</code> -> i controller</li> | |
</ul> | |
<h3 id="model"><strong>Model</strong></h3> | |
<p>Il componente Model dell’applicazione è implementato direttamente come collezione di classi Python, che poi andranno <br> | |
a rappresentare le tabelle del database. Il codice SQL per creare lo schema viene generato automaticamente da Django <br> | |
quando si esegue il deployment del modello.</p> | |
<p>Si crea un n ‘virtual object database’ accessibile e usabile direttamente attraverso linguaggio di programmazione ad oggetti.</p> | |
<h3 id="view"><strong>View</strong></h3> | |
<p>Si usano template per generare diversi tipi di output, e accesso ai dati del model attraverso API Python.</p> | |
<h3 id="controller"><strong>Controller</strong></h3> | |
<p>il file <code>urls.py</code> serve per mappare gli URL richiesti sulle view che restituiscono le pagine richieste. Si possono usare <br> | |
funzioni built-in e espressioni regolari per mappare gli URL richiesti.</p> | |
<h3 id="filosofia-di-progetto"><strong>Filosofia di progetto</strong></h3> | |
<p>Favorire lo sviluppo rapido dell’applicazione, scrivendo meno codice senza ripeterlo, e interagendo con il DB principalmente <br> | |
con codice SQL autogenerato.</p> | |
<h4 id="api-per-il-db"><strong>API per il DB</strong></h4> | |
<p>Limitare al massimo le interazioni con il DB. I dati sono accessibii da ogni modulo e i join vengono creati automaticamente <br> | |
dallo strato di interfacciamento software. Rimane possibile scrivere direttamente codice SQL, ma solo quando è veramente <br> | |
necessario.</p> | |
<h4 id="url-nel-controller"><strong>URL nel controller</strong></h4> | |
<p>URL puliti e riutilizzabili: evitare le estensioni negli URL.</p> | |
<h4 id="utilizzo-di-template-per-le-view"><strong>Utilizzo di template per le view</strong></h4> | |
<p>Lo scopo è separare la logica dalla presentazione. Si evita la ridondanza, si è più protetti contro codice malevolo e si <br> | |
facilita l’estendibilità per il futuro.</p> | |
<h4 id="view-1"><strong>View</strong></h4> | |
<p>Nessun bisogno di creare nuove classi, sono essenzialmente realizzate attraverso delle funzioni Python. Si usano oggetti <br> | |
che incapsulano le richieste HTTP.</p> | |
<h3 id="struttura-di-un-progetto-django"><strong>Struttura di un progetto Django</strong></h3> | |
<pre data-initialized="true" data-gclp-id="1"><code> mysite (dir top-level) | |
|_ manage.py | |
|_ mysite (package Python) | |
| |_ __init__.py | |
| |_ settings.py | |
| |_ urls.py | |
|_ app1 (package Python) | |
|_ __init__.py | |
|_ models.py | |
|_ vies.py | |
|_ ... | |
</code></pre> | |
<p>Un singolo progetto Django può essere spezzato in diverse sotto-applicazioni (ognuna sarà un package separato, come <code>app1</code>), <br> | |
che avrà tutti i suoi componenti MVC (model, views, urls). Applicazioni diverse possono interagire importado i package.</p> | |
<ul> | |
<li><p><code>manage.py</code> <br> | |
script per automatizzare le operazioni di Django.</p></li> | |
<li><p><code>mysite/mysite</code> <br> | |
cartella della applicazione principale (omonima )del progetto</p></li> | |
<li><p><code>settings.py</code> <br> | |
impostazioni per il funzionamento dell’applicazione, tra cui: definizione dei path dell’applicazione e configurazione di <br> | |
accesso a DB</p></li> | |
<li><p><code>urls.py</code> <br> | |
configurazione degli URL per il progetto (livello root). </p></li> | |
</ul> | |
<h2 id="tutorial-base"><strong>Tutorial base</strong></h2> | |
<p>Creo il progetto direttamente da pycharm, oppure con il comando:</p> | |
<pre data-initialized="true" data-gclp-id="2"><code>django-admin.py startproject mysite | |
</code></pre> | |
<p>Per usare MySQL devo prima creare il DB:</p> | |
<pre data-initialized="true" data-gclp-id="3"><code>$ mysql --user=root -p | |
mysql> CREATE DATABASE djangodb; | |
mysql> CREATE USER 'djangouser'@'localhost' IDENTIFIED BY 'rue1iep5'; | |
mysql> GRANT ALL PRIVILEGES ON djangodb.* TO 'djangouser'@'localhost' WITH GRANT OPTION; | |
mysql> show databases; | |
</code></pre> | |
<p>Devo aggiungere la connessione al DB creato nel file <code>settings.py</code></p> | |
<pre data-initialized="true" data-gclp-id="4"><code>DATABASES = { | |
'default': { | |
'ENGINE': 'django.db.backends.mysql', | |
'NAME': 'djangodb', | |
'USER': 'djangouser', | |
'PASSWORD': 'rue1iep5', | |
'HOST': '', | |
'PORT': '', | |
} | |
} | |
</code></pre> | |
<p>Per PostgreSQL:</p> | |
<pre data-initialized="true" data-gclp-id="5"><code>DATABASES = { | |
'default': { | |
'ENGINE': 'django.db.backends.postgresql_psycopg2', | |
'NAME': 'djangodb', | |
'USER': 'djangouser', | |
'PASSWORD': 'rue1iep5', | |
'HOST': '', | |
'PORT': '', | |
} | |
} | |
</code></pre> | |
<p>All’interno del progetto creo una nuova app</p> | |
<pre data-initialized="true" data-gclp-id="6"><code>$ python manage.py startapp polls | |
</code></pre> | |
<p>Creo i modelli all’interno di <code>polls/models.py</code></p> | |
<pre data-initialized="true" data-gclp-id="7"><code>from django.db import models | |
class Question(models.Model): | |
question_text = models.CharField(max_length=200) | |
pub_date = models.DateTimeField('date published') | |
def __unicode__(self): | |
return self.question_text | |
def was_published_recently(self): | |
return self.pub_date >= timezone.now() - datetime.timedelta(days=1) | |
class Choice(models.Model): | |
question = models.ForeignKey(Question) | |
choice_text = models.CharField(max_length=200) | |
votes = models.IntegerField(default=0) | |
def __unicode__(self): | |
return self.choice_text | |
</code></pre> | |
<p>Aggiungo l’app alla lista delle app disponibili in <code>mysite/settings.py</code></p> | |
<pre data-initialized="true" data-gclp-id="8"><code>INSTALLED_APPS = ( | |
'django.contrib.admin', | |
'django.contrib.auth', | |
'django.contrib.contenttypes', | |
'django.contrib.sessions', | |
'django.contrib.messages', | |
'django.contrib.staticfiles', | |
'polls', | |
) | |
</code></pre> | |
<p>Ora tutto è pronto per sfruttare le comodità di django:</p> | |
<ul> | |
<li><p>sincronizziamo il D</p> | |
<pre data-initialized="true" data-gclp-id="9"><code>$ python manage.py makemigrations polls | |
</code></pre></li> | |
<li><p>creiamo le tabelle</p> | |
<pre data-initialized="true" data-gclp-id="10"><code>$ python manage.py migrate | |
</code></pre></li> | |
<li><p>SQL generato (mostra il codice SQL generato)</p> | |
<pre data-initialized="true" data-gclp-id="11"><code>$ python manage.py sqlmigrate polls 0001 | |
</code></pre></li> | |
<li><p>Controllo</p> | |
<pre data-initialized="true" data-gclp-id="12"><code>$ python manage.py check | |
</code></pre></li> | |
<li><p>Python shell </p> | |
<pre data-initialized="true" data-gclp-id="13"><code>$ python manage.py shell | |
</code></pre></li> | |
</ul> | |
<p>Le migrazioni sono uno strumento estremamente potente che permette modifiche in corso d’opera senza perdere i dati già inseriti. I passaggi per applicare le modifiche consistono in:</p> | |
<ul> | |
<li>modificare il modello</li> | |
<li>lanciare <code>python manage.py makemigrations</code></li> | |
<li>lanciare <code>python manage.py migrate</code></li> | |
</ul> | |
<p>A questo punto il model mette a disposizione una serie di metodi e utility per interrogare il DB, ad esempio</p> | |
<table> | |
<thead> | |
<tr> | |
<th>Statement</th> | |
<th>descrizione</th> | |
</tr> | |
</thead> | |
<tbody><tr> | |
<td>q = Question(question_text=”What’s new?”, pub_date=timezone.now())</td> | |
<td>crea un nuovo oggetto Question</td> | |
</tr> | |
<tr> | |
<td>q.save()</td> | |
<td>salva sul DB (sincronizzazione esplicita)</td> | |
</tr> | |
<tr> | |
<td>Question.objects.all()</td> | |
<td>select di tutti gli elementi della tabella Question (se non è definito <code>__unicode__</code> (<code>__str__</code> in python 3) da una rappresentazione inutile,come quando non si fa l’override di toString() in JAVA)</td> | |
</tr> | |
<tr> | |
<td>Question.objects.get(id=1)</td> | |
<td>con <code>get</code> si possono recuperare <strong>singoli</strong> oggetti come una <code>WHERE</code> sugli attributi della classe</td> | |
</tr> | |
<tr> | |
<td>Question.objects.filter(<code><predicato></code>)</td> | |
<td>ritorna la lista degli oggetti filtrati sulla base del predicato</td> | |
</tr> | |
<tr> | |
<td>Question.objects.exclude(<code><predicato></code>)</td> | |
<td>ritorna la lista degli oggetti filtrati che non soddisfano il predicato di filtraggio</td> | |
</tr> | |
<tr> | |
<td>q.choice_set.all()</td> | |
<td>fa il join tra la tabella Question e la tabella Choice (relazione one-to-many)</td> | |
</tr> | |
<tr> | |
<td>c = q.choice_set.create(choice_text=’Just hacking again’, votes=0)</td> | |
<td>crea l’oggetto choice, lo aggiunge al set della question <code>q</code> (fa la insert) e lo ritorna</td> | |
</tr> | |
<tr> | |
<td>q.choice_set.count()</td> | |
<td>fa il count delle Choice associate a una Question</td> | |
</tr> | |
<tr> | |
<td>Choice.objects.filter(question__pub_date__year = current_year)</td> | |
<td></td> | |
</tr> | |
</tbody></table> | |
<p>I predicati di filtraggio sono nella forma <code><nomeCampo>__criterio=<valore></code>. Alcuni possibili predicati di filtraggio sono:</p> | |
<table> | |
<thead> | |
<tr> | |
<th>Criterio</th> | |
<th>Esempio</th> | |
</tr> | |
</thead> | |
<tbody><tr> | |
<td>Sottocampi</td> | |
<td>__year, __month</td> | |
</tr> | |
<tr> | |
<td>Substring</td> | |
<td>__startswith, __contains</td> | |
</tr> | |
<tr> | |
<td>Operatori di confronto</td> | |
<td>__gte, __gt</td> | |
</tr> | |
<tr> | |
<td>Uguaglianza</td> | |
<td>__exact</td> | |
</tr> | |
</tbody></table> | |
<h2 id="interfaccia-di-amministrazione"><strong>Interfaccia di amministrazione</strong></h2> | |
<p>Viene generata automaticamente da Django ed è pensata per gli amministratori dei siti e i content publisher. Permette i gestire tutti i modelli e i dati in essi contenuti. L’interfaccia è raggiungibile a </p> | |
<pre data-initialized="true" data-gclp-id="14"><code>127.0.0.1:8000/admin | |
</code></pre> | |
<p>La prima cosa da fare è creare un superuser Django:</p> | |
<pre data-initialized="true" data-gclp-id="15"><code>$ python manage.py createsuperuser | |
</code></pre> | |
<p>Inserire le info e rilanciare il server.</p> | |
<p>Per aggiungere un’ app alla pagine admin modifico il file <code><nome_app>/admin.py</code></p> | |
<pre data-initialized="true" data-gclp-id="16"><code>from .models import Question | |
admin.site.register(Question) | |
</code></pre> | |
<p>Ora basta ricaricare la pagina admin</p> | |
<h3 id="customizzare-linterfaccia-admin"><strong>Customizzare l’interfaccia admin</strong></h3> | |
<p>Si personalizza modificando il file <code>admin.py</code> dell’applicazione, creadno una classe che descriva come vogliamo personalizzare l’interfaccia del modello.</p> | |
<pre data-initialized="true" data-gclp-id="17"><code>#non funzia, capire perchè | |
class QuestionAdmin(admin.ModelAdmin): | |
fields = ['pub_date', 'question'] | |
admin.site.register(Question, QuestionAdmin) | |
</code></pre> | |
<p>Si possono raggruppare in sezioni i campi (comodo quando se ne hanno tanti)</p> | |
<pre data-initialized="true" data-gclp-id="18"><code>class QuestionAdmin(admin.ModelAdmin): | |
fieldsets = [ | |
(None, {'fields': ['question_text']}), | |
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), | |
] | |
admin.site.register(Question, QuestionAdmin) | |
</code></pre> | |
<p>E’ possibile aggiungere anche le info prese dalla tabella che andrebbe in join con Question, ovvero Choice, in modo da avere tutto inline, anche in inserimento.</p> | |
<pre data-initialized="true" data-gclp-id="19"><code>class ChoiceInline(admin.StackedInline): | |
model = Choice | |
# number of choices for each question | |
extra = 3 | |
</code></pre> | |
<p>Ulteriori customizzazioni possono essere fatte ad esempio per mostrare info aggiuntive e non solo quelle del metodo <code>__unicode__</code> che è ciò che viene fatto di default.</p> | |
<pre data-initialized="true" data-gclp-id="20"><code> class QuestionAdmin(admin.ModelAdmin): | |
# … | |
inlines = [ChoiceInline] | |
list_display = ('question_text', 'pub_date', 'was_published_recently') | |
</code></pre> | |
<p>All’interno di <code>list_display</code> si possono mettere sia attributi del modello che risultati di metodi. Per quanto riguarda i risultati di metodi, non è consentito l’ordinamento, ma si può aggiungere nel seguento modo:</p> | |
<pre data-initialized="true" data-gclp-id="21"><code>class Question(models.Model): | |
#... | |
# enable ordering | |
was_published_recently.admin_order_field = 'pub_date' | |
# show a symbol instead of True | |
was_published_recently.boolean = True | |
# deine the column name | |
was_published_recently.short_description = 'Published recently?' | |
</code></pre> | |
<p>Per specificare un opzione di filtraggio, aggiuntere al file <code>admin.py</code></p> | |
<pre data-initialized="true" data-gclp-id="22"><code>list_filter = ['pub_date'] #in class QuestionAdmin | |
</code></pre> | |
<p>Per le funzioni di ricerca invece, sempre in <code>class QuestionAdmin</code>:</p> | |
<pre data-initialized="true" data-gclp-id="23"><code>search_fields = ['question_text'] | |
# non funzia | |
date_hierarchy = 'pub_date' | |
</code></pre> | |
<h2 id="template-tutorial"><strong>Template tutorial</strong></h2> | |
<p>(modifichiamo la pagina admin) <br> | |
La prima cosa da fare è aggiungere il path delle cartella template, in modo che django sappia dove prendere i template. Modificare il file <code>settings.py</code></p> | |
<pre data-initialized="true" data-gclp-id="24"><code>TEMPLATES = [ | |
{ | |
'BACKEND': \ | |
'django.template.backends.django.DjangoTemplates', | |
'DIRS': [os.path.join(BASE_DIR, 'templates')], #nuova riga | |
'APP_DIRS': True, | |
…. | |
} | |
</code></pre> | |
<p>creare una cartella in template di nome admin e copiarci il template <code>base_site.html</code> che si trova in :</p> | |
<pre data-initialized="true" data-gclp-id="25"><code>/usr/local/lib/python2.7/dist-packages/django/contrib/admin/templates/admin/base_site.html | |
</code></pre> | |
<p>dopodichè si modifica il file. Ogni template Django può essere sovrascritto ini <br> | |
questo modo: copia dalla directory di default a quella del progetto e modifica del file.</p> | |
<p>I template-loader cercano i template all’interno della cartella template al cui interno sarà presente una cartella per ogni progetto/package Python. Se ‘APP_DIRS<code>è</code>True<code>all'interno di</code>settings.py`, si assume che ci sia una sotto-directory templates per ogni applicazione/package del progetto.</p> | |
<p>Sostanzialmente i template sono un insieme di blocchi del tipo: </p> | |
<pre data-initialized="true" data-gclp-id="26"><code>{% block NOMEBLOCCO %}{% endblock %} | |
</code></pre> | |
<p>Quando un template eredita da un altro, i blocchi ridefiniti vanno a sostituire la definizione originale dei blocchi.</p> | |
<h3 id="template-filtri"><strong>Template - filtri</strong></h3> | |
<p>E’ possibile manipolare l’output delle variabili mediante filtri. <br> | |
I filtri sono definiti attraverso l’uso di pipe:</p> | |
<pre data-initialized="true" data-gclp-id="27"><code>{{variabile | filtro}} | |
</code></pre> | |
<p>Alcuni filtri sono:</p> | |
<table> | |
<thead> | |
<tr> | |
<th>Filtro</th> | |
<th>Descrizione</th> | |
<th>esempio</th> | |
</tr> | |
</thead> | |
<tbody><tr> | |
<td>|default</td> | |
<td>se la variabile è falsa o vuota viene mostrato il parametro del filtro</td> | |
<td>{{ value | default:”nothing” }}</td> | |
</tr> | |
<tr> | |
<td>| length</td> | |
<td>mostra la lunghezza del dato</td> | |
<td></td> | |
</tr> | |
<tr> | |
<td>| join</td> | |
<td>separa con , e spazio</td> | |
<td>{{ list | join:”, ” }}</td> | |
</tr> | |
<tr> | |
<td>| lower</td> | |
<td></td> | |
<td></td> | |
</tr> | |
<tr> | |
<td>| upper</td> | |
<td></td> | |
<td></td> | |
</tr> | |
<tr> | |
<td>|capfirst</td> | |
<td></td> | |
<td></td> | |
</tr> | |
<tr> | |
<td>|add:X</td> | |
<td></td> | |
<td></td> | |
</tr> | |
<tr> | |
<td>|date:”M d, Y”</td> | |
<td></td> | |
<td>{{my_date|date:”Y-m-d”}}</td> | |
</tr> | |
<tr> | |
<td>|first, |last</td> | |
<td></td> | |
<td></td> | |
</tr> | |
<tr> | |
<td>|truncatechars:X, |truncatewords:X</td> | |
<td>mostra solo i primi X caratteri/parole</td> | |
<td></td> | |
</tr> | |
</tbody></table> | |
<h3 id="template-controllo-di-flusso"><strong>Template: controllo di flusso</strong></h3> | |
<p>Ereditarietà:</p> | |
<pre data-initialized="true" data-gclp-id="28"><code>{% extends “base.html” %} | |
</code></pre> | |
<p>Tag con costrutti condizionali if:</p> | |
<pre data-initialized="true" data-gclp-id="29"><code>{% if athlete_list %} | |
Number of athletes: {{ athlete_list|length }} | |
{% else %} | |
No athletes. | |
{% endif %} | |
{% if user.is_authenticated %} | |
Hello, {{ user.username }}. | |
{% endif %} | |
{% for story in story_list %} | |
<a href="{{ story.get_absolute_url }}"> | |
{{ story.headline|upper }} | |
</a> | |
{% endfor %} | |
</code></pre> | |
<h2 id="view-2"><strong>View</strong></h2> | |
<p>In django pagine Web e contenuti sono distribuiti attraverso il meccanismo delle view, implementate tramite una funzione python. Per associare un URL a una view, si esegue il mapping nel file <code>ulrs.py</code>. <br> | |
Gli URL sono nella forma :</p> | |
<pre data-initialized="true" data-gclp-id="30"><code> /newsarchive/<year>/<month>/ | |
</code></pre> | |
<p>dove year e month sono parametri. <br> | |
Ogni view è definita come funzione nel file <code>view.py</code>, e deve ritornare un oggetto di tipo HttpResponse</p> | |
<pre data-initialized="true" data-gclp-id="31"><code>from django.http import HttpResponse | |
def index(request): | |
return HttpResponse("You're at the poll index.") | |
</code></pre> | |
<p><code>request</code> consente di accedere ai dettagli della richiesta.</p> | |
<p>una volta mappati gli URL ad esempio:</p> | |
<pre data-initialized="true" data-gclp-id="32"><code>from django.conf.urls import url | |
from . import views | |
urlpatterns = [ | |
url(r'^$', views.index, name='index'), | |
] | |
</code></pre> | |
<p>dobbiamo integrare lo spazion di URL dell’ applicazione con quelli del progetto.</p> | |
<pre data-initialized="true" data-gclp-id="33"><code>from django.conf.urls import patterns, include, url | |
from django.contrib import admin | |
urlpatterns = [ | |
url(r'^polls/', include('polls.urls')), | |
url(r'^admin/', include(admin.site.urls)), | |
] | |
</code></pre> | |
<p>Ora andando alla pagina <code>localhost:8000/polls</code> otteniamo una risposta perchè:</p> | |
<p>Quando arriva la richiesta Django va a cercare il mapping di questo indirizzo nel file principale <code>urls.py</code> e capisce grazie a <code>r'^polls/'</code> che a tutte le pagine che iniziano con <code>polls</code> fanno match con con <code>polls.urls</code> quindi andrà a cercare il mapping del resto dell’ URL in questo file. la parte <code>polls</code> viene tolta perchè ha già fatto match in <code>mysite/urls.py</code> quindi ciò che rimane è una stringa vuota, che viene gestita nel file <code>polls/urls.py</code> grazie all’ espressione regolare <code>r'^$'</code> che fa match, appunto, con tutte le stringhe vuote.</p> | |
<h3 id="regular-expression-in-pythondjango"><em>* Regular expression in Python/Django*</em></h3> | |
<blockquote> | |
<p>Written with <a href="https://stackedit.io/">StackEdit</a>.</p> | |
</blockquote></div></body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment