/var/https://spapas.github.io/2023-11-29T15:20:00+02:00Various programming stuffA simple OpenID connect tutorial2023-11-29T15:20:00+02:002023-11-29T15:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2023-11-29:/2023/11/29/openid-connect-tutorial/<p>A simple tutorial for OpenID connect using only <span class="caps">HTTP</span> requests</p><h2 id="introduction">Introduction</h2>
<p>OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol. It allows clients to verify the identity of the user based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the user.</p>
<p>To learn more about the OpenID Connect protocol, you can read <a href="https://openid.net/developers/how-connect-works/">how it works</a> or, even better
if you want to get a deeper understanding, you can read the <a href="https://openid.net/specs/openid-connect-core-1_0.html">specification</a>. </p>
<p>The main difference between OpenID Connect and OAuth 2.0 is that OpenID Connect is an authentication protocol, while OAuth 2.0 is an authorization protocol; this means that OpenID Connect is used to verify the identity of the user, while OAuth 2.0 is used to verify
if the user has access to some resources (and if so also very his identity). </p>
<p>In the following tutorial we’ll try to authenticate a user using OpenID Connect without any external libraries so we can understand how the authentication works. We’ll mainly work from the client side (i.e the web application) but we’ll also be able to understand how the authentication server should work.</p>
<p>We’ll use python and the requests library to make the <span class="caps">HTTP</span> requests however you can use any language you want, even command line with curl. To decode the <span class="caps">JWT</span> tokens we’ll use the python cryptography library.</p>
<h2 id="authentication-flow">Authentication flow</h2>
<p>Some terminology</p>
<ul>
<li>user: The user that tries to log in to the client application</li>
<li>client / client application: The application that the user tries to log in to; this is be a server side web application (however acts as a client for the OpenID Connect protocol)</li>
<li>server / authorization server: The server that authenticates the user and returns an id token and optionally an access token. This is the server that implements the OpenID Connect protocol.</li>
</ul>
<p>The authentication flow for OpenID connect is more or less the following:</p>
<ul>
<li>The user tries to “log in” on the client application</li>
<li>The client application generates a <span class="caps">URL</span> that points to the authorization endpoint of the authorization server adding some parameters to that url query and <em>redirects</em> the user’s browser there</li>
<li>The user will get a log in screen and try to log in to the authorization server (or if he’s already logged in he doesn’t need to do anything)</li>
<li>The authorization server redirects the user user back to the client application using a pre-agreed redirect url and passing it an authorization code</li>
<li>The client application retrieves the authorization code through the redirect url (callback) and sends a request to the token endpoint of the authorization server passing the authorization code and the client secret</li>
<li>The authorization server responds with an id token and optionally an access token</li>
<li>The client application can now use the id token to read the user information and optionally the access token to access more resources (if available)</li>
</ul>
<p>This is also explained in the section 1.3. Overview of the <a href="https://openid.net/specs/openid-connect-core-1_0.html#Overview">OpenID Connect specification</a>.</p>
<h2 id="authentication-server">Authentication server</h2>
<p>For the Authentication server for our tutorial we’ll use <a href="https://www.keycloak.org/">Keycloak</a>, an open source Identity and Access Management server that implements OpenID Connect. I won’t go into details on how to install and configure Keycloak,
the thing is that you need to setup a new client for your realm, enable authentication and add a client secret that will be used later.</p>
<p>Let’s suppose that you have created a realm with the name <code>sample-realm</code> and your keycloak server is hosted on <code>https://kc.example.com</code>. This realm will have a <em>base</em> url with the value: <code>https://kc.example.gr/realms/sample-realm/</code>. This base url will be used to build other urls and will be stored as <code>OIDC_BASE_PROVIDER_URL</code>.</p>
<p>If you are using a different OpenID Connect server, you’ll need to have the client id and client secret for your server and the token and authorization endpoints of your server.</p>
<h2 id="automatic-discovery">Automatic Discovery</h2>
<p>Some OpenID Connect servers support automatic discovery of various information from authorization server (including the needed endpoints). This is called <code>WebFinger</code> and is described in detail in the <a href="https://openid.net/specs/openid-connect-discovery-1_0.html">discovery specification</a>. </p>
<p>To use the automatic discovery you can send a <span class="caps">GET</span> request to the <code>/.well-known/openid-configuration</code> endpoint of the authorization server. In our keycloak case, the endpoint will be
<code>https://kc.example.gr/realms/sample-realm/.well-known/openid-configuration</code>. The response is a <span class="caps">JSON</span> object that contains the endpoints of the authorization server. For example, using python requests:</p>
<div class="highlight"><pre><span></span><code><span class="n">finger</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<span class="n">settings</span><span class="o">.</span><span class="n">OIDC_BASE_PROVIDER_URL</span> <span class="o">+</span> <span class="s2">".well-known/openid-configuration"</span>
<span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</code></pre></div>
<p>This may return a lot of info but you are basically interested in the following:</p>
<div class="highlight"><pre><span></span><code><span class="n">authorization_endpoint</span> <span class="o">=</span> <span class="n">finger</span><span class="p">[</span><span class="s2">"authorization_endpoint"</span><span class="p">]</span>
<span class="n">token_endpoint</span> <span class="o">=</span> <span class="n">finger</span><span class="p">[</span><span class="s2">"token_endpoint"</span><span class="p">]</span>
</code></pre></div>
<h2 id="the-authentication-url">The Authentication url</h2>
<p>The first step of the authentication flow is to generate an authentication url and redirect the user to that. This means that when the user tries to log in to your app (by clicking a button or visiting some url etc) your web application will redirect the user to the authentication url.</p>
<p>The authentication url is the authorization endpoint of the authorization server with some parameters. The parameters are
(see 3.1.2.1. Authentication Request of the <a href="https://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect specification</a> for more info if you want):</p>
<ul>
<li><code>client_id</code>: The client id of your application. This will be used by the authorization server to identify your application</li>
<li><code>response_type</code>: The response type, in our case it’s <code>code</code>. There are various response types that can be used however <code>code</code> is the one that should be used to initiate the <em>Authorization Code Flow</em> which is used for server-side applications (there are two more flows described in the sections 3.2 and 3.3 but they aren’t used for traditional web applications).</li>
<li><code>scope</code>: The scope of the request, in our case it has to contain <code>openid</code> (it can also have more scopes but <code>openid</code> is the minimum)</li>
<li><code>redirect_uri</code>: The redirect uri of your application, in our case it’s <code>http://localhost:8000/auth/callback</code>. This is the url that the authorization server will redirect the user after the authentication is complete. Please notice that the authentication server will only redirect the user to this url if it’s in the list of allowed redirect uris of your application so make sure that you have added it to the list of allowed redirect uris.</li>
<li><code>state</code>: A random string that will be used to verify the response from the authorization server</li>
</ul>
<p>So, to create the url from python we’ll do something like: </p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">secrets</span> <span class="kn">import</span> <span class="n">choice</span>
<span class="k">def</span> <span class="nf">rndstr</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="mi">16</span><span class="p">):</span>
<span class="c1"># Pick a random string of size `size` from the alphabet</span>
<span class="n">alphabet</span> <span class="o">=</span> <span class="n">string</span><span class="o">.</span><span class="n">ascii_letters</span> <span class="o">+</span> <span class="n">string</span><span class="o">.</span><span class="n">digits</span>
<span class="k">return</span> <span class="s2">""</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">choice</span><span class="p">(</span><span class="n">alphabet</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">size</span><span class="p">)])</span>
<span class="n">state</span> <span class="o">=</span> <span class="n">rndstr</span><span class="p">()</span>
<span class="n">authorization_url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">authorization_endpoint</span><span class="si">}</span><span class="s2">?client_id=</span><span class="si">{</span><span class="n">settings</span><span class="o">.</span><span class="n">OIDC_CLIENT_ID</span><span class="si">}</span><span class="s2">&response_type=code&scope=openid&redirect_uri=</span><span class="si">{</span><span class="n">settings</span><span class="o">.</span><span class="n">OIDC_REDIRECT_URIS</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">&state=</span><span class="si">{</span><span class="n">state</span><span class="si">}</span><span class="s2">"</span>
</code></pre></div>
<p>and the uri will be similar to:</p>
<div class="highlight"><pre><span></span><code><span class="nx">https</span><span class="p">:</span><span class="c1">//kc.example.gr/realms/sample-realm/protocol/openid-connect/auth?client_id=sample-client&response_type=code&scope=openid&redirect_uri=http://localhost:8000/auth/callback&state=skldfj98sdfjio12</span>
</code></pre></div>
<h2 id="user-authentication-and-redirect">User Authentication and redirect</h2>
<p>Our application will now redirect the user to the authentication url. To test for this tutorial, we’ll just copy paste the authentication url on our browser; the user will need to log in to the authorization server and then he’ll be redirected back to our redirect_uri we provided with an authorization code. </p>
<p>If we don’t have anything running on localhost:8000 we’ll get an error but we’ll be able to see the redict <span class="caps">URL</span> on our browser’s bar(!). So, we’ll see something like <code>http://localhost:8000/auth/callback?state=KpT23RpwimxzXzHa&session_state=d54e38e1-8dfa-4658-ab4e-817ef6d4029b&code=b4938b28-12b4-463b-b2bc-ffc91b29e79e.d54e38e1-8dfa-4658-ab4e-817ef6d4029b.f9302f16-4765-4f2a-b7bc-381208ec71d6</code>. There are three parameters that are returned:</p>
<ul>
<li><code>code</code>: The authorization code. This is the code that we’ll use to get the id token.</li>
<li><code>state</code>: The state we provided earlier. We need to check that this is the same as the one we provided earlier to make sure that the response is valid.</li>
<li><code>session_state</code>: The session state. This is optional and can be used to check if the user is still logged in to the authorization server. We won’t use this in our tutorial but if you want to learn more you can read the <a href="https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions">session specification</a>.</li>
</ul>
<div class="highlight"><pre><span></span><code><span class="n">query</span> <span class="o">=</span> <span class="n">urlparse</span><span class="p">(</span><span class="n">next_url</span><span class="p">)</span><span class="o">.</span><span class="n">query</span>
<span class="n">query_dict</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">parse_qs</span><span class="p">(</span><span class="n">query</span><span class="p">)</span><span class="o">.</span><span class="n">items</span><span class="p">()}</span>
</code></pre></div>
<p>Now query_dict will contain all the parameters of the url as a dictionary. So we can check that we got the correct state:</p>
<div class="highlight"><pre><span></span><code><span class="k">assert</span> <span class="n">query_dict</span><span class="p">[</span><span class="s2">"state"</span><span class="p">]</span> <span class="o">==</span> <span class="n">state</span>
</code></pre></div>
<h2 id="getting-the-token">Getting the token</h2>
<p>The next step is for our web application to retrieve
the id token. For that it’ll need to send a <span class="caps">POST</span> request to the token endpoint of the authorization server passing it the following parameters (see 3.1.3.1. Token Request of the <a href="https://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect specification</a>):</p>
<ul>
<li><code>client_id</code>: The client id of your application</li>
<li><code>client_secret</code>: The client secret of your application</li>
<li><code>grant_type</code>: The grant type, in our case it must be <code>authorization_code</code> .</li>
<li><code>code</code>: The authorization code we got from the authorization server</li>
<li><code>redirect_uri</code>: The redirect uri we used before</li>
</ul>
<p>Here’s the request we need to do. Please notice that this request will be done by your server-side application and not by the user’s browser (so the client secret will be safe).</p>
<div class="highlight"><pre><span></span><code><span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">post</span><span class="p">(</span>
<span class="n">token_endpoint</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="p">{</span>
<span class="s2">"client_id"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">OIDC_CLIENT_ID</span><span class="p">,</span>
<span class="s2">"client_secret"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">OIDC_CLIENT_SECRET</span><span class="p">,</span>
<span class="s2">"grant_type"</span><span class="p">:</span> <span class="s2">"authorization_code"</span><span class="p">,</span>
<span class="s2">"code"</span><span class="p">:</span> <span class="n">query_dict</span><span class="p">[</span><span class="s2">"code"</span><span class="p">],</span>
<span class="s2">"redirect_uri"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">OIDC_REDIRECT_URIS</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="p">},</span>
<span class="p">)</span>
</code></pre></div>
<p>The above response will return a bunch of parameters depending on the authentication server. The most important of these parameters are:</p>
<ul>
<li><code>id_token</code>: The id token that contains information about the user. This is a <span class="caps">JWT</span> token and needs to be decoded and verified. We’ll see how to do that later.</li>
<li><code>access_token</code>: The access token that can be used to access the user information.</li>
<li><code>token_type</code>: This has the <code>Bearer</code> value</li>
</ul>
<h2 id="decoding-a-jwt-json-web-token">Decoding a <span class="caps">JWT</span> (<span class="caps">JSON</span> Web Token)</h2>
<p>Although there are libraries that can be used for decoding and verifying the token (<a href="https://github.com/jpadilla/pyjwt">pyjwt</a>), we’ll do it using only python (and <a href="https://github.com/pyca/cryptography/">the cryptography library</a>) to understand how it works.</p>
<p>The <span class="caps">JWT</span> token is a string that contains three parts separated by a dot (<code>.</code>). The first part is the header, the second part is the payload and the third part is the signature. Each part is base64 encoded. To decode the token we can use the following function:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">decode_jwt</span><span class="p">(</span><span class="n">jwt_token</span><span class="p">):</span>
<span class="n">header</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">signature</span> <span class="o">=</span> <span class="n">jwt_token</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span>
<span class="n">decoded_header</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">urlsafe_b64decode</span><span class="p">(</span><span class="n">header</span> <span class="o">+</span> <span class="s2">"="</span> <span class="o">*</span> <span class="p">(</span><span class="o">-</span><span class="nb">len</span><span class="p">(</span><span class="n">header</span><span class="p">)</span> <span class="o">%</span> <span class="mi">4</span><span class="p">))</span>
<span class="n">decoded_payload</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">urlsafe_b64decode</span><span class="p">(</span><span class="n">payload</span> <span class="o">+</span> <span class="s2">"="</span> <span class="o">*</span> <span class="p">(</span><span class="o">-</span><span class="nb">len</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span> <span class="o">%</span> <span class="mi">4</span><span class="p">))</span>
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">decoded_header</span><span class="p">),</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">decoded_payload</span><span class="p">)</span>
</code></pre></div>
<p>The signature will be used later to verify the token.</p>
<p>The decoded header will be similar to this </p>
<div class="highlight"><pre><span></span><code><span class="p">{</span><span class="s1">'alg'</span><span class="p">:</span> <span class="s1">'RS256'</span><span class="p">,</span>
<span class="s1">'kid'</span><span class="p">:</span> <span class="s1">'NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg'</span><span class="p">,</span>
<span class="s1">'typ'</span><span class="p">:</span> <span class="s1">'JWT'</span><span class="p">}</span>
</code></pre></div>
<p>defining the type as <span class="caps">JWT</span>, the algorithm used to sing the <span class="caps">JWT</span> (<span class="caps">RS256</span>) and the public key that was used for the signing (we’ll see later how we can retrieve that public key).</p>
<p>The decoded payload may have various fields depending on the authentication server. The most important fields are (see
2. <span class="caps">ID</span> Token of the <a href="https://openid.net/specs/openid-connect-core-1_0.html#IDToken">OpenID Connect specification</a>):</p>
<ul>
<li><code>iss</code>: The issuer of the token. This <em>must</em> be the same as the <code>OIDC_BASE_PROVIDER_URL</code> we defined earlier.</li>
<li><code>sub</code>: The subject of the token. This is more or less a unique id for that particular user that identified. Please notice that this usually is not the username of the user but some internal and unique key of that user.</li>
<li><code>aud</code>: The audience of the token. This <em>must</em> contain the <code>OIDC_CLIENT_ID</code> we defined earlier (i.e sample-client).</li>
<li><code>exp</code>: Expiration time of the <span class="caps">JWT</span> (in seconds since epoch)</li>
<li><code>iat</code>: The time the <span class="caps">JWT</span> was issued (in seconds since epoch)</li>
<li><code>auth_time</code>: The time the user was authenticated (in seconds since epoch)</li>
</ul>
<p>For example the decoded payload may be something like:</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="s1">'aud'</span><span class="p">:</span> <span class="s1">'sample-client'</span><span class="p">,</span>
<span class="s1">'auth_time'</span><span class="p">:</span> <span class="mi">1701168800</span><span class="p">,</span>
<span class="s1">'exp'</span><span class="p">:</span> <span class="mi">1701169100</span><span class="p">,</span>
<span class="s1">'iat'</span><span class="p">:</span> <span class="mi">1701168800</span><span class="p">,</span>
<span class="s1">'iss'</span><span class="p">:</span> <span class="s1">'https://kc.example.gr/realms/sample-realm'</span><span class="p">,</span>
<span class="s1">'email'</span><span class="p">:</span> <span class="s1">'sample@example.com'</span><span class="p">,</span>
<span class="s1">'preferred_username'</span><span class="p">:</span> <span class="s1">'sample'</span><span class="p">,</span>
<span class="s1">'name'</span><span class="p">:</span> <span class="s1">'Sample'</span><span class="p">,</span>
<span class="s1">'sub'</span><span class="p">:</span> <span class="s1">'e22f5a0d-e5ac-472d-b41b-06ecb9e4b3f6'</span><span class="p">,</span>
<span class="s1">'typ'</span><span class="p">:</span> <span class="s1">'ID'</span>
<span class="p">}</span>
</code></pre></div>
<p>As you see the payload may contain some extra information about the user (like the email, name etc). This information is not guaranteed to be there and it depends on the authentication server. If the information you want is there you can
<em>verify the <span class="caps">JWT</span></em> and finish the authentication flow here.</p>
<p>If not you can call the <em>userinfo endpoint</em> with the help of the <code>access_token</code> to get more information about the user.</p>
<h2 id="retrieving-the-public-key">Retrieving the public key</h2>
<p>The <span class="caps">JWT</span> token is signed by the authentication server using a private key. To verify the token we need to get the public key of the authentication server and use it to verify the signature of the token. </p>
<p>There are multiple ways to retrieve the public key. The simplest is to get it directly from the authentication server (i.e keycloak has an option to export the <span class="caps">RS256</span> public key as text from the realm settings - keys of your realm). Also, if you visit the base url we defined later you’ll get a <span class="caps">JSON</span> with the public key of the server i.e</p>
<div class="highlight"><pre><span></span><code><span class="n">base_info</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">OIDC_BASE_PROVIDER_URL</span><span class="p">)</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="n">base_info</span><span class="p">[</span><span class="s2">"public_key"</span><span class="p">]</span>
</code></pre></div>
<p>I’m not sure if this is supported by other server beyond keycloak though.</p>
<p>Finally you can use the <code>jwks_uri</code> of the authentication server to get the public key. The jwks_uri would be returned from the WebFinger response we described earlier. There you’ll see a <span class="caps">JSON</span> object named <code>keys</code> that contains <span class="caps">JSON</span> array with the list of the keys that the server has.</p>
<p>Each key will have the following fields:</p>
<ul>
<li><code>kid</code>: The key id; this can be used to correlate the key with the one in the header of the <span class="caps">JWT</span> token</li>
<li><code>kty</code>: The key type; this should be <code>RSA</code></li>
<li><code>alg</code>: The algorithm used to sign the <span class="caps">JWT</span> token; this should be <code>RS256</code></li>
<li><code>use</code>: The use of the key; this should be <code>sig</code> (for signing)</li>
<li><code>n</code>: The modulus of the <span class="caps">RSA</span> key</li>
<li><code>e</code>: The exponent of the <span class="caps">RSA</span> key</li>
<li><code>x5c</code>: The x509 certificate chain of the key</li>
<li><code>x5t</code>: The x509 certificate <span class="caps">SHA</span>-1 thumbprint of the key</li>
<li><code>x5t#S256</code>: The x509 certificate <span class="caps">SHA</span>-256 thumbprint of the key</li>
</ul>
<p>Although the public key isn’t there it is rather straightforward to produce it either from the <code>n</code> and <code>e</code> or by reading the x509 certificate through the <code>x5c</code> field. </p>
<p>Since we have now entered the cryptography fields we <em>need</em> to use the python cryptography library.</p>
<p>If we have the public key as a string we can use the following function to get the public key:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">cryptography.hazmat.backends</span> <span class="kn">import</span> <span class="n">default_backend</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.primitives</span> <span class="kn">import</span> <span class="n">serialization</span>
<span class="n">public_key_str</span> <span class="o">=</span> <span class="s2">"-----BEGIN PUBLIC KEY-----</span><span class="se">\n</span><span class="si">{0}</span><span class="se">\n</span><span class="s2">-----END PUBLIC KEY-----"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">settings</span><span class="o">.</span><span class="n">OIDC_PUBLIC_KEY_STR</span>
<span class="p">)</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="n">default_backend</span><span class="p">()</span><span class="o">.</span><span class="n">load_pem_public_key</span><span class="p">(</span><span class="n">public_key_str</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
<span class="n">public_key_pem</span> <span class="o">=</span> <span class="n">public_key</span><span class="o">.</span><span class="n">public_bytes</span><span class="p">(</span>
<span class="n">encoding</span><span class="o">=</span><span class="n">serialization</span><span class="o">.</span><span class="n">Encoding</span><span class="o">.</span><span class="n">PEM</span><span class="p">,</span>
<span class="nb">format</span><span class="o">=</span><span class="n">serialization</span><span class="o">.</span><span class="n">PublicFormat</span><span class="o">.</span><span class="n">SubjectPublicKeyInfo</span>
<span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">public_key_pem</span><span class="o">.</span><span class="n">decode</span><span class="p">())</span>
</code></pre></div>
<p>If we want to produce the public key using the <code>n</code> and <code>e</code> fields we can use the following function:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.primitives.asymmetric</span> <span class="kn">import</span> <span class="n">rsa</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.backends</span> <span class="kn">import</span> <span class="n">default_backend</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.primitives</span> <span class="kn">import</span> <span class="n">serialization</span>
<span class="n">n_bytes</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">urlsafe_b64decode</span><span class="p">(</span><span class="n">key</span><span class="p">[</span><span class="s1">'n'</span><span class="p">]</span> <span class="o">+</span> <span class="s2">"==="</span><span class="p">)</span>
<span class="n">n</span> <span class="o">=</span> <span class="nb">int</span><span class="o">.</span><span class="n">from_bytes</span><span class="p">(</span><span class="n">n_bytes</span><span class="p">,</span> <span class="s1">'big'</span><span class="p">)</span>
<span class="n">e_bytes</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">urlsafe_b64decode</span><span class="p">(</span><span class="n">key</span><span class="p">[</span><span class="s1">'e'</span><span class="p">]</span> <span class="o">+</span> <span class="s2">"==="</span><span class="p">)</span>
<span class="n">e</span> <span class="o">=</span> <span class="nb">int</span><span class="o">.</span><span class="n">from_bytes</span><span class="p">(</span><span class="n">e_bytes</span><span class="p">,</span> <span class="s1">'big'</span><span class="p">)</span>
<span class="n">public_key</span> <span class="o">=</span> <span class="n">rsa</span><span class="o">.</span><span class="n">RSAPublicNumbers</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="n">n</span><span class="p">)</span><span class="o">.</span><span class="n">public_key</span><span class="p">(</span><span class="n">default_backend</span><span class="p">())</span>
<span class="n">public_key_pem</span> <span class="o">=</span> <span class="n">public_key</span><span class="o">.</span><span class="n">public_bytes</span><span class="p">(</span>
<span class="n">encoding</span><span class="o">=</span><span class="n">serialization</span><span class="o">.</span><span class="n">Encoding</span><span class="o">.</span><span class="n">PEM</span><span class="p">,</span>
<span class="nb">format</span><span class="o">=</span><span class="n">serialization</span><span class="o">.</span><span class="n">PublicFormat</span><span class="o">.</span><span class="n">SubjectPublicKeyInfo</span>
<span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">public_key_pem</span><span class="o">.</span><span class="n">decode</span><span class="p">())</span>
</code></pre></div>
<p>Or, to get the public key from the x509 certificate:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">cryptography</span> <span class="kn">import</span> <span class="n">x509</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.backends</span> <span class="kn">import</span> <span class="n">default_backend</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.primitives</span> <span class="kn">import</span> <span class="n">serialization</span>
<span class="n">certificate_str</span> <span class="o">=</span> <span class="n">key</span><span class="p">[</span><span class="s2">"x5c"</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
<span class="n">decoded_certificate_str</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">certificate_str</span><span class="p">)</span>
<span class="n">certificate</span> <span class="o">=</span> <span class="n">x509</span><span class="o">.</span><span class="n">load_der_x509_certificate</span><span class="p">(</span><span class="n">decoded_certificate_str</span><span class="p">,</span> <span class="n">default_backend</span><span class="p">())</span>
<span class="n">pub_key</span> <span class="o">=</span> <span class="n">certificate</span><span class="o">.</span><span class="n">public_key</span><span class="p">()</span>
<span class="n">public_key_pem</span> <span class="o">=</span> <span class="n">pub_key</span><span class="o">.</span><span class="n">public_bytes</span><span class="p">(</span>
<span class="n">encoding</span><span class="o">=</span><span class="n">serialization</span><span class="o">.</span><span class="n">Encoding</span><span class="o">.</span><span class="n">PEM</span><span class="p">,</span>
<span class="nb">format</span><span class="o">=</span><span class="n">serialization</span><span class="o">.</span><span class="n">PublicFormat</span><span class="o">.</span><span class="n">SubjectPublicKeyInfo</span>
<span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="n">public_key_pem</span><span class="o">.</span><span class="n">decode</span><span class="p">())</span>
</code></pre></div>
<p>In all three above cases we should see the <em>same</em> public key. </p>
<h2 id="verifying-the-jwt-token">Verifying the <span class="caps">JWT</span> token</h2>
<p>Finally after retrieving the public key (using any of the above methods) we can verify the <span class="caps">JWT</span> token. </p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">cryptography.hazmat.primitives</span> <span class="kn">import</span> <span class="n">hashes</span>
<span class="kn">from</span> <span class="nn">cryptography.hazmat.primitives.asymmetric</span> <span class="kn">import</span> <span class="n">padding</span>
<span class="k">def</span> <span class="nf">verify_rs256_signature</span><span class="p">(</span><span class="n">jwt_token</span><span class="p">,</span> <span class="n">pub_key</span><span class="p">):</span>
<span class="n">header</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">signature</span> <span class="o">=</span> <span class="n">jwt_token</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">)</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">(</span><span class="n">header</span> <span class="o">+</span> <span class="s2">"."</span> <span class="o">+</span> <span class="n">payload</span><span class="p">)</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
<span class="n">decoded_signature</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">urlsafe_b64decode</span><span class="p">(</span>
<span class="n">signature</span> <span class="o">+</span> <span class="s2">"="</span> <span class="o">*</span> <span class="p">(</span><span class="o">-</span><span class="nb">len</span><span class="p">(</span><span class="n">signature</span><span class="p">)</span> <span class="o">%</span> <span class="mi">4</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">pub_key</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span>
<span class="n">signature</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">padding</span><span class="o">.</span><span class="n">PKCS1v15</span><span class="p">(),</span> <span class="n">hashes</span><span class="o">.</span><span class="n">SHA256</span><span class="p">()</span>
<span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Signature verified"</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Signature verification failed!"</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
</code></pre></div>
<p>And call it like:</p>
<div class="highlight"><pre><span></span><code><span class="n">verify_rs256_signature</span><span class="p">(</span><span class="n">id_token</span><span class="p">,</span> <span class="n">pub_key</span><span class="p">)</span>
</code></pre></div>
<p>Where the id_token is the <span class="caps">JWT</span> token we got earlier from the token endpoint (i.e <code>id_token = resp.json()["id_token"]</code>) and the pub_key is the public key object.</p>
<p>Notice that beyond the signature verification we also need to verify that the token has valid values the <code>iss</code> and <code>aud</code> fields and also that it has valid times
(i.e not expired, not issued in the future etc). We can do that using the <code>exp</code>, <code>iat</code> and <code>auth_time</code> fields of the payload. These times are in <span class="caps">UTC</span> and in seconds since epoch. So we can do something like:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">datetime</span>
<span class="n">exp_datetime</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">utcfromtimestamp</span><span class="p">(</span><span class="n">decoded_payload</span><span class="p">[</span><span class="s1">'exp'</span><span class="p">])</span>
</code></pre></div>
<p>to convert them to datetime objects and then compare them with the current time (in <span class="caps">UTC</span>). </p>
<p>After the id token is verified we can safely use the information contained in the payload to authenticate the user. If this information is enough we can finish the authentication flow here. If not we can use the access token to call the userinfo endpoint.</p>
<h2 id="calling-the-userinfo-endpoint">Calling the userinfo endpoint</h2>
<p>Calling the userinfo endpoint is rather straightforward. We just need to send a <span class="caps">GET</span> request to the userinfo endpoint of the authentication server passing it the access token as a bearer token. The userinfo endpoint is returned from the WebFinger response we described earlier. </p>
<div class="highlight"><pre><span></span><code><span class="n">userinfo_endpoint</span> <span class="o">=</span> <span class="n">finger</span><span class="p">[</span><span class="s2">"userinfo_endpoint"</span><span class="p">]</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<span class="n">userinfo_endpoint</span><span class="p">,</span>
<span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">"Authorization"</span><span class="p">:</span> <span class="sa">f</span><span class="s2">"Bearer </span><span class="si">{</span><span class="n">access_token</span><span class="si">}</span><span class="s2">"</span><span class="p">},</span>
<span class="p">)</span>
</code></pre></div>
<p>The resp may be also a <span class="caps">JWT</span> token so you’ll need to decode and verify it as we did before. If it’s not a <span class="caps">JWT</span> token it’ll be a <span class="caps">JSON</span> object with the user information. Notice that it may not have more information than the id token so could avoid calling the userinfo endpoint. However this depends on the implementation so you probably should check it yourself.</p>
<h2 id="what-about-the-authorization-server">What about the authorization server?</h2>
<p>What we would need to do if we wanted to implement the authorization server? It isn’t so complicated:</p>
<p>We’ll need to store the users with their passwords and the clients with their secrets and redirect urls. Then we’d need to implement the following urls:</p>
<ul>
<li><code>/login</code>: A user facing login page. The client application will redirect the user’s browser to that page. After the user connects the server would redirect the user’s browser to the redirect url of the client passing it an authorization code.</li>
<li><code>/token</code>: The client would issue a post request to that url passing the authorization code and the client secret. The server now would return the id token encoded as a jwt token. The id token would contain the user information.</li>
</ul>
<p>These two are more or less enough for a simple authentication server. Of course there are more things that can be implemented like the userinfo endpoint, the fingering etc but these are optional.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this tutorial we saw how we can authenticate a user using OpenID Connect without any external libraries. We saw how we can generate the authentication url, how we can get the authorization code and how we can use the authorization code to get the id token and access token. We also saw how we can decode and verify the id token, how we can retrieve the public key of the authorization server and finally and how we can use the access token to call the userinfo endpoint.</p>Simple Django - DataTables integration2023-05-23T13:20:00+03:002023-05-23T13:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2023-05-23:/2023/05/23/simple-django-datatables-integration/<p>Integration of jquery DataTables with Django</p><p>In this small post I’ll show a simple and quick way to integrate
the <a href="https://datatables.net/">jquery DataTables library</a> with Django.</p>
<p>The DataTables library has a lot of features however in this article we’ll only
take advantage of the basic features that should be enough for most use cases:</p>
<ul>
<li>Ajax loading of data</li>
<li>Ajax pagination</li>
<li>Ajax search/filtering</li>
</ul>
<p>If you want to use more features you can try a django package like
<a href="https://github.com/morlandi/django-ajax-datatable">django-ajax-datatable</a> that
supports most of the DataTables features; however because of the DataTables complexity you’ll see that it will need a lot of work even for a simple integration.</p>
<p>The following model will be used for our example:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">Item</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">code</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="p">)</span>
<span class="n">name_en</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="p">)</span>
</code></pre></div>
<p>We’ll create a Django class based view that will return a template with an empty <code><table></code> element that we’ll then fill with Ajax. The same view will check if it receives datatables requests and return the correct data in the format datatables expects:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.db.models</span> <span class="kn">import</span> <span class="n">Q</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">JsonResponse</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">ListView</span>
<span class="k">class</span> <span class="nc">ItemListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Item</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s2">"item_datatable.html"</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"datatables"</span><span class="p">):</span>
<span class="n">draw</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"draw"</span><span class="p">,</span> <span class="s2">"1"</span><span class="p">))</span>
<span class="n">length</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"length"</span><span class="p">,</span> <span class="s2">"10"</span><span class="p">))</span>
<span class="n">start</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"start"</span><span class="p">,</span> <span class="s2">"0"</span><span class="p">))</span>
<span class="n">sv</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"search[value]"</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="n">qs</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s2">"code"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">sv</span><span class="p">:</span>
<span class="n">qs</span> <span class="o">=</span> <span class="n">qs</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">Q</span><span class="p">(</span><span class="n">name__icontains</span><span class="o">=</span><span class="n">sv</span><span class="p">)</span>
<span class="o">|</span> <span class="n">Q</span><span class="p">(</span><span class="n">code__icontains</span><span class="o">=</span><span class="n">sv</span><span class="p">)</span>
<span class="o">|</span> <span class="n">Q</span><span class="p">(</span><span class="n">name_en__icontains</span><span class="o">=</span><span class="n">sv</span><span class="p">)</span>
<span class="p">)</span>
<span class="n">filtered_count</span> <span class="o">=</span> <span class="n">qs</span><span class="o">.</span><span class="n">count</span><span class="p">()</span>
<span class="n">qs</span> <span class="o">=</span> <span class="n">qs</span><span class="p">[</span><span class="n">start</span> <span class="p">:</span> <span class="n">start</span> <span class="o">+</span> <span class="n">length</span><span class="p">]</span>
<span class="k">return</span> <span class="n">JsonResponse</span><span class="p">(</span>
<span class="p">{</span>
<span class="s2">"recordsTotal"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span><span class="o">.</span><span class="n">count</span><span class="p">(),</span>
<span class="s2">"recordsFiltered"</span><span class="p">:</span> <span class="n">filtered_count</span><span class="p">,</span>
<span class="s2">"draw"</span><span class="p">:</span> <span class="n">draw</span><span class="p">,</span>
<span class="s2">"data"</span><span class="p">:</span> <span class="nb">list</span><span class="p">(</span><span class="n">qs</span><span class="o">.</span><span class="n">values</span><span class="p">()),</span>
<span class="p">},</span>
<span class="n">safe</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">)</span>
</code></pre></div>
<p>The above is a simple <code>ListView</code> for our <code>Item</code> model. It overrides the <code>render_to_response</code> method to return the Ajax json data if the request is a datatables request. To do that, it first checks to see if there’s a <code>datatables</code> parameter in the <code>request.GET</code>. If this isn’t a datatables request it will return the normal template response.</p>
<p>However, if it is a datatables request it will pick the
<code>draw</code>,
<code>length</code>, <code>start</code> and <code>search[value]</code> parameters from the <code>request.GET</code> (with default values if they aren’t there) and use them to prepare the response. Notice that:</p>
<ul>
<li>for the filter we need to do an <code>OR (|)</code> because of how the datatables default fildering works (one single filter field for all columns)</li>
<li>we can select any of the fields we want to filter with by adding them to the <code>OR</code> expression</li>
<li>we’ll choose the correct page using qs[start:start+length], the length will be changed if the user uses the page-size field of the datatables</li>
<li>we need to count the filtered results <em>before</em> taking the slice or else Django will throw an error</li>
<li>the <code>draw</code> parameter should be converted to integer and passed back to the response (it is used in case there are multiple pending datatable ajax requests).</li>
</ul>
<p>Finally, we return a <code>JsonResponse</code> with the correct data. The <code>safe=False</code> is needed because we are returning a list of dictionaries and not a single dictionary. Notice the <code>recordsTotal</code> and <code>recordsFiltered</code> keys; these are needed by datatables to know how many records there are in total and how many records are returned after filtering.</p>
<p>The <code>item_datatable.html</code> template for this view is the following: </p>
<div class="highlight"><pre><span></span><code>{% extends "site_base.html" %}
{% load static %}
{% block extra_style %}
<span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"stylesheet"</span> <span class="na">type</span><span class="o">=</span><span class="s">"text/css"</span> <span class="na">href</span><span class="o">=</span><span class="s">"{% static 'datatables.min.css' %}"</span><span class="p">/></span>
{% endblock %}
{% block page_content %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"row"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"col-md-12"</span><span class="p">></span>
<span class="p"><</span><span class="nt">table</span> <span class="na">id</span><span class="o">=</span><span class="s">'table'</span> <span class="na">class</span><span class="o">=</span><span class="s">'table'</span><span class="p">></</span><span class="nt">table</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endblock %}
{% block extra_script %}
<span class="p"><</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">"text/javascript"</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% static 'jquery.min.js' %}"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">"text/javascript"</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% static 'datatables.min.js' %}"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span><span class="p">></span>
<span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#table'</span><span class="p">).</span><span class="nx">DataTable</span><span class="p">(</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"ordering"</span><span class="o">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w"> </span><span class="nx">ajax</span><span class="o">:</span><span class="w"> </span><span class="s1">'{{ request.path }}?datatables=1'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">serverSide</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nx">columns</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="s1">'code'</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s1">'Κωδικός'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="s1">'name'</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s1">'Περιγραφή'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="s1">'name_en'</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s1">'Περιγραφή (αγγλικά)'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nx">language</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"sDecimal"</span><span class="o">:</span><span class="w"> </span><span class="s2">","</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sEmptyTable"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Δεν υπάρχουν δεδομένα στον πίνακα"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sInfo"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Εμφανίζονται _START_ έως _END_ από _TOTAL_ εγγραφές"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sInfoEmpty"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Εμφανίζονται 0 έως 0 από 0 εγγραφές"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sInfoFiltered"</span><span class="o">:</span><span class="w"> </span><span class="s2">"(φιλτραρισμένες από _MAX_ συνολικά εγγραφές)"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sInfoPostFix"</span><span class="o">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sInfoThousands"</span><span class="o">:</span><span class="w"> </span><span class="s2">"."</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sLengthMenu"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Δείξε _MENU_ εγγραφές"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sLoadingRecords"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Φόρτωση..."</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sProcessing"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Επεξεργασία..."</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sSearch"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Αναζήτηση:"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sSearchPlaceholder"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Αναζήτηση"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sThousands"</span><span class="o">:</span><span class="w"> </span><span class="s2">"."</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sUrl"</span><span class="o">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sZeroRecords"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Δεν βρέθηκαν εγγραφές που να ταιριάζουν"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"oPaginate"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"sFirst"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Πρώτη"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sPrevious"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Προηγούμενη"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sNext"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Επόμενη"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sLast"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Τελευταία"</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"oAria"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"sSortAscending"</span><span class="o">:</span><span class="w"> </span><span class="s2">": ενεργοποιήστε για αύξουσα ταξινόμηση της στήλης"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"sSortDescending"</span><span class="o">:</span><span class="w"> </span><span class="s2">": ενεργοποιήστε για φθίνουσα ταξινόμηση της στήλης"</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">);</span>
<span class="p">})</span>
<span class="p"></</span><span class="nt">script</span><span class="p">></span>
{% endblock %}
</code></pre></div>
<p>(Please ignore the <code>language</code> setting this is needed to translate the datatables messages to greek.)</p>
<p>The important part is that we add the jquery and datatable dependencies (remeber that datatables also has a css) and then add an empty table (<code><table class='table'></table></code>). Finally, after the page is loaded the table is initialized as datatable using
<code>$('table.table').DataTable(options)</code>.</p>
<p>The options we pass to enable the ajax functionality are:</p>
<div class="highlight"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nx">ordering</span><span class="o">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
<span class="w"> </span><span class="nx">ajax</span><span class="o">:</span><span class="w"> </span><span class="s1">'{{ request.path }}?datatables=1'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">serverSide</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nx">columns</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="s1">'code'</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s1">'Κωδικός'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="s1">'name'</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s1">'Περιγραφή'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="s1">'name_en'</span><span class="p">,</span><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="s1">'Περιγραφή (αγγλικά)'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div>
<p>I didn’t need ordering so I haven’t implemented it here however it would be possible to implement it by picking the order-related parameters from the request similar to the filtering and using them as <code>order_by</code> parameters to the queryset, see the <code>order[i][column]</code> and <code>order[i][dir]</code> <a href="https://datatables.net/manual/server-side">here</a>.</p>
<p>For the response, we use the current request url passing it the <code>datatables=1</code> parameter as discussed before. We define the datatable columns using the <code>columns</code> attr; the <code>data</code> key is the name of the field in the json data returned by the server and the <code>title</code> is the title of the column. These columns must exist in the json data returned by the server.</p>
<p>Finally, we need to add the view to our urls.py:</p>
<div class="highlight"><pre><span></span><code> <span class="n">path</span><span class="p">(</span>
<span class="s2">"item_datatable/"</span><span class="p">,</span>
<span class="n">ItemListListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"item_datatable"</span><span class="p">,</span>
<span class="p">),</span>
</code></pre></div>
<p>The above is enough to have a working datatable with ajax loading and filtering in your Django list views.</p>AI auto-subtitling2023-05-22T13:20:00+03:002023-05-22T13:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2023-05-22:/2023/05/22/ai-auto-subtitling/<p>Auto-subtitling movies with <span class="caps">AI</span></p><h2 id="introduction">Introduction</h2>
<p>As English is not my native language, I rely on subtitles to fully enjoy and comprehend most movies.
Unfortunately there are a lot of movies that don’t include subtitles or the subtitles that I am able
to find are not synchronized with the movie.</p>
<p>In this small post I’ll give you some instructions on how to use the newest “<span class="caps">AI</span>” trends to automatically
generate subtitles for a movie. The process does not achive 100% accuracy but it is very good and should definitely allow you to understand what’s being said. </p>
<p>Also, the described process will be very useful to people that do the actual subtitling for new movies since it
should save them a lot of manual labour. Instead of adding subtitles and timings manually they can use this to generate an automatic subtitle draft and then edit it by hand and ear.</p>
<p>Finally, please notice that all tools described here are free and open source and you should be able to run everything on your <span class="caps">PC</span> even if it is very slow and doesn’t have a <span class="caps">GPU</span>.</p>
<p><em>Everything described here is for educational purposes only. Please don’t use it to generate subtitles for movies that you don’t own or have the rights to do so.</em></p>
<h2 id="whispercpp">Whisper.cpp</h2>
<p>For the auto-subtitling we’ll use the <a href="https://github.com/ggerganov/whisper.cpp">whisper.cpp</a> library. This
library can be used to auto-transcribe audio files i.e it gets
audio as input and outputs the text that is being said in the audio.</p>
<p>This library can be compiled on your <span class="caps">PC</span> however to avoid complex workflows you can
<a href="https://github.com/ggerganov/whisper.cpp/releases/tag/v1.4.0">download the binaries for your system</a>. I’d also
recommend to download the <span class="caps">BLAS</span> binaries since they should work faster if your system supports them.</p>
<p>After you download the whisper.cpp extract it in a folder of your choice.</p>
<p>Beyond whisper.cpp, you need to download a model that will be used for the transcription.
The simplest way is to go to the hugging face whisper.cpp <a href="https://huggingface.co/ggerganov/whisper.cpp/tree/main">model repository</a>
and download the model from there. You only need 1 model file. The largest models will give you better results but will be slower and require more resources. I think that the base or small models should be good and fast enough.</p>
<p>I’ll give you some test results later to see the differences.</p>
<p>Notice that if your movie is in english you should download the .en models. </p>
<p>To continue copy the downloaded models in the whisper.cpp folder.</p>
<h2 id="extract-audio-from-the-movie">Extract audio from the movie</h2>
<p>The whisper.cpp library requires uncompressed audio (a .wav file) with specific characteristics (a sample rate of 16khz) to work.</p>
<p>To be able to extract that audio from our movie will use <a href="https://ffmpeg.org/">ffmpeg</a>. Download the win64 binaries
<a href="https://github.com/BtbN/FFmpeg-Builds/releases">from here</a> (get the <code>ffmpeg-master-latest-win64-gpl.zip</code> file) and copy over ffmpeg.exe to the whisper.cpp folder.</p>
<p>Then, to extract the audio from the movie you can use the following command:</p>
<div class="highlight"><pre><span></span><code>ffmpeg.exe<span class="w"> </span>-i<span class="w"> </span><span class="s2">"movie.mp4"</span><span class="w"> </span>-f<span class="w"> </span>wav<span class="w"> </span>-vn<span class="w"> </span>-acodec<span class="w"> </span>pcm_s16le<span class="w"> </span>-ar<span class="w"> </span><span class="m">16000</span><span class="w"> </span>-ac<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="s2">"movie.wav"</span>
</code></pre></div>
<p>(please change movie.mp4 and movie.wav with the correct names for your movie file).</p>
<p>To understand the command:</p>
<ul>
<li><code>-i "movie.mp4"</code>: the input file</li>
<li><code>-f wav</code>: the output format (a .wav file)</li>
<li><code>-vn</code>: no video</li>
<li><code>-acodec pcm_s16le</code>: the audio codec (the correct one for .wav file)</li>
<li><code>-ar 16000</code>: the sample rate (16khz)</li>
<li><code>-ac 1</code>: the number of channels (1 for mono, 2 for stereo)</li>
<li><code>"movie.wav"</code>: the output file</li>
</ul>
<p>The process should be very fast and will give you a .wav file with the same length as the movie.</p>
<h2 id="transcribing-the-audio">Transcribing the audio</h2>
<p>The last step is to do the actual audio transcribing using whisper.cpp. The great thing about whisper.cpp
is that it can directly create .srt (subtitle) files. The command to use is:</p>
<div class="highlight"><pre><span></span><code>main.exe<span class="w"> </span>-osrt<span class="w"> </span>-m<span class="w"> </span>ggml-base.en.bin<span class="w"> </span>-f<span class="w"> </span>movie.wav
</code></pre></div>
<p>Notice that</p>
<ul>
<li>The whisper.cpp bundle you downloaded should have a main.exe file</li>
<li><code>-osrt</code>: Generate a .srt file as output</li>
<li><code>-m ggml-base.en.bin</code>: The model to use for the transcription</li>
<li><code>-f movie.wav</code>: The input file</li>
</ul>
<p>The above command will start outputing the text it detects and when it finishes it will generate a <code>movie.wav.srt</code>. You can then use that .srt file to add subtitles for your movie!</p>
<h2 id="results">Results</h2>
<p>To test the process I used the first 14 minutes of the movie Kill Bill 2 as input. To generate the .wav file I used the following command:</p>
<div class="highlight"><pre><span></span><code>ffmpeg.exe<span class="w"> </span>-i<span class="w"> </span><span class="s2">"kb2.mp4"</span><span class="w"> </span>-f<span class="w"> </span>wav<span class="w"> </span>-vn<span class="w"> </span>-acodec<span class="w"> </span>pcm_s16le<span class="w"> </span>-ar<span class="w"> </span><span class="m">16000</span><span class="w"> </span>-ac<span class="w"> </span><span class="m">1</span><span class="w"> </span>-ss<span class="w"> </span><span class="m">00</span>:00:00<span class="w"> </span>-to<span class="w"> </span><span class="m">00</span>:14:00<span class="w"> </span><span class="s2">"kb2.wav"</span>
</code></pre></div>
<p>(please notice the <code>-ss</code> and <code>-to</code> to select the first 14 minutes of the movie).</p>
<p>This generated a 26 <span class="caps">MB</span> wav file. Then I tried the results using
three different models:</p>
<ul>
<li>ggml-tiny.en.bin with a size of 77 <span class="caps">MB</span></li>
<li>ggml-base.en.bin with a size of 147 <span class="caps">MB</span></li>
<li>ggml-small.en.bin with a size of 488 <span class="caps">MB</span></li>
<li>ggml-medium.bin with a size of 1533 <span class="caps">MB</span></li>
</ul>
<h3 id="ggml-tinyenbin">ggml-tiny.en.bin</h3>
<p>Some stats:</p>
<div class="highlight"><pre><span></span><code><span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">mem</span><span class="w"> </span><span class="n">required</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">201.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="p">(</span><span class="o">+</span><span class="w"> </span><span class="mf">3.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="n">per</span><span class="w"> </span><span class="n">decoder</span><span class="p">)</span>
<span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">73.54</span><span class="w"> </span><span class="n">MB</span>
<span class="n">whisper_print_timings</span><span class="p">:</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="n">time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">166330.89</span><span class="w"> </span><span class="n">ms</span>
</code></pre></div>
<p><strong>So time needed was ~ 160 seconds</strong></p>
<p>And the actual transcription:</p>
<div class="highlight"><pre><span></span><code><span class="k">[00:00:00.000 --> 00:00:03.000] [MUSIC PLAYING]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:03.000 --> 00:00:16.480] Do you finally sit just now?</span>
<span class="k">[00:00:16.480 --> 00:00:19.120] [MUSIC PLAYING]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:19.120 --> 00:00:21.480] No, I can't do.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:21.480 --> 00:00:27.600] I'd like to believe you're aware enough even now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:27.600 --> 00:00:35.080] No, that there is nothing suggesting in my actions.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:35.080 --> 00:00:44.000] This moment, this is me and my most nice against them.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:44.000 --> 00:00:47.440] Well, it's your name.</span>
<span class="k">[00:00:47.440 --> 00:00:52.920] [MUSIC PLAYING]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:52.920 --> 00:00:55.120] But Dad didn't I?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:55.120 --> 00:00:56.640] Well, I wasn't.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:56.640 --> 00:00:58.040] But it wasn't from lack of trying.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:58.040 --> 00:01:00.360] I can tell you that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:00.360 --> 00:01:03.600] Actually, Bill's last bullet put me in a coma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:03.600 --> 00:01:07.600] A coma, I was to lie in for four years.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:07.600 --> 00:01:09.360] And I woke up.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:09.360 --> 00:01:11.280] I went on with a movie advertisements</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:11.280 --> 00:01:15.720] for two as a roaring rampage of revenge.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:15.720 --> 00:01:17.120] I roared.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:17.120 --> 00:01:18.800] And I relanged.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:18.800 --> 00:01:22.480] And I got bloody satisfaction.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:22.480 --> 00:01:26.360] I've killed a hell of a lot of people to get to this point.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:26.360 --> 00:01:29.720] But I have only one more.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:29.720 --> 00:01:31.760] The last one.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:31.760 --> 00:01:35.360] The one I'm driving to right now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:35.360 --> 00:01:38.280] The only one left.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:38.280 --> 00:01:42.040] And when I arrive at my destination,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:42.040 --> 00:01:44.200] I have going to kill Bill.</span>
<span class="k">[00:01:44.200 --> 00:01:47.200] [MUSIC PLAYING]</span>
<span class="k">[00:01:47.200 --> 00:02:10.880] [MUSIC PLAYING]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:10.880 --> 00:02:13.480] Now the incident that happened at the two pines wedding</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:13.480 --> 00:02:17.360] chapel that put this whole gory story into motion</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:17.360 --> 00:02:20.120] has since become legend.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:20.120 --> 00:02:22.800] Massacre at two pines.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:22.800 --> 00:02:24.800] That's what the newspapers called it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:24.800 --> 00:02:28.760] The local TV news called it the El Paso, Texas wedding</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:28.760 --> 00:02:30.920] chapel massacre.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:30.920 --> 00:02:32.280] How it happened?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:32.280 --> 00:02:33.680] Who was there?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:33.680 --> 00:02:36.600] How many got killed and who killed them?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:36.600 --> 00:02:40.320] Changes depending on who's telling the story.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:40.320 --> 00:02:43.080] In actual fact, the massacre didn't happen</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:43.080 --> 00:02:45.720] during a wedding at all.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:45.720 --> 00:02:48.360] It was a wedding rehearsal.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:48.360 --> 00:02:51.800] Now when we come to the park where I say you make kiss,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:51.800 --> 00:02:55.120] the bride, you make kiss, the bride.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:55.120 --> 00:02:56.960] But don't stick your tongue in her mouth.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:56.960 --> 00:03:01.840] This might be funny to your friends,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:01.840 --> 00:03:06.440] but it would be embarrassing to your parents.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:06.440 --> 00:03:08.200] We'll try to do strange things.</span>
<span class="k">[00:03:08.200 --> 00:03:11.080] [LAUGHTER]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:11.080 --> 00:03:12.040] Y'all got a song?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:12.040 --> 00:03:18.000] How about love me, tender?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:18.000 --> 00:03:18.840] I'd play that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:18.840 --> 00:03:24.360] Let me tender be great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:24.360 --> 00:03:27.520] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:27.520 --> 00:03:30.640] Rufus, who was that he used to play for?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:30.640 --> 00:03:32.640] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:32.640 --> 00:03:34.760] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:34.760 --> 00:03:35.680] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:35.680 --> 00:03:37.080] I was a dreel.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:37.080 --> 00:03:38.600] I was a drifter.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:38.600 --> 00:03:40.080] I was a colister.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:40.080 --> 00:03:41.920] I was a part of the game.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:41.920 --> 00:03:43.760] I was a bar kid.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:43.760 --> 00:03:48.200] If they come through Texas, I can play with him.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:48.200 --> 00:03:50.720] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:50.720 --> 00:03:56.840] I never forgot anything.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:56.840 --> 00:04:00.440] Oh, yes, you forgot the seating arrangements.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:00.440 --> 00:04:03.200] Thank you, Mother.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:03.200 --> 00:04:07.840] Now the way we normally do this, we have the bride's side,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:07.840 --> 00:04:09.560] and then we have the groom's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:09.560 --> 00:04:12.880] But since the bride ain't got nobody coming,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:12.880 --> 00:04:16.960] and the groom's got far too many people coming--</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:16.960 --> 00:04:18.920] Well, yeah, they're coming all the way from Oklahoma.</span>
<span class="k">[00:04:18.920 --> 00:04:21.600] [LAUGHTER]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:21.600 --> 00:04:23.160] Right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:23.160 --> 00:04:27.880] Well, I don't see no problem with the groom's side</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:27.880 --> 00:04:29.840] sharing the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:29.840 --> 00:04:30.760] Do you, Mother?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:30.760 --> 00:04:32.720] Not a problem with that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:32.720 --> 00:04:38.880] But honey, you know, it would be good if you had somebody come.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:38.880 --> 00:04:42.840] You know, it was a sign of good faith.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:42.840 --> 00:04:49.400] Well, I don't have anybody except for Tommy and my friends.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:49.400 --> 00:04:52.000] You have no family?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:52.000 --> 00:04:53.600] Well, I'm working on changing that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:53.600 --> 00:04:55.360] Mrs. Harmony, we're all the family.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:55.360 --> 00:04:56.760] This is Langel's ever going to need.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:56.760 --> 00:05:01.760] I don't feel very well in this bitch.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:01.760 --> 00:05:03.680] This started to piss me off.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:03.680 --> 00:05:08.280] So while you all blather on, I'm going to go outside and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:08.280 --> 00:05:10.320] I'm a Reverend, sorry.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:10.320 --> 00:05:11.840] She's going to go out and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:11.840 --> 00:05:12.440] Yeah.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:12.440 --> 00:05:15.320] Given her delicate condition, she just needs a few minutes</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:15.320 --> 00:05:15.920] to get it together.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:15.920 --> 00:05:16.920] She'll be OK.</span>
<span class="k">[00:05:16.920 --> 00:05:19.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:19.880 --> 00:05:23.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:23.880 --> 00:05:27.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:27.880 --> 00:05:31.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:31.880 --> 00:05:35.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:35.880 --> 00:05:39.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:39.880 --> 00:05:43.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:43.880 --> 00:05:47.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:47.880 --> 00:05:51.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:51.880 --> 00:05:55.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:55.880 --> 00:05:59.880] [MUSIC PLAYING]</span>
<span class="k">[00:05:59.880 --> 00:06:01.880] [MUSIC PLAYING]</span>
<span class="k">[00:06:01.880 --> 00:06:03.880] [MUSIC PLAYING]</span>
<span class="k">[00:06:03.880 --> 00:06:05.880] [MUSIC PLAYING]</span>
<span class="k">[00:06:05.880 --> 00:06:07.880] [MUSIC PLAYING]</span>
<span class="k">[00:06:07.880 --> 00:06:09.880] [MUSIC PLAYING]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:35.880 --> 00:06:37.880] Hello, kiddo.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:37.880 --> 00:06:45.880] How did you find me?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:45.880 --> 00:06:46.880] I'm the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:46.880 --> 00:06:54.880] What are you doing here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:54.880 --> 00:06:57.880] Am I doing?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:57.880 --> 00:07:02.880] Well, I'm only going to play my flute.</span>
<span class="k">[00:07:02.880 --> 00:07:05.880] [MUSIC PLAYING]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:05.880 --> 00:07:14.880] This moment, I'm looking at the most beautiful</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:14.880 --> 00:07:18.880] bright and these old eyes of every scene.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:18.880 --> 00:07:20.880] Where are you here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:20.880 --> 00:07:21.880] Nice look.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:21.880 --> 00:07:26.880] Are you going to be nice?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:26.880 --> 00:07:30.880] I've never been nice my whole life.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:30.880 --> 00:07:33.880] But I'll do my best to be sweet.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:33.880 --> 00:07:42.880] I was told you, your sweet side is your best side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:42.880 --> 00:07:45.880] I guess that's why you're the only one who's ever seen it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:45.880 --> 00:07:51.880] See, you got a bun in the oven.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:51.880 --> 00:07:54.880] Hmm.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:54.880 --> 00:07:57.880] I'm knocked out.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:57.880 --> 00:07:59.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:59.880 --> 00:08:01.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:01.880 --> 00:08:03.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:03.880 --> 00:08:05.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:05.880 --> 00:08:07.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:07.880 --> 00:08:09.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:09.880 --> 00:08:11.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:11.880 --> 00:08:13.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:13.880 --> 00:08:15.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:15.880 --> 00:08:17.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:17.880 --> 00:08:19.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:19.880 --> 00:08:21.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:21.880 --> 00:08:23.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:23.880 --> 00:08:25.880] I'm not sure what you're doing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:25.880 --> 00:08:28.880] That's hardly a prompt.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:28.880 --> 00:08:30.880] But you're right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:30.880 --> 00:08:35.880] What is your young man do for a living?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:35.880 --> 00:08:39.880] He owns a used record store here in El Paso.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:39.880 --> 00:08:41.880] Music lover, right?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:41.880 --> 00:08:44.880] He's fond of music.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:44.880 --> 00:08:46.880] Not me all.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:46.880 --> 00:08:54.880] And what are you doing for a J.O.B. these days?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:55.880 --> 00:08:58.880] I work in the record store.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:58.880 --> 00:09:06.880] Oh, so it all suddenly seems so clear.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:06.880 --> 00:09:09.880] Do you like it?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:09.880 --> 00:09:13.880] Yeah, I like it a lot, smartass.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:13.880 --> 00:09:15.880] I get to listen to music all day.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:15.880 --> 00:09:17.880] Talk about music all day.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:17.880 --> 00:09:20.880] It's really cool.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:20.880 --> 00:09:22.880] It's going to be a great environment</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:22.880 --> 00:09:27.880] for a little girl to grow up in.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:27.880 --> 00:09:30.880] As opposed to getting around the world,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:30.880 --> 00:09:33.880] killing human beings, and being paid best</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:33.880 --> 00:09:36.880] sums of money.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:36.880 --> 00:09:38.880] Precisely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:38.880 --> 00:09:41.880] Well, my own friend.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:41.880 --> 00:09:43.880] Do we choose someone?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:43.880 --> 00:09:48.880] However, all cocklockery aside,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:48.880 --> 00:09:51.880] I am looking forward to meeting your young man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:51.880 --> 00:09:54.880] I happen to be more or less particular.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:54.880 --> 00:09:59.880] Who might get married?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:59.880 --> 00:10:01.880] You want to come to the wedding?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:01.880 --> 00:10:04.880] Only if I can sit on the bright side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:04.880 --> 00:10:07.880] You'll find it a bit lonely on my side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:07.880 --> 00:10:11.880] Your side always was a bit lonely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:11.880 --> 00:10:15.880] But I wouldn't sit anywhere else.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:15.880 --> 00:10:20.880] You know, I had a lovely stream of money.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:20.880 --> 00:10:22.880] Lovely stream about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:22.880 --> 00:10:23.880] Oh, here's Tommy.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:23.880 --> 00:10:25.880] Call me, Arlene.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:25.880 --> 00:10:26.880] You must be Tommy.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:26.880 --> 00:10:27.880] Uh-huh.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:27.880 --> 00:10:29.880] Arlene's told me so much about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:29.880 --> 00:10:30.880] Aren't you okay?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:30.880 --> 00:10:31.880] Oh, I'm fine.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:31.880 --> 00:10:34.880] Tommy, I'd like you to meet my father.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:34.880 --> 00:10:37.880] Oh, my God.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:37.880 --> 00:10:39.880] Oh, my God, this is great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:39.880 --> 00:10:41.880] I'm so glad to meet you, sir.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:41.880 --> 00:10:42.880] Oh, Dad.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:42.880 --> 00:10:44.880] The name is Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:44.880 --> 00:10:46.880] Well, it's great to meet you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:46.880 --> 00:10:47.880] Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:47.880 --> 00:10:49.880] Arlene told me you could make it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:49.880 --> 00:10:50.880] Surprise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:50.880 --> 00:10:52.880] That's my pop for you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:52.880 --> 00:10:54.880] Always full of surprises.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:54.880 --> 00:11:00.880] Well, in the surprise department, the apple doesn't fall far from the tree.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:00.880 --> 00:11:02.880] When did you get in?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:02.880 --> 00:11:03.880] Just now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:03.880 --> 00:11:05.880] Did you come straight from Australia?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:05.880 --> 00:11:06.880] Of course.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:06.880 --> 00:11:08.880] Daddy, I told Tommy that you were in Perth,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:08.880 --> 00:11:11.880] lining for silver and no one could meet you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:11.880 --> 00:11:15.880] Lucky for us all, that's not the case.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:15.880 --> 00:11:19.880] So, what's this all about?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:19.880 --> 00:11:26.880] I've heard of wedding rehearsals, but I don't believe I've ever heard of a wedding dress rehearsal before.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:26.880 --> 00:11:33.880] We thought, well, I paid so much money for a dress you only going to wear once, especially when Arlene looks so goddamn beautiful in it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:33.880 --> 00:11:38.880] So, uh, we're going to try to get all the mileage we can out of it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:38.880 --> 00:11:43.880] Isn't it supposed to be bad luck for the groom to see the bride and her wedding dress?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:43.880 --> 00:11:45.880] People with a ceremony?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:45.880 --> 00:11:50.880] Well, I guess I just believe I'm having dangerous.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:50.880 --> 00:11:53.880] I know just what you mean.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:53.880 --> 00:11:54.880] Some.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:54.880 --> 00:11:56.880] Some of us are places to be.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:56.880 --> 00:11:58.880] Show them to do.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:58.880 --> 00:12:01.880] Look, we got to go through this one more time.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:01.880 --> 00:12:03.880] So, uh, why don't you have a--</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:03.880 --> 00:12:05.880] Oh, my God.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:05.880 --> 00:12:06.880] What am I thinking?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:06.880 --> 00:12:07.880] You should give her away.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:07.880 --> 00:12:10.880] Tommy, that's not exactly Daddy's cup of tea.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:10.880 --> 00:12:15.880] I'm not even sure if you're much more comfortable sitting with the rest of the cats.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:15.880 --> 00:12:17.880] Really?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:17.880 --> 00:12:20.880] That's asking a lot.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:20.880 --> 00:12:21.880] No.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:21.880 --> 00:12:22.880] Okay.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:22.880 --> 00:12:23.880] Forget it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:23.880 --> 00:12:25.880] But how about we go out to dinner tonight, celebrate?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:25.880 --> 00:12:28.880] Only on the condition that I pay for everything.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:28.880 --> 00:12:29.880] Deal.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:29.880 --> 00:12:31.880] We have to do this now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:31.880 --> 00:12:32.880] Can I watch?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:32.880 --> 00:12:33.880] Absolutely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:33.880 --> 00:12:35.880] Have a seat.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:35.880 --> 00:12:37.880] Which is the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:37.880 --> 00:12:39.880] Right over here.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:39.880 --> 00:12:44.880] Mother, here we go.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:44.880 --> 00:12:45.880] Yes.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:45.880 --> 00:12:48.880] Now, son, pop them, bowels.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:48.880 --> 00:12:49.880] Yeah.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:49.880 --> 00:12:51.880] You can't hear me.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:51.880 --> 00:12:53.880] You should be.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:53.880 --> 00:12:54.880] No.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:54.880 --> 00:12:58.880] I just don't want to.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:58.880 --> 00:13:00.880] You don't want me to put them there.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:00.880 --> 00:13:07.880] If he's the man you want, then go stand by.</span>
<span class="k">[00:13:07.880 --> 00:13:31.880] [MUSIC]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:31.880 --> 00:13:33.880] Does it look pretty?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:33.880 --> 00:13:35.880] Oh, yeah.</span>
<span class="k">[00:13:35.880 --> 00:13:45.880] [MUSIC]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:45.880 --> 00:13:47.880] Thank you.</span>
<span class="k">[00:13:47.880 --> 00:13:52.880] [MUSIC]</span>
<span class="k">[00:13:52.880 --> 00:13:59.880] [MUSIC]</span>
</code></pre></div>
<h2 id="ggml-baseenbin">ggml-base.en.bin</h2>
<p>Some stats:</p>
<div class="highlight"><pre><span></span><code><span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">mem</span><span class="w"> </span><span class="n">required</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">310.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="p">(</span><span class="o">+</span><span class="w"> </span><span class="mf">6.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="n">per</span><span class="w"> </span><span class="n">decoder</span><span class="p">)</span>
<span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">140.54</span><span class="w"> </span><span class="n">MB</span>
<span class="n">whisper_print_timings</span><span class="p">:</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="n">time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">433205.62</span><span class="w"> </span><span class="n">ms</span>
</code></pre></div>
<p><strong>So time needed was ~ 433 seconds</strong></p>
<p>Actual transcription:</p>
<div class="highlight"><pre><span></span><code><span class="k">[00:00:00.000 --> 00:00:15.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:15.000 --> 00:00:17.000] Do you find me sadistic?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:17.000 --> 00:00:20.000] No, Kato.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:20.000 --> 00:00:23.000] I'd like to believe</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:23.000 --> 00:00:27.000] you're aware enough, even now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:27.000 --> 00:00:33.000] I know that there is nothing sadistic in my actions.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:33.000 --> 00:00:36.000] This moment,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:36.000 --> 00:00:40.000] this is me,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:40.000 --> 00:00:44.000] and I most miss the case.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:44.000 --> 00:00:45.000] Well,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:45.000 --> 00:00:48.000] it's your baby.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:48.000 --> 00:00:54.000] The dead didn't I?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:55.000 --> 00:00:57.000] Well, I wasn't.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:57.000 --> 00:01:00.000] But it wasn't from lack of try and I can tell you that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:00.000 --> 00:01:03.000] Actually, Bill's last bullet put me in a coma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:03.000 --> 00:01:07.000] A coma I was to lie in for four years.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:07.000 --> 00:01:09.000] When I woke up,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:09.000 --> 00:01:15.000] I went on with the movie advertisements referred to as a roaring rampage of revenge.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:15.000 --> 00:01:17.000] I roared,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:17.000 --> 00:01:19.000] and I rampaged,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:19.000 --> 00:01:22.000] and I got bloody satisfaction.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:22.000 --> 00:01:26.000] I've killed a hell of a lot of people who get to this point.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:26.000 --> 00:01:29.000] But I have only one more.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:29.000 --> 00:01:31.000] The last one.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:31.000 --> 00:01:35.000] The one I'm driving to right now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:35.000 --> 00:01:38.000] The only one left.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:38.000 --> 00:01:41.000] And when I arrive at my destination,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:41.000 --> 00:01:45.000] I am gonna kill Bill.</span>
<span class="k">[00:01:45.000 --> 00:02:02.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:02.000 --> 00:02:14.000] Now the incident that happened at the two pines wedding chapel</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:14.000 --> 00:02:20.000] that put this whole gory story into motion has since become legend.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:20.000 --> 00:02:23.000] Massacre at two pines.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:23.000 --> 00:02:25.000] That's what the newspapers called it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:25.000 --> 00:02:31.000] The local TV news called it the El Paso Texas Wedding Chapel Massacre.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:31.000 --> 00:02:32.000] How it happened?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:32.000 --> 00:02:33.000] Who was there?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:33.000 --> 00:02:36.000] How many got killed and who killed them?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:36.000 --> 00:02:40.000] Changes depending on who's telling the story.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:40.000 --> 00:02:42.000] In actual fact,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:42.000 --> 00:02:46.000] the massacre didn't happen during a wedding at all.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:46.000 --> 00:02:48.000] It was a wedding rehearsal.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:48.000 --> 00:02:53.000] Now when we come to the park where I say you may kiss the bride,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:53.000 --> 00:02:55.000] you may kiss the bride.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:55.000 --> 00:02:59.000] But don't stick your tongue in her mouth.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:59.000 --> 00:03:02.000] This might be funny to your friends,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:02.000 --> 00:03:06.000] but it would be embarrassing to your parents.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:06.000 --> 00:03:11.000] We'll try to be strange.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:11.000 --> 00:03:16.000] Y'all got a song?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:16.000 --> 00:03:18.000] How about "Love Me Tender"?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:18.000 --> 00:03:22.000] I can play that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:22.000 --> 00:03:24.000] Let me tend to be great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:24.000 --> 00:03:27.000] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:27.000 --> 00:03:28.000] Rufus?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:28.000 --> 00:03:30.000] Who was that to use to play for?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:30.000 --> 00:03:32.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:32.000 --> 00:03:34.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:34.000 --> 00:03:36.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:36.000 --> 00:03:37.000] I was a drill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:37.000 --> 00:03:38.000] I was a drifted.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:38.000 --> 00:03:40.000] I was a coaster.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:40.000 --> 00:03:42.000] I was part of the gang.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:42.000 --> 00:03:44.000] I was a bar-k.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:44.000 --> 00:03:48.000] If they come through Texas, I'd play with him.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:48.000 --> 00:03:50.000] Rufus?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:50.000 --> 00:03:54.000] He's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:54.000 --> 00:03:57.000] Have I forgot anything?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:57.000 --> 00:04:00.000] Oh yes, you forgot the seating arrangements.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:00.000 --> 00:04:03.000] Thank you, mother.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:03.000 --> 00:04:05.000] Now the way we normally do this,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:05.000 --> 00:04:08.000] we have the bride's side,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:08.000 --> 00:04:12.000] but since the bride ain't got nobody coming,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:12.000 --> 00:04:16.000] and the groom's got far too many people coming,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:16.000 --> 00:04:20.000] well yeah, they're coming all the way from Oklahoma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:20.000 --> 00:04:22.000] Right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:22.000 --> 00:04:27.000] Well, I don't see no problem with the groom's side</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:27.000 --> 00:04:29.000] sharing the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:29.000 --> 00:04:30.000] Do you, mother?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:30.000 --> 00:04:32.000] Not a problem with that,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:32.000 --> 00:04:37.000] but honey, you know, it would be good if you had somebody come</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:37.000 --> 00:04:41.000] You know, is it sign of good faith?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:41.000 --> 00:04:44.000] Well, I don't have anybody,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:44.000 --> 00:04:48.000] except for Tommy and my friends.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:48.000 --> 00:04:51.000] You have no family?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:51.000 --> 00:04:53.000] Well, I'm working on changing then.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:53.000 --> 00:04:55.000] Mrs. Harmony, we're all the family</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:55.000 --> 00:04:58.000] this Alangel's ever going to need.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:58.000 --> 00:05:00.000] I'm not feeling very well,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:00.000 --> 00:05:03.000] and this bitch is starting to piss me off.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:03.000 --> 00:05:05.000] So while you all bother on,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:05.000 --> 00:05:08.000] I'm going to go outside and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:08.000 --> 00:05:10.000] I'm sorry.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:10.000 --> 00:05:12.000] She's going to go out and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:12.000 --> 00:05:14.000] Yeah, given her delicate condition,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:14.000 --> 00:05:17.000] she just needs a few minutes to give it to get us to be okay.</span>
<span class="k">[00:05:18.000 --> 00:05:21.000] [music]</span>
<span class="k">[00:05:22.000 --> 00:05:25.000] [music]</span>
<span class="k">[00:05:26.000 --> 00:05:29.000] [music]</span>
<span class="k">[00:05:30.000 --> 00:05:33.000] [music]</span>
<span class="k">[00:05:34.000 --> 00:05:37.000] [music]</span>
<span class="k">[00:05:38.000 --> 00:05:41.000] [music]</span>
<span class="k">[00:05:42.000 --> 00:05:45.000] [music]</span>
<span class="k">[00:05:46.000 --> 00:05:49.000] [music]</span>
<span class="k">[00:05:50.000 --> 00:05:53.000] [music]</span>
<span class="k">[00:05:53.000 --> 00:05:57.000] [music]</span>
<span class="k">[00:05:57.000 --> 00:06:01.000] [music]</span>
<span class="k">[00:06:01.000 --> 00:06:05.000] [music]</span>
<span class="k">[00:06:05.000 --> 00:06:09.000] [music]</span>
<span class="k">[00:06:09.000 --> 00:06:13.000] [music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:36.000 --> 00:06:40.000] Hello, kiddo.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:40.000 --> 00:06:46.000] How did you find me?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:46.000 --> 00:06:50.000] I'm the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:50.000 --> 00:06:54.000] What are you doing here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:54.000 --> 00:06:58.000] What am I doing?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:58.000 --> 00:07:00.000] Well,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:00.000 --> 00:07:06.000] I was playing my flute.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:06.000 --> 00:07:12.000] At this moment,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:12.000 --> 00:07:15.000] I'm looking at the most beautiful bride</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:15.000 --> 00:07:19.000] in these old eyes of every scene.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:19.000 --> 00:07:21.000] Why are you here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:21.000 --> 00:07:25.000] Last look.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:25.000 --> 00:07:27.000] Are you going to be nice?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:27.000 --> 00:07:31.000] I've never been nice my whole life.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:31.000 --> 00:07:36.000] But I'll do my best to be sweet.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:36.000 --> 00:07:39.000] I always told you,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:39.000 --> 00:07:42.000] your sweet side is your best side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:42.000 --> 00:07:49.000] I guess that's why you're the only one who's ever seen it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:49.000 --> 00:07:54.000] See, you got a bun in the oven?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:54.000 --> 00:07:58.000] I'm knocked up.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:58.000 --> 00:08:00.000] Chase Louise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:00.000 --> 00:08:02.000] That young man here is sure</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:02.000 --> 00:08:06.000] it doesn't believe in wasting time, does he?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:06.000 --> 00:08:10.000] Have you seen Tommy?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:10.000 --> 00:08:12.000] A guy on the tux?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:12.000 --> 00:08:13.000] Yes.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:13.000 --> 00:08:16.000] And I saw him.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:16.000 --> 00:08:20.000] I like his hair.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:20.000 --> 00:08:24.000] You promised you'd be nice.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:24.000 --> 00:08:26.000] I said I'd do my best.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:26.000 --> 00:08:29.000] That's hardly a promise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:29.000 --> 00:08:31.000] But you're right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:31.000 --> 00:08:36.000] What does your young man do for a living?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:36.000 --> 00:08:40.000] He owns a used record store here in El Paso.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:40.000 --> 00:08:42.000] He's a lover, right?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:42.000 --> 00:08:45.000] He's fond of music.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:45.000 --> 00:08:51.000] Aren't we all?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:51.000 --> 00:08:56.000] And what are you doing for a J.O.B. these days?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:56.000 --> 00:08:59.000] I work in the record store.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:59.000 --> 00:09:03.000] Ah, so...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:03.000 --> 00:09:08.000] it all suddenly seems so clear.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:08.000 --> 00:09:10.000] Do you like it?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:10.000 --> 00:09:14.000] Yeah, I like it a lot, smart ass.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:14.000 --> 00:09:17.000] I get to listen to music all day.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:17.000 --> 00:09:21.000] Talk about music all day. It's really cool.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:21.000 --> 00:09:28.000] It's going to be a great environment for my little girl to grow up in.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:28.000 --> 00:09:33.000] As opposed to jetting around the world, killing human beams,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:33.000 --> 00:09:38.000] and being paid best sums of money.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:38.000 --> 00:09:40.000] Precisely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:40.000 --> 00:09:44.000] I have a friend.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:44.000 --> 00:09:47.000] Do each you own.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:47.000 --> 00:09:51.000] However, all clockwork re-assigned.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:51.000 --> 00:09:55.000] I am looking forward to meeting your young man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:55.000 --> 00:09:59.000] I happen to be more or less particular.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:59.000 --> 00:10:03.000] Oh, my God, Mary.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:03.000 --> 00:10:05.000] You want to come to the wedding?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:05.000 --> 00:10:08.000] Only if I can sit on the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:08.000 --> 00:10:12.000] Your side always was a bit lonely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:12.000 --> 00:10:17.000] But I wouldn't sit anywhere else.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:17.000 --> 00:10:22.000] You know, I had a lovely stream about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:22.000 --> 00:10:25.000] Oh, here's Tommy. Call me Arlene.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:25.000 --> 00:10:29.000] You must be Tommy. Arlene's told me so much about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:29.000 --> 00:10:31.000] Are you okay?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:31.000 --> 00:10:35.000] Oh, I'm fine. Tommy, I'd like you to meet my father.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:35.000 --> 00:10:38.000] Oh, my God.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:38.000 --> 00:10:40.000] Oh, my God, this is great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:40.000 --> 00:10:42.000] I'm so glad to meet you, sir.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:42.000 --> 00:10:45.000] Oh, Dad, the name is Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:45.000 --> 00:10:48.000] Well, it's great to meet you. Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:48.000 --> 00:10:50.000] Arlene told me you could make it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:50.000 --> 00:10:51.000] Surprise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:51.000 --> 00:10:54.000] That's my pot for you. Always full of surprises.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:54.000 --> 00:10:58.000] Well, in a surprise department.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:58.000 --> 00:11:01.000] The Apple doesn't fall far from the tree.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:01.000 --> 00:11:04.000] When did you get in? Just now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:04.000 --> 00:11:06.000] Did you come straight from Australia?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:06.000 --> 00:11:07.000] Of course.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:07.000 --> 00:11:09.000] Daddy, I told Tommy that you were in Perth,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:09.000 --> 00:11:13.000] finding for silver, and no one could reach you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:13.000 --> 00:11:16.000] Lucky for us all, that's not the case.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:16.000 --> 00:11:20.000] So, what's this all about?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:20.000 --> 00:11:27.000] I've heard of wedding rehearsals, but I don't believe I've ever heard of a wedding dress rehearsal before.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:27.000 --> 00:11:31.000] We thought, "Why pay so much money for a dress you only gonna wear once?"</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:31.000 --> 00:11:34.000] Especially when Arlene looks so goddamn beautiful in it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:34.000 --> 00:11:39.000] So, we're gonna try to get all the mileage we can out of it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:39.000 --> 00:11:44.000] Isn't it supposed to be bad luck for the groom to see the bride in her wedding dress?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:44.000 --> 00:11:46.000] People with a ceremony?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:46.000 --> 00:11:51.000] Wow. I guess I just believe in them dangerously.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:51.000 --> 00:11:54.000] I know just what you mean.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:54.000 --> 00:11:59.000] "San, Sama Lacha, places to be. It's your duty."</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:59.000 --> 00:12:02.000] But we gotta go through this one more time.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:02.000 --> 00:12:05.000] So, uh, why don't you have a... oh my god.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:05.000 --> 00:12:08.000] What am I thinking? You should give her away!</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:08.000 --> 00:12:11.000] Tommy, that's not exactly Daddy's cup of tea.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:11.000 --> 00:12:16.000] I think Father seemed much more comfortable sitting with the rest of the guests.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:16.000 --> 00:12:18.000] Really?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:18.000 --> 00:12:21.000] That's asking a lot.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:21.000 --> 00:12:24.000] Oh. Okay. We'll forget it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:24.000 --> 00:12:26.000] But how about we go out to dinner tonight and celebrate?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:26.000 --> 00:12:29.000] Only on the condition that I pay for everything.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:29.000 --> 00:12:32.000] Deal. We have to do this now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:32.000 --> 00:12:33.000] Can I watch?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:33.000 --> 00:12:36.000] Absolutely. You have a seat.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:36.000 --> 00:12:38.000] Which is the bride's side?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:38.000 --> 00:12:40.000] Right over here.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:40.000 --> 00:12:45.000] Father, here we go.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:45.000 --> 00:12:50.000] Yeah. Now, Sean, drop them bows.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:50.000 --> 00:12:53.000] Yeah.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:53.000 --> 00:12:56.000] Oh, Sean.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:56.000 --> 00:12:59.000] Oh. I just want...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:59.000 --> 00:13:03.000] You don't want me a damn thing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:03.000 --> 00:13:08.000] If he's the man you want, then go stand by.</span>
<span class="k">[00:13:08.000 --> 00:13:11.000] [Sighs]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:11.000 --> 00:13:34.000] Does it look pretty?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:34.000 --> 00:13:37.000] Oh, yes.</span>
<span class="k">[00:13:37.000 --> 00:13:40.000] [Sighs]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:40.000 --> 00:13:49.000] Thank you.</span>
<span class="k">[00:13:49.000 --> 00:13:56.000] [Sighs]</span>
<span class="k">[00:13:56.000 --> 00:14:00.000] [Music]</span>
</code></pre></div>
<h2 id="ggml-smallenbin">ggml-small.en.bin</h2>
<p>Some stats:</p>
<div class="highlight"><pre><span></span><code><span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">mem</span><span class="w"> </span><span class="n">required</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">743.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="p">(</span><span class="o">+</span><span class="w"> </span><span class="mf">16.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="n">per</span><span class="w"> </span><span class="n">decoder</span><span class="p">)</span>
<span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">464.44</span><span class="w"> </span><span class="n">MB</span>
<span class="n">whisper_print_timings</span><span class="p">:</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="n">time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1713762.12</span><span class="w"> </span><span class="n">ms</span>
</code></pre></div>
<p><strong>So time needed was ~ 1713 seconds (almost half an hour)</strong></p>
<p>And the actual transcription:</p>
<div class="highlight"><pre><span></span><code><span class="k">[00:00:00.000 --> 00:00:10.000] [MUSIC]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:10.000 --> 00:00:17.000] Do you find me sadistic?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:17.000 --> 00:00:21.000] No, kiddo.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:21.000 --> 00:00:27.000] I'd like to believe you're aware enough, even now,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:27.000 --> 00:00:33.000] to know that there is nothing sadistic in my actions.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:33.000 --> 00:00:44.000] This moment, this is me and my most nicer kiss to be.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:44.000 --> 00:00:48.000] Well, it's your baby.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:48.000 --> 00:00:55.000] Look, Dad, didn't I?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:55.000 --> 00:01:00.000] Well, I wasn't. But it wasn't from lack of trying, I can tell you that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:00.000 --> 00:01:03.000] Actually, Bill's last bullet put me in a coma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:03.000 --> 00:01:07.000] A coma I was to lie in for four years.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:07.000 --> 00:01:11.000] When I woke up, I went on with the movie advertisements</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:11.000 --> 00:01:15.000] referred to as a roaring rampage of revenge.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:15.000 --> 00:01:22.000] I roared, and I rampaged, and I got bloody satisfaction.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:22.000 --> 00:01:26.000] I've killed a hell of a lot of people to get to this point.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:26.000 --> 00:01:29.000] But I have only one more.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:29.000 --> 00:01:31.000] The last one.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:31.000 --> 00:01:35.000] The one I'm driving to right now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:35.000 --> 00:01:38.000] The only one left.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:38.000 --> 00:01:45.000] And when I arrive at my destination, I am gonna kill Bill.</span>
<span class="k">[00:01:45.000 --> 00:02:10.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:10.000 --> 00:02:14.000] Now, the incident that happened at the Two Pines Wedding Chapel</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:14.000 --> 00:02:20.000] that put this whole gory story into motion has since become legend.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:20.000 --> 00:02:22.000] Massacre at Two Pines.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:22.000 --> 00:02:24.000] That's what the newspapers called it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:24.000 --> 00:02:30.000] The local TV news called it the El Paso, Texas Wedding Chapel Massacre.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:30.000 --> 00:02:36.000] How it happened, who was there, how many got killed, and who killed them.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:36.000 --> 00:02:40.000] Changes depending on who's telling the story.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:40.000 --> 00:02:45.000] In actual fact, the massacre didn't happen during a wedding at all.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:45.000 --> 00:02:48.000] It was a wedding rehearsal.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:48.000 --> 00:02:53.000] Now, when we come to the park where I say you may kiss the bride,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:53.000 --> 00:02:55.000] you may kiss the bride.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:55.000 --> 00:02:59.000] But don't stick your tongue in her mouth.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:59.000 --> 00:03:06.000] This might be funny to your friends, but it would be embarrassing to your parents.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:06.000 --> 00:03:10.000] We'll try and restrain ourselves from that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:10.000 --> 00:03:16.000] Y'all got a song?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:16.000 --> 00:03:19.000] How about "Love Me, Tender"? I can play that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:19.000 --> 00:03:21.000] Sure.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:21.000 --> 00:03:24.000] "Love Me, Tender" would be great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:24.000 --> 00:03:27.000] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:27.000 --> 00:03:30.000] Rufus, who was that you used to play for?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:30.000 --> 00:03:32.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:32.000 --> 00:03:34.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:34.000 --> 00:03:35.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:35.000 --> 00:03:39.000] I was a drill, I was a drifter, I was a coaster,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:39.000 --> 00:03:43.000] I was part of the gang, I was a bar-k.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:43.000 --> 00:03:47.000] If they come through Texas, I haven't played with them.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:47.000 --> 00:03:53.000] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:53.000 --> 00:03:56.000] Have you forgotten anything?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:56.000 --> 00:04:00.000] Oh, yes, you forgot the seating arrangements.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:00.000 --> 00:04:02.000] Thank you, Mother.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:02.000 --> 00:04:07.000] Now, the way we normally do this, we have the bride's side,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:07.000 --> 00:04:09.000] and then we have the groom's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:09.000 --> 00:04:12.000] But since the bride ain't got nobody coming,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:12.000 --> 00:04:16.000] and the groom's got far too many people coming...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:16.000 --> 00:04:19.000] Well, yeah, they're coming all the way from Oklahoma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:19.000 --> 00:04:22.000] Right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:22.000 --> 00:04:29.000] Well, I don't see no problem with the groom's side sharing the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:29.000 --> 00:04:30.000] Do you, Mother?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:30.000 --> 00:04:32.000] I don't have a problem with that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:32.000 --> 00:04:38.000] But, honey, you know, it would be good if you had somebody come.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:38.000 --> 00:04:41.000] You know, is that a sign of good faith?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:41.000 --> 00:04:48.000] Well, I don't have anybody, except for Tommy and my friends.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:48.000 --> 00:04:51.000] You have no family?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:51.000 --> 00:04:53.000] Well, I'm working on changing that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:53.000 --> 00:04:57.000] Mrs. Harmony, we're all the family this little angel's ever gonna need.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:57.000 --> 00:05:03.000] I'm not feeling very well, and this bitch is starting to piss me off.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:03.000 --> 00:05:07.000] So while you all blather on, I'm gonna go outside and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:07.000 --> 00:05:09.000] Um, uh, Reverend, sorry.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:09.000 --> 00:05:11.000] She's gonna go out and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:11.000 --> 00:05:13.000] Yeah, given her delicate condition.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:13.000 --> 00:05:16.000] She just needs a few minutes to get it together. She'll be okay.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:16.000 --> 00:05:18.000] Okay.</span>
<span class="k">[00:05:19.000 --> 00:05:22.000] [♪♪♪]</span>
<span class="k">[00:05:23.000 --> 00:05:25.000] [♪♪♪]</span>
<span class="k">[00:05:26.000 --> 00:05:28.000] [♪♪♪]</span>
<span class="k">[00:05:28.000 --> 00:05:30.000] [♪♪♪]</span>
<span class="k">[00:05:30.000 --> 00:05:32.000] [♪♪♪]</span>
<span class="k">[00:05:33.000 --> 00:05:35.000] [♪♪♪]</span>
<span class="k">[00:05:36.000 --> 00:05:38.000] [♪♪♪]</span>
<span class="k">[00:05:39.000 --> 00:05:41.000] [♪♪♪]</span>
<span class="k">[00:05:41.000 --> 00:05:43.000] [♪♪♪]</span>
<span class="k">[00:05:43.000 --> 00:05:45.000] [♪♪♪]</span>
<span class="k">[00:05:45.000 --> 00:05:47.000] [♪♪♪]</span>
<span class="k">[00:05:47.000 --> 00:05:49.000] [♪♪♪]</span>
<span class="k">[00:05:49.000 --> 00:05:51.000] [♪♪♪]</span>
<span class="k">[00:05:51.000 --> 00:05:53.000] [♪♪♪]</span>
<span class="k">[00:05:53.000 --> 00:05:55.000] [♪♪♪]</span>
<span class="k">[00:05:55.000 --> 00:05:57.000] [♪♪♪]</span>
<span class="k">[00:05:57.000 --> 00:05:59.000] [♪♪♪]</span>
<span class="k">[00:05:59.000 --> 00:06:01.000] [♪♪♪]</span>
<span class="k">[00:06:01.000 --> 00:06:03.000] [♪♪♪]</span>
<span class="k">[00:06:03.000 --> 00:06:05.000] [♪♪♪]</span>
<span class="k">[00:06:05.000 --> 00:06:07.000] [♪♪♪]</span>
<span class="k">[00:06:07.000 --> 00:06:09.000] [♪♪♪]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:36.000 --> 00:06:38.000] Hello, kiddo.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:38.000 --> 00:06:45.000] How did you find me?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:45.000 --> 00:06:48.000] I'm the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:48.000 --> 00:06:53.000] What are you doing here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:53.000 --> 00:06:57.000] What am I doing?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:57.000 --> 00:07:03.000] Well, a moment ago I was playing my flute.</span>
<span class="k">[00:07:04.000 --> 00:07:06.000] [♪♪♪]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:06.000 --> 00:07:15.000] At this moment, I'm looking at the most beautiful bride</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:15.000 --> 00:07:18.000] these old eyes have ever seen.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:18.000 --> 00:07:22.000] Why are you here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:22.000 --> 00:07:25.000] Last look.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:25.000 --> 00:07:28.000] Are you gonna be nice?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:28.000 --> 00:07:30.000] I've never been nice my whole life.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:32.000 --> 00:07:34.000] I'm just a guest to be sweet.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:34.000 --> 00:07:42.000] I always told you, your sweet side is your best side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:42.000 --> 00:07:47.000] I guess that's why you're the only one who's ever seen it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:47.000 --> 00:07:52.000] See, you got a bun in the oven.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:52.000 --> 00:07:57.000] I'm knocked up.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:58.000 --> 00:08:01.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:01.000 --> 00:08:04.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:04.000 --> 00:08:07.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:07.000 --> 00:08:10.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:10.000 --> 00:08:13.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:13.000 --> 00:08:16.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:16.000 --> 00:08:19.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:19.000 --> 00:08:22.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:22.000 --> 00:08:25.000] I'm not a man of the way.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:26.000 --> 00:08:28.000] It's hardly a promise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:28.000 --> 00:08:31.000] But you're right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:31.000 --> 00:08:34.000] What does your young man do for a living?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:34.000 --> 00:08:39.000] He owns a used record store here in El Paso.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:39.000 --> 00:08:42.000] A music lover, right?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:42.000 --> 00:08:44.000] He's fond of music.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:44.000 --> 00:08:47.000] Aren't we all?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:47.000 --> 00:08:55.000] And what are you doing for a J-O-B these days?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:56.000 --> 00:08:58.000] I work in the record store.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:58.000 --> 00:09:01.000] Ah, so...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:01.000 --> 00:09:06.000] it all suddenly seems so clear.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:06.000 --> 00:09:10.000] Do you like it?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:10.000 --> 00:09:13.000] Yeah, I like it a lot, smartass.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:13.000 --> 00:09:16.000] I get to listen to music all day,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:16.000 --> 00:09:19.000] talk about music all day. It's really cool.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:19.000 --> 00:09:24.000] It's gonna be a great environment for my little girl to grow up in.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:24.000 --> 00:09:30.000] As opposed to jetting around the world,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:30.000 --> 00:09:32.000] killing human beings,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:32.000 --> 00:09:35.000] and being paid vast sums of money.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:35.000 --> 00:09:39.000] Precisely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:39.000 --> 00:09:41.000] Well, my old friend,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:41.000 --> 00:09:43.000] to each his own.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:43.000 --> 00:09:45.000] However,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:45.000 --> 00:09:49.000] all cock-blockery aside,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:49.000 --> 00:09:52.000] I am looking forward to meeting your young man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:52.000 --> 00:09:55.000] I happen to be more or less particular</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:55.000 --> 00:09:58.000] who my gal marries.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:58.000 --> 00:10:02.000] You wanna come to the wedding?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:02.000 --> 00:10:04.000] Only if I can sit on the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:04.000 --> 00:10:08.000] You'll find it a bit lonely on my side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:08.000 --> 00:10:12.000] Your side always was a bit lonely,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:12.000 --> 00:10:15.000] but I wouldn't sit anywhere else.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:15.000 --> 00:10:19.000] You know,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:19.000 --> 00:10:22.000] I had the loveliest dream about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:22.000 --> 00:10:24.000] Oh, here's Tommy. Call me Arlene.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:24.000 --> 00:10:27.000] You must be Tommy.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:27.000 --> 00:10:29.000] Arlene's told me so much about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:29.000 --> 00:10:31.000] Honey, you okay?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:31.000 --> 00:10:32.000] Oh, I'm fine.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:32.000 --> 00:10:35.000] Tommy, I'd like you to meet my father.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:35.000 --> 00:10:38.000] Oh, my God.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:38.000 --> 00:10:40.000] Oh, my God, this is great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:40.000 --> 00:10:42.000] I'm so glad to meet you, sir.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:42.000 --> 00:10:43.000] Oh, Dad.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:43.000 --> 00:10:45.000] The name's Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:45.000 --> 00:10:47.000] Well, it's great to meet you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:47.000 --> 00:10:50.000] So, Arlene told me you couldn't make it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:50.000 --> 00:10:51.000] Surprise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:51.000 --> 00:10:53.000] That's my pot for you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:53.000 --> 00:10:55.000] Always full of surprises.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:55.000 --> 00:10:58.000] Well, in the surprise department,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:58.000 --> 00:11:01.000] the apple doesn't fall far from the tree.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:01.000 --> 00:11:03.000] When did you get in?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:03.000 --> 00:11:04.000] Just now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:04.000 --> 00:11:06.000] Did you come straight from Australia?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:06.000 --> 00:11:07.000] Of course.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:07.000 --> 00:11:09.000] Daddy, I told Tommy that you were in Perth</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:09.000 --> 00:11:12.000] lining for Silver and no one could reach you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:12.000 --> 00:11:16.000] Lucky for us all, that's not the case.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:16.000 --> 00:11:20.000] So, what's this all about?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:20.000 --> 00:11:22.000] I've heard of wedding rehearsals,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:22.000 --> 00:11:24.000] but I don't believe I've ever heard</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:24.000 --> 00:11:26.000] of a wedding dress rehearsal before.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:26.000 --> 00:11:29.000] We thought, why pay so much money for a dress</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:29.000 --> 00:11:31.000] you're only gonna wear once?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:31.000 --> 00:11:34.000] Especially when Arlene looks so goddamn beautiful in it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:34.000 --> 00:11:38.000] So, I think we're gonna try to get all the mileage we can out of it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:38.000 --> 00:11:41.000] Isn't it supposed to be bad luck</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:41.000 --> 00:11:44.000] for the groom to see the bride in her wedding dress?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:44.000 --> 00:11:46.000] People of the ceremony?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:46.000 --> 00:11:50.000] I guess I just believe in living dangerously.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:50.000 --> 00:11:54.000] I know just what you mean.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:54.000 --> 00:11:57.000] Some...some of us are places to be.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:57.000 --> 00:11:59.000] It's your duty.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:59.000 --> 00:12:01.000] Look, we gotta go through this one more time.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:01.000 --> 00:12:03.000] So, why don't you have a s...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:03.000 --> 00:12:05.000] Oh, my God.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:05.000 --> 00:12:07.000] What am I thinking? You should give her away.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:07.000 --> 00:12:11.000] Tommy, that's not exactly Daddy's cup of tea.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:11.000 --> 00:12:14.000] I think Father would be much more comfortable</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:14.000 --> 00:12:16.000] sitting with the rest of the guests.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:16.000 --> 00:12:18.000] Really?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:18.000 --> 00:12:20.000] That's asking a lot.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:20.000 --> 00:12:24.000] Oh. Okay. We'll forget it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:24.000 --> 00:12:26.000] But how about we go out to dinner tonight and celebrate?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:26.000 --> 00:12:29.000] Only on the condition that I pay for everything.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:29.000 --> 00:12:32.000] Deal. We gotta do this now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:32.000 --> 00:12:33.000] Can I watch?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:33.000 --> 00:12:35.000] Absolutely. Have a seat.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:35.000 --> 00:12:38.000] Which is the bride's side?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:38.000 --> 00:12:40.000] Right over here.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:41.000 --> 00:12:44.000] Father, here we go.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:44.000 --> 00:12:45.000] Yes.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:45.000 --> 00:12:48.000] Now, son, about them vows.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:48.000 --> 00:12:57.000] Belle.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:57.000 --> 00:12:58.000] I just don't want...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:58.000 --> 00:13:01.000] You know only a damn thing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:01.000 --> 00:13:04.000] If he's the man you want,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:04.000 --> 00:13:07.000] then go stand by.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:07.000 --> 00:13:09.000] Stand by.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:09.000 --> 00:13:33.000] Do I look pretty?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:33.000 --> 00:13:35.000] Oh, yes.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:35.000 --> 00:13:37.000] Thank you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:38.000 --> 00:13:40.000] Thank you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:41.000 --> 00:13:43.000] Thank you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:44.000 --> 00:13:46.000] Thank you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:47.000 --> 00:13:49.000] Thank you.</span>
<span class="k">[00:13:49.000 --> 00:13:52.000] [♪♪♪]</span>
<span class="k">[00:13:53.000 --> 00:13:55.000] [♪♪♪]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:55.000 --> 00:13:57.420] (soft music)</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:57.420 --> 00:13:59.320] (slow, dramatic music)</span>
</code></pre></div>
<h2 id="ggml-mediumenbin">ggml-medium.en.bin</h2>
<p>Some stats:</p>
<div class="highlight"><pre><span></span><code><span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">mem</span><span class="w"> </span><span class="n">required</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1899.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="p">(</span><span class="o">+</span><span class="w"> </span><span class="mf">43.00</span><span class="w"> </span><span class="n">MB</span><span class="w"> </span><span class="n">per</span><span class="w"> </span><span class="n">decoder</span><span class="p">)</span>
<span class="n">whisper_model_load</span><span class="p">:</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="n">size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1462.12</span><span class="w"> </span><span class="n">MB</span>
<span class="n">whisper_print_timings</span><span class="p">:</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="n">time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">3563774.75</span><span class="w"> </span><span class="n">ms</span>
</code></pre></div>
<p><strong>So time needed was ~ 3563 seconds (almost 1 hour)</strong></p>
<p>And the actual transcription:</p>
<div class="highlight"><pre><span></span><code><span class="k">[00:00:00.000 --> 00:00:15.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:15.000 --> 00:00:19.000] Do you find me sadistic?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:19.000 --> 00:00:21.000] No, kiddo.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:21.000 --> 00:00:24.000] I'd like to believe</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:24.000 --> 00:00:27.000] you're aware enough, even now,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:27.000 --> 00:00:31.000] to know that there's nothing sadistic</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:31.000 --> 00:00:34.000] in my actions.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:34.000 --> 00:00:38.000] This moment,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:38.000 --> 00:00:41.000] this is me</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:41.000 --> 00:00:44.000] and my most masochistic.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:44.000 --> 00:00:46.000] Well,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:46.000 --> 00:00:48.000] it's your baby.</span>
<span class="k">[00:00:48.000 --> 00:00:50.000] [Gunshot]</span>
<span class="k">[00:00:50.000 --> 00:00:53.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:53.000 --> 00:00:55.000] You looked dead, didn't I?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:55.000 --> 00:00:57.000] Well, I wasn't.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">00:57.000 --> 00:01:00.000] But it wasn't from lack of trying, I can tell you that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:00.000 --> 00:01:03.000] Actually, Bill's last bullet put me in a coma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:03.000 --> 00:01:07.000] A coma I was to lie in for four years.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:07.000 --> 00:01:09.000] When I woke up,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:09.000 --> 00:01:15.000] I went on what the movie advertisements refer to as a "roaring rampage of revenge."</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:15.000 --> 00:01:18.000] I roared, and I rampaged,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:18.000 --> 00:01:22.000] and I got bloody satisfaction.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:22.000 --> 00:01:26.000] I've killed a hell of a lot of people to get to this point.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:26.000 --> 00:01:29.000] But I have only one more.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:29.000 --> 00:01:31.000] The last one.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:31.000 --> 00:01:35.000] The one I'm driving to right now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:35.000 --> 00:01:38.000] The only one left.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:38.000 --> 00:01:42.000] And when I arrive at my destination,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">01:42.000 --> 00:01:45.000] I am gonna kill Bill.</span>
<span class="k">[00:01:45.000 --> 00:02:10.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:10.000 --> 00:02:17.000] Now, the incident that happened at the Two Pines wedding chapel that put this whole gory story into motion</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:17.000 --> 00:02:20.000] has since become legend.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:20.000 --> 00:02:22.000] "Massacre at Two Pines."</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:22.000 --> 00:02:24.000] That's what the newspapers called it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:24.000 --> 00:02:30.000] The local TV news called it the "El Paso, Texas Wedding Chapel Massacre."</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:30.000 --> 00:02:32.000] How it happened.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:32.000 --> 00:02:33.000] Who was there.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:33.000 --> 00:02:36.000] How many got killed and who killed them.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:36.000 --> 00:02:40.000] Changes depending on who's telling the story.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:40.000 --> 00:02:45.000] In actual fact, the massacre didn't happen during a wedding at all.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:45.000 --> 00:02:48.000] It was a wedding rehearsal.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:48.000 --> 00:02:55.000] Now, when we come to the part where I say, "You may kiss the bride, you may kiss the bride,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:55.000 --> 00:02:59.000] but don't stick your tongue in her mouth."</span>
<span class="na">[00</span><span class="o">:</span><span class="s">02:59.000 --> 00:03:06.000] This might be funny to your friends, but it would be embarrassing to your parents.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:06.000 --> 00:03:11.000] We'll try to restrain ourselves from it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:11.000 --> 00:03:16.000] Y'all got a song?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:16.000 --> 00:03:20.000] How about "Love Me Tender." I can play that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:20.000 --> 00:03:22.000] Sure.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:22.000 --> 00:03:24.000] "Love Me Tender" would be great.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:24.000 --> 00:03:27.000] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:27.000 --> 00:03:30.000] Rufus, who was that you used to play for?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:30.000 --> 00:03:32.000] Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:32.000 --> 00:03:35.000] Rufus Thomas. Rufus Thomas.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:35.000 --> 00:03:43.000] I was a drill, I was a drifter, I was a coaster, I was part of the gang, I was a bar-quet.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:43.000 --> 00:03:47.000] If they come through Texas, I done played with them.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:47.000 --> 00:03:51.000] Rufus, he's the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:54.000 --> 00:03:57.000] Have you ever forgotten anything?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">03:57.000 --> 00:04:00.000] Oh yes, you forgot the seating arrangements.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:00.000 --> 00:04:03.000] Thank you, Mother.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:03.000 --> 00:04:10.000] Now, the way we normally do this, we have the bride's side and then we have the groom's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:10.000 --> 00:04:17.000] But since the bride ain't got nobody coming, and the groom's got far too many people coming.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:17.000 --> 00:04:21.000] Well yeah, they're coming all the way from Oklahoma.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:21.000 --> 00:04:30.000] Right. Well I don't see no problem with the groom's side sharing the bride's side. Do you, Mother?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:30.000 --> 00:04:32.000] No, I don't have a problem with that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:32.000 --> 00:04:38.000] But, honey, you know it would be good if you had somebody come.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:38.000 --> 00:04:42.000] You know, is it a sign of good faith?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:42.000 --> 00:04:49.000] Well, I don't have anybody. Except for Tommy and my friends.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:49.000 --> 00:04:52.000] You have no family?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:52.000 --> 00:04:54.000] Well, I'm working on changing that.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:54.000 --> 00:04:58.000] Mrs. Harmony, we're all the family this little angel's ever gonna need.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">04:58.000 --> 00:05:04.000] I'm not feeling very well, and this bitch is starting to piss me off.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:04.000 --> 00:05:08.000] So while you all blather on, I'm gonna go outside and get some air.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:08.000 --> 00:05:10.000] Um, uh, Reverend, sorry.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:10.000 --> 00:05:12.000] She's gonna go out and get some air?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:12.000 --> 00:05:14.000] Yeah, given her delicate condition.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">05:14.000 --> 00:05:19.000] She just needs a few minutes to get it together. She'll be okay.</span>
<span class="k">[00:05:20.000 --> 00:05:25.000] [Music]</span>
<span class="k">[00:05:26.000 --> 00:05:31.000] [Music]</span>
<span class="k">[00:05:31.000 --> 00:05:36.000] [Music]</span>
<span class="k">[00:05:36.000 --> 00:05:41.000] [Music]</span>
<span class="k">[00:05:41.000 --> 00:05:46.000] [Music]</span>
<span class="k">[00:05:46.000 --> 00:05:51.000] [Music]</span>
<span class="k">[00:05:51.000 --> 00:05:56.000] [Music]</span>
<span class="k">[00:05:56.000 --> 00:06:01.000] [Music]</span>
<span class="k">[00:06:01.000 --> 00:06:06.000] [Music]</span>
<span class="k">[00:06:06.000 --> 00:06:11.000] [Music]</span>
<span class="k">[00:06:11.000 --> 00:06:16.000] [Music]</span>
<span class="k">[00:06:16.000 --> 00:06:21.000] [Music]</span>
<span class="k">[00:06:21.000 --> 00:06:26.000] [Music]</span>
<span class="k">[00:06:26.000 --> 00:06:31.000] [Music]</span>
<span class="k">[00:06:31.000 --> 00:06:36.000] [Music]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:36.000 --> 00:06:38.000] Hello, kiddo.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:38.000 --> 00:06:45.000] How did you find me?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:45.000 --> 00:06:48.000] I'm the man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:48.000 --> 00:06:53.000] What are you doing here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:55.000 --> 00:06:57.000] What am I doing?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">06:57.000 --> 00:07:03.000] Well, a moment ago I was playing my flute.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:03.000 --> 00:07:17.000] This moment, I'm looking at the most beautiful bride these whole eyes have ever seen.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:17.000 --> 00:07:21.000] Why are you here?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:21.000 --> 00:07:23.000] Last look.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:24.000 --> 00:07:26.000] Are you gonna be nice?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:26.000 --> 00:07:29.000] I've never been nice my whole life.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:29.000 --> 00:07:34.000] But I'll do my best to be sweet.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:34.000 --> 00:07:41.000] I always told you, your sweet side is your best side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:41.000 --> 00:07:46.000] I guess that's why you're the only one who's ever seen it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:46.000 --> 00:07:51.000] See, you got a bun in the oven.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:52.000 --> 00:07:53.000] Hmm.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:53.000 --> 00:07:56.000] I'm knocked up.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:56.000 --> 00:07:59.000] Jeez, Louise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">07:59.000 --> 00:08:04.000] That young man of yours sure doesn't believe in wasting time, does he?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:04.000 --> 00:08:07.000] Have you seen Tommy?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:07.000 --> 00:08:11.000] Big guy in the tux?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:11.000 --> 00:08:12.000] Yes.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:12.000 --> 00:08:14.000] Then I saw him.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:14.000 --> 00:08:18.000] I like his hair.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:20.000 --> 00:08:22.000] You promised you'd be nice.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:22.000 --> 00:08:28.000] I said I'd do my best. That's hardly a promise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:28.000 --> 00:08:30.000] But you're right.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:30.000 --> 00:08:34.000] What does your young man do for a living?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:34.000 --> 00:08:39.000] He owns a used record store here in El Paso.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:39.000 --> 00:08:41.000] Ah. Music lover, eh?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:41.000 --> 00:08:44.000] He's fond of music.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:44.000 --> 00:08:47.000] Aren't we all?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:48.000 --> 00:08:52.000] And what are you doing for a J.O.B. these days?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:52.000 --> 00:08:55.000] I work in the record store.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:55.000 --> 00:08:59.000] Ah, so...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">08:59.000 --> 00:09:03.000] it all suddenly seems so clear.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:03.000 --> 00:09:07.000] Do you like it?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:07.000 --> 00:09:10.000] Yeah, I like it a lot, smartass.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:10.000 --> 00:09:13.000] I get to listen to music all day.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:14.000 --> 00:09:17.000] Talk about music all day. It's really cool.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:17.000 --> 00:09:22.000] It's gonna be a great environment for my little girl to grow up in.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:22.000 --> 00:09:30.000] As opposed to jetting around the world, killing human beings,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:30.000 --> 00:09:33.000] and being paid vast sums of money?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:33.000 --> 00:09:36.000] Precisely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:36.000 --> 00:09:39.000] Well, my old friend,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:39.000 --> 00:09:41.000] to each his own,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:42.000 --> 00:09:44.000] to each his own.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:44.000 --> 00:09:49.000] However, all cock-luckery aside,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:49.000 --> 00:09:52.000] I am looking forward to meeting your young man.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:52.000 --> 00:09:56.000] I happen to be more or less particular,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:56.000 --> 00:09:58.000] whom my gout marries.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">09:58.000 --> 00:10:02.000] You wanna come to the wedding?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:02.000 --> 00:10:05.000] Only if I can sit on the bride's side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:05.000 --> 00:10:09.000] You'll find it a bit lonely on my side.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:09.000 --> 00:10:12.000] Your side always was a bit lonely.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:12.000 --> 00:10:15.000] But I wouldn't sit anywhere else.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:15.000 --> 00:10:19.000] You know,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:19.000 --> 00:10:22.000] I had the loveliest dream about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:22.000 --> 00:10:25.000] Oh, here's Tommy. Call me Arlene.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:25.000 --> 00:10:27.000] You must be Tommy.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:27.000 --> 00:10:29.000] Arlene's told me so much about you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:29.000 --> 00:10:31.000] Arlene, you okay?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:31.000 --> 00:10:32.000] Oh, I'm fine.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:32.000 --> 00:10:35.000] Tommy, I'd like you to meet my father.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:35.000 --> 00:10:38.000] Oh, my God!</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:38.000 --> 00:10:40.000] Oh, my God! This is great!</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:40.000 --> 00:10:42.000] I'm so glad to meet you, sir.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:42.000 --> 00:10:43.000] Oh, Dad.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:43.000 --> 00:10:45.000] The name's Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:45.000 --> 00:10:47.000] Well, it's great to meet you, Bill.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:47.000 --> 00:10:49.000] Arlene told me you couldn't make it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:49.000 --> 00:10:51.000] Surprise.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:51.000 --> 00:10:52.000] That's my pop for you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:52.000 --> 00:10:54.000] Always full of surprises.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:54.000 --> 00:10:57.000] Well, in the surprise department,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">10:57.000 --> 00:11:00.000] the apple doesn't fall far from the tree.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:00.000 --> 00:11:02.000] When did you get in?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:02.000 --> 00:11:03.000] Just now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:03.000 --> 00:11:05.000] Did you come straight from Australia?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:05.000 --> 00:11:07.000] Of course.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:07.000 --> 00:11:09.000] Daddy, I told Tommy that you were in Perth</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:09.000 --> 00:11:12.000] mining for silver, and no one could reach you.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:12.000 --> 00:11:16.000] Lucky for us all, that's not the case.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:16.000 --> 00:11:20.000] So, what's this all about?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:20.000 --> 00:11:22.000] I've heard of wedding rehearsals,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:22.000 --> 00:11:24.000] but I don't believe I've ever heard</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:24.000 --> 00:11:27.000] of a wedding dress rehearsal before.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:27.000 --> 00:11:28.000] We thought,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:28.000 --> 00:11:29.000] "Why pay so much money for a dress</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:29.000 --> 00:11:31.000] you're only gonna wear once?"</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:31.000 --> 00:11:34.000] Especially when Arlene looks so goddamn beautiful in it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:34.000 --> 00:11:36.000] So, uh, I think we're gonna try to get all the mileage</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:36.000 --> 00:11:37.000] we can out of it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:37.000 --> 00:11:41.000] Isn't it supposed to be bad luck</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:41.000 --> 00:11:44.000] for the groom to see the bride in her wedding dress</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:44.000 --> 00:11:46.000] before the ceremony?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:46.000 --> 00:11:50.000] Well, I guess I just believe I live in danger, so...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:50.000 --> 00:11:54.000] I know just what you mean.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:54.000 --> 00:11:57.000] Son, some of us have places to be.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:57.000 --> 00:11:59.000] It's your old dude.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">11:59.000 --> 00:12:01.000] Look, we gotta go through this one more time.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:01.000 --> 00:12:03.000] So, uh, why don't you have a s--</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:03.000 --> 00:12:05.000] Oh, my God.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:05.000 --> 00:12:07.000] What am I thinking? You should give her away.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:07.000 --> 00:12:11.000] Tommy, that's not exactly Daddy's cup of tea.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:11.000 --> 00:12:14.000] I think Father would be much more comfortable</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:14.000 --> 00:12:16.000] sitting with the rest of the guests.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:16.000 --> 00:12:17.000] Really?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:17.000 --> 00:12:19.000] That's asking a lot.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:19.000 --> 00:12:21.000] Oh.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:21.000 --> 00:12:23.000] Okay. Well, forget it.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:23.000 --> 00:12:26.000] But how about we go out to dinner tonight and celebrate?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:26.000 --> 00:12:29.000] Only on the condition that I pay for everything.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:29.000 --> 00:12:31.000] Deal. We gotta do this now.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:31.000 --> 00:12:33.000] Can I watch?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:33.000 --> 00:12:35.000] Absolutely. Have a seat.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:35.000 --> 00:12:37.000] Which is the bride's side?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:37.000 --> 00:12:39.000] Right over here.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:39.000 --> 00:12:44.000] Mother, here we go.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:44.000 --> 00:12:49.000] Now, son, about them vows.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:49.000 --> 00:12:57.000] No.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:57.000 --> 00:12:59.000] I just don't want...</span>
<span class="na">[00</span><span class="o">:</span><span class="s">12:59.000 --> 00:13:02.000] You don't owe me a damn thing.</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:03.000 --> 00:13:05.000] If he's the man you want,</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:05.000 --> 00:13:08.000] then go stand by.</span>
<span class="k">[00:13:08.000 --> 00:13:10.000] [chuckles]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:10.000 --> 00:13:34.000] Do I look pretty?</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:34.000 --> 00:13:36.000] Oh, yeah.</span>
<span class="k">[00:13:36.000 --> 00:13:39.000] [♪♪♪]</span>
<span class="na">[00</span><span class="o">:</span><span class="s">13:39.000 --> 00:13:48.000] Thank you.</span>
<span class="k">[00:13:48.000 --> 00:13:51.000] [♪♪♪]</span>
<span class="k">[00:13:51.000 --> 00:13:54.000] [♪♪♪]</span>
<span class="k">[00:13:54.000 --> 00:13:57.000] [♪♪♪]</span>
<span class="k">[00:13:57.000 --> 00:13:59.920] [MUSIC]</span>
</code></pre></div>
<h2 id="comparison-of-results">Comparison of results</h2>
<table>
<thead>
<tr>
<th>tiny 160 s</th>
<th>base 433 s</th>
<th>small 1713 s</th>
<th>medium 3563 s</th>
</tr>
</thead>
<tbody>
<tr>
<td>Do you finally sit just now?</td>
<td>Do you find me sadistic?</td>
<td>Do you find me sadistic?</td>
<td>Do you find me sadistic?</td>
</tr>
<tr>
<td>[<span class="caps">MUSIC</span> <span class="caps">PLAYING</span>]</td>
<td>No, Kato.</td>
<td>No, kiddo.</td>
<td>No, kiddo.</td>
</tr>
<tr>
<td>No, I can’t do.</td>
<td>I’d like to believe</td>
<td>I’d like to believe you’re aware enough, even now,</td>
<td>I’d like to believe</td>
</tr>
<tr>
<td>I’d like to believe you’re aware enough even now.</td>
<td>you’re aware enough, even now.</td>
<td>to know that there is nothing sadistic in my actions.</td>
<td>you’re aware enough, even now,</td>
</tr>
<tr>
<td>No, that there is nothing suggesting in my actions.</td>
<td>I know that there is nothing sadistic in my actions.</td>
<td>This moment, this is me and my most nicer kiss to be.</td>
<td>to know that there’s nothing sadistic</td>
</tr>
<tr>
<td>This moment, this is me and my most nice against them.</td>
<td>This moment,</td>
<td>Well, it’s your baby.</td>
<td>in my actions.</td>
</tr>
<tr>
<td>Well, it’s your name.</td>
<td>this is me,</td>
<td>Look, Dad, didn’t I?</td>
<td>This moment,</td>
</tr>
<tr>
<td>[<span class="caps">MUSIC</span> <span class="caps">PLAYING</span>]</td>
<td>and I most miss the case.</td>
<td>Well, I wasn’t. But it wasn’t from lack of trying, I can tell you that.</td>
<td>this is me</td>
</tr>
<tr>
<td>But Dad didn’t I?</td>
<td>Well,</td>
<td>Actually, Bill’s last bullet put me in a coma.</td>
<td>and my most masochistic.</td>
</tr>
<tr>
<td>Well, I wasn’t.</td>
<td>it’s your baby.</td>
<td>A coma I was to lie in for four years.</td>
<td>Well,</td>
</tr>
<tr>
<td>But it wasn’t from lack of trying.</td>
<td>The dead didn’t I?</td>
<td>When I woke up, I went on with the movie advertisements</td>
<td>it’s your baby.</td>
</tr>
<tr>
<td>I can tell you that.</td>
<td>Well, I wasn’t.</td>
<td>referred to as a roaring rampage of revenge.</td>
<td>[Gunshot]</td>
</tr>
<tr>
<td>Actually, Bill’s last bullet put me in a coma.</td>
<td>But it wasn’t from lack of try and I can tell you that.</td>
<td>I roared, and I rampaged, and I got bloody satisfaction.</td>
<td>[Music]</td>
</tr>
<tr>
<td>A coma, I was to lie in for four years.</td>
<td>Actually, Bill’s last bullet put me in a coma.</td>
<td>I’ve killed a hell of a lot of people to get to this point.</td>
<td>You looked dead, didn’t I?</td>
</tr>
<tr>
<td>And I woke up.</td>
<td>A coma I was to lie in for four years.</td>
<td>But I have only one more.</td>
<td>Well, I wasn’t.</td>
</tr>
<tr>
<td>I went on with a movie advertisements</td>
<td>When I woke up,</td>
<td>The last one.</td>
<td>But it wasn’t from lack of trying, I can tell you that.</td>
</tr>
<tr>
<td>for two as a roaring rampage of revenge.</td>
<td>I went on with the movie advertisements referred to as a roaring rampage of</td>
<td>The one I’m driving to right now.</td>
<td>Actually, Bill’s last bullet put me in a coma.</td>
</tr>
<tr>
<td>I roared.</td>
<td>I roared,</td>
<td>The only one left.</td>
<td>A coma I was to lie in for four years.</td>
</tr>
<tr>
<td>And I relanged.</td>
<td>and I rampaged,</td>
<td>And when I arrive at my destination, I am gonna kill Bill.</td>
<td>When I woke up,</td>
</tr>
<tr>
<td>And I got bloody satisfaction.</td>
<td>and I got bloody satisfaction.</td>
<td></td>
<td>I went on what the movie advertisements refer to as a “roaring rampage of revenege.”</td>
</tr>
<tr>
<td>I’ve killed a hell of a lot of people to get to this point.</td>
<td>I’ve killed a hell of a lot of people who get to this point.</td>
<td></td>
<td>I roared, and I rampaged,</td>
</tr>
<tr>
<td>But I have only one more.</td>
<td>But I have only one more.</td>
<td></td>
<td>and I got bloody satisfaction.</td>
</tr>
<tr>
<td>The last one.</td>
<td>The last one.</td>
<td></td>
<td>I’ve killed a hell of a lot of people to get to this point.</td>
</tr>
<tr>
<td>The one I’m driving to right now.</td>
<td>The one I’m driving to right now.</td>
<td></td>
<td>But I have only one more.</td>
</tr>
<tr>
<td>The only one left.</td>
<td>The only one left.</td>
<td></td>
<td>The last one.</td>
</tr>
<tr>
<td>And when I arrive at my destination,</td>
<td>And when I arrive at my destination,</td>
<td></td>
<td>The one I’m driving to right now.</td>
</tr>
<tr>
<td>I have going to kill Bill.</td>
<td>I am gonna kill Bill.</td>
<td></td>
<td>The only one left.</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td>And when I arrive at my destination,</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td>I am gonna kill Bill.</td>
</tr>
</tbody>
</table>
<h1 id="conclusion">Conclusion</h1>
<p>If you take a peek at the above results (and you remember the movie or download an .srt of the actual subtitles) you’ll see that the results for the small and medium model were almost perfect! The base model was also good enough considering that it took much less time than these two. </p>
<p>The tiny model wasn’t so good however even that model is good enough to understand everything that is being said in the movie from the subtitles and context. Finally, consider that both the tiny and base models were faster than real-time even on my (very slow) computer.</p>HTML form disable after submit2023-05-12T15:20:00+03:002023-05-12T15:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2023-05-12:/2023/05/12/disable-your-forms/<p>A simple way to improve the functionality of your <span class="caps">HTML</span> forms by disabling the submit button after submitting.</p><p>One of the most common problems I get in my apps is double submissions of forms. A lot of users
can’t understand the difference between single and double click and end up double clicking
the form submit button. Also, if the form takes too long to submit they might thing that they didn’t
press the button correctly and click it again.</p>
<p>This, depending on how your app is built could result in either working perfectly, or showing errors
to users or (which is the worst) duplicate entries in your database.</p>
<p>There is a <em>very simple</em> fix for that: Disable the submit button after the first click. Here’s how to
do it with jQuery:</p>
<div class="highlight"><pre><span></span><code><span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">ready</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'form'</span><span class="p">).</span><span class="nx">submit</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">submit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">find</span><span class="p">(</span><span class="s1">':input[type=submit]'</span><span class="p">)</span>
<span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">prop</span><span class="p">(</span><span class="s1">'disabled'</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">submit</span><span class="p">.</span><span class="nx">val</span><span class="p">())</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">val</span><span class="p">(</span><span class="nx">submit</span><span class="p">.</span><span class="nx">val</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' ⌛'</span><span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">html</span><span class="p">(</span><span class="nx">submit</span><span class="p">.</span><span class="nx">html</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' ⌛'</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">})</span>
<span class="p">});</span>
</code></pre></div>
<p>and with vanilla.js if you don’t use jquery</p>
<div class="highlight"><pre><span></span><code><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'DOMContentLoaded'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">'form'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">form</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">form</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'submit'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">submit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'input[type="submit"], button[type="submit"]'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">disabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">submit</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' ⌛'</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' ⌛'</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">});</span>
</code></pre></div>
<p>The above will find all forms and add an event listener to the submit event. When the form is submitted
it will find the submit button and disable it. Finally, it adds a unicode hourglass character (⌛) to the
displayed button text so the user gets a quick feedback that the form is being submitted.</p>
<p>The above snippets should work correctly no matter if you use
an <code><input type="submit"></code> or a <code><button type="submit"></code> element (that’s why we use <code>:input</code> in jquery to capture both types of elements or we do the double check on the <code>querySelector</code>, also notice that it checks if the element has a <code>val()/value</code> and sets
sets <code>val()/value</code> or <code>html()/innerHTML</code> accordingly).</p>
<p>I use the above snippet on every project I work on and it has saved me a lot of headaches. Please be advised that if you do funny <span class="caps">JS</span> things with your form this snippet may not work and break its functionality, but in this case you are probably handling the form disabling yourself.</p>Multiple storages for the same FileField in Django2023-04-11T14:20:00+03:002023-04-11T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2023-04-11:/2023/04/11/django-multiple-file-storages/<p>Using multiple file storages from the same FileField in Django</p><p>When you need to support user-uploaded files from Django (usually called <em>media</em>)
you will probably use a <a href="https://docs.djangoproject.com/en/4.2/topics/files/">FileField</a>
in your models. This is translated to a simple varchar (text) field in the database that
contains a unique identifier for the file. This usually would be the path to the file,
however this is not always the case!</p>
<p>What really happens is that Django has an underlying concept called a
<a href="https://docs.djangoproject.com/en/4.2/topics/files/#file-storage">File Storage</a>
which is a class that has information on how to talk to the actual storage backend,
and particularly how to translate the unique identifier stored on the db to an actual file object.
By default
Django stores files in the file system using the
<a href="https://docs.djangoproject.com/en/4.2/ref/files/storage/#django.core.files.storage.FileSystemStorage">FileSystemStorage</a>
however it is possible to use different backends through an add-on
(for example <a href="https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html">Amazon S3</a>)
or even write your own.</p>
<p>Each FileField can be configured to use a different storage backend by passing the <code>storage</code> parameter;
if you don’t use this parameter then the default storage backend is used. So you can easily configure a <code>FileField</code>
that would upload files to your filesystem and another one that would upload files to S3.</p>
<p>However, one thing that is not
supported though is to use multiple storages for <em>the same</em> FileField depending on some parameter of the model instance.
Unfortunately, in a recent project I had to do exactly that: We had a <code>FileField</code> on a model that contained
hundreds of GBs of files stored on the filesystem; we wanted to be able to upload the files of new
instances of that model on S3 but also wanted to keep the old files on the filesystem to avoid moving all these
to S3 (which would result to a lot of downtime). I also wanted a way to be “flexible” on this i.e to be able
to change again the storage backend for some instances if needed and definitely not move/copy all these files!</p>
<p>If you take a peek
at the FileField options you’ll see that there’s a <code>storage</code> parameter that can be a
<a href="https://docs.djangoproject.com/en/4.2/topics/files/#using-a-callable">callable</a>. However this
callable is initialized with the models and is not evaluated again until the app is restarted so it can’t be used
to decide on the storage for each model instance. </p>
<p>The only thing that is evaluated each time a file is uploaded through the FileField is when <code>upload_to</code>
is a function. This function receives the model instance and returns the path that the file will be uploaded to.</p>
<p>The idea is to use this <code>upload_to</code> function to return a different path depending on the model instance
and then use a custom storage backend that will use the path to decide on the actual storage backend to use.</p>
<p>This is the code I ended up with for the <code>upload_to</code> function:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">file_upload_path</span><span class="p">(</span><span class="n">instance</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span>
<span class="n">dt_str</span> <span class="o">=</span> <span class="n">app</span><span class="o">.</span><span class="n">created_on</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y/%m/</span><span class="si">%d</span><span class="s2">"</span><span class="p">)</span>
<span class="n">file_storage</span> <span class="o">=</span> <span class="s2">""</span>
<span class="k">if</span> <span class="n">instance</span><span class="o">.</span><span class="n">id</span> <span class="o">>=</span> <span class="n">settings</span><span class="o">.</span><span class="n">STORAGE_CHANGE_ID</span><span class="p">:</span>
<span class="n">file_storage</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">STORAGE_SELECTION_STR</span> <span class="o">+</span> <span class="s2">"/"</span>
<span class="k">return</span> <span class="s2">"protected/</span><span class="si">{0}{1}</span><span class="s2">/</span><span class="si">{2}</span><span class="s2">/</span><span class="si">{3}</span><span class="s2">"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">file_storage</span><span class="p">,</span> <span class="n">dt_str</span><span class="p">,</span> <span class="n">instance</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Model</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">file</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">FileField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="n">file_upload_path</span><span class="p">)</span>
</code></pre></div>
<p>What happens here is that I have a setting <code>STORAGE_CHANGE_ID</code> that is the <code>id</code> of the instance
after which all instances will use the different storage backend. You can use whatever method
you want here to decide on the storage that would be used; the only thing to keep in mind is to
put the storage <em>somewhere on the returned path</em>.</p>
<p>I also have a setting
<code>STORAGE_SELECTION_STR</code> that is the string that will be used in the path to differentiate the storage backend.
The <code>STORAGE_SELECTION_STR</code> has the value of <code>minios3</code> for this project.</p>
<p>Using this function the paths of the instances that are >= <code>STORAGE_CHANGE_ID</code> will be of the form <code>protected/minios3/2021/04/11/1234/filename.ext</code> while for the old files these will be of the form <code>protected/2021/04/11/1234/filename.ext</code>. Notice the <code>minios3</code> string in between. </p>
<p>Of course this is not enough. We also need to tell Django to use the different storage backend for the new files. In order
to do this we have to implement a <a href="https://docs.djangoproject.com/en/4.2/howto/custom-file-storage/">custom storage class</a> like this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.core.files.storage</span> <span class="kn">import</span> <span class="n">FileSystemStorage</span><span class="p">,</span> <span class="n">Storage</span>
<span class="kn">from</span> <span class="nn">storages.backends.s3boto3</span> <span class="kn">import</span> <span class="n">S3Boto3Storage</span>
<span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="k">class</span> <span class="nc">FilenameBasedStorage</span><span class="p">(</span><span class="n">Storage</span><span class="p">):</span>
<span class="n">minio_choice</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">STORAGE_SELECTION_STR</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_open</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">mode</span><span class="o">=</span><span class="s2">"rb"</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">mode</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">mode</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">content</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">delete</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">exists</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">size</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">url</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">url</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">url</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">path</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">minio_choice</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
<span class="k">return</span> <span class="n">S3Boto3Storage</span><span class="p">()</span><span class="o">.</span><span class="n">path</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">FileSystemStorage</span><span class="p">()</span><span class="o">.</span><span class="n">path</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
</code></pre></div>
<p>This class should be self explainable: It uses the <code>settings.STORAGE_SELECTION_STR</code>
we mentioned above to decide which storage backend to use and then it forwards each
method to the corresponding backend (either the filesystem storage or the S3 storage).</p>
<p>One thing to notice is that the <code>django.core.files.storage.Storage</code> class this class
inherits from has more methods that can be implemented (and would raise if called without implementing them)
however this implementation works fine for my needs.</p>
<p>One question some readers may have is what happens if the user uploads a file named <code>test-minios3.pdf</code> (i.e a file containing the
<code>STORAGE_SELECTION_STR</code>). Well you may just as well ignore it; it will just be saved always on the minio-s3
storage backend. Or you can make sure to <em>remove</em> that string from the filename before saving it on the <code>file_upload_path</code>.
I chose to ignore it since it doesn’t matter for my use case.</p>
<p>Finally, we need to tell Django to use this storage class for the <code>file</code> field. We can do this by
adding it to the <code>FileField</code> like:</p>
<div class="highlight"><pre><span></span><code><span class="n">file</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">FileField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="n">file_upload_path</span><span class="p">,</span> <span class="n">storage</span><span class="o">=</span><span class="n">FilenameBasedStorage</span><span class="p">)</span>
</code></pre></div>
<p>or we can configure it on the <code>DEFAULT_FILE_STORAGE</code> setting
(<a href="https://docs.djangoproject.com/en/4.2/ref/settings/#default-file-storage">for Django < 4.2</a>)
or on the
<code>STORAGES</code> dict (<a href="https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-STORAGES">for Django >= 4.2</a>).</p>
<p>I hope this helps someone else that needs to do something similar!</p>Using Unpoly with Django2023-04-04T10:20:00+03:002023-04-04T10:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2023-04-04:/2023/04/04/using-unpoly-with-django/<p>A guide on using Unpoly with Django</p><div class="toc"><span class="toctitle">Table of contents:</span><ul>
<li><a href="#the-unpoly-demo">The unpoly demo</a></li>
<li><a href="#integrating-unpoly-with-django">Integrating unpoly with Django</a></li>
<li><a href="#unpoly-configuration">Unpoly configuration</a></li>
<li><a href="#navigation-improvements">Navigation improvements</a><ul>
<li><a href="#using-the-up-main">Using the up-main</a></li>
<li><a href="#make-all-links-followable">Make all links followable</a></li>
<li><a href="#navigation-feedback">Navigation feedback</a></li>
<li><a href="#aliases-for-navigation-feedback">Aliases for navigation feedback</a></li>
</ul>
</li>
<li><a href="#handling-forms">Handling forms</a><ul>
<li><a href="#integrating-with-messages">Integrating with messages</a></li>
<li><a href="#immediate-form-validation">Immediate form validation</a></li>
<li><a href="#other-form-helpers">Other form helpers</a></li>
</ul>
</li>
<li><a href="#understanding-layers">Understanding layers</a><ul>
<li><a href="#static-overlay-content">Static overlay content</a></li>
<li><a href="#advanced-layersoverlays">Advanced layers/overlays</a></li>
<li><a href="#opening-new-layers-over-existing-ones">Opening new layers over existing ones</a></li>
<li><a href="#closing-overlays">Closing overlays</a><ul>
<li><a href="#closing-the-overlay-when-visiting-a-link">Closing the overlay when visiting a link</a></li>
<li><a href="#handling-hardcoded-urls">Handling hardcoded urls</a></li>
<li><a href="#explicitly-closing-the-layer">Explicitly closing the layer</a></li>
<li><a href="#closing-the-layer-by-emitting-an-unpoly-event">Closing the layer by emitting an unpoly event</a></li>
</ul>
</li>
<li><a href="#doing-stuff-when-a-layer-is-closed">Doing stuff when a layer is closed</a></li>
<li><a href="#overlays-and-messages">Overlays and messages</a><ul>
<li><a href="#improving-delete">Improving delete</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#improving-interaction-with-django-packages">Improving interaction with Django packages</a></li>
<li><a href="#advanced-concepts">Advanced concepts</a><ul>
<li><a href="#more-about-upcompiler">More about up.compiler</a></li>
<li><a href="#passing-context-from-unpoly-to-server">Passing context from unpoly to server</a></li>
<li><a href="#listening-to-unpoly-events">Listening to unpoly events</a></li>
<li><a href="#updating-history">Updating history</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</div>
<p>Over the past few years, there has been a surge in the popularity of frontend frameworks, such as React and Vue. While there are certainly valid use cases for these frameworks, I believe that they are often unnecessary, as most web applications can be adequately served by traditional request/response web pages without any frontend framework. The high usage of these frameworks is largely driven by <span class="caps">FOMO</span> and lack of knowledge about alternatives. However, using such frameworks can add unnecessary complexity to your project, as you now have to develop two projects in parallel (the frontend and the backend) and maintain two separate codebases.</p>
<p>That being said, I understand that some projects may require extra <span class="caps">UX</span> enhancements such as modals, navigation and form submissions without full page reloads, immediate form validation feedback, page fragment updates etc. If you want some of this functionality but do not want to hop on the <span class="caps">JS</span> framework train, you can use the
<a href="https://unpoly.com/">Unpoly</a> library. </p>
<p>Unpoly is similar to other libraries like intercooler, htmx or turbo however I find it to be the easiest to be used in the kind of projects I work on. These libraries allow you to write dynamic web applications with minimal changes to your existing server-side code. </p>
<p>In this guide, we’ll go over how to use Unpoly with Django. Specifically, we’ll cover the following topics:</p>
<ul>
<li>An unpoly demo</li>
<li>Integrating unpoly with Django</li>
<li>Navigation improvements</li>
<li>Form improvements</li>
<li>Modal improvements (layers)</li>
<li>Integration with (some) django packages</li>
<li>More advanced concepts</li>
</ul>
<h2 id="the-unpoly-demo">The unpoly demo</h2>
<p>Unpoly provides a <a href="https://demo.unpoly.com/">demo application</a> written in Ruby. You can go on and play with it for a bit to understand what it offers compared to a traditional web app.</p>
<p>I’ve re-implemented this in Django
so you can compare the code with a non-unpoly Django app. It can be found on https://github.com/spapas/django-unpoly-demo
and the actual demo in Django is at: https://unpoly-demo.spapas.net or https://unpoly-demo.fly.dev/ (deployed on fly.io)
or https://unpoly-demo.onrender.com/ (deployed on render.com; notice the free tier of render.com is very slow, this isn’t
related to the app). The demo app uses an ephemeral database so the data may be deleted at any time.</p>
<p>Try navigating the demo site and you’ll see things like:</p>
<ul>
<li>Navigation feedback</li>
<li>Navigation without page reloads</li>
<li>Forms opening in modals</li>
<li>Modals over modals</li>
<li>Form submissions without page reloads</li>
<li>Form validation feedback without page reloads</li>
</ul>
<p>All this is implemented mostly with traditional Django class based views and templates in addition to a few unpoly attributes.</p>
<p>To understand how much of a difference this makes, after you have taken a peek at the “companies” functionality in the demo, take
a look at the actual code that implementation (I’m only pasting the views, the other components are exactly the same as in a normal Django app):</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">FormMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">is_valid</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">up</span><span class="o">.</span><span class="n">validate</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"success_message"</span><span class="p">):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">success_message</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="n">form</span><span class="o">=</span><span class="n">form</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_initial</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">initial</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_initial</span><span class="p">()</span>
<span class="n">initial</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">dict</span><span class="p">())</span>
<span class="k">return</span> <span class="n">initial</span>
<span class="k">class</span> <span class="nc">CompanyListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="k">class</span> <span class="nc">CompanyDetailView</span><span class="p">(</span><span class="n">DetailView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="k">class</span> <span class="nc">CompanyCreateView</span><span class="p">(</span><span class="n">FormMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s2">"Company created successfully"</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"name"</span><span class="p">,</span> <span class="s2">"address"</span><span class="p">]</span>
<span class="k">class</span> <span class="nc">CompanyUpdateView</span><span class="p">(</span><span class="n">FormMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s2">"Company updated successfully"</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"name"</span><span class="p">,</span> <span class="s2">"address"</span><span class="p">]</span>
<span class="k">class</span> <span class="nc">CompanyDeleteView</span><span class="p">(</span><span class="n">DeleteView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="k">def</span> <span class="nf">get_success_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">reverse</span><span class="p">(</span><span class="s2">"company-list"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">up</span><span class="o">.</span><span class="n">layer</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span><span class="s2">"company:destroyed"</span><span class="p">,</span> <span class="p">{})</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="s2">"Company deleted successfully"</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</code></pre></div>
<p>Experienced Django developers will immediately recognize that the above code has only two small diferences
from what a traditional Django app would have:</p>
<ul>
<li>the check for <code>self.request.up.validate</code> on the <code>form_valid</code> of the <code>FormMixin</code></li>
<li>the <code>self.request.up.layer.emit</code> on the <code>DeleteView</code> <code>form_valid</code></li>
</ul>
<p>We’ll explain these later. However the thing to keep is that this is the same as a good-old Django app,
without the need to implement special functionality like checks for ajax views, fragments, special form handling etc.</p>
<h2 id="integrating-unpoly-with-django">Integrating unpoly with Django</h2>
<p>To integrate unpoly with Django you only need to include the unpoly JavaScript and <span class="caps">CSS</span> library to your project. This is a normal .js file
that you can retrieve from the <a href="https://unpoly.com/install">unpoly install page</a>. Also, if you are using Bootstrap 3,4 or 5 I recommend to also download the corresponding unpoly-bootstrapX.js file.</p>
<p>Unpoly communicates with your backend through custom X-<span class="caps">HTTP</span>-<span class="caps">UP</span> headers. You could use the headers directly however it is also possible to install the <a href="https://gitlab.com/rocketduck/python-unpoly">python-unpoly</a>
library to make things easier. After installing that library you’ll add the <code>unpoly.contrib.django.UnpolyMiddleware</code> in your <code>MIDDLEWARE</code> list resulting in an extra <code>up</code> attribute to your request. You can then use this <code>up</code> attribute through the <a href="https://unpoly.readthedocs.io/en/latest/usage.html"><span class="caps">API</span></a> for easier access to the unpoly headers.</p>
<p>To access <code>up</code> through your Django templates you can use <code>request.up</code> or add it to the default context using a context processor so you can access it directly.</p>
<p>To make sure that everything works, add the <code>up-follow</code> to one of your links, i.e change <code><a href='linkto'>link</a></code>
to <code><a up-follow href='linkto'>link</a></code>. When you click on this link you should observe that instead of a full-page reload
you’ll get the response immediately! What really happens is that unpoly will make an <span class="caps">AJAX</span> request to the server, retrieve the response and render it on the current page making the response seem much faster! </p>
<h2 id="unpoly-configuration">Unpoly configuration</h2>
<p>The main way to use unpoly is to add <code>up-x</code> attributes to your html elements to enable unpoly behavior. However it is possible to use the unpoly js <span class="caps">API</span> (<code>window.up</code> or <code>up</code>) to set some global configuration. For example, you can use <code>up.log.enable()</code> and <code>up.log.disable()</code> to enable/disable the unpoly logging to your console. I recommend enabling it for your development environment because it will help you debug when things don’t seem to be working. </p>
<p>To use <code>up</code> to configure unpoly you only need to add it on a <code><script></code> element after loading the unpoly library, for example:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% static 'unpoly/unpoly.min.js' %}"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% static 'unpoly/unpoly-bootstrap4.min.js' %}"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% static 'application.js' %}"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
</code></pre></div>
<p>And in <code>application.js</code> you can use <code>up</code> directly, for example to enable logging:</p>
<div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">log</span><span class="p">.</span><span class="nx">enable</span><span class="p">()</span>
</code></pre></div>
<p>We’ll see more <code>up</code> configuration directives later, however keep in mind that for a lot of <code>up-x</code> attributes it is possible to use the config to automatically add that attribute to multiple elements using a selector.</p>
<h2 id="navigation-improvements">Navigation improvements</h2>
<p>Using the <code>up-follow</code> directive you can start adding <code>up-follow</code> to all your links and you’ll get a much more responsive application. This is very simple and easy.</p>
<p>One interesting thing is that we didn’t need to change <em>anything</em> on the backend. The whole response will be retrieved by unpoly and
will <em>replace</em> the <code>body</code> of the current page. Actually, it is possible to instruct unpoly to replace only a specific part of the page
using a css selector (i.e replace only the <code>#content</code> div). To do this you can add the <code>up-target</code> attribute to the link, i.e <code><a up-target='#content' up-follow href='linkto'>link</a></code>. When unpoly retrieves the response, it will make sure that it has an <code>#content</code>
element and put its contents to the original page <code>#content</code> element.</p>
<p>This technique is called <code>linking to fragments</code> in the unpoly docs. To see this in action, try going to the tasks in the demo and add a couple of new task. Then try to edit a that task. You’ll notice that the edit form of the task will <em>replace</em> the task show card! To do that, unpoly loads the edit task form and matches the <code>.task</code> element there with the <em>current</em> <code>.task</code> element and does the replacement (see <a href="https://unpoly.com/fragment-placement#interaction-origin-is-considered">here</a> for rules on how this works). </p>
<p>Beyond the <code>up-follow</code>, you can also use two more directives to further improve the navigation: </p>
<ul>
<li><code>up-instant</code> to follow the link on mousedown (without waiting for the user releasing the mouse button)</li>
<li><code>up-preload</code> to follow the link when the mouse hovers over the link</li>
</ul>
<h3 id="using-the-up-main">Using the up-main</h3>
<p>To make things simpler, you can declare an element to be the default replacement target. This is done by adding the <code>up-main</code> attribute to an element. This way, all <code>up-follow</code> links will replace that particular element by default unless they have an <code>up-target</code> element themselves.</p>
<p>What I usually do is that I’ve got a <code>base.html</code> template looking something like this:</p>
<div class="highlight"><pre><span></span><code> {% include "partials/_nav.html" %}
<span class="p"><</span><span class="nt">div</span> <span class="na">up-main</span> <span class="na">class</span><span class="o">=</span><span class="s">"container"</span><span class="p">></span>
{% include "partials/_messages.html" %}
{% block content %}
{% endblock %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% include "partials/_footer.html" %}
</code></pre></div>
<p>See the <code>up-main</code> on the <code>.container</code>? This way, all my <code>up-follow</code> links will replace the contents of the <code>.container</code> element by default. If I wanted to replace a specific part of the page, I could add the <code>up-target</code> attribute to the link.</p>
<p>If there’s no <code>up-main</code> element, unpoly will replace the whole <code>body</code> element.</p>
<h3 id="make-all-links-followable">Make all links followable</h3>
<p>It is possible to make all links (or links that follow a selector) <a href="https://unpoly.com/handling-everything#following-all-links">followable by default</a> by using the <code>up.link.config.followSelectors</code> option.
I would recommend to only do this on greenfield projects where you’ll test the functionality anyway. For existing projects I think it’s better to add the <code>up-follow</code> attribute explicitly to the links you want to make followable.</p>
<p>This is recommend it because there are cases where using unpoly will break some pages, especially if you have some JavaScript code that relies on the page being loaded. We’ll talk about this in the up.compiler section.</p>
<p>If you have made all the links followable but you want to skip some links and do a full page reload instead, add the <code>up-follow=false</code> attribute to the link or use the <code>up.link.config.noFollowSelectors</code> config to make multiple links non-followable.</p>
<p>You can also make all links instant or preload for example by using <code>up.link.config.instantSelectors.push('a[href]')</code> to make all followable links load on mousedown. This should be safe because it will only work on links that are <a href="https://unpoly.com/handling-everything#following-all-links-on-mousedown">already followable</a>. </p>
<h3 id="navigation-feedback">Navigation feedback</h3>
<p>One very useful feature of unpoly is that it adds more or less <em>free</em> <a href="https://unpoly.com/up.feedback">navigation feedback</a>. This can be enabled by adding an <code>[up-nav]</code> element to the navigation section of your page. Unpoly then will add an <code>up-current</code> class to the links in that section that match the current <span class="caps">URL</span>. This works no matter if you are using <code>up-follow</code> or not. You can then style <code>.up-current</code> links as you want.</p>
<p>If you are using Bootstrap along with the unpoly-bootstrap integrations you’ll get all that without any extra work! The unpoly-bootstrap has the following configuration:</p>
<div class="highlight"><pre><span></span><code><span class="nx">up</span><span class="p">.</span><span class="nx">feedback</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">currentClasses</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="nx">up</span><span class="p">.</span><span class="nx">feedback</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">navSelectors</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">'.nav'</span><span class="p">,</span><span class="w"> </span><span class="s1">'.navbar'</span><span class="p">);</span>
</code></pre></div>
<p>So it will automatically add the <code>up-nav</code> element on <code>.nav</code> and <code>.navbar</code> elements and will add the <code>active</code> class to the current link (in addition to the <code>.up-current</code> class). This is what happens in the demo, if you take a peek you’ll see that there are no <code>up-nav</code> elements (since these are marked by the unpoly-bootstrap integration) in the navigation bar and we style the <code>.active</code> nav links.</p>
<h3 id="aliases-for-navigation-feedback">Aliases for navigation feedback</h3>
<p>Unpoly also allows you to add aliases for the navigation feedback. For example, you may have <code>/companies/</code> and <code>/companies/new</code> and you want the <code>companies</code> nav link to be active on both of them. To allow that you need to use the <code>up-alias</code> attribute on the link like </p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">'nav-item nav-link'</span> <span class="na">up-follow</span> <span class="na">href</span><span class="o">=</span><span class="s">'{% url "company-list" %}'</span> <span class="na">up-alias</span><span class="o">=</span><span class="s">'{% url "company-list" %}new/'</span><span class="p">></span>Companies<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>(notice that in my case the url of <code>company-list</code> is <code>/companies/</code> that’s why I added <code>{% url "company-list" %}new/</code> on the alias so the resulting alias path would be <code>/companies/new/</code>), or even add multiple links to the alias</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">'nav-item nav-link'</span> <span class="na">up-follow</span> <span class="na">href</span><span class="o">=</span><span class="s">'{% url "company-list" %}'</span> <span class="na">up-alias</span><span class="o">=</span><span class="s">'{% url "company-list" %}*'</span><span class="p">></span>Companies<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>This will add the <code>up-current</code> class to the <code>a</code> element whenever the url starts with <code>/companies/</code> (i.e <code>/companies/</code>, <code>/companies/new</code>, <code>/companies/1/edit</code> etc).</p>
<p>Please notice that it is recommended to have a proper url hierarchy for this to work better. For example, if you have <code>/companies_list/</code> and <code>/add_new_company/</code> you’ll need to add the aliases like <code>up-alias='/companies_list/ /add_new_company/'</code> (notice the space between the urls to add two aliases). Also, if you want to also handle <span class="caps">URLS</span> with query parameters i.e <code>/companies/?name=foo</code> then you’ll need to add <code>?*</code> i.e <code>/companies/?*</code>. These urls aren’t aliased by default so <code>/companies/</code> doesn’t match <code>/companies/?name=foo</code> unless you add an alias.</p>
<p>One final remark is that it is possible to do some trickery to automatically add up-alias to all your nav links. This is useful in case you have many nav elements and you don’t want to add aliases to each one of them, for example, using this code:</p>
<div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">compiler</span><span class="p">(</span><span class="s1">'nav a[href]'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">link</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">link</span><span class="p">.</span><span class="nx">href</span><span class="p">.</span><span class="nx">endsWith</span><span class="p">(</span><span class="s1">'#'</span><span class="p">))</span><span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-alias'</span><span class="p">,</span><span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">href</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">'*'</span><span class="p">)</span>
<span class="w"> </span><span class="p">})</span>
</code></pre></div>
<p>an <code>up-alias</code> attribute will be added to all links. The callback of the compiler will be called when the selector is matched and
in this case add the <code>up-alias</code> attribute to the link. We’ll talk later about compilers more.</p>
<h2 id="handling-forms">Handling forms</h2>
<p>Unpoly can also be used to handle forms without page reloads, similar to following links. This is simple to do by adding an <code>up-submit</code> attribute to your form. Also similar to links you can make <a href="https://unpoly.com/handling-everything#handling-all-forms">all your forms handled by unpoly</a> but I recommend to be cautious before doing this on existing projects to make sure that stuff doesn’t break.</p>
<p>When you add an <code>up-submit</code> to a form unpoly will do an <span class="caps">AJAX</span> post to submit the form and replace the contents of the <code>up-target</code> element with the response (if you don’t specify an <code>up-target</code> element, it will use the <code>up-main</code> element in a similar way as links). This works fine with the default Django behavior, i.e when the form is valid Django will do a redirect to the success url, unpoly will follow that link and render the response of the redirect.</p>
<h3 id="integrating-with-messages">Integrating with messages</h3>
<p>Django has the <a href="https://docs.djangoproject.com/en/stable/ref/contrib/messages/">messages framework</a> that can be used to add one-time flash messages
after a form is successfully submitted. You need to make sure that these messages are actually rendered! For example, in the <code>base.htm</code> template I mentioned before, we’ve got the following:</p>
<div class="highlight"><pre><span></span><code> {% include "partials/_nav.html" %}
<span class="p"><</span><span class="nt">div</span> <span class="na">up-main</span> <span class="na">class</span><span class="o">=</span><span class="s">"container"</span><span class="p">></span>
{% include "partials/_messages.html" %}
{% block content %}
{% endblock %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% include "partials/_footer.html" %}
</code></pre></div>
<p>please notice that we’ve got the <code>partials/_messages.html</code> template included in the <code>up-main</code> element (<em>inside</em> the container). This means that when unpoly replaces the contents of the <code>up-main</code> element with the response of the form submission, the messages will be rendered as well. So it will work fine in this case. </p>
<p>However, if you are using <code>up-target</code> to render only particular parts of the page the flash messages will be actually lost! This happens because unpoly will load the page with the flash messages normally, so these messages will be consumed; then it will match the <code>.target</code> and display <em>only</em> that part of the response.</p>
<p>To resolve that you can use the <code>up-hungry</code> attribute on your messages. For example, in the <code>partials/_messages.html</code> template we’ve got the following:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'flash-messages'</span> <span class="na">class</span><span class="o">=</span><span class="s">"flash-messages"</span> <span class="na">up-hungry</span><span class="p">></span>
{% for message in messages %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"alert fade show {% if message.tags %} alert-{% if 'error' in message.tags %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}"</span><span class="p">></span>
{{ message }}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>The <code>up-hungry</code> attribute will make unpoly refresh that particular part of the page on every page load <a href="https://unpoly.com/up-hungry">even if it’s not on the target</a>. For example notice how the message is displayed when you edit or mark as done an existing task in the demo. </p>
<p>However also notice that no messages are displayed if you create a new task! This happens because the actual response is “eaten” by the layer and the messages are discarded! We’ll see how to fix that later.</p>
<h3 id="immediate-form-validation">Immediate form validation</h3>
<p>Another area in which unpoly helps with our forms is that if we add the <code>up-validate</code> attribute to our form, unpoly will do an <span class="caps">AJAX</span> post to the server whenever the input focus changes and will display the errors in the form without reloading the page. For this we need a little modification to our views to check if the unpoly wants to validate the form. I’m using the following <code>form_valid</code> on a form mixin:</p>
<div class="highlight"><pre><span></span><code><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">is_valid</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">up</span><span class="o">.</span><span class="n">validate</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"success_message"</span><span class="p">):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">success_message</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="n">form</span><span class="o">=</span><span class="n">form</span><span class="p">))</span>
</code></pre></div>
<p>So if the form is not valid or we get an unpoly validate request from unpoly we’ll render the response - this will render the form with or without errors. However if the form is actually valid and this is not an unpoly validate request we’ll do the usual form save and redirect to the success url. This is enough to handle all cases and is very simple and straightforward. It works fine without unpoly as well since the <code>up.validate</code> will be always <code>False</code> in this case.</p>
<p>One thing to keep in mind is that this works fine in most cases but may result to problematic behavior if you use components that rely on javascript onload events. The <code>up-validate</code> will behave more or less the same as with <code>up-follow</code> links. </p>
<h3 id="other-form-helpers">Other form helpers</h3>
<p>Beyond these, unpoly offers a <a href="https://unpoly.com/up.form">bunch of form helpers</a> to run callbacks or auto-submit a form when a field is changed. Most of this functionality can be replicated by other js libraries (i.e jquery) or even by vanilla.js and is geared towards the front-end so I won’t cover it more here.</p>
<h2 id="understanding-layers">Understanding layers</h2>
<p>One of the most powerful features of unpoly is <a href="https://unpoly.com/up.layer">layers</a>. To understand the terminology, a layer is any page that is stacked on top of another. The initial page is called the root layer, all other layers are called overlays. Layers can be arbitrary opened and stacked, there’s no limit on the number of layers that can be opened. </p>
<p>An overlay can be rendered like a modal / popup / drawer. The simplest way to use an overlay is to add an <code>up-layer='new'</code> attribute to a link. For example, in the demo app, the link to open a company is like this:</p>
<div class="highlight"><pre><span></span><code> <span class="p"><</span><span class="nt">a</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-on-dismissed</span><span class="o">=</span><span class="s">"up.reload('.table', { focus: ':main' })"</span>
<span class="na">up-dismiss-event</span><span class="o">=</span><span class="s">'company:destroyed'</span>
<span class="na">href</span><span class="o">=</span><span class="s">"{% url 'company-detail' company.id %}"</span><span class="p">></span>{{ company.name }}<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>(ignore the dismiss-related attributes for now). This opens a new modal dialog with the contents of the company detail. It will render the whole contents of the <code>up-main</code> element inside the modal since we don’t provide an <code>up-target</code>. If we added an <code>up-target='.projects'</code> attribute to this it would render <em>only</em> the <code>.projects</code> element inside the modal (but remember that it will retrieve the <em>whole</em> response since the /companies/detail/id is a normal django DetailView). So with <code>up-layer='new'</code> we open a page on a new overlay/modal. If we also add an <code>up-target</code> to it we’ll open only a particular part of that page.</p>
<p>You can use <code>up-mode</code> attribute to <a href="https://unpoly.com/layer-terminology#available-modes">change the kind of overlay</a>; the default is a <code>modal</code>. Also if you want to configure the ways this modal closes you can use the
<code>up-dismissable</code> <a href="https://unpoly.com/closing-overlays#customizing-dismiss-controls">attribute</a>, for example add
<code>up-dismissable='button'</code> to allow closing only with the X button on the top right. Another useful thing is that there’s an
<code>up-size</code> attribute for changing the <a href="https://unpoly.com/customizing-overlays#overlay-sizes">size of the overlay</a>. I recommend playing a bit with these options to have a feel on how they are working and what you can do with them.</p>
<h3 id="static-overlay-content">Static overlay content</h3>
<p>An overlay can also contain “static” content (i.e not follow a link but display some html) by using the <code>up-content</code> <a href="https://unpoly.com/a-up-follow#up-content">attribute</a>. This is how the green dots are implemented, their html is similar to this:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#"</span> <span class="na">class</span><span class="o">=</span><span class="s">"tour-dot viewed"</span> <span class="na">up-layer</span><span class="o">=</span><span class="s">"new popup"</span> <span class="na">up-content</span><span class="o">=</span><span class="s">"<p>Navigation links have the <code>[up-follow]</code> attribute. </span>
<span class="s"> <p></span>
<span class="s"> <a href=&quot;#&quot; up-dismiss class=&quot;btn btn-success btn-sm&quot;>OK</a></span>
<span class="s"> </p></span>
<span class="s"> "</span> <span class="na">up-position</span><span class="o">=</span><span class="s">"right"</span> <span class="na">up-align</span><span class="o">=</span><span class="s">"top"</span> <span class="na">up-class</span><span class="o">=</span><span class="s">"tour-hint"</span> <span class="na">up-size</span><span class="o">=</span><span class="s">"medium"</span><span class="p">></span>
<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>Notice that the <code>up-content</code> contains a whole html snippet. This is implemented in Django using the following template tag:</p>
<div class="highlight"><pre><span></span><code><span class="nd">@register</span><span class="o">.</span><span class="n">tag</span><span class="p">(</span><span class="s2">"tourdot"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">do_tourdot</span><span class="p">(</span><span class="n">parser</span><span class="p">,</span> <span class="n">token</span><span class="p">):</span>
<span class="n">nodelist</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse</span><span class="p">((</span><span class="s2">"endtourdot"</span><span class="p">,))</span>
<span class="n">parser</span><span class="o">.</span><span class="n">delete_first_token</span><span class="p">()</span>
<span class="k">return</span> <span class="n">TourDotNode</span><span class="p">(</span><span class="n">nodelist</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">TourDotNode</span><span class="p">(</span><span class="n">template</span><span class="o">.</span><span class="n">Node</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">nodelist</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">nodelist</span> <span class="o">=</span> <span class="n">nodelist</span>
<span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">nodelist</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">context</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">size</span> <span class="o">=</span> <span class="s2">"medium"</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">strip_tags</span><span class="p">(</span><span class="n">rendered</span><span class="p">))</span> <span class="o">></span> <span class="mi">400</span><span class="p">:</span>
<span class="n">size</span> <span class="o">=</span> <span class="s2">"large"</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">rendered</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"<p"</span><span class="p">):</span>
<span class="n">rendered</span> <span class="o">=</span> <span class="s2">"<p></span><span class="si">{}</span><span class="s2"></p>"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
<span class="n">rendered</span> <span class="o">+=</span> <span class="s2">"""</span>
<span class="s2"> <p></span>
<span class="s2"> <a href="#" up-dismiss class="btn btn-success btn-sm">OK</a></span>
<span class="s2"> </p></span>
<span class="s2"> """</span>
<span class="kn">from</span> <span class="nn">django.utils.html</span> <span class="kn">import</span> <span class="n">escape</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">escape</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
<span class="k">return</span> <span class="s2">"""</span>
<span class="s2"> <a </span>
<span class="s2"> href="#" class="tour-dot" up-layer="new popup" </span>
<span class="s2"> up-position="right" up-align="top" up-class="tour-hint"</span>
<span class="s2"> up-content="</span><span class="si">{}</span><span class="s2">"</span>
<span class="s2"> up-size="</span><span class="si">{}</span><span class="s2">"</span>
<span class="s2"> ></span>
<span class="s2"> </a></span>
<span class="s2"> """</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">output</span><span class="p">,</span> <span class="n">size</span>
<span class="p">)</span>
</code></pre></div>
<p>So we can do something like this in our Django templates:</p>
<div class="highlight"><pre><span></span><code>{% tourdot %}
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Navigation links have the <span class="p"><</span><span class="nt">code</span><span class="p">></span>[up-follow]<span class="p"></</span><span class="nt">code</span><span class="p">></span> attribute. Clicking such links only updates a <span class="p"><</span><span class="nt">b</span><span class="p">></span>page fragment<span class="p"></</span><span class="nt">b</span><span class="p">></span>. The remaining DOM is not changed.<span class="p"></</span><span class="nt">p</span><span class="p">></span>
{% endtourdot %}
</code></pre></div>
<h3 id="advanced-layersoverlays">Advanced layers/overlays</h3>
<p>Opening overlays for popups or for modals to view links that don’t have interactivity is simple. However, when you open forms with overlays
and need to handle these (i.e close the modals only when the form is submitted succesfully) the situation unfortunately starts to get more complex. I recommend to start by reading the
<a href="https://unpoly.com/subinteractions">subinteractions</a> section of the unpoly documentation to understand how these things work. In the following subsections we’ll talk about specific cases and how to handle them with unpoly layers and Django.</p>
<h3 id="opening-new-layers-over-existing-ones">Opening new layers <em>over</em> existing ones</h3>
<p>How opening a new layer <em>over</em> an existing layer (i.e a modal inside a modal) would work? All links and forms that are handled in an existing layer will be handled in the same layer. So if we have opened a layer and there are <code>up-follow</code> links in the html of the layer, the user would be able to follow them normally inside that layer (of course if there are non <code>up-follow</code> links then a full page reload will be performed and the layer will disappear without a trace).</p>
<p>If we want to open a new layer we need to use the <code>up-layer='new'</code> attribute on that link; it doesn’t matter if this is inside an already opened layer, it will work as expected and open a <em>layer-in-a-layer</em>. If the parent layer is an overlay then it will open an <em>overlay-in-an-overlay</em>. </p>
<p>In the demo, if you click on an existing company to see its details you’ll get an overlay. If you try to edit that company the edit for will be opened <em>in the same layer</em> (notice that if you press the X button to close it you’ll go back to the company list without layers). Compare this with the behavior when adding a new project or viewing an existing one. You’ll get an overlay <em>inside</em> the parent overlay (both overlays should be visible). You need to close both overlays to go back to the company detail at the root layer. </p>
<p>Even more impressive: Go to the company detail layer, click an existing project to get to the project detail layer, click <em>edit</em>; this will be opened on the <em>project detail</em> layer. You can edit the project or even delete it, when you click that overlay the company overlay will be updated with the new data and work fine! All this also works fine from the project detail list <em>without</em> any modifications on the Django code.</p>
<p>The thing to remember here is that the layer behavior is very intuitive and is compatible with how a server side application works. Everything should work the same no matter if the link is opened in an overlay or in a new page or even an overlay over an overlay.</p>
<h3 id="closing-overlays">Closing overlays</h3>
<p>There are three main ways to <a href="https://unpoly.com/closing-overlays">close an overlay</a> (beyond of course using the (X) button or esc etc):</p>
<ul>
<li>Visiting a pre-defined link</li>
<li>Explicitly closing the overlay from the server</li>
<li>Emitting an unpoly event</li>
</ul>
<p>Also, when an overlay is closed we can decide if the overlay did something (i.e the user saved the form) or not (i.e the user clicked the X button). This is called <code>accepted</code> or <code>dismissed</code> respectively. We can use this to do different things. All the methods of closing an overlay have a version for accepting or dismissing the overlay.</p>
<h4 id="closing-the-overlay-when-visiting-a-link">Closing the overlay when visiting a link</h4>
<p>To close the overlay on visiting a link we’ll use the <code>up-accept-location</code> and <code>up-dismiss-location</code> respectively. For example, let’s take a peek on the new company link:</p>
<div class="highlight"><pre><span></span><code> <span class="p"><</span><span class="nt">a</span>
<span class="na">class</span><span class="o">=</span><span class="s">'btn btn-primary'</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-on-accepted</span><span class="o">=</span><span class="s">"up.reload('.table', { focus: ':main' })"</span>
<span class="na">up-accept-location</span><span class="o">=</span><span class="s">'/core/companies/detail/$id/'</span>
<span class="na">href</span><span class="o">=</span><span class="s">'{% url "company-create" %}'</span><span class="p">></span>New company<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>The important thing here is the <code>up-accept-location</code>. When Django creates a new object it redirects to the detail view of that object. In our case this detail view is <code>'/core/companies/detail/$id/'</code>; the <code>$id</code> is an unpoly thingie that will be replaced by the id of the new object and will be <a href="https://unpoly.com/closing-overlays#overlay-result-values">the result value of the overlay</a>. This value (the id) can then be used on the <code>up-on-accepted</code> callback if we want.</p>
<p>Now, let’s suppose that we want to close the overlay when the user clicks on a <em>cancel</em> button that returns to the list of companies. We can do that by adding the <code>up-dismiss-location</code> attribute to that <code><a></code></p>
<div class="highlight"><pre><span></span><code>up-dismiss-location='{% url "company-list" %}'
</code></pre></div>
<p>The difference between these two is that the <code>up-on-accepted</code> event will only be called when the overlay is accepted and not on dismissed.</p>
<h4 id="handling-hardcoded-urls">Handling hardcoded urls</h4>
<p>One thing that Django developers may not like is that the url is hardcoded here. This is because using <code>{% url "company-detail" "$id" %}</code> will not work with our urls since we use have the following path for the company detail <code>"companies/detail/<int:pk>/"</code>. We can change it to <code>"companies/detail/<str:pk>/",</code> to make it work but then it will allow strings in the url and it will throw 500 error instead of 404 when the user uses a string there (to improve that we have to override the <code>get_object</code> of the <code>DetailView</code> to handle the string case). Another way to improve that is to create a <code>urlid</code> template tag like this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">reverse</span>
<span class="nd">@register</span><span class="o">.</span><span class="n">simple_tag</span>
<span class="k">def</span> <span class="nf">urlid</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">arg</span><span class="p">):</span>
<span class="k">if</span> <span class="n">arg</span> <span class="o">==</span> <span class="s2">"$id"</span><span class="p">:</span>
<span class="n">arg</span> <span class="o">=</span> <span class="mi">999</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">reverse</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="n">arg</span><span class="p">])</span>
<span class="k">return</span> <span class="n">url</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"999"</span><span class="p">,</span> <span class="s2">"$id"</span><span class="p">)</span>
</code></pre></div>
<p>And then using it like this on the up-accept-location:</p>
<div class="highlight"><pre><span></span><code>up-accept-location='{% urlid "company-detail" "$id" %}'
</code></pre></div>
<h4 id="explicitly-closing-the-layer">Explicitly closing the layer</h4>
<p>To close the layer from the server you can you use the
<a href="https://unpoly.com/X-Up-Accept-Layer"><code>X-Up-Accept-Layer</code></a>
or <a href="https://unpoly.com/X-Up-Dismiss-Layer"><code>X-Up-Dismiss-Layer</code></a>
response header. When unpoly sees this header in a response it will close the overlay by accepting/dismissing it.</p>
<p>To do that from Django if you have integrated the unpoly middleware, call <code>request.up.layer.accept()</code> and <code>request.up.layer.dismiss()</code> respectively (passing an optional value if you want).</p>
<p>The same feature can be used to close the overlay from the client side. For example, if you want to close the overlay when the user clicks on a <em>cancel</em> button that returns to the list of companies you can do that by adding the <code>up-accept</code> or <code>up-dismiss</code> attribute, like:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">'{% urlid "company-detail" "$id" %}'</span> <span class="na">up-dismiss</span><span class="p">></span>Return<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>Please notice that the <code>href</code> here could be like <code>href='#'</code> since this is javascript only to close the overlay, however we added the correct href to make sure the return button will also work when we open the link in a new page (without any overlays). </p>
<p>Please notice that difference between this and <code>up-accept-location</code> or <code>up-dismiss-location</code> we mentioned before. In this case the <code>up-accept/dismiss</code> directive in placed in the a link that <em>closes</em> the overlay. In the former case the <code>up-accept/dismiss-location</code> directive is placed in the link that <em>opens</em> the overlay.</p>
<h4 id="closing-the-layer-by-emitting-an-unpoly-event">Closing the layer by emitting an unpoly event</h4>
<p>The final way to close an overlay is by emitting an event. Unpoly can emit events both from the server, using the <a href="https://unpoly.com/X-Up-Events"><code>X-Up-Event</code></a> response header or using <code>request.up.emit(event_type, data)</code> from the unpoly Django integration. Also events can be emitted from the client side using the <a href="https://unpoly.com/up.emit"><code>up-emit</code></a> attribute.</p>
<p>To close the overlay from an event we need to use <code>up-accept-event</code> and <code>up-dismiss-event</code> on the link that opens the overlay.</p>
<p>Let’s see what happens when we delete a company. We’ve got a form like this:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-submit</span> <span class="na">up-confirm</span><span class="o">=</span><span class="s">'Really?'</span> <span class="na">class</span><span class="o">=</span><span class="s">"d-inline"</span> <span class="na">method</span><span class="o">=</span><span class="s">'POST'</span> <span class="na">action</span><span class="o">=</span><span class="s">'{% url "company-delete" company.id %}'</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span> <span class="na">value</span><span class="o">=</span><span class="s">'Delete'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-danger mr-3'</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</code></pre></div>
<p>This form asks the user for confirmation (using the <code>up-confirm</code> directive) and then submits the form on the company delete view. The <code>CompanyDeleteView</code> is like this:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">CompanyDeleteView</span><span class="p">(</span><span class="n">DeleteView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="k">def</span> <span class="nf">get_success_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">reverse</span><span class="p">(</span><span class="s2">"company-list"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">up</span><span class="o">.</span><span class="n">layer</span><span class="o">.</span><span class="n">emit</span><span class="p">(</span><span class="s2">"company:destroyed"</span><span class="p">,</span> <span class="p">{})</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</code></pre></div>
<p>So, it will emit the <code>company:destroyed</code> event and redirect to the list of companies (this is needed to make sure that delete works fine if we call it from a full page instead of an overlay). The company detail view overlay is opened from the following <code>a</code> link:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-on-dismissed</span><span class="o">=</span><span class="s">"up.reload('.table', { focus: ':main' })"</span>
<span class="na">up-dismiss-event</span><span class="o">=</span><span class="s">'company:destroyed'</span>
<span class="na">href</span><span class="o">=</span><span class="s">"{% url 'company-detail' company.id %}"</span><span class="p">></span>{{ company.name }}<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>Notice that we have the <code>up-dismiss-event</code> here. If we didn’t have that then the overlay wouldn’t be closed when we deleted the company but we’d see the list of companies on the overlay because of the redirect on the Django side! Also, instead of the <code>up-dismiss-event</code> we could use the <code>up-dismiss-location='{% url "company-list" %}'</code> similar to how we discussed before. If we did it this way we wouldn’t even need to do anything unpoly related in our DeleteView, however using events for this is useful for educational reasons and we’ll see later how events will help us to dispaly a message when companies are deleted.</p>
<h3 id="doing-stuff-when-a-layer-is-closed">Doing stuff when a layer is closed</h3>
<p>After a layer is closed (and depending if it was accepted or dismissed) unpoly allows us to use callbacks to do stuff. The most obvious things are to reload the list of results if a result is added/edited/deleted or to choose a result in a form if we used the overlay as an object picker.</p>
<p>The callbacks are <code>up-on-accepted</code> and <code>up-on-dismissed</code>.</p>
<p>Let’s see some examples from the demo.</p>
<p>On on the new company link we’ve got <code>up-on-accepted="up.reload('.table', { focus: ':main' })"</code>. However on the show details company link we’ve got <code>up-on-dismissed="up.reload('.table', { focus: ':main' })"</code>. This is a little strange (why <code>up-on-accepted</code> on the new vs <code>up-on-dismissed</code> on the detail) at first but we can explain it. </p>
<p>First of all, the <a href="https://unpoly.com/up.reload">up.reload</a> method will do an <span class="caps">HTTP</span> request and reload that specific element from the server (in our case the <code>.table</code> element that contains the list of companies). The focus option that is passed instructs unopoly to move the focus to (that element)[https://unpoly.com/focus-option].</p>
<p>For the “Add new” company we reload the companies when the form is accepted (when the user clicks on the “Save” button). However for the show details we’ll reload every time the overlay is dismissed because when the user edits a company the layer will not be closed but will display the edit company data. Also when we delete the company the layer will be dismissed. </p>
<p>Notice that if the user clicks the company details and then presses the (X) button <em>we’ll still do a reload</em> even though it may not be needed because we can’t know if the user actually edited the company or not before closing the overlay. This is a little bit of a tradeoff but it’s not a big deal. </p>
<p>Actually, it <em>is</em> possible to know if the overlay was dismissed because the user clicked the (X) button (or pressed escape) or if the overlay was dismissed because the object was deleted. This is useful if we wanted to display a message to the user when the company was deleted since we’d need to differentiate between these cases. We’ll see how the section about overlays and messages.</p>
<p>On the company detail we’ve got <code>up-on-accepted='up.reload(".projects")'</code> for adding a new project but same as before we’ve got <code>up-on-dismissed='up.reload(".projects")'</code> for viewing the project detail. The <code>.projects</code> element is the projects holder inside the company detail. This is exactly the same behavior we explained before.</p>
<p>On the project form we’ve got <code>up-on-accepted</code> both on the suggest name and on the new company button. In the first case, we are opening the name suggestion overlay like this:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new popup'</span>
<span class="na">up-align</span><span class="o">=</span><span class="s">'left'</span>
<span class="na">up-size</span><span class="o">=</span><span class="s">'large'</span>
<span class="na">up-accept-event</span><span class="o">=</span><span class="s">'name:select'</span>
<span class="na">up-on-accepted</span><span class="o">=</span><span class="s">"up.fragment.get('#id_name').value = value.name"</span>
<span class="na">href</span><span class="o">=</span><span class="s">'{% url "project-suggest-name" %}'</span><span class="p">></span>Suggest name<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>Notice that this overlay will be accepted when it receives the <code>name:select</code> event. This event passes it the selected name so it will put it on the <code>#id_name</code> input. The <a href="https://unpoly.com/up.fragment.get"><code>up.fragment.get</code></a> is used to retrieve the input. To understand how this works we need to also see the name suggestion overlay. This is more or less similar to:</p>
<div class="highlight"><pre><span></span><code> {% for n in names %}
<span class="p"><</span><span class="nt">a</span> <span class="na">up-emit</span><span class="o">=</span><span class="s">"name:select"</span>
<span class="na">up-emit-props</span><span class="o">=</span><span class="s">'{"name": "{{ n }}"}'</span>
<span class="na">class</span><span class="o">=</span><span class="s">"btn btn-info text-light mb-2 mr-1"</span>
<span class="na">tabindex</span><span class="o">=</span><span class="s">"0"</span><span class="p">></span>
{{ n }}
<span class="p"></</span><span class="nt">a</span><span class="p">></span>
{% endfor %}
</code></pre></div>
<p>So we are using the <code>up-emit</code> directive here to emit the <code>name:select</code> event <em>and</em> we pass it some data which must be a json object. Then this data will be available as a javascript object named <code>value</code> on the <code>up-on-accepted</code> callback.</p>
<p>So the flow is:</p>
<ol>
<li>When we click the suggest name link we open a new overlay and wait for the <code>name:select</code> event to be emitted. We don’t care if we are a full page or already inside an overlay</li>
<li>The suggest name overlay displays a link of <code><a></code> elements that emit the <code>name:select</code> event when clicked and also pass the selected name as data on the event</li>
<li>The overlay opener receives the <code>name:select</code> event and closes the overlay. It then uses the data to fill the <code>#id_name</code> input</li>
</ol>
<p>The second case is similar but instead of filling an input it opens a new overlay to create and select a new company. This is the create company link from inside the project form:</p>
<div class="highlight"><pre><span></span><code> <span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">'{% url "company-create" %}'</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-accept-location</span><span class="o">=</span><span class="s">'{% urlid "company-detail" "$id" %}'</span>
<span class="na">up-on-accepted</span><span class="o">=</span><span class="s">"up.validate('form', { params: { 'company': value.id } })"</span>
<span class="p">></span>
New company
<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>Nothing extra is needed from the company form side! We use the <code>up-accept-location</code> to accept the overlay when the company is created (so the user will be redirect to the company-detail view). Then we call <code>up.validate('form', { params: { 'company': value.id } })</code> after the overlay is accepted. First of all, please remember that when we use the <code>up-accept-location</code> the overlay result
<a href="https://unpoly.com/a-up-layer-new#up-accept-location">will be an object with the captured parts of the url</a>. In this case we capture the new company id. Then, we call <code>up.validate</code> passing it the form and the company id we just retrieved (i.e the id of the newly created company).</p>
<p>It is important to understand that we do <code>up.validate</code> here instead of simply setting the value of the select to the newly created id (similar to what we did before with the name) because the newly created value <em>is not</em> in the options that this select contains so it can’t be picked at this time; when the validate returns though it <em>will</em> contain the newly created company to the options so it will be selected then.</p>
<p>If we wanted to select the newly created company without doing the validate instead we’d need to first add a new option to the select with the correct id and then set it to that value (which is a little bit more complex since we don’t know the name of the new company at this point). To properly implement that and to further understand how unpoly works, we’d need to emit a <code>company:create</code> event from our <code>CreateCompanyView</code> which would contain as data both the id and the name of the newly created company. Then we’d change our accept condition to <code>up-accept-event='company:create'</code>. Finally, our <code>up-on-accepted</code> would add a new option with the <code>value.name</code> and <code>value.id</code> it received from the event and select <em>that</em> option.</p>
<h3 id="overlays-and-messages">Overlays and messages</h3>
<p>This probably is the most complex part of integrating unpoly with Django. The problem is that when we do an action the messages will
be displayed on the page that our response redirects to. If we don’t display that page but we only use it as <code>on-accept-location</code> we’ll
miss these messages. There are various solutions on how this can be fixed, and there also is a <a href="https://github.com/unpoly/unpoly/discussions/400">long discussion</a> in the unpoly repo discussions about that. </p>
<p>We’ve already discussed about the <code>up-hungry</code> in your messages container element that will reread its contents from all responses. This will resolve all cases where the response is <em>not</em> discarded. For example, try to edit a project and you’ll see the edit message <em>on</em> the overlay (instead of the main page). This is because the overlay is contained in the <code>up-main</code> element so it will be rendered in the response in the overlay.</p>
<p>The problematic behavior is when creating a new project or deleting one. In both these cases we discard the response so the messages are lost. The simplest way to actually fix this is to ignore the server side message and render again the message <em>from unpoly</em>. This avoids changing anything on your server-side code. So, in order to implement this, we’ll use this function which we add on <code>application.js</code> (after a suggestion on the afforementioned discussion):</p>
<div class="highlight"><pre><span></span><code><span class="k">async</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">reloadWithFlash</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span><span class="w"> </span><span class="nx">flash</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">reload</span><span class="p">(</span><span class="nx">selector</span><span class="p">)</span>
<span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">affix</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">'flash-messages'</span><span class="p">),</span><span class="w"> </span><span class="s1">'.alert.fade.show.alert-success'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="nx">flash</span><span class="w"> </span><span class="p">})</span>
<span class="p">}</span>
</code></pre></div>
<p>This function will call <code>up.reload</code> with a selection we pass to it (f.e <code>up.reload('.table')</code>) and wait until this function finished.
Then it will add a new element on the <code>flash-messages</code> container with the flash text we pass to it. In order to use it, we’ll change
the new company link to:</p>
<div class="highlight"><pre><span></span><code> <span class="p"><</span><span class="nt">a</span>
<span class="na">class</span><span class="o">=</span><span class="s">'btn btn-primary'</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-on-accepted</span><span class="o">=</span><span class="s">"reloadWithFlash('.table', 'Company created!')"</span>
<span class="na">up-accept-location</span><span class="o">=</span><span class="s">'{% urlid "company-detail" "$id" %}'</span>
<span class="na">href</span><span class="o">=</span><span class="s">'{% url "company-create" %}'</span><span class="p">></span>New company<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>(remember that <code>up-on-accepted</code> before was <code>up-on-accepted="up.reload('.table', { focus: ':main' })")</code>, let’s skip <code>focus</code> for now it’s not important). If we try it this way we’ll notice that we’ll get the <code>Company created</code> message after the overlay is closed! As I said before, the problem with this is that we ignore the server side message and duplicate the message both on server and on client side. The Django side message will be used when we open the /companies/new link on a new page (not an overlay) so the overlay functionality won’t be used and the message will be rendered properly on the response. When we use an overlay the client side message will be rendered instead.</p>
<p>Another solution would be to change our <code>CompanyCreateView</code> to redirect to the companies list page (instead of the newly created page). In this case, we can change the new company form like:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-submit</span> <span class="na">up-validate</span> <span class="na">method</span><span class="o">=</span><span class="s">"POST"</span> <span class="na">up-layer</span><span class="o">=</span><span class="s">'root'</span><span class="p">></span>
...
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</code></pre></div>
<p>Adding the <code>up-layer='root'</code> will render the response in the root layer which will close the overlay and render everything on the <code>up-main</code> element. Since we redirect to the companies list, we’ll get the list of companies along with the server-side message. This solution is actually simpler but modifies our server-side app (instead of the usual behavior of redirecting to the new company detail well’ll redirect to the companies list).</p>
<p>Let’s now talk about delete. As we’ve already discussed above, the company detail overlay will be closed either when the user closes it explicitly by clicking the (X) button or because the company was deleted. In both cases we want to reload the companies but when the company is deleted we also want to display a message. So we need to know when the overlay was closed because the company was deleted vs when the overlay was closed explicitly by the user. </p>
<p>Right now we’ve got </p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-dismissable</span><span class="o">=</span><span class="s">'button'</span>
<span class="na">up-on-dismissed</span><span class="o">=</span><span class="s">"up.reload('.table', { focus: ':main' })"</span>
<span class="na">up-dismiss-event</span><span class="o">=</span><span class="s">'company:destroyed'</span>
<span class="na">href</span><span class="o">=</span><span class="s">"{% url 'company-detail' company.id %}"</span><span class="p">></span>{{ company.name }}<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>For starters, we’ll add the following function:</p>
<div class="highlight"><pre><span></span><code><span class="k">async</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">reloadWithFlashIfEvent</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span><span class="w"> </span><span class="nx">flash</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">reload</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">focus</span><span class="o">:</span><span class="w"> </span><span class="s1">':main'</span><span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">value</span><span class="w"> </span><span class="ow">instanceof</span><span class="w"> </span><span class="nx">Event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">affix</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">'flash-messages'</span><span class="p">),</span><span class="w"> </span><span class="s1">'.alert.fade.show.alert-danger'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="nx">flash</span><span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>and change <code>up-on-dismissed</code> to <code>up-on-dismissed="reloadWithFlashIfEvent('.table', 'Company deleted!', value)"</code> on the open overlay link.</p>
<p>The <code>up-on-dismissed</code> and <code>up-on-accepted</code> callbacks are passed <a href="https://unpoly.com/a-up-layer-new#up-on-dismissed">these paremeters</a> by unpoly:
* <code>this</code> The link that originally opened the overlay
* <code>layer</code> An up.Layer object for the dismissed overlay
* <code>value</code> The overlay’s dismissal value
* <code>event</code> An up:layer:dismissed event</p>
<p>If the event was dismissed because the user clicked the (X) button, the <code>value</code> would have a similar to <code>:button</code> (there are same string values for pressing escape or clicking outside the modal). However if it was dismissed because of the <code>company:destroyed</code> event, the value would be an <code>Event</code> object. So we pass the <code>value</code> to our <code>reloadWithFlashIfEvent</code> callback and check if the value is an <code>Event</code> object. If it is, we know that the overlay was dismissed because the company was deleted and we can display the flash message. If it’s not, we know that the overlay was dismissed because the user clicked the (X) button and we won’t display the flash message.</p>
<p>Another way we could implement this would be if we closed the company detail overlay when the company was deleted and returned the response (which is the company list view) to the root layer. Something like this:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-submit</span> <span class="na">up-confirm</span><span class="o">=</span><span class="s">'Really?'</span> <span class="na">class</span><span class="o">=</span><span class="s">"d-inline"</span> <span class="na">method</span><span class="o">=</span><span class="s">'POST'</span> <span class="na">action</span><span class="o">=</span><span class="s">'{% url "company-delete" company.id %}'</span> <span class="na">up-layer</span><span class="o">=</span><span class="s">'root'</span><span class="p">></span>
</code></pre></div>
<p>(notice we added the <code>up-layer='root'</code> attribute). For this to work we need to <em>not</em> <code>reload</code> in the <code>up-on-dismissed</code> function because if we reload the companies list the contents of the flash-messages will be re-read (because it has the <code>up-hungry</code> attr) and be immediately cleared out! However in this case we need to reload because a company may be edited!</p>
<h4 id="improving-delete">Improving delete</h4>
<p>Right now, the delete button is a form, similar to this:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-submit</span> <span class="na">up-confirm</span><span class="o">=</span><span class="s">'Really?'</span> <span class="na">class</span><span class="o">=</span><span class="s">"d-inline"</span> <span class="na">method</span><span class="o">=</span><span class="s">'POST'</span> <span class="na">action</span><span class="o">=</span><span class="s">'{% url "company-delete" company.id %}'</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span> <span class="na">value</span><span class="o">=</span><span class="s">'Delete'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-danger mr-3'</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</code></pre></div>
<p>So this is an unpoly-handled form and will display a <code>Really?</code> javascript prompt to make sure the user really wants to delete the company. </p>
<p>I have to confess that I don’t like javascript prompts because they can’t be styled and seem out of context from the app. However we can improve that behavior with unpoly. Here’s an improved version of the delete functionality:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-danger'</span> <span class="na">up-layer</span><span class="o">=</span><span class="s">"new"</span> <span class="na">up-content</span><span class="o">=</span><span class="s">'</span>
<span class="s"> <h3>Delete company {{ company.name }}</h3></span>
<span class="s"> Do you want to delete the company? </span>
<span class="s"> <form up-submit up-target=".table" up-layer="root" class="d-inline" method="POST" action="{% url "company-delete" company.id %}"></span>
<span class="s"> {% csrf_token %}</span>
<span class="s"> <input type="submit" value="Yes" class="btn btn-danger mr-3"/></span>
<span class="s"> <a href="#" class="btn btn-secondary" up-dismiss>No</a></span>
<span class="s"> </form></span>
<span class="s"> '</span><span class="p">></span>Delete<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>We changed the delete button to open a new layer. Instead of having a special view for the delete confirmation, we’re using the <code>up-content</code> attribute to directly pass the static <span class="caps">HTML</span> for the confirmation, which actually includes the delete form like before. Notice that we also include an <code>up-dismiss</code> button that clears the overlay when the user presses No. The up-layer of the form is <code>root</code> so when the form is submitted it will close both the confirmation overlay and the company detail overlay! Now, we’ll change the <code>reloadWithFlashIfEvent</code> like this:</p>
<div class="highlight"><pre><span></span><code><span class="k">async</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">reloadWithFlashIfEvent</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span><span class="w"> </span><span class="nx">flash</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">reload</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">focus</span><span class="o">:</span><span class="w"> </span><span class="s1">':main'</span><span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">value</span><span class="w"> </span><span class="ow">instanceof</span><span class="w"> </span><span class="nx">Event</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">value</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s1">':peel'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">affix</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">'flash-messages'</span><span class="p">),</span><span class="w"> </span><span class="s1">'.alert.fade.show.alert-danger'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="nx">flash</span><span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>This checks if the value is an event <em>or</em> <code>:peel</code>; this is the value that is passed when the overlay is dismissed because we use the <code>up-layer='root'</code> from the delete form.</p>
<h2 id="improving-interaction-with-django-packages">Improving interaction with Django packages</h2>
<p>There are two very important packages that I use on almost all my projects: <a href="https://django-tables2.readthedocs.io/en/latest/">django-tables2</a> and <a href="https://django-filter.readthedocs.io/en/stable/">django-filter</a>. You can see these in action at the <code>/core/tf/</code> path on the demo app. You’ll see that:</p>
<ul>
<li>Filtering is instant (when entering a character it will filter without the need to submit the form explicitly )</li>
<li>The row detail links open in an overlay</li>
<li>Sorting and pagination are handled by unpoly (so they don’t do full page reloads)</li>
</ul>
<p>To have the instant filtering we’ve changed our filter form like this:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-autosubmit</span> <span class="na">up-delay</span><span class="o">=</span><span class="s">'250'</span> <span class="na">class</span><span class="o">=</span><span class="s">'form-inline'</span> <span class="na">method</span><span class="o">=</span><span class="s">'GET'</span> <span class="na">action</span><span class="o">=</span><span class="s">''</span> <span class="na">up-target</span><span class="o">=</span><span class="s">'.form-data'</span><span class="p">></span>
{{ filter.form|crispy }}
<span class="p"><</span><span class="nt">input</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-info'</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span> <span class="na">value</span><span class="o">=</span><span class="s">'Filter'</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">up-follow</span> <span class="na">href</span><span class="o">=</span><span class="s">'{{ request.path }}'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-secondary'</span><span class="p">></span>Reset<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</code></pre></div>
<p>Notice the <code>up-autosubmit</code>; this will submit the form when a field changes. Also the <code>up-delay</code> adds a small
delay before the form is submitted so when the user writes <code>foo</code> it will do 1 query instead of 3 (if he writes
fast enough of course). The <code>up-target</code> attribute is used to specify the element that will be updated with the
response. In this case we’re using a <code>form-data</code> element that includes the whole table (using django-tables2 of course):</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'form-data'</span><span class="p">></span>
{% render_table table %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>To open the links in a layer we only need to pass the correct parameters to the table field, for example in our case the table is like this:</p>
<div class="highlight"><pre><span></span><code><span class="k">class</span> <span class="nc">CompanyTable</span><span class="p">(</span><span class="n">tables</span><span class="o">.</span><span class="n">Table</span><span class="p">):</span>
<span class="nb">id</span> <span class="o">=</span> <span class="n">tables</span><span class="o">.</span><span class="n">LinkColumn</span><span class="p">(</span>
<span class="s2">"company-detail"</span><span class="p">,</span>
<span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="n">A</span><span class="p">(</span><span class="s2">"id"</span><span class="p">)],</span>
<span class="n">attrs</span><span class="o">=</span><span class="p">{</span>
<span class="s2">"a"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"class"</span><span class="p">:</span> <span class="s2">"btn btn-primary btn-sm"</span><span class="p">,</span>
<span class="s2">"up-on-dismissed"</span><span class="p">:</span> <span class="s2">"up.reload('.table', { focus: ':main' })"</span><span class="p">,</span>
<span class="s2">"up-layer"</span><span class="p">:</span> <span class="s2">"new"</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">)</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Company</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s2">"django_tables2/bootstrap4.html"</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s2">"id"</span><span class="p">,</span> <span class="s2">"name"</span><span class="p">,</span> <span class="s2">"address"</span><span class="p">)</span>
</code></pre></div>
<p>So we a pass the attributes directly to the link’s <code>a</code> element. Nothing really fancy is needed.</p>
<p>Furthermore, notice that we use the builtin bootstrap4 template. We don’t change the template at all. The original django-tables2 template does <em>not</em> have <code>unpoly</code> interation! So if we leave it like this the pagination
and header links will start a full request/response. To fix that, we could override the template with our own however this is not the ideal solution for me. </p>
<p>Instead, we can use <code>up.compiler</code>: </p>
<div class="highlight"><pre><span></span><code><span class="nx">up</span><span class="p">.</span><span class="nx">compiler</span><span class="p">(</span><span class="s1">'.pagination .page-item a.page-link'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">link</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-target'</span><span class="p">,</span><span class="w"> </span><span class="s2">".table-container"</span><span class="p">)</span>
<span class="p">})</span>
<span class="nx">up</span><span class="p">.</span><span class="nx">compiler</span><span class="p">(</span><span class="s1">'th.orderable a[href]'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">link</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-target'</span><span class="p">,</span><span class="w"> </span><span class="s2">".table-container"</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div>
<p>The <code>up.compiler</code> function takes a <span class="caps">CSS</span> selector and a callback function. The callback function is called
when a snippet matching the selector is added to the <span class="caps">DOM</span>. In this case we’re adding the <code>up-target='.table-container'</code> unpoly attributes
to both the pagination and the table header order links. The <code>.table-container</code> is the element that contains the table (it is added by django-tables2).</p>
<p>This way, when unpoly sees these links it will add the <code>up-target</code> attribute (and functionality) to these without the need to override any templates.</p>
<h2 id="advanced-concepts">Advanced concepts</h2>
<p>We’ll discuss some more advanced concepts of unpoly now.</p>
<h3 id="more-about-upcompiler">More about up.compiler</h3>
<p>The <code>up.compiler</code> function is <a href="https://unpoly.com/up.compiler">very powerful</a>. We already used it to add functionality to
all our nav links (see the navigation aliases before) to avoid forgetting it and to add the <code>up-target</code> to our table links
to avoid overriding the django-table2 templates.</p>
<p>Beyond these, the most important functionality of <code>up.compiler</code> is to replace the javascript on load (or jquery <code>$(function() {})</code>) event.
Most common javascript libraries will be initialized when the document is ready. Unfortunately, when a page is loaded through unpoly
this event will <em>not</em> be trigger, so the javascript elements will not be initialized! Let’s suppose that we’ve got a bunch traditional jquery ui datepicker elements and all these have the <code>.datepicker</code> css class. Normally we’d initialize it like</p>
<div class="highlight"><pre><span></span><code><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'.datepicker'</span><span class="p">).</span><span class="nx">datepicker</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div>
<p>If we are to load a form with these elements through unpoly we won’t get the datepicker functionality. To fix this we can use <code>up.compiler</code>:</p>
<div class="highlight"><pre><span></span><code><span class="nx">up</span><span class="p">.</span><span class="nx">compiler</span><span class="p">(</span><span class="s1">'.datepicker'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">element</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="nx">element</span><span class="p">).</span><span class="nx">datepicker</span><span class="p">()</span>
<span class="p">})</span>
</code></pre></div>
<p>So when unpoly sees a <code>.datepicker</code> element it will call that callback function and initialize it! This will work properly if you follow links through <code>up-follow</code> or open new overlays with <code>up-layer='new'</code>.</p>
<h3 id="passing-context-from-unpoly-to-server">Passing context from unpoly to server</h3>
<p>Unpoly has an <code>up-context</code> attribute that can be used to pass context to the server. This must be a json object and can then be used to change the response based on that context. If we are using the unpoly python package then the context will be available in the <code>request.up.context</code> dictionary.</p>
<p>Let’s see a particular example from the demo. When we create a new task we’ve got the following link:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span>
<span class="na">class</span><span class="o">=</span><span class="s">'btn btn-primary'</span>
<span class="na">up-layer</span><span class="o">=</span><span class="s">'new'</span>
<span class="na">up-context</span><span class="o">=</span><span class="s">'{"new_task": true}'</span>
<span class="na">up-accept-location</span><span class="o">=</span><span class="s">'/core/tasks/detail/$id/'</span>
<span class="na">up-on-accepted</span><span class="o">=</span><span class="s">"reloadWithFlash('.tasks', 'Task created!')"</span>
<span class="na">href</span><span class="o">=</span><span class="s">'{% url "task-create" %}'</span><span class="p">></span>New task<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</code></pre></div>
<p>Compare this with the edit link: </p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">a</span> <span class="na">up-target</span><span class="o">=</span><span class="s">'.task'</span> <span class="na">href</span><span class="o">=</span><span class="s">'{% url "task-update" task.id %}'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-sm btn-outline-secondary'</span><span class="p">></span>Edit<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</code></pre></div>
<p>Notice that the <code>up-context</code> is only included in the new link. Now, let’s see how the task form is implemented:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-submit</span> <span class="err">{%</span> <span class="na">if</span> <span class="na">not</span> <span class="na">up</span><span class="err">.</span><span class="na">context</span><span class="err">.</span><span class="na">new_task</span> <span class="err">%}</span><span class="na">up-target</span><span class="o">=</span><span class="s">'.task'</span><span class="err">{%</span> <span class="na">endif</span> <span class="err">%}</span> <span class="na">class</span><span class="o">=</span><span class="s">'task card'</span> <span class="na">method</span><span class="o">=</span><span class="s">"POST"</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-body d-flex flex-column"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group flex-grow-1 mb-0"</span><span class="p">></span>
{{ form|crispy }}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"flex-grow-0"</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-primary mt-2'</span> <span class="na">value</span><span class="o">=</span><span class="s">'Save'</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</code></pre></div>
<p>So, using Django we check to see if there’s <code>new_task</code> in the context and add an <code>up-target='.task'</code> if <em>not</em>. This way, we’ll get an <code>up-target='.task'</code> in the form <em>only</em> if we click the edit task button. Beyond this the form is the same for both the new and edit links. </p>
<p>This is needed because when we open the edit task it will be loaded in the same <code>.task</code> element we clicked the edit
link from (remember that unpoly is smart enough <a href="https://unpoly.com/fragment-placement#interaction-origin-is-considered">to match closer elements</a>). When the form is submitted we want the detail view of
the task to also be rendered on the same <code>.task</code> element so we use the <code>up-target='.task'</code>. This isn’t needed in
the create new since it will be rendered in a new layer and we want to reload the the tasks with the flash message
when the new task is created. </p>
<p>Please notice that if we were to use <code>up-target='.task'</code> for both the new and edit form we’d get an error when
the new task form was submitted because it wouldn’t be able to match the target <code>.task</code> element!</p>
<h3 id="listening-to-unpoly-events">Listening to unpoly events</h3>
<p>For most things happening in unpoly you’ll find out that there are events that you can listen to and add behavior. There are cases where handling these is useful. </p>
<p>For example, I’ve observed that if you’re using bootstrap dropdowns and click a link while the dropdowns are <em>opened</em>, the dropdowns <em>will remain open</em> when the fragment has been loaded! This is very annoying. One simple way to resolve that is include the navigation inside your <code>up-main</code> element so the dropdowns will be reloaded. However there’s a better way by using unpoly events like in the following snippet:</p>
<div class="highlight"><pre><span></span><code><span class="w"> </span><span class="nx">up</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'up:link:follow'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">,</span><span class="w"> </span><span class="nx">link</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Hide visible dropdowns</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">dropdownElementList</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">'.dropdown-toggle.show'</span><span class="p">)</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">dropdownList</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[...</span><span class="nx">dropdownElementList</span><span class="p">].</span><span class="nx">map</span><span class="p">(</span><span class="nx">dropdownToggleEl</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">bootstrap</span><span class="p">.</span><span class="nx">Dropdown</span><span class="p">(</span><span class="nx">dropdownToggleEl</span><span class="p">))</span>
<span class="w"> </span><span class="nx">dropdownList</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">dropdown</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">dropdown</span><span class="p">.</span><span class="nx">hide</span><span class="p">())</span>
<span class="w"> </span><span class="p">})</span>
</code></pre></div>
<p>Please notice that this code is for bootstrap 5 (not 4 as the remaining code in the demo since it’s from a different project). So what happens is that whenever a link is followed from unpoly we’ll clear the open dropdowns (the code isn’t very important here).</p>
<h3 id="updating-history">Updating history</h3>
<p>One thing to consider when using unpoly is <em>when</em> we actually need to update the browser history and url.
<a href="https://unpoly.com/a-up-follow#up-history">By default</a>, unpoly will update the url only if the <code>up-target</code>
matches <code>up-main</code> (so if there’s no <code>up-target</code> the url will always be upgraded).</p>
<p>This can be configured through the <code>up-history</code> attribute. By default this has the value <code>'auto'</code> and we can
set to <code>'true'</code> or <code>'false'</code> if we want to configure it so that unpoly updates the history or not for a particlar
link or form submission. </p>
<p>Let’s see a particular example from the demo. Because the <code>up-target</code> of the filter form on the is set to <code>.form-data</code>:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-autosubmit</span> <span class="na">up-delay</span><span class="o">=</span><span class="s">'250'</span> <span class="na">class</span><span class="o">=</span><span class="s">'form-inline'</span> <span class="na">method</span><span class="o">=</span><span class="s">'GET'</span> <span class="na">action</span><span class="o">=</span><span class="s">''</span> <span class="na">up-target</span><span class="o">=</span><span class="s">'.form-data'</span><span class="p">></span>
</code></pre></div>
<p>the url will not be updated when the filter is changed. This is contrary to the usual way these kind of filters work
(i.e update the url with the filter parameters). So we can add the <code>up-history</code> attribute:</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">form</span> <span class="na">up-autosubmit</span> <span class="na">up-history</span><span class="o">=</span><span class="s">'true'</span> <span class="na">up-delay</span><span class="o">=</span><span class="s">'250'</span> <span class="na">class</span><span class="o">=</span><span class="s">'form-inline'</span> <span class="na">method</span><span class="o">=</span><span class="s">'GET'</span> <span class="na">action</span><span class="o">=</span><span class="s">''</span> <span class="na">up-target</span><span class="o">=</span><span class="s">'.form-data'</span><span class="p">></span>
</code></pre></div>
<p>The same applies for the pagination and header ordering links. They update the <code>.table-container</code> element so we’ll need
to add <code>up-history=true</code> also to them. Thus we’ll change the <code>up.compiler</code> for these elements like this:</p>
<div class="highlight"><pre><span></span><code><span class="nx">up</span><span class="p">.</span><span class="nx">compiler</span><span class="p">(</span><span class="s1">'.pagination .page-item a.page-link'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">link</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-follow'</span><span class="p">,</span><span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">href</span><span class="p">)</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-target'</span><span class="p">,</span><span class="w"> </span><span class="s2">".table-container"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-history'</span><span class="p">,</span><span class="w"> </span><span class="s2">"true"</span><span class="p">)</span>
<span class="p">})</span>
<span class="nx">up</span><span class="p">.</span><span class="nx">compiler</span><span class="p">(</span><span class="s1">'th.orderable a[href]'</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">link</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-follow'</span><span class="p">,</span><span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">href</span><span class="p">)</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-target'</span><span class="p">,</span><span class="w"> </span><span class="s2">".table-container"</span><span class="p">)</span>
<span class="w"> </span><span class="nx">link</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">'up-history'</span><span class="p">,</span><span class="w"> </span><span class="s2">"true"</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div>
<p>This way, both the ordering links and the selected page will be reflected on the url history. </p>
<p>The final result is that this filter/table page will have the usual functionality of updating the url when the filter
is changed or the table sorting/pagination links are used.</p>
<h3 id="troubleshooting">Troubleshooting</h3>
<p>As I’ve already mentioned, the most common problem you are going to have with unpoly is when you use javascript on your page ready event. Unfortunately there’s a lot of functionality that relies on that event and pages will break when you use unpoly in these cases. That’s why I recommend to use <code>up-follow</code> and <code>up-submit</code> for your links and forms on a case-by-case basis on non greenfield projects so you’ve got more control on what works with unpoly and what is not working. Another thing that is very important to notice here is that I’ve stumbled upon libraries that not only rely on the load event but actually <em>there’s no other way to initialize them</em>! For example, there’ are js libraries that have code like </p>
<div class="highlight"><pre><span></span><code><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">initElement</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'.selected-elements'</span><span class="p">).</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">initElement</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
<span class="w"> </span><span class="p">})</span>
<span class="p">})</span>
</code></pre></div>
<p>so the actual function that does the initialization (<code>initElement</code>) isn’t public and you can’t call it from the <code>up.compiler</code>. In these cases you’ll need to somehow make the <code>initElement</code> public so you can call it from the <code>up.compiler</code> or use a different library!</p>
<p>The other major case for headaches in unpoly overlays. Although they are very powerful I recommend to <em>not</em> abuse them and use them only when you feel that are really needed and would improve the <span class="caps">UX</span> of the user. For example, I’d recommend using them to add new options on a select list (similar to how the project form works for companies and of course similar to how django admin does it). Also you could use overlays to have a functionality similar to django-inlines (see how the projects are added to the company) however I’d probably prefer to do that using normal django inlines especially when the standalone child edit functionality isn’t needed.</p>
<p>Special care must be taken for the integration between layers and messages. I have tried to provide a solution in the previous sections by proposing flashing the message with javascript on the cases where the message will be “eaten” by a discarded response however I’m afraid that depending on how you’ve architectured your app you’ll may still get problems. The important thing is to understand how messages work (or not) and in which cases you may skip using messages at all since the feedback would be immediate and the users don’t really need messages.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In conclusion, using unpoly with your Django apps can enhance the <span class="caps">UX</span> of your users by reducing page reloads and providing a more responsive and intuitive interface with little work from the developer. I recommend everybody to start integrating unpoly in their projects and see how it can improve the <span class="caps">UX</span> of your users!</p>Accessing MS Access databases from Python and Django2023-03-22T15:20:00+02:002023-03-22T15:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2023-03-22:/2023/03/22/access-microsoft-access-python-django/<p>Accessing data from a Microsoft Access database with Django</p><p>Have you ever needed to use data from a Microsoft Access database on a Django project?
If so, you might have found that the data is contained in an .accdb file, which can make accessing it a challenge.
Actually, access files are either .mdb (older versions) and .accdb (more current versions); although the
.mdb files is easier to access from python, most Access database would be .accdb nowadays.</p>
<p>The naive/simple way to access this data is to bite the bullet, install Access on your computer
and use it to export the tables one by one in a
more “open” format (i.e xlsx files). After some research I found out that there are ways to connect to
this Access database through python and querying it directly. Thus I decided to implement a more automatic method
of exporting your data. In this article, I’ll walk you through the steps to accomplish this,
specifically we’ll cover how to:</p>
<ul>
<li>Connect to an accdb database </li>
<li>Export all the tables of that database to a json file</li>
<li>Create a models.py based on the exported data</li>
<li>Import the json data that was exported into the newly created models</li>
</ul>
<p>For the first two steps we’ll only use python. For the last two we’ll also need some Django.
By the end of this article, you’ll have a streamlined process for accessing your Microsoft
Access data in your Django projects.</p>
<h2 id="connecting-to-a-microsoft-access-database-from-python">Connecting to a Microsoft Access database from python</h2>
<p>To be able to connect to an Access database from python you can use the
<a href="https://github.com/pypyodbc/pypyodbc">pypyodbc</a>. This is a pure-python library that can connect
to <span class="caps">ODBC</span>. To install it run <code>pip install pypyodbc</code>; since this is a pure-python the installation should
be always successfull.</p>
<p>However, there is an important caveat when working with .accdb databases:
<em>You must use Windows</em> and install
<a href="https://www.microsoft.com/en-US/download/details.aspx?id=13255">Microsoft Access Database Engine 2010 Redistributable</a>.
This is necessary to ensure that the correct drivers are available, you can also take a look at
the instructions from pyodbc <a href="https://github.com/mkleehammer/pyodbc/wiki/Connecting-to-Microsoft-Access">here</a>.</p>
<p>When installing the Access Redistributable, it’s crucial to remember that you need to install
either the 32-bit or 64-bit of the Access Redistributable
depending on your python (you can’t install both).
To check if your python is 32bit or 64bit run <code>python</code> and check if it says 32 or 64 bit. Also
please notice that you may be able to install <em>only</em> the 32bit or <em>only</em> the 64bit version if you have an <span class="caps">MS</span> Office installed
(the Access Redistributable will match the <span class="caps">MS</span> Office bitness).</p>
<p>If that’s the case then my recommendation would be to generate a new virtualenv (similiar to the bitness of the
Access Redistributable you’ve installed on your system). Then install pypyodbc on that virtualenv and you should be fine.</p>
<p>If you want to use a .mdb database you should be able to do it without installing anything on Windows and it also should
be possible on Unix (I haven’t tested it though). </p>
<p>To ensure that you’ve got the correct drivers, run the following snippet
on a python shell:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pypyodbc</span>
<span class="nb">print</span><span class="p">(</span><span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">pypyodbc</span><span class="o">.</span><span class="n">drivers</span><span class="p">()))</span>
</code></pre></div>
<p>If you have installed the correct Microsoft Access Database Engine 2010 Redistributable you should see .accdb somewhere in the output, like this:</p>
<div class="highlight"><pre><span></span><code>Microsoft Access Driver (*.mdb)
Microsoft dBase Driver (*.dbf)
Microsoft Excel Driver (*.xls)
Microsoft ODBC for Oracle
Microsoft Paradox Driver (*.db )
Microsoft Text Driver (*.txt; <span class="gs">*.csv)</span>
<span class="gs">SQL Server</span>
<span class="gs">Oracle in OraClient10g_home1</span>
<span class="gs">SQL Server Native Client 11.0</span>
<span class="gs">ODBC Driver 17 for SQL Server</span>
<span class="gs">Microsoft Access Driver (*</span>.mdb, <span class="gs">*.accdb)</span>
<span class="gs">Microsoft Excel Driver (*</span>.xls, <span class="gs">*.xlsx, *</span>.xlsm, <span class="gs">*.xlsb)</span>
<span class="gs">Microsoft Access dBASE Driver (*</span>.dbf, <span class="gs">*.ndx, *</span>.mdx)
Microsoft Access Text Driver (*.txt, *.csv)
</code></pre></div>
<p>If on the other hand you can’t access .accdb files you’ll get much less options:</p>
<div class="highlight"><pre><span></span><code><span class="nv">SQL</span><span class="w"> </span><span class="nv">Server</span>
<span class="nv">PostgreSQL</span><span class="w"> </span><span class="nv">ANSI</span><span class="ss">(</span><span class="nv">x64</span><span class="ss">)</span>
<span class="nv">PostgreSQL</span><span class="w"> </span><span class="nv">Unicode</span><span class="ss">(</span><span class="nv">x64</span><span class="ss">)</span>
<span class="nv">PostgreSQL</span><span class="w"> </span><span class="nv">ANSI</span>
<span class="nv">PostgreSQL</span><span class="w"> </span><span class="nv">Unicode</span>
<span class="nv">SQL</span><span class="w"> </span><span class="nv">Server</span><span class="w"> </span><span class="nv">Native</span><span class="w"> </span><span class="nv">Client</span><span class="w"> </span><span class="mi">11</span>.<span class="mi">0</span>
<span class="nv">ODBC</span><span class="w"> </span><span class="nv">Driver</span><span class="w"> </span><span class="mi">17</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="nv">SQL</span><span class="w"> </span><span class="nv">Server</span>
</code></pre></div>
<p>In any case, after you’ve installed the correct drivers you can connect to the database
(let’s suppose it’s named <code>access_data.accdb</code> on the parent directory) like this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pypyodbc</span>
<span class="n">pypyodbc</span><span class="o">.</span><span class="n">lowercase</span> <span class="o">=</span> <span class="kc">False</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">pypyodbc</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span>
<span class="sa">r</span><span class="s2">"Driver={Microsoft Access Driver (*.mdb, *.accdb)};"</span>
<span class="o">+</span> <span class="sa">r</span><span class="s2">"Dbq=..</span><span class="se">\\</span><span class="s2">access_data.accdb;"</span>
<span class="p">)</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">cur</span><span class="o">.</span><span class="n">tables</span><span class="p">(</span><span class="n">tableType</span><span class="o">=</span><span class="s2">"TABLE"</span><span class="p">):</span>
<span class="nb">print</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>
</code></pre></div>
<p>If everything’s ok the above will print all the tables that are contained in the database.</p>
<h2 id="exporting-the-data-from-the-access-database-to-a-json-file">Exporting the data from the Access database to a json file</h2>
<p>After you’re able to connect to the database you can export all the data to a json file. Actually we’ll export both the data of the database and a “description” of the data (the names of the tables along with their columns and types). The description of the data will be useful later. </p>
<p>The general idea is:</p>
<ol>
<li>Connect the database</li>
<li>Get the names of the tables in a list</li>
<li>For each table<ul>
<li>Export a description of its columns</li>
<li>Export all its data</li>
</ul>
</li>
<li>Write the description and the data to two json files</li>
</ol>
<p>This is done by running the following snippet:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">pypyodbc</span>
<span class="kn">import</span> <span class="nn">struct</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">date</span>
<span class="kn">import</span> <span class="nn">decimal</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"running as </span><span class="si">{0}</span><span class="s2">-bit"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">struct</span><span class="o">.</span><span class="n">calcsize</span><span class="p">(</span><span class="s2">"P"</span><span class="p">)</span> <span class="o">*</span> <span class="mi">8</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">normalize</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""A simple function to normalize table names"""</span>
<span class="k">return</span> <span class="n">s</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">" "</span><span class="p">,</span> <span class="s2">"_"</span><span class="p">)</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">pypyodbc</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span>
<span class="sa">r</span><span class="s2">"Driver={Microsoft Access Driver (*.mdb, *.accdb)};"</span>
<span class="o">+</span> <span class="sa">r</span><span class="s2">"Dbq=..</span><span class="se">\\</span><span class="s2">access_data.accdb;"</span>
<span class="p">)</span>
<span class="n">cur</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">tables</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">cur</span><span class="o">.</span><span class="n">tables</span><span class="p">(</span><span class="n">tableType</span><span class="o">=</span><span class="s2">"TABLE"</span><span class="p">):</span>
<span class="c1"># Only get the table names</span>
<span class="n">tables</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
<span class="c1"># data will contain the data of all tables. It will have the following structure:</span>
<span class="c1"># {"table_1": [{"column_1": value, "column_2": value}, ...], "table_2": ...}</span>
<span class="n">data</span> <span class="o">=</span> <span class="p">{}</span>
<span class="c1"># descriptions will have a description of all the tables. It will have the following structure:</span>
<span class="c1"># [</span>
<span class="c1"># {</span>
<span class="c1"># "table_name": "table 1",</span>
<span class="c1"># "fixed_table_name": "table_1",</span>
<span class="c1"># "columns": [</span>
<span class="c1"># {"name": "column_1", "fixed_name": "column_1","type": "str"},</span>
<span class="c1"># {"name": "column_2", "fixed_name": "column_2","type": "int"},</span>
<span class="c1"># ]</span>
<span class="n">descriptions</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">table_name</span> <span class="ow">in</span> <span class="n">tables</span><span class="p">:</span>
<span class="n">fixed_table_name</span> <span class="o">=</span> <span class="n">normalize</span><span class="p">(</span><span class="n">table_name</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"~~~~~~~~~~~~~</span><span class="si">{</span><span class="n">table_name</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">fixed_table_name</span><span class="si">}</span><span class="s2">~~~~~~~~~~~~~"</span><span class="p">)</span>
<span class="n">q</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">'SELECT * FROM "</span><span class="si">{</span><span class="n">table_name</span><span class="si">}</span><span class="s1">"'</span>
<span class="n">description</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"table_name"</span><span class="p">:</span> <span class="n">table_name</span><span class="p">,</span>
<span class="s2">"fixed_table_name"</span><span class="p">:</span> <span class="n">fixed_table_name</span><span class="p">,</span>
<span class="s2">"columns"</span><span class="p">:</span> <span class="p">[],</span>
<span class="p">}</span>
<span class="n">descriptions</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">description</span><span class="p">)</span>
<span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">q</span><span class="p">)</span>
<span class="c1"># Here we get the description of the columns of the table from the cursor; we'll use that to fill the description.columns list</span>
<span class="n">columns</span> <span class="o">=</span> <span class="n">cur</span><span class="o">.</span><span class="n">description</span>
<span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">columns</span><span class="p">:</span>
<span class="n">description</span><span class="p">[</span><span class="s2">"columns"</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="n">c</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="s2">"fixed_name"</span><span class="p">:</span> <span class="n">normalize</span><span class="p">(</span><span class="n">c</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="s2">"type"</span><span class="p">:</span> <span class="n">c</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="vm">__name__</span><span class="p">}</span>
<span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
<span class="c1"># And here we retrieve the data of the whole table</span>
<span class="c1"># Notice we use some double for loop comprehension to </span>
<span class="c1"># create a json object with a column_name: value structure</span>
<span class="c1"># for each row</span>
<span class="n">data</span><span class="p">[</span><span class="n">fixed_table_name</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span><span class="n">normalize</span><span class="p">(</span><span class="n">columns</span><span class="p">[</span><span class="n">index</span><span class="p">][</span><span class="mi">0</span><span class="p">]):</span> <span class="n">column</span> <span class="k">for</span> <span class="n">index</span><span class="p">,</span> <span class="n">column</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">value</span><span class="p">)}</span>
<span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">cur</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<span class="p">]</span>
<span class="n">cur</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="c1"># This is a function to serialize datetime and decimal objects </span>
<span class="c1"># to json; without it the json.dump function will fail if the </span>
<span class="c1"># results contain dates or decimals</span>
<span class="k">def</span> <span class="nf">json_serial</span><span class="p">(</span><span class="n">obj</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""JSON serializer for objects not serializable by default json code"""</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="p">(</span><span class="n">datetime</span><span class="p">,</span> <span class="n">date</span><span class="p">)):</span>
<span class="k">return</span> <span class="n">obj</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">obj</span><span class="p">,</span> <span class="n">decimal</span><span class="o">.</span><span class="n">Decimal</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">obj</span><span class="p">)</span>
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"Type </span><span class="si">%s</span><span class="s2"> not serializable"</span> <span class="o">%</span> <span class="nb">type</span><span class="p">(</span><span class="n">obj</span><span class="p">))</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"..</span><span class="se">\\</span><span class="s2">access_description.json"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">outfile</span><span class="p">:</span>
<span class="c1"># Notice the default=json_serial </span>
<span class="n">json</span><span class="o">.</span><span class="n">dump</span><span class="p">(</span><span class="n">descriptions</span><span class="p">,</span> <span class="n">outfile</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">json_serial</span><span class="p">)</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"..</span><span class="se">\\</span><span class="s2">access_data.json"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">outfile</span><span class="p">:</span>
<span class="n">json</span><span class="o">.</span><span class="n">dump</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">outfile</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="n">json_serial</span><span class="p">)</span>
</code></pre></div>
<p>If you run the above code and don’t see any errors you should have two json files in the parent directory: <code>access_description.json</code> and <code>access_data.json</code>. The dump of your access database is complete!</p>
<h2 id="creating-a-modelspy-based-on-the-exported-data">Creating a models.py based on the exported data</h2>
<p>Now that we have the description of the data in our database it is possible to create a small script that would help us
generate the models for importing that data into Django. This could by done by a snippet similar to this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">json</span>
<span class="k">def</span> <span class="nf">get_ctype</span><span class="p">(</span><span class="n">t</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""Depending on the type of each column add a different field in the model"""</span>
<span class="k">if</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"str"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"TextField(blank=True, )"</span>
<span class="k">elif</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"int"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"IntegerField(blank=True, null=True)"</span>
<span class="k">elif</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"float"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"FloatField(blank=True, null=True)"</span>
<span class="k">elif</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"bool"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"BooleanField(blank=True, null=True)"</span>
<span class="k">elif</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"datetime"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"DateTimeField(blank=True, null=True)"</span>
<span class="k">elif</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"date"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"DateField(blank=True, null=True)"</span>
<span class="k">elif</span> <span class="n">t</span> <span class="o">==</span> <span class="s2">"Decimal"</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">"DecimalField(blank=True, null=True, max_digits=15, decimal_places=5)"</span>
<span class="k">else</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
<span class="k">raise</span> <span class="ne">NotImplementedError</span>
<span class="c1"># Load the descriptions we created in the previous step</span>
<span class="n">descriptions</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="s2">"..</span><span class="se">\\</span><span class="s2">access_description.json"</span><span class="p">))</span>
<span class="c1"># mlines will be an array of the lines of the models.py file</span>
<span class="n">mlines</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"from django.db import models"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="s2">""</span><span class="p">]</span>
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">descriptions</span><span class="p">:</span>
<span class="c1"># Create a model for each table</span>
<span class="n">mname</span> <span class="o">=</span> <span class="n">d</span><span class="p">[</span><span class="s2">"fixed_table_name"</span><span class="p">]</span><span class="o">.</span><span class="n">capitalize</span><span class="p">()</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"class </span><span class="si">{</span><span class="n">mname</span><span class="si">}</span><span class="s2">(models.Model):"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">d</span><span class="p">[</span><span class="s2">"columns"</span><span class="p">]:</span>
<span class="n">ctype</span> <span class="o">=</span> <span class="n">get_ctype</span><span class="p">(</span><span class="n">c</span><span class="p">[</span><span class="s2">"type"</span><span class="p">])</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">" </span><span class="si">{</span><span class="n">c</span><span class="p">[</span><span class="s1">'fixed_name'</span><span class="p">]</span><span class="si">}</span><span class="s2"> = models.</span><span class="si">{</span><span class="n">ctype</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">" class Meta:"</span><span class="p">)</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">" db_table = '</span><span class="si">{</span><span class="n">d</span><span class="p">[</span><span class="s1">'fixed_table_name'</span><span class="p">]</span><span class="si">}</span><span class="s2">'"</span><span class="p">)</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">" verbose_name = '</span><span class="si">{</span><span class="n">d</span><span class="p">[</span><span class="s1">'table_name'</span><span class="p">]</span><span class="si">}</span><span class="s2">'"</span><span class="p">)</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
<span class="n">mlines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"..</span><span class="se">\\</span><span class="s2">access_models.py"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">outfile</span><span class="p">:</span>
<span class="n">outfile</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">mlines</span><span class="p">))</span>
</code></pre></div>
<p>This will generate a fine named access_models.py. You should edit this file a
to add your primary and foreign keys. In an ideal world this would be done automatically,
however I couldn’t find a way to extract the primary and foreign keys of the tables from
the Access database. Also by default I’ve set all fields to allow blank and null values; please
you should fix that according to your needs.</p>
<p>After you edit the file, create a new app in your Django project and
copy the file to the models.py file of the new app. Add that app to your <code>INSTALLED_APPS</code> in
the settings.py file
and run <code>python manage.py migrate</code> to create the tables in your database.</p>
<h2 id="import-the-json-data-to-django">Import the json data to Django</h2>
<p>The final piece of the puzzle is to import the data we extracted before directly in Django.
Because we know the names of all the models and fields this is trivial to do:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.core.management.base</span> <span class="kn">import</span> <span class="n">BaseCommand</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">transaction</span>
<span class="kn">from</span> <span class="nn">access_data</span> <span class="kn">import</span> <span class="n">models</span>
<span class="c1"># This is a list of all the fields that are foreign keys; these need special handling</span>
<span class="n">FK_FIELDS</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1"># ...</span>
<span class="p">]</span>
<span class="c1"># You need to add the table names from the access database here. This is required</span>
<span class="c1"># if you have relations in order to add first the tables without dependencies and last</span>
<span class="c1"># the tables that belong on these </span>
<span class="n">TABLE_NAMES</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1"># ...</span>
<span class="p">]</span>
<span class="k">def</span> <span class="nf">fix_fks</span><span class="p">(</span><span class="n">k</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""Add _id to the end of the field name if it is a foreign key to pass the pk of the</span>
<span class="sd"> foreign key instead of the whole object"""</span>
<span class="k">if</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">FK_FIELDS</span><span class="p">:</span>
<span class="k">return</span> <span class="n">k</span> <span class="o">+</span> <span class="s1">'_id'</span>
<span class="k">return</span> <span class="n">k</span>
<span class="k">def</span> <span class="nf">get_model_by_table</span><span class="p">(</span><span class="n">table</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""Get the model by the table name"""</span>
<span class="k">return</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">models</span><span class="p">,</span> <span class="n">table</span><span class="o">.</span><span class="n">capitalize</span><span class="p">())</span>
<span class="k">class</span> <span class="nc">Command</span><span class="p">(</span><span class="n">BaseCommand</span><span class="p">):</span>
<span class="nd">@transaction</span><span class="o">.</span><span class="n">atomic</span>
<span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">):</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"..</span><span class="se">\\</span><span class="s2">access_data.json"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
<span class="n">j</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
<span class="c1"># Delete the existing data before importing. This is optional but I find it useful</span>
<span class="c1"># Notice that we delete the tables in reverse order to avoid foreign key errors</span>
<span class="k">for</span> <span class="n">table</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">TABLE_NAMES</span><span class="p">):</span>
<span class="n">get_model_by_table</span><span class="p">(</span><span class="n">table</span><span class="p">)</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">delete</span><span class="p">()</span>
<span class="k">for</span> <span class="n">table</span> <span class="ow">in</span> <span class="n">TABLE_NAMES</span><span class="p">:</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">j</span><span class="p">[</span><span class="n">table</span><span class="p">]:</span>
<span class="c1"># Create a dictionary with the column name: column value;</span>
<span class="c1"># notice the fix_fks to add the _id to the column</span>
<span class="n">row_ok</span> <span class="o">=</span> <span class="p">{</span><span class="n">fix_fks</span><span class="p">(</span><span class="n">k</span><span class="p">):</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span><span class="n">v</span> <span class="ow">in</span> <span class="n">row</span><span class="o">.</span><span class="n">items</span><span class="p">()}</span>
<span class="nb">print</span><span class="p">(</span><span class="n">row_ok</span><span class="p">)</span>
<span class="c1"># Create the object; we could add thse to an array and do a bulk_create instead</span>
<span class="n">get_model_by_table</span><span class="p">(</span><span class="n">table</span><span class="p">)</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="o">**</span><span class="n">row_ok</span><span class="p">)</span>
</code></pre></div>
<p>The above code may error out if you have missing or bad data in your database. You should fix accordingly.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In conclusion, accessing and using data from Microsoft Access databases in Django may seem daunting at first,
but with the right tools and techniques, it can be a straightforward process.
By using the pypyodbc library and following the instructions outlined in this post, you can connect to your
.mdb or .accdb database and export its tables and schema to <span class="caps">JSON</span> files.
From there, it is trivial to create a models.py file for Django and
a management command to import the data.</p>
<p>Although I’ve presented these steps as separate snippets, you could also combine them into a
single management command within Django. The possibilities are endless, and with a little bit of
creativity, you can tailor this approach to your specific needs and data.</p>My essential guidelines for better Django development2022-09-28T11:10:00+03:002022-09-28T11:10:00+03:00Serafeim Papastefanostag:spapas.github.io,2022-09-28:/2022/09/28/django-guidelines/<p class="first last">A list of guidelines that I follow in every non-toy Django project I develop</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#model-design-guidelines" id="toc-entry-2">Model design guidelines</a><ul>
<li><a class="reference internal" href="#avoid-using-choices" id="toc-entry-3">Avoid using choices</a></li>
<li><a class="reference internal" href="#always-use-surrogate-keys" id="toc-entry-4">Always use surrogate keys</a></li>
<li><a class="reference internal" href="#always-use-a-through-model-on-your-m2m-relations" id="toc-entry-5">Always use a through model on your m2m relations</a></li>
<li><a class="reference internal" href="#use-a-custom-user-model" id="toc-entry-6">Use a custom user model</a></li>
</ul>
</li>
<li><a class="reference internal" href="#views-guidelines" id="toc-entry-7">Views guidelines</a><ul>
<li><a class="reference internal" href="#use-class-based-views" id="toc-entry-8">Use class based views</a></li>
<li><a class="reference internal" href="#view-method-overriding-guidelines" id="toc-entry-9">View method overriding guidelines</a></li>
</ul>
</li>
<li><a class="reference internal" href="#querying-guidelines" id="toc-entry-10">Querying guidelines</a><ul>
<li><a class="reference internal" href="#guidelines-for-the-n-1-problem" id="toc-entry-11">Guidelines for the n+1 problem</a></li>
<li><a class="reference internal" href="#re-use-your-queries" id="toc-entry-12">Re-use your queries</a></li>
</ul>
</li>
<li><a class="reference internal" href="#forms-guidelines" id="toc-entry-13">Forms guidelines</a><ul>
<li><a class="reference internal" href="#always-use-django-forms" id="toc-entry-14">Always use django-forms</a></li>
<li><a class="reference internal" href="#overriding-form-methods-guidelines" id="toc-entry-15">Overriding Form methods guidelines</a></li>
<li><a class="reference internal" href="#proper-cleaning" id="toc-entry-16">Proper cleaning</a></li>
<li><a class="reference internal" href="#overriding-the-init" id="toc-entry-17">Overriding the __init__</a></li>
<li><a class="reference internal" href="#laying-out-forms" id="toc-entry-18">Laying out forms</a></li>
<li><a class="reference internal" href="#improve-the-formset-functionality" id="toc-entry-19">Improve the formset functionality</a></li>
</ul>
</li>
<li><a class="reference internal" href="#template-guidelines" id="toc-entry-20">Template guidelines</a><ul>
<li><a class="reference internal" href="#stick-to-the-built-in-django-template-backend" id="toc-entry-21">Stick to the built-in Django template backend</a></li>
<li><a class="reference internal" href="#don-t-add-template-tags-when-you-can-use-a-method" id="toc-entry-22">Don’t add template tags when you can use a method</a></li>
<li><a class="reference internal" href="#re-use-templates-with-partials" id="toc-entry-23">Re-use templates with partials</a></li>
</ul>
</li>
<li><a class="reference internal" href="#settings-guidelines" id="toc-entry-24">Settings guidelines</a><ul>
<li><a class="reference internal" href="#use-a-package-instead-of-module" id="toc-entry-25">Use a package instead of module</a></li>
<li><a class="reference internal" href="#handle-secrets-properly" id="toc-entry-26">Handle secrets properly</a></li>
</ul>
</li>
<li><a class="reference internal" href="#static-and-media-guidelines" id="toc-entry-27">Static and media guidelines</a><ul>
<li><a class="reference internal" href="#use-manifeststaticfilesstorage" id="toc-entry-28">Use ManifestStaticFilesStorage</a></li>
<li><a class="reference internal" href="#organize-your-media-files" id="toc-entry-29">Organize your media files</a></li>
<li><a class="reference internal" href="#do-not-serve-media-through-your-application-server" id="toc-entry-30">Do not serve media through your application server</a></li>
<li><a class="reference internal" href="#secure-your-media-properly" id="toc-entry-31">Secure your media properly</a></li>
<li><a class="reference internal" href="#handle-stale-media" id="toc-entry-32">Handle stale media</a></li>
</ul>
</li>
<li><a class="reference internal" href="#debugging-guidelines" id="toc-entry-33">Debugging guidelines</a><ul>
<li><a class="reference internal" href="#be-careful-when-using-django-debug-toolbar" id="toc-entry-34">Be careful when using django-debug-toolbar</a></li>
<li><a class="reference internal" href="#use-the-werkzeug-debugger" id="toc-entry-35">Use the Werkzeug debugger</a></li>
</ul>
</li>
<li><a class="reference internal" href="#general-guidelines" id="toc-entry-36">General guidelines</a><ul>
<li><a class="reference internal" href="#consider-using-a-cookiecutter-project-template" id="toc-entry-37">Consider using a cookiecutter project template</a></li>
<li><a class="reference internal" href="#be-careful-on-your-selection-of-packages-addons" id="toc-entry-38">Be careful on your selection of packages/addons</a></li>
<li><a class="reference internal" href="#don-t-be-afraid-to-use-threadlocals" id="toc-entry-39">Don’t be afraid to use threadlocals</a></li>
<li><a class="reference internal" href="#the-async-tasks-situation" id="toc-entry-40">The async tasks situation</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>In this article I’d like to present a list of guidelines I follow when I develop
Django projects, especially projects that are destined to be used in a production
environment for many years. I am using django for more than 10 years as my day to
day work tool to develop applications for the public sector organization I work for.</p>
<p>My organization has got a number of different Django projects that cover its needs, with
some of them running successfully for a lot of years, since Django 1.4. I have
been involved in the development of all of them, and I have learned a lot in the process.
I understand that some of these may be controversial but they have served me well over these years.</p>
</div>
<div class="section" id="model-design-guidelines">
<h2><a class="toc-backref" href="#toc-entry-2">Model design guidelines</a></h2>
<div class="section" id="avoid-using-choices">
<h3><a class="toc-backref" href="#toc-entry-3">Avoid using choices</a></h3>
<p>Django has the convenient feature of allowing you to define <a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/models/fields/#choices">choices for a field</a> by defining
a tuple of key-value pairs the field can take. So you’ll define a field like
<tt class="docutils literal">choice_field = models.CharField(choices=<span class="caps">CHOICES</span>)</tt> with <tt class="docutils literal"><span class="caps">CHOICES</span></tt> being a tuple like</p>
<div class="highlight"><pre><span></span><span class="n">CHOICES</span> <span class="o">=</span> <span class="p">(</span>
<span class="p">(</span><span class="s1">'CHOICE1'</span><span class="p">,</span> <span class="s1">'Choice 1 description'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'CHOICE2'</span><span class="p">,</span> <span class="s1">'Choice 2 description'</span><span class="p">),</span>
<span class="p">(</span><span class="s1">'CHOICE3'</span><span class="p">,</span> <span class="s1">'Choice 3 description'</span><span class="p">),</span>
<span class="p">)</span>
</pre></div>
<p>and your database will contain <tt class="docutils literal"><span class="caps">CHOICE1</span></tt>, <tt class="docutils literal"><span class="caps">CHOICE2</span></tt> or <tt class="docutils literal"><span class="caps">CHOICE3</span></tt> as values for the field while your users will see
the corresponding description.</p>
<p>This is a great feature for prototyping however I
suggest to use it only on toy-prototyping-<span class="caps">MVP</span> projects and use normal relations in production projects instead. So the choice field
would be a Foreign Key and the choices would be tuples on the referenced table. The reasons for this are:</p>
<ul class="simple">
<li>The integrity of the choices is only on the application level. So people can go to the database and change a choice field with a random value.</li>
<li>More general, the database design is not normalized; saving <tt class="docutils literal"><span class="caps">CHOICE1</span></tt> for every row is not ideal.</li>
<li>Your users may want to edit the choices (add new ones) or change their descriptions. This is easy with a foreign key through the django-admin but needs a code change with choices.</li>
<li>It is almost sure that you will need to add “properties” to your choices. No matter what your current requirements are, they are going to change. For example, you may want to make a choice “obsolete” so it can’t be picked by users. This is trivial when you use a foreign key but not very easy when you use choices.</li>
<li>The values of the choices is saved only inside your app. The database has only the <tt class="docutils literal">'<span class="caps">CHOICE1</span>', '<span class="caps">CHOICE2</span>'</tt> etc values, so you’ll need to re-use the descriptions when your app is not used. For example, you may have reports that are generated directly from database queries so you’ll need to add the description of each key to your query using something like <tt class="docutils literal"><span class="caps">CASE</span></tt>.</li>
<li>It easier to use the <span class="caps">ORM</span> to annotate your queries when you use relations instead of the choices.</li>
</ul>
<p>The disadvantage of relations is of course that you’ll need to follow the relation to display the values. So you must be
careful to use <tt class="docutils literal">select_related</tt> to avoid the n+1 queries problem.</p>
<p>So, in short, I suggest to use choices only for quick prototyping and covert them to normal relations in production projects.
If you already are using choices in your project but want to convert them to normal relations, you can use take a look
at my <a class="reference external" href="https://spapas.github.io/2015/09/01/django-rq-redux/">Django choices to ForeignKey article</a>.</p>
</div>
<div class="section" id="always-use-surrogate-keys">
<h3><a class="toc-backref" href="#toc-entry-4">Always use surrogate keys</a></h3>
<p>A <a class="reference external" href="https://en.wikipedia.org/wiki/Surrogate_key">surrogate key</a> is a unique identifier for a database tuple which is used as the primary key. By default Django always adds a
surrogate key to your models. However, some people may be tempted to use a natural key as the primary key. Although this is possible
and supported in Django, I’d recommend to stick to integer surrogate keys. Why ?</p>
<ul class="simple">
<li>Django is more or less build upon having integer primary keys. Although non-integer primary keys are supported in core Django, you can’t be assured that this will be supported by the various addons/packages that you’ll want to use.</li>
<li>I understand that your requirements say that “the field X will be unique and should be used to identify the row”. This is never true; this can easily be changed in the future and your primary key may stop being unique! It has happened to me and the solution was <em>not</em> something I’d like to discuss here. If there’s a field in the row that is guaranteed to be unique you can make it unique in the database level by adding <tt class="docutils literal"><span class="pre">unique==True</span></tt>; there’s no reason to also make it a primary key.</li>
<li>Relying on all your models having an <tt class="docutils literal">id</tt> integer primary key makes it easier to write your code and other people reading it.</li>
<li>Using an auto-increment primary key is the fastest way to insert a new row in the database (when compared to, for example using a random uuid)</li>
</ul>
<p>An even worse idea is to use composite keys (i.e define a primary key using two fields of your tuple). There’s actually
a <a class="reference external" href="https://code.djangoproject.com/ticket/373">17-year an open issue</a> about that in Django! This should be enough for you to understand that you shouldn’t touch that
with a 10-foot pole. Even if it is implemented somehow in core django, you’ll have something that can’t be used with all
other packages that rely on primary key being a single field.</p>
<p>Now, I understand that some public facing projects may not want to expose the auto-increment primary key since that discloses information
about the number of rows in the database, the number of rows that are added between a user’s tuples etc. In this case, you may want to
either add a unique uuid field, or a slug field, or even better use a library like hashid to convert your integer ids to hashes. I haven’t
used uuids myself, but for a slug field I had used the <a class="reference external" href="https://github.com/justinmayer/django-autoslug">django-autoslug</a> library and was very happy with it.</p>
<p>Concerning hashids, I’d recommend reading my <a class="reference external" href="https://spapas.github.io/2021/01/07/django-hashids/">Django hashids article</a>.</p>
</div>
<div class="section" id="always-use-a-through-model-on-your-m2m-relations">
<h3><a class="toc-backref" href="#toc-entry-5">Always use a through model on your m2m relations</a></h3>
<p>To add a many-to-many relation in Django, you’ll usually do something like <tt class="docutils literal">toppings = models.ManyToManyField(Topping)</tt>
(for a pizza). This is a very convenient but, similar to the choices I mentioned above, it is not a good practice for
production projects.
This is because your requirements <em>will</em> change and you’ll need to add properties to your m2m relation. Although this <em>is possible</em>,
it definitely is not pretty so it’s better to be safe than sorry.</p>
<p>When you use the <tt class="docutils literal">ManyToManyField</tt> field, django will generate an intermediate table with a name similar to app_model1_model2, i.e
for pizza and topping it will be <cite>pizzas_pizza_topping</cite>. This table will have 3 fields - the primary key, a foreign key to the pizza
table and a foreign key to the topping table. This is the default behavior of Django and it is not configurable.</p>
<p>What happens if you want to add a relation to the pizzas_pizza_topping table? For example, the amount of each topping on a pizza. Or
the fact that some pizzas used to have that topping but it has been replaced now by another one? This is not possible unless you use
a through table. As I said it is possible to fix that but it’s not something that you’ll want to do.</p>
<p>So, my recommendation is to <em>always</em> add a through table when you use a m2m relation. Create a model that will represent the relation
and has foreign keys to both tables along with any extra attributes the relation may have.</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">PizzaTopping</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">pizza</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Pizza</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
<span class="n">topping</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Topping</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
<span class="n">amount</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">()</span>
</pre></div>
<p>and define your pizza toppings relation like <tt class="docutils literal">toppings = models.ManyToManyField(Topping, through=PizzaTopping)</tt>.</p>
<p>If the relation doesn’t have no extra attributes don’t worry: You’ll be prepared when these are requested!</p>
<p>A bonus to that is that now you can query directly the PizzaTopping model and you can also add an admin interface for it.</p>
<p>There are <em>no</em> disadvantages to adding the through model (except the 1 minute needed to add the through model minor) since
Django will anyway create the intermediate table to represent the relation so you’ll still need to use <tt class="docutils literal">prefetch_related</tt>
to get the toppings of a pizza and avoid the n+1 query problem.</p>
</div>
<div class="section" id="use-a-custom-user-model">
<h3><a class="toc-backref" href="#toc-entry-6">Use a custom user model</a></h3>
<p>Using a custom user model when starting a new project is already <a class="reference external" href="https://docs.djangoproject.com/en/stable/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project">advised in the Django documentation</a>. This will make it
easier to add custom fields to your user model and have better control over it. Also, although you may be able to add
a <tt class="docutils literal">Profile</tt> model with an one to one relation with the default <tt class="docutils literal">django.auth.User</tt> model you’ll still need to use
a join to retrieve the profile for each user (something that won’t be necessary when the extra fields are on your custom user model).</p>
<p>Another very important reason to use a custom user model is that you’ll be able to easily add custom methods to your user model.
For example, there’s the <tt class="docutils literal">get_full_name</tt> method in builtin-Django that returns the first_name plus the last_name, with a space in between
so you’re able to call it like <tt class="docutils literal">{{ user.get_full_name }}</tt> in your templates. If you don’t have a custom user model, you’ll need to
add template tags for similar functionality; see the discussion about not adding template tags when you can use a method.</p>
<p>There’s no real disadvantage to using a custom user model except the 5 minute it is needed to set it up. I actually recommend
create a <tt class="docutils literal">users</tt> app that you’re going to use to keep user related information (see
the <a class="reference external" href="https://github.com/spapas/cookiecutter-django-starter/tree/master/%7B%7Bcookiecutter.project_name%7D%7D/%7B%7Bcookiecutter.project_name%7D%7D/users">users app on my cookiecutter project</a>).</p>
</div>
</div>
<div class="section" id="views-guidelines">
<h2><a class="toc-backref" href="#toc-entry-7">Views guidelines</a></h2>
<div class="section" id="use-class-based-views">
<h3><a class="toc-backref" href="#toc-entry-8">Use class based views</a></h3>
<p>I recommend to prefer using class-based views instead of function-based views. This is because class-based views are easier to
reuse and extend. I’ve written an extensive <a class="reference external" href="https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/">comprehensive Django <span class="caps">CBV</span> guide</a> that you can read to
learn everything about class based views!</p>
<p>Also, by properly using CBVs people reading your code will use sensible defaults and you be able to understand what you
or others are doing much easier. Consider this</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">FooDetailView</span><span class="p">(</span><span class="n">DetailView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Foo</span>
</pre></div>
<p>vs</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">object_detail_view</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">pk</span><span class="p">):</span>
<span class="n">foo</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">Foo</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="n">pk</span><span class="p">)</span>
<span class="k">return</span> <span class="n">render</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="s1">'foo/foo_detail.html'</span><span class="p">,</span> <span class="p">{</span><span class="s1">'foo'</span><span class="p">:</span> <span class="n">foo</span><span class="p">})</span>
</pre></div>
<p>These are more or less the same. However in the function-based view you need to actually write some logic for retrieving
the Foo instance and then define the name of the template and the context object. Also notice that you use the <tt class="docutils literal">get_object_or_404</tt>
function that helps you being <span class="caps">DRY</span>. Whereas in the class based view this is
already done for you using well-known defaults. So, for example you’ll know which is the name of the template without the
need to check the code.</p>
</div>
<div class="section" id="view-method-overriding-guidelines">
<h3><a class="toc-backref" href="#toc-entry-9">View method overriding guidelines</a></h3>
<p>It is important to know which method you need to override to add functionality to your class based views. You can
use the excellent <a class="reference external" href="https://ccbv.co.uk/"><span class="caps">CBV</span> Inspector</a> app to understand how each <span class="caps">CBV</span> is working. Also, I’ve got
many examples in my <a class="reference external" href="https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/">comprehensive Django <span class="caps">CBV</span> guide</a>.</p>
<p>Some quick guidelines follow:</p>
<ul class="simple">
<li>For <em>all</em> methods do not forget to call the parent’s method by <tt class="docutils literal">super()</tt>.</li>
<li>Override <tt class="docutils literal">dispatch(self, request, *args, **kwargs)</tt> if you want to add functionality that is executed before any other method. For example to add permission checks or add some attribute (<tt class="docutils literal">self.foo</tt>) to your view instance. This method will <em>always</em> run on both <span class="caps">HTTP</span> <span class="caps">GET</span>/<span class="caps">POST</span> or whatever. Must return a Response object (i.e <tt class="docutils literal">HttpResponse</tt>, <tt class="docutils literal">HttpResponseRedirect</tt>, <tt class="docutils literal">HttpResponseForbidden</tt> etc)</li>
<li>You should rarely need to override the <tt class="docutils literal">get</tt> or <tt class="docutils literal">post</tt> methods of your CBVs since they are called directly after <tt class="docutils literal">dispatch</tt> so any code should be there.</li>
<li>To add extra data in your context (template) override <tt class="docutils literal">get_context_data(self, **kwargs)</tt>. This should return a dictionary with the context data.</li>
<li>To pass extra data to your form (i.e the current request) override <tt class="docutils literal">get_form_kwargs(self)</tt>. This data will be passed on the <tt class="docutils literal">__init__</tt> of your form, you need to <em>remove it</em> by using something like <tt class="docutils literal">self.request = <span class="pre">kwargs.pop('request')</span></tt> before calling <tt class="docutils literal"><span class="pre">super().__init(*args,</span> **kwargs)</tt></li>
<li>To override the initial data of your form override <tt class="docutils literal">get_form_initial(self)</tt>. This should return a dictionary with the initial data.</li>
<li>You can override <tt class="docutils literal">get_form(self, form_class=None)</tt> to use a configurable form instance or <tt class="docutils literal">get_form_class(self)</tt> to use a configurable form class. The form instance will be generated by <tt class="docutils literal"><span class="pre">self.get_form_class()(**self.get_form_kwargs())</span></tt> (notice that the kwargs will contain an <tt class="docutils literal">initial=self.get_form_initial()</tt> value)</li>
<li>To do stuff after a valid form is submitted you’ll override <tt class="docutils literal">form_valid(self, form)</tt>. This should return an <tt class="docutils literal">HttpResponse</tt> object and more specifically an <tt class="docutils literal">HttpResponseRedirect</tt> to avoid double form submission. This is the place where you can also add flash messages to your responses.</li>
<li>You can also override <tt class="docutils literal">form_invalid(self, form)</tt> but this is rarely useful. This should return a normal response (not a redirect)</li>
<li>Override <tt class="docutils literal">get_success_url(self)</tt> if you only want to set where you’ll be redirected after a valid form submission (notice this is used by <tt class="docutils literal">form_valid</tt>)</li>
<li>You can use a different template based on some condition by overriding <tt class="docutils literal">get_template_names(self)</tt>. This is useful to return a partial response on an ajax request (for example the same detail view will return a full html view of an object when visited normally but will return a small partial html with the object’s info when called through an ajax call)</li>
<li>For views that return 1 or multiple objects (<tt class="docutils literal">DetailView, ListView, UpdateView</tt> etc) you almost always need to override the <tt class="docutils literal">get_queryset(self)</tt> method, <em>not</em> the <tt class="docutils literal">get_object</tt>. I’ll talk about that a little more later.</li>
<li>The <tt class="docutils literal">get_object(self, queryset=None)</tt> method will use the queryset returned by <tt class="docutils literal">get_queryset</tt> to get the object based on its pk, slug etc. I’ve observed that this rarely needs to be overridden since most of the time overriding <tt class="docutils literal">get_queryset</tt> will suffice. One possible use case for overriding <tt class="docutils literal">get_object</tt> is for views that don’t care at all about the queryset; for example you may implement a <tt class="docutils literal">/profile</tt> detail view that will pick the current user and display some stuff. This can be implemented by a <tt class="docutils literal">get_object</tt> similar to <tt class="docutils literal">return self.request.user</tt>.</li>
</ul>
</div>
</div>
<div class="section" id="querying-guidelines">
<h2><a class="toc-backref" href="#toc-entry-10">Querying guidelines</a></h2>
<div class="section" id="guidelines-for-the-n-1-problem">
<h3><a class="toc-backref" href="#toc-entry-11">Guidelines for the n+1 problem</a></h3>
<p>The most common Django newbie mistake is not considering the n+1 problem when writing your queries.</p>
<p>Because Django automatically follows relations it is very easy to write code that will result in the n+1 queries
problem. A simple example is having something like</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Category</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">255</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Product</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">255</span><span class="p">)</span>
<span class="n">category</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Category</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">"</span><span class="si">{0}</span><span class="s2"> (</span><span class="si">{1}</span><span class="s2">)"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">category</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
</pre></div>
<p>and doing something like:</p>
<div class="highlight"><pre><span></span><span class="k">for</span> <span class="n">product</span> <span class="ow">in</span> <span class="n">Product</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">():</span>
<span class="nb">print</span><span class="p">(</span><span class="n">product</span><span class="p">)</span>
</pre></div>
<p>or even having <tt class="docutils literal">products = Product.objects.all()</tt> as a context variabile in your template:</p>
<div class="highlight"><pre><span></span>{% for product in products %}
{{ product }}
{% endfor %}
</pre></div>
<p>If you’ve got 100 products, the above will run 101 queries to the database: The first one
will get all the products and the other 100 will return each product’s category one by one!
Consider what may happen if you had thousands of products…</p>
<p>To avoid this problem you should add the <tt class="docutils literal">select_related</tt>, so <tt class="docutils literal">products = <span class="pre">Product.objects.all().select_related('category')</span></tt>.
This will do an <span class="caps">SQL</span> <span class="caps">JOIN</span> between the products and categories table so each product will include its category instance. Now, when
you’ve got a many to many relation the situation is a little different. Let’s suppose you’ve got a <tt class="docutils literal">tags = models.ManyToManyField(Tag)</tt>
field in your <tt class="docutils literal">Product</tt> model. If you wanted to do something like <tt class="docutils literal">{{ <span class="pre">product.tags.all|join:",</span> " }}</tt> to display the product tags you’d
also get a n+1 situation because Django will do a query for each product to get its tags. To avoid this you cannot use
<tt class="docutils literal">select_related</tt> but should use the <tt class="docutils literal">prefetch_related</tt>
method so <tt class="docutils literal">products = <span class="pre">Product.objects.all().prefetch_related('tags')</span></tt>. This will result in 2 queries, one for
products and one for their tags, the joining will be done in python.</p>
<p>One final comment about the <tt class="docutils literal">prefetch_related</tt> is that you must be very careful to use what you prefetch. Let’s suppose that we
had prefeched the tags but we wanted to display them ordered by name: Doing this <tt class="docutils literal">", <span class="pre">".join([tag</span> for tag in <span class="pre">product.tags.all().order_by('name')])</span></tt>
will <em>not</em> use the prefetched tags but will do a new query for each product to get its tags resulting in the n+1 problem! Django has
<tt class="docutils literal">tag.objects.all()</tt> for each product, <em>not</em> <tt class="docutils literal"><span class="pre">tag.objects.all().order_by('name')</span></tt>. To fix that you need to use <cite>Prefetch</cite> like this:</p>
<div class="highlight"><pre><span></span>Product.objects.prefetch_related(Prefetch('tags', queryset=Tag.objects.order_by('name')))
</pre></div>
<p>The same is true if you wanted to filter your tags etc.</p>
<p>Now, one thing to understand is that this behavior of Django is intentional. Instead of automatically following the relationships,
Django could throw an exception when you tried to follow a relationship that wasn’t in a <tt class="docutils literal">select_related</tt>
(this how it works in other frameworks). The disadvantage
of this is that it would make Django <em>more difficult</em> to use for new users. Also, there are cases that the n+1 problem isn’t
really a big deal, for example you may have a DetailView fetching a single object so in this case the n+1 problem will be 1+1
and wouldn’t really matter. So, at least for Django, it’s a case of premature optimization: Write your queries as good as you
can (but keep in mind the n+1 problem), if you miss some cases that actually make your views slow, you can easily optimize them later.</p>
</div>
<div class="section" id="re-use-your-queries">
<h3><a class="toc-backref" href="#toc-entry-12">Re-use your queries</a></h3>
<p>You should re-use your queries to avoid re-writing them. You can either put them inside your models
(as instance methods) or in a mixin for the queries of your views or even add a new manager for
your model. Let’s see some examples:</p>
<p>Let’s suppose I wanted to get the tags of my product: I’d add this method to my <tt class="docutils literal">Product</tt> model:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Product</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">get_tags</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">tags</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'name'</span><span class="p">)</span>
</pre></div>
<p>Please notice that if you haven’t used a proper prefetch this will result in the n+1 queries problem. See the discussion above
for more info. To get the products with their tags I could add a new manager like:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ProductWithTagManager</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Manager</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span><span class="o">.</span><span class="n">prefetch_related</span><span class="p">(</span><span class="n">Prefetch</span><span class="p">(</span><span class="s1">'tags'</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="n">Tag</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'name'</span><span class="p">)))</span>
<span class="k">class</span> <span class="nc">Product</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># ...</span>
<span class="n">products_with_tags</span> <span class="o">=</span> <span class="n">ProductWithTagManager</span><span class="p">()</span>
</pre></div>
<p>Now I could do <tt class="docutils literal">[p.get_tags() for p in <span class="pre">Product.products_with_tags.all()]</span></tt> and not have a n+1 problem.</p>
<p>Actually, if I knew that I would <em>always</em> wanted to display the product’s tags I could override the default manager like</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Product</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># ...</span>
<span class="n">objects</span> <span class="o">=</span> <span class="n">ProductWithTagManager</span><span class="p">()</span>
</pre></div>
<p>However I would not recommend that since having a consistent behavior when you run Model.objects is very important. If you
are to modify the default manager then you’ll need to always remember what your default manager does. This is very problematic
in old projects and when you want to quickly query your database from a shell. Also, even more problematic is if you
override your default manager to <em>filter</em> (hide) objects. Don’t do that or you’ll definitely regret it.</p>
<p>The other query re-use option is through a mixin that would override the <tt class="docutils literal">get_queryset</tt> of your models.
Let’s suppose that each user can only see his products: I could add a mixin like:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ProductPermissionMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">created_by</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">)</span>
</pre></div>
<p>Then I could inherit my <tt class="docutils literal">ListView, DetailView, UpdateView</tt> and <tt class="docutils literal">DeleteView</tt>
i.e <tt class="docutils literal">ProductListView(ProductPermissionMixin, ListView)</tt> from that mixin and I’d have a consistent behavior on
which products each user can view. More on this can be found on my
<a class="reference external" href="https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/">comprehensive Django <span class="caps">CBV</span> guide</a>.</p>
</div>
</div>
<div class="section" id="forms-guidelines">
<h2><a class="toc-backref" href="#toc-entry-13">Forms guidelines</a></h2>
<div class="section" id="always-use-django-forms">
<h3><a class="toc-backref" href="#toc-entry-14">Always use django-forms</a></h3>
<p>This is a no-brainer: The django-forms offers some great class-based functionality for your forms. I’ve
seen people creating html forms “by hand” and missing all this. Don’t be that guy; use django-forms!</p>
<p>I understand that sometimes the requirements of your forms may be difficult to be implemented with
a django form and you prefer to use a custom form. This may seem fine at first but in the long run
you’re gonna need (and probably re-implement) most of the django-forms capabilities.</p>
</div>
<div class="section" id="overriding-form-methods-guidelines">
<h3><a class="toc-backref" href="#toc-entry-15">Overriding Form methods guidelines</a></h3>
<p>Your <tt class="docutils literal">CustomForm</tt> inherits from a Django <tt class="docutils literal">Form</tt> so you can override some of its methods. Which ones
should you override?</p>
<ul class="simple">
<li>The most usual method for overriding is <tt class="docutils literal">clean(self)</tt>. This is used to add your own server-side checks to the form. I’ll talk a bit more about overriding clean later.</li>
<li>The second most usual to override is <tt class="docutils literal">__init__(self, *args, **kwargs)</tt>. You should override it to “pop”
any extra kwargs from the <tt class="docutils literal">kwargs</tt> dict <em>before</em> calling <tt class="docutils literal"><span class="pre">super().__init__(*args,</span> **kwargs)</tt>. See the view method overriding guidelines for more info. Also you’ll use it to change.</li>
<li>I usually <em>avoid</em> overriding the form’s <tt class="docutils literal">save()</tt> method. The <tt class="docutils literal">save()</tt> is almost always called from the view’s <tt class="docutils literal">form_valid</tt> method so I prefer to do any extra stuff from the view. This is mainly a personal preference in order to avoid having to hop between the form and view modules; by knowing that the form’s save is always the default the behavior will be consistent. This is personal preference though.</li>
</ul>
<p>There shouldn’t be a need to override any other method of a <tt class="docutils literal">Form</tt> or <tt class="docutils literal">ModelForm</tt>. However please notice that you can easily
use mixins to add extra functionality to your forms. For example, if you had a particular check that would be called from <em>many</em> forms,
you could add a</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">CustomFormMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">clean</span><span class="p">()</span> <span class="c1"># Not really needed here but I recommend to add it to keep the inheritance chain</span>
<span class="c1"># The common checks that does the mixin</span>
<span class="k">class</span> <span class="nc">CustomForm</span><span class="p">(</span><span class="n">CustomFormMixin</span><span class="p">,</span> <span class="n">Form</span><span class="p">):</span>
<span class="c1"># Other stuff</span>
<span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">clean</span><span class="p">()</span> <span class="c1"># This will run the mixin's clean</span>
<span class="c1"># Any checks that only this form needs to do</span>
</pre></div>
</div>
<div class="section" id="proper-cleaning">
<h3><a class="toc-backref" href="#toc-entry-16">Proper cleaning</a></h3>
<p>When you override the <tt class="docutils literal">clean(self)</tt> method of a <tt class="docutils literal">Form</tt> you should always use the <tt class="docutils literal">self.cleaned_data</tt> to check the
data of the form. The common way to mark errors is to use the <tt class="docutils literal">self.add_error</tt> method, for example, if you have a
<tt class="docutils literal">date_from</tt> and <tt class="docutils literal">date_to</tt> and date_from is after the <tt class="docutils literal">date_to</tt> you can do your clean something like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">date_from</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">cleaned_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"date_from"</span><span class="p">)</span>
<span class="n">date_to</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">cleaned_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"date_to"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">date_from</span> <span class="ow">and</span> <span class="n">date_to</span> <span class="ow">and</span> <span class="n">date_from</span> <span class="o">></span> <span class="n">date_to</span><span class="p">:</span>
<span class="n">error_str</span> <span class="o">=</span> <span class="s2">"Date from cannot be after date to"</span>
<span class="bp">self</span><span class="o">.</span><span class="n">add_error</span><span class="p">(</span><span class="s2">"date_from"</span><span class="p">,</span> <span class="n">error_str</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">add_error</span><span class="p">(</span><span class="s2">"date_from"</span><span class="p">,</span> <span class="n">error_str</span><span class="p">)</span>
</pre></div>
<p>Please notice above that I am checking that both <tt class="docutils literal">date_from</tt> and <tt class="docutils literal">date_to</tt> are not null (or else it will try to compare
null dates and will throw). Then I am adding the same error message to both fields. Django will see that the form has errors
and run <tt class="docutils literal">form_invalid</tt> on the view and re-display the form with the errors.</p>
<p>Beyond the <tt class="docutils literal">self.add_error</tt> method that adds the error to the field there’s a possibility to add an error to the “whole”
form using:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.core.exceptions</span> <span class="kn">import</span> <span class="n">ValidationError</span>
<span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="n">form_has_error</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ValidationError</span><span class="p">(</span><span class="sa">u</span><span class="s2">"The form has an error!"</span><span class="p">)</span>
</pre></div>
<p>This kind of error won’t be correlated with a field. You can use this approach when an error is correlated to multiple fields
instead of adding the same error to multiple fields.</p>
<p>You must be very careful because if you are using a non-standard
form layout method (i.e you enumerate the fields) you also need to display the <tt class="docutils literal">{{ form.errors }}</tt> in your template or else
you’ll get a rejected form without any errors! This is a very common mistake.</p>
<p>Another thing to notice is that when your clean method raises it will display only the first such error. So if you’ve got multiple
checks like:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="n">form_has_error</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ValidationError</span><span class="p">(</span><span class="sa">u</span><span class="s2">"The form has an error!"</span><span class="p">)</span>
<span class="k">if</span> <span class="n">form_has_another_error</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ValidationError</span><span class="p">(</span><span class="sa">u</span><span class="s2">"The form has another error!"</span><span class="p">)</span>
</pre></div>
<p>and your form has <em>both</em> errors only the 1st one will be displayed to the user. Then after he fixes it he’ll also see the 2nd one. When
you use <tt class="docutils literal">self.add_error</tt> the user will get both at the same time.</p>
</div>
<div class="section" id="overriding-the-init">
<h3><a class="toc-backref" href="#toc-entry-17">Overriding the __init__</a></h3>
<p>You can override the <tt class="docutils literal">__init__</tt> method of your forms for three main reasons:</p>
<p>1. Override some field attributes on a ModelForm. A Django ModelForm will automatically create a field for each model field.
Some times you may want to override some of the attributes of the field. For example, you may want to change the label of the field
or make a field required. To do that, you can do something like:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">fields</span><span class="p">[</span><span class="s2">"my_field"</span><span class="p">]</span><span class="o">.</span><span class="n">label</span> <span class="o">=</span> <span class="s2">"My custom label"</span> <span class="c1"># Change the label</span>
<span class="bp">self</span><span class="o">.</span><span class="n">fields</span><span class="p">[</span><span class="s2">"my_field"</span><span class="p">]</span><span class="o">.</span><span class="n">help_text</span> <span class="o">=</span> <span class="s2">"My custom label"</span> <span class="c1"># Change the help text</span>
<span class="bp">self</span><span class="o">.</span><span class="n">fields</span><span class="p">[</span><span class="s2">"my_field"</span><span class="p">]</span><span class="o">.</span><span class="n">required</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># change the required attribute</span>
<span class="bp">self</span><span class="o">.</span><span class="n">fields</span><span class="p">[</span><span class="s2">"my_field"</span><span class="p">]</span><span class="o">.</span><span class="n">queryset</span> <span class="o">=</span> <span class="n">Model</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">is_active</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span> <span class="c1"># Only allow specific objects for the forein key</span>
</pre></div>
<p>Please notice that we need to use <tt class="docutils literal"><span class="pre">self.fields["my_field"]</span></tt> <em>after</em> we call <tt class="docutils literal"><span class="pre">super().__init__(*args,</span> **kwargs)</tt>.</p>
<p>2. Retrieve parameters (usually the request or user) from the view. A view (either a function-based or a <span class="caps">CBV</span> through <tt class="docutils literal">get_form_kwargs</tt>)
can pass parameters to the form’s constructor. You need to override <tt class="docutils literal">__init__</tt> to handle these parameters:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">"request"</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>Please notice that we must pop the <tt class="docutils literal">request</tt> from the <tt class="docutils literal">kwargs</tt> dict before calling <tt class="docutils literal"><span class="pre">super().__init__</span></tt> or else
we’ll get an exception since the <tt class="docutils literal">Form.__init__</tt> method accepts only specific kwargs.</p>
<p>3. Add functionality related to the current user/request. For example, you may want to add a field that is only editable if
the user is superuser:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">"request"</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_superuser</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">fields</span><span class="p">[</span><span class="s2">"my_field"</span><span class="p">]</span><span class="o">.</span><span class="n">widget</span><span class="o">.</span><span class="n">attrs</span><span class="p">[</span><span class="s1">'readonly'</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
</pre></div>
<p>or you may want to allow some custom validation logic only for non - superusers:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_superuser</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'my_field'</span><span class="p">]:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">add_error</span><span class="p">(</span><span class="s2">"my_field"</span><span class="p">,</span> <span class="s2">"Please field this field"</span><span class="p">)</span>
</pre></div>
</div>
<div class="section" id="laying-out-forms">
<h3><a class="toc-backref" href="#toc-entry-18">Laying out forms</a></h3>
<p>To lay out the forms I recommend using a library like <a class="reference external" href="https://github.com/django-crispy-forms/django-crispy-forms">django-crispy-forms</a>. This integrates your forms properly with your
front-end engine and helps you have proper styling. I’ve got some more info on
<a class="reference external" href="https://spapas.github.io/2020/03/18/django-crispy-form-quick-easy-layout/">form layout post</a>.</p>
<p>Please notice that the <a class="reference external" href="https://github.com/django-crispy-forms/django-crispy-forms">django-crispy-forms</a> supports specific front-end frameworks like bootstrap or tailwind (see its docs
for all available options). If you’re using a non-supported front-end framework you can
<a class="reference external" href="https://django-crispy-forms.readthedocs.io/en/latest/template_packs.html">create a custom template pack</a>. This seems like a lot of work but I recommend to do it. Also you don’t need to implement
everything, only the functionality you’re going to need, when you need it.</p>
</div>
<div class="section" id="improve-the-formset-functionality">
<h3><a class="toc-backref" href="#toc-entry-19">Improve the formset functionality</a></h3>
<p>Beyond simple forms, Django allows you to use a functionality it calls <a class="reference external" href="https://docs.djangoproject.com/en/4.1/topics/forms/formsets/">formsets</a>. A formset is a collection of forms that
can be used to edit multiple objects at the same time. This is usually used in combination with inlines which are a
way to edit models on the same page as a parent model.
For example you may have something like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Pizza</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="n">toppings</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ManyToManyField</span><span class="p">(</span><span class="s1">'Topping'</span><span class="p">,</span> <span class="n">through</span><span class="o">=</span><span class="s1">'PizzaTopping'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Topping</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">PizzaTopping</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">amount</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">PositiveIntegerField</span><span class="p">()</span>
<span class="n">pizza</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'Pizza'</span><span class="p">)</span>
<span class="n">topping</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'Topping'</span><span class="p">)</span>
</pre></div>
<p>Now we’d like to have a form that allows us to edit a pizza by both changing the pizza name <em>and</em> the toppings of the pizza
along with their amounts. The pizza form will be the main form and the topping/amount will be the inline form. Notice that we
won’t also create/edit the topping name, we’ll just select it from the existing toppings (we’re gonna have a completely different
view for adding/editing individual toppings).</p>
<p>First of all, to create a class based view that includes a formset we can use the <a class="reference external" href="https://github.com/AndrewIngram/django-extra-views">django-extra-views</a>
package (this isn’t supported by built-in django CBVs unless we implement the functionality ourselves). Then we’d do something like:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">extra_views</span> <span class="kn">import</span> <span class="n">CreateWithInlinesView</span><span class="p">,</span> <span class="n">InlineFormSetFactory</span>
<span class="k">class</span> <span class="nc">ToppingInline</span><span class="p">(</span><span class="n">InlineFormSetFactory</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Topping</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'topping'</span><span class="p">,</span> <span class="s1">'amount'</span><span class="p">]</span>
<span class="k">class</span> <span class="nc">CreatePizzaView</span><span class="p">(</span><span class="n">CreateWithInlinesView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Pizza</span>
<span class="n">inlines</span> <span class="o">=</span> <span class="p">[</span><span class="n">ToppingInline</span><span class="p">]</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">]</span>
</pre></div>
<p>This will create a form that will allow us to create a pizza and add toppings to it. Now, to display the formset we’d
modify our template to be similar to:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">"post"</span><span class="p">></span>
...
{{ form }}
{% for formset in inlines %}
{{ formset }}
{% endfor %}
...
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"submit"</span> <span class="na">value</span><span class="o">=</span><span class="s">"Submit"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</pre></div>
<p>This works however it will be very ugly. The default behavior is to display the <tt class="docutils literal">Pizza</tt> form and three empty <tt class="docutils literal">Topping</tt> forms.
If we want to add more toppings we’ll have to submit that form so it will be saved and then edit it. But once again we’ll get our
existing toppings and three more. I am not fond of this behavior.</p>
<p>That’s why my recommendation is to follow the instructions on my
<a class="reference external" href="https://spapas.github.io/2022/06/28/better-django-inlines/">better django inlines</a> article that allows you to sprinkle some javascript on your
template and get a much better, dynamic behavior. I.e you’ll get an “add more” button to add extra toppings without the need t
submit the form every time.</p>
</div>
</div>
<div class="section" id="template-guidelines">
<h2><a class="toc-backref" href="#toc-entry-20">Template guidelines</a></h2>
<div class="section" id="stick-to-the-built-in-django-template-backend">
<h3><a class="toc-backref" href="#toc-entry-21">Stick to the built-in Django template backend</a></h3>
<p>Django has its own built-in template engine but it also allows you to use the Jinja template engine or even
use a completely different one! The django template backend is considered “too restrictive” by some people mainly
because you can only call functions without parameters from it.</p>
<p>My opinion is to just stick to the builtin Django template. Its restriction is actually a strength, enabling you
to create re-usable custom template tags (or object methods) instead of calling business logic from the template.
Also, using a completely custom backend means that you’ll add dependencies to your project; please see my the guideline
about the selection of using external packages. Finally, don’t forget that any packages you’ll use that provide
templates would be for the Django template backend, so you’ll need to convert/re-write these templates to be used with
a different engine.</p>
<p>I would consider the Jinja engine only if I already had a bunch of Jinja templates from a different project and
wanted to quickly use them on my project.</p>
</div>
<div class="section" id="don-t-add-template-tags-when-you-can-use-a-method">
<h3><a class="toc-backref" href="#toc-entry-22">Don’t add template tags when you can use a method</a></h3>
<p>Continuing from the discussion on the previous guideline, I recommend you to add methods to your models instead of
adding template tags. For example, let’s suppose that we want to get our pizza toppings order by their name. We could
add a template tag that would do that like:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_pizza_toppings</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">pizza</span><span class="p">):</span>
<span class="k">return</span> <span class="n">pizza</span><span class="o">.</span><span class="n">toppings</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'name'</span><span class="p">)</span>
</pre></div>
<p>and use it like <tt class="docutils literal">{% get_pizza_toppings pizza as pizza_toppings %}</tt> in our template. Notice that if you don’t care about
the ordering you could instead do <tt class="docutils literal">{{ pizza.toppings.all }}</tt> but you need to use the order_by and pass a parameter so you
can’t call the method.</p>
<p>Instead of adding the template tag that I recommend adding a method to your <tt class="docutils literal">pizza</tt> model like:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_toppings</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">toppings</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'name'</span><span class="p">)</span>
</pre></div>
<p>and then call it like <tt class="docutils literal">{{ pizza.get_toppings }}</tt> in your template. This is much cleaner and easier to understand.</p>
<p>Please notice that this guideline is not a proposal towards the “fat models” approach. You can add 1 line methods to
your models that would only call the corresponding service methods if needed.</p>
</div>
<div class="section" id="re-use-templates-with-partials">
<h3><a class="toc-backref" href="#toc-entry-23">Re-use templates with partials</a></h3>
<p>When you have a part of a template that will be used in multiple places you can use partials to avoid repeating yourself.
For example, let’s suppose you like to display your pizza details. These details would be displayed in the list of
pizzas, in the cart page, in the receipt page etc. So can create an html page named <tt class="docutils literal">_pizza_details.html</tt> under a
<tt class="docutils literal">partial</tt> folder (or whatever name you want but I recommend having a way to quickly check your partials) with contents
similar to:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'pizza-details'</span><span class="p">></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>{{ pizza.name }}<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
{% if show_photo %}
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">'{{ pizza.photo.url }}'</span><span class="p">></span>
{% endif %}
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Toppings: {{ pizza.get_toppings|join:", " }}<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>and then include it in your templates like <tt class="docutils literal">{% inlude "pizzas/partials/_pizza_details.html" %}</tt> to display the info without photo or
<tt class="docutils literal">{% inlude "pizzas/partials/_pizza_details.html" with show_photo=True %}</tt> to display the photo. Also notice that you can override the
{{ pizza }} context variable so, if you want to display two pizzas in a template you’ll write something like</p>
<div class="highlight"><pre><span></span>{% inlude "partials/_pizza_details.html" with show_photo=True pizza=pizza1 %}
{% inlude "partials/_pizza_details.html" with show_photo=True pizza=pizza2 %}
</pre></div>
</div>
</div>
<div class="section" id="settings-guidelines">
<h2><a class="toc-backref" href="#toc-entry-24">Settings guidelines</a></h2>
<div class="section" id="use-a-package-instead-of-module">
<h3><a class="toc-backref" href="#toc-entry-25">Use a package instead of module</a></h3>
<p>This is a well known guideline but I’d like to mention it here. When you create a new project, Django will
create a <tt class="docutils literal">settings.py</tt> file. This file is a python module. I recommend to create a settings folder next to the
<tt class="docutils literal">settings.py</tt> and put
in it the <tt class="docutils literal">settings.py</tt> renamed as <tt class="docutils literal">base.py</tt> and an <tt class="docutils literal">__init__.py</tt> file so the <tt class="docutils literal">settings</tt> folder will be a
python package. So instead of <tt class="docutils literal">project\settings.py</tt> you’ll have <tt class="docutils literal">project\settings\base.py</tt> and <tt class="docutils literal">project\settings\__init__.py</tt>.</p>
<p>Now, you’ll add an extra module inside settings for each kind of environment you are gonna use your app on. For example, you’ll
have something like
* <tt class="docutils literal">project\settings\dev.py</tt> for your development environment
* <tt class="docutils literal">project\settings\uat.py</tt> for the <span class="caps">UAT</span> environment
* <tt class="docutils literal">project\settings\prod.py</tt> for the production environment</p>
<p>Each of these files will import the <tt class="docutils literal">base.py</tt> file and override the settings that are different from the base settings, i.e
these files will start like:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">.base</span> <span class="kn">import</span> <span class="o">*</span>
<span class="c1"># And now all options that are different from the base settings</span>
</pre></div>
<p>All these files will be put in your version control. You won’t put any secrets in these files. We’ll see how to handle
secrets later.</p>
<p>When Django starts, it will by default look for the <tt class="docutils literal">project/settings.py</tt> module. So, if you try to run <tt class="docutils literal">python manage.py</tt>
now it will complain. To fix that, you have to set the <tt class="docutils literal">DJANGO_SETTINGS_MODULE</tt> environment variable to point to
the correct settings module you wanna use. For example, in the dev env you’ll do <tt class="docutils literal">DJANGO_SETTINGS_MODULE=project.settings.dev</tt>.</p>
<p>To avoid doing that every time I recommend creating a script that will initiate the project’s virtual environment and set the
settings module. For example, in my projects I have a file named dovenv.bat (I use windows) with the following contents:</p>
<!-- code-block
call ..\venv\scripts\activate
set DJANGO_SETTINGS_MODULE=project.settings.dev -->
</div>
<div class="section" id="handle-secrets-properly">
<h3><a class="toc-backref" href="#toc-entry-26">Handle secrets properly</a></h3>
<p>You should never put secrets (i.e your database password or <span class="caps">API</span> <span class="caps">KEYS</span>) on your version control. There are two
ways that can be used to handle secrets in Django:</p>
<ul class="simple">
<li>Use a <tt class="docutils literal">settings/local.py</tt> file that contains all your secrets for the current environment and is not under version control.</li>
<li>Use environment variables.</li>
</ul>
<p>For the <tt class="docutils literal">settings/local.py</tt> solution, you’ll add the following code at the end of each one of your settings environment
modules (i.e you should put it at the end of <tt class="docutils literal">dev.py</tt>, <tt class="docutils literal">uat.py</tt>, <tt class="docutils literal">prod.py</tt> etc):</p>
<div class="highlight"><pre><span></span><span class="k">try</span><span class="p">:</span>
<span class="kn">from</span> <span class="nn">.local</span> <span class="kn">import</span> <span class="o">*</span>
<span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
<span class="k">pass</span>
</pre></div>
<p>The above will try to read a module named <tt class="docutils literal">local.py</tt> and if it exists it will import it. If it doesn’t exist it will
just ignore it. Because this file is at the end of the corresponding settings module, it will override any settings that are already
defined. The above file should be excluded from version control so you’ll add the line <tt class="docutils literal">local.py</tt> to your <tt class="docutils literal">.gitignore</tt>.</p>
<p>Notice that the same solution to store secrets can be used if you don’tt use the settings package approach but you have a <tt class="docutils literal">settings.py</tt>
module. Create a <tt class="docutils literal">settings_local.py</tt> module and import from that at the end of your settings module instead. However I strongly
recommend to use the settings package approach.</p>
<p>To catalogue my secrets, I will usually add a <tt class="docutils literal">local.py.template</tt> file that has all the settings that I need to override in my
local.py with empty values. I.e it will may be similar to:</p>
<div class="highlight"><pre><span></span><span class="n">API_TOKEN</span><span class="o">=</span><span class="s1">''</span>
<span class="n">ANOTHER_API_TOKEN</span><span class="o">=</span><span class="s1">''</span>
<span class="n">DATABASES_U</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'ENGINE'</span><span class="p">:</span> <span class="s1">'django.db.backends.postgresql_psycopg2'</span><span class="p">,</span>
<span class="s1">'NAME'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="s1">'USER'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="s1">'PASSWORD'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="s1">'HOST'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="s1">'PORT'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Then I’ll copy over <tt class="docutils literal">local.py.template</tt> to <tt class="docutils literal">local.py</tt> when I initialize my project and fill in the values.</p>
<p>Before continuing, it is important to understand the priority of the settings modules. So let’s suppose we are on
production. We should have a <tt class="docutils literal">DJANGO_SETTINGS_MODULE=project.settings.prod</tt>. The players will be <tt class="docutils literal">base.py</tt>,
<tt class="docutils literal">prod.py</tt> and <tt class="docutils literal">local.py</tt>. The priority will be</p>
<ol class="arabic simple">
<li><tt class="docutils literal">local.py</tt></li>
<li><tt class="docutils literal">prod.py</tt></li>
<li><tt class="docutils literal">base.py</tt></li>
</ol>
<p>So any settings defined in <tt class="docutils literal">prod.py</tt> will override the settings of <tt class="docutils literal">base.py</tt>. And any settings defined in <tt class="docutils literal">local.py</tt>
will override any settings defined either in <tt class="docutils literal">prod.py</tt> or <tt class="docutils literal">base.py</tt>. Please notice that I mention <em>any</em> setting, not
just secrets.</p>
<p>To use the environment variables approach, you’ll have to read the values of the secrets from your environment.
A simple way to do that is to use ths os.getenv function, for example in your <tt class="docutils literal">prod.py</tt> you may have something like:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">os</span>
<span class="n">API_TOKEN</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s1">'API_TOKEN'</span><span class="p">)</span>
</pre></div>
<p>This will set <tt class="docutils literal">API_TOKEN</tt> setting to <tt class="docutils literal">None</tt> if the <tt class="docutils literal">API_TOKEN</tt> env var is not found. You can do something like
<tt class="docutils literal"><span class="pre">os.environ["API_TOKEN"]</span></tt> instead to throw an exception. Also, there are libraries that will help you with this
like <a class="reference external" href="https://github.com/theskumar/python-dotenv">python-dotenv</a>, However I can’t really recommend them because I haven’t used them.</p>
<p>Now, which one to use? My recommendation (and what I always do) is to use the first approach (<tt class="docutils literal">local.py</tt>) <em>unless</em> you need to use
environment variables to configure your project. For example, if you are using a PaaS like Heroku, you’ll have to use
environment variables because of the way you deploy so you can’t really choose. However using the <tt class="docutils literal">local.py</tt> is much
simpler, does not have any dependencies and you can quickly understand which settings are overriden. Also you can
use it to override <em>any</em> setting by putting it in your local.py, not just secrets.</p>
</div>
</div>
<div class="section" id="static-and-media-guidelines">
<h2><a class="toc-backref" href="#toc-entry-27">Static and media guidelines</a></h2>
<div class="section" id="use-manifeststaticfilesstorage">
<h3><a class="toc-backref" href="#toc-entry-28">Use ManifestStaticFilesStorage</a></h3>
<p>Django has a <tt class="docutils literal">STATICFILES_STORAGE</tt> setting that can be used to specify the storage engine that will be used to store
the static files. By default, Django uses the <tt class="docutils literal">StaticFilesStorage</tt> engine which stores the files in the file system
under the <tt class="docutils literal">STATIC_ROOT</tt> folder and with a <tt class="docutils literal">STATIC_URL</tt> url.</p>
<p>For example if you’ve got a <tt class="docutils literal"><span class="pre">STATIC_ROOT=/static_root</span></tt> and a <tt class="docutils literal"><span class="pre">STATIC_URL=/static_url/</span></tt> and you’ve got a file named <tt class="docutils literal">styles.css</tt>
which you include with <tt class="docutils literal">{% static "styles.css" %}</tt>. When you run <tt class="docutils literal">python manage.py collectstatic</tt> the <tt class="docutils literal">styles.css</tt> will be copied
to <tt class="docutils literal">/static_root/styles.css</tt> and you’ll be able to access it with <tt class="docutils literal">/static_url/styles.css</tt>.</p>
<p>Please notice that the above should be configured in your web server (i.e nginx). Thus, you need to configure your
web server so as to publish the files under <tt class="docutils literal">/static_root</tt> on the <tt class="docutils literal">/static_url</tt> url. This should work without Django,
i.e if you have configured the web server properly you’ll be able to visit <tt class="docutils literal">example.com/static_url/styles.css</tt> even if
your Django app isn’t running. For more info see <a class="reference external" href="https://docs.djangoproject.com/en/4.1/howto/static-files/deployment/">how to deploy static files</a>.</p>
<p>Now, the problem with the <tt class="docutils literal">StaticFilesStorage</tt> is that if you change the <tt class="docutils literal">styles.css</tt> there won’t be any
way for the user’s browser to understand that the file has been changed so it will keep using the cached version.</p>
<p>This is why I recommend using the <a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#django.contrib.staticfiles.storage.ManifestStaticFilesStorage">ManifestStaticFilesStorage</a> instead. This storage will append the md5 has of each static
file when copying it so the <tt class="docutils literal">styles.css</tt> will be copied to <tt class="docutils literal">/static_root/styles.fb2be32168f5.css</tt> and the url will be
<tt class="docutils literal">/static_url/styles.fb2be32168f5.css</tt>. When the <tt class="docutils literal">styles.css</tt> is changed, its hash will also be changed so the users
are guaranteed to pick the correct file each time.</p>
</div>
<div class="section" id="organize-your-media-files">
<h3><a class="toc-backref" href="#toc-entry-29">Organize your media files</a></h3>
<p>When you upload a file to your app, Django will store it in the <tt class="docutils literal">MEDIA_ROOT</tt> folder and serve it through <tt class="docutils literal">MEDIA_URL</tt>
similar to the static files as I explained before. The problem with this approach is that you’ll end up with a lot of files
in the same folder. This is why I recommend creating a folder structure for your media files. To create this structure
you should set the <a class="reference external" href="https://docs.djangoproject.com/en/4.1/ref/models/fields/#django.db.models.FileField.upload_to">upload_to</a> attribute of <tt class="docutils literal">FileField</tt>.</p>
<p>So instead of having <tt class="docutils literal">file = models.FileField</tt> or <tt class="docutils literal">image = models.ImageField</tt> you’d do something like
<tt class="docutils literal">file = <span class="pre">models.FileField(upload_to='%Y/%m/files')</span></tt> or <tt class="docutils literal">image = <span class="pre">models.ImageField(upload_to='%Y/%m/images')</span></tt> to
upload these files to their corresponding folder organized by year/month.</p>
<p>Notice that instead of a string you can also pass a function to the <tt class="docutils literal">upload_to</tt> attribute. This function will need to
return a string that will contain the path of the uploaded file <em>including</em> the filename. For example, an upload_to
function can be similar to this:</p>
<div class="highlight"><pre><span></span>def custom_upload_path(instance, filename):
dt_str = instance.created_on.strftime("%Y/%m/%d")
fname, ext = os.path.splitext(filename)
slug_fn = slugify(anyascii.anyascii(fname))
if ext:
slug_fn += "" + ext
return "protected/{0}/{1}/{2}".format(dt_str, instance.id, slug_fn)
</pre></div>
<p>The above code will convert the filename to an ascii slug (i.e a file named <tt class="docutils literal">δοκιμή.pdf</tt> will be
converted to <tt class="docutils literal">dokime.pdf</tt>) and will store it in a folder after the created date year/month/day and id of the
object instance the file belongs to. So if for example the file <tt class="docutils literal">δοκιμή.pdf</tt> belongs to the object with id 3242
and created date 2022-09-30 will be stored on the directory <tt class="docutils literal">protected/2022/09/30/3242/dokime.pdf</tt>.</p>
<p>The above code is just an example. You can use it as a starting point and modify it to fit your needs. Having the
media files in separate folders will enable you to easily navigate the folder structure and for example back up
only a portion of the files.</p>
</div>
<div class="section" id="do-not-serve-media-through-your-application-server">
<h3><a class="toc-backref" href="#toc-entry-30">Do not serve media through your application server</a></h3>
<p>This is important. The media files of your app have to be served through your web server (i.e nginx) and <em>not</em> your
application server (i.e gunicorn). This is because the application server has a limited number of workers and if you
serve the media files through them, it will be a bottleneck for your app. Thus you need to configure your web server
to serve the media files by publishing the <tt class="docutils literal">MEDIA_ROOT</tt> folder under the <tt class="docutils literal">MEDIA_URL</tt> url similar to the static files
as described above.</p>
<p>Notice that by default Django will only serve your media files for development by using the following at the end of your
<tt class="docutils literal">urls.py</tt> file:</p>
<div class="highlight"><pre><span></span><span class="k">if</span> <span class="n">settings</span><span class="o">.</span><span class="n">DEBUG</span><span class="p">:</span>
<span class="n">urlpatterns</span> <span class="o">+=</span> <span class="n">static</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">MEDIA_URL</span><span class="p">,</span> <span class="n">document_root</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">MEDIA_ROOT</span><span class="p">)</span>
</pre></div>
<p>Under no circumstances you should use this when <tt class="docutils literal">settings.<span class="caps">DEBUG</span> = False</tt> (i.e on production).</p>
</div>
<div class="section" id="secure-your-media-properly">
<h3><a class="toc-backref" href="#toc-entry-31">Secure your media properly</a></h3>
<p>Continuing from the above, if you are not allowed to serve your media files through your application then how are
you supposed to secure them? For example you may want to allow a user to upload files to your app but you want only
that particular user to be able to download them and not anybody else. So you’ll need to check somehow that the
user that tries to download the file is the same user that uploaded it. How can you do that?</p>
<p>The answer is to use a functionality offered by most web servers called X SendFile. First of all I’d like to explain how this works:</p>
<ol class="arabic simple">
<li>A user wants to download a file with id <tt class="docutils literal">1234</tt> so he clicks the “download” button for that file</li>
<li>The browser of the user will then visit a normal django view for example <tt class="docutils literal">/download/1234</tt></li>
<li>This view will check if the user is allowed to download the file by doing any permissions checks it needs to do, all in Django code</li>
<li>If the user is not allowed to download, it will return a 403 (forbidden) or 404 (not-found) response</li>
<li>However if the user is <em>allowed</em> to download the Django view will return an http response that <em>will not</em> contain the file but will have a special header with the path of the file to download (which is the path that file 1234 is saved on)</li>
<li>When the web server (i.e nginx) receives the http response it will check if the response has the special header and if it does it will serve the response it got <em>along</em> with the file, directly from the file system without going through the application server (i.e gunicorn)</li>
</ol>
<p>The above gives us the best of both worlds: We are allowed to do any checks we want in Django and the file is served through nginx.</p>
<p>A library that implements this functionality is django-sendfile2 which is a fork of the non-maintained anymore django-sendfile.
To use it you’ll need to follow the instructions provided and depend on your web server. However, let’s see a quick example for
nginx from one production project:</p>
<div class="highlight"><pre><span></span><span class="c1"># nginx conf</span>
<span class="n">server</span> <span class="p">{</span>
<span class="c1"># other stuff</span>
<span class="n">location</span> <span class="o">/</span><span class="n">media_project</span><span class="o">/</span><span class="n">protected</span><span class="o">/</span> <span class="p">{</span>
<span class="n">internal</span><span class="p">;</span>
<span class="n">alias</span> <span class="o">/</span><span class="n">home</span><span class="o">/</span><span class="n">files</span><span class="o">/</span><span class="n">project</span><span class="o">/</span><span class="n">media</span><span class="o">/</span><span class="n">protected</span><span class="o">/</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">location</span> <span class="o">/</span><span class="n">media_project</span><span class="o">/</span> <span class="p">{</span>
<span class="n">alias</span> <span class="o">/</span><span class="n">home</span><span class="o">/</span><span class="n">files</span><span class="o">/</span><span class="n">project</span><span class="o">/</span><span class="n">media</span><span class="o">/</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>For nginx we add a new location block that will serve the files under the <tt class="docutils literal">/media_project/protected/</tt> url. The <tt class="docutils literal">internal;</tt>
directive will prevent the client from going directly to the <span class="caps">URI</span>, so visiting <tt class="docutils literal">example.com/media_project/protected/file.pdf</tt> directly
will not work. We also have a <tt class="docutils literal">/media_project/</tt> location that serves the files under /media that are not protected. Please notice that
nginx matches the most specific path first so all files under protected will be matched with the correct, internal location.</p>
<div class="highlight"><pre><span></span><span class="c1"># django settings</span>
<span class="n">MEDIA_ROOT</span> <span class="o">=</span> <span class="s2">"/home/files/project/media"</span>
<span class="n">SENDFILE_ROOT</span> <span class="o">=</span> <span class="s2">"/home/files/project/media/protected"</span>
<span class="n">MEDIA_URL</span> <span class="o">=</span> <span class="s2">"/media_project/"</span>
<span class="n">SENDFILE_URL</span> <span class="o">=</span> <span class="s2">"/media_project/protected"</span>
<span class="n">SENDFILE_BACKEND</span> <span class="o">=</span> <span class="s2">"sendfile.backends.nginx"</span>
</pre></div>
<p>Notice the difference between the <tt class="docutils literal">MEDIA_ROOT</tt> (that contains all our media files - some are not protected) and <tt class="docutils literal">SENDFILE_ROOT</tt>
and same for <tt class="docutils literal">MEDIA_URL</tt> and <tt class="docutils literal">SENDFILE_URL</tt></p>
<div class="highlight"><pre><span></span><span class="c1"># django view</span>
<span class="k">def</span> <span class="nf">get_document</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">doc_id</span><span class="p">):</span>
<span class="kn">from</span> <span class="nn">django_sendfile</span> <span class="kn">import</span> <span class="n">sendfile</span>
<span class="n">doc</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">Document</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="n">doc_id</span><span class="p">)</span>
<span class="n">rules_light</span><span class="o">.</span><span class="n">require</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">,</span> <span class="s2">"apps.app.read_docs"</span><span class="p">,</span> <span class="n">doc</span><span class="o">.</span><span class="n">app</span><span class="p">)</span>
<span class="k">return</span> <span class="n">sendfile</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">doc</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="n">attachment</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre></div>
<p>So this view first gets the <tt class="docutils literal">Document</tt> instance from its id and checks to see if the current user
can read it. Finally, it returns the <tt class="docutils literal">sendfile</tt> response that will serve the file directly from the file system passing
the <tt class="docutils literal">path</tt> of that file. This function view will have a url like <tt class="docutils literal"><span class="pre">path("get_doc/<int:doc_id>/",</span> login_required(views.get_document), <span class="pre">name="get_document",</span> ),</tt></p>
<p>A final comment is that for your <tt class="docutils literal">dev</tt> environment you probably want to use the
<tt class="docutils literal">SENDFILE_BACKEND = "django_sendfile.backends.development"</tt> (please see the settings package guideline on how to
override settings per env).</p>
</div>
<div class="section" id="handle-stale-media">
<h3><a class="toc-backref" href="#toc-entry-32">Handle stale media</a></h3>
<p>Django does never delete your media files. For example if you have an object that has a file field and the object is deleted,
the file that this file field refers to will not be deleted. The same is true if you upload a new file on that file field,
the old file will also be kept there!</p>
<p>This is very problematic in some cases, resulting to <span class="caps">GB</span> of unused files in your disk. To handle that, there are two solutions:</p>
<ul class="simple">
<li>Add a signal in your models that checks if they are deleted or a file field is updated and delete the non-used file. This is implemented by the <a class="reference external" href="https://github.com/un1t/django-cleanup">django-cleanup</a> package.</li>
<li>Use a management command that will periodically check for stale files and delete them. This is implemented by the <a class="reference external" href="https://github.com/akolpakov/django-unused-media">django-unused-media</a> package.</li>
</ul>
<p>I’ve used both packages in various projects and they work great. I’d recommend the django-cleanup on greenfield projects so as to avoid stale files from the beginning.</p>
</div>
</div>
<div class="section" id="debugging-guidelines">
<h2><a class="toc-backref" href="#toc-entry-33">Debugging guidelines</a></h2>
<div class="section" id="be-careful-when-using-django-debug-toolbar">
<h3><a class="toc-backref" href="#toc-entry-34">Be careful when using django-debug-toolbar</a></h3>
<p>The <a class="reference external" href="https://github.com/jazzband/django-debug-toolbar">django-debug-toolbar</a> is a great and very popular library that can help you debug your Django applications
and identify slow views and n+1 query problems. However I have observed that it makes your development app <em>much slower</em>.
For some views I am seeing like 10x decrease in speed i.e instead of 500 ms we’ll need more than 5 seconds to display
that view! Since Django development (at least for me) is based on a very quick feedback loop, this is a huge problem.</p>
<p>Thus, I recommend to keep it disabled when you are doing normal development and only enable it when you need it,
for example to identify n+1 query problems.</p>
</div>
<div class="section" id="use-the-werkzeug-debugger">
<h3><a class="toc-backref" href="#toc-entry-35">Use the Werkzeug debugger</a></h3>
<p>Instead of using the traditional runserver to run your app in development
I recommend installing the <a class="reference external" href="https://github.com/django-extensions/django-extensions">django-extensions</a> package so as to be able to
use the Werkzeug debugger. This will enable you to get a python prompt
whenever your code throws an exception or even to add your own breakpoints by throwing exceptions.</p>
<p>In a nutshell, you’ll something like <tt class="docutils literal"><span class="pre">aa+=1</span></tt> (<tt class="docutils literal">aa</tt> should not be an integer) somewhere in your code (in a view, or a model method etc)
and python will throw an exception. You’ll be able to get a python shell and inspect the state of your app inside that particular point,
so you can see what variables are available and their values, run code etc. This is a superpower that after you start using it you’ll
never want to go back to the traditional runserver.</p>
<p>More info on my <a class="reference external" href="https://spapas.github.io/2016/06/07/django-werkzeug-debugger/">Django Werkzeug debugger article</a>.</p>
</div>
</div>
<div class="section" id="general-guidelines">
<h2><a class="toc-backref" href="#toc-entry-36">General guidelines</a></h2>
<div class="section" id="consider-using-a-cookiecutter-project-template">
<h3><a class="toc-backref" href="#toc-entry-37">Consider using a cookiecutter project template</a></h3>
<p>If you are working on a Django shop so you need to create frequenctly new Django apps I’d recommend to
consider creating (or use an existing) <a class="reference external" href="https://github.com/cookiecutter/cookiecutter">cookiecutter</a> project template. You can use <a class="reference external" href="https://github.com/spapas/cookiecutter-django-starter">my own cookiecutter</a>
to create your projects or as an inspiration to create your own. It follows all the conventions I mention in
this post and it is very simple to use.</p>
</div>
<div class="section" id="be-careful-on-your-selection-of-packages-addons">
<h3><a class="toc-backref" href="#toc-entry-38">Be careful on your selection of packages/addons</a></h3>
<p>Django, because of its popularity, has an <a class="reference external" href="https://djangopackages.org/">abundance of packages/addons</a> that can help you do almost anything.
However, my experience has taught me that you should be very careful and do your research before adding a new
package to your project. I’ve been left many times with projects that I was not able to upgrade because they
heavily relied on functionality from an external package that was abandoned by its creator. I also have lost
many hours trying to debug a problem that was caused by a package that was not compatible with the latest version
of Django.</p>
<p>So my guidelines before using an external Django addon are:</p>
<ul class="simple">
<li>Make sure that it has been upgraded recently. There are <em>no</em> finished Django addons. Django is constantly evolving by releasing new versions and that must be true for the addons. Even if the addons are compatible with the new Django version they need to denote that in their <span class="caps">README</span> so as to know that their maintainers care.</li>
<li>Avoid using very new packages. I’ve seen many packages that are not yet mature and they are not yet ready for production. If you really need to use such a package make sure that you understand what it does and you can fix problems with the package if needed.</li>
<li>Avoid using packages that rely heavily on Javascript; this is usually better to do on your own.</li>
<li>Try to understand, at least at a high level, what the package does. If you don’t understand it, you will not be able to debug if it breaks.</li>
<li>Make sure that the package is well documented and that it has a good test coverage.</li>
<li>Don’t use very simple packages that you can easily implement yourself. Don’t be a left-pad developer.</li>
</ul>
<p>I already propose some packages in this article but I also like to point you out to my
<a class="reference external" href="https://spapas.github.io/2017/10/11/essential-django-packages/">Django essential package list</a>. This list was compiled 5 years ago and
I’m happy to still recommend <em>all</em> of these packages with the following minor changes:</p>
<ul class="simple">
<li>Nowadays I recommend using wkhtmltopdf for creating PDFs from Django instead of xhtml2pdf. Please see my <a class="reference external" href="https://spapas.github.io/2022/02/14/django-pdfs-2022/">PDFs in Django like it’s 2022</a> article for more info. Notice that there’s nothing wrong with the xhtml2pdf package, it still works great and is supported but my personal preference is to use the wwhtmltopdf.</li>
<li>The django-sendfile is no longer supported so you need to use <a class="reference external" href="https://github.com/moggers87/django-sendfile2">django-sendfile2</a> instead. This is a drop-in replacement from django-sendfile2. See the point about media securing for more info.</li>
<li><a class="reference external" href="https://github.com/django-auth-ldap/django-auth-ldap">django-auth-ldap</a> uses github now (nothing changed, it just uses github instead of bitbucket).</li>
</ul>
<p>The fact that from a list of ~30 packages only one (django-sendfile) is no longer supported
(and the fact that even for that there’s a drop-in replacement) is
a testament to the quality of the Django ecosystem (and to my choosing capabilities).</p>
<p>In addition to the packages of my list, this article already contains a bunch of packages
that I’ve used in my projects and I am happy with them so I’d also recommend them to you.</p>
</div>
<div class="section" id="don-t-be-afraid-to-use-threadlocals">
<h3><a class="toc-backref" href="#toc-entry-39">Don’t be afraid to use threadlocals</a></h3>
<p>One controversial aspect if Django is that it avoids using the threadlocals functionality. The <a class="reference external" href="https://docs.python.org/3/library/threading.html#thread-local-data">thread-local data</a> is a
way to store data that is specific to the current running thread. This, combined with the fact that each one of the
requests to your Django app <em>will be served by the same thread</em> (worker) gives you a super powerful way to store and then
access data that is specific to the current request and would be very difficult (if at all possible) to do it otherwise.</p>
<p>The usual way to work with thread locals in Django is to add a middleware that sets the current request in the thread local
data. Then you can access this data from wherever you want in your code, like a global. You can either create that middleware
yourself but I’d recommend using the <a class="reference external" href="https://github.com/jedie/django-tools/">django-tools</a> library for adding this functionality. You’ll add the
<tt class="docutils literal">'django_tools.middlewares.ThreadLocal.ThreadLocalMiddleware'</tt> to your list of middleware (at the end of the listt
unless you want to use the current user from another middleware) and then you’ll use it like this:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django_tools.middlewares</span> <span class="kn">import</span> <span class="n">ThreadLocal</span>
<span class="c1"># Get the current request object:</span>
<span class="n">request</span> <span class="o">=</span> <span class="n">ThreadLocal</span><span class="o">.</span><span class="n">get_current_request</span><span class="p">()</span>
<span class="c1"># You can get the current user directly with:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">ThreadLocal</span><span class="o">.</span><span class="n">get_current_user</span><span class="p">()</span>
</pre></div>
<p>Please notice that Django recommends avoiding this technique because it hides the request/user dependency and makes
testing more difficult. However I’d like to respectfully disagree with their rationale.</p>
<ul class="simple">
<li>First of all, please notice that this is exactly how <a class="reference external" href="https://flask.palletsprojects.com/en/2.2.x/reqcontext/">Flask works</a> when you access the current request. It stores the request in the thread locals and then you can access it from anywhere in your code.</li>
<li>Second, there are things that are very difficult (or even not possible) without using the threadlocals. I’ll give you an example in a little.</li>
<li>Third, you can be careful to use the thread locals functionality properly. After all it is a very simple concept. The fact that you are using thread locals can be integrated to your tests.</li>
</ul>
<p>One example of why thread locals are so useful is this abstract class that I use in almost all my projects and models:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">UserDateAbstractModel</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="p">)</span>
<span class="n">modified_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">created_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span>
<span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span>
<span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span>
<span class="n">related_name</span><span class="o">=</span><span class="s2">"</span><span class="si">%(class)s</span><span class="s2">_created"</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">modified_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span>
<span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span>
<span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span>
<span class="n">related_name</span><span class="o">=</span><span class="s2">"</span><span class="si">%(class)s</span><span class="s2">_modified"</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">ThreadLocal</span><span class="o">.</span><span class="n">get_current_user</span><span class="p">()</span>
<span class="k">if</span> <span class="n">user</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">pk</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">created_by</span> <span class="o">=</span> <span class="n">user</span>
<span class="bp">self</span><span class="o">.</span><span class="n">modified_by</span> <span class="o">=</span> <span class="n">user</span>
<span class="nb">super</span><span class="p">(</span><span class="n">UserDateAbstractModel</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>Models that override this abstract model will automatically set the <tt class="docutils literal">created_by</tt> and <tt class="docutils literal">modified_by</tt> fields to the current user. This works
the same no matter if I edit the object from the admin, or from a view. To use that functionality all I need to do is to inherit from that model i.e
<tt class="docutils literal">class MyModel(UserDateAbstractModel)</tt> and that’s it.</p>
<p>What would I need to do if I didn’t use the thread locals? I’d need to create a mixin from which <em>all my views</em> (that modify an object)
would inherit! This mixin would pick the current user from the request and set it up. Please consider the difference between these two approaches;
using the model based approach with the thread locals I can be assured that no matter where I modify an object, the <tt class="docutils literal">created_by</tt> and <tt class="docutils literal">modified_by</tt>
will be set properly (unless of course I modify it through the database or django shell — actually, I could make <tt class="docutils literal">save</tt> throw if
the current use hasn’t been setup so it wouldn’t be possible to modify from the shell). If I use the mixin approach, I need to make sure that
all my views inherit from that mixin and that I don’t forget to do it. Also other people that add code to my project will also need to
remember that. This is a lot more error prone and difficult to maintain.</p>
<p>The above is a <em>simple</em> example. I have seen many more cases where without the use of thread locals I’d need to replicate 3-4 classes
from an external library (this library was django-allauth for anybody interested) in order to be able to pass through the current user
to where I needed to use this. This is a lot of code duplication and a maintenance hell.</p>
<p>One final comment: I’m not recommending to do it like Flask, i.e use thread locals anywhere. For example, in your views and forms it is
easy to get the current request, there’s no need to use thread locals there. However, in places where there’s no simple path for
accessing the current user then definitely use thread locals and don’t feel bad about it!</p>
</div>
<div class="section" id="the-async-tasks-situation">
<h3><a class="toc-backref" href="#toc-entry-40">The async tasks situation</a></h3>
<p>It is very common for new projects to add support for async tasks, either using celery, or django-rq or various other
solutions. I’ve already written a
<a class="reference external" href="https://spapas.github.io/2015/01/27/async-tasks-with-django-rq/">bunch</a>.
<a class="reference external" href="https://spapas.github.io/2015/09/01/django-rq-redux/">of</a>.
<a class="reference external" href="https://spapas.github.io/2019/02/25/django-fix-async-db/">posts</a>.
about this topic.</p>
<p>First of all let’s understand <em>why</em> we need async tasks: When you serve Djagno (or any Python web app) in production, you’ll
start a number of worker processes that will be used to serve the users. These usually are normal <span class="caps">OS</span> processes. The guidelines
are to start a finite amount of such processes, equal to 2-4 times the number of the <span class="caps">CPU</span> cores of your server. So with 2 cores
you’ll have like 8 workers. This means your Django app can handle up to 8 concurrent requests at the same time. If we have a
view that takes too long to response (e.g. because it runs a slow query), we’ll have many workers
“stuck” on that view, resulting in delays for the other users since the number of workers always stays the same.</p>
<p>To resolve that issue we can use async tasks to offload the work to the “background”. I.e instead of the view waiting
for the slow query to finish, it will now add a task in a queue and return immediately. The tasks in the queue will then be run
by the async worker one after another. The other way to resolve that is to increase the number of workers, but that is not
a good idea since each worker takes a certain amount of memory and resources and we still can’t be positive that we’ll be able
to handle all traffic peaks to our slow views.</p>
<p>So, although I believe that async tasks are essential for <em>some</em> situations,
my recommendation here is to be very careful and think twice before adding support for async tasks for your project.
Because of how python works, the <em>only</em> way to have support for async tasks is to have <em>one or more extra</em> moving parts
to your project.</p>
<p>These moving parts will be always a task worker process (that would pick the async tasks from the queue
and execute them asynchronously) and probably an external process that would store your queue. Actually the queue process may be redis
if you already use it for caching or even the database but also there are projects that use a separate application for the queue
like Rabbitmq.</p>
<p>This may look like a small thing but in a production environment this means that instead of running 1 thing for
your django app (a gunicorn or uwsgi app server) you need to add at least another thing (the worker). This results tp</p>
<ul class="simple">
<li>Make sure that the worker <em>sees</em> and handles your tasks</li>
<li>Monitoring the worker (getting alerts when the worker stops, make sure it runs etc)</li>
<li>Start the worker when your server starts (i.e when your server reboots)</li>
<li>Re-start the worker when you deploy changes (this is critical and easily missed; your worker won’t pick any changes to your app when you deploy it, if you don’t re-start it it will run stale code)</li>
<li>Handle exceptions to your async tasks properly</li>
<li>Make sure you have some kind of logging and exception tracking for the worker</li>
</ul>
<p>All this adds up especially if you need to do for every new app.</p>
<p>Taking this into account, I’d recommend to think twice before adding support for async tasks in you Django app. If you really need it, then
of course, you’ll need to bite the bullet and add it. But lately my understanding is that people tend to add support for async tasks
even though they don’t really need it. Let’s see some examples:</p>
<ul class="simple">
<li><em>I’ve got a view that sends mails and it is too slow</em>. If the view opens a connection to an <span class="caps">SMTP</span> server and sends the email then probably it will be slow. However, before using the async task situation, consider using a service like sendgrid or mailgun that will send your mails for you and will be much faster.</li>
<li><em>I need to run slow queries</em>. Well… no you don’t. Your queries should <em>not</em> be slow. You should try to optimize your queries to run faster. If you have done all optimizations and still your queries are slow then you should consider de-normalizing your data to increase performance. This (at least in my book) is preferable over adding async tasks.</li>
<li><em>I need to do bulk operations</em>. This probably is a reason to run async tasks. But before biting the bullet, consider: how many of your users are going to run such bulk operations at the same time?</li>
</ul>
</div>
</div>
Setting up Postgres on Windows for development2022-09-20T11:10:00+03:002022-09-20T11:10:00+03:00Serafeim Papastefanostag:spapas.github.io,2022-09-20:/2022/09/20/postgresql-windows-dev/<p class="first last">How to setup Postgres for development on Windows</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p>To install Postgresql for a production server on Windows you’d usually go to the
<a class="reference external" href="https://www.postgresql.org/download/windows/">official website</a> and use the download link. This will give you an executable
installer that would install Postgresql on your server and help you configure it.</p>
<p>However, since I only use Windows for development (and never running any in
production on Windows) I’ve found out that there’s a much better and easier way to install
postgresql for development and windows which I’ll describe in this post.</p>
<p>If you want to avoid reading the whole post, you can just follow the steps described on the
<a class="reference internal" href="#tl-dr-below"><span class="caps">TL</span>;<span class="caps">DR</span> below</a> however I’d recommend reading to understand everything.</p>
</div>
<div class="section" id="downloading-the-server">
<h2>Downloading the server</h2>
<p>First, you’ll
click the <a class="reference external" href="https://www.enterprisedb.com/download-postgresql-binaries">zip archives</a> link on the . <a class="reference external" href="https://www.postgresql.org/download/windows/">official website</a> and then
download the zip archive of the Postgres version you’ll want to install. Right now there are
archives for every current version like 14.5, 13.8, 12.12 etc. Let’s get the latest one, 14.5.</p>
<p>This will give me a zip file named <tt class="docutils literal"><span class="pre">postgresql-14.5-1-windows-x64-binaries.zip</span></tt> which contains a
single folder named <tt class="docutils literal">pgsql</tt>. I’ll extract that folder, rename it to <tt class="docutils literal">pgsql145</tt> and move it to <tt class="docutils literal"><span class="pre">c:\progr</span></tt>
(I keep stuff there to avoid putting everything on C:). Now you should have a folder named
<tt class="docutils literal"><span class="pre">c:\progr\pgsql145</span></tt> that contains a bunch of folder named <tt class="docutils literal">bin</tt>, <tt class="docutils literal">doc</tt>, <tt class="docutils literal">include</tt> etc.</p>
</div>
<div class="section" id="setting-up-the-server">
<h2>Setting up the server</h2>
<p>Now we are ready to setup Postgresql. Open a command line and move to the <tt class="docutils literal">pgsql145\bin</tt> folder:</p>
<pre class="code literal-block">
cd c:\progr\pgsql145\bin
</pre>
<p>The bin folder contains <em>all</em> executables of your server and client, like <tt class="docutils literal">psql.exe</tt> (the <span class="caps">CUI</span> client),
<tt class="docutils literal">pg_dump.exe</tt> (backup), <tt class="docutils literal">initdb.exe</tt> (create a new <span class="caps">DB</span> cluster), <tt class="docutils literal">createdb/dropdb/createuser/dropuser.exe ``
(create/drop database/user - these can also be run from <span class="caps">SQL</span>)
and ``postgres.exe</tt> which is the actual server executable.</p>
<p>Our first step is to create a database cluster using initdb. We need to pass it a folder that will
contain the data of our cluster. So we’ll run it like:</p>
<pre class="code literal-block">
initdb.exe -D c:\progr\pgsql145\data
</pre>
<p>(also you could run <tt class="docutils literal">initdb.exe <span class="pre">-D</span> <span class="pre">..\data</span></tt>, since we are on the bin folder). We’ll get output similar to:</p>
<pre class="code literal-block">
The files belonging to this database system will be owned by user "serafeim".
This user must also own the server process.
The database cluster will be initialized with locale "Greek_Greece.1252".
The default database encoding has accordingly been set to "WIN1252".
The default text search configuration will be set to "greek".
Data page checksums are disabled.
fixing permissions on existing directory c:/progr/pgsql145/data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... windows
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Bucharest
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
pg_ctl -D ^"c^:^\progr^\pgsql145^\data^" -l logfile start
</pre>
<p>And now we’ll have a folder named <tt class="docutils literal"><span class="pre">c:\progr\pgsql145\data</span></tt> that contains files like
<tt class="docutils literal">pg_hba.conf</tt>, <tt class="docutils literal">pg_ident.conf</tt>, <tt class="docutils literal">postgresql.conf</tt> and various folders that will keep our
database server data. All these can be configured but we’re going to keep using the default config
since it fits our needs!</p>
<p>Notice that:</p>
<ul class="simple">
<li>The files of our database belong to the “serafeim” role. This role is automatically created by initdb. This is the same username that I’m using to log in to windows (i.e my home folder is <tt class="docutils literal"><span class="pre">c:\users\serafeim\</span></tt> folder) so this will be different for you. If you wanted to use a different user name or the classic <tt class="docutils literal">postgres</tt> you could pass it to <tt class="docutils literal">initdb</tt> with the -U parameter, for example: <tt class="docutils literal">initdb.exe <span class="pre">-D</span> <span class="pre">c:\progr\pgsql145\data_postgres</span> <span class="pre">-U</span> postgres</tt>.</li>
<li>By default “trust” authentication has been configured. This means, copying from <a class="reference external" href="https://www.postgresql.org/docs/current/auth-trust.html">postgres trust authentication page</a> that “[…] PostgreSQL assumes that anyone who can connect to the server is authorized to access the database with whatever database user name they specify (even superuser names)”. So local connections will always be accepted with the username we are passing. We’ll see how this works in a minute.</li>
<li>The default database encoding will be <span class="caps">WIN1252</span> (on my system). We’ll talk about that a little more later (hint: it’s better to pass -E utf-8 to set your cluster encodign to utf-8)</li>
</ul>
</div>
<div class="section" id="starting-the-server">
<h2>Starting the server</h2>
<p>We could use the <tt class="docutils literal">pg_ctl.exe</tt> executable as proposed by the initdb to start the server as a a background process.
However, for our purposes it’s better to start the server as a foreground process on a dedicated window. So we’ll run the <tt class="docutils literal">postgres.exe</tt> directly like:</p>
<pre class="code literal-block">
postgres.exe -D c:\progr\pgsql145\data
</pre>
<p>or, from the <tt class="docutils literal">bin</tt> directory we could run <tt class="docutils literal">postgres.exe <span class="pre">-D</span> <span class="pre">..\data</span></tt>. The output will be</p>
<pre class="code literal-block">
2022-09-20 09:34:10.184 EEST [10648] LOG: starting PostgreSQL 14.5, compiled by Visual C++ build 1914, 64-bit
2022-09-20 09:34:10.189 EEST [10648] LOG: listening on IPv6 address "::1", port 5432
2022-09-20 09:34:10.189 EEST [10648] LOG: listening on IPv4 address "127.0.0.1", port 5432
2022-09-20 09:34:10.330 EEST [3084] LOG: database system was shut down at 2022-09-20 09:34:08 EEST
2022-09-20 09:34:10.369 EEST [10648] LOG: database system is ready to accept connections
</pre>
<p>Success! Our server is running and listening on 127.0.0.1 port 5432. This means that it accepts connection <em>only</em> from our local machine
(which is what we want for our purposes). We can now connect to it using the <tt class="docutils literal">psql.exe</tt> client. Open another cmd, go to <tt class="docutils literal"><span class="pre">C:\progr\pgsql145\bin</span></tt>
and run <tt class="docutils literal">psql.exe</tt>: You’ll probably get an error similar to <tt class="docutils literal">psql: error: connection to server at "localhost" <span class="pre">(::1),</span> port 5432 failed: <span class="caps">FATAL</span>: database "serafeim" does not exist</tt>
(unless your windows username is <tt class="docutils literal">postgres</tt>).</p>
<p>By default psql.exe tries to connect with a role with the username of your Windows user and to a database named after the user you are
connecting with. Our database server <em>has</em> a role named <tt class="docutils literal">serafeim</tt> (it is created by default by the initdb as described before) but it doesn’t have a database named <tt class="docutils literal">serafeim</tt>! Let’s connect
to the <tt class="docutils literal">postgres</tt> database instead by passing it as a parameter <tt class="docutils literal">psql postgres</tt>:</p>
<pre class="code literal-block">
C:\progr\pgsql145\bin>psql postgres
psql (14.5)
WARNING: Console code page (437) differs from Windows code page (1252)
8-bit characters might not work correctly. See psql reference
page "Notes for Windows users" for details.
Type "help" for help.
postgres=# select version();
version
------------------------------------------------------------
PostgreSQL 14.5, compiled by Visual C++ build 1914, 64-bit
(1 row)
</pre>
<p>Success!</p>
<p>Let’s cerate a sample user and database to make user that everything’s working fine <tt class="docutils literal">createuser.exe koko</tt>,
<tt class="docutils literal">createdb kokodb</tt> and connect to the <tt class="docutils literal">kokodb</tt> as <tt class="docutils literal">koko</tt>: <tt class="docutils literal">psql <span class="pre">-U</span> koko kokodb</tt>.</p>
<pre class="code literal-block">
kokodb=> create table kokotable(foo varchar);
CREATE TABLE
kokodb=> insert into kokotable values('kokoko');
INSERT 0 1
kokodb=> select * from kokotable;
foo
--------
kokoko
(1 row)
</pre>
<p>Everything’s working fine! In the meantime, we should get useful output on our postgres dedicated windows, like
<tt class="docutils literal"><span class="pre">2022-09-20</span> 09:36:01.899 <span class="caps">EEST</span> [9704] <span class="caps">FATAL</span>: database "serafeim" does not exist</tt>. To stop it, just press <tt class="docutils literal">Ctrl+C</tt>
on that window and you should get output similar to:</p>
<pre class="code literal-block">
2022-09-20 09:46:45.178 EEST [10648] LOG: background worker "logical replication launcher" (PID 7860) exited with exit code 1
2022-09-20 09:46:45.185 EEST [10048] LOG: shutting down
2022-09-20 09:46:45.278 EEST [10648] LOG: database system is shut down
</pre>
<p>I usually add a <tt class="docutils literal">pg.bat</tt> file on my <tt class="docutils literal"><span class="pre">c:\progr\pgsql145\</span></tt> that will start the database with its data folder. It’s contents are only
<tt class="docutils literal">bin\postgres.exe <span class="pre">-D</span> data</tt></p>
<p>So let’s create the pg.bat like this:</p>
<pre class="code literal-block">
c:\>cd c:\progr\pgsql145
c:\progr\pgsql145>copy con pg.bat
bin\postgres.exe -D data
^Z
1 file(s) copied.
c:\progr\pgsql145>pg.bat
2022-09-20 09:49:53.642 EEST [11660] LOG: starting PostgreSQL 14.5, compiled by Visual C++ build 1914, 64-bit
...
</pre>
<p>One final thing to notice is that, since we use the trust authentication there’s no check for the password, so if we
tried to pass a password like <tt class="docutils literal">psql <span class="pre">-U</span> koko <span class="pre">-W</span> kokodb</tt> it will work no matter what password we type.</p>
</div>
<div class="section" id="encoding-stuff">
<h2>Encoding stuff</h2>
<div class="section" id="the-default-encoding-situation">
<h3>The default encoding situation</h3>
<p>You may have noticed before that the default encoding for databases will be <tt class="docutils literal"><span class="caps">WIN1252</span></tt> (or some other
similar 8-bit character set). You never want that (I guess this default is there for compatibility reasons),
you want to have utf-8 encoding. So you should either
pass the proper encoding to initdb, like:</p>
<pre class="code literal-block">
initdb -D ..\datautf8 -E utf-8
</pre>
<p>This will create a new cluster with utf-8 encoding. All databases created on that cluster will be utf-8 by default.</p>
<p>If you’ve already got a non-utf-8 cluster, you should force utf-8 for your new database instead:</p>
<pre class="code literal-block">
createdb -E utf-8 -T template0 dbutf8
</pre>
<p>Notice that I also passed the <tt class="docutils literal"><span class="pre">-T</span> template0</tt> parameter to use the <tt class="docutils literal">template0</tt> <a class="reference external" href="https://www.postgresql.org/docs/current/manage-ag-templatedbs.html">template database</a>. If I
tried to run <tt class="docutils literal">createdb <span class="pre">-E</span> <span class="pre">utf-8</span> dbutf8</tt> (so it would use the <tt class="docutils literal">template1</tt>) I’d get an error similar to:</p>
<pre class="code literal-block">
createdb: error: database creation failed: ERROR: new encoding (UTF8) is incompatible with the encoding of the template database (WIN1252)
HINT: Use the same encoding as in the template database, or use template0 as template.
</pre>
</div>
<div class="section" id="about-the-psql-codepage-warning">
<h3>About the psql codepage warning</h3>
<p>You may (or may not) have noticed a warning similar to this when starting the server:</p>
<pre class="code literal-block">
WARNING: Console code page (437) differs from Windows code page (1252)
8-bit characters might not work correctly. See psql reference
page "Notes for Windows users" for details.
</pre>
<p>Some more info about this can be found in the <a class="reference external" href="https://www.postgresql.org/docs/14/app-psql.html`">psql reference page</a> and
<a class="reference external" href="https://stackoverflow.com/questions/20794035/postgresql-warning-console-code-page-437-differs-from-windows-code-page-125">this <span class="caps">SO</span> issue</a>. To avoid this warning you’ll use <tt class="docutils literal">chcp 1252</tt> to set the console code page to 1252
before running psql.</p>
<p>I have to warn you though that using psql.exe from the windows console <strong>will be problematic</strong> anyway
because of not good unicode support. You can use it fine as long as you write only ascii characters but
I’d avoid anything else.</p>
<p>That’s why I’d recommend using a graphical database client like for example <a class="reference external" href="https://dbeaver.io/">dbeaver</a>.</p>
</div>
</div>
<div class="section" id="a-tl-dr-walkthrough">
<span id="tl-dr-below"></span><h2>A <span class="caps">TL</span>;<span class="caps">DR</span> walkthrough</h2>
<p>Here are the steps to follow to get a working postgresql server on windows:</p>
<ol class="arabic simple">
<li>Download the postgresql windows binaries of the version you want from the <a class="reference external" href="https://www.enterprisedb.com/download-postgresql-binaries">zip archives</a> page and extract it to a folder, let’s name it <tt class="docutils literal">pgsql</tt>.</li>
<li>Go to <tt class="docutils literal">pgsql\bin</tt> folder on a command line</li>
<li>Run <tt class="docutils literal">initdb.exe <span class="pre">-D</span> <span class="pre">..\data</span> <span class="pre">-E</span> <span class="pre">utf-8</span></tt> from inside the <tt class="docutils literal">pgsql\bin</tt> folder of the to create a new database cluster with utf-8 encoding on the <tt class="docutils literal">data</tt> directory</li>
<li>Run <tt class="docutils literal">postgresql.exe <span class="pre">-D</span> <span class="pre">..\data</span></tt> to start the database server</li>
<li>Go to <tt class="docutils literal">pgsql\bin</tt> folder on another command line</li>
<li>Run <tt class="docutils literal">psql postgres</tt> to connect to the <tt class="docutils literal">postgres</tt> database with a role similar to your windows username</li>
<li>Profit!</li>
</ol>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Using the above steps you can easily setup a postgres database server on windows for development. Some advantages of the method
proposed here are:</p>
<ul class="simple">
<li>Since you configure the data directory you can have as many clusters as you want (run initdb with different data directories and pass them to postgres)</li>
<li>Since nothing is installed globally, you can have as many postgresql versions as you want, each one having its own data directory. Then you’ll start the one you want each time! For example I’ve got Postgresql 12,13 and 14.5.</li>
<li>Using the trust authentication makes it easy to connect with whatever user</li>
<li>Running the database from postgresql.exe so it has a dedicated window makes it easy to know what the database is doing, peeking at the logs and stopping it (using ctrl+c)</li>
</ul>
</div>
Better Django inlines2022-06-28T12:20:00+03:002022-06-28T12:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2022-06-28:/2022/06/28/better-django-inlines/<p class="first last">How to improve our Django inlines</p>
<p>Django has a feature called <em>inlines</em> which can be used to edit multiple related
objects at once. You’ll get a single view that will contain a single html form that
includes a different Django form for each object, edit any of them and submit them
all to be saved.</p>
<p>This feature is heavily used when you have objects that have a parent-child relation
between them. For example, a book and a testimonial for each book. Each testimonial
will belong to a single book and from a <span class="caps">UX</span> point of view it seems better to be able
to edit all testimonials for each book at the same time.</p>
<p>The biggest disadvantage of inlines is that, because of how Django works, their
interface is very primitive: For adding new objects, you need to define the number
of empty forms that will be included for each inline. The user can fill them up
and press save. Then the objects will be created and the user will get new empty forms to fill.
To understand this better, let’s suppose that you have defined 3 empty forms (which is the
default) and the user wants to create 10 inline objects. The flow will be:</p>
<ul class="simple">
<li>The user sees the 3 empty forms and fills them with data.</li>
<li>The user presses save to <span class="caps">POST</span> the data.</li>
<li>The user sees the 3 new objects and another 3 empty forms.</li>
<li>The user fills the 3 empty forms with data.</li>
<li>The user presses save to <span class="caps">POST</span> the data.</li>
<li>The user sees the 6 new objects and another 3 empty forms.</li>
<li>The user fills the 3 empty forms with data.</li>
<li>The user presses save to <span class="caps">POST</span> the data.</li>
<li>The user sees the 9 new objects and another 3 empty forms.</li>
<li>The user fills 1 empty form with data.</li>
<li>The user presses save to <span class="caps">POST</span> the data.</li>
<li>The user sees the 10 new objects and another 3 empty forms.</li>
</ul>
<p>As you can see the user is filling up the available forms and presses save all the time to get the
new forms to display the objects. This makes the
experience very problematic and confuses users that are not familiar with it. Also, when deleting
objects, the user will see a delete checkbox for each object which needs to select and press save to
actually <em>delete</em> the object. This is also counter-intuitive because it’s not easy for the user
to understand that the object will be deleted when he saves the form.</p>
<p>In this article I’ll present a way to improve the experience of inlines: We’ll have a way to add
new inlines without the need to save the form all the time. Also we’ll be able to improve the
behavior of the delete button so it has a better <span class="caps">UX</span>.</p>
<p>The work in this article is published in this github repository: <a class="reference external" href="https://github.com/spapas/inlinesample">https://github.com/spapas/inlinesample</a>.</p>
<p>The project implements a Book model containing multiple testimonials and editions. For each book
you use inlines to add/edit the Book, its testimonials and editions in the same form. Also, I have included
two ways to add/edit a book: Using the traditional django inlines way and using the javascript way I propose here.</p>
<div class="section" id="models">
<h2>Models</h2>
<p>The models used in this project are the following:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Book</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">256</span><span class="p">)</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">256</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Edition</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">book</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Book</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
<span class="n">publisher</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">256</span><span class="p">)</span>
<span class="n">year</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">()</span>
<span class="n">pages</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">Testimonial</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">book</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Book</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">CASCADE</span><span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">256</span><span class="p">)</span>
<span class="n">testimonial</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">()</span>
</pre></div>
<p>As you can see they are very simple; each edition and testimonial has a foreign key to a book.</p>
</div>
<div class="section" id="views">
<h2>Views</h2>
<p>For the views I’m going to use the <a class="reference external" href="https://github.com/AndrewIngram/django-extra-views">django-extra-views</a> package that provides a bunch of
useful inline-related Class Based Views:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">ListView</span>
<span class="kn">from</span> <span class="nn">extra_views</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">CreateWithInlinesView</span><span class="p">,</span>
<span class="n">UpdateWithInlinesView</span><span class="p">,</span>
<span class="n">InlineFormSetFactory</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">.</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">BookListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Book</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span><span class="o">.</span><span class="n">prefetch_related</span><span class="p">(</span><span class="s2">"edition_set"</span><span class="p">,</span> <span class="s2">"testimonial_set"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">EditionInline</span><span class="p">(</span><span class="n">InlineFormSetFactory</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Edition</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"publisher"</span><span class="p">,</span> <span class="s2">"year"</span><span class="p">,</span> <span class="s2">"pages"</span><span class="p">]</span>
<span class="n">factory_kwargs</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"extra"</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="k">class</span> <span class="nc">TestimonialInline</span><span class="p">(</span><span class="n">InlineFormSetFactory</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Testimonial</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"name"</span><span class="p">,</span> <span class="s2">"testimonial"</span><span class="p">]</span>
<span class="n">factory_kwargs</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"extra"</span><span class="p">:</span> <span class="mi">1</span><span class="p">}</span>
<span class="k">class</span> <span class="nc">BetterMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_template_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"better"</span><span class="p">):</span>
<span class="k">return</span> <span class="p">[</span><span class="s2">"books/book_better_form.html"</span><span class="p">]</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_success_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">"/"</span>
<span class="k">class</span> <span class="nc">BookCreateView</span><span class="p">(</span><span class="n">BetterMixin</span><span class="p">,</span> <span class="n">CreateWithInlinesView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Book</span>
<span class="n">inlines</span> <span class="o">=</span> <span class="p">[</span><span class="n">EditionInline</span><span class="p">,</span> <span class="n">TestimonialInline</span><span class="p">]</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"title"</span><span class="p">,</span> <span class="s2">"author"</span><span class="p">]</span>
<span class="k">class</span> <span class="nc">BookUpdateView</span><span class="p">(</span><span class="n">BetterMixin</span><span class="p">,</span> <span class="n">UpdateWithInlinesView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Book</span>
<span class="n">inlines</span> <span class="o">=</span> <span class="p">[</span><span class="n">EditionInline</span><span class="p">,</span> <span class="n">TestimonialInline</span><span class="p">]</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"title"</span><span class="p">,</span> <span class="s2">"author"</span><span class="p">]</span>
</pre></div>
<p>As you can see for starts we add a BookListView that will be mapped to the / <span class="caps">URL</span>. This displays a table with all the books
along with links to add a new or edit an existing book using both the traditional and better approach.</p>
<p>Then we define two classes inheriting from <tt class="docutils literal">InlineFormSetFactory</tt>: <tt class="docutils literal">EditionInline</tt> and <tt class="docutils literal">TestimonialInline</tt>.
These classes define our inlines: We set a model for them, the fields that will be displayed and pass extra parameters
if needed. In this case we pass <tt class="docutils literal">factory_kwargs = {"extra": 1}</tt> to have a single extra form for each inline. If we didn’t
pass this Django would create 3 extra forms for each inline. Notice that if we were only using the better inlines we’d pass
0 to the extra parameter since it’s not really needed here. However because we use the same inlines for both the traditional
and the better inlines I’m using 1 here (or else we wouldn’t be able to add new objects on the traditional approach).</p>
<p>Then we define a <cite>BetterMixin</cite>; the only thing it does it to return a different html template if the user visits the <em>better</em>
views and override the get_sucess_url method to return to “/”. As you can understand from this, the only difference between the traditional and better approach is the template.</p>
<p>Finally, we’ve got two views for adding/editing a new book. We inherit from <tt class="docutils literal">CreateWithInlinesView</tt> and <tt class="docutils literal">UpdateWithInlinesView</tt>
and set their model, inlines and fields attributes to the correct values.</p>
</div>
<div class="section" id="traditional-templates">
<h2>Traditional templates</h2>
<p>The traditional book_form.html template is like this:</p>
<div class="highlight"><pre><span></span>{% extends "base.html" %}
{% load crispy_forms_tags %}
{% block html_title %}Book form{% endblock%}
{% block page_title %}Book form{% endblock%}
{% block content %}
<span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">'POST'</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card w-full bg-base-100 shadow-xl card-bordered card-compact border border-gray-900"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-body"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-title"</span><span class="p">></span>Book<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
{{ form|crispy }}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% include "partials/_inline_set_simple.html" with formset=inlines.0 title="Editions" %}
{% include "partials/_inline_set_simple.html" with formset=inlines.1 title="Testimonials" %}
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn bg-blue-600'</span> <span class="na">value</span><span class="o">=</span><span class="s">'Save'</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">'/'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn bg-gray-600'</span><span class="p">></span>Back<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
{% endblock %}
</pre></div>
<p>I’m using tailwind css for the templates. As you can see we get a two important context variables: <tt class="docutils literal">form</tt> and <tt class="docutils literal">inlines</tt>. The
<tt class="docutils literal">form</tt> is the main object form (book) and the <tt class="docutils literal">inlines</tt> is the list of inlines (editions and testimonials). Notice that I’m
using a partial template for each of the inlines to improve re-usability. The <cite>_inline_set_simple.html</cite> is like this:</p>
<div class="highlight"><pre><span></span>{% load crispy_forms_tags %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card w-full bg-base-100 shadow-xl card-bordered card-compact border border-gray-900"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-body"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-title"</span><span class="p">></span>{{ title }}<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
{{ formset.management_form }}
{% for form in formset %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'flex border rounded p-1 m-1'</span><span class="p">></span>
{% for field in form %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'flex-col mx-2 my-2'</span><span class="p">></span>
{{ field|as_crispy_field }}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>This uses the django-crispy-forms package to improve form handling. See
<a class="reference external" href="https://spapas.github.io/2020/03/18/django-crispy-form-quick-easy-layout/">this article</a> for a tutorial on using django-crispy-forms.</p>
<p>Notice that i’m doing <tt class="docutils literal">formset=inlines[n]</tt>, so each inline will have a <tt class="docutils literal">management_form</tt> that is
used internally by django and
a bunch of forms (1 for each object). Each form will have the fields we defined for that inline with
the addition of the delete checkbox.</p>
<p>This is enough to get the basic function. The user will get the following form when adding a new book:</p>
<img alt="The traditional book form" src="/images/book-form.png" style="width: 640px;" />
<p>As we already discussed, the user fills the info and presses save if he wants to add more testimonials or editions.</p>
</div>
<div class="section" id="better-templates">
<h2>Better templates</h2>
<p>Let’s now take a peek at the <tt class="docutils literal">book_better_form.html</tt> template:</p>
<div class="highlight"><pre><span></span>{% extends "base.html" %}
{% load crispy_forms_tags static %}
{% block html_title %}Book better form{% endblock%}
{% block page_title %}Book better form{% endblock%}
{% block content %}
<span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">'POST'</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card w-full bg-base-100 shadow-xl card-bordered card-compact border border-gray-900"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-body"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-title"</span><span class="p">></span>Book<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
{{ form|crispy }}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% include "partials/_inline_set.html" with inline_name='edition_set' inline_title="Editions" formset=inlines.0 %}
{% include "partials/_inline_set.html" with inline_name='testimonial_set' inline_title="Testimonials" formset=inlines.1 %}
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn bg-blue-600'</span> <span class="na">value</span><span class="o">=</span><span class="s">'Save'</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">'/'</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn bg-gray-600'</span><span class="p">></span>Back<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% static 'inline-editor.js' %}"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
{% endblock %}
</pre></div>
<p>This is similar to the <tt class="docutils literal">book_form.html</tt> with the following differences:</p>
<ul class="simple">
<li>We include the <tt class="docutils literal">partials/_inline_set.html</tt> partial template passing it the inline_name which is used to identify the inline. We also pass it the actual inline formset object and a title.</li>
<li>We include some custom javascript called <tt class="docutils literal"><span class="pre">inline-editor.js</span></tt> that is used to handle the inline formset.</li>
</ul>
<p>Notice here that we need to use the <em>correct</em> inline_name and not whatever we want! Usually it will be <tt class="docutils literal">child_name_set</tt> but to be sure
we can easily find it by taking a
peek at the management form django will generate for us (we’ll see something like <tt class="docutils literal"><span class="pre">testimonial_set-TOTAL_FORMS</span></tt>, so we know that
the name is <tt class="docutils literal">testimonial_set</tt>).</p>
<p>The partial <tt class="docutils literal">_inline_set.html</tt> is a little more complex:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'better_inline_{{ inline_name }}'</span> <span class="na">class</span><span class="o">=</span><span class="s">"card w-full bg-base-100 shadow-xl card-bordered card-compact border border-gray-900"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-body"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span> <span class="na">class</span><span class="o">=</span><span class="s">"card-title"</span><span class="p">></span>
{{ inline_title }}
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">'btn btn-primary'</span> <span class="na">type</span><span class="o">=</span><span class="s">"button bg-blue-500"</span> <span class="na">id</span><span class="o">=</span><span class="s">"add-form-{{ inline_name }}"</span><span class="p">></span>Add<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
{% if formset.non_form_errors %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"alert alert-danger"</span><span class="p">></span>{{ formset.non_form_errors }}<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endif %}
<span class="p"><</span><span class="nt">template</span> <span class="na">id</span><span class="o">=</span><span class="s">"empty-form-{{ inline_name }}"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'flex border border-primary rounded p-1 m-1 inline-form'</span><span class="p">></span>
{% for field in formset.empty_form %}
{% include "partials/_inline_field.html" %}
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">template</span><span class="p">></span>
{{ formset.management_form }}
{% for form in formset %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'flex border rounded p-1 m-1 inline-form'</span><span class="p">></span>
{% for field in form %}
{% include "partials/_inline_field.html" %}
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% empty %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'flex p-1 m-1 inline-form'</span><span class="p">></</span><span class="nt">div</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span> <span class="cm"><!-- card body --></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span><span class="cm"><!-- card --></span>
</pre></div>
<p>We use the <tt class="docutils literal">inline_name</tt> we passed to generate a unique id for this inline to reference it in the javascript. Then
we have an add new form button. We also add an empty form template that we’ll use to copy over when adding a new form.
The <tt class="docutils literal">formset.empty_form</tt> is generated by django. After we include the <tt class="docutils literal">management_form</tt> we enumerate the forms using
a for loop. Notice the empty div <tt class="docutils literal"><div <span class="pre">class='flex</span> <span class="pre">p-1</span> <span class="pre">m-1</span> <span class="pre">inline-form'></div></span></tt> when there are no forms, we need that
to help us position the place of the forms to be added as will be explained later. The same <tt class="docutils literal"><span class="pre">inline-form</span></tt> class is used
on the empty template and on the existing forms.</p>
<p>This uses the _inline_field.html partial template which is like this:</p>
<div class="highlight"><pre><span></span>{% load widget_tweaks %}
{% load crispy_forms_tags %}
{% if field.field.widget.input_type == 'hidden' %}
{{ field }}
{% else %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'flex-col my-1 mx-2'</span><span class="p">></span>
{% if "DELETE" in field.name %}
{{ field|add_class:""|attr:"onclick:delClick(this)"|as_crispy_field }}
{% elif field.name == "testimonial" %}
{{ field|attr:"rows:2"|as_crispy_field }}
{% else %}
{{ field|as_crispy_field }}
{% endif %}
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endif %}
</pre></div>
<p>In this template we add an onclick function called <tt class="docutils literal">delClick</tt> when the user clicks the delete checkbox. We could also do
various other stuff like hide the delete checkbox and add a delete button instead but i’m leaving it as an exercise to the reader.</p>
</div>
<div class="section" id="better-templates-js">
<h2>Better templates js</h2>
<p>Let’s now take a peek at the actual javascript. First of all we define a function named inlinEditor:</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">inlineEditor</span><span class="p">(</span><span class="nx">inlineSetName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">tmpl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#empty-form-'</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">inlineSetName</span><span class="p">);</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">counter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'[name='</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">inlineSetName</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">'-TOTAL_FORMS]'</span><span class="p">)</span>
<span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#add-form-'</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">inlineSetName</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span><span class="w"> </span><span class="nx">ev</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">ev</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">()</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">newForm</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">tmpl</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">cloneNode</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="w"> </span><span class="nx">newForm</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">'[id*=__prefix__]'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'__prefix__'</span><span class="p">,</span><span class="w"> </span><span class="nx">counter</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">name</span><span class="p">)</span><span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'__prefix__'</span><span class="p">,</span><span class="w"> </span><span class="nx">counter</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">newForm</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">'[for*=__prefix__]'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">htmlFor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">htmlFor</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="s1">'__prefix__'</span><span class="p">,</span><span class="w"> </span><span class="nx">counter</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
<span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="nx">counter</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nb">Number</span><span class="p">(</span><span class="nx">counter</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">last_element_selector</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'form #better_inline_'</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">inlineSetName</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' .inline-form:last-of-type'</span>
<span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="nx">last_element_selector</span><span class="p">).</span><span class="nx">insertAdjacentElement</span><span class="p">(</span><span class="s1">'afterend'</span><span class="p">,</span><span class="w"> </span><span class="nx">newForm</span><span class="p">.</span><span class="nx">children</span><span class="p">[</span><span class="mf">0</span><span class="p">])</span>
<span class="w"> </span><span class="p">})</span>
<span class="p">}</span>
</pre></div>
<p>This initially function saves the empty form template and the number of forms in the inline. The number of the forms initially is provided
by the django management form. Then we add a click event to the add button for that particular inline. When the user clicks the add
button we’ll add a new empty form to the end of the existing forms. This works like this:</p>
<p>Each of the inline forms has an id that has the following form <tt class="docutils literal"><span class="pre">inline_name-<span class="caps">NUMBER</span>-field_name</span></tt>, so for example for the first form of editions publisher we’ll get something like <tt class="docutils literal"><span class="pre">edition_set-0-publisher</span></tt>. The empty form has the string <tt class="docutils literal">__prefix__</tt> instead of the number
so it will be <tt class="docutils literal"><span class="pre">edition_set-__prefix__-publisher</span></tt>. To create the new form we clone the empty form template and replace the <tt class="docutils literal">__prefix__</tt>
on the elements with the correct number (based on the total number of forms). Then we increase the number of forms and insert the new form
next to the element with the <tt class="docutils literal">last_element_selector</tt> we define there. As you can see this selector will find the last element that
is inside our inline and has a class of <tt class="docutils literal"><span class="pre">inline-form</span></tt>.
That’s why we need the <tt class="docutils literal"><span class="pre">inline-form</span></tt> class to all three cases as we discussed above</p>
<p>Beyond this, we also have the implementation of <tt class="docutils literal">delClick</tt> that adds a red-border class to form of the element that was clicked
(notice the parentElement.parentElement thingie):</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">delClick</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">checked</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">'border-red-500'</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="s1">'border-red-500'</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Finally, we generate the inlineEditors when the dom is loaded:</p>
<div class="highlight"><pre><span></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'DOMContentLoaded'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">inlineEditor</span><span class="p">(</span><span class="s1">'edition_set'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">inlineEditor</span><span class="p">(</span><span class="s1">'testimonial_set'</span><span class="p">);</span>
<span class="p">})</span>
</pre></div>
<p>Please notice that here we also need to use the <em>correct</em> name of the inlines (both here and in the template).</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Using the better approach our book form will be like this:</p>
<img alt="The better book form" src="/images/better-book-form.png" style="width: 640px;" />
<p>Now the user can click the add button and a new form will be added in the end of the current list of forms. Also
when he clicks the delete button he’ll get a red border around the form to be deleted.</p>
<p>Before finishing this tutorial I’d like to point out some things that you need to be extra careful about, especially
since you are probably going to use your own html structure:</p>
<ul class="simple">
<li>Don’t forget to use the correct name for the inlines in the partial template and when initializing it with inlineEditor</li>
<li>Make sure to add the inline-form class to an empty div if there are no forms in the inline, to the existing forms of the inline and to the empty template</li>
<li>Be careful on where you’ll add classes to the delClick handler; it depends on the structure of your html</li>
</ul>
</div>
Using clojure from Windows2022-04-14T11:20:00+03:002022-04-14T11:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2022-04-14:/2022/04/14/clojure-windows/<p class="first last">How to install and use clojure from Windows</p>
<p>In this small article I’m going to post a guide on how to install and use clojure from Windows using good old’ cmd.exe.</p>
<p>Unfortunately, most guides on the official clojure site have instructions on using Clojure from Windows through Powershell or <span class="caps">WSL</span>.
For my own reasons I hate both these approaches and only use the cmd.exe to interact with the Windows command line.</p>
<p>There are more or less two approaches to using clojure. Using <a class="reference external" href="https://leiningen.org/">leiningen</a> or using the clj tools.
The clojure official guide seems to be <cite>biased towards clj tools</cite>. However I think that leiningen may be easier for new users.
I’ll cover both approaches here.</p>
<p><em>Warning</em> Before doing anything else please make sure to install Java. You need a version of java that is at least 1.8. Try running
<tt class="docutils literal">java <span class="pre">-version</span></tt> in cmd.exe to make sure you have java and it is the correct version.</p>
<div class="section" id="leiningen">
<h2>Leiningen</h2>
<p>To install leiningen you just download the lein.bat file from their page and put it in a folder in your <span class="caps">PATH</span>. You’ll then run
lein and it will download all dependencies and install itself!</p>
<p>To start a clojure repl to be able to play with clojure you write <tt class="docutils literal">lein repl</tt>. If everything went smooth you should see a prompt
and if you write <tt class="docutils literal">(+ 1 2)</tt> you should get <tt class="docutils literal">3</tt>. To exit press <tt class="docutils literal">ctrl+d</tt> or write <tt class="docutils literal">exit</tt>.</p>
<p>To start a new project you’ll use <tt class="docutils literal">lein new [template name] [project name]</tt>. For example, to create a new app you’ll write:
<tt class="docutils literal">lein new app leinapp</tt>. You’ll get a new directory called <tt class="docutils literal">leinapp</tt>. The important stuff in this directory are:</p>
<ul class="simple">
<li><tt class="docutils literal">project.clj</tt>: The basic descriptor of your project; here you can set various attrs of your project and also add dependencies</li>
<li>src\leinapp: The source directory of your project. This is where you’ll put your code.</li>
<li>test\leinapp: Add tests here</li>
</ul>
<p>There should be a <tt class="docutils literal">core.clj</tt> file inside your src\leinapp folder. The <tt class="docutils literal">main</tt> function is the entry point of the app. Try running
<tt class="docutils literal">lein run</tt> from the project folder and you should get the output of the <tt class="docutils literal">main</tt> function.</p>
<p>Add this to the end of the <tt class="docutils literal">core.clj</tt> to define a <tt class="docutils literal">foo</tt> function:</p>
<div class="highlight"><pre><span></span><span class="p">(</span><span class="kd">defn </span><span class="nv">foo</span><span class="w"> </span><span class="p">[]</span>
<span class="w"> </span><span class="s">"bar"</span><span class="p">)</span>
</pre></div>
<p>And run <tt class="docutils literal">lein repl</tt>. You should get a repl command prompt for your application
in the <tt class="docutils literal">leinapp.core</tt> namespace (if you named your app <tt class="docutils literal">leinapp</tt>). Type
<tt class="docutils literal">(foo)</tt> and you should see <tt class="docutils literal">"bar"</tt>.</p>
<p>To create a stand alone jar with your code (called <em>uberjar</em>) you can use <tt class="docutils literal">lein uberjar</tt>. This will create a file
named <tt class="docutils literal"><span class="pre">target\uberjar\leinapp-0.1.0-<span class="caps">SNAPSHOT</span>-standalone.jar</span></tt>. Then try <tt class="docutils literal">java <span class="pre">-jar</span> <span class="pre">target\uberjar\leinapp-0.1.0-<span class="caps">SNAPSHOT</span>-standalone.jar</span></tt>
(notice I’m still on the leinapp project folder) and you’ll see the output of main!</p>
</div>
<div class="section" id="clj">
<h2>clj</h2>
<p>Using the clj is a more <em>modern</em> approach to clojure development. As I said before the official clojure page seems to be biased towards
using this approach. The problem is that it seems to require Powershell to run as you can see on the <cite>clj on Windows</cite> page.</p>
<p>Thankfully, the good people at the <a class="reference external" href="https://clojurians.slack.com/">clojurians</a> slack pointed me to <a class="reference external" href="https://github.com/borkdude/deps.clj">deps.clj</a> project. This is an implementation of clj in clojure and
can be installed natively on Windows by downloading the .zip <a class="reference external" href="https://github.com/borkdude/deps.clj/releases">from the releases page</a>. This zip should contain a deps.exe file. Put
that executable it in your path. You can also rename it to clj.exe if you want. Also if you have the powershell installed you can run this command from cmd.exe
<tt class="docutils literal">PowerShell <span class="pre">-Command</span> "iwr <span class="pre">-useb</span> <span class="pre">https://raw.githubusercontent.com/borkdude/deps.clj/master/install.ps1</span> | iex"</tt> to install it automatically.</p>
<p>You can now run <tt class="docutils literal">deps</tt> and you should get a clojure repl similar to <tt class="docutils literal">lein repl</tt>.</p>
<p>To create a new project skeleton you can use the
use the <a class="reference external" href="https://github.com/seancorfield/deps-new">deps-new</a> project. To install it run the following command from cmd.exe:
<tt class="docutils literal">deps <span class="pre">-Ttools</span> install <span class="pre">io.github.seancorfield/deps-new</span> <span class="pre">"{:git/tag</span> <span class="pre">"""v0.4.9"""}"</span> :as new</tt> (please notice that there are various
<a class="reference external" href="https://clojure.org/reference/deps_and_cli#quoting">problems with the quoting on windows</a> but this command should work fine).</p>
<p>To create a new app run: <tt class="docutils literal">deps <span class="pre">-Tnew</span> app :name organization/depsapp</tt> and you’ll get your app in the <tt class="docutils literal">depsapp</tt> folder. If you want
a similar form as with lein, try <tt class="docutils literal">deps <span class="pre">-Tnew</span> app :name depsapp/core <span class="pre">:target-dir</span> depsapp</tt>. Now the <tt class="docutils literal">depsapp</tt> folder will contain:</p>
<ul class="simple">
<li><tt class="docutils literal">deps.edn</tt>: The basic descriptor of your project; here you can set various attrs of your project and also add dependencies. This more or less changes the project.clj we got from leiningen.</li>
<li>src\depsapp: The source directory of your project. This is where you’ll put your code.</li>
<li>test\depsapp: Add tests here</li>
</ul>
<p>To run the project, try: <tt class="docutils literal">deps <span class="pre">-M</span> <span class="pre">-m</span> depsapp.core</tt> or
<tt class="docutils literal">deps <span class="pre">-M:run-m</span></tt> or
<tt class="docutils literal">deps <span class="pre">-X:run-x</span></tt> to directly run the greet function (<tt class="docutils literal"><span class="pre">run-m</span></tt> and <tt class="docutils literal"><span class="pre">run-x</span></tt> are aliases defined in <tt class="docutils literal">deps.edn</tt> take a peek).</p>
<p>To start a <span class="caps">REPL</span>, run <tt class="docutils literal">deps</tt>. Notice this will start on the <tt class="docutils literal">user</tt> namespace, so you’ll need to do something like:</p>
<div class="highlight"><pre><span></span><span class="nv">user=></span><span class="w"> </span><span class="p">(</span><span class="nf">require</span><span class="w"> </span><span class="ss">'depsapp.core</span><span class="p">)</span>
<span class="nv">nil</span>
<span class="nv">user=></span><span class="w"> </span><span class="p">(</span><span class="nf">depsapp.core/foo</span><span class="p">)</span>
<span class="s">"bar"</span>
</pre></div>
<p>to run a <tt class="docutils literal">(foo)</tt> function that you’ve added in the <tt class="docutils literal">core.clj</tt> file.</p>
<p>To run the tests use: <tt class="docutils literal">deps <span class="pre">-T:build</span> test</tt>.</p>
<p>To create the uberjar you’ll run:
<tt class="docutils literal">deps <span class="pre">-T:build</span> ci</tt> (tests must pass). Then execute it directly using
<tt class="docutils literal">java <span class="pre">-jar</span> <span class="pre">target\core-0.1.0-<span class="caps">SNAPSHOT</span>.jar</span></tt>.</p>
<p>Also, notice that it’s really simple to create a new project with deps without the deps-new. For example,
create a folder named <tt class="docutils literal">manualapp</tt> and in this folder
create a <tt class="docutils literal">deps.edn</tt> file containing just the string <tt class="docutils literal">{}</tt>. Then add another folder named <tt class="docutils literal">src</tt> with a <tt class="docutils literal">hello.clj</tt> file
containing something like:</p>
<div class="highlight"><pre><span></span><span class="p">(</span><span class="kd">ns </span><span class="nv">hello</span><span class="p">)</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">foo</span><span class="w"> </span><span class="p">[]</span>
<span class="w"> </span><span class="s">"bar"</span><span class="p">)</span>
<span class="p">(</span><span class="kd">defn </span><span class="nv">run</span><span class="w"> </span><span class="p">[</span><span class="nv">opts</span><span class="p">]</span>
<span class="w"> </span><span class="p">(</span><span class="nb">println </span><span class="s">"Hello world"</span><span class="p">))</span>
</pre></div>
<p>You can then open a <span class="caps">REPL</span> on the project using <tt class="docutils literal">deps</tt> or run the run function using <tt class="docutils literal">deps <span class="pre">-X</span> hello/run</tt>.</p>
</div>
<div class="section" id="vscode-integration">
<h2>VSCode integration</h2>
<p>Both leining and clj projects can easily be used with VSCode. First of all, install the calva package in your VSCode. Then, open your
clojure project in VScode and press <tt class="docutils literal">ctrl+shift+p</tt> to bring up the command pallete. Here write “Jack” (from jack-in) and select it
(also this has the shortctut <tt class="docutils literal">ctrl+alt+c ctrl+alt+j</tt>). Select the correct project type (<tt class="docutils literal">leiningen</tt> or <tt class="docutils literal">deps.edn</tt>). A repl
will be opened to the side; you can then go to your core.clj file and run <tt class="docutils literal">ctrl+alt+c enter</tt> to load the current file.</p>
<p>Then you can move to the repl on the side and run the function with <tt class="docutils literal">(foo)</tt> or run <tt class="docutils literal"><span class="pre">(-main)</span></tt>. Also you can write <tt class="docutils literal">(foo)</tt>
in your source file and press <tt class="docutils literal">ctrl+enter</tt> to execute it and see the result; the <tt class="docutils literal">ctrl+enter</tt> will execute the form where your
cursor is. See <a class="reference external" href="https://calva.io/try-first/">this</a> for more.</p>
</div>
PDFs in Django like it’s 2022!2022-02-14T12:20:00+02:002022-02-14T12:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2022-02-14:/2022/02/14/django-pdfs-2022/<p class="first last">How to render PDFs in Django like it’s 2022!</p>
<p>In a <a class="reference external" href="https://spapas.github.io/2015/11/27/pdf-in-django/">previous article</a> I had written a very comprehensive
guide on how to render PDFs in Django using tools like reportlab and xhtml2pdf. Although these tools
are still valid and work fine I have decided that usually it’s way too much pain to set them up to work properly.</p>
<p>Another common way to generate PDFs in the Python world is using the <a class="reference external" href="https://weasyprint.org/">weasyprint</a> library.
Unfortunately this library has way too many requirements and installing it on Windows
is worse than <a class="reference external" href="https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#windows">putting needles in your eyes</a>. I don’t like needles in my eyes, thank you very much.</p>
<p>There are various other ways to generate PDFs like using a report server like Japser or
<span class="caps">SQL</span> Server Reporting Services but these are too “enterpris-y” for most people and require
another server, a learning curve, etc.</p>
<p>I was actually so disappointed by the status of <span class="caps">PDF</span> generation today that in some recent projects
instead of the <span class="caps">PDF</span> file I generated an <span class="caps">HTML</span> page with a nice pdf-printing stylesheet and
instructed the users to print it as <span class="caps">PDF</span> (from the browser) so as to generate the <span class="caps">PDF</span> themselves!</p>
<p>However, recently I found another way to generate PDFs in my Django projects which I’d like to share
with you: Using the <a class="reference external" href="https://wkhtmltopdf.org/">wkhtmltopdf</a> tool. The wkhtmltopdf is a command line program that has binaries
for more or less every operating system. It’s a single binary that you can download and put it in
a directory, you don’t need to run another server or any fancy installation. Only an executable. To
use it? You call it like <cite>wkhtmltopdf http://google.com google.pdf</cite> and it will download the url
and generate the pdf! It’s as simple as that! This tool is old and heavily used but only recently I
researched its integration with Django.</p>
<p>Please notice that there’s actually a <a class="reference external" href="https://github.com/incuna/django-wkhtmltopdf">django-wkhtmltopdf</a> library for integrating wkhtmltopdf with
django. However I din’t have good results while trying to use it (maybe because of my Windows dev
environment). Also, implementing the integration myself allowed my to more easily understand what’s
happening and better debug the wkhtmltopdf. However <span class="caps">YMMV</span>, after you read this small post to understand
how the integration works you can try django-wkhtmltopdf to see if it works in your case.</p>
<p>In any way, the first thing you need to do is download and install wkhtmltopdf for your platform and save its
full path in your settings.py like this:</p>
<div class="highlight"><pre><span></span><span class="c1"># For linux</span>
<span class="n">WKHTMLTOPDF_CMD</span> <span class="o">=</span> <span class="s1">'/usr/local/bin/wkhtmltopdf'</span>
<span class="c1"># or for windows</span>
<span class="n">WKHTMLTOPDF_CMD</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'c:\util\wkhtmltopdf.exe'</span>
</pre></div>
<p>Notice that I’m using the full path. I have observed that even if you put the binary to a directory
in the system <span class="caps">PATH</span> it won’t be picked (at least in my case) thus I recommend using the full path.</p>
<p>Now, let’s suppose we’ve got a <tt class="docutils literal">DetailView</tt> (let’s call it <tt class="docutils literal">SampleDetailView</tt>) that we’d like to render as <span class="caps">PDF</span>. We can use the following
<span class="caps">CBV</span> for that:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">check_output</span>
<span class="kn">from</span> <span class="nn">django.template</span> <span class="kn">import</span> <span class="n">Context</span><span class="p">,</span> <span class="n">Template</span>
<span class="kn">from</span> <span class="nn">django.template.loader</span> <span class="kn">import</span> <span class="n">get_template</span>
<span class="kn">from</span> <span class="nn">tempfile</span> <span class="kn">import</span> <span class="n">NamedTemporaryFile</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="k">class</span> <span class="nc">SamplePdfDetailView</span><span class="p">(</span><span class="n">SampleDetailView</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_resp_from_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
<span class="n">template</span> <span class="o">=</span> <span class="n">get_template</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
<span class="k">return</span> <span class="n">resp</span>
<span class="k">def</span> <span class="nf">get_resp_from_string</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">template_str</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
<span class="n">template</span> <span class="o">=</span> <span class="n">Template</span><span class="p">(</span><span class="n">template_str</span><span class="p">)</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">template</span><span class="o">.</span><span class="n">render</span><span class="p">(</span><span class="n">Context</span><span class="p">(</span><span class="n">context</span><span class="p">))</span>
<span class="k">return</span> <span class="n">resp</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
<span class="n">context</span><span class="p">[</span><span class="s1">'pdf'</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># We can use either</span>
<span class="n">resp</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_resp_from_string</span><span class="p">(</span><span class="s2">"<h1>Hello, world! {{ object }}</h1>"</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span>
<span class="c1"># or</span>
<span class="c1"># resp = self.get_resp_from_file('test_pdf.html', context)</span>
<span class="n">tempfile</span> <span class="o">=</span> <span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="n">mode</span><span class="o">=</span><span class="s1">'w+b'</span><span class="p">,</span> <span class="n">buffering</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span>
<span class="n">suffix</span><span class="o">=</span><span class="s1">'.html'</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s1">'tmp'</span><span class="p">,</span>
<span class="nb">dir</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">delete</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">tempfile</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s1">'utf-8'</span><span class="p">))</span>
<span class="n">tempfile</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
<span class="n">tempfile</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">settings</span><span class="o">.</span><span class="n">WKHTMLTOPDF_CMD</span><span class="p">,</span>
<span class="s1">'--page-size'</span><span class="p">,</span> <span class="s1">'A4'</span><span class="p">,</span> <span class="s1">'--encoding'</span><span class="p">,</span> <span class="s1">'utf-8'</span><span class="p">,</span>
<span class="s1">'--footer-center'</span><span class="p">,</span> <span class="s1">'[page] / [topage]'</span><span class="p">,</span>
<span class="s1">'--enable-local-file-access'</span><span class="p">,</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="s1">'-'</span><span class="p">]</span>
<span class="c1"># print(" ".join(cmd))</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">check_output</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">os</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">tempfile</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">out</span><span class="p">,</span> <span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
</pre></div>
<p>We can put the pdf view on our url patterns right next to our <tt class="docutils literal">DetailView</tt> i.e:</p>
<div class="highlight"><pre><span></span><span class="p">[</span>
<span class="o">...</span>
<span class="n">path</span><span class="p">(</span>
<span class="s2">"detail/<int:pk>/"</span><span class="p">,</span>
<span class="n">permission_required</span><span class="p">(</span><span class="s2">"core.user"</span><span class="p">)(</span>
<span class="n">views</span><span class="o">.</span><span class="n">SampleDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()</span>
<span class="p">),</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"detail"</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">path</span><span class="p">(</span>
<span class="s2">"detail/<int:pk>/pdf/"</span><span class="p">,</span>
<span class="n">permission_required</span><span class="p">(</span><span class="s2">"core.user"</span><span class="p">)(</span>
<span class="n">views</span><span class="o">.</span><span class="n">SamplePdfDetailView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()</span>
<span class="p">),</span>
<span class="n">name</span><span class="o">=</span><span class="s2">"detail_pdf"</span><span class="p">,</span>
<span class="p">),</span>
<span class="o">...</span>
<span class="p">]</span>
</pre></div>
<p>Let’s try to understand how this works: First of all notice that we have two options, either
create a <span class="caps">PDF</span> from an html string or from a normal template file. For the first option we pass
the full html string to the <tt class="docutils literal">get_resp_from_string</tt> and the context and we’ll get the rendered html
(i.e the context will be applied to the template).
For the second option we pass the filename of a django template and the context. Notice that
there’s a small difference on how the <tt class="docutils literal">template.render()</tt> method is called in the two methods.</p>
<p>After that we’ve got an html file saved in the <tt class="docutils literal">resp</tt> string. We want to give this to wkhtmltopdf so
as to be converted to <span class="caps">PDF</span>. To do that we first create a temporary file using the <tt class="docutils literal">NamedTemporaryFile</tt>
class and write the <tt class="docutils literal">resp</tt> to it. Then we call wkhtmltopdf passing it this temporary file. Notice we
use the <tt class="docutils literal">subprocess.check_output</tt> function that will capture the output of the command and return it.</p>
<p>Finally we delete the temporary file and return the pdf as an <tt class="docutils literal">HttpResponse</tt>.</p>
<p>We call the wkhtmltopdf like this:</p>
<pre class="code literal-block">
c:\util\wkhtmltopdf.exe --page-size A4 --encoding utf-8 --footer-center [page] / [topage] --enable-local-file-access C:\Users\serafeim\AppData\Local\Temp\tmp_lh5r6f9.html -
</pre>
<p>The page-size can be changed to letter if you are in the <span class="caps">US</span>. The encoding should be utf-8. The —footer-center option adds a
footer to the <span class="caps">PDF</span> page with the current page and the total number of pages. The —enable-local-file-access is very important
since it allows <tt class="docutils literal">wkhtmltopdf</tt> to access local files (in the filesystem) and not only remote ones. After that we’ve got the
full path of our temporary file and following is the <tt class="docutils literal">-</tt> which means that the pdf output will be on the stdout (so we’ll capture it
with <tt class="docutils literal">check_output</tt>).</p>
<p>Notice that there’s a commented out print command before the <tt class="docutils literal">check_output</tt> call. If you have problems you can call this
command from your command line to debug the wkhtmltopdf command (don’t forget to comment out the <tt class="docutils literal">os.remove</tt> line to keep
the temporary file). Also, wkhtmltopdf will output some stuff while rendering the command, for example something like:</p>
<pre class="code literal-block">
Loading pages (1/6)
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done
</pre>
<p>You can pass the <tt class="docutils literal"><span class="pre">--quiet</span></tt> option to hide this output. However the output is useful to see what wkhtmltopdf is doing in
case there are problems so I recommend leaving it on while developing. Let’s take a look at a problematic output:</p>
<pre class="code literal-block">
Loading pages (1/6)
Error: Failed to load file:///static/bootstrap/css/bootstrap.min.css, with network status code 203 and http status code 0 - Error opening /static_edla/bootstrap/css/govgr_bootstrap.min.css: The system cannot find the path specified.
[...]
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done
</pre>
<p>The above output means that our template tries to load a css file that wkhtmltopdf can’t find and errors out! To understand this error, I had a line like this in my template:</p>
<pre class="code literal-block">
<link href="{% static 'bootstrap/css/bootstrap.min.css' %}" rel="stylesheet">
</pre>
<p>which will be converted to a link like <tt class="docutils literal">`/static/bootstrap/css/bootstrap.min.css</tt>.
However notice that I tell wkhtmltopdf to render a file from my temporary directory, it doesn’t
know where that link points to!
Following this thing you need to be extra careful to <em>include everything</em> in your <span class="caps">HTML</span>-pdf template and not
use any external links. So all styles must be inlined in the template using <tt class="docutils literal"><style></tt> tags and all images must be
converted to data images with base64, something like:</p>
<pre class="code literal-block">
<img src='data:image/png;base64,...>
</pre>
<p>To do that in python for a dynamic image you can use something like:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">base64</span>
<span class="k">def</span> <span class="nf">convert_to_data</span><span class="p">(</span><span class="n">image</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">'data:image/xyz;base64,</span><span class="si">{}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">base64</span><span class="o">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">image</span><span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s1">'utf-8'</span><span class="p">))</span>
</pre></div>
<p>and then use that as your image src (notice I’m using <tt class="docutils literal">image/xyz</tt> here for an
arbitrary image, please use the correct image type if you know it i.e <tt class="docutils literal">image/png</tt> or <tt class="docutils literal">image/jpg</tt>).</p>
<p>If you’ve got a static image you want to include you can convert it to base64 using an online service <a class="reference external" href="https://www.base64-image.de/">like this</a>,
or read it with python and convert it:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">base64</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">'static/images/image.png'</span><span class="p">,</span> <span class="s1">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">image</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="n">base64</span><span class="o">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">image</span><span class="o">.</span><span class="n">read</span><span class="p">())</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s1">'utf-8'</span><span class="p">))</span>
</pre></div>
<p>Instead of a <tt class="docutils literal">DetailView</tt> we could use the same approach for any kind of <span class="caps">CBV</span>. If you are to use the <span class="caps">PDF</span>
response to multiple CBVs I recommend exporting the functionality to a mixin and inheriting from that also
(see my <a class="reference external" href="https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/"><span class="caps">CBV</span> guide</a> for more).</p>
<p>Finally, the big question in the room is why should I convert my template to a file and pass that to
wkhtmltopdf, can’t I use the <span class="caps">URL</span> of my template, i.e pass wkhtmltopdf something like <a class="reference external" href="http://example.com/app/detail/321/">http://example.com/app/detail/321/</a>?</p>
<p>By all means you can! This will also enable you to avoid using inline styles and media!!
However keep in mind that the usual case is that this view will not be public but will need an authenticated user to
access it; wkhtmltopdf is publicly trying to access it, it doesn’t have any rights to it so you’ll get a 404 or 403 error!
Of course you can
start an adventure on authenticating it somehow (and maybe doing something stupid) or you can just follow my lead
and render it to a file :)</p>
A forward and reverse proxy primer for the layman2021-09-21T14:20:00+03:002021-09-21T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2021-09-21:/2021/09/21/layman-proxy-primer/<p class="first last">A primer for explaining forward and reverse proxies to the layman</p>
<p>Before some days I’d <a class="reference external" href="https://news.ycombinator.com/item?id=28381220">written an answer on <span class="caps">HN</span></a> where I explained as simply as possible
how a forward and a reverse proxy is working and what is the difference between them. In this article
I’m going to extend this answer a bit to make it a full post and clarify some things even more.</p>
<p>Forward and reverse proxies is an important concept that a lot of technical people aren’t familiar with.
<span class="caps">HTTP</span> Proxying is a process of forwarding (<span class="caps">HTTP</span>) requests from one server to the other. So when an <span class="caps">HTTP</span>
client issues a request to the server, the request will pass through the proxy server and be forwarded to the
destination server (called the origin server). This explanation is true both for forward and reverse proxying.</p>
<div class="section" id="forward-proxy">
<h2>Forward Proxy</h2>
<p>A forward proxy is used when an <span class="caps">HTTP</span> Client (i.e a browser) wants to access resources in the internet
but isn’t allowed to connect directly to the public internet so instead uses the proxy.</p>
<p>Usually companies don’t allow unrestricted access to the internet from their internal network.
Thus the internal users would need to use a proxy to access the internet. This is the concept of the forward proxy.
What happens is that when an internal user want to access an internet resource (i.e www.google.com) her client (i.e browser)
will ask a specific server (the proxy server) for that resource. The client needs to be configured properly with the address
of the proxy server.</p>
<p>So instead of <a class="reference external" href="http://www.google.com">http://www.google.com</a> the browser will access <a class="reference external" href="http://proxy.company.com/?url=www.google.com">http://proxy.company.com/?url=www.google.com</a> and the
proxy will fetch the results and return them to you. If the browser wants to access <a class="reference external" href="https://www.google.com">https://www.google.com</a> <em>without</em>
a configured proxy server it will get a network error.</p>
<p>Here’s an image that explains this:</p>
<img alt="Forward proxy" src="/images/fw_proxy.png" style="width: 800px;" />
<p>The internal client can access the internal web server directly without problems. However he cannot access
the internet server directly so he needs to use the proxy to access it.</p>
<p>One thing that needs to be made crystal is that the fact that your browser works with the proxy does not mean
that any other <span class="caps">HTTP</span> clients you use will also work. For example, you may want to run <tt class="docutils literal">curl</tt> or <tt class="docutils literal">wget</tt> to download
some files from an external server; these programs will not work without setting a proxy (usually by setting the http_proxy
and https_proxy environment variables or by passing a parameter). Also, the proxy only works for <span class="caps">HTTP</span> requests. If you are
in a private network without external access you will <em>not</em> be able to access non-<span class="caps">HTTP</span> resources. For example you will not be able
to access your non-company mail server (which uses either <span class="caps">IMAP</span> or <span class="caps">POP3</span>) from behind your company’s network. Typically, you’ll use
a web client for accessing your mails.</p>
<p>So it seems that using a proxy heavily restricts the internal users usage of internet. What are the advantages of using a forward proxy?</p>
<ul class="simple">
<li>Security: Since the internal computers of a company will not have internet access there’s no easy way for attackers to access these computers.</li>
<li>Content moderation: The company through the proxy can block access to various internet sites (i.e social network, gaming etc) that the users shouldn’t access during work.</li>
<li>Caching: The proxy server can have a cache so when multiple users access the same internet resource it will downloaded only once saving the company’s bandwidth.</li>
</ul>
<p>Especially the security thing is so important that almost all corporate (or university etc) networks will use a proxy server and never
allow direct access to the internet.</p>
<p>A well known, open source forward proxy server is <a class="reference external" href="http://www.squid-cache.org/">Squid</a>.</p>
</div>
<div class="section" id="reverse-proxy">
<h2>Reverse proxy</h2>
<p>A reverse proxy is an <span class="caps">HTTP</span> server that “proxies” (i.e forwards) some (or all) requests it receives to a different <span class="caps">HTTP</span>
server and returns the answer back. For example, a company may have a couple of <span class="caps">HTTP</span> servers in its internal network. These
servers have private addresses and cannot be accessed through the internet. To allow external users to access these servers,
the company will configure a reverse proxy server that will forward the requests to the internal servers as seen in the picture:</p>
<img alt="Reverse proxy" src="/images/reverseproxy.png" style="width: 800px;" />
<p>What happens is that the proxy server will forward requests that fulfill some specific
criteria to other web servers. The criteria may be requests that have
* a specific host (forward the requests that have a hostname of <tt class="docutils literal">www.server1.company.com</tt> to the internal server named <tt class="docutils literal">server1</tt> and <tt class="docutils literal">www.server2.company.com</tt> to the internal server named <tt class="docutils literal">server2</tt>)
* or a specific port (forward requests in the port 81 to <tt class="docutils literal">server1</tt> and requests in the port 82 to <tt class="docutils literal">server2</tt>)
* or even a particular path (forward requests with the path <tt class="docutils literal">www.company.com/server1</tt> to <tt class="docutils literal">server1</tt> and requests with the path <tt class="docutils literal">www.company.com/server2</tt> to <tt class="docutils literal">server2</tt>)</p>
<p>or even other criteria that may be decided.</p>
<p>Let’s see some example of reverse proxying:</p>
<ul class="simple">
<li>A characteristic example of reverse proxy is the well-known 3-tier architecture (web server / app server / database server). The web server is used to serve all requests but it “proxies” (forwards) some of the requests to the app server. This is used because the web server cannot serve dynamic replies but can serve static replies like for example files.</li>
<li>Offloading the <span class="caps">SSL</span> (https) security to a particular web server. This server will store the private key of your certificate and terminate the <span class="caps">SSL</span> connections. It will then forward the requests to the internal web servers using plain <span class="caps">HTTP</span>.</li>
<li>An <span class="caps">HTTP</span> load balancer will proxy the requests to a set of other servers based on some algorithm to share the load (i.e the HAProxy software load balancer or even a hardware load balancer)</li>
<li>A reverse proxy can be used to act as a security and <span class="caps">DOS</span> “shield” for your web servers. It will check the requests for common attack patterns and forward them to your servers only if they are safe</li>
<li>A reverse proxy can be used for caching; it will return cached versions of resources if they are available to avoid overloading the application servers</li>
<li>A <span class="caps">CDN</span> (content delivery network) is more or less a set of glorified reverse proxy servers that act as a first step for serving the user’s requests (based on the geographic location) also offering security protection and caching (this is what akamai or cloudflare do)</li>
</ul>
<p>As can be seen from the previous examples there are a lot of apps that do reverse proxying, for example apache <span class="caps">HTTP</span>, nginx, HAProxy, varnish cache et al.</p>
<p>Notice that while there’s only one forward proxy, there could be a (large) chain of reverse proxies when accessing a remote server.
Let’s take a look at a rather complex scenario: A user in a corporate network will access an application in another network. In this case the user’s request
may pass through:</p>
<p>forward proxy (squid) ->
security server / <span class="caps">CDN</span> (akamai) ->
ssl termination (nginx) ->
caching (varnish) ->
web server (nginx again) ->
app server (tomcat or gunicorn or <span class="caps">IIS</span> etc)
as can be seen on the following image:</p>
<img alt="Reverse proxy" src="/images/reverseproxy2.png" style="width: 800px;" />
<p>Notice that is this case (which is not uncommon) there are six (05) servers between your client and the application server!</p>
<p>One common problem with this is that unless <em>all</em> the intermediate servers are configured properly
(by properly modifying and passing the <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For">X-Forwarded-For</a> header)
you won’t be able to retrieve the <span class="caps">IP</span> of the user that did the initial request.</p>
</div>
Token Authentication for django-rest-framework2021-08-25T12:40:00+03:002021-08-25T12:40:00+03:00Serafeim Papastefanostag:spapas.github.io,2021-08-25:/2021/08/25/django-token-rest-auth/<p class="first last">How to authenticate django-rest-framework with tokens</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p>In a <a class="reference external" href="https://spapas.github.io/2018/03/01/django-rest-auth/">a previous article</a>
I explained how to authenticate for your django-rest-framework <span class="caps">API</span>
using the django-rest-auth package.
Since then I have observed that various things have changed and most importantly that
the library I used there (django-rest-auth) is not updated anymore and has been
superseded by another one. Also, some of my information there is contradictory,
especially the parts that deal with the session authentication and csrf protection.</p>
<p>Thus I’ve written this new article that betters describes a recommended
authentication workflow using tokens. This workflow does not rely on sessions at all.
Beyond that, is more or less the same as the previous one with some updates
and clarifications where needed.</p>
<p>I have also updated the accompanying project of the previous article which
can be found on <a class="reference external" href="https://github.com/spapas/rest_authenticate">https://github.com/spapas/rest_authenticate</a>.</p>
<p>Before continuing with the tutorial, let’s take a look at what we’ll build here:</p>
<img alt="Our project" src="/images/rest-auth.gif" style="width: 640px;" />
<p>This is a single html page (styled with <a class="reference external" href="https://picturepan2.github.io/spectre/">spectre.css</a>) that checks if the user is logged in
and either displays the login or logout button (using javascript). When you click the login you’ll get a modal in which you
can enter your credentials which will be submitted through <span class="caps">REST</span> to the authentication endpoint and
depending on the response will set a javascript variable (and a corresponding session/local storage key).
Then you can use the “Test auth” button that works only on authenticated users and returns their username.
Finally, notice that after you log out the “test auth” button returns a 403 access denied.</p>
<p>The javascript client uses token authentication so you can run the client in the same server as the server or
in a completely different server (if you are using the proper <span class="caps">CORS</span> headers of course).</p>
</div>
<div class="section" id="some-theory">
<h2>Some theory</h2>
<p>Here I will try to explain a bunch of important concepts:</p>
<div class="section" id="sessions">
<h3>Sessions</h3>
<p>After you log in with Django normally, your authentication information is saved to the <a class="reference external" href="https://docs.djangoproject.com/en/stable/topics/http/sessions/">session</a>.
The session is a bucket of information
that the Django application saves about your visit — to distinguish between different visitors a cookie with a unique
value named <tt class="docutils literal">sessionid</tt> will be used. So, your web browser will send this cookie with each page request thus allowing Django
to know which bucket of information is yours (and if you’ve authenticated know who are you). This is not a Django
related concept but a general one (supported by most if not all <span class="caps">HTTP</span> frameworks) and is used to add state to an otherwise
stateless medium (<span class="caps">HTTP</span>).</p>
<p>Since the <tt class="docutils literal">sessionid</tt> cookie is sent not only with traditional but also with Ajax request it can be used to authenticate
<span class="caps">REST</span> requests after you’ve logged in. This is what is used by default in django-rest-framework is a very good solution for
most use cases: You login to django and you can go ahead and call the <span class="caps">REST</span>
<span class="caps">API</span> through Ajax; the <tt class="docutils literal">sessionid</tt> cookie will be sent along with the request and you’ll be authenticated automatically.</p>
<p>Now, although the session authentication is nice for using in browsers, you may need to access your <span class="caps">API</span> through a desktop
or a mobile application where, setting the cookies yourself is not the optimal solution. Also, you may have an <span class="caps">SPA</span> that needs
to access an <span class="caps">API</span> in a different domain; using <a class="reference external" href="https://stackoverflow.com/questions/3342140/cross-domain-cookies">using cookies for this is not easy</a> - if possible at all.</p>
</div>
<div class="section" id="csrf-protection">
<h3><span class="caps">CSRF</span> protection</h3>
<p>One important thing that you should be aware if you are going to use session authentication for your <span class="caps">API</span> is the
<a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/csrf/"><span class="caps">CSRF</span> protection</a>. This is a mechanism that helps prevent cross-site request forgery (<span class="caps">CSRF</span>) attacks.
A <span class="caps">CSRF</span> attack works like this: Let’s suppose that site A is a bank, and has a form with an email and a money amount.
When the user submits the form via <span class="caps">POST</span> it will send this much money to the entered email using Paypal. Now, site B is
a malicious site. When the user visits site B, it will automatically generate a <span class="caps">POST</span> request containing the malicious
user’s email and the money he wants and submit it to site A. Now, if the user is authenticated with sessions on site A
then site A will think that this is a valid form submission and will actually process the form as normally and send the
money to the malicious user!</p>
<p>As you can understand this is a very serious and easy to exploit attack. To prevent this attack, the <span class="caps">CSRF</span> protection is
used: In order to submit the form on site A, the request must contain a unique string (the <span class="caps">CSRF</span> token) that is generated
automatically by site A. Thankfully, site B cannot access this token and thus cannot submit the form.</p>
<p>The <span class="caps">CSRF</span> situation is only related to sessions. If you are not using sessions then <span class="caps">CSRF</span> protection is not needed because
there’s no way for site B to submit the form on site A (for example, with TokenAuthentication, site B cannot access the
token that site A has).</p>
<p>However if you <em>are</em> using sessions then you must be extra careful
to protect your <span class="caps">POST</span> views against <span class="caps">CSRF</span> attacks. Django does this by default so you don’t need to do anything
fancy. However, when you actually want to submit a form using an <span class="caps">API</span> with sessions you must be careful to also
include the <span class="caps">CSRF</span> token as explained in the Django docs about the topic (<a class="reference external" href="https://docs.djangoproject.com/en/stable/ref/csrf/"><span class="caps">CSRF</span> protection</a>).</p>
</div>
<div class="section" id="tokens">
<h3>Tokens</h3>
<p>For cases where you can’t use the session to authenticate, django-rest-framework
offers a different authentication method called <tt class="docutils literal">TokenAuthentication_</tt>. Using this method, each user of the Django application
is correlated with a random string (Token) which is passed along with each request at its header thus the Django app can authenticate
the user using this token. The token is retrieved when the user logs using his credentials and is saved in the browser.</p>
<p>One thing that may seem strange is that since both the session cookie and a token are
set through <span class="caps">HTTP</span> Headers why all the fuss about tokens? Why not just use the session cookie and be done with it?
Well, there are
various reasons - here’s a <a class="reference external" href="https://auth0.com/blog/angularjs-authentication-with-cookies-vs-token/">rather extensive article</a> explaining some of them. Some of the reasons are that a token can be valid forever
while the session is something ephemeral - beyond authorization information, sessions may keep various other data for a web
application and are expired after some time to save space. Also, since tokens are used for exactly this (authentication) they
are much easier to use and reason about. Finally, as I’ve already explained, sharing cookies by multiple sites is not something
you’d like to do. Actually, to make things easier for you just follow this rule:
<strong>If your <span class="caps">API</span> will be run on a different domain than your client (i.e api.example.com and www.example.com)
or your client not run on the web (i.e. is a desktop/mobile app) then you must not use session authentication</strong>. Use token
authentication as proposed here or whatever else you may want that doesn’t rely on sessions.</p>
</div>
<div class="section" id="cors">
<h3><span class="caps">CORS</span></h3>
<p>Another thing that must concern the people that will want to use an <span class="caps">API</span> is the <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"><span class="caps">CORS</span></a> situation.
By default cross-origin requests are not allowed, i.e site B cannot issue Ajax requests to site A.
Each server can be configured to allow cross-origin requests from other servers. This means that
if you have a server api.example.com that is used as a backend and a server www.example.com that will
serve your front-end, you can configure api.example.com to allow requests only from www.example.com.</p>
<p>By default Django does not allow any cross origin requests and you need to use the <a class="reference external" href="https://github.com/adamchainz/django-cors-headers">django-cors-headers</a>
package to properly configure it.</p>
<p>Notice that <span class="caps">CORS</span> protection is enforced by the Browser. For example if you have build a mobile app
and are consuming an <span class="caps">API</span> in api.example.com then <span class="caps">CORS</span> protection does not apply to your http client.</p>
</div>
</div>
<div class="section" id="installation-configuration">
<h2>Installation <span class="amp">&</span> configuration</h2>
<p>The project will use <a class="reference external" href="http://www.django-rest-framework.org">django-rest-framework</a>, <a class="reference external" href="https://github.com/iMerica/dj-rest-auth">dj-rest-auth</a> and <a class="reference external" href="https://github.com/adamchainz/django-cors-headers">django-cors-headers</a>.</p>
<p>To install django-rest-framework and dj-rest-auth just follow <a class="reference external" href="https://dj-rest-auth.readthedocs.io/en/latest/installation.html">the instructions here</a> i.e just add
<tt class="docutils literal">'rest_framework', 'rest_framework.authtoken'</tt> and <tt class="docutils literal">'dj_rest_auth'</tt> to your <cite>INSTALLED_APPS</cite> in
<tt class="docutils literal">settings.py</tt> and run migrate.</p>
<p>To install django-cors-headers follow the <a class="reference external" href="https://github.com/adamchainz/django-cors-headers#setup">the setup instructions</a>: Add <tt class="docutils literal">"corsheaders"</tt> to your <tt class="docutils literal">INSTALLED_APPS</tt> and
<tt class="docutils literal">"django.middleware.common.CommonMiddleware"</tt> to your <tt class="docutils literal"><span class="caps">MIDDLEWARE</span></tt> in <tt class="docutils literal">settings.py</tt>. Then you can use the
<tt class="docutils literal">CORS_ALLOWED_ORIGINS</tt> setting to configure which origins are allowed to make requests to your project. Let’s
suppose that you are running your project at 127.0.0.1:8000 and you want to allow requests from a client
running at 127.0.0.1:8001. You can do this by adding the following to your settings.py:
<tt class="docutils literal">CORS_ALLOWED_ORIGINS = <span class="pre">['http://127.0.0.1:8001',</span> <span class="pre">'http://localhost:8001']</span></tt>. Actually, try running the project
with and without that setting and see how the javascript client behaves.</p>
<p>Since I won’t be adding any other apps to this project (no models are actually needed), I’ve added
two directories <tt class="docutils literal">static</tt> and <tt class="docutils literal">templates</tt> to put static files and templates there. This is configured
by adding the <tt class="docutils literal">'<span class="caps">DIRS</span>'</tt> attribte to <tt class="docutils literal"><span class="caps">TEMPLATES</span></tt>, like this:</p>
<div class="highlight"><pre><span></span><span class="n">TEMPLATES</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="s1">'BACKEND'</span><span class="p">:</span> <span class="s1">'django.template.backends.django.DjangoTemplates'</span><span class="p">,</span>
<span class="s1">'DIRS'</span><span class="p">:</span> <span class="p">[</span>
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s1">'templates'</span><span class="p">),</span>
<span class="p">],</span>
<span class="o">//</span> <span class="o">...</span>
</pre></div>
<p>and adding the <cite>STATICFILES_DIRS</cite> setting:</p>
<div class="highlight"><pre><span></span><span class="n">STATICFILES_DIRS</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s2">"static"</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>The remaining setting are the default as were created by <tt class="docutils literal"><span class="pre">django-admin</span> startproject</tt>.</p>
</div>
<div class="section" id="urls">
<h2>Urls</h2>
<p>I have included the the following urls to <tt class="docutils literal">urls.py</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'test_auth/'</span><span class="p">,</span> <span class="n">TestAuthView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'test_auth'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'rest-auth/logout/'</span><span class="p">,</span> <span class="n">LogoutViewEx</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'rest_logout'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'rest-auth/login/'</span><span class="p">,</span> <span class="n">LoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'rest_login'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="n">HomeTemplateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'home'</span><span class="p">,</span> <span class="p">),</span>
<span class="p">]</span> <span class="o">+</span> <span class="n">static</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">STATIC_URL</span><span class="p">,</span> <span class="n">document_root</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">STATIC_ROOT</span><span class="p">)</span>
</pre></div>
<p>These are: The django-admin, a <tt class="docutils literal">test_auth</tt> view (that works only for authenticated users and returns their username),
a view (<tt class="docutils literal">LogoutViewEx</tt>) that overrides the rest-auth <span class="caps">REST</span> logout-view (I’ll explain why this is needed in a minute),
the rest-auth <span class="caps">REST</span> login-view, the home template view (which is the only view implemented) and finally a mapping
of your static files to the <tt class="docutils literal">STATIC_URL</tt>.</p>
<p>The <tt class="docutils literal">LoginView</tt> is the default provided by the dj-rest-auth project. One thing to consider is that
this view will check if the credentials you pass are valid and return a valid token for your user. However,
it will also optionally login the user using sessions (i.e create a new session and return a sessionid cookie). This
is configured by the <tt class="docutils literal">REST_SESSION_LOGIN</tt> option which by default is <tt class="docutils literal">True</tt>.</p>
<p>To test this functionality, try logging in using this login view with a superuser and then visit the django-admin. You
will see that you are already logged in. Now, logout and add (or change) <tt class="docutils literal">REST_SESSION_LOGIN=False</tt> to your settings.py.
Login again from the rest view and now if you visit the django-admin you should see that you need to login again.</p>
<p>Another way to test this is by checking out the response headers of the <tt class="docutils literal"><span class="caps">POST</span></tt> to <tt class="docutils literal"><span class="pre">rest-auth/login/</span></tt> from your
browser’s development tools. When you are using <tt class="docutils literal">REST_SESSION_LOGIN=True</tt> (or you haven’t defined it since by
default it is true) you’ll see the following <tt class="docutils literal"><span class="pre">Set-Cookie</span></tt> line:</p>
<pre class="code literal-block">
sessionid=pw8rp7l7yy33lk7geuxbczaleh35w9je; expires=Wed, 08 Sep 2021 08:29:40 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
</pre>
<p>This cookie won’t be set if you login again with <tt class="docutils literal">REST_SESSION_LOGIN=False</tt>.</p>
</div>
<div class="section" id="the-views">
<h2>The views</h2>
<p>I’ve defined three views in this application - the <tt class="docutils literal">HomeTemplateView</tt>, the <tt class="docutils literal">TestAuthView</tt>
and the <tt class="docutils literal">LogoutViewEx</tt> view that overrides the normal <tt class="docutils literal">LogoutView</tt> of <tt class="docutils literal"><span class="pre">django-rest-auth</span></tt>.</p>
<div class="section" id="hometemplateview">
<h3>HomeTemplateView</h3>
<p>The <tt class="docutils literal">HomeTemplateView</tt> is
a simple <tt class="docutils literal">TemplateView</tt> that just
displays an html page and loads the client side code - we’ll talk about it later in the front-side section.
This is more or less similar (without the django-stuff) with the standalone client page that can be found on
<tt class="docutils literal">client/index.html</tt>.</p>
</div>
<div class="section" id="testauthview">
<h3>TestAuthView</h3>
<p>The <tt class="docutils literal">TestAuthView</tt> is implemented like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">TestAuthView</span><span class="p">(</span><span class="n">APIView</span><span class="p">):</span>
<span class="n">authentication_classes</span> <span class="o">=</span> <span class="p">(</span><span class="n">authentication</span><span class="o">.</span><span class="n">TokenAuthentication</span><span class="p">,)</span>
<span class="n">permission_classes</span> <span class="o">=</span> <span class="p">(</span><span class="n">permissions</span><span class="o">.</span><span class="n">IsAuthenticated</span><span class="p">,)</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">return</span> <span class="n">Response</span><span class="p">(</span><span class="s2">"Hello </span><span class="si">{0}</span><span class="s2">!"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">post</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">return</span> <span class="n">Response</span><span class="p">(</span><span class="s2">"Hello </span><span class="si">{0}</span><span class="s2">! Posted!"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">))</span>
</pre></div>
<p>This is very simple however I’d like to make a few comments about the above. First of all you see that
I’ve defined both a <tt class="docutils literal">get</tt> and a <tt class="docutils literal">post</tt> method. When you use the token authentication you’ll see that the
<tt class="docutils literal">post</tt> method will work without the need to provide a csrf token as already discussed before.</p>
</div>
<div class="section" id="authentication-and-permission">
<h3>Authentication and permission</h3>
<p>Notice that both <tt class="docutils literal">authentication_classes</tt> and <tt class="docutils literal">permission_classes</tt> are included in the <tt class="docutils literal">TestAuthView</tt>. These options define:</p>
<ul class="simple">
<li>which method will be used for authenticating access to the <span class="caps">REST</span> view i.e finding out if the user
requesting access has logged in and if yes what’s his username (in our case only <tt class="docutils literal">TokenAuthentication</tt> will be used)</li>
<li>if the user is authorized (has permission) to call this <span class="caps">REST</span> view (in our case only authenticated users will be allowed)</li>
</ul>
<p>The authentication and permission classes can be set globally
in your <tt class="docutils literal">settings.py</tt> using <tt class="docutils literal"><span class="pre">REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES']</span></tt> and
<tt class="docutils literal"><span class="pre">REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES']</span></tt>
or defined per-class like this. If I wanted to have the same authentication and permission classes defined
in my <tt class="docutils literal">settings.py</tt> so I wouldn’t need to set these options per-class I’d add the following to my <tt class="docutils literal">settings.py</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">REST_FRAMEWORK</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'DEFAULT_AUTHENTICATION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.authentication.TokenAuthentication'</span><span class="p">,</span>
<span class="p">),</span>
<span class="s1">'DEFAULT_PERMISSION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.permissions.IsAuthenticated'</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">}</span>
</pre></div>
<p>Please keep in mind that you haven’t defined these in your views or your settings, they will have the
following <a class="reference external" href="http://www.django-rest-framework.org/api-guide/settings/#default_authentication_classes">default</a> <a class="reference external" href="http://www.django-rest-framework.org/api-guide/settings/#default_permission_classes">values</a>:</p>
<div class="highlight"><pre><span></span><span class="n">REST_FRAMEWORK</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'DEFAULT_AUTHENTICATION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.authentication.SessionAuthentication'</span><span class="p">,</span>
<span class="s1">'rest_framework.authentication.BasicAuthentication'</span>
<span class="p">),</span>
<span class="s1">'DEFAULT_PERMISSION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.permissions.AllowAny'</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">}</span>
</pre></div>
<p>The above mean that if you don’t define authentication and permission classes anywhere then the <span class="caps">REST</span>
views will use either session authentication (i.e the user has logged in normally using
the Django login views as explained before) or <span class="caps">HTTP</span> basic authentication
(the request provides the credentials in the header using traditional <span class="caps">HTTP</span> Basic authentication)
and also that all users (logged in or not) will be allowed to call all APIs (this is
probably not something you want).</p>
</div>
<div class="section" id="tokens-1">
<h3>Tokens</h3>
<p>The <tt class="docutils literal">TokenAuthentication</tt> that we are using for the <tt class="docutils literal">TestAuthView</tt>
means that for every request a valid token must be passed (there’s no concept of state
in <span class="caps">HTTP</span> so you need to pass it whenever you communicate with the server).</p>
<p>The tokens are normal object instances of <tt class="docutils literal">rest_framework.authtoken.models.Token</tt>
and you can take a look at them (or even add one) through the Django admin (auth token - tokens). You can also
even do whatever you normally would do to an object instance, for example:</p>
<div class="highlight"><pre><span></span><span class="o">>>></span> <span class="p">[</span> <span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">user</span><span class="p">,</span> <span class="n">x</span><span class="o">.</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">Token</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()]</span>
<span class="p">[(</span><span class="o"><</span><span class="n">User</span><span class="p">:</span> <span class="n">root</span><span class="o">></span><span class="p">,</span> <span class="s1">'db4dcc1b9d00d1af74fb3cb41e1f9e673208485b'</span><span class="p">)]</span>
</pre></div>
<p>To authenticate with a token (using <a class="reference external" href="http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication">TokenAuthentication</a>), you must add an extra header to your request with the format
<tt class="docutils literal">Authorization: Token token</tt> for example in the previous case <tt class="docutils literal">root</tt> would add
<tt class="docutils literal">Authorization: Token db4dcc1b9d00d1af74fb3cb41e1f9e673208485b</tt>. To do this you’ll need something
client-side code which we’ll see in the next section.</p>
<p>To debug your authentication with <a class="reference external" href="https://curl.haxx.se">curl</a> you can just do something like this:</p>
<div class="highlight"><pre><span></span>curl<span class="w"> </span>http://127.0.0.1:8000/test_auth/<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization:Token db4dcc1b9d00d1af74fb3cb41e1f9e673208485b"</span>
</pre></div>
<p>Try it with a valid and invalid token and without providing a token at all and see the response each time.</p>
</div>
<div class="section" id="dj-rest-auth">
<h3>dj-rest-auth</h3>
<p>So, django-rest-framework provides the model (Token) and the mechanism (add the extra Authentication header) for
authentication with Tokens. What it does not provide is a simple way to create/remove tokens for users: This
is where the dj-rest-auth project comes to the rescue! Its login and logout <span class="caps">REST</span> views will automatically
create (and delete) tokens for the users that are logging in.</p>
<p>As already described above, the login view will also authenticate the user
using the session when the REST_SESSION_LOGIN is set to True (default) - this means that if a user
logs in using the login <span class="caps">REST</span> endpoint he’ll then
be logged in normally to the site and be able to access non-<span class="caps">REST</span> parts of the site (for example the django-admin).</p>
<p>Also, if the user logs in through the dj-rest-auth <span class="caps">REST</span> end point and if you have are using <tt class="docutils literal">SessionAuthentication</tt>
to one of your views then he’ll be able to authenticate to these views <em>without</em> the need to pass the token (make sure
you understand why).</p>
</div>
<div class="section" id="logoutviewex">
<h3>LogoutViewEx</h3>
<p>Finally, let’s take a look at the <tt class="docutils literal">LogoutViewEx</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">LogoutViewEx</span><span class="p">(</span><span class="n">LogoutView</span><span class="p">):</span>
<span class="n">authentication_classes</span> <span class="o">=</span> <span class="p">(</span><span class="n">authentication</span><span class="o">.</span><span class="n">TokenAuthentication</span><span class="p">,)</span>
</pre></div>
<p>This class only defines the authentication_classes attribute. Is this really needed? Well, it depends on
you project. If you take a look at the source code of
<tt class="docutils literal">LogoutView</tt> (<a class="reference external" href="https://github.com/iMerica/dj-rest-auth/blob/master/dj_rest_auth/views.py#L131">https://github.com/iMerica/dj-rest-auth/blob/master/dj_rest_auth/views.py#L131</a>)
you’ll see that it does not define <tt class="docutils literal">authentication_classes</tt>. This, as we’ve already discussed, means that it will
fall-back to whatever you have defined in the settings (or the defaults of django-rest-framework).</p>
<p>So, if you haven’t
defined anything in the settings then you’ll get the by default the
<tt class="docutils literal">SessionAuthentication</tt> and <tt class="docutils literal">BasicAuthentication</tt> methods (hint: <em>not</em> the <tt class="docutils literal">TokenAuthentication</tt>).
This means that you won’t be able to
logout when you pass the token (but <em>will</em> be able to logout from the web-app after you login - why?). So to make everything
crystal and be able to reason better about the behavior I specifically define the <tt class="docutils literal">LogoutViewEx</tt> to use
the <tt class="docutils literal">TokenAuthentication</tt> to properly log out your user. This of course means that you need to pass
the token to your logout view also or else there won’t be any way to associate the request with a user to log out.</p>
</div>
</div>
<div class="section" id="the-client-side-scripts">
<h2>The client side scripts</h2>
<p>I’ve included all client-side code to a <tt class="docutils literal">home.html</tt> template that is loaded
from the <tt class="docutils literal">HomeTemplateView</tt>. Also, the same code has been included in <tt class="docutils literal">client/index.html</tt>. This is
a completely standalone javascript client that you can run in a different http server than your Django server,
for example by running <tt class="docutils literal">py <span class="pre">-3</span> <span class="pre">-m</span> http.server 8001</tt> from the client folder and visiting <a class="reference external" href="http://127.0.0.1:8001">http://127.0.0.1:8001</a>.</p>
<p>The client-side code has been implemented only with jQuery because I think
this is the library that most people are familiar with - and is really easy to be understood even if you
are not familiar with it. It more or less consists of five sections in html:</p>
<ul class="simple">
<li>A user-is-logged-in section that displays the username and the logout button</li>
<li>A user-is-not-logged-in section that displays a message and the login button</li>
<li>A test-auth section that displays a button for calling the <tt class="docutils literal">TestAuthView</tt> with <span class="caps">GET</span> defined previously and outputs its response</li>
<li>A test-auth <span class="caps">POST</span> section that displays a button for calling the <tt class="docutils literal">TestAuthView</tt> with <span class="caps">POST</span> defined previously and outputs its response</li>
<li>The login modal</li>
</ul>
<p>Here’s the html (using spectre.css for styling):</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"container grid-lg"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>Test<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"non-logged-in"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
You have to log-in!
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'loginButton'</span><span class="p">></span>Login<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"logged-in"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
Welcome <span class="p"><</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">'span-username'</span><span class="p">></</span><span class="nt">span</span><span class="p">></span>!
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'logoutButton'</span><span class="p">></span>Logout<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"test"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'testAuthButton'</span><span class="p">></span>Test auth<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-9'</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'test-auth-response'</span> <span class="p">></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"test"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'testAuthPostButton'</span><span class="p">></span>Test auth (POST)<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-9'</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'test-auth-post-response'</span> <span class="p">></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal"</span> <span class="na">id</span><span class="o">=</span><span class="s">"login-modal"</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#close"</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-overlay close-modal"</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">"Close"</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-container"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-header"</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#close"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-clear float-right close-modal"</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">"Close"</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-title h5"</span><span class="p">></span>Please login<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-body"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"content"</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-label"</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-username"</span><span class="p">></span>Username<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-input"</span> <span class="na">type</span><span class="o">=</span><span class="s">"text"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-username"</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">"Name"</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-label"</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-password"</span><span class="p">></span>Password<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-input"</span> <span class="na">type</span><span class="o">=</span><span class="s">"password"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-password"</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">"Password"</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-checkbox"</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-local-storage"</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"checkbox"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-local-storage"</span> <span class="p">/></span> <span class="p"><</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-icon"</span><span class="p">></</span><span class="nt">i</span><span class="p">></span> Use local storage (remember me)
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'label label-error mt-1 d-invisible'</span> <span class="na">id</span><span class="o">=</span><span class="s">'modal-error'</span><span class="p">></span>
Unable to login!
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-footer"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'loginOkButton'</span> <span class="p">></span>Ok<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#close"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn close-modal"</span> <span class="p">></span>Close<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>The html is very simple and I don’t think I need to explain much - notice that the <tt class="docutils literal"><span class="pre">#logged-in</span></tt> and <tt class="docutils literal"><span class="pre">#non-logged-in</span></tt>
sections are mutually exclusive (I use <tt class="docutils literal">$.show()</tt> and <tt class="docutils literal">$.hide()</tt> to show and hide them) but the <tt class="docutils literal">#test</tt> section is always displayed
so you’ll be able to call the test <span class="caps">REST</span> <span class="caps">API</span> when you are and are not authenticated. For the modal
to be displayed you need to add an <tt class="docutils literal">active</tt> class to its <tt class="docutils literal">#modal</tt> container.</p>
<p>For the javascript, let’s take a look at some initialization stuff:</p>
<div class="highlight"><pre><span></span><span class="kd">var</span><span class="w"> </span><span class="nx">g_urls</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'login'</span><span class="o">:</span><span class="w"> </span><span class="s1">'{% url "rest_login" %}'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'logout'</span><span class="o">:</span><span class="w"> </span><span class="s1">'{% url "rest_logout" %}'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'test_auth'</span><span class="o">:</span><span class="w"> </span><span class="s1">'{% url "test_auth" %}'</span><span class="p">,</span>
<span class="p">};</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">initLogin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#non-logged-in'</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#logged-in'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#span-username'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">username</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">remember_me</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">,</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">,</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#non-logged-in'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#logged-in'</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#span-username'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s1">''</span><span class="p">);</span>
<span class="w"> </span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="w"> </span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">""</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-post-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">""</span><span class="p">);</span>
<span class="p">};</span>
</pre></div>
<p>First of all, I define a <tt class="docutils literal">g_urls</tt> window/global object that will keep the required <span class="caps">REST</span> <span class="caps">URLS</span> (login/logout and test auth). These
are retrieved from Django using the <tt class="docutils literal">{% url %}</tt> template tag and are not hard-coded (in the js only client they are hard-coded of course).
After that, I check to see if the user has authenticated before. Notice that because
this is client-side code, I need to do that every time the page loads or else the <span class="caps">JS</span> won’t be initialized properly! The user login
information is stored to an object named <tt class="docutils literal">g_auth</tt> and contains three attributes: <tt class="docutils literal">username</tt>, <tt class="docutils literal">key</tt> (token) and <tt class="docutils literal">remember_me</tt>.</p>
<p>To keep the login information I use either a key named <tt class="docutils literal">auth</tt> to either the <tt class="docutils literal">localStorage</tt> or the <tt class="docutils literal">sessionStorage</tt>. The <tt class="docutils literal">sessionStorage</tt> is used to save
info for the current browser tab (<em>not</em> window) while the <tt class="docutils literal">localStorage</tt> saves info for ever (until somebody deletes it). Thus,
<tt class="docutils literal">localStorage</tt> can be used for implementing a “remember me” functionality.</p>
<p>The final function we define here, <tt class="docutils literal">initLogin</tt> (which is called a little later) checks to see if there is login information
and hides/displays the correct things in html. It will also set the local or session storage (depending on remember me value).</p>
<p>After that, we have some client side code that is inside the <tt class="docutils literal">$()</tt> function which will be called after the page has completely loaded:</p>
<div class="highlight"><pre><span></span><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">initLogin</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#loginButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#login-modal'</span><span class="p">).</span><span class="nx">addClass</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'.close-modal'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#login-modal'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#testAuthButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="nx">g_urls</span><span class="p">.</span><span class="nx">test_auth</span><span class="p">,</span>
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">"GET"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">beforeSend</span><span class="o">:</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Token "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">"<span class='label label-success'>Ok! Response: "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">"<span class='label label-error'>Fail! Response: "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">responseText</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" (status: "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span><span class="o">+</span><span class="s2">")</span>"</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#testAuthPostButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Same as with the GET</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="c1">// continuing below ...</span>
</pre></div>
<p>The first thing happening here is to call the <tt class="docutils literal">initLogin</tt> function to properly initialize the page and then we add a couple of
handlers to the click buttons of the <tt class="docutils literal">#loginButton</tt> (which just displays the modal by adding the <tt class="docutils literal">active</tt> class ),
<tt class="docutils literal"><span class="pre">.close-modal</span></tt> class (there are multiple
ways to close the modal thus I use a class which just removes that <tt class="docutils literal">active</tt> class) and finally to the <tt class="docutils literal">#testAuthButton</tt>
and <tt class="docutils literal">#testAuthPostButton#</tt>.
These
button will do a <tt class="docutils literal"><span class="caps">GET</span></tt> and <tt class="docutils literal"><span class="caps">POST</span></tt> request to the <tt class="docutils literal">g_urls.test_auth</tt> we defined before. The important thing to notice
here is that we add
a <tt class="docutils literal">beforeSend</tt> attribute to the <tt class="docutils literal">$.ajax</tt> request which, if <tt class="docutils literal">g_auth</tt> is defined, adds an <tt class="docutils literal">Authorization</tt> header with the token
in the form that django-rest-framework <tt class="docutils literal">TokenAuthentication</tt> expects and as we’ve already discussed above:</p>
<div class="highlight"><pre><span></span><span class="nx">beforeSend</span><span class="o">:</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Token "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>If this ajax call returns without errors (the <tt class="docutils literal">done</tt> part of the ajax call)
we just add the <tt class="docutils literal">data</tt> to a green label else if there’s an error (<tt class="docutils literal">fail</tt> part)
we add the response text and status to a red label. You can try clicking the buttons and you see that only if you’ve logged in
you will succeed in this call. Also, notice that both <span class="caps">GET</span> and <span class="caps">POST</span> requests work normally without the need to also include
a csrf token (I hope you understand why by now).</p>
<p>Let’s now take a look at the <tt class="docutils literal">#loginOkbutton</tt> click handler (inside the modal):</p>
<div class="highlight"><pre><span></span><span class="nx">$</span><span class="p">(</span><span class="s1">'#loginOkButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#input-username'</span><span class="p">).</span><span class="nx">val</span><span class="p">();</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">password</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#input-password'</span><span class="p">).</span><span class="nx">val</span><span class="p">();</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">remember_me</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#input-local-storage'</span><span class="p">).</span><span class="nx">prop</span><span class="p">(</span><span class="s1">'checked'</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">username</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">password</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Will try to login with "</span><span class="p">,</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span><span class="w"> </span><span class="nx">password</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#modal-error'</span><span class="p">).</span><span class="nx">addClass</span><span class="p">(</span><span class="s1">'d-invisible'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="nx">g_urls</span><span class="p">.</span><span class="nx">login</span><span class="p">,</span>
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">"POST"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">username</span><span class="o">:</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span>
<span class="w"> </span><span class="nx">password</span><span class="o">:</span><span class="w"> </span><span class="nx">password</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"DONE: "</span><span class="p">,</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">username</span><span class="o">:</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span>
<span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">key</span><span class="p">,</span>
<span class="w"> </span><span class="nx">remember_me</span><span class="o">:</span><span class="w"> </span><span class="nx">remember_me</span>
<span class="w"> </span><span class="p">};</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#login-modal'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">initLogin</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"FAIL"</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#modal-error'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'d-invisible'</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#modal-error'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'d-invisible'</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">});</span>
</pre></div>
<p>All three user inputs (<tt class="docutils literal">username, password, remember_me</tt>) are read from the form and if both username and
password have been defined an Ajax request will be done to the <tt class="docutils literal">g_urls.login</tt> url. We pass
<tt class="docutils literal">username</tt> and <tt class="docutils literal">password</tt> as the request data. Now, if there’s an
error (<tt class="docutils literal">fail</tt>) I just display a generic message (by removing it’s <cite>d-invisible</cite> class) while, if the
request was Ok I retrieve the <tt class="docutils literal">key</tt> (token) from the response, initialize the <tt class="docutils literal">g_auth</tt> object with the
<tt class="docutils literal">username</tt>, <tt class="docutils literal">key</tt> and <tt class="docutils literal">remember_me</tt> values and call <tt class="docutils literal">initLogin</tt> to show the correct divs and save
to the session/local storage.</p>
<p>Finally, here’s the code for logout (still inside the <tt class="docutils literal">$(function () {</tt>):</p>
<div class="highlight"><pre><span></span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#logoutButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Trying to logout"</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="nx">g_urls</span><span class="p">.</span><span class="nx">logout</span><span class="p">,</span>
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">"POST"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">beforeSend</span><span class="o">:</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Token "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"DONE: "</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="w"> </span><span class="nx">initLogin</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"FAIL: "</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">});</span><span class="w"> </span><span class="c1">// End of $(function () {</span>
</pre></div>
<p>The code here is very simple - just do a <tt class="docutils literal"><span class="caps">POST</span></tt> to the <tt class="docutils literal">g_urls.logout</tt> and if everything is ok delete the <tt class="docutils literal">g_auth</tt> values
and call <tt class="docutils literal">initLogin()</tt> to show the correct divs and remove the <tt class="docutils literal">auth</tt> key from local/session storage. Notice that when
you <tt class="docutils literal"><span class="caps">POST</span></tt> to the <tt class="docutils literal">logout</tt> <span class="caps">REST</span> end-point, you need to also add the <tt class="docutils literal">Authorization</tt> header with the token or else
(since we’ve defined only <tt class="docutils literal">TokenAuthentication</tt> for the <tt class="docutils literal">authentication_classes</tt> for the <tt class="docutils literal">LogoutViewEx</tt> class)
there won’t be any way to correlate the request with the user and log him out!</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Using the info presented on this article you should be able to properly login and logout to Django using <span class="caps">REST</span> and
also call <span class="caps">REST</span> end-points using the <tt class="docutils literal">TokenAuthentication</tt>.</p>
<p>I recommend using the <tt class="docutils literal">curl</tt> utility to try to call the rest
end point with various parameters to see the response. Also, you change the <tt class="docutils literal">LogoutViewEx</tt> with the
default django-rest-auth <tt class="docutils literal">LogoutView</tt> and then try logging out through the web-app <em>and</em> through curl and see
what happens when you try to access the test-auth end-point.</p>
<p>As a final remark, a couple of thing to note:</p>
<ul class="simple">
<li>You can use the <a class="reference external" href="https://github.com/James1345/django-rest-knox">django-rest-knox</a> package to improve the functionality and security of your <span class="caps">REST</span> tokens (by allowing multiple tokens per user, storing them hashed in the database and configuring expiration times for the tokens)</li>
<li>If you are using Apache and mod_wsgi to run you Django project you need to set the <a class="reference external" href="https://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIPassAuthorization.html">WSGIPassAuthorization</a> option to <tt class="docutils literal">on</tt> in order to pass the Authorization header to your Django app.</li>
</ul>
</div>
Changing choices to a ForeignKey using Django migrations2021-07-15T09:50:00+03:002021-07-15T09:50:00+03:00Serafeim Papastefanostag:spapas.github.io,2021-07-15:/2021/07/15/django-choices-to-fk-using-migrations/<p class="first last">How to convert a choices field to a ForeignKey using Django migrations</p>
<p>One common requirement I’ve seen in projects is that a model will start with a
choices CharField but in the future this field will need to be converted to a normal
foreign key to another model. This is such a common requirement that I’ve concluded that
you need to double think before using choices because there’s a high possibility that in the
lifetime of your project you’ll also need to convert it to a foreign key.</p>
<p>For example, let’s suppose you’ve got the following model:</p>
<div class="highlight"><pre><span></span><span class="n">CATEGORY_CHOICES</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s1">'cat1'</span><span class="p">,</span> <span class="s1">'Category 1 name'</span><span class="p">,),</span>
<span class="p">(</span><span class="s1">'cat2'</span><span class="p">,</span> <span class="s1">'Category 2 name'</span><span class="p">,),</span>
<span class="p">(</span><span class="s1">'cat3'</span><span class="p">,</span> <span class="s1">'Category 3 name'</span><span class="p">,),</span>
<span class="p">(</span><span class="s1">'cat4'</span><span class="p">,</span> <span class="s1">'Category 4 name'</span><span class="p">,),</span>
<span class="p">]</span>
<span class="k">class</span> <span class="nc">Sample</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
<span class="n">category</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">CATEGORY_CHOICES</span><span class="p">)</span>
</pre></div>
<p>You will need to convert it like this</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Category</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span>
<span class="k">class</span> <span class="nc">Sample</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
<span class="n">category</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'Category'</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">)</span>
</pre></div>
<p>There are various reasons that you may be forced to convert the choices field to a ForeignKey, some are:</p>
<ul class="simple">
<li>Your site administrators may need to sometime change these choices themselves</li>
<li>You may want to add some properties to each choice i.e if a choice is active or not</li>
<li>The choices info is local in your django project. If for some reason you want your data to be used by a different project (for example execute a reporing query directly from the database) you’ll just get the code for each choice (and not its name) leading you to ugly case statements in your queries to display the name of each choice. Furthermore, if the choices do change you’ll need to change them in two places (your django project and your reporting queries)</li>
<li>The choice thing, although is very helpful and quick to implement leads to a non-normalized design. The name of each choice will be a string that would be duplicated to each row that has that particular choice.</li>
</ul>
<p>In a <a class="reference external" href="https://spapas.github.io/2017/07/04/postgresql-auto-create-category-column/">previous article</a> I had provided a recipe on how to
properly normalize a database table containing a choices field like this using <span class="caps">PL</span>/pgSQL. This script should work in
this case also but if you have a Django project then you <em>should</em> use migrations to do the conversion.</p>
<p>So let’s see how to convert our category choices field to a Foreign Key using django migrations!</p>
<p>The proper way to do it is in three distinct steps/migrations:</p>
<ol class="arabic simple">
<li>Create the <tt class="docutils literal">Category</tt> model and add a foreign key to it in the <tt class="docutils literal">Sample</tt> model. <em>You should not remove the existing choices field</em>! So you’ll need to add another field to <tt class="docutils literal">Sample</tt> for example named <tt class="docutils literal">category_fk</tt>.</li>
<li>Create a data migration to run a python script that will read the existing <tt class="docutils literal">Sample</tt> instances and fill their <tt class="docutils literal">category_fk</tt> field based on their <tt class="docutils literal">category</tt> field.</li>
<li>Remove the <tt class="docutils literal">category</tt> field from <tt class="docutils literal">Sample</tt> model and rename <tt class="docutils literal">category_fk</tt> to <tt class="docutils literal">category</tt>.</li>
</ol>
<p>Let’s go through the steps one by one:</p>
<p>First we will change our initial models.py like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Category</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Sample</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
<span class="n">category</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">CATEGORY_CHOICES</span><span class="p">)</span>
<span class="n">category_fk</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'Category'</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre></div>
<p>So I’ve just added the <tt class="docutils literal">Category</tt> model and the <tt class="docutils literal">category_fk</tt> field to the <tt class="docutils literal">Sample</tt> model. Notice the <tt class="docutils literal">category</tt> choices field is still there since I need it to fill my <tt class="docutils literal">category_fk</tt>!
Also notice that I’ve added a <tt class="docutils literal">null=True</tt> to the <tt class="docutils literal">category_fk</tt> so it will allow the field to be added with a null value to the existing. I will fix that later.
We can create and run an automatic migration now:</p>
<pre class="code literal-block">
C:\progr\py3\migrations_tutorial>python manage.py makemigrations
Migrations for 'core':
core\migrations\0002_auto_20210715_0836.py
- Create model Category
- Add field category_fk to sample
C:\progr\py3\migrations_tutorial>python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
Applying core.0002_auto_20210715_0836... OK
</pre>
<p>So now all my rows have an empty <tt class="docutils literal">category_fk</tt> field.</p>
<p>For the second step, we will create the data migration that will fill the category_fk field. First of all let’s create an empty migration (notice my app is called <tt class="docutils literal">core</tt>):</p>
<pre class="code literal-block">
C:\progr\py3\migrations_tutorial>python manage.py makemigrations --empty core
Migrations for 'core':
core\migrations\0003_auto_20210715_0844.py
</pre>
<p>Let’s take a look at what Django has created for us:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">migrations</span>
<span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s1">'core'</span><span class="p">,</span> <span class="s1">'0002_auto_20210715_0836'</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">]</span>
</pre></div>
<p>This is an empty migration file, it just says that it will be run after the previous migration we just created. We’ll need to
add an operation to it that will do the needed work of filling the <tt class="docutils literal">category_fk</tt> field.</p>
<p>This can be done like this:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">migrations</span>
<span class="k">def</span> <span class="nf">fill_category_fk</span><span class="p">(</span><span class="n">apps</span><span class="p">,</span> <span class="n">schema_editor</span><span class="p">):</span>
<span class="n">Sample</span> <span class="o">=</span> <span class="n">apps</span><span class="o">.</span><span class="n">get_model</span><span class="p">(</span><span class="s1">'core'</span><span class="p">,</span> <span class="s1">'Sample'</span><span class="p">)</span>
<span class="n">Category</span> <span class="o">=</span> <span class="n">apps</span><span class="o">.</span><span class="n">get_model</span><span class="p">(</span><span class="s1">'core'</span><span class="p">,</span> <span class="s1">'Category'</span><span class="p">)</span>
<span class="k">for</span> <span class="n">sample</span> <span class="ow">in</span> <span class="n">Sample</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">():</span>
<span class="n">sample</span><span class="o">.</span><span class="n">category_fk</span><span class="p">,</span> <span class="n">created</span> <span class="o">=</span> <span class="n">Category</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get_or_create</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">sample</span><span class="o">.</span><span class="n">category</span><span class="p">)</span>
<span class="n">sample</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s1">'core'</span><span class="p">,</span> <span class="s1">'0002_auto_20210715_0836'</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RunPython</span><span class="p">(</span><span class="n">fill_category_fk</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>The above should be straight forward. The only thing to notice is that you should use <tt class="docutils literal">migrations.RunPython</tt> to declare that
the migration will need to run some python code. Notice that <tt class="docutils literal">RunPython</tt> takes a second parameter with another function which
will be run during the backwards migration. In our case we don’t really need it, since we omit it, it will throw an error if
you try to apply this migration backwards.</p>
<p>The <tt class="docutils literal">fill_category_fk</tt> uses the <tt class="docutils literal">apps.get_model</tt> function to have access to the models it needs. You should use this instead
of importing the models directly because the current state of the database models may not be the same as the state that the
migration expects. I’m just using <tt class="docutils literal">get_or_create</tt> to insert or retrieve the proper <tt class="docutils literal">Category</tt> instance (remember that
<tt class="docutils literal">get_or_create</tt> returns an (instance, created) tuple so we need to use the first element).</p>
<p>Now we can try running the migration:</p>
<div class="highlight"><pre><span></span><span class="n">C</span><span class="p">:</span>\<span class="n">progr</span>\<span class="n">py3</span>\<span class="n">migrations_tutorial</span><span class="o">></span><span class="n">python</span> <span class="n">manage</span><span class="o">.</span><span class="n">py</span> <span class="n">migrate</span>
<span class="n">Operations</span> <span class="n">to</span> <span class="n">perform</span><span class="p">:</span>
<span class="n">Apply</span> <span class="nb">all</span> <span class="n">migrations</span><span class="p">:</span> <span class="n">admin</span><span class="p">,</span> <span class="n">auth</span><span class="p">,</span> <span class="n">contenttypes</span><span class="p">,</span> <span class="n">core</span><span class="p">,</span> <span class="n">sessions</span>
<span class="n">Running</span> <span class="n">migrations</span><span class="p">:</span>
<span class="n">Applying</span> <span class="n">core</span><span class="mf">.0003</span><span class="n">_auto_20210715_0844</span><span class="o">...</span> <span class="n">OK</span>
</pre></div>
<p>If any errors happened you will see the stack trace here and you will need to fix them. Don’t worry, the state of your database
will not be changed until the migration finishes.</p>
<p>Now our database has both the (old) <tt class="docutils literal">category</tt> and the (new) <tt class="docutils literal">category_fk</tt> fields. Each will have the same value!</p>
<p>Now we need to remove the old <tt class="docutils literal">category</tt> field and rename the existing <tt class="docutils literal">category_fk</tt>. Let’s do it!</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Sample</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
<span class="n">category</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'Category'</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span>
</pre></div>
<p>And run the migration:</p>
<pre class="code literal-block">
C:\progr\py3\migrations_tutorial>python manage.py makemigrations
Migrations for 'core':
core\migrations\0004_auto_20210715_0909.py
- Remove field category_fk from sample
- Alter field category on sample
</pre>
<p>Uh oh! This does not seem to do what I want. Let’s take a peek at the generated migration file:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s1">'core'</span><span class="p">,</span> <span class="s1">'0003_auto_20210715_0844'</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RemoveField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s1">'sample'</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'category_fk'</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">AlterField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s1">'sample'</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'category'</span><span class="p">,</span>
<span class="n">field</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">django</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">models</span><span class="o">.</span><span class="n">deletion</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="n">to</span><span class="o">=</span><span class="s1">'core.Category'</span><span class="p">),</span>
<span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>This will remove the <tt class="docutils literal">category_fk</tt> field we just filled from our model and then try to convert the old <tt class="docutils literal">category</tt> field
to a foreign key! If you try to run the migration you’ll get an exception because the existing <tt class="docutils literal">category</tt> field cannot be
converted to a ForeignKey!</p>
<p>It seems that Django migrations isn’t so smart after all… To resolve that we could just create two separate migrations:
One to remove the old <tt class="docutils literal">category</tt> field and the other to rename the <tt class="docutils literal">category_fk</tt> field to <tt class="docutils literal">category</tt>. Django would
know then that we have renamed the <tt class="docutils literal">category_fk</tt> field. This method works fine but if you are using <tt class="docutils literal">category</tt> in your
admin (or forms) django will complain with errors like this:</p>
<p><tt class="docutils literal"><class <span class="pre">'core.admin.SampleAdmin'>:</span> (admin.E108) The value of 'list_display[1]' refers to 'category', which is not a callable, an attribute of 'SampleAdmin', or an attribute or method on 'core.Sample'.</tt></p>
<p>So you’ll need to rename to fix this before running the migration (and if you actually fix it you may just bite the bullet and use category_fk to avoid re-renaming it back to category).</p>
<p>This is rather a pain so I’ll give you another way: Edit the created migration file to do exactly what you need, i.e remove the existing
<tt class="docutils literal">category</tt> field and rename <tt class="docutils literal">category_fk</tt> to <tt class="docutils literal">category</tt>. Here’s the migration file:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Migration</span><span class="p">(</span><span class="n">migrations</span><span class="o">.</span><span class="n">Migration</span><span class="p">):</span>
<span class="n">dependencies</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="s1">'core'</span><span class="p">,</span> <span class="s1">'0003_auto_20210715_0844'</span><span class="p">),</span>
<span class="p">]</span>
<span class="n">operations</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RemoveField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s1">'sample'</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'category'</span><span class="p">,</span>
<span class="p">),</span>
<span class="n">migrations</span><span class="o">.</span><span class="n">RenameField</span><span class="p">(</span>
<span class="n">model_name</span><span class="o">=</span><span class="s1">'sample'</span><span class="p">,</span>
<span class="n">old_name</span><span class="o">=</span><span class="s1">'category_fk'</span><span class="p">,</span>
<span class="n">new_name</span><span class="o">=</span><span class="s1">'category'</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>So in this migration we first remove the existing <tt class="docutils literal">category</tt> field and then we rename the <tt class="docutils literal">category_fk</tt> field to <tt class="docutils literal">category</tt>. Let’s try to run it:</p>
<pre class="code literal-block">
C:\progr\py3\migrations_tutorial>python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
Applying core.0004_auto_20210715_0909... OK
</pre>
<p>Success!</p>
Saving in Dark Souls2021-06-15T14:20:00+03:002021-06-15T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2021-06-15:/2021/06/15/dark-souls-saves/<p class="first last">How to properly save your game in the Dark Souls trilogy</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p>The Dark Souls Trilogy (1-2-3) from FromSoftware is one of the modern gaming classics. The games should
be experienced by everybody because of their excellent gameplay, combat mechanics, atmosphere and
character development. The defining characteristic of the Dark Souls Trilogy and what scares most gamers
is their over the top difficulty.</p>
<p>This great difficulty is increased even more because of the saving mechanism of these games: There’s a
single save game in the game, if you die you’ll return to a previous checkpoint (called bonfire). These
checkpoint are sparcely located within the gaming world and they are not always near boss fights, so
if you die in a boss fight you may need to kill enemies for sometime before you reach the boss again to
retry. Also, everything is permanent so if you
screw up somehow (i.e you kill an important <span class="caps">NPC</span>) there’s no way to “restore” your game; you’ll lose him
(and his items if he’s a merchant) for the rest of your current game!</p>
<p>If the above seems too difficult for you to even try, fear not! There a particular way to have “real” saves in all three
Dark Souls games, even if it is a little cumbersome. It will be much less cumbersome than having to restart
the game because you killed an important <span class="caps">NPC</span>.</p>
</div>
<div class="section" id="disclaimer">
<h2>Disclaimer</h2>
<p>Before describing the technique I’d like to provide some disclaimer points:</p>
<ul class="simple">
<li>The Dark Souls Trilogy should be experienced as-is. You shouldn’t use this method because you’ll make the games easier and not as difficult as it was intented by their publisher. Use it only as a last resort when you are going to abandon the game.</li>
<li>Most other Dark Souls players will mock you and hate you for using these techniques.</li>
<li>You may break your save if you do something wrong so I won’t be held responsible for losing your progress.</li>
</ul>
</div>
<div class="section" id="how-dark-souls-saves-your-game">
<h2>How Dark Souls saves your game</h2>
<p>All three Dark Souls games have a particular directory in your hard disk where they place their save game. There’s a single file with your save game that has an extension endingg in .sl2.</p>
<p>From my <span class="caps">PC</span>, the folders and names of each of these games are the following:</p>
<ul class="simple">
<li>Dark Souls Remastered: Folder <tt class="docutils literal"><span class="pre">C:\Users\username\Documents\<span class="caps">NBGI</span>\<span class="caps">DARK</span></span> <span class="caps">SOULS</span> <span class="caps">REMASTERED</span>\1638</tt>, filename: <tt class="docutils literal"><span class="caps">DRAKS0005</span>.sl2</tt></li>
<li>Dark Souls 2: Scholar of the First Sin: <tt class="docutils literal"><span class="pre">C:\Users\username\AppData\Roaming\DarkSoulsII\0110000100000666\</span></tt>, filename: <tt class="docutils literal"><span class="caps">DS2SOFS0000</span>.sl2</tt></li>
<li>Dark Souls 3: <tt class="docutils literal"><span class="pre">C:\Users\username\AppData\Roaming\DarkSoulsIII\0110000100000666\</span></tt>, filename: <tt class="docutils literal"><span class="caps">DS3000</span>.sl2</tt></li>
</ul>
<p>Notice that the username will be your user’s username while the numbers you see will probably be different.</p>
<p>Now, when some particular action occurs (i.e when you kill an enemy) the game will overwrite the file in the folder with a new one
with the changes. You will see a flame in the top right of your screen when this happens. Notice that this happens on particular moments,
for example if you are just moving without encountering enemies your game won’t be saved (so if for example you make a difficult jump
the game won’t be saved right after the jump). Also, Dark Souls will save your game when you quit (so if you do a difficult jump, quit the game
and restart you will be after the jump).</p>
<p>The above description enables you to actually have proper saves: Quit the game (not completely, just display the title screen),
backup the save file in a different location, start the game.
If you die, quit the game (again just display the title screen), copy over from the backup to the save location and start the game again.
Notice that you should always quit the
game before restoring from a save file because Dark Souls reads the saves only then.
If you copy over a backup save while playing the game the
backup will be just overwritten with the new save data.</p>
<p>However you can backup your game without actually quitting: When you’ve reach a point you feel it needs saving, just alt+tab outside of your game
copy over the save to a backup location (you can even give it a proper name) and continue playing. When you want to load that save you’ll need to
quit, restore the backup and start the game again. Notice that when you do this the game will show you a warning that you “did not properly quit
the game”. From what I can understand, when you quit the game Dark Souls writes some flag to your save game. If you shut down your <span class="caps">PC</span> while
playing (or copy over the save game) then Dark Souls won’t write that flag to your save game. However from my experience in all three Dark Souls
games this warning doesn’t mean anything, the game will continue normally without any problems.</p>
</div>
<div class="section" id="making-it-simpler">
<h2>Making it simpler</h2>
<p>Copying over the save game in a different location is cumbersome and makes it easy to do mistakes (i.e copy instead of restoring your backup
save, copy over the current save to your backup). To make this process easier I will give you here a simple autohotkey script that will do
this for you using F7 to backup your save and F8 to restore it (don’t forget that you can only restore when you have quit the game and see the
title screen).</p>
<p>To use this script you need the excellent <a class="reference external" href="https://www.autohotkey.com/">autohotkey</a> utility. Download and install it and then execute the script by double clicking it (it needs to have an .ahk extension):</p>
<pre class="code literal-block">
#SingleInstance Force
#MaxHotkeysPerInterval 99999
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
SAVE_FOLDER_DS := "C:\Users\serafeim\AppData\Roaming\DarkSoulsII\0110000100000666\"
SAVE_FILENAME_DS := "DS2SOFS0000.sl2"
BACKUP_FOLDER_DS := "C:\Users\serafeim\Documents\ds2\"
GetFolderMax(f)
{
MAX := 0
Loop, Files, %f%\*.*
{
NUM_EXT := 1 * A_LoopFileExt
if (NUM_EXT> MAX)
{
MAX := NUM_EXT
}
}
return MAX
}
F7::
{
;MsgBox % "F7"
;MsgBox % "Will copy " . SAVE_FILENAME_DS . " to " . BACKUP_FOLDER_DS
MAX_P1 := GetFolderMax(BACKUP_FOLDER_DS) + 1
;MsgBox % "Max + 1 is " . MAX_P1
SOURCE := SAVE_FOLDER_DS . SAVE_FILENAME_DS
DEST := BACKUP_FOLDER_DS . SAVE_FILENAME_DS . "." . MAX_P1
;MsgBox % "Will copy " . SOURCE . " to " . DEST
FileCopy, %SOURCE%, %DEST%
return
}
F8::
{
;MsgBox % "F8"
MAX := GetFolderMax(BACKUP_FOLDER_DS)
MAX_FILE := BACKUP_FOLDER_DS . SAVE_FILENAME_DS . "." . MAX
;MsgBox % "Maxfile is " . MAX_FILE
SOURCE := MAX_FILE
DEST := SAVE_FOLDER_DS . SAVE_FILENAME_DS
;MsgBox % "Will copy " . SOURCE . " to " . DEST
FileCopy, %SOURCE%, %DEST%, 1
return
}
</pre>
<p>The script is very easy to understand but I’ll explain it a bit here: First of all you need to define the
<tt class="docutils literal">SAVE_FOLDER_DS, SAVE_FILENAME_DS</tt> and <tt class="docutils literal">BACKUP_FOLDER_DS</tt> variables. The first two are the folder and
filename of your game (in my example I’m using it for <span class="caps">DS2</span>). The <tt class="docutils literal">BACKUP_FOLDER_DS</tt> is where you want your
backups to be placed. This script will backup your save file in that folder when you press F7. To keep better
backups it will append an increasing number in the end of your filename so when you press F7 you will see
that it will create a file named <tt class="docutils literal"><span class="caps">DS2SOFS0000</span>.sl2.0</tt>, then <tt class="docutils literal"><span class="caps">DS2SOFS0000</span>.sl2.1</tt> etc in the <tt class="docutils literal">BACKUP_FOLDER_DS</tt>.
When you press F8 it will get the file with the biggest number in the end, strip that number and copy it over your
Dark Souls save file.</p>
<p>As you can see there’s a <tt class="docutils literal">GetFolderMax</tt> function that retrieves the max number from your backup folder. Then,
F7 and F8 will use that function to either copy over your Dark Souls save file in the backup with an increased
number or retrieve the latest one and restore it in your save folder.</p>
<p>The script works independently of the game so if you configure it and press F7 you should see that the backup file
will be created. Also if you delete (or rename) your Dark Souls save file and press F8 you should see that it will
be restore by the backup.</p>
<p>So using the above script, my play workflow is like this: Start Dark Souls, kill an enemy, press F7, kill another
enemy, press F7 (depending on how difficult the enemies are of course). Die from an enemy,
quit the game, press F8, continue my game.</p>
<p>One thing to notice is that in Windows 10 it seems that the hotkeys are not captured from autohotkey when the game
runs in full screen. When I run the games in a window it works fine. Some people say that if you run autohotkey
as administrator it will capture the key-presses but it didn’t work fine for me.</p>
</div>
Using matplotlib to generate graphs in Django2021-02-08T14:55:00+02:002021-02-08T14:55:00+02:00Serafeim Papastefanostag:spapas.github.io,2021-02-08:/2021/02/08/django-matplotlib/<p class="first last">How to use the matplotlib library to generate server-side graphs with Django</p>
<p>Nowadays the most common way to generate graphs in your Django apps (or web apps in general) is to
pass the data as json to the page and use a javascript lib. The big advantage these javascript libs
offer is interactivity: You can hover over points to see their values making studying the graph much easier.</p>
<p>Yet, there are times where you need some simple (or not so simple) graphs and don’t care about
offering interactivity through javascript nor you want to mess with javascript at all. For these cases
you can generate the graphs server-side using django and the <a class="reference external" href="https://matplotlib.org/">matplotlib</a> plot library.</p>
<p>matplotlib is a very popular library in the scientific cycles. It can be used to create more or less
any kind of graph and has unlimited capabilities! I won’t go into much detail about matplotlib here
because the subject is huge but I recommend you to take a look at the <a class="reference external" href="https://matplotlib.org/tutorials/index.html">comprehensive tutorials</a> on its homepage.</p>
<p>To install matplotlib on unix you need to do a <tt class="docutils literal">pip install matplotlib</tt> while, for windows,
you can download the proper ready-made binaries from the <a class="reference external" href="https://www.lfd.uci.edu/~gohlke/pythonlibs/">Unofficial Windows Binaries for Python Extension Packages</a>
site that offers pre-compiled versions of almost all python packages! Just make sure to download the correct version
for your python version and architecture (32bit or 64bit). After you’ve downloaded the file you can install it
for your project using something like <tt class="docutils literal">pip install <span class="pre">matplotlib-3.3.4-cp38-cp38-win32</span></tt> from inside your virtual environment.</p>
<p>Before actually creating a graph I recommend playing a bit with matplotlib to understand the basic concepts. Start a django shell
and do the following:</p>
<pre class="code literal-block">
>>> import matplotlib.pyplot as plt
>>> fig, ax = plt.subplots()
>>> ax.plot([1, 2, 3, 4], [1, 4, 2, 3])
[<matplotlib.lines.Line2D object at 0x0FBF5F58>]
>>> fig.show()
</pre>
<p>The above should open a window and display the graph. This works fine on Window 10 with python 3.8 and matplotlib 3.3.4 but I
can’t guarantee other versions. If however <tt class="docutils literal">fig.show()</tt> shows an error or does not display the graph, you can just do something like:</p>
<pre class="code literal-block">
>>> fig.savefig('test')
</pre>
<p>that will output the figure in a file named <tt class="docutils literal">test.png</tt> which you can the view. Please notice that the above are with the default
options; there are various ways that matplotlib can be configured.</p>
<p>In any case, after you’ve played a bit with the shell and generate a nice figure (take a look at the <a class="reference external" href="https://matplotlib.org/3.1.1/gallery/index.html">matplotlib examples</a> for
inspiration) you are ready to integrate matplotlib with Django!</p>
<p>I can think of two ways which you can integrate matplotlib with Django:</p>
<ul class="simple">
<li>Use a special view that would render the graph and just return a <span class="caps">PNG</span> object. Use a normal <tt class="docutils literal"><img></tt> element pointing to that view in your template.</li>
<li>Put the graph in the context of a normal django view encoded as a base64 object and use a special <tt class="docutils literal"><img></tt> with an <tt class="docutils literal">src</tt> attribute of <tt class="docutils literal"><span class="pre">data:image/png;base64,{{</span> graph }}</tt> to actually embed the image in the template!</li>
</ul>
<p>I prefer the second approach because it’s much more flexible since you don’t need to create a different Django view for each graph you
want to generate. For this reason I will explain this approach right now and give you some hints if you need to follow the dedicated
graph view approach.</p>
<p>Our view should:</p>
<ul class="simple">
<li>Generate the graph</li>
<li>Save it in a BytesIO object</li>
<li>Convert that BytesIO to base64</li>
<li>Put the string value of the base64 encoded graph to the template</li>
</ul>
<p>Then the template will just output that base64 value using the special img we mentioned above.</p>
<p>Here’s a snippet of a view that does exactly this:</p>
<pre class="code literal-block">
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import io, base64
from django.db.models.functions import TruncDay
from matplotlib.ticker import LinearLocator
class SampleListView(ListView):
model = Sample
def get_context_data(self, **kwargs):
by_days = get_queryset().annotate(day=TruncDay('created_on')).values('day').annotate(c=Count('id')).order_by('day')
days = [x['day'] for x in by_days]
counts = [x['c'] for x in by_days]
fig, ax = plt.subplots(figsize=(10,4))
ax.plot(days, counts, '--bo')
fig.autofmt_xdate()
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
ax.set_title('By date')
ax.set_ylabel("Count")
ax.set_xlabel("Date")
ax.grid(linestyle="--", linewidth=0.5, color='.25', zorder=-10)
ax.yaxis.set_minor_locator(LinearLocator(25))
flike = io.BytesIO()
fig.savefig(flike)
b64 = base64.b64encode(flike.getvalue()).decode()
context['chart'] = b64
return context
</pre>
<p>Please notice that after importing <tt class="docutils literal">matplotlib</tt> I’m using the <tt class="docutils literal"><span class="pre">matplotlib.use('Agg')</span></tt> command to use
the <tt class="docutils literal">Agg</tt> backend. You can <a class="reference external" href="https://matplotlib.org/faq/usage_faq.html#what-is-a-backend">learn more about backends here</a>, but it should be sufficient for now to
know that using the <tt class="docutils literal">Agg</tt> you’ll be able to save your graphs in png.</p>
<p>The above code uses some Django <span class="caps">ORM</span> trickery to group values by their created_on day value and then
assings the days and counts to two arrays (<tt class="docutils literal">days</tt>, <tt class="docutils literal">counts</tt>). It then creates a new empty graph
with a specific size using <tt class="docutils literal">fig, ax = <span class="pre">plt.subplots(figsize=(10,4))</span></tt> and plots the data with some
fancy styles with <tt class="docutils literal">ax.plot(days, counts, <span class="pre">'--bo')</span></tt>. After that it sets various options in the graph
like the labels, grid etc.</p>
<p>The save and convert to base64 part follows: A new file like object is created using <tt class="docutils literal">io.BytesIO()</tt> and
the figure is saved there (<tt class="docutils literal">fig.savefig(flike)</tt>). Then it is converted to a base64 string using the
<tt class="docutils literal">b64 = <span class="pre">base64.b64encode(flike.getvalue()).decode()</span></tt>. Finally it is just passed to the context of
the template as <tt class="docutils literal">chart</tt>.</p>
<p>Now, inside the template I’ve got the following line:</p>
<pre class="code literal-block">
<img src='data:image/png;base64,{{ chart }}'>
</pre>
<p>This will include the data of the chart inline and display it as a png image. If you’ve followed along
you should be able to see the graph when you load that view!</p>
<p>If instead of including the graphs in your normal django template views you want to use a dedicated
graph-generating view, you can follow my
<a class="reference external" href="https://spapas.github.io/2014/09/15/django-non-html-responses/">Django non-<span class="caps">HTML</span> responses tutorial</a>. You could then
modify the <tt class="docutils literal">render_to_response</tt> method of your view like this:</p>
<pre class="code literal-block">
def render_to_response(self, generator, **response_kwargs):
response = HttpResponse(content_type='image/png')
fig, ax = plt.subplots(figsize=(10,4))
# fill the report here
fig.savefig(response)
return response
</pre>
<p>Since <tt class="docutils literal">response</tt> is a file-like object you can save your graph directly there!</p>
Using hashids to hide ids of objects in Django2021-01-07T12:20:00+02:002021-01-07T12:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2021-01-07:/2021/01/07/django-hashids/<p class="first last">How to hide the ids (primary keys) of your objects in Django using the hashids library</p>
<p>A common pattern in Django urls is to have the following setup for <span class="caps">CRUD</span> operations of your objects. Let’s suppose we
have a <tt class="docutils literal">Ship</tt> object. It’s <span class="caps">CRUD</span> urls would be something like:</p>
<ul class="simple">
<li><tt class="docutils literal">/ships/create/</tt> To add a new object</li>
<li><tt class="docutils literal">/ships/list/</tt> To display a list of your objects</li>
<li><tt class="docutils literal">/ships/detail/id/</tt> To display the particular object with that id (primary key)</li>
<li><tt class="docutils literal">/ships/update/id/</tt> To update/edit the particular object with that id (primary key)</li>
<li><tt class="docutils literal">/ships/delete/id/</tt> To delete the particular object with that id (primary key)</li>
</ul>
<p>This is very easy to implement using class based views. For example for the detail view add the following to your views.py:</p>
<pre class="code literal-block">
class ShipDetailView(DetailView):
model = models.Ship
</pre>
<p>and then in your urls.py add the line:</p>
<pre class="code literal-block">
urlpatterns = [
# ...
path(
"detail/<int:pk>/",
login_required(views.ShipDetailView.as_view()),
name="ship_detail",
),
</pre>
<p>This path means that it expects an integer (<cite>int</cite>) which will be used as the primary key of the ship (<cite>pk</cite>).</p>
<p>Now, a common requirement if you are using integers as primary keys is to not display them to the public. So you
shouldn’t allow the users to write something like <tt class="docutils literal">/ships/detail/43</tt> to see the details of ship 43. Even if you
have add proper authorization (each user only sees the ids he has access to) you are opening a window for abuse. Also
you don’t want the users to be able to estimate how many objects there are in your database (if a user creates a
new ship he’ll get the latest id and know approximately how many ships are in your database).</p>
<p>One simple requirement is to use some encryption mechanism to encode the ids to some string and display that string
to the public urls. When you receive the string you’ll then decode it to get the id.</p>
<p>Thankfully, not only there’s a particular library that makes this whole encode/decode procedure very easy but Django
has functionality to make trivial to integrate this functionality to an existing project with only miniman changes!</p>
<p>The library I propose for this is called <a class="reference external" href="https://github.com/davidaurelio/hashids-python">hashids-python</a>. This is the python branch of the <a class="reference external" href="https://hashids.org/">hashids</a> library that works
for many languages. If you take a look at the documentation you’ll see that it can be used like this:</p>
<pre class="code literal-block">
from hashids import Hashids
hashids = Hashids()
hashid = hashids.encode(123) # 'Mj3'
ints = hashids.decode('xoz') # (456,)
</pre>
<p>This library offers two useful utilities: Define a random salt so that the generated hashids will be unique for your app
and add a minimum hash length so that the real length of the id will be obfuscated. I’ve found out that a length of 8 characters
will be more than enough to encode all possible ids up to 99 billion:</p>
<pre class="code literal-block">
hashids = Hashids(min_length=8)
len(hashids.encode(99_999_999_999)) # 8
</pre>
<dl class="docutils">
<dt>This is more than enough since by default django will use an integer to store the primary keys which is around 4 billion (you actually can</dt>
<dd>use 7 characters to encode up to 5 billion but I prefer even numbers.</dd>
</dl>
<p>Finally, you can use a different alphabet, for example to use all greek characters:</p>
<pre class="code literal-block">
hashids = Hashids(alphabet='ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ')
hashids.encode(123) # 'ΣΝΦ'
</pre>
<p>This isn’t recommended though for our case because not all characters are url-safe.</p>
<p>To integrate the hashids with Django we are going to use a <a class="reference external" href="https://docs.djangoproject.com/en/3.1/topics/http/urls/#registering-custom-path-converters">custom path converter</a>. The custom path converter
is similar to the <tt class="docutils literal">int</tt> portion of the <tt class="docutils literal"><span class="pre">"detail/<int:pk>/"</span></tt> of the url i.e it will retrieve something and convert it
to a python object. To implement your custom path converter just add a file named utils.py in one of your applications with
the following conents:</p>
<pre class="code literal-block">
from hashids import Hashids
from django.conf import settings
hashids = Hashids(settings.HASHIDS_SALT, min_length=8)
def h_encode(id):
return hashids.encode(id)
def h_decode(h):
z = hashids.decode(h)
if z:
return z[0]
class HashIdConverter:
regex = '[a-zA-Z0-9]{8,}'
def to_python(self, value):
return h_decode(value)
def to_url(self, value):
return h_encode(value)
</pre>
<p>The above will generate a <tt class="docutils literal">hashids</tt> global object with a min length of 8 as discussed above and retrieving
a custom salt from your settings (just add <tt class="docutils literal">HASHIDS_SALT=some_random_string</tt> to your project settings). The
<tt class="docutils literal">HashIdConverter</tt> defines a regex that will match the default aplhabet that hasid uses and two methods to convert
from url to python and vice versa. Notice that <tt class="docutils literal">hashids.decode</tt> returns an array so we’ll retrieve the first number only.</p>
<p>To use that custom path converter you will need to add the following lines to your urls.py to register your
<tt class="docutils literal">HashIdConverter</tt> as <tt class="docutils literal">hashid</tt>:</p>
<pre class="code literal-block">
from core.utils import HashIdConverter
register_converter(HashIdConverter, "hashid")
</pre>
<p>and then use it in your urls.py like this:</p>
<pre class="code literal-block">
urlpatterns = [
# ...
path(
"detail/<hashid:pk>/",
login_required(views.ShipDetailView.as_view()),
name="ship_detail",
),
</pre>
<p>That’s it! Your CBVs do not need any other changes! The <tt class="docutils literal">hashid</tt> will match the hashid in the url and convert it to
the model’s pk using the to_python method we defined above!</p>
<p>Of course you should also add the opposite direction (i.e convert from the primary key to the hashid). To do that we’ll
add a <tt class="docutils literal">get_absolute_url</tt> method to our Ship model, like this:</p>
<pre class="code literal-block">
class Ship(models.Model):
def get_hashid(self):
return h_encode(self.id)
def get_absolute_url(self):
return reverse("ship_detail", args=[self.id])
</pre>
<p>Notice that you just call the <tt class="docutils literal">reverse</tt> function passing <tt class="docutils literal">self.id</tt>; everything else will be done
automatically from the <tt class="docutils literal">hashid</tt> custom path generator <tt class="docutils literal">to_url</tt> method. I’ve also added a <tt class="docutils literal">get_hashid</tt>
method to my model to have quick access to the id in case I need it.</p>
<p>Now you don’t have any excuses to not hide your database ids from the public!</p>
Adding a timeline of your wagtail Posts2020-09-18T16:20:00+03:002020-09-18T16:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2020-09-18:/2020/09/18/wagtail-add-posts-timeline/<p class="first last">How to add a timeline wagtail Posts</p>
<div class="section" id="intro">
<h2>Intro</h2>
<p>In this small post I’ll present a small tutorial on how to add a timelne of your <a class="reference external" href="https://wagtail.io/">Wagtail</a>
posts using the <a class="reference external" href="http://horizontal-timeline.ycodetech.co.uk/">Horizontal Timeline</a> jquery plugin.</p>
<p>This will be a step by step tutorial to help you understand the concepts. As a base
we’ll use the <cite>bakerydemo</cite> wagtail demo. After you’ve properly followed the instructions
you’ll see that this demo site has a “Blog” that contains articles about breads. Following
we’ll add a timeline of these articles grouped by their publish month.</p>
</div>
<div class="section" id="decisions-decisions">
<h2>Decisions, decisions</h2>
<p>For this demo we’ll include <em>all</em> the “blog” pages in the timeline. However we may
wanted to select which pages we want to include in the timeline. This could be done
either by adding an extra field in our blog pages (class <cite>blog.models.BlogPage</cite>) like
<cite>include_in_timeline</cite> or by using the Wagtail <a class="reference external" href="https://docs.wagtail.io/en/latest/reference/contrib/modeladmin/">ModelAdmin</a> functionality. For the ModelAdmin
we’d create an extra Django model (i.e <cite>BlogTimeLineEntry</cite>) that would contain a link
to the original page. We could enchance this field with extra fields that we may
want to display in the timeline, for example a smaller description. Something like this:</p>
<!-- code
class TimeLineEntry(models.Model):
description = RichTextField()
page = models.ForeignKey("blog.BlogPage", on_delete=models.PROTECT)
panels = [ FieldPanel("description"), PageChooserPanel("page")]
def pub_date(self):
return self.page.date_published
pub_date.admin_order_field = "page__date_published" -->
<p>The other decision is where to actually output the timeline. For the demo we’ll just put it
in the <cite>BlogIndexPage</cite> page. If we wanted to add the timeline in a number of different
page types then we’d need to add a template tag that would include it. But since it will be
available only to a single page type we’ll just need to override the <cite>get_context</cite> method and
the template of that particular type.</p>
</div>
<div class="section" id="overriding-the-get-context">
<h2>Overriding the get_context</h2>
<p>As we described above, we want to group the timeline entries based on their publish month. For
this, we’ll use the following code in the <tt class="docutils literal">BlogIndexPage.get_context</tt> method:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">BlogIndexPage</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="n">context</span><span class="p">[</span><span class="s1">'posts'</span><span class="p">]</span> <span class="o">=</span> <span class="n">BlogPage</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">descendant_of</span><span class="p">(</span>
<span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">live</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span>
<span class="s1">'-date_published'</span><span class="p">)</span>
<span class="n">entries</span> <span class="o">=</span> <span class="n">context</span><span class="p">[</span><span class="s1">'posts'</span><span class="p">]</span>
<span class="n">dentries</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">entries</span><span class="p">:</span>
<span class="n">month</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">date_published</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%m/%Y"</span><span class="p">)</span>
<span class="n">month_entries</span> <span class="o">=</span> <span class="n">dentries</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">month</span><span class="p">,</span> <span class="p">[])</span>
<span class="n">month_entries</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
<span class="n">dentries</span><span class="p">[</span><span class="n">month</span><span class="p">]</span> <span class="o">=</span> <span class="n">month_entries</span>
<span class="n">lentries</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span>
<span class="p">[</span>
<span class="p">{</span>
<span class="s2">"date_small"</span><span class="p">:</span> <span class="n">k</span><span class="p">,</span>
<span class="s2">"date_large"</span><span class="p">:</span> <span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">date_published</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%B %Y"</span><span class="p">),</span>
<span class="s2">"entries"</span><span class="p">:</span> <span class="n">v</span><span class="p">,</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span> <span class="ow">in</span> <span class="n">dentries</span><span class="o">.</span><span class="n">items</span><span class="p">()</span>
<span class="p">],</span>
<span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">z</span><span class="p">:</span> <span class="n">z</span><span class="p">[</span><span class="s2">"entries"</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">date_published</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">timeline</span><span class="o">=</span><span class="n">lentries</span><span class="p">)</span>
<span class="k">return</span> <span class="n">context</span>
</pre></div>
<p>So what’s the purpose of the above? First of all we use <cite>super</cite> to retrieve the context that
any parent classes may have setup. After that we add a <cite>posts</cite> attribute to the context
that is a queryset of all the published children of the current page (which is the <cite>BlogIndexPage</cite>),
sorted by their published date.</p>
<p>In the <cite>for</cite> loop that follows, do some dict trickery to “gather” all entries for a particlular month/year
in a list under that particular key in the <cite>dentries</cite> dict.</p>
<p>Finally, we create the <cite>lentries</cite> list which will be a list of the form:</p>
<div class="highlight"><pre><span></span><span class="p">[{</span>
<span class="s2">"date_small"</span><span class="p">:</span> <span class="s2">"09/2020"</span>
<span class="s2">"date_large"</span><span class="p">:</span> <span class="s2">"September 2020"</span>
<span class="s2">"entries: [BlogPage, BlogPage, BlogPage...]</span>
<span class="p">},</span> <span class="p">{</span><span class="o">...</span><span class="p">},</span> <span class="o">...</span><span class="p">]</span>
</pre></div>
<p>This struct will help us in the next step when we implement the timeline template.</p>
</div>
<div class="section" id="fixing-the-template">
<h2>Fixing the template</h2>
<p>To use the horizontal timeline we need to add a couple of css/js dependencies to our template. For this,
we’ll first go to the bakerydemotemplatesbase.html file and add the following snippet near the end of the file just before
<tt class="docutils literal"></body></tt>:</p>
<pre class="code literal-block">
{% block extra_script %}
{% endblock %}
</pre>
<p>The above is required to give us a placeholder for adding some needed js dependencies and code.</p>
<p>After that we’ll go to the <tt class="docutils literal">bakerydemo\templates\blog\blog_index_page.html</tt> file and add the following just before
<tt class="docutils literal">{% block content %}</tt></p>
<pre class="code literal-block">
{% block head-extra %}
<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/gh/ycodetech/horizontal-timeline-2.0@2/css/horizontal_timeline.2.0.min.css">
<style>
.timeline .selected {
font-size: 24px;
font-weight: bold;
}
#timeline ol {
list-style: none;
}
.horizontal-timeline .events-content li {
background: #f2f2f2;
font-size: .8em;
}
#timeline img {
width: 200px;
}
</style>
{% endblock head-extra %}
</pre>
<p>And the following at the end of the file</p>
<pre class="code literal-block">
{% block extra_script %}
<script src="//cdn.jsdelivr.net/gh/ycodetech/horizontal-timeline-2.0@2/JavaScript/horizontal_timeline.2.0.min.js"></script>
<script>
$(function() {
$('#timeline').horizontalTimeline({
dateIntervals: {
"desktop": 200,
"tablet": 150,
"mobile": 120,
"minimal": true
}
});
})
</script>
{% endblock %}
</pre>
<p>Notice that the <tt class="docutils literal"><span class="pre">head-extra</span></tt> block is already there in the base.html file so we don’t need to add it again. It just has some
styling changes for the timeline to be displayed nice. Also the <tt class="docutils literal"><script></tt> tags we added just include the needed dependency
and initialize the timeline component.</p>
<p>Of course we haven’t yet added the actual timeline! To do that, we’ll need to add a file named <tt class="docutils literal">timeline_partial.html</tt> under the
<tt class="docutils literal">blog/templates/blog</tt> folder (same folder that <tt class="docutils literal">blog_index_page.html</tt> is) with the following:</p>
<pre class="code literal-block">
{% load wagtailcore_tags wagtailimages_tags %}
<div class="horizontal-timeline" id="timeline">
<div class="events-content">
<ol>
{% for month in timeline %}
<li class="{% if forloop.last %}selected{% endif %}" data-horizontal-timeline='{"date": "{{ month.date_small }}"}'>
<h3>{{ month.date_large }}</h3>
{% for te in month.entries %}
<div class='row'>
<div class='col-md-6'>
<h4><a href='{% pageurl te %}'>{{ te.title }}</a></h4>
<span>{{ te.introduction }}</span>
</div>
<div class='col-md-6'>
{% with img=te.image %}
{% image img width-200 as img_thumb %}
<img class="" src="{{ img_thumb.url }}" alt="{{ img.title }}">
{% endwith %}
</div>
</div>
<div class="clear bottommargin-sm"></div>
{% endfor %}
</li>
{% endfor %}
</ol>
</div>
</div>
</pre>
<p>The above will generate a <tt class="docutils literal"><li <span class="pre">data-horizontal-timeline='{"date":</span> <span class="pre">"01/2020"}></span></tt> list element for all months and
inside that it will add an <tt class="docutils literal"><h3></tt> with the full name of the month and a bunch of bootstrap rows, one for the
entries of that particular month (including its title, description and their image at the side). It should be easy enough to follow.</p>
<p>Finally, we need to incldue the above partial template. So add the line <tt class="docutils literal">{% include "blog/timeline_partial.html" %}</tt>
immediately above the <tt class="docutils literal"><div <span class="pre">class="row</span> <span class="pre">row-eq-height</span> <span class="pre">blog-list"></span></tt> line in the file <tt class="docutils literal">blog_index_page.html</tt>.</p>
<p>If you’ve followed the instructions you should be able to see something like this:</p>
<img alt="The timeline" src="/images/tl-demo.gif" style="width: 640px;" />
</div>
Getting alerts from OS Mon in your Elixir application2020-05-15T14:20:00+03:002020-05-15T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2020-05-15:/2020/05/15/elixir-osmon-alerts/<p class="first last">How to receive alerts from the Erlang osmon (<span class="caps">OS</span> Monitoring) application in your elixir/phoenix application</p>
<p>When I upgraded my <a class="reference external" href="https://github.com/spapas/phxcrd/">Phoenix template application</a> to Phoenix 1.5.1 I also enabled the new
<a class="reference external" href="https://github.com/phoenixframework/phoenix_live_dashboard">Phoenix LiveDashboard</a> and its “<span class="caps">OS</span> Data” tab. To enable that <span class="caps">OS</span> Data tab you have to
enable the <tt class="docutils literal">:os_mon</tt> erlang application by adding it (along with <tt class="docutils literal">:logger</tt> and <tt class="docutils literal">:runtime_tools</tt>) to
your <tt class="docutils literal">extra_applications</tt> setting <a class="reference external" href="https://hexdocs.pm/phoenix_live_dashboard/os_mon.html#enabling-os_mon">as described here</a>.</p>
<p>When I enabled the <tt class="docutils literal">os_mon</tt> application I immediately saw a warning in my logs that one of disks is almost full (which is
a fact). I knew that I wanted to understand how these warnings are generated and if I could handle them with
some custom code to send an email for example.</p>
<p>This journey lead me to an interesting erlang rabbit hole which I’ll describe in this small post.</p>
<div class="section" id="the-os-mon-erlang-application">
<h2>The os_mon erlang application</h2>
<p><a class="reference external" href="http://erlang.org/doc/man/os_mon_app.html">os_mon</a> is an erlang application that, when started will run 4 processes for monitoring
<span class="caps">CPU</span> load, disk, memory and some <span class="caps">OS</span> settings. These don’t work for all operating systems
but memory and disk which are the most interesting to me do work on both unix and Windows.</p>
<p>The disk and memory monitoring processes are called <tt class="docutils literal">memsup</tt> and <tt class="docutils literal">disksup</tt> and run a periodic
configurable check that checks if the memory or disk space usage is above a (configurable)
threshold. If the usage is over the threashold then an error will be reported to the
<a class="reference external" href="http://erlang.org/doc/man/alarm_handler.html"><span class="caps">SASL</span> alarm handler</a> (<span class="caps">SASL</span> is erlang’s System Architecture Support Libraries).</p>
</div>
<div class="section" id="the-alarm-handler-situation">
<h2>The alarm handler situation</h2>
<p>The <span class="caps">SASL</span> alarm handler is a process that implements the <a class="reference external" href="http://erlang.org/doc/man/gen_event.html">gen_event</a> behavior. It must
be noted that this behavior is <a class="reference external" href="https://pattern-match.com/blog/2018/08/31/what-is-wrong-with-gen-event-an-update/">rather controversial</a> and should not be used
for your own event handling (you can use your own gen server solution or gen stage).
A <tt class="docutils literal">gen_event</tt> process is an event manager. This event manager keeps a list of
event handlers; when an event happens the event manager will notify each of the
event handlers. Each event handler is just a module so when an event occurs all
event handlers will be run in the same process one after the other (that’s the
actual reason of why gen_event is not very loved).</p>
<p>The <span class="caps">SASL</span> alarm handler (the gen_event event manager)
is implemented in a module named <tt class="docutils literal">:alarm_handler</tt>. A rather
unfortunate decision is that the default simple alarm handler
(the gen_event event handler) is <em>also</em> implemented
in the same module so in the following you’ll see <tt class="docutils literal">:alarm_handler</tt> twice!</p>
<p>The default simple alarm handler can be exchanged with your own custom implementation or
you can even add additional alarm handlers so they’ll be called one after the other.</p>
<p>To add another custom event handler for alarms, you’ll use the <a class="reference external" href="http://erlang.org/doc/man/gen_event.html#add_handler-3">add_handler</a> method of gen_event. To change it
with your own, you’ll use the <a class="reference external" href="http://erlang.org/doc/man/gen_event.html#swap_handler-3">swap_handler</a> of gen_event. When the default simple alarm handler
is swapped it will return a list of the existing alarms in the system which will the be passed to
the new alarm handler.</p>
</div>
<div class="section" id="a-simple-alarm-handler-implementation">
<h2>A simple alarm handler implementation</h2>
<p>As noted in the docs, an alarm handler implementation must handle the following two events:</p>
<p><tt class="docutils literal">{:set_alarm, {alarm_id, alarm_description}}</tt> and
<tt class="docutils literal">{:clear_alarm, alarm_id}</tt>. The first one will be called from the event manager when a new alarm
is created and the send one when the cause of the alarm not longer exists.</p>
<p>Let’s see a simple implementation of an alarm event handler:</p>
<div class="highlight"><pre><span></span><span class="w"> </span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">Phxcrd.AlarmHandler</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="kn">import</span><span class="w"> </span><span class="nc">Bamboo.Email</span>
<span class="w"> </span><span class="kn">require</span><span class="w"> </span><span class="nc">Logger</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">init</span><span class="p">({</span><span class="n">_args</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="ss">:alarm_handler</span><span class="p">,</span><span class="w"> </span><span class="n">alarms</span><span class="p">}})</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nc">Logger</span><span class="o">.</span><span class="n">debug</span><span class="w"> </span><span class="s2">"Custom alarm handler init!"</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">{</span><span class="n">alarm_id</span><span class="p">,</span><span class="w"> </span><span class="n">alarm_description</span><span class="p">}</span><span class="w"> </span><span class="o"><-</span><span class="w"> </span><span class="n">alarms</span><span class="p">,</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">handle_alarm</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">,</span><span class="w"> </span><span class="n">alarm_description</span><span class="p">)</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="p">[]}</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">handle_event</span><span class="p">({</span><span class="ss">:set_alarm</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="n">alarm_id</span><span class="p">,</span><span class="w"> </span><span class="n">alarm_description</span><span class="p">}},</span><span class="w"> </span><span class="n">state</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nc">Logger</span><span class="o">.</span><span class="n">warn</span><span class="w"> </span><span class="s2">"Got an alarm "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="nc">Atom</span><span class="o">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">)</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="s2">" "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="n">alarm_description</span>
<span class="w"> </span><span class="n">handle_alarm</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">,</span><span class="w"> </span><span class="n">alarm_description</span><span class="p">)</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p">}</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">handle_event</span><span class="p">({</span><span class="ss">:clear_alarm</span><span class="p">,</span><span class="w"> </span><span class="n">alarm_id</span><span class="p">},</span><span class="w"> </span><span class="n">state</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nc">Logger</span><span class="o">.</span><span class="n">debug</span><span class="w"> </span><span class="s2">"Clearing the alarm "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="nc">Atom</span><span class="o">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">)</span>
<span class="w"> </span><span class="n">state</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">IO</span><span class="o">.</span><span class="n">inspect</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">state</span><span class="p">}</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">handle_alarm</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">,</span><span class="w"> </span><span class="n">alarm_description</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nc">Logger</span><span class="o">.</span><span class="n">debug</span><span class="w"> </span><span class="s2">"Handling alarm "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="nc">Atom</span><span class="o">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">)</span>
<span class="w"> </span><span class="n">new_email</span><span class="p">(</span>
<span class="w"> </span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="s2">"foo@foo.com"</span><span class="p">,</span>
<span class="w"> </span><span class="ss">from</span><span class="p">:</span><span class="w"> </span><span class="s2">"bar@bar.gr"</span><span class="p">,</span>
<span class="w"> </span><span class="ss">subject</span><span class="p">:</span><span class="w"> </span><span class="s2">"New alarm!"</span><span class="p">,</span>
<span class="w"> </span><span class="ss">html_body</span><span class="p">:</span><span class="w"> </span><span class="s2">"<strong>Alert:"</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="nc">Atom</span><span class="o">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">)</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="s2">" "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="n">alarm_description</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="s2">"</strong>"</span><span class="p">,</span>
<span class="w"> </span><span class="ss">text_body</span><span class="p">:</span><span class="w"> </span><span class="s2">"Alert:"</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="nc">Atom</span><span class="o">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">)</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="s2">" "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="n">alarm_description</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Phxcrd.Mailer</span><span class="o">.</span><span class="n">deliver_later</span><span class="p">()</span>
<span class="w"> </span><span class="nc">Logger</span><span class="o">.</span><span class="n">debug</span><span class="w"> </span><span class="s2">"End handling alarm "</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="nc">Atom</span><span class="o">.</span><span class="n">to_string</span><span class="p">(</span><span class="n">alarm_id</span><span class="p">)</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>This implementation also has an <tt class="docutils literal">init</tt> function that is called when the handler
is first started. Notice that it receives a list of the existing alarms; for each
one of them I’ll calle the handle_alarm function. This is needed to handle any
existing alarms when the application is starting. The <tt class="docutils literal">:set_alarm</tt> handler also
calls <tt class="docutils literal">handle_alarm</tt> passing the <tt class="docutils literal">alarm_id</tt> and <tt class="docutils literal">alarm_description</tt> it received.</p>
<p>The <tt class="docutils literal">clear_alarm</tt> doesn’t do anything (it would be useful if this module used state to
keep a list of the current alarms). Finally, the <tt class="docutils literal">handle_alarm</tt> will just send an
email using <a class="reference external" href="https://github.com/fewlinesco/bamboo_smtp">bamboo_smtp</a>. Notice that I use deliver_later() to send the mail asynchronously.</p>
<p>As you can see this is a very simple example. You can do more things here but I think that
getting the Alarm email should be enough for most situations!</p>
</div>
<div class="section" id="integrating-the-alarm-handler-into-your-elixir-app">
<h2>Integrating the alarm handler into your elixir app</h2>
<p>To use the above mentioned custom alarm event handler I’ve added the following line to
the start of my <tt class="docutils literal">Application.start</tt> function:</p>
<div class="highlight"><pre><span></span><span class="ss">:gen_event</span><span class="o">.</span><span class="n">swap_handler</span><span class="p">(</span><span class="ss">:alarm_handler</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="ss">:alarm_handler</span><span class="p">,</span><span class="w"> </span><span class="ss">:swap</span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="nc">Phxcrd.AlarmHandler</span><span class="p">,</span><span class="w"> </span><span class="ss">:ok</span><span class="p">})</span>
</pre></div>
<p>Please notice that the <tt class="docutils literal">:alarm_handler</tt> atom is encountered twice: The first is the event manager
module (for which we want to swich the event handler) while the second is the event handler module
(which is the one we want to replace).</p>
</div>
<div class="section" id="os-mon-configuration">
<h2>os_mon configuration</h2>
<p>The are a number of options you can configure for <cite>os_mon</cite>. You can find them all at the manual page.
For example, just add the following to your <tt class="docutils literal">config.exs</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">config</span><span class="w"> </span><span class="ss">:os_mon</span><span class="p">,</span>
<span class="w"> </span><span class="ss">disk_space_check_interval</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
<span class="w"> </span><span class="ss">memory_check_interval</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
<span class="w"> </span><span class="ss">disk_almost_full_threshold</span><span class="p">:</span><span class="w"> </span><span class="mf">0.90</span><span class="p">,</span>
<span class="w"> </span><span class="ss">start_cpu_sup</span><span class="p">:</span><span class="w"> </span><span class="no">false</span>
</pre></div>
<p>This will set the interval for disk space check to 1 minute, for memory check to 5 minutes, the
disk usage threshold to 90% and will not start the cpu_sup process to get <span class="caps">CPU</span> info.</p>
</div>
<div class="section" id="testing-with-the-terminal">
<h2>Testing with the terminal</h2>
<p>If no alerts are active in your system, you can test your custom event handler using something like this
from an <tt class="docutils literal">iex <span class="pre">-S</span> mix</tt> terminal:</p>
<div class="highlight"><pre><span></span><span class="ss">:alarm_handler</span><span class="o">.</span><span class="n">set_alarm</span><span class="p">({</span><span class="ss">:koko</span><span class="p">,</span><span class="w"> </span><span class="s2">"ZZZZZZZZZ"</span><span class="p">}</span>
<span class="c1"># or</span>
<span class="ss">:alarm_handler</span><span class="o">.</span><span class="n">clear_alarm</span><span class="p">(</span><span class="ss">:koko</span><span class="p">)</span>
</pre></div>
<p>Also you can see some of the current data or configuration options:</p>
<div class="highlight"><pre><span></span><span class="n">iex</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span><span class="o">></span><span class="w"> </span><span class="ss">:disksup</span><span class="o">.</span><span class="n">get_disk_data</span>
<span class="p">[{</span><span class="s1">'C:\\'</span><span class="p">,</span><span class="w"> </span><span class="mi">234195964</span><span class="p">,</span><span class="w"> </span><span class="mi">55</span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="s1">'E:\\'</span><span class="p">,</span><span class="w"> </span><span class="mi">822396924</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">}]</span>
<span class="c1"># or</span>
<span class="n">iex</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span><span class="o">></span><span class="w"> </span><span class="ss">:disksup</span><span class="o">.</span><span class="n">get_check_interval</span>
<span class="mi">60000</span>
</pre></div>
<p>Please notice that the check interval is in seconds when you set it, in ms when you retrieve it.</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>The above should help you if you also want to better understand alert_handler, os_mon and
how to configure it to run your own custom alert handlers. Of course in a production server
you should have proper monitoring tools for the health of your server but since os_mon is more
or less free thanks to erlang, why not add another safety valve?</p>
<p>If you want to take a look at an application that has everything configured, take a
look at my <a class="reference external" href="https://github.com/spapas/phxcrd/">Phoenix template application</a>.</p>
</div>
Adding a latest-changes list to your Wagtail site2020-03-27T14:20:00+02:002020-03-27T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2020-03-27:/2020/03/27/wagtail-add-latest-changes/<p class="first last">How to add a list of the latest changes to your wagtail site</p>
<p>I think that a very important tool for a new production <a class="reference external" href="https://wagtail.io">Wagtail</a> site is to have a list
where you’ll be able to take a look at the latest changes. Most editors are not
experienced enough when using a new tool so it’s easy to make bad quality edits. A
user could take a look at their changes and guide them if something’s not up to good standards.</p>
<p>In this article I’ll present a simple way to add a latest-changes list in your Wagtail site.
This is working excellent with Wagtail 2.9, I haven’t tested it with other wagtail versions
so your milage may vary. In the meantime, I’ll also introduce a bunch of concepts of Wagtail
I find interesting.</p>
<p><strong>Update 08/05/2020</strong> Please notice that this article was originally written for Wagtail 2.8
projects. However, Wagtail 2.8 didn’t have an official <span class="caps">API</span> for adding reports thus I had to
check the source code for some things. Those things since were not part of any <span class="caps">API</span> have
been changed and are not working in Wagtail 2.9.</p>
<p>Thus I’ve updated
the project to use the proper APIs and work with Wagtail 2.9 (and hopefully the next versions).
If you want to see what’s changed between the two versions you can take a look at
<a class="reference external" href="https://github.com/spapas/wagtail-latest-changes/commit/d751cd7978fc99c3b2f10e84c2f9b72c555f0930">this commit</a> from the companion project. You may also want to take a look at the
<a class="reference external" href="https://docs.wagtail.io/en/stable/advanced_topics/adding_reports.html">adding reports</a> tutorial on the Wagtail docs.</p>
<div class="section" id="a-starter-project">
<h2>A starter project</h2>
<p>Let’s create a simple wagtail-starter project (this is for windows you should be able to easily follow the same steps in Unix like systems):</p>
<pre class="code literal-block">
C:\progr\py3>mkdir wagtail-starter
C:\progr\py3>cd wagtail-starter
C:\progr\py3\wagtail-starter>py -3 -m venv venv
C:\progr\py3\wagtail-starter>venv\Scripts\activate
(venv) C:\progr\py3\wagtail-starter>pip install wagtail
(venv) C:\progr\py3\wagtail-starter>wagtail.exe start wagtail_starter
(venv) C:\progr\py3\wagtail-starter>cd wagtail_starter
(venv) C:\progr\py3\wagtail-starter\wagtail_starter>python manage.py migrate
(venv) C:\progr\py3\wagtail-starter\wagtail_starter>python manage.py createsuperuser
(venv) C:\progr\py3\wagtail-starter\wagtail_starter>python manage.py runserver
</pre>
<p>When you’ve finished all the above you should be able to go to <a class="reference external" href="http://127.0.0.1/">http://127.0.0.1/</a> and see your homepage and then
visit <a class="reference external" href="http://127.0.0.1/admin/">http://127.0.0.1/admin/</a> and login with your superuser. What we’d like to do is add a “Latest changes”
link in the “Reports” admin section like this:</p>
<img alt="New menu" src="/images/latest-changes-template.png" style="width: 580px;" />
</div>
<div class="section" id="implementing-the-latest-changes-menu">
<h2>Implementing the latest-changes menu</h2>
<div class="section" id="start-with-a-new-app">
<h3>Start with a new app</h3>
<p>To start the menu implementation I recommend putting everything related to it in a separate django application
so you can easily re-use it to multiple sites. For this, let’s create a new django app using:</p>
<pre class="code literal-block">
(venv) C:\progr\py3\wagtail-starter\wagtail_starter>python manage.py startapp latest_changes
</pre>
<p>And add <tt class="docutils literal">'latest_changes'</tt> to the list of our <tt class="docutils literal">INSTALLED_APPS</tt> at the file <tt class="docutils literal">wagtail_starter\settings\base.py</tt>.</p>
</div>
<div class="section" id="add-the-view">
<h3>Add the view</h3>
<p>Let’s add the code for the view that will display the latest changes page. Modify the latest_changes/views.py file like this:</p>
<pre class="code literal-block">
from django.shortcuts import render
from wagtail.admin.views.reports import PageReportView
from wagtail.core.models import UserPagePermissionsProxy
from wagtail.core.models import Page
class LatestChangesView(PageReportView):
template_name = "reports/latest_changes.html"
title = "Latest changes"
header_icon = "date"
def get_queryset(self):
self.queryset = Page.objects.order_by("-last_published_at")
return super().get_queryset()
def dispatch(self, request, *args, **kwargs):
if not UserPagePermissionsProxy(request.user).can_remove_locks():
return permission_denied(request)
return super().dispatch(request, *args, **kwargs)
</pre>
<p>As you can see the above code adds a very small view that overrides <tt class="docutils literal">PageReportView</tt> which is used
also by the locked pages view so most things are already implemented by that view. The only thing we do
here is to override the <tt class="docutils literal">get_queryset</tt> method to denote which pages we want to display and
the <tt class="docutils literal">dispatch</tt> to add some permission checks. Here we check that a user <tt class="docutils literal">can_remove_locks</tt> but we
could do other checks if needed. Finally, notice that we have overriden the template name which we’ll
define in a minute.</p>
<p>Beyond <tt class="docutils literal">PageReportView</tt> that should be used for generating <tt class="docutils literal">Page</tt> reports, you can override
<tt class="docutils literal">ReportView</tt> which can be used to implement generic reports.</p>
<p>To properly add that view in our urls.py we can use a wagtail hook named <tt class="docutils literal">register_admin_py</tt>. Wagtail hooks
are a great way to excend the wagtail admin; to use them, you have to generate a file name <tt class="docutils literal">wagtail_hooks.py</tt>
in one of your apps. This file will be auto-impoted by wagtail when your app is started.</p>
<p>Thus, in our case we’ll add a <tt class="docutils literal">wagtail_hooks.py</tt> file in the <tt class="docutils literal">latest_changes</tt> app with the following code:</p>
<pre class="code literal-block">
from django.http import HttpResponse
from django.conf.urls import url
from wagtail.core import hooks
from .views import LatestChangesView
@hooks.register('register_admin_urls')
def urlconf_time():
return [
url(r'^latest_changes/$', LatestChangesView.as_view(), name='latest_changes'),
]
</pre>
<p>The above just hooks up the <tt class="docutils literal">LatestChangesView</tt> we defined before to the <tt class="docutils literal">/admin/latest_changes/</tt> url.</p>
<p>If everything’s ok till now you should be able to visit: <a class="reference external" href="http://127.0.0.1:8000/admin/latest_changes/">http://127.0.0.1:8000/admin/latest_changes/</a> and
get an error for a missing template - remember that we haven’t yet defined <tt class="docutils literal">utils/reports/latest_changes.html</tt>.</p>
</div>
<div class="section" id="add-the-template">
<h3>Add the template</h3>
<p>To add the template we’ll need to create a folder named <tt class="docutils literal">templates</tt> under our <tt class="docutils literal">latest_changes</tt> app and then
add a <tt class="docutils literal">reports</tt> folder to it. Finally in that folder add a <tt class="docutils literal">latest_changes.html</tt>. So the full path of
the <tt class="docutils literal">latest_changes.html</tt> should be: <tt class="docutils literal">wagtail_starter\latest_changes\templates\reports\latest_changes.html</tt>:</p>
<pre class="code literal-block">
{% extends 'wagtailadmin/reports/base_page_report.html' %}
{% load i18n %}
{% block listing %}
{% include "reports/_list_latest.html" %}
{% endblock %}
{% block no_results %}
<p>{% trans "No changes found." %}</p>
{% endblock %}
</pre>
<p>I’ve selected the <tt class="docutils literal">reports</tt> subfolder just to be compatible with what wagtail does, you can just put <tt class="docutils literal">latest_changes.html</tt> directly
under <tt class="docutils literal">templates</tt>; don’t forget to update the <tt class="docutils literal">LatestChangesView</tt> defined before though! This template extends the
<tt class="docutils literal">base_page_report.html</tt> template that Wagtail provides for page reports. It also includes a
snippet named <tt class="docutils literal">reports/_list_latest.html" thus you also need to add a ``_list_latest.html</tt> file in the same folder with the
following contents:</p>
<pre class="code literal-block">
{% extends "wagtailadmin/pages/listing/_list_explore.html" %}
{% load i18n wagtailadmin_tags %}
{% block post_parent_page_headers %}
<tr>
<th>Title</th>
<th>Last update</th>
<th>Kind</th>
<th>Status</th>
<th>Owner / last publish / last edit</th>
</tr>
{% endblock %}
{% block page_navigation %}
<td>
{{ page.owner }} / {{ page.live_revision.user }} / {{ page.get_latest_revision.user }}
</td>
{% endblock %}
</pre>
<p>Please notice that my <tt class="docutils literal">_list_latest.html</tt> snippet extends the Wagtail provided <tt class="docutils literal">_list_explore.html</tt> template and
overrides some things that can be overriden from that file. If you want to do more changes you’ll need to copy over
everything and change things as you wish instead of extending.</p>
<p>Also, keep in mind that because you added a <tt class="docutils literal">templates</tt> folder you’ll need to restart your django development server.</p>
<p>Finally, if everything is ok until now you should be able to visit <a class="reference external" href="http://127.0.0.1:8000/admin/latest_changes/">http://127.0.0.1:8000/admin/latest_changes/</a> and see
your view! It will say “No changes found” if you’ve followed the steps here; just go to Pages - Home from the wagtail
menu and edit that page (just save it). Now visit <a class="reference external" href="http://127.0.0.1:8000/admin/latest_changes/">http://127.0.0.1:8000/admin/latest_changes/</a> again and behold! Your
own latest changes view:</p>
<img alt="The view" src="/images/last_changes_view.png" style="width: 780px;" />
</div>
<div class="section" id="displaying-our-menu-item">
<h3>Displaying our menu item</h3>
<p>The last piece of the puzzle missing is to actually display a menu item under the Reports menu of wagtail admin. For this
we are going to use our friends, the wagtail hooks. So, change the wagtail_hooks.py file like this (I’m also including
the code from adding the url):</p>
<pre class="code literal-block">
from django.http import HttpResponse
from django.conf.urls import url
from django.urls import reverse
from wagtail.admin.menu import MenuItem
from wagtail.core import hooks
from wagtail.core.models import UserPagePermissionsProxy
from .views import LatestChangesView
@hooks.register('register_admin_urls')
def urlconf_time():
return [
url(r'^latest_changes/$', LatestChangesView.as_view(), name='latest_changes'),
]
class LatestChangesPagesMenuItem(MenuItem):
def is_shown(self, request):
return UserPagePermissionsProxy(request.user).can_remove_locks()
@hooks.register("register_reports_menu_item")
def register_latest_changes_menu_item():
return LatestChangesPagesMenuItem(
"Latest changes", reverse("latest_changes"), classnames="icon icon-date", order=100,
)
</pre>
<p>The above code uses the <tt class="docutils literal">register_reports_menu_item</tt> which is a hook that can be used to add a child
specifically to the Reports menu item. Notice that it uses the <tt class="docutils literal">LatestChangesPagesMenuItem</tt> which
is a class that inherits from <tt class="docutils literal">MenuItem</tt>; the only thing that is overriden there is the <tt class="docutils literal">is_shown</tt>
method so it will have the same permissions as the <tt class="docutils literal">LatestChangesView</tt> we defined above so user
that will see the menu item will also have permissions to display the view. Here’s the final menu item:</p>
<img alt="The menu item" src="/images/latest_changes_menu.png" style="width: 380px;" />
</div>
<div class="section" id="conclusion">
<h3>Conclusion</h3>
<p>We’ve seen the steps required to add a latest pages view to your wagtail admin site. I have to admit that
it is a little work however the nice thing is that this is all self-included in a single application. You can
just get tha application and copy over it to your wagtail site; after you add that application to INSTALLED_APPS
you should get the whole functionality without any more modifications to your project. To help you more
with this I’ve included the whole code of this project in the <a class="reference external" href="https://github.com/spapas/wagtail-latest-changes">https://github.com/spapas/wagtail-latest-changes</a> repository.</p>
<p>You can either clone this repository to see the functionality or just copy over the <tt class="docutils literal">latest_changes</tt> folder to
your wagtail project to include the functionality directly (don’t forget to fix the <tt class="docutils literal">INSTALLED_APPS</tt> setting)!
It should work with all Wagtail 2.9 and later projects.</p>
</div>
</div>
Quick and easy layout of django forms using django-crispy-forms and django-widget-tweaks2020-03-18T11:20:00+02:002020-03-18T11:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2020-03-18:/2020/03/18/django-crispy-form-quick-easy-layout/<p class="first last">How to easily layout your django-crispy-forms forms</p>
<p>One of the first problems you have when you want to create a traditional <span class="caps">HTML</span> django site (i.e
not an <span class="caps">SPA</span> one) is how to properly and beautifully layout your forms. In this small article I’ll
talk about two very useful django packages that will help you have great layouts in your forms:
<a class="reference external" href="https://github.com/django-crispy-forms/django-crispy-forms">django-crispy-forms</a> and <a class="reference external" href="https://github.com/jazzband/django-widget-tweaks">django-widget-tweaks</a>.</p>
<div class="section" id="django-crispy-forms">
<h2>django-crispy-forms</h2>
<p>The django-crispy-forms django package is a great way to properly format your forms. If you don’t
already use it I totally recommend to check it out; it’s very useful and you’ll definitely love it
if you are a heavy django forms user. It helps you properly layout your forms either implicitly or explicitly.</p>
<p>For explicitly laying out your forms you should add a <tt class="docutils literal">FormHelper</tt> to your django form class and
use the <tt class="docutils literal">{% crispy %}</tt> template tag. You can use this to explicitly define your form layout with
as much detail as you want since you have full control. I won’t go into more details about this since it’s
<a class="reference external" href="https://django-crispy-forms.readthedocs.io/en/latest/crispy_tag_forms.html#crispy-tag-with-forms">explained thoroughly in the docs</a>.</p>
<p>For implicitly laying out your forms, you will just use the <tt class="docutils literal">|crispy</tt> template filter. This gets a
normal django form (without any modifications) and converts it to a crispy form based on the <a class="reference external" href="https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs">template pack</a>
you are using. This works great for many situations however sometimes you’ll need to have more control
over this without going to the extra effort to add a complete layout to each of your forms using
the <tt class="docutils literal">FormHelper</tt>.</p>
<p>So how do you resolve this? Enter the <tt class="docutils literal">|as_crispy_field</tt> template filter. To use that filter you’ll need
to add the <tt class="docutils literal">{% load crispy_forms_tags %}</tt> lines to your template and then you can pass any one of your
form’s fields to it so it will be properly “crispified”! Let’s see a quick example of adding the fields of
a form in a <tt class="docutils literal"><div <span class="pre">class='col-md-6'></span></tt> so they will be in two columns (using bootstrap):</p>
<pre class="code literal-block">
<form class='form' method="POST">
<div class='row'>
{% for field in form %}
<div class='col-md-6'>
{{ field|as_crispy_field }}
</div>
{% endfor %}
{% csrf_token %}
</div>
<input class='btn btn-primary' type='submit'>
</form>
</pre>
<p>So the above code enumerates all form fields and uses the <tt class="docutils literal">|as_crispy_field</tt> to properly add the
crispified information to it. If you want to re-use the above two column layout in multiple forms and
be more dry you can create a template snippet and <tt class="docutils literal">{% include %}</tt> it in the part of your code you
want the form to be rendered.</p>
</div>
<div class="section" id="django-widget-tweaks">
<h2>django-widget-tweaks</h2>
<p>Using the <tt class="docutils literal">as_crispy_field</tt> is excellent however sometimes you may need even more control
of your for fields, for example add an extra class (like <tt class="docutils literal"><span class="pre">form-control-lg</span></tt>) to your form controls.
The answer to this is the django-widget-tweaks package: It enables you to easily modify form
fields by adding classes, attributes etc to them from within your django templates.</p>
<p>To use it you need to add a <tt class="docutils literal">{% load widget_tweaks %}</tt> to your templates. Then you’ll be
able to use the <tt class="docutils literal">|add_class</tt> form field to add a class to your form field. For example the
previous example can be modified like this to have smaller controls:</p>
<pre class="code literal-block">
<form class='form' method="POST">
<div class='row'>
{% for field in form %}
<div class='col-md-6'>
{{ field|add_class:"form-control-sm"|as_crispy_field }}
</div>
{% endfor %}
{% csrf_token %}
</div>
<input class='btn btn-primary' type='submit'>
</form>
</pre>
<p>Please notice that I use both <tt class="docutils literal">add_class</tt> and <tt class="docutils literal">as_crispy_fields</tt> together; notice the
order, the <tt class="docutils literal">add_class</tt> needs to be <em>before</em> the <tt class="docutils literal">as_crispy_field</tt> or you’ll get an error. This
way the django form field will have the <tt class="docutils literal"><span class="pre">form-control-sm</span></tt> class <em>and</em> then be rendered as a
crispy field.</p>
<p>Let’s now suppose that you need to more control over your fields. For example you need to add a class
only to your select fields or even only to a particular field (depending on its name). To do that
you can use the <tt class="docutils literal">name</tt> and <tt class="docutils literal">field.widget.input_type</tt> attributes of each for field. So, to make
select fields smaller and with a <tt class="docutils literal">warning</tt> background and fields that have a name of <tt class="docutils literal">name</tt>
or <tt class="docutils literal">year</tt> larger you can use something like this:</p>
<pre class="code literal-block">
<form class='form' method="POST">
<div class='row'>
{% for field in form %}
{% if field.name == 'name' or field.name == 'year' %}
{{ field|add_class:"form-control-lg"|as_crispy_field }}
{% elif field.field.widget.input_type == 'select' %}
{{ field|add_class:"form-control-sm"|add_class:"bg-warning"|as_crispy_field }}
{% else %}
{{ field|as_crispy_field }}
{% endif %}
{% endfor %}
{% csrf_token %}
</div>
<input class='btn btn-primary' type='submit'>
</form>
</pre>
<p>Using the above techniques you should be able to quickly layout and format your form fields with
much control! If you need something more I recommend going the <tt class="docutils literal">FormLayout</tt> route I mentioned
in the beginning.</p>
</div>
Declarative Ecto query sorting2019-10-17T12:20:00+03:002019-10-17T12:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2019-10-17:/2019/10/17/declarative-ecto-query-sorting/<p class="first last">Being able to declare your Ecto query sorting even on fields spanning joins</p>
<p>In a <a class="reference external" href="https://spapas.github.io/2019/07/25/declarative-ecto-query-filters/">previous article</a> I presented a method for declaring dynamic filters for your ecto queries.
Continuing this article, I’ll present here a way to allow dynamic sorting for your queries using
fields that may even span relations.</p>
<div class="section" id="what-will-it-do">
<h2>What will it do</h2>
<p>The solution is a couple of function that can be put inside the <tt class="docutils literal">QueryFilterEx</tt> I mentioned
in the <a class="reference external" href="https://spapas.github.io/2019/07/25/declarative-ecto-query-filters/">previous article</a>. Please make sure that you’ve completely read and understand this
article before continuing here.</p>
<p>To use the dynamic sorting function you’ll need to declare the fields that would allow sorting using
a simple array of strings. The sort fields should then be added as links to your phoenix page which
will then pass an <tt class="docutils literal">order_by=field_name</tt> query parameter to your controller.</p>
<p>The module has a very simple <span class="caps">API</span> consisting of a single function:</p>
<ul class="simple">
<li><tt class="docutils literal">sort_by_params(query, params, allowed_sort_fields)</tt>: Pass it the query, the <tt class="docutils literal"><span class="caps">GET</span></tt> request parameters you got from your form and the declared sort fields array to return you a sorted query</li>
</ul>
<p>You can find a sample of the technique presented in this article in my <span class="caps">PHXCRD</span> repository:
<a class="reference external" href="https://github.com/spapas/phxcrd">https://github.com/spapas/phxcrd</a> for example in the <tt class="docutils literal">user_controller</tt> or <tt class="docutils literal">authority_controller</tt>.</p>
</div>
<div class="section" id="preparing-the-query">
<h2>Preparing the query</h2>
<p>In order to use dynamic sorting you’ll need to properly “prepare” your Ecto query by <em>naming all your relations</em>
as I’ve already explained in the <a class="reference external" href="https://spapas.github.io/2019/07/25/declarative-ecto-query-filters/">previous article</a>.</p>
</div>
<div class="section" id="declaring-the-sort-fields">
<h2>Declaring the sort fields</h2>
<p>To declare the sort fields you’ll just add an array of fields you’ll want to allow sorting on. Each field
should have the form <tt class="docutils literal">binding_name__field_name</tt> where <tt class="docutils literal">binding_name</tt> is the name of the table
you’ve declared in your
query and <tt class="docutils literal">field_name</tt> is the name of the field that the query will be sorted by. This is the way
that the sort
fields will also be declared in the phoenix html page. Django users will definitely remember the
<tt class="docutils literal">model__field</tt> convention.</p>
<p>Declaring the sort fields here and using them again in the html page may seem reduntant, however
it is absolute necessary to declare a priori which fields are allowed because the sort http params
will be received
as strings and to be used in queries these strings will be converted to atoms. The number of atoms is
finite (there’s an absolute limit of allowed atoms in an erlang program; if that limit is surpassed
your program will crash) so you can’t allow the user to pass whatever he wants (so if the <tt class="docutils literal">order_by</tt>
parameter does not contain one of the fields you declare here then no strings will be converted to atoms).</p>
</div>
<div class="section" id="integrating-with-a-controller">
<h2>Integrating with a controller</h2>
<p>As an example let’s see how the dynamic sort fields will be integrated with the phxcrd user_controller.
The query I’d like to filter on is the following (see that everything I’ll need is named using <tt class="docutils literal">:as</tt>):</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">authority_id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">UserPermission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Permission</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:permission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">permission_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">p</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">preload</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">authority</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="ss">permissions</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="p">]</span>
<span class="p">)</span>
</pre></div>
<p>To declare the sort fields I like to create a module attribute ending with <tt class="docutils literal">sort_fields</tt>, something like
<tt class="docutils literal">@user_sort_fields</tt> for example. Here’s the sort fields I’m going to use for user_controller:</p>
<div class="highlight"><pre><span></span><span class="na">@user_sort_fields</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="s2">"user__username"</span><span class="p">,</span><span class="w"> </span><span class="s2">"user__name"</span><span class="p">,</span><span class="w"> </span><span class="s2">"user__last_login"</span>
<span class="p">]</span>
</pre></div>
<p>So it will only allow the <tt class="docutils literal">user.username</tt>, <tt class="docutils literal">user.name</tt> and <tt class="docutils literal">user.last_login</tt> fields for sorting.
I could easily sort by <tt class="docutils literal">authority.name</tt> or <tt class="docutils literal">permission.name</tt> in a similar fashion.</p>
<p>Finally, here’s the full code of the index controller:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">index</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryFilterEx</span><span class="o">.</span><span class="n">get_changeset_from_params</span><span class="p">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="na">@user_filters</span><span class="p">)</span>
<span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span>
<span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">authority_id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">UserPermission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Permission</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:permission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">permission_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">p</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">preload</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">authority</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="ss">permissions</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="p">]</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">QueryFilterEx</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="na">@user_filters</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">QueryFilterEx</span><span class="o">.</span><span class="n">sort_by_params</span><span class="p">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="na">@user_sort_fields</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"index.html"</span><span class="p">,</span><span class="w"> </span><span class="ss">users</span><span class="p">:</span><span class="w"> </span><span class="n">users</span><span class="p">,</span><span class="w"> </span><span class="ss">changeset</span><span class="p">:</span><span class="w"> </span><span class="n">changeset</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
<p>Notice that this is exactly the
same as the controller I discussed in the dynamic filters article with the addition of the
<tt class="docutils literal">QueryFilterEx.sort_by_params(params, @user_sort_fields)</tt> pipe to do the sorting.</p>
</div>
<div class="section" id="the-template">
<h2>The template</h2>
<p>The template for the user index action is also the same with a couple of minor changes: Instead of
using a static header for the table title I will use a link that will change the sorting order:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">thead</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>
<span class="err"><</span>%= link gettext("Username"), to: create_order_url(@conn, "user__username") %>
<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>
<span class="err"><</span>%= link gettext("Name"), to: create_order_url(@conn, "user__name") %>
<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>First name<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>Last name<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>Email<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>Am / Am phxcrd<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>Kind<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>
<span class="err"><</span>%= link gettext("Last login"), to: create_order_url(@conn, "user__last_login") %>
<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>Is enabled<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></</span><span class="nt">th</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"></</span><span class="nt">thead</span><span class="p">></span>
</pre></div>
<p>Notice that I just used the <tt class="docutils literal">create_order_url</tt> function passing it the <tt class="docutils literal">@conn</tt> and the
sort field. This <tt class="docutils literal">create_order_url</tt> function is implemented in a module I include in
all my views and will properly add an <tt class="docutils literal">order_by=field</tt> in the url (it will also add
an <tt class="docutils literal"><span class="pre">order_by=-field</span></tt> if the same header is clicked twice). I will explain it more
in the following sections.</p>
<p>Finally, please notice that if you use pagination and sorting you need to properly handle the <tt class="docutils literal">order_by</tt>
query parameter when creating the next-previous page links. Actually, there are three things
competing on their url parameter dominance; I’d like to talk about that in the next interlude.</p>
<div class="section" id="interlude-http-get-parameter-priority">
<h3>Interlude: <span class="caps">HTTP</span> <span class="caps">GET</span> parameter priority</h3>
<p>Now, in an index page you will probably have three things all of which will want to put parameters
to your urls to be activated:</p>
<ul class="simple">
<li>Query filtering; this will put a <tt class="docutils literal">filter</tt> query parameter to filter your query. Notice that because of how phoenix works (it allows maps in the query parameters) the filter can be a single query parameter but contain multiple filters (i.e the filter will be something like <tt class="docutils literal"><span class="pre">%{"key1"</span> => "value1", "key2" => "value2"}</tt></li>
<li>Order by: This will put an <tt class="docutils literal">order_by</tt> query parameter to denote the field that the query will be sorted</li>
<li>Pagination: This will put an <tt class="docutils literal">page</tt> query parameter to denote the current page</li>
</ul>
<p>I like to give them a priority in the order I’ve listed them; when one of them is changed, it will
<em>clear</em> the ones following it. So if the query filters are changed both the pagination and the order by
fields will be cleared, if the order by field is changed then only the pagination field will be cleared
but if the pagination field is changed both the query filters and the order by fields will be kept there.</p>
<p>I think that’s the best way to do it from an <span class="caps">UX</span> point of view; try to think about it and you’ll probably agree.</p>
</div>
</div>
<div class="section" id="how-does-this-work">
<h2>How does this work?</h2>
<p>In this section I’ll try to explain exactly how the dynamic sort fields work.</p>
<p>So I’ll split this explanation in two parts: Explain <tt class="docutils literal">create_order_url</tt>
and then explain <tt class="docutils literal">sort_by_params</tt>.</p>
<div class="section" id="create-order-url">
<h3><tt class="docutils literal">create_order_url</tt></h3>
<p>This function receives three parameters: The current <tt class="docutils literal">@conn</tt>, the name of a <tt class="docutils literal">field</tt> to
sort by and an optional list of query parameters that need to be kept while creating the
order by links. I’ve put this function in a <tt class="docutils literal">ViewHelpers</tt> module that I am including to
all my views (by adding an <tt class="docutils literal">import PhxcrdWeb.ViewHelpers</tt> line to the <tt class="docutils literal">PhxcrdWeb</tt> module).</p>
<p>Let’s take a look at the code:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">create_order_url</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">field_name</span><span class="p">,</span><span class="w"> </span><span class="n">allowed_keys</span><span class="w"> </span><span class="p">\\</span><span class="w"> </span><span class="p">[</span><span class="s2">"filter"</span><span class="p">])</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nc">Phoenix.Controller</span><span class="o">.</span><span class="n">current_url</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">get_order_params</span><span class="p">(</span><span class="n">conn</span><span class="o">.</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">allowed_keys</span><span class="p">,</span><span class="w"> </span><span class="n">field_name</span><span class="p">))</span>
<span class="k">end</span>
</pre></div>
<p>This doesn’t do much, it just uses the phoenix’s <tt class="docutils literal">current_url</tt> that generates a new
url to the current page, passing it a dictionary
of http get parameters that should be appended to the url that
are created through <tt class="docutils literal">get_order_params</tt>. Notice that there’s an
<tt class="docutils literal">allowed_keys</tt> parameter that contains the query parameters that we need to keep after
the sorting (see the previous interlude).
By default I pass the <tt class="docutils literal">filter</tt> query parameter so if theres a filter (check
my previous article) it will keep it when sorting (but any pagination will be cleared;
if I sort by a new field I want to go to the first page there’s no reason for me to keep
seeing the page I was on before changing the order by).</p>
<p>The <tt class="docutils literal">get_order_params</tt> receives the query parameters of the current connection (as a map),
the allowed keys I mentioned before and the actual name of the field to sort on. This method is a
little more complex:</p>
<div class="highlight"><pre><span></span><span class="kd">defp</span><span class="w"> </span><span class="n">get_order_params</span><span class="p">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">allowed_keys</span><span class="p">,</span><span class="w"> </span><span class="n">order_key</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">params</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">take</span><span class="p">(</span><span class="n">allowed_keys</span><span class="w"> </span><span class="o">++</span><span class="w"> </span><span class="p">[</span><span class="s2">"order_by"</span><span class="p">])</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="k">fn</span><span class="w"> </span><span class="p">{</span><span class="n">k</span><span class="p">,</span><span class="w"> </span><span class="n">v</span><span class="p">}</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="p">{</span><span class="nc">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="n">k</span><span class="p">),</span><span class="w"> </span><span class="n">v</span><span class="p">}</span><span class="w"> </span><span class="k">end</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">update</span><span class="p">(</span>
<span class="w"> </span><span class="ss">:order_by</span><span class="p">,</span>
<span class="w"> </span><span class="n">order_key</span><span class="p">,</span>
<span class="w"> </span><span class="o">&</span><span class="k">case</span><span class="w"> </span><span class="ni">&1</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="o">^</span><span class="n">order_key</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">order_key</span>
<span class="w"> </span><span class="o">^</span><span class="n">order_key</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="n">order_key</span>
<span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="n">order_key</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
<p>It only keeps the parameters in the <tt class="docutils literal">allowed_keys</tt> list and the current <tt class="docutils literal">order_by</tt> parameter
(if there’s one) discarding everything else. It will then convert the keys of the map to atoms and
put them in a new map. Finally, it will update the <tt class="docutils literal">order_by</tt> field (if exists) either by
switching the <tt class="docutils literal">-</tt> in front of the field to declare asc/desc sorting or adding it for the field
that was clicked. Actually the logic of that <tt class="docutils literal">Map.update</tt> is the following:</p>
<ul class="simple">
<li>If there’s no <tt class="docutils literal">:order_by</tt> key then add it and assign the passed <tt class="docutils literal">order_key</tt></li>
<li>If the current value of <tt class="docutils literal">:order_by</tt> is equal to <tt class="docutils literal">order_key</tt> with or without a <tt class="docutils literal">-</tt> then toggle the <tt class="docutils literal">-</tt> (this happens when you click on a field that is already used for sorting)</li>
<li>If the current value of <tt class="docutils literal">:order_by</tt> is anything else (i.e not the same as the <tt class="docutils literal">order_key</tt>) then just change <tt class="docutils literal">:order_by</tt> to <tt class="docutils literal"><span class="pre">-orderKey</span></tt> (this happens when there’s sorting but you click on a different field, not the one used for the sorting)</li>
</ul>
<p>Notice that this juggling between map, list of keywords and then map again (using <tt class="docutils literal">Enum.map</tt> and then
<tt class="docutils literal">Map.new</tt> etc) is needed because
the query parameters are in a map with strings as keys form (<tt class="docutils literal"><span class="pre">%{"key"</span> => "value"}</tt>) while
the <tt class="docutils literal">current_url</tt> function needs the query params in a map with atoms as keys form
(<tt class="docutils literal">%{key: "value"}</tt>).</p>
</div>
<div class="section" id="sort-by-params">
<h3><tt class="docutils literal">sort_by_params</tt></h3>
<p>The <tt class="docutils literal">sort_by_params</tt> method gets three parameters: The <tt class="docutils literal">query</tt> that will be sorted,
the existing http parameters map (so as to retrieve the <tt class="docutils literal">order_by</tt> value)
and the declared list of allowed sorting fields. Let’s take a look at it:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">sort_by_params</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span><span class="w"> </span><span class="p">%{</span><span class="s2">"order_by"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o"><></span><span class="w"> </span><span class="n">val</span><span class="p">},</span><span class="w"> </span><span class="n">allowed</span><span class="p">),</span>
<span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">do_sort_by_params</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span><span class="w"> </span><span class="n">val</span><span class="p">,</span><span class="w"> </span><span class="ss">:asc</span><span class="p">,</span><span class="w"> </span><span class="n">allowed</span><span class="p">)</span>
<span class="kd">def</span><span class="w"> </span><span class="n">sort_by_params</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span><span class="w"> </span><span class="p">%{</span><span class="s2">"order_by"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="n">val</span><span class="p">},</span><span class="w"> </span><span class="n">allowed</span><span class="p">),</span>
<span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">do_sort_by_params</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span><span class="w"> </span><span class="n">val</span><span class="p">,</span><span class="w"> </span><span class="ss">:desc</span><span class="p">,</span><span class="w"> </span><span class="n">allowed</span><span class="p">)</span>
<span class="kd">def</span><span class="w"> </span><span class="n">sort_by_params</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">,</span><span class="w"> </span><span class="bp">_</span><span class="p">),</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="n">qs</span>
</pre></div>
<p>This multi-legged function will only do something if there’s an <tt class="docutils literal">order_by</tt> parameter in the http
parameters (else it will just return the query as is) and will call <tt class="docutils literal">do_sort_by_params</tt> passing
it the received query,
either <tt class="docutils literal">:asc</tt> or <tt class="docutils literal">:desc</tt> (depending if there’s a <tt class="docutils literal">-</tt> in front of the value) and the
received allowed fields list.</p>
<p>The <tt class="docutils literal">do_sort_by_params</tt> makes sure that the passed parameter is in the allowed list
and if yes
it creates the atoms of the binding and field name (using <tt class="docutils literal">String.to_atom</tt>) and
does the actual sorting to the passed query:</p>
<div class="highlight"><pre><span></span><span class="kd">defp</span><span class="w"> </span><span class="n">do_sort_by_params</span><span class="p">(</span><span class="n">qs</span><span class="p">,</span><span class="w"> </span><span class="n">val</span><span class="p">,</span><span class="w"> </span><span class="n">ord</span><span class="p">,</span><span class="w"> </span><span class="n">allowed</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">val</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="n">allowed</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">[</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">val</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"__"</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&</span><span class="nc">String</span><span class="o">.</span><span class="n">to_atom</span><span class="o">/</span><span class="mi">1</span><span class="p">)</span>
<span class="w"> </span><span class="n">qs</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">order_by</span><span class="p">([{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span><span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">ord</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">name</span><span class="p">)}])</span>
<span class="w"> </span><span class="k">else</span>
<span class="w"> </span><span class="n">qs</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>The line <tt class="docutils literal">qs |> <span class="pre">order_by([{^binding,</span> <span class="pre">t}],</span> <span class="pre">[{^ord,</span> field(t, <span class="pre">^name)}])</span></tt> may seem a little
complex but it has been thoroughly explained in the previous article.</p>
</div>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>By using the methods described here you can easily add a dynamic sorting to your queries
through fields that may span relations just by creating a bunch of http <span class="caps">GET</span> links
and passing them an <tt class="docutils literal">order_by</tt> query parameter.</p>
</div>
How to properly handle an HTML form2019-10-03T14:20:00+03:002019-10-03T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2019-10-03:/2019/10/03/html-form-submit-php/<p class="first last">An introductory tutorial on how to properly handle a form. <span class="caps">PHP</span> is used for pedagogical reasons.</p>
<p>One of the simplest things somebody would like to do in a web application is to display a form to the user and then handle the data the application
received from the form. This is a very common task however there are many loose ends and things that a new developer can do wrong. Recently, I tried
to explain this to somebody however I couldn’t find a tutorial containing all the information I think it is important. So I decided to write it here.</p>
<p>To make it very easy to test it and understand it I decided to use <span class="caps">PHP</span> to handle the form submission. I must confess that I don’t like <span class="caps">PHP</span> and I never
use it in production apps (unless I need to support an existing one). However, for such simple tasks and examples <span class="caps">PHP</span> is probably the easiest thing for
a new developer to understand: Just create a <tt class="docutils literal">.php</tt> file and put it in a folder in your apache <tt class="docutils literal">htdocs</tt>; there now you can test the behavior!</p>
<p>This tutorial will be as comprehensive as possible and will try to explain all things that I feel that need explaining. However I won’t go into details
about <span class="caps">HTML</span> or <span class="caps">PHP</span> syntax; you’ll need to know some things about them to understand what’s going on here; after all the important thing is to understand
the big picture so you can re-implement them in your case (or understand why your web framework does what it does).</p>
<div class="section" id="a-quick-http-primer">
<h2>A quick <span class="caps">HTTP</span> primer</h2>
<p>So, what happens during a form display and submission? To properly understand that you need to know what an <span class="caps">HTTP</span> request is. An <span class="caps">HTTP</span> request is the way
the browser “requests” a <span class="caps">URL</span> from a web server. This is thoroughly explained in the <span class="caps">HTTP</span> protocol however, for our purposes here it should suffice to say
that an <span class="caps">HTTP</span> request is a text message that is send from the browser/<span class="caps">HTTP</span> Client to the <span class="caps">HTTP</span>/web server. This message should have the following format:</p>
<pre class="code literal-block">
METHOD PATH HTTP/VERSION
Header 1: Value 1
Header 2: Value 2
Request data
</pre>
<p>The <tt class="docutils literal"><span class="caps">METHOD</span></tt> can be one of various methods supported in the <span class="caps">HTTP</span> protocol like <cite><span class="caps">GET</span>, <span class="caps">POST</span>, <span class="caps">PUT</span>, <span class="caps">OPTIONS</span></cite> etc however the most popular and the ones we’ll talk about here
are <tt class="docutils literal"><span class="caps">GET</span></tt> and <tt class="docutils literal"><span class="caps">POST</span></tt>. The <tt class="docutils literal"><span class="caps">PATH</span></tt> is the actual url of the “page” you want to view without the server part. So if you are visiting <tt class="docutils literal"><span class="pre">http://www.example.com/path/to/page.html</span></tt>
the path parameter will have a value of <tt class="docutils literal">/path/to/page.html</tt>. The <tt class="docutils literal"><span class="caps">HTTP</span>/<span class="caps">VERSION</span></tt> will contain the version of <span class="caps">HTTP</span> the client uses; usually it is something like
<tt class="docutils literal"><span class="caps">HTTP</span>/1.1</tt>. Finally, after that first line there’s a bunch of optional headers with various extra information the client wants to pass to the
server (for example what encoding it supports, what’s the host it connects to etc).</p>
<p>Additionally, in case
of a <tt class="docutils literal"><span class="caps">POST</span></tt> request the headers are followed by a blank line which in turn is followed by a chunk of “data” that the client passes to the server.</p>
<p>The server will then response back with a text message similar to this (<span class="caps">HTTP</span> Response):</p>
<pre class="code literal-block">
HTTP/VERSION STATUS
Header 1: Value 1
Header 2: Value 2
Response data
</pre>
<p>The <tt class="docutils literal"><span class="caps">HTTP</span>/<span class="caps">VERSION</span></tt> will also be something like <tt class="docutils literal"><span class="caps">HTTP</span>/1.1</tt> while the <tt class="docutils literal"><span class="caps">STATUS</span></tt> will be the status of the response. This status
is a 3-digit numeric value followed by a textual description of the
status. There are <a class="reference external" href="https://httpstatuses.com/">various statuses that you can receive</a>, however the statuses can be grouped by the number they start with like this:</p>
<ul class="simple">
<li>1xx: Information; rarely used</li>
<li>2xx: Success; status 200 is the most common one</li>
<li>3xx: Redirection (browser must visit another page); either permanent or temporary</li>
<li>4xx: Client error; 404 is page not found (also access denied errors will be 40x)</li>
<li>5xx: Server error; something fishy happened to the server while responding</li>
</ul>
<p>In this article we’ll mainly talk about 2xx and 3xx: A <tt class="docutils literal">200 <span class="caps">OK</span></tt> answer is the most common one, it means that the <span class="caps">HTTP</span> request was
completed successfully. A <tt class="docutils literal">302 <span class="caps">FOUND</span></tt> request means that the browser should display a “different” path; that path will be provided in
a <tt class="docutils literal">Location: path</tt> header. When the browser receives a redirect it will do another <tt class="docutils literal"><span class="caps">GET</span></tt> request to retrieve the redirect to page.</p>
<p>Notice that the Headers and Data parts of the server reply may also be optional like for the client however they usually exist
(especially with a 200 response; without the data the client won’t have anything to display).</p>
<p>So when a browser “requests” a page it will send an <span class="caps">HTTP</span> <tt class="docutils literal"><span class="caps">GET</span></tt> to the path. This happens all the time when we visit(click) links or entering
urls to our browser. However, when we submit an <span class="caps">HTML</span> form the situation is a little more complex.</p>
</div>
<div class="section" id="form-methods">
<h2>Form methods</h2>
<p>An html form is a <tt class="docutils literal"><form></tt> tag that contains a bunch of <tt class="docutils literal"><input></tt> elements each one of which should have <em>at least</em> a name property.
One of the inputs will usually be is a submit button.</p>
<p>Also, the form tag has two important attributes:</p>
<ul class="simple">
<li><tt class="docutils literal">action</tt>: Defines the url where the post will be submitted to; it can be omitted to submit the form to the current path</li>
<li><tt class="docutils literal">method</tt>: Will be either <span class="caps">GET</span> or <span class="caps">POST</span></li>
</ul>
<p>Thus, a sample form is something like:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">"GET"</span> <span class="na">action</span><span class="o">=</span><span class="s">"form.php"</span> <span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'text'</span> <span class="na">name</span><span class="o">=</span><span class="s">'input1'</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'text'</span> <span class="na">name</span><span class="o">=</span><span class="s">'input2'</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">'submit'</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
</pre></div>
<p>So what are the differences between a <tt class="docutils literal"><form <span class="pre">method='<span class="caps">GET</span>'></span></tt> and a <tt class="docutils literal"><form <span class="pre">method='<span class="caps">POST</span>'></span></tt>?</p>
<ul class="simple">
<li>Submitting an <span class="caps">HTML</span> form will translate to either an <span class="caps">HTTP</span> <span class="caps">GET</span> request or an <span class="caps">HTTP</span> <span class="caps">POST</span> request to the server depending on the method attribute</li>
<li>The data of a <span class="caps">GET</span> form will be encoded in the <span class="caps">PATH</span> of the <span class="caps">HTTP</span> request while the data of the <span class="caps">POST</span> form will be in the corresponding data part of the <span class="caps">HTTP</span> Request</li>
<li>A form that is submitted with <span class="caps">GET</span> should be idempotent i.e it should <em>not</em> modify anything in the server; a form that is submitted with <span class="caps">POST</span> should modify something the server</li>
</ul>
<p>So, the form we defined previously (that has a <span class="caps">GET</span> method) will issue the following <span class="caps">HTTP</span> request (if we fill the values value1 and value2 to the inputs):</p>
<pre class="code literal-block">
GET /form.php?input1=value1&input2=value2 HTTP/1.1
</pre>
<p>One the other hand if the form had a <span class="caps">POST</span> method the <span class="caps">HTTP</span> request would be like this:</p>
<pre class="code literal-block">
POST /form.php HTTP/1.1
input1=value1&input2=value2
</pre>
<p>Notice that in the first case the data is in the <span class="caps">PATH</span> in the 1st line of the request while in the second case it is passed in the data section.
Also, in both cases the encoded data of the form is similar to <tt class="docutils literal">input1=value1&input2=value2</tt>: it is a list of <tt class="docutils literal">key=value</tt> pairs seperated
with <tt class="docutils literal">&</tt> where the <tt class="docutils literal">name</tt> attribute of each <tt class="docutils literal">input</tt> is used as the key.</p>
<p>Concerning the idempotency of the action; this is not something that the <span class="caps">HTTP</span> protocol can enforce but it relies on the developer to implement it.
When a form will not change anything to the server then it should be implemented as a method=<span class="caps">GET</span>. For example when you have a form with a search
box that just returns some results. On the other hand, when a form does change things for example when you create a new item in an application
then it should be implemented as a method=<span class="caps">POST</span>.</p>
<p>The browser has a different behavior after a <span class="caps">GET</span> vs after a <span class="caps">POST</span> because it expects that when you do a <span class="caps">GET</span> request then it won’t matter if that
request is repeated many times. One the other hand, the browser will try to prevent you from submitting a request many times (because something is
changed in the server so the user must do it intentionally by for example pressing a button to submit the form).</p>
</div>
<div class="section" id="proper-form-handling">
<h2>Proper form handling</h2>
<p>So, following the previous section we can now explain how to handle a form properly:</p>
<p>For a <span class="caps">GET</span> form we don’t have to do anything fancy; we just retrieve the parameter values and we display the data these parameters correspond to
just like if we displayed any other page.</p>
<p>For a <span class="caps">POST</span> form however we need to be extra careful as to avoid re-duplication of data and have a good user experience: When a <span class="caps">POST</span> form is
submitted we need first to make sure that the submitted parameters are valid (for example there are no missing required fields). If the form is not
valid then we will return a status 200 <span class="caps">OK</span> explaining to the user what went wrong; we usually return the same page containing the initial form with
the fields that had errors marked.</p>
<p>On the other hand, if the form <em>was valid</em> we need to do the actual action that the form corresponds to; for example insert something to the database.
After this is finished we should return a <em>redirect</em> (302) to either a different or even the same page. This will result to the browsing doing a
<span class="caps">GET</span> request to the page we redirect to so there would be no danger of the user refreshing the page and resubmitting the form. We should <em>not</em>
return a 200 <span class="caps">OK</span> after a <span class="caps">POST</span> request because then the user would be able to press F5 to duplicate the previous <span class="caps">POST</span> request (and re-insert the data).</p>
<p>One extra thing that we need to consider is how should we inform the user that his action was successful after the form submission and
redirection? As we said, we can’t return a 200 <span class="caps">OK</span> message so we can’t really “create” the response, we instead need to redirect to another page.
A common practice for this is to use a “flash” message; this is offered by many web frameworks through specific functions
but can be easily implemented. I’ll explain how in the next section.</p>
</div>
<div class="section" id="implementing-flash-messages">
<h2>Implementing flash messages</h2>
<p>Before talking about the flash message I’d like to quickly explain what’s a cookie and a browser session in <span class="caps">HTTP</span>, because
the flash message builds upon these concepts:</p>
<p>A cookie is a way for the server to tell the client to store some information to be re-used later. What happens is that
the server returns an <span class="caps">HTTP</span> header line similar to <tt class="docutils literal"><span class="pre">Set-Cookie:</span> <span class="pre">cookie-name=cookie-value</span></tt>. When the client receives that
header line it will pass an <span class="caps">HTTP</span> header line similar to <tt class="docutils literal">Cookie: <span class="pre">cookie-name=cookie-value</span></tt> to all future requests
so the server will know which value it had send to the client (there are various options for expiring cookies etc but they
are not important here). This may seem like a primitive solution however because <span class="caps">HTTP</span> is a stateless protocol
that’s the only way for the server to store information about a client. If you disable cookies completely in a browser
then there won’t be a way for the server to remember you, for example you won’t be able to login anywhere!</p>
<p>A session is a better way to store info about the client that builds upon the cookies. What happens is that when a client visits
a site for the first time (so it has no cookies for that particular site) the server will send back a cookie named <tt class="docutils literal">session_id</tt> (or something like
that) containing a very big random number. The server will save this session id number in a persistent storage (for example in a
database or a text file) and will correlate that number with information for that particular client. When the client sends back that
<tt class="docutils literal">session_id</tt> cookie the server will fetch the correlated info for that particular client from the persistent storage (and may
update them etc). This way the server can store whatever info it wants about a particular client. The server usually keeps a
a dictionary (map) of key-values for each session.</p>
<p>Now, a flash message is some information (message) that should be displayed to the user <em>once</em>. For example a message like
“Your form has been submitted!”.</p>
<p>A simple way to implement this is to add a <tt class="docutils literal">message</tt>
attribute to the session when you want to display the flash message. The next page that is displayed (irrelevantly if there’s a redirect involved)
will check to see if there’s a <tt class="docutils literal">message</tt> attribute to the session; if yes it will display the actual message and remove the <tt class="docutils literal">message</tt> attribute
from the session (so it won’t be displayed again).</p>
</div>
<div class="section" id="implementing-the-form-submission">
<h2>Implementing the form submission</h2>
<p>Following the above guidelines I’ll present here a typical, production-ready form submission for <span class="caps">PHP</span>. Some choices I’ve made:</p>
<ul class="simple">
<li>I am going to implement a <span class="caps">POST</span> form since a <span class="caps">GET</span> form doesn’t need any special handling</li>
<li>The form handler will be the same <span class="caps">PHP</span> page as the one displaying the form. This is a usual thing to do, you check the <span class="caps">HTTP</span> method and either display the form as-is (if it is <span class="caps">GET</span>) or handle the submission (if it is <span class="caps">POST</span>)</li>
<li>When the form is submitted successfully redirect to the same page and display a flash message</li>
<li>Check for valid input and display the error message</li>
</ul>
<p>So without further ado here’s the complete php code that will submit your form; store it in a file named <tt class="docutils literal">test.php</tt>:</p>
<div class="highlight"><pre><span></span><span class="x"><!DOCTYPE HTML></span>
<span class="x"><html></span>
<span class="x"><head></span>
<span class="x"> <meta charset="UTF-8"></span>
<span class="x"> <meta name="viewport" content="width=device-width, initial-scale=1.0"></span>
<span class="x"> <meta http-equiv="X-UA-Compatible" content="ie=edge"></span>
<span class="x"> <title>TEST FORM</title></span>
<span class="x"> <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"></span>
<span class="x"></head></span>
<span class="x"><body class="bg-gray-100"></span>
<span class="x"> <div class="px-8 py-8 w-1/2 m-auto"></span>
<span class="x"> </span><span class="cp"><?php</span>
<span class="nb">session_start</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'message'</span><span class="p">]</span> <span class="o">??</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span>
<span class="k">echo</span> <span class="s1">'<div class="rounded bg-green-300 px-2 py-2">'</span><span class="o">.</span><span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'message'</span><span class="p">]</span><span class="o">.</span><span class="s1">'</div>'</span><span class="p">;</span>
<span class="nb">unset</span><span class="p">(</span><span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'message'</span><span class="p">]);</span>
<span class="p">}</span>
<span class="nv">$nameErr</span> <span class="o">=</span> <span class="nv">$wsErr</span> <span class="o">=</span> <span class="nv">$name</span> <span class="o">=</span> <span class="nv">$comment</span> <span class="o">=</span> <span class="s2">""</span><span class="p">;</span>
<span class="nv">$formValid</span> <span class="o">=</span> <span class="k">true</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s2">"REQUEST_METHOD"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"POST"</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s2">"name"</span><span class="p">]))</span> <span class="p">{</span>
<span class="nv">$nameErr</span> <span class="o">=</span> <span class="s2">"Name is required"</span><span class="p">;</span>
<span class="nv">$formValid</span> <span class="o">=</span> <span class="k">false</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nv">$name</span> <span class="o">=</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s2">"name"</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s2">"comment"</span><span class="p">]))</span> <span class="p">{</span>
<span class="nv">$comment</span> <span class="o">=</span> <span class="s2">""</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nv">$comment</span> <span class="o">=</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s2">"comment"</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$formValid</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="mi">1</span> <span class="o">==</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span> <span class="p">{</span>
<span class="nv">$_SESSION</span><span class="p">[</span><span class="s1">'message'</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Success! You have submitted the values: <b>"</span> <span class="o">.</span> <span class="nv">$name</span> <span class="o">.</span> <span class="s2">" / "</span> <span class="o">.</span> <span class="nv">$comment</span> <span class="o">.</span> <span class="s2">"</b>"</span><span class="p">;</span>
<span class="nb">header</span><span class="p">(</span><span class="s1">'Location: ./test.php'</span><span class="p">);</span>
<span class="k">exit</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nv">$wsErr</span> <span class="o">=</span> <span class="s2">"Error while trying to submit the form!"</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="cp">?></span>
<span class="x"> <h1 class="text-4xl font-bold text-indigo-500">Test a PHP form!</h1></span>
<span class="x"> <form class='border border-blue-800 rounded p-2' method="POST"></span>
<span class="x"> </span><span class="cp"><?</span><span class="o">=</span> <span class="nv">$wsErr</span> <span class="o">?</span> <span class="s2">"<div class='text-red-600 py-3'>"</span> <span class="o">.</span> <span class="nv">$wsErr</span> <span class="o">.</span> <span class="s2">"</div>"</span> <span class="o">:</span> <span class="s2">""</span> <span class="cp">?></span>
<span class="x"> <div class="p-1"></span>
<span class="x"> <label for="name">Name:</label> <input class="border border-blue-800 rounded" id="name" type="text" name="name" value="</span><span class="cp"><?</span><span class="o">=</span> <span class="nv">$name</span> <span class="cp">?></span><span class="x">"></span>
<span class="x"> <span class="text-red-600">* </span><span class="cp"><?</span><span class="o">=</span> <span class="nv">$nameErr</span><span class="p">;</span> <span class="cp">?></span><span class="x"></span></span>
<span class="x"> </div></span>
<span class="x"> <div class="p-1"></span>
<span class="x"> <label for="comment">Comment:</label> <textarea class="border border-blue-800 rounded" id="comment" name="comment" rows="5" cols="40"></span><span class="cp"><?</span><span class="o">=</span> <span class="nv">$comment</span> <span class="cp">?></span><span class="x"></textarea></span>
<span class="x"> </div></span>
<span class="x"> <input class="rounded px-2 my-4 py-2 bg-blue-800 text-gray-100" type="submit" name="submit" value="Save"></span>
<span class="x"> </form></span>
<span class="x"> </div></span>
<span class="x"></body></span>
<span class="x"></html></span>
</pre></div>
<p>So how is that working? As we can see there’s the php code first and the html is following (with some sprinkles of php). The <span class="caps">HTML</span> code is rather simple: It will
display a <tt class="docutils literal"><span class="caps">POST</span></tt> form with two inputs named <tt class="docutils literal">name</tt> and <tt class="docutils literal">comment</tt>. Notice that we pass the <tt class="docutils literal">$name</tt> and <tt class="docutils literal">$comment</tt> php variables as their values.
It also has a submit button and will display the <tt class="docutils literal">$wsErr</tt> variable if it is not null (which means that there was an error while submitting the data). The <span class="caps">PHP</span>
code now first starts the session (i.e it passes the <tt class="docutils literal">session_id</tt> cookie to the client if such a cookie does not exist) checks to see if there’s a
<tt class="docutils literal">message</tt> attribute to the session. If such a message exists it will display it in a rounded green panel and remove that from the session (so it won’t be displayed
again next time):</p>
<div class="highlight"><pre><span></span><span class="x">session_start();</span>
<span class="x">if ($_SESSION['message'] ?? '') {</span>
<span class="x"> echo '<div class="rounded bg-green-300 px-2 py-2">'.$_SESSION['message'].'</div>';</span>
<span class="x"> unset($_SESSION['message']);</span>
<span class="x">}</span>
</pre></div>
<p>After that there are some variable initializations and we check if the <span class="caps">HTTP</span> request is <tt class="docutils literal"><span class="caps">POST</span></tt> (if the request is <span class="caps">GET</span> we’ll just disply the <span class="caps">HTML</span>):</p>
<div class="highlight"><pre><span></span><span class="x">if ($_SERVER["REQUEST_METHOD"] == "POST") {</span>
</pre></div>
<p>For each of the inputs we check if they are empty or not and assign their values to the corresponding php variable. If the name is empty then we’ll
set <tt class="docutils literal">$formValid = false;</tt> and add an error message since this field is required. Then, if <tt class="docutils literal">$formValid</tt> is not false we can do the actual action
(for example write to the database). I’ve simulated that using a coin-toss with rand (so there’s a 50% possibility that the action will fail). If
the action “failed” then nothing happened in the database so we should return the same page with the <tt class="docutils literal">wsErr</tt> variable containing the error.</p>
<p>However if the action is successful that means that the data has been inserted to the database so we’ll need to set the flash message and do
the redirect (the name of the page containing the form is <tt class="docutils literal">/test.php</tt> so we’ll redirect to it):</p>
<div class="highlight"><pre><span></span><span class="x">$_SESSION['message'] = "Success! You have submitted the values: <b>" . $name . " / " . $comment . "</b>";</span>
<span class="x">header('Location: ./test.php');</span>
<span class="x">exit();</span>
</pre></div>
<p>The two commands above (<tt class="docutils literal">header</tt> and <tt class="docutils literal">exit</tt>) will do the actual redirect in php. Since the session contains the <tt class="docutils literal">message</tt> it will
be displayed after the redirect has finished!</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>So following the above tutorial, here’s what you should absolutely do to submit an <span class="caps">HTML</span> form:</p>
<ul class="simple">
<li>Use <span class="caps">HTTP</span> <span class="caps">POST</span> if the form is going to change data on the server; use <span class="caps">HTTP</span> <span class="caps">GET</span> otherwise (mainly for search/filter forms)</li>
<li>When using <span class="caps">POST</span>: Redirect when the form is valid and the action on the server has finished successfully; never return a 200 <span class="caps">OK</span> status when you’ve changed things in the server (database)</li>
<li>Use flash messages to pass information to the user after a redirect</li>
</ul>
</div>
Declarative Ecto query filters2019-07-25T14:20:00+03:002019-07-25T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2019-07-25:/2019/07/25/declarative-ecto-query-filters/<p class="first last">Being able to declare your Ecto query filter even on fields spanning joins</p>
<p>Continuing my Elixir <a class="reference external" href="https://spapas.github.io/2019/06/04/phoenix-form-select2-ajax/">journey</a> I’d like to discuss here a method to implement one of my
<a class="reference external" href="https://spapas.github.io/2017/10/11/essential-django-packages/">favorite Django features</a>: Declarative query filters. This functionality is not a core Django
feature but it is offered
through the excellent <a class="reference external" href="https://github.com/carltongibson/django-filter/:">django-filter</a> package: Using this, you can create a <tt class="docutils literal">Filter</tt> class which defines
which fields are to be used for filtering the queryset and how each field will be queried (i.e using
things like exact, like, year of date etc).</p>
<p>This is a functionality I am greatly missing in Elixir/phoenix
so I’ve tried implementing it on my own. Of course, django-filter has various other capabilities that
result from the implicit generation of things that Django offers like automatically creating the html
for the declared fields, automatically declare the fields based on their types etc but such things are
not supported by phoenix in any case so I won’t be trying in them here.</p>
<p>During my research I’ve seen a bunch of blog posts or packages about this thing however they didn’t
properly support joins (i.e you could only filter on fields on a specific schema) or needed too much
work to filter on joins (i.e define different filters for each part of the join). In the solution
I’ll present here you’ll just define a filter for the specific query you need to filter no matter how
many joins it has (just like in django-filters).</p>
<div class="section" id="what-will-it-do">
<h2>What will it do</h2>
<p>The solution is more or less a self contained Elixir module named <tt class="docutils literal">QueryFilterEx</tt> that can be used
to declaratively filter your queries.
To use that you’ll need to declare your filters using
a simple array of maps. The filters should then be added in your form using a different input
for each filter; then your queryset will be filtered with all the values you’ve added to the
inputs using <tt class="docutils literal"><span class="caps">AND</span></tt>.</p>
<p>The module has a very simple <span class="caps">API</span> consisting of three functions:</p>
<ul class="simple">
<li><tt class="docutils literal">get_changeset_from_params(params, filters)</tt>: Pass it the <tt class="docutils literal"><span class="caps">GET</span></tt> request parameters you got from your form and the declared filters array to return you a proper changeset (which you can then use to build your form in your html)</li>
<li><tt class="docutils literal">make_filter_changeset(filters, params)</tt>: This function actually generates the changeset using the filters and a <tt class="docutils literal">Map</tt> of <tt class="docutils literal">filter_name: value</tt> pairs (it is actually used by <tt class="docutils literal">get_changeset_from_params</tt>)</li>
<li><tt class="docutils literal">filter(query, changeset, filters)</tt>: Filter the <tt class="docutils literal">query</tt> using the previously created <tt class="docutils literal">changeset</tt> and the declared filters array</li>
</ul>
<p>You can find a sample of the technique presented in this article in my <span class="caps">PHXCRD</span> repository:
<a class="reference external" href="https://github.com/spapas/phxcrd">https://github.com/spapas/phxcrd</a> for example in the <tt class="docutils literal">user_controller</tt> or <tt class="docutils literal">authority_controller</tt>.</p>
</div>
<div class="section" id="preparing-the-query">
<h2>Preparing the query</h2>
<p>In order to use the <tt class="docutils literal">QueryFilterEx</tt> module you’ll need to properly “prepare” your Ecto query. By preparing
I don’t mean a big deal just the fact that you’ll need to <em>name all your relations</em> (or at least name all
the relations you’re going to use for filtering). This is very simple to do, for example for the following query:</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">join</span><span class="p">:</span><span class="w"> </span><span class="n">ak</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">AuthorityKind</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">authority_kind_id</span><span class="p">],</span>
<span class="w"> </span><span class="ss">preload</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">authority_kind</span><span class="p">:</span><span class="w"> </span><span class="n">ak</span><span class="p">]</span>
<span class="p">)</span>
</pre></div>
<p>you can name the relations by adding two <tt class="docutils literal">as:</tt> atoms like this:</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">join</span><span class="p">:</span><span class="w"> </span><span class="n">ak</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">AuthorityKind</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority_kind</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">authority_kind_id</span><span class="p">],</span>
<span class="w"> </span><span class="ss">preload</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">authority_kind</span><span class="p">:</span><span class="w"> </span><span class="n">ak</span><span class="p">]</span>
<span class="p">)</span>
</pre></div>
<p>So after each <cite>join:</cite> you’ll add a name for your joined relation (and also add a name for your initial
relation). Please notice that you can use any name you want for these (not related to the schema names).</p>
</div>
<div class="section" id="declaring-the-filters">
<h2>Declaring the filters</h2>
<p>To declare the filters you’ll just add an array of simple Elixir maps. Each map must have the following fields:</p>
<ul class="simple">
<li><tt class="docutils literal">:name</tt> This is the name of the specific filter; it is mainly used in conjunction with the queryset and the form fields to set initial values etc</li>
<li><tt class="docutils literal">:type</tt> This is the type of the specific filter; it should be a proper Ecto type like <tt class="docutils literal">:string</tt>, <tt class="docutils literal">:date</tt>, <tt class="docutils literal">:integer</tt> etc. This is needed to properly cast the values and catch errors</li>
<li><tt class="docutils literal">:binding</tt> This is the name of the relation this filter concerns which you defined in your query using <tt class="docutils literal">:as</tt> (discussed in previous section)</li>
<li><tt class="docutils literal">:field_name</tt> This is the actual name of the field you want to filter on</li>
<li><tt class="docutils literal">:method</tt> How to filter on this field; I’ve defined a couple of methods I needed but you can implement anything you want</li>
</ul>
<p>The methods I’ve implemented are the following:</p>
<ul class="simple">
<li><tt class="docutils literal">:eq</tt> Equality</li>
<li><tt class="docutils literal">:ilike</tt> Field value starts with the input - ignore case</li>
<li><tt class="docutils literal">:icontains</tt> Field value contains the input - ignore case</li>
<li><tt class="docutils literal">:year</tt> Field is a date or datetime an its year is the same as the value</li>
<li><tt class="docutils literal">:date</tt> Field is a datetime and its date part is equal to the value</li>
</ul>
<p>Anything else will just be compared using <tt class="docutils literal">=</tt> (same as <tt class="docutils literal">:eq</tt>).</p>
</div>
<div class="section" id="integrating-it-with-a-controller">
<h2>Integrating it with a controller</h2>
<p>As an example let’s see how <tt class="docutils literal">QueryFilterEx</tt> is integrated it with the phxcrd user_controller.
The query I’d like to filter on is the following (see that everything I’ll need is named using <tt class="docutils literal">:as</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">authority_id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">UserPermission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Permission</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:permission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">permission_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">p</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">preload</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">authority</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="ss">permissions</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="p">]</span>
<span class="p">)</span>
</pre></div>
<p>To declare the filters I like to create a module attribute ending with <tt class="docutils literal">filters</tt>, something like
<tt class="docutils literal">@user_filters</tt> for example. Here’s the filters I’m going to use for user_controller:</p>
<div class="highlight"><pre><span></span><span class="na">@user_filters</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="ss">:username</span><span class="p">,</span><span class="w"> </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">binding</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">,</span><span class="w"> </span><span class="ss">field_name</span><span class="p">:</span><span class="w"> </span><span class="ss">:username</span><span class="p">,</span><span class="w"> </span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="ss">:ilike</span><span class="p">},</span>
<span class="w"> </span><span class="p">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority_name</span><span class="p">,</span><span class="w"> </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">binding</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span><span class="w"> </span><span class="ss">field_name</span><span class="p">:</span><span class="w"> </span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="ss">:icontains</span><span class="p">},</span>
<span class="w"> </span><span class="p">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="ss">:permission_name</span><span class="p">,</span><span class="w"> </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">binding</span><span class="p">:</span><span class="w"> </span><span class="ss">:permission</span><span class="p">,</span><span class="w"> </span><span class="ss">field_name</span><span class="p">:</span><span class="w"> </span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="ss">:ilike</span><span class="p">},</span>
<span class="w"> </span><span class="p">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="ss">:last_login_date</span><span class="p">,</span><span class="w"> </span><span class="ss">type</span><span class="p">:</span><span class="w"> </span><span class="ss">:date</span><span class="p">,</span><span class="w"> </span><span class="ss">binding</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">,</span><span class="w"> </span><span class="ss">field_name</span><span class="p">:</span><span class="w"> </span><span class="ss">:last_login</span><span class="p">,</span><span class="w"> </span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="ss">:date</span><span class="p">}</span>
<span class="p">]</span>
</pre></div>
<p>So it will check if the <tt class="docutils literal">user.username</tt> and <tt class="docutils literal">permission.name</tt> start with the passed value,
<tt class="docutils literal">authority.name</tt> contains the passed value and if the <tt class="docutils literal">user.login_date</tt> (which is a datetime)
is the same as the passed date value.</p>
<p>Finally, here’s the full code of the index controller:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">index</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">QueryFilterEx</span><span class="o">.</span><span class="n">get_changeset_from_params</span><span class="p">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="na">@user_filters</span><span class="p">)</span>
<span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span>
<span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">authority_id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">UserPermission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">user_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">left_join</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Permission</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:permission</span><span class="p">,</span>
<span class="w"> </span><span class="ss">on</span><span class="p">:</span><span class="w"> </span><span class="n">up</span><span class="o">.</span><span class="n">permission_id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">p</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">preload</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="ss">authority</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="p">,</span><span class="w"> </span><span class="ss">permissions</span><span class="p">:</span><span class="w"> </span><span class="n">p</span><span class="p">]</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">QueryFilterEx</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="na">@user_filters</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"index.html"</span><span class="p">,</span><span class="w"> </span><span class="ss">users</span><span class="p">:</span><span class="w"> </span><span class="n">users</span><span class="p">,</span><span class="w"> </span><span class="ss">changeset</span><span class="p">:</span><span class="w"> </span><span class="n">changeset</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
<p>It is very simple, it just uses the <tt class="docutils literal">get_changeset_from_params</tt> method I discussed before to
generate the changeset and then uses it to filter the query. Also please notice that it passes
the changeset to the template to be properly rendered in the filter form.</p>
</div>
<div class="section" id="the-template">
<h2>The template</h2>
<p>The template for the user index action is the following:</p>
<div class="highlight"><pre><span></span><span class="err"><</span>%= form_for @changeset, AdminRoutes.user_path(@conn, :index), [method: :get, class: "filter-form", as: :filter], fn f -> %>
<span class="err"><</span>%= label f, :username, gettext "Username" %>
<span class="err"><</span>%= text_input f, :username %>
<span class="err"><</span>%= label f, :authority_name, gettext "Authority name" %>
<span class="err"><</span>%= text_input f, :authority_name %>
<span class="err"><</span>%= label f, :permission_name, gettext "Permission name" %>
<span class="err"><</span>%= text_input f, :permission_name %>
<span class="err"><</span>%= label f, :last_login_date, gettext "Last login date" %>
<span class="err"><</span>%= text_input f, :last_login_date %>
<span class="err"><</span>%= error_tag f, :last_login_date %>
<span class="err"><</span>%= submit gettext("Filter"), class: "ml-5" %>
<span class="err"><</span>%= link gettext("Reset"), to: AdminRoutes.user_path(@conn, :index), class: "button button-outline ml-2" %>
<span class="err"><</span>% end %>
<span class="err"><</span>%= for user <span class="p"><</span><span class="nt">-</span> <span class="err">@</span><span class="na">users</span> <span class="na">do</span> <span class="err">%</span><span class="p">></span>
<span class="cm"><!-- display the user info --></span>
<span class="err"><</span>% end %>
</pre></div>
<p>Notice that it gets the <tt class="docutils literal">@changeset</tt> and uses it to properly fill the initial values and display
error messages. For this case I’ve only added an error_tag for the <tt class="docutils literal">:last_login_date</tt> field,
the others since are strings do not really need it since they will accept all values.</p>
<p>Also, the form method form must be <tt class="docutils literal">:get</tt> since we only filter (not change anything) and I’ve passed
the <tt class="docutils literal">as: :filter</tt> option to the <tt class="docutils literal">form_for</tt> to collect the parameters under the <tt class="docutils literal">filter</tt> server side
parameter (this can be anything you want and can be optionally be
passed to <tt class="docutils literal">QueryFilterEx.get_changeset_from_params</tt> to know which parameter the filters are collected on).</p>
</div>
<div class="section" id="how-does-this-work">
<h2>How does this work?</h2>
<p>In this section I’ll try to explain exactly how the <tt class="docutils literal">QueryFilterEx</tt> module works. Before continuing
I want to thank the people at the <a class="reference external" href="https://elixirforum.com/">Elixir forum</a> and #elixir-lang Freenode <span class="caps">IRC</span> chat
that helped me with understanding how to be able to <a class="reference external" href="https://elixirforum.com/t/create-dynamic-bindings-for-where-clause/23797/7">create dynamic bindings</a>.</p>
<p>So I’ll split this explanation in two parts: Explain <tt class="docutils literal">QueryFilterEx.get_changeset_from_params</tt>
and <tt class="docutils literal">make_filter_changeset</tt> (easy) and
then explain <tt class="docutils literal">QueryFilterEx.filter</tt> (more difficult).</p>
<div class="section" id="queryfilterex-get-changeset-from-params-and-make-filter-changeset">
<h3><tt class="docutils literal">QueryFilterEx.get_changeset_from_params</tt> and <tt class="docutils literal">make_filter_changeset</tt></h3>
<p>This function generates a changeset using the <span class="caps">GET</span> request parameters and the list of declared filters. The
create changeset is a <a class="reference external" href="https://hexdocs.pm/ecto/Ecto.Changeset.html#module-schemaless-changesets">schemaless one</a> since it may contains fields of various schemas (or fields that
are not even exist on a schema). To generate it it uses the <a class="reference external" href="https://hexdocs.pm/ecto/Ecto.Changeset.html#cast/4">cast/4</a> function passing it a <tt class="docutils literal">{data, types}</tt>
first parameter to generate the schemaless changeset. It has two public methods: <tt class="docutils literal">get_changeset_from_params</tt>
and <tt class="docutils literal">make_filter_changeset</tt>. The <tt class="docutils literal">get_changeset_from_params</tt> is the one we’ve used to integrate
with the controller and is used to retrieve the filter parameters from the request
parameters based on the collect parameter of the form we mentioned before (the <tt class="docutils literal">as: :filter</tt>). If such
parameters are found they will be passed to <tt class="docutils literal">make_filter_changeset</tt> (or else it will pass an empty
struct). Notice that the <tt class="docutils literal">filter_name</tt> by default is <tt class="docutils literal">"filter"</tt> but you can change it to anything
you want.</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">get_changeset_from_params</span><span class="p">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">filter_name</span><span class="w"> </span><span class="p">\\</span><span class="w"> </span><span class="s2">"filter"</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="n">params</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">%{</span><span class="o">^</span><span class="n">filter_name</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="n">filter_params</span><span class="p">}</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">make_filter_changeset</span><span class="p">(</span><span class="n">filter_params</span><span class="p">)</span>
<span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">make_filter_changeset</span><span class="p">(%{})</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>The <tt class="docutils literal">make_filter_changeset</tt> is the function that actually creates the schemaless changeset. To do that
it uses two private functions that operate on the passed filters array: <tt class="docutils literal">make_filter_keys</tt>
to extract the <tt class="docutils literal">:name</tt> field of each key filter and the <tt class="docutils literal">make_filter_types</tt> to generate a
<tt class="docutils literal">Map</tt> of <tt class="docutils literal">%{name: :type}</tt> as needed by the <tt class="docutils literal">types</tt> of the <tt class="docutils literal">{data, types}</tt> tuple passed
to <tt class="docutils literal">cast</tt> (the <tt class="docutils literal">data</tt> is just an empty <tt class="docutils literal">Map</tt>):</p>
<div class="highlight"><pre><span></span><span class="kd">defp</span><span class="w"> </span><span class="n">make_filter_keys</span><span class="p">(</span><span class="n">filters</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&</span><span class="w"> </span><span class="ni">&1</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
<span class="k">end</span>
<span class="kd">defp</span><span class="w"> </span><span class="n">make_filter_types</span><span class="p">(</span><span class="n">filters</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&</span><span class="p">{</span><span class="ni">&1</span><span class="o">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="ni">&1</span><span class="o">.</span><span class="n">type</span><span class="p">})</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">new</span><span class="p">()</span>
<span class="k">end</span>
<span class="kd">def</span><span class="w"> </span><span class="n">make_filter_changeset</span><span class="p">(</span><span class="n">filters</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">%{}</span>
<span class="w"> </span><span class="n">types</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">make_filter_types</span>
<span class="w"> </span><span class="p">{</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">types</span><span class="p">}</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">cast</span><span class="p">(</span><span class="n">params</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">make_filter_keys</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p">(%{</span><span class="ss">action</span><span class="p">:</span><span class="w"> </span><span class="ss">:insert</span><span class="p">})</span>
<span class="k">end</span>
</pre></div>
<p>One interesting thing here is the <tt class="docutils literal"><span class="pre">Map.merge(%{action:</span> :insert})</tt> that is piped to the
generated changeset. This is needed to actually display the validation errors, if there’s
no action to the changeset (and there won’t be since we aren’t going do any updates to the database
with this changeset) then the casting errors won’t be displayed.</p>
<p>Please notice that although I use the <tt class="docutils literal">get_changeset_from_params</tt> in my controller the important
function here is the <tt class="docutils literal">make_filter_changeset</tt>. The <tt class="docutils literal">get_changeset_from_params</tt> is mainly used to
retrieve the filter-related <span class="caps">GET</span> query parameter; however to use <tt class="docutils literal">QueryFilterEx</tt> you can just
create (however you want) a <tt class="docutils literal">Map</tt> of <tt class="docutils literal">filter_name: value</tt> pairs and pass it to
<tt class="docutils literal">make_filter_changeset</tt> to get the changeset.</p>
</div>
<div class="section" id="queryfilterex-filter">
<h3><tt class="docutils literal">QueryFilterEx.filter</tt></h3>
<p>The <tt class="docutils literal">filter</tt> method gets three parameters. The <tt class="docutils literal">query</tt>, the <tt class="docutils literal">changeset</tt> (that was created with
<tt class="docutils literal">make_filter_changeset</tt>) and the declared <tt class="docutils literal">filters</tt>. This function will then check all declared <tt class="docutils literal">filters</tt>
one by one and see if the <tt class="docutils literal">changeset</tt> contains a change for this filter (i.e if the field has a value).
If yes it will append a <a class="reference external" href="https://hexdocs.pm/ecto/Ecto.Query.html#where/3">where/3</a> to the query based on the passed value of the <tt class="docutils literal">changeset</tt> and the
declared filter <tt class="docutils literal">:method</tt>.</p>
<p>To do that it just uses <tt class="docutils literal">Enum.reduce</tt> starting with the initial query as an accumulator and
reducing on all the declared <tt class="docutils literal">filters</tt>:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">filter</span><span class="p">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">filters</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">changes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch!</span><span class="p">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:changes</span><span class="p">)</span>
<span class="w"> </span><span class="n">filters</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="n">creat_where_clauses_reducer</span><span class="p">(</span><span class="n">changes</span><span class="p">))</span>
<span class="k">end</span>
<span class="kd">defp</span><span class="w"> </span><span class="n">creat_where_clauses_reducer</span><span class="p">(</span><span class="n">changes</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="k">fn</span><span class="w"> </span><span class="p">%{</span><span class="ss">name</span><span class="p">:</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="ss">field_name</span><span class="p">:</span><span class="w"> </span><span class="n">field_name</span><span class="p">,</span><span class="w"> </span><span class="ss">binding</span><span class="p">:</span><span class="w"> </span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="ss">method</span><span class="p">:</span><span class="w"> </span><span class="n">method</span><span class="p">},</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="n">changes</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p">}</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">creat_where_clause</span><span class="p">(</span><span class="n">field_name</span><span class="p">,</span><span class="w"> </span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">method</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p">)</span>
<span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">acc</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Notice that the <tt class="docutils literal">creat_where_clauses_reducer</tt> function returns a function (the reducer) that
<tt class="docutils literal">reduce</tt> will use. This function checks to see if the current changes of the <tt class="docutils literal">changeset</tt> contain
the <tt class="docutils literal">filter_name:</tt>. If yes it will pass the following values to the <tt class="docutils literal">creat_where_clause</tt> function:</p>
<ul class="simple">
<li>The accumulated query (<tt class="docutils literal">acc</tt>)</li>
<li>The <tt class="docutils literal">field_name:</tt>, <tt class="docutils literal">:binding</tt> and <tt class="docutils literal">:method</tt> values of the current filter</li>
<li>The value of the changes of the <tt class="docutils literal">changeset</tt></li>
</ul>
<p>If the current <tt class="docutils literal">filter_name</tt> is not contained in the changes then it just returns the accumulated query as it is.</p>
<p>Let’s now take a look at the <tt class="docutils literal">creat_where_clause</tt> function:</p>
<div class="highlight"><pre><span></span><span class="kd">defp</span><span class="w"> </span><span class="n">creat_where_clause</span><span class="p">(</span><span class="n">acc</span><span class="p">,</span><span class="w"> </span><span class="n">field_name</span><span class="p">,</span><span class="w"> </span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">method</span><span class="p">,</span><span class="w"> </span><span class="n">value</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="n">method</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="ss">:eq</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="n">value</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="ss">:ilike</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">ilike</span><span class="p">(</span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">value</span><span class="si">}</span><span class="s2">%"</span><span class="p">)</span><span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="ss">:icontains</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">ilike</span><span class="p">(</span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="p">(</span><span class="s2">"%</span><span class="si">#{</span><span class="n">value</span><span class="si">}</span><span class="s2">%"</span><span class="p">)</span><span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="ss">:year</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">fragment</span><span class="p">(</span><span class="s2">"extract (year from ?) = ?"</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="n">value</span><span class="p">)</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="ss">:date</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">fragment</span><span class="p">(</span><span class="s2">"? >= cast(? as date) and ? < (cast(? as date) + '1 day'::interval"</span><span class="p">),</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="n">value</span><span class="p">)</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="bp">_</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="n">value</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>This function is just a simple <tt class="docutils literal">case</tt> that pipes the accumulated query to a different <tt class="docutils literal">where</tt> clause
depending on the <tt class="docutils literal">method:</tt>. Let’s take a closer look at what happens when <tt class="docutils literal">:method == :eq</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">acc</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="o">^</span><span class="n">value</span>
<span class="p">)</span>
</pre></div>
<p>This may seem a little confusing so let’s take a look at a simple <tt class="docutils literal">where</tt> first:</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">([</span><span class="n">u</span><span class="p">],</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"root"</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
</pre></div>
<p>Nothing fancy here, now let’s add a named query:</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">([</span><span class="ss">user</span><span class="p">:</span><span class="w"> </span><span class="n">u</span><span class="p">],</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"root"</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
</pre></div>
<p>Notice that now we can declare that <tt class="docutils literal">u</tt> is an alias for the <tt class="docutils literal">users</tt> named binding. What if
we used the tuples syntax for the <tt class="docutils literal">user: u</tt> instead of the keyword one:</p>
<div class="highlight"><pre><span></span><span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">([{</span><span class="ss">:user</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">}],</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"root"</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
</pre></div>
<p>Yes this still works. What if we wanted to use a variable for the binding name in the where?</p>
<div class="highlight"><pre><span></span><span class="n">binding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ss">:user</span>
<span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">([{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">}],</span><span class="w"> </span><span class="n">u</span><span class="o">.</span><span class="n">name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"root"</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
</pre></div>
<p>I think it starts to make sense now, let’s finally use a variable for the field name also:</p>
<div class="highlight"><pre><span></span><span class="n">binding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ss">:user</span>
<span class="n">field_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ss">:name</span>
<span class="n">from</span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">as</span><span class="p">:</span><span class="w"> </span><span class="ss">:user</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">where</span><span class="p">([{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">}],</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">u</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"root"</span><span class="p">)</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
</pre></div>
<p>So this is exactly how this works!</p>
<p>Beyond the <tt class="docutils literal">:eq</tt> I’ve got the definitions for the other methods I described there, the most
complex one is probably the <tt class="docutils literal">:date</tt> which is something like:</p>
<div class="highlight"><pre><span></span><span class="n">where</span><span class="p">(</span>
<span class="w"> </span><span class="p">[{</span><span class="o">^</span><span class="n">binding</span><span class="p">,</span><span class="w"> </span><span class="n">t</span><span class="p">}],</span>
<span class="w"> </span><span class="n">fragment</span><span class="p">(</span><span class="s2">"? >= cast(? as date) and ? < (cast(? as date) + '1 day'::interval"</span><span class="p">),</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="p">(</span><span class="n">t</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="n">field_name</span><span class="p">),</span><span class="w"> </span><span class="o">^</span><span class="n">value</span><span class="p">)</span>
<span class="p">)</span>
</pre></div>
<p>What this does is that it generates the following <span class="caps">SQL</span> fragment:</p>
<div class="highlight"><pre><span></span><span class="n">field_name</span><span class="w"> </span><span class="o">>=</span><span class="w"> </span><span class="k">cast</span><span class="p">(</span><span class="n">value</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nb">date</span><span class="p">)</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">field_name</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="p">(</span><span class="k">cast</span><span class="p">(</span><span class="n">value</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nb">date</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">'1 day'</span><span class="p">::</span><span class="nb">interval</span><span class="p">)</span>
</pre></div>
<p>You can add your own methods by adding more clauses to the case of the <tt class="docutils literal">creat_where_clause</tt> function
and following a similar pattern.</p>
</div>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>By using the <tt class="docutils literal">QueryFilterEx</tt> module presented here you can very quickly declare the fields you want
to filter on and the method you want to use for each field no matter if these fields are in the same
schema or are accessed through joins. You can easily extend the functionality of the module by adding
your own methods. The only extra thing you need to do is to just add names to your queries.</p>
</div>
Phoenix forms integration with select2 and ajax2019-06-04T14:20:00+03:002019-06-04T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2019-06-04:/2019/06/04/phoenix-form-select2-ajax/<p class="first last">How to create a proper ajax-autocomplete solution for your foreign key fields with Phoenix Forms and select2</p>
<p>During the past months I’ve tried to implement a project using <a class="reference external" href="https://elixir-lang.org/">Elixir</a> and the <a class="reference external" href="https://phoenixframework.org/">Phoenix framework</a>. Old
visitors of my blog will probably remember that I mainly use Django for back-end development but I decided to
also give Phoenix a try.</p>
<p>My first impressions are positive but I don’t want to go into detail in this post; I’ll try to add a more
extensive post comparing Elixir / Phoenix with Python / Django someday.</p>
<p>The problem that this particular post will try to explain is how to properly integrate a jQuery <a class="reference external" href="https://select2.org/">select2</a>
dropdown ajax with autocomplete search to your <a class="reference external" href="https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html">Phonix Forms</a>. This seems like a very common problem however I
couldn’t find a proper solution anywhere in the internet. It seems that most people using Phoenix prefer to
implement their autocompletes using <span class="caps">SPA</span> like functionality (react etc). Also I found <a class="reference external" href="https://github.com/nico-amsterdam/phoenix_form_awesomplete">this project</a> that
seems to be working, however it does not use select2 and I really didn’t like to mess with a different
<span class="caps">JS</span> library for reasons that should be too obvious to most people.</p>
<p>So here we’ll implement a simple solution for allowing your foreign key value to be autocompleted through ajax
using select2. The specific example is that you have a User that belongs to an Authority i.e user has a field
named <cite>authority_id</cite> which is a foreign key to authority. We’ll add a functionality to the user edit form to
select the authority using ajax-autocomplete.</p>
<p>Please notice that you can find a working version of this tutorial in my Phoenix Crud template project:
<a class="reference external" href="https://github.com/spapas/phxcrd">https://github.com/spapas/phxcrd</a>. This project contains various other functionality that I need but you should be
able to test the user - authority integration by following the instructions there.</p>
<div class="section" id="the-schemas">
<h2>The schemas</h2>
<p>For this tutorial, we’ll use two schemas: A <tt class="docutils literal">User</tt> and an <tt class="docutils literal">Authority</tt>. Each <tt class="docutils literal">User</tt> belongs to an <tt class="docutils literal">Authority</tt>
(thus will have a foreign key to <tt class="docutils literal">Authority</tt>; that’s what we want to set using the ajax select2). Here are
the ecto schemas for these entities:</p>
<div class="highlight"><pre><span></span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">Phxcrd.Auth.Authority</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span>
<span class="w"> </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span>
<span class="w"> </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Phxcrd.Auth.User</span>
<span class="w"> </span><span class="n">schema</span><span class="w"> </span><span class="s2">"authorities"</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span>
<span class="w"> </span><span class="n">has_many</span><span class="w"> </span><span class="ss">:users</span><span class="p">,</span><span class="w"> </span><span class="nc">User</span><span class="p">,</span><span class="w"> </span><span class="ss">on_replace</span><span class="p">:</span><span class="w"> </span><span class="ss">:nilify</span>
<span class="w"> </span><span class="n">timestamps</span><span class="p">()</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">changeset</span><span class="p">(</span><span class="n">authority</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">authority</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">cast</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="ss">:name</span><span class="p">])</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">validate_required</span><span class="p">([</span><span class="ss">:name</span><span class="p">],</span><span class="w"> </span><span class="ss">message</span><span class="p">:</span><span class="w"> </span><span class="s2">"The field is required"</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">unique_constraint</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span><span class="w"> </span><span class="ss">message</span><span class="p">:</span><span class="w"> </span><span class="s2">"The name already exists!"</span><span class="p">)</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="kn">use</span><span class="w"> </span><span class="nc">Accessible</span>
<span class="k">end</span>
</pre></div>
<div class="highlight"><pre><span></span><span class="kd">defmodule</span><span class="w"> </span><span class="nc">Phxcrd.Auth.User</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="kn">use</span><span class="w"> </span><span class="nc">Ecto.Schema</span>
<span class="w"> </span><span class="kn">import</span><span class="w"> </span><span class="nc">Ecto.Changeset</span>
<span class="w"> </span><span class="kn">alias</span><span class="w"> </span><span class="nc">Phxcrd.Auth.Authority</span>
<span class="w"> </span><span class="n">schema</span><span class="w"> </span><span class="s2">"users"</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span>
<span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="ss">:username</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span>
<span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="ss">:password_hash</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span>
<span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="ss">:password</span><span class="p">,</span><span class="w"> </span><span class="ss">:string</span><span class="p">,</span><span class="w"> </span><span class="ss">virtual</span><span class="p">:</span><span class="w"> </span><span class="no">true</span>
<span class="w"> </span><span class="n">belongs_to</span><span class="w"> </span><span class="ss">:authority</span><span class="p">,</span><span class="w"> </span><span class="nc">Authority</span>
<span class="w"> </span><span class="n">timestamps</span><span class="p">()</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="na">@doc</span><span class="w"> </span><span class="no">false</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">changeset</span><span class="p">(</span><span class="n">user</span><span class="p">,</span><span class="w"> </span><span class="n">attrs</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">user</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">cast</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="ss">:username</span><span class="p">,</span><span class="w"> </span><span class="ss">:email</span><span class="p">,</span><span class="w"> </span><span class="ss">:authority_id</span><span class="p">])</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">validate_required</span><span class="p">([</span><span class="ss">:username</span><span class="p">,</span><span class="w"> </span><span class="ss">:email</span><span class="w"> </span><span class="p">])</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="kn">use</span><span class="w"> </span><span class="nc">Accessible</span>
<span class="k">end</span>
</pre></div>
<p>Notice that both these entities are contained in the <tt class="docutils literal">Auth</tt> context and were created using
<tt class="docutils literal">mix phx.gen.html</tt>; I won’t include the migrations here.</p>
</div>
<div class="section" id="the-search-api">
<h2>The search <span class="caps">API</span></h2>
<p>Let’s now take a look at the search api for <tt class="docutils literal">Authority</tt>. I’ve added an <tt class="docutils literal">ApiController</tt> which contains
the following function:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">search_authorities</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">q</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">params</span><span class="p">[</span><span class="s2">"q"</span><span class="p">]</span>
<span class="w"> </span><span class="n">authorities</span><span class="w"> </span><span class="o">=</span>
<span class="w"> </span><span class="n">from</span><span class="p">(</span><span class="n">a</span><span class="w"> </span><span class="ow">in</span><span class="w"> </span><span class="nc">Authority</span><span class="p">,</span>
<span class="w"> </span><span class="ss">where</span><span class="p">:</span><span class="w"> </span><span class="n">ilike</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="o">^</span><span class="s2">"%</span><span class="si">#{</span><span class="n">q</span><span class="si">}</span><span class="s2">%"</span><span class="p">)</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">limit</span><span class="p">(</span><span class="mi">20</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"authorities.json"</span><span class="p">,</span><span class="w"> </span><span class="ss">authorities</span><span class="p">:</span><span class="w"> </span><span class="n">authorities</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
<p>Notice that this retrieves a <cite>q</cite> parameter and makes an <cite>ilike</cite> query to <cite>Authority.name</cite>. It then
passes the results to the view for rendering. Here’s the corresponding function for <cite>ApiView</cite>:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="s2">"authorities.json"</span><span class="p">,</span><span class="w"> </span><span class="p">%{</span><span class="ss">authorities</span><span class="p">:</span><span class="w"> </span><span class="n">authorities</span><span class="p">})</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">%{</span><span class="ss">results</span><span class="p">:</span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">authorities</span><span class="p">,</span><span class="w"> </span><span class="o">&</span><span class="n">authority_json</span><span class="o">/</span><span class="mi">1</span><span class="p">)}</span>
<span class="w"> </span><span class="k">end</span>
<span class="w"> </span><span class="kd">def</span><span class="w"> </span><span class="n">authority_json</span><span class="p">(</span><span class="n">a</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">%{</span>
<span class="w"> </span><span class="ss">id</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="w"> </span><span class="ss">text</span><span class="p">:</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">name</span>
<span class="w"> </span><span class="p">}</span>
<span class="k">end</span>
</pre></div>
<p>Notice that select2 wants its results in a <span class="caps">JSON</span> struct with the following form <tt class="docutils literal">{results: [{id: 1, name: "Authority <span class="pre">1"}]}</span></tt>.</p>
<p>To add this controller action to my routes I’ve added this to <tt class="docutils literal">router.ex</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">scope</span><span class="w"> </span><span class="s2">"/api"</span><span class="p">,</span><span class="w"> </span><span class="nc">PhxcrdWeb</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">pipe_through</span><span class="w"> </span><span class="ss">:api</span>
<span class="w"> </span><span class="n">get</span><span class="w"> </span><span class="s2">"/search_authorities"</span><span class="p">,</span><span class="w"> </span><span class="nc">ApiController</span><span class="p">,</span><span class="w"> </span><span class="ss">:search_authorities</span>
<span class="k">end</span>
</pre></div>
<p>Thus if you visit <tt class="docutils literal"><span class="pre">http://127.0.0.1/search_authorities?q=A</span></tt> you should retrieve authorities containing <tt class="docutils literal">A</tt> in their name.</p>
</div>
<div class="section" id="the-controller">
<h2>The controller</h2>
<p>Concenring the <tt class="docutils literal">UserController</tt> I’ve added the following methods to it for creating and updating users:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">new</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="n">_params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Auth</span><span class="o">.</span><span class="n">change_user</span><span class="p">(%</span><span class="nc">User</span><span class="p">{})</span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"new.html"</span><span class="p">,</span><span class="w"> </span><span class="ss">changeset</span><span class="p">:</span><span class="w"> </span><span class="n">changeset</span><span class="p">)</span>
<span class="k">end</span>
<span class="kd">def</span><span class="w"> </span><span class="n">create</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p">%{</span><span class="s2">"user"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="n">user_params</span><span class="p">})</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="nc">Auth</span><span class="o">.</span><span class="n">create_user</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p">}</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">conn</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">put_flash</span><span class="p">(</span><span class="ss">:info</span><span class="p">,</span><span class="w"> </span><span class="s2">"</span><span class="si">#{</span><span class="n">user</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2"> created!"</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">redirect</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">Routes</span><span class="o">.</span><span class="n">user_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">:show</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p">))</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="n">changeset</span><span class="p">}</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"new.html"</span><span class="p">,</span><span class="w"> </span><span class="ss">changeset</span><span class="p">:</span><span class="w"> </span><span class="n">changeset</span><span class="p">)</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
<span class="kd">def</span><span class="w"> </span><span class="n">edit</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p">%{</span><span class="s2">"id"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="n">id</span><span class="p">})</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Auth</span><span class="o">.</span><span class="n">get_user!</span><span class="p">(</span><span class="n">id</span><span class="p">)</span>
<span class="w"> </span><span class="n">changeset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Auth</span><span class="o">.</span><span class="n">change_user</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"edit.html"</span><span class="p">,</span><span class="w"> </span><span class="ss">user</span><span class="p">:</span><span class="w"> </span><span class="n">user</span><span class="p">,</span><span class="w"> </span><span class="ss">changeset</span><span class="p">:</span><span class="w"> </span><span class="n">changeset</span><span class="p">)</span>
<span class="k">end</span>
<span class="kd">def</span><span class="w"> </span><span class="n">update</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="p">%{</span><span class="s2">"id"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="s2">"user"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="n">user_params</span><span class="p">})</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Auth</span><span class="o">.</span><span class="n">get_user!</span><span class="p">(</span><span class="n">id</span><span class="p">)</span>
<span class="w"> </span><span class="n">user_params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">merge</span><span class="p">(%{</span><span class="s2">"authority_id"</span><span class="w"> </span><span class="o">=></span><span class="w"> </span><span class="no">nil</span><span class="p">},</span><span class="w"> </span><span class="n">user_params</span><span class="p">)</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="nc">Auth</span><span class="o">.</span><span class="n">update_user</span><span class="p">(</span><span class="n">user</span><span class="p">,</span><span class="w"> </span><span class="n">user_params</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p">}</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">conn</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">put_flash</span><span class="p">(</span><span class="ss">:info</span><span class="p">,</span><span class="w"> </span><span class="s2">"User updated successfully."</span><span class="p">)</span>
<span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="n">redirect</span><span class="p">(</span><span class="ss">to</span><span class="p">:</span><span class="w"> </span><span class="nc">Routes</span><span class="o">.</span><span class="n">user_path</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="ss">:show</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p">))</span>
<span class="w"> </span><span class="p">{</span><span class="ss">:error</span><span class="p">,</span><span class="w"> </span><span class="p">%</span><span class="nc">Ecto.Changeset</span><span class="p">{}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">changeset</span><span class="p">}</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="n">render</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span><span class="w"> </span><span class="s2">"edit.html"</span><span class="p">,</span><span class="w"> </span><span class="ss">user</span><span class="p">:</span><span class="w"> </span><span class="n">user</span><span class="p">,</span><span class="w"> </span><span class="ss">changeset</span><span class="p">:</span><span class="w"> </span><span class="n">changeset</span><span class="p">)</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Most of these are more or less the default things that <tt class="docutils literal">mix phx.gen.html</tt> creates.
One thing that may seem a strange here is the <tt class="docutils literal">user_params = <span class="pre">Map.merge(%{"authority_id"</span> => nil}, user_params)</tt>
line of <tt class="docutils literal">update</tt>. What happens here is that I want to be able to clear the authority of a user (I’ll
explain how in the next sections). If I do
that then the <tt class="docutils literal">user_params</tt> that is passed to <tt class="docutils literal">update</tt> will <em>not</em> contain an <tt class="docutils literal">authority_id</tt> key thus
the <tt class="docutils literal">authority_id</tt> won’t be changed at all (so even though I cleared it, it will keep its previous value after
I save it). To fix that I set a default value of <tt class="docutils literal">nil</tt> to <tt class="docutils literal">authority_id</tt>; if the user has actually selected
an authority from the form this will be overriden when merging the two maps. So the resulting <tt class="docutils literal">user_params</tt> will
<em>always</em> contain an <tt class="docutils literal">authority_id</tt> key, either set to nil or to the selected authority.</p>
<p>Beyond that I wont’ go into detail explaining the above functions, but if something seems strange feel free to ask. I
also won’t explain the <tt class="docutils literal">Auth.*</tt> functions; all these are created by phoenix in the context module.</p>
</div>
<div class="section" id="the-view">
<h2>The view</h2>
<p>The <tt class="docutils literal">UserView</tt> module contains a simple but very important function:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">get_select_value</span><span class="p">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">attr</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="n">changeset</span><span class="o">.</span><span class="n">changes</span><span class="p">[</span><span class="n">attr</span><span class="p">]</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="no">nil</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="nc">Map</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">changeset</span><span class="o">.</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">attr</span><span class="p">)</span>
<span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="o">-></span><span class="w"> </span><span class="n">z</span>
<span class="w"> </span><span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>This functions gets two parameters: The changeset and the name of the attribute (<tt class="docutils literal">:authority_id</tt> in our case). What
it does is to first check if this attribute is contained in the changeset.changes; if yes it will return that value. If
it isn’t contained in the changeset.changes then it will return the value of changeset.data for that attribute.</p>
<p>This is a little complex but let’s try to understand its logic: When you start editing a <tt class="docutils literal">User</tt> you want to display
the current authority of that instance. However, when you submit an edited user and retrieve an errored form (for example
because you forgot to fill the username) you want to display the authority that <em>was submitted</em> in the form. So the
<tt class="docutils literal">changeset.changes</tt> contains the changes that were submitted just before while the <tt class="docutils literal">changeset.data</tt> contain the
initial value of the struct.</p>
<p><strong>Update 02/07/2019:</strong> Please notice that instead of using the
<tt class="docutils literal">get_select_value</tt> I presented before you can use the
<tt class="docutils literal">Ecto.Changeset.get_field</tt> function that does exactly this! So
<tt class="docutils literal">get_select_value</tt> could be defined like this:</p>
<div class="highlight"><pre><span></span><span class="kd">def</span><span class="w"> </span><span class="n">get_select_value</span><span class="p">(</span><span class="n">changeset</span><span class="p">,</span><span class="w"> </span><span class="n">attr</span><span class="p">)</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="n">changeset</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Ecto.Changeset</span><span class="o">.</span><span class="n">get_field</span><span class="p">(</span><span class="n">attr</span><span class="p">)</span>
<span class="k">end</span>
</pre></div>
</div>
<div class="section" id="the-form-template">
<h2>The form template</h2>
<p>Both the <tt class="docutils literal">:new</tt> and <tt class="docutils literal">:edit</tt> actions include a common form.html.eex template:</p>
<div class="highlight"><pre><span></span><span class="err"><</span>%= form_for @changeset, @action, fn f -> %>
<span class="err"><</span>%= if @changeset.action do %>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"alert alert-danger"</span><span class="p">></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span><span class="err"><</span>%= gettext("Problems while saving") %><span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="err"><</span>% end %>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'row'</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column'</span><span class="p">></span>
<span class="err"><</span>%= label f, :username %>
<span class="err"><</span>%= text_input f, :username %>
<span class="err"><</span>%= error_tag f, :username %>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column'</span><span class="p">></span>
<span class="err"><</span>%= label f, :email %>
<span class="err"><</span>%= text_input f, :email %>
<span class="err"><</span>%= error_tag f, :email %>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'row'</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column'</span><span class="p">></span>
<span class="err"><</span>%= label f, :authority %>
<span class="err"><</span>%= select(f,
:authority_id, [
(with sv when not is_nil(sv) <span class="p"><</span><span class="nt">-</span> <span class="na">get_select_value</span><span class="err">(@</span><span class="na">changeset</span><span class="err">,</span> <span class="na">:authority_id</span><span class="err">),</span>
<span class="na">a</span> <span class="err"><</span><span class="na">-</span> <span class="na">Phxcrd</span><span class="err">.</span><span class="na">Auth</span><span class="err">.</span><span class="na">get_authority</span><span class="err">!(</span><span class="na">sv</span><span class="err">),</span> <span class="na">do:</span> <span class="err">{</span><span class="na">a</span><span class="err">.</span><span class="na">name</span><span class="err">,</span> <span class="na">a</span><span class="err">.</span><span class="na">id</span><span class="err">})</span>
<span class="err">],</span>
<span class="na">style:</span> <span class="err">"</span><span class="na">width:</span> <span class="na">100</span><span class="err">%")</span>
<span class="err">%</span><span class="p">></span>
<span class="err"><</span>%= error_tag f, :authority_id %>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="err"><</span>%= submit gettext("Save") %>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="err"><</span>% end %>
</pre></div>
<p>This is a custom Phoenix form but it has the following addition which is more or less the meat of this article
(along with the <tt class="docutils literal">get_select_value</tt> function I explained before):</p>
<div class="highlight"><pre><span></span><span class="n">select</span><span class="p">(</span><span class="n">f</span><span class="p">,</span><span class="w"> </span><span class="ss">:authority_id</span><span class="p">,</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">(</span><span class="n">with</span><span class="w"> </span><span class="n">sv</span><span class="w"> </span><span class="ow">when</span><span class="w"> </span><span class="ow">not</span><span class="w"> </span><span class="n">is_nil</span><span class="p">(</span><span class="n">sv</span><span class="p">)</span><span class="w"> </span><span class="o"><-</span><span class="w"> </span><span class="n">get_select_value</span><span class="p">(</span><span class="na">@changeset</span><span class="p">,</span><span class="w"> </span><span class="ss">:authority_id</span><span class="p">),</span>
<span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o"><-</span><span class="w"> </span><span class="nc">Phxcrd.Auth</span><span class="o">.</span><span class="n">get_authority!</span><span class="p">(</span><span class="n">sv</span><span class="p">),</span><span class="w"> </span><span class="ss">do</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="n">a</span><span class="o">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">a</span><span class="o">.</span><span class="n">id</span><span class="p">})</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="ss">style</span><span class="p">:</span><span class="w"> </span><span class="s2">"width: 100%"</span><span class="p">)</span>
</pre></div>
<p>So this will create an html select element which will contain a single value (the array in the third
parameter of <tt class="docutils literal">select</tt>): The authority of that object or the authority that the user had submitted
in the form. For this it uses <tt class="docutils literal">get_select_value</tt> to retrieve the :authority_id and if it’s not nil
it passes it to <tt class="docutils literal">get_authority!</tt> to retrieve the actual authority and return a tuple with its name and id.</p>
<p>By default when you create a <tt class="docutils literal">select</tt> element you’ll pass an array of all options in the third
parameter, for example:</p>
<div class="highlight"><pre><span></span><span class="n">select</span><span class="p">(</span><span class="n">f</span><span class="p">,</span><span class="w"> </span><span class="ss">:authority_id</span><span class="p">,</span><span class="w"> </span><span class="nc">Phxcrd.Auth</span><span class="o">.</span><span class="n">list_authorities</span><span class="w"> </span><span class="o">|></span><span class="w"> </span><span class="nc">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&</span><span class="p">{</span><span class="ni">&1</span><span class="o">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="ni">&1</span><span class="o">.</span><span class="n">id</span><span class="p">}))</span>
</pre></div>
<p>Of course this beats the purpose of using ajax since all options will be rendered.</p>
<p>The final step is to add the required custom javascript to convert that select to select2-with-ajax:</p>
<div class="highlight"><pre><span></span><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#user_authority_id'</span><span class="p">).</span><span class="nx">select2</span><span class="p">({</span>
<span class="w"> </span><span class="nx">allowClear</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">'Select authority'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">ajax</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="s1">'<%= Routes.api_path(@conn, :search_authorities) %>'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">dataType</span><span class="o">:</span><span class="w"> </span><span class="s1">'json'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">delay</span><span class="o">:</span><span class="w"> </span><span class="mf">150</span><span class="p">,</span>
<span class="w"> </span><span class="nx">minimumInputLength</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">})</span>
</pre></div>
<p>The <span class="caps">JS</span> very rather simple; the <tt class="docutils literal">allowClear</tt> option will display an <tt class="docutils literal">x</tt> so that you can clear the
selected authority while the ajax url will be that of the <tt class="docutils literal">:search_authorities</tt>.</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Although this article may seem a little long, as I’ve already mentioned the most important thing
to keep is how to properly set the value that should be displayed in your <tt class="docutils literal">select2</tt> widget. Beyond
that everything is a walk in the park by following the docs.</p>
</div>
How to create a custom filtered adapter in Android2019-04-05T12:20:00+03:002019-04-05T12:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2019-04-05:/2019/04/05/android-custom-filter-adapter/<p class="first last">How to create a custom filtered adapter in Android; we’ll be using Kotlin for this.</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p>Android offers a nice component named <a class="reference external" href="https://developer.android.com/reference/android/widget/AutoCompleteTextView">AutoCompleteTextView</a> that can be used to auto-fill a text box from a list of values.
In its simplest form, you just create an array adapter passing it a list of objects (that have a proper <tt class="docutils literal">toString()</tt> method).
Then you type some characters to the textbox and by default it will filter the results searching
in the <em>beginning of the backing object’s toString() result</em>.</p>
<p>However there are times that you don’t want to look at the beginning of the string (because you want to look at the middle of the string) or
you don’t want to just to search in toString() method of the object or you want to do some more fancy things in object output. For this
you must override the <tt class="docutils literal">ArrayAdapter</tt> and add a custom <tt class="docutils literal">Filter</tt>.</p>
<p>Unfurtunately this isn’t as straightforward as I’d like and I couldn’t find a quick and easy tutorial on how it can be done.</p>
<p>So here goes nothing: In the following I’ll show you a very simple android application that will have <em>the minimum viable</em> custom filtered
adapter implementation. You can find the whole project in github: <a class="reference external" href="https://github.com/spapas/CustomFilteredAdapeter">https://github.com/spapas/CustomFilteredAdapeter</a> but I am going to discuss
everything here also.</p>
</div>
<div class="section" id="the-application">
<h2>The application</h2>
<p>Just create a new project with an empty activity from Android Studio. Use kotlin as the language.</p>
</div>
<div class="section" id="the-layout">
<h2>The layout</h2>
<p>I’ll keep it as simple as possible:</p>
<div class="highlight"><pre><span></span><span class="cp"><?xml version="1.0" encoding="utf-8"?></span>
<span class="nt"><LinearLayout</span>
<span class="w"> </span><span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
<span class="w"> </span><span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
<span class="w"> </span><span class="na">android:orientation=</span><span class="s">"vertical"</span>
<span class="w"> </span><span class="na">android:layout_width=</span><span class="s">"match_parent"</span><span class="w"> </span><span class="na">android:layout_height=</span><span class="s">"match_parent"</span>
<span class="w"> </span><span class="na">tools:context=</span><span class="s">".MainActivity"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><TextView</span>
<span class="w"> </span><span class="na">android:layout_width=</span><span class="s">"match_parent"</span><span class="w"> </span><span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
<span class="w"> </span><span class="na">android:text=</span><span class="s">"Hello World!"</span>
<span class="w"> </span><span class="na">android:textSize=</span><span class="s">"32sp"</span>
<span class="w"> </span><span class="na">android:textAlignment=</span><span class="s">"center"</span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><AutoCompleteTextView</span>
<span class="w"> </span><span class="na">android:layout_marginTop=</span><span class="s">"32dip"</span>
<span class="w"> </span><span class="na">android:layout_width=</span><span class="s">"match_parent"</span><span class="w"> </span><span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
<span class="w"> </span><span class="na">android:id=</span><span class="s">"@+id/autoCompleteTextView"</span><span class="nt">/></span>
<span class="nt"></LinearLayout></span>
</pre></div>
<p>You should just care about the <tt class="docutils literal">AutoCompleteTextView</tt> with an id of <tt class="docutils literal">autoCompleteTextView</tt>.</p>
</div>
<div class="section" id="the-backing-data-object">
<h2>The backing data object</h2>
<p>I’ll use a simple PoiDao Kotlin data class for this:</p>
<div class="highlight"><pre><span></span><span class="kd">data</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nc">PoiDao</span><span class="p">(</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">id</span><span class="p">:</span><span class="w"> </span><span class="kt">Int</span><span class="p">,</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">name</span><span class="p">:</span><span class="w"> </span><span class="kt">String</span><span class="p">,</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">city</span><span class="p">:</span><span class="w"> </span><span class="kt">String</span><span class="p">,</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">category_name</span><span class="p">:</span><span class="w"> </span><span class="kt">String</span>
<span class="p">)</span>
</pre></div>
<p>I’d like to be able to search to both name, city and category_name of each object. To create a list of the pois to be used to the adapter I can do something like:</p>
<div class="highlight"><pre><span></span><span class="kd">val</span><span class="w"> </span><span class="nv">poisArray</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">listOf</span><span class="p">(</span>
<span class="w"> </span><span class="n">PoiDao</span><span class="p">(</span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="s">"Taco Bell"</span><span class="p">,</span><span class="w"> </span><span class="s">"Athens"</span><span class="p">,</span><span class="w"> </span><span class="s">"Restaurant"</span><span class="p">),</span>
<span class="w"> </span><span class="n">PoiDao</span><span class="p">(</span><span class="m">2</span><span class="p">,</span><span class="w"> </span><span class="s">"McDonalds"</span><span class="p">,</span><span class="w"> </span><span class="s">"Athens"</span><span class="p">,</span><span class="s">"Restaurant"</span><span class="p">),</span>
<span class="w"> </span><span class="n">PoiDao</span><span class="p">(</span><span class="m">3</span><span class="p">,</span><span class="w"> </span><span class="s">"KFC"</span><span class="p">,</span><span class="w"> </span><span class="s">"Piraeus"</span><span class="p">,</span><span class="w"> </span><span class="s">"Restaurant"</span><span class="p">),</span>
<span class="w"> </span><span class="n">PoiDao</span><span class="p">(</span><span class="m">4</span><span class="p">,</span><span class="w"> </span><span class="s">"Shell"</span><span class="p">,</span><span class="w"> </span><span class="s">"Lamia"</span><span class="p">,</span><span class="s">"Gas Station"</span><span class="p">),</span>
<span class="w"> </span><span class="n">PoiDao</span><span class="p">(</span><span class="m">5</span><span class="p">,</span><span class="w"> </span><span class="s">"BP"</span><span class="p">,</span><span class="w"> </span><span class="s">"Thessaloniki"</span><span class="p">,</span><span class="w"> </span><span class="s">"Gas Station"</span><span class="p">)</span>
<span class="p">)</span>
</pre></div>
</div>
<div class="section" id="the-custom-adapter">
<h2>The custom adapter</h2>
<p>This will be an <tt class="docutils literal">ArrayAdapter<PoiDao></tt> implementing also the <tt class="docutils literal">Filterable</tt> interface:</p>
<div class="highlight"><pre><span></span><span class="kd">inner</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nc">PoiAdapter</span><span class="p">(</span><span class="n">context</span><span class="p">:</span><span class="w"> </span><span class="n">Context</span><span class="p">,</span><span class="w"> </span><span class="nd">@LayoutRes</span><span class="w"> </span><span class="kd">private</span><span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">layoutResource</span><span class="p">:</span><span class="w"> </span><span class="kt">Int</span><span class="p">,</span><span class="w"> </span><span class="kd">private</span><span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">allPois</span><span class="p">:</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="n">PoiDao</span><span class="o">></span><span class="p">):</span>
<span class="w"> </span><span class="n">ArrayAdapter</span><span class="o"><</span><span class="n">PoiDao</span><span class="o">></span><span class="p">(</span><span class="n">context</span><span class="p">,</span><span class="w"> </span><span class="n">layoutResource</span><span class="p">,</span><span class="w"> </span><span class="n">allPois</span><span class="p">),</span>
<span class="w"> </span><span class="n">Filterable</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">private</span><span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nv">mPois</span><span class="p">:</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="n">PoiDao</span><span class="o">></span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">allPois</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">getCount</span><span class="p">():</span><span class="w"> </span><span class="kt">Int</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">mPois</span><span class="p">.</span><span class="na">size</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">getItem</span><span class="p">(</span><span class="n">p0</span><span class="p">:</span><span class="w"> </span><span class="kt">Int</span><span class="p">):</span><span class="w"> </span><span class="n">PoiDao? </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">mPois</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">p0</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">getItemId</span><span class="p">(</span><span class="n">p0</span><span class="p">:</span><span class="w"> </span><span class="kt">Int</span><span class="p">):</span><span class="w"> </span><span class="kt">Long</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Or just return p0</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">mPois</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">p0</span><span class="p">).</span><span class="na">id</span><span class="p">.</span><span class="na">toLong</span><span class="p">()</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">getView</span><span class="p">(</span><span class="n">position</span><span class="p">:</span><span class="w"> </span><span class="kt">Int</span><span class="p">,</span><span class="w"> </span><span class="n">convertView</span><span class="p">:</span><span class="w"> </span><span class="n">View?,</span><span class="w"> </span><span class="n">parent</span><span class="p">:</span><span class="w"> </span><span class="n">ViewGroup?)</span><span class="p">:</span><span class="w"> </span><span class="n">View</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">view</span><span class="p">:</span><span class="w"> </span><span class="n">TextView</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">convertView</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">TextView? </span><span class="o">?:</span><span class="w"> </span><span class="n">LayoutInflater</span><span class="p">.</span><span class="na">from</span><span class="p">(</span><span class="n">context</span><span class="p">).</span><span class="na">inflate</span><span class="p">(</span><span class="n">layoutResource</span><span class="p">,</span><span class="w"> </span><span class="n">parent</span><span class="p">,</span><span class="w"> </span><span class="kc">false</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">TextView</span>
<span class="w"> </span><span class="n">view</span><span class="p">.</span><span class="na">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"</span><span class="si">${</span><span class="n">mPois</span><span class="o">[</span><span class="n">position</span><span class="o">]</span><span class="p">.</span><span class="na">name</span><span class="si">}</span><span class="s"> </span><span class="si">${</span><span class="n">mPois</span><span class="o">[</span><span class="n">position</span><span class="o">]</span><span class="p">.</span><span class="na">city</span><span class="si">}</span><span class="s"> (</span><span class="si">${</span><span class="n">mPois</span><span class="o">[</span><span class="n">position</span><span class="o">]</span><span class="p">.</span><span class="na">category_name</span><span class="si">}</span><span class="s">)"</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">view</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">getFilter</span><span class="p">():</span><span class="w"> </span><span class="n">Filter</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// See next section</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>You’ll see that we add an instance variable named <tt class="docutils literal">mPois</tt> that gets initialized in the start with <tt class="docutils literal">allPois</tt> (which is the initial list of all pois that is passed to the adapter). The mPois
will contain the <em>filtered</em> results. Then,
for <tt class="docutils literal">getCount</tt> and <tt class="docutils literal">getItem</tt> we return the corresponding valeus from <tt class="docutils literal">mPois</tt>; the <tt class="docutils literal">getItemId</tt> is used when you have an sqlite backed adapter but I’m including it here for completeness.</p>
<p>The <tt class="docutils literal">getView</tt> will create the specific line for each item in the dropdown. As you’ll see the layout that is passed must have a <tt class="docutils literal">text</tt> child which is set based on some of the attributes of
the corresponding poi for each position. Notice that we can use whatever view layout we want for our dropdown result line (this is the <tt class="docutils literal">layoutResource</tt> parameter) but we need to configure
it (i.e bind it with the values of the backing object) here properly.</p>
<p>Finally we create a custom instance of the <tt class="docutils literal">Filter</tt>, explained in the next section.</p>
</div>
<div class="section" id="the-custom-filter">
<h2>The custom filter</h2>
<p>The <tt class="docutils literal">getFilter</tt> creates an object instance of a Filter and returns it:</p>
<div class="highlight"><pre><span></span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">getFilter</span><span class="p">():</span><span class="w"> </span><span class="n">Filter</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">object</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="nc">Filter</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">publishResults</span><span class="p">(</span><span class="n">charSequence</span><span class="p">:</span><span class="w"> </span><span class="n">CharSequence?,</span><span class="w"> </span><span class="n">filterResults</span><span class="p">:</span><span class="w"> </span><span class="n">Filter</span><span class="p">.</span><span class="na">FilterResults</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">mPois</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">filterResults</span><span class="p">.</span><span class="na">values</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">List</span><span class="o"><</span><span class="n">PoiDao</span><span class="o">></span>
<span class="w"> </span><span class="n">notifyDataSetChanged</span><span class="p">()</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">performFiltering</span><span class="p">(</span><span class="n">charSequence</span><span class="p">:</span><span class="w"> </span><span class="n">CharSequence?)</span><span class="p">:</span><span class="w"> </span><span class="n">Filter</span><span class="p">.</span><span class="na">FilterResults</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">queryString</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">charSequence</span><span class="o">?.</span><span class="na">toString</span><span class="p">()</span><span class="o">?.</span><span class="na">toLowerCase</span><span class="p">()</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">filterResults</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Filter</span><span class="p">.</span><span class="na">FilterResults</span><span class="p">()</span>
<span class="w"> </span><span class="n">filterResults</span><span class="p">.</span><span class="na">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">queryString</span><span class="o">==</span><span class="kc">null</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">queryString</span><span class="p">.</span><span class="na">isEmpty</span><span class="p">())</span>
<span class="w"> </span><span class="n">allPois</span>
<span class="w"> </span><span class="k">else</span>
<span class="w"> </span><span class="n">allPois</span><span class="p">.</span><span class="na">filter</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nb">it</span><span class="p">.</span><span class="na">name</span><span class="p">.</span><span class="na">toLowerCase</span><span class="p">().</span><span class="na">contains</span><span class="p">(</span><span class="n">queryString</span><span class="p">)</span><span class="w"> </span><span class="o">||</span>
<span class="w"> </span><span class="nb">it</span><span class="p">.</span><span class="na">city</span><span class="p">.</span><span class="na">toLowerCase</span><span class="p">().</span><span class="na">contains</span><span class="p">(</span><span class="n">queryString</span><span class="p">)</span><span class="w"> </span><span class="o">||</span>
<span class="w"> </span><span class="nb">it</span><span class="p">.</span><span class="na">category_name</span><span class="p">.</span><span class="na">toLowerCase</span><span class="p">().</span><span class="na">contains</span><span class="p">(</span><span class="n">queryString</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">filterResults</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>This object instance overrides two methods of <tt class="docutils literal">Filter</tt>: <tt class="docutils literal">performFiltering</tt> and <tt class="docutils literal">publishResults</tt>. The <tt class="docutils literal">performFiltering</tt> is where the actual filtering is done;
it should return a <tt class="docutils literal">FilterResults</tt> object containing a <tt class="docutils literal">values</tt> attribute with the filtered values. In this method
we retrieve the <tt class="docutils literal">charSequence</tt> parameter and converit it to lowercase. Then, if this parameter is not empty we filter the corresponding elements of <tt class="docutils literal">allPois</tt>
(i.e name, city and category_name in our case) using contains. If the query parameter is empty then we just return all pois. Warning java developers; here the if
is used as an expression (i.e its result will be assigned to <tt class="docutils literal">filterResults.values</tt>).</p>
<p>After the performFiltering has finished, the <tt class="docutils literal">publishResults</tt> method is called. This method retrieves the filtered results in its <tt class="docutils literal">filterResults</tt> parameter. Thus it sets
<tt class="docutils literal">mPois</tt> of the custom adapter is set to the result of the filter operation and calls <tt class="docutils literal">notifyDataSetChanged</tt> to display the results.</p>
</div>
<div class="section" id="using-the-custom-adapter">
<h2>Using the custom adapter</h2>
<p>To use the custom adapter you can do something like this in your activity’s onCreate:</p>
<div class="highlight"><pre><span></span><span class="kd">override</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">onCreate</span><span class="p">(</span><span class="n">savedInstanceState</span><span class="p">:</span><span class="w"> </span><span class="n">Bundle?)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">super</span><span class="p">.</span><span class="na">onCreate</span><span class="p">(</span><span class="n">savedInstanceState</span><span class="p">)</span>
<span class="w"> </span><span class="n">setContentView</span><span class="p">(</span><span class="n">R</span><span class="p">.</span><span class="na">layout</span><span class="p">.</span><span class="na">activity_main</span><span class="p">)</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">poisArray</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">listOf</span><span class="p">(</span>
<span class="w"> </span><span class="c1">// See previous sections</span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">adapter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">PoiAdapter</span><span class="p">(</span><span class="k">this</span><span class="p">,</span><span class="w"> </span><span class="n">android</span><span class="p">.</span><span class="na">R</span><span class="p">.</span><span class="na">layout</span><span class="p">.</span><span class="na">simple_list_item_1</span><span class="p">,</span><span class="w"> </span><span class="n">poisArray</span><span class="p">)</span>
<span class="w"> </span><span class="n">autoCompleteTextView</span><span class="p">.</span><span class="na">setAdapter</span><span class="p">(</span><span class="n">adapter</span><span class="p">)</span>
<span class="w"> </span><span class="n">autoCompleteTextView</span><span class="p">.</span><span class="na">threshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">3</span>
<span class="w"> </span><span class="n">autoCompleteTextView</span><span class="p">.</span><span class="na">setOnItemClickListener</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">parent</span><span class="p">,</span><span class="w"> </span><span class="n">_</span><span class="p">,</span><span class="w"> </span><span class="n">position</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">-></span>
<span class="w"> </span><span class="kd">val</span><span class="w"> </span><span class="nv">selectedPoi</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">parent</span><span class="p">.</span><span class="na">adapter</span><span class="p">.</span><span class="na">getItem</span><span class="p">(</span><span class="n">position</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">PoiDao?</span>
<span class="w"> </span><span class="n">autoCompleteTextView</span><span class="p">.</span><span class="na">setText</span><span class="p">(</span><span class="n">selectedPoi</span><span class="o">?.</span><span class="na">name</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>We create the PoiAdapter passing it the poisArray and <tt class="docutils literal">android.R.layout.simple_list_item_1</tt> as the layout. That layout just contains a textview named text. As we’ve already
discussed you can pass something more complex here. The <tt class="docutils literal">thresold</tt> defined the number of characters that the user that needs to enter to do the filtering (default is 2).</p>
<p>Please notice that when the user clicks (selects) on an item of the dropdown we set the contents of the textview (or else it will just use the object’s toString() method to set it).</p>
</div>
Fixing your Django async job - database integration2019-02-25T15:20:00+02:002019-02-25T15:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2019-02-25:/2019/02/25/django-fix-async-db/<p class="first last">How to properly fix the errors you get when integrating async jobs (rq, celery etc) with your database in Djang</p>
<p>I’ve already written
<a class="reference external" href="https://spapas.github.io/2015/01/27/async-tasks-with-django-rq/">two</a>
<a class="reference external" href="https://spapas.github.io/2015/09/01/django-rq-redux/">articles</a>
about django-rq and implementing asynchronous tasks in Django. However I’ve found out
that there’s a very important thing missing from them: How to properly integrate
your asynchronous tasks with your Django database. This is very important because
if it is not done right you will start experiencing strange errors about missing
database objects or duplicate keys. The most troublesome thing about these errors is
that they are not consistent. Your app may work fine but for some reason you’ll see some
of your asynchronous tasks fail with these errors. When you re-queue the async jobs everything will
be ok.</p>
<p>Of course this behavior (code that runs <em>sometimes</em>) smells of a race condition but its not easy to debug
it if you don’t know the full story.</p>
<p>In the following I will describe the cause of this error and how you can fix it. As a companion
to this article I’ve implemented a small project that can be used to test the error and the
fix: <a class="reference external" href="https://github.com/spapas/async-job-db-fix">https://github.com/spapas/async-job-db-fix</a>.</p>
<p>Notice that although this article is written for django-rq it should also help people that have
the same problems with other async job systems (like celery or django-q).</p>
<div class="section" id="description-of-the-project">
<h2>Description of the project</h2>
<p>The project is very simple, you can just add a url and it will retrieve its content asynchronously and
report its length. For the models, it just has a <tt class="docutils literal">Task</tt> model which is used to provide information about
what we want to the asynchronous task to do and retrieve the result:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Task</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="n">url_length</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">PositiveIntegerField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">job_id</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre></div>
<p>It also has a home view that can be used to start new asynchronous tasks by creating a <tt class="docutils literal">Task</tt> object
with the url we got and passing it to the asynchronous task:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.views.generic.edit</span> <span class="kn">import</span> <span class="n">FormView</span>
<span class="kn">from</span> <span class="nn">.forms</span> <span class="kn">import</span> <span class="n">TaskForm</span>
<span class="kn">from</span> <span class="nn">.tasks</span> <span class="kn">import</span> <span class="n">get_url_length</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Task</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">transaction</span>
<span class="k">class</span> <span class="nc">TasksHomeFormView</span><span class="p">(</span><span class="n">FormView</span><span class="p">):</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">TaskForm</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'tasks_home.html'</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s1">'/'</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'url'</span><span class="p">])</span>
<span class="n">get_url_length</span><span class="o">.</span><span class="n">delay</span><span class="p">(</span><span class="n">task</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'tasks'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'-created_on'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
<p>And finally the asynchronous job itself that retrieves the task from the database,
requests its url and saves its length:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Task</span>
<span class="kn">from</span> <span class="nn">rq</span> <span class="kn">import</span> <span class="n">get_current_job</span>
<span class="kn">from</span> <span class="nn">django_rq</span> <span class="kn">import</span> <span class="n">job</span>
<span class="nd">@job</span>
<span class="k">def</span> <span class="nf">get_url_length</span><span class="p">(</span><span class="n">task_id</span><span class="p">):</span>
<span class="n">jb</span> <span class="o">=</span> <span class="n">get_current_job</span><span class="p">()</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<span class="nb">id</span><span class="o">=</span><span class="n">task_id</span>
<span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">task</span><span class="o">.</span><span class="n">url</span><span class="p">)</span>
<span class="n">task</span><span class="o">.</span><span class="n">url_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
<span class="n">task</span><span class="o">.</span><span class="n">job_id</span> <span class="o">=</span> <span class="n">jb</span><span class="o">.</span><span class="n">get_id</span><span class="p">()</span>
<span class="n">task</span><span class="o">.</span><span class="n">result</span> <span class="o">=</span> <span class="s1">'OK'</span>
<span class="n">task</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
</pre></div>
<p>The above should be fairly obvious: The user visits the homepage and enters a url at the input. When he presses submit
the view will create a new <tt class="docutils literal">Task</tt> object with the url that the user entered and fire-off the <tt class="docutils literal">get_url_length</tt> asynchronous job passing the
task id of the task that was just created. It will then return immediately without waiting for the asynchronous job to complete. The user will
need to refresh to see the result of his job; this is the usual behavior with async jobs.</p>
<p>The asynchronous job on the other hand will retrieve the task whose id got as a parameter from the database, do the work it needs to do
and update the result when it is finished.</p>
<p>Unfortunately, the above simple setup will <em>probably</em> behave erratically by randomly throwing database related errors!</p>
</div>
<div class="section" id="cause-of-the-problem">
<h2>Cause of the problem</h2>
<p>In the previous section I said <em>probably</em> because the erratic behavior is caused by a specific setting of your Django project; the <tt class="docutils literal">ATOMIC_REQUESTS</tt>.
This setting can be set on your database connection and if it is <tt class="docutils literal"><span class="caps">TRUE</span></tt> then each request will be <em>atomic</em>. This means that each request will be tied with
a database transaction i.e a transaction will be started when your request starts and commited only when your requests finishes; if for some reason your
request throws an error then the transaction will be rolled back. An example of this setting is:</p>
<div class="highlight"><pre><span></span><span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'ENGINE'</span><span class="p">:</span> <span class="s1">'django.db.backends.sqlite3'</span><span class="p">,</span>
<span class="s1">'NAME'</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s1">'db.sqlite3'</span><span class="p">),</span>
<span class="s1">'ATOMIC_REQUESTS'</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Now, in my opinion, <tt class="docutils literal">ATOMIC_REQUESTS</tt> is a great thing to have because it makes everything much easier. I always set it to <tt class="docutils literal">True</tt> to my projects because
I don’t need to actually think about transactions and requests; I know that if there’s a problem in a request the whole transaction will be rolle back and no
garbage will be left in the database. If on the other hand for some reason a request does not need to be tied to a transaction I just set it off
for this specific transaction (using <cite>transaction.non_atomic_requests_</cite>). Please notice that by default the <tt class="docutils literal">ATOMIC_REQUESTS</tt> has a <tt class="docutils literal">False</tt> value which means that
the database will be in autocommit mode meaning that every command will be executed immediately.</p>
<p>So although the <tt class="docutils literal">ATOMIC_REQUESTS</tt> is great, it is actually the reason that there are problems with asynchronous tasks. Why? Let’s take a closer look at what the <tt class="docutils literal">form_valid</tt> of the view does:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'url'</span><span class="p">])</span> <span class="c1">#1</span>
<span class="n">get_url_length</span><span class="o">.</span><span class="n">delay</span><span class="p">(</span><span class="n">task</span><span class="o">.</span><span class="n">id</span><span class="p">)</span> <span class="c1">#2</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span> <span class="c1">#3</span>
</pre></div>
<p>It creates the task in #1, fires off the asynchronous task in #2 and continues the execution of the view processing in #3. The important thing to understand
here is that the transaction will be commited <em>only after #3</em> is finished. This means that there’s a possibility that the asynchronous task will be started
before #3 is finished thus it won’t find the task because the task will <em>not</em> be created yet(!) This is a little counter-intuitive but you must remember
that the async task is run by a worker which is a different process than your application server; the worker may be able to start before the transaction is commited.</p>
<p>If you want to actually see the problem <em>every time</em> you can add a small delay between the start of the async task and the <tt class="docutils literal">form_valid</tt> something like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'url'</span><span class="p">])</span>
<span class="n">get_url_length</span><span class="o">.</span><span class="n">delay</span><span class="p">(</span><span class="n">task</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>This will make the view more slow so the asynchronous worker will always have time to start executing the task (and get the not found error). Also notice
that if you had <tt class="docutils literal">ATOMIC_REQUESTS: False</tt> the above code would work fine because the task would be created immediately (auto-commited) and the async job would be able to find it.</p>
</div>
<div class="section" id="the-solution">
<h2>The solution</h2>
<p>So how is this problem solved? Well it’s not that difficult now that you know what’s causing it!</p>
<p>One solution would be to set <tt class="docutils literal">ATOMIC_REQUESTS</tt> to <tt class="docutils literal">False</tt> but that would make all database commands auto-commit so you’ll lose
request-transaction-tieing. Another solution would be to set <tt class="docutils literal">ATOMIC_REQUESTS</tt> to <tt class="docutils literal">True</tt> and disable atomic requests for the specific view that starts the
asynchronous job using <cite>transaction.non_atomic_requests_</cite>. This is a viable solution however I don’t like it because I’d lose the comfort of transaction per request
for this specific request and I would need to add my own transaction handling.</p>
<p>A third solution is to avoid messing with the database in your view and create the task object in the async job. Any parameters you want to pass to the async job would be
passed directly to the async function. This may work fine in some cases but I find it more safe to create the task in the database before starting the async job so that
I have better control and error handling. This way even if there’s an error in my worker and for some reason the async job never starts or it breaks before being able to
handle the database, I will have the task object in the database because it will have been created in the view.</p>
<p>Is there anything better? Isn’t there a way to start the executing the async job <em>after</em> the transaction of the
view is commited? Actually yes, there is! For this, <a class="reference external" href="https://docs.djangoproject.com/en/2.1/topics/db/transactions/#django.db.transaction.on_commit">transaction.on_commit</a> comes to the rescue! This function
receives a callback that will be called after the transaction is commited! Thus, to properly fix you project, you should
change the <tt class="docutils literal">form_valid</tt> method like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">url</span><span class="o">=</span><span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'url'</span><span class="p">])</span>
<span class="n">transaction</span><span class="o">.</span><span class="n">on_commit</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="n">get_url_length</span><span class="o">.</span><span class="n">delay</span><span class="p">(</span><span class="n">task</span><span class="o">.</span><span class="n">id</span><span class="p">))</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>Notice that I need to use <tt class="docutils literal">lambda</tt> to create a callback function that will call <tt class="docutils literal">get_url_length.delay(task.id)</tt> when the transaction is commited. Now
even though I have the delay there the async job will start after the transaction is commited, ie after the view handler is finished (after the 1 second delay).</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>From the above you should be able to understand why sometimes you have problems when your async jobs use the database. To fix it you have various
options but at least for me, the best solution is to start your async jobs <em>after</em> the transaction is commited using <tt class="docutils literal">transaction.on_commit</tt>. Just
change each <tt class="docutils literal">async.job.delay(parameters)</tt> call to <tt class="docutils literal">transaction.on_commit(lambda: async.job.delay(parameters))</tt> and you will be fine!</p>
</div>
Use du to find out the disk usage of each directory in unix2018-11-12T14:20:00+02:002018-11-12T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2018-11-12:/2018/11/12/du-disk-usage/<p class="first last">How to use the du utility to find out the disk usage of each directory in unix</p>
<p>One usual problem I have when dealing with production servers is that their
disks get filled. This results in various warnings and errors and should be fixed
immediately. The first step to resolve this issue is to actually find out where is that
hard disk space is used!</p>
<p>For this you can use the <cite>du</cite> unix tool with some parameters. The problem is that <cite>du</cite>
has various parameters (not needed for the task at hand) and the various
places I search for contain other info not related to this specific task.</p>
<p>Thus I’ve decided to write this small blog post to help people struggling with
this and also to help <em>me</em> avoid googling for it by searching in pages that
also contain other <tt class="docutils literal">du</tt> recipies and also avoid the trial and error that this
would require.</p>
<p>So to print out the disk usage summary for a directory go to that directory
and run <tt class="docutils literal">du <span class="pre">-h</span> <span class="pre">-s</span> *</tt>; you need to have access to the child subdirectories
so probably it’s better to try this as root (unless you go to your home dir
for example).</p>
<p>Here’s a sample usage:</p>
<pre class="code literal-block">
[root@server1 /]# cd /
[root@server1 /]# du -h -s *
7.2M bin
55M boot
164K dev
35M etc
41G home
236M lib
25M lib64
20K lost+found
8.0K media
155G mnt
0 proc
1.6G root
12M sbin
8.0K srv
427M tmp
3.2G usr
8.9G var
</pre>
<p>The parameters are -h to print human readable sizes (G, M etc) and -s to
print a summary usage of <em>each</em> parameter. Since this will output the
summary for each parameter I finally pass <tt class="docutils literal">*</tt> to be changed to all files/dirs
in that directory. If I used <tt class="docutils literal">du <span class="pre">-h</span> <span class="pre">-s</span> /tmp</tt> instead I’d get the total usage only for
the <tt class="docutils literal">/tmp</tt> directory.</p>
<p>Another trick that may help you quickly find out the offending directories is to
append the <tt class="docutils literal">| grep G</tt> pipe command (i.e run <tt class="docutils literal">du <span class="pre">-h</span> <span class="pre">-s</span> * | grep G</tt>) which will
filter out only the entries containing a <tt class="docutils literal">G</tt> (i.e only print the folders having
more than 1 <span class="caps">GB</span> size). Yeh I know that this will also print entries that have
also a G in their name but since there aren’t many directores that have
G in their name you should be ok.</p>
<p>If you run the above from <tt class="docutils literal">/</tt> so that <tt class="docutils literal">/proc</tt> is included you may
get a bunch of <tt class="docutils literal">du: cannot access 'proc/nnn/task/nnn/fd/4': No such file or directory</tt>
errors; just add the <tt class="docutils literal">2> /dev/null</tt> pipe redirect to redirect the stderr output
to <tt class="docutils literal">/dev/null</tt>, i.e run <tt class="docutils literal">du <span class="pre">-h</span> <span class="pre">-s</span> * 2> /dev/null</tt>.</p>
<p>Finally, please notice that if there are <em>lots</em> of files in your directory you’ll get
a lot of output entries (since the <cite>*</cite> will match both files and directories).
In this case you can use <tt class="docutils literal">echo */</tt> or <tt class="docutils literal">ls <span class="pre">-d</span> */</tt> to list only the directories;
append that command inside a ` pair or <tt class="docutils literal">$()</tt> (to substitute for the command
output) instead of the <tt class="docutils literal">*</tt> to only get the sizes of the
directories, i.e run <tt class="docutils literal">du <span class="pre">-h</span> <span class="pre">-s</span> $(echo */)</tt> or <tt class="docutils literal">du <span class="pre">-h</span> <span class="pre">-s</span> `echo */`</tt>.</p>
<p>One thing that you must be aware of is that this command may take <em>a long time</em>
especially if you have lots of small files somewhere. Just let it run and it
should finish after some time. If it takes too long time try to exclude any
mounted network
directories (either with <span class="caps">SMB</span> or <span class="caps">NFS</span>) since these will take extra long time.</p>
<p>Also, if you awant a nice interactive output
using ncurses you can download and compile the <a class="reference external" href="https://dev.yorhel.nl/ncdu">ncdu tool</a> (NCurses Disk Usage).</p>
Adding a delay to Django HTTP responses2018-05-08T23:20:00+03:002018-05-08T23:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2018-05-08:/2018/05/08/django-reponse-add-delay/<p class="first last">How add a delay to your Django <span class="caps">HTTP</span> responses using a middleware or a <span class="caps">CBV</span></p>
<p>Sometimes you’d like to make your Django views more slow by adding a fake delay. This may
sound controversial (why would somebody want to make some of his views slower) however it is a real requirement,
at least when developing an application.</p>
<p>For example, you may be using a <span class="caps">REST</span> <span class="caps">API</span> and you want to implement a spinner while your form is loading. However, usually when
developing your responses will load so soon that you won’t be able to admire your spinner in all its glory! Also, when you
submit a <span class="caps">POST</span> form (i.e a form that changes your data), it is advisable to disable your submit button so that when your users
double click it the form won’t be submitted two times (it may seem strange to some people but this is a very common error that
has bitten me many times; there are many users that think that they need to double click the buttons; thus I always disable
my submit buttons after somebody clicks them); in this case you also need to make your response a little slower to make sure
that the button is actually disabled!</p>
<p>I will propose two methods for adding this delay to your responses. One that will affect all (or most) your views using
a <a class="reference external" href="https://docs.djangoproject.com/en/2.0/topics/http/middleware/">middleware</a> and another that you can add to any <span class="caps">CBV</span> you want using a mixin; please see my previous <a class="reference external" href="https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/"><span class="caps">CBV</span> guide</a> for more on
Django CBVs and mixins. For the middleware solution we’ll also take a quick look at what is the Django middleware mechanism and
how it can be used to add functionality.</p>
<div class="section" id="using-middleware">
<h2>Using middleware</h2>
<p>The Django <a class="reference external" href="https://docs.djangoproject.com/en/2.0/topics/http/middleware/">middleware</a> is a mechanism for adding your own code to the Django request / response cycle. I’ll try to explain
this a bit; Django is waiting for an <span class="caps">HTTP</span> Request (i.e <span class="caps">GET</span> a url with these headers and these query parameters), it will
parse this <span class="caps">HTTP</span> Request and prepare an <span class="caps">HTTP</span> Response (i.e some headers and a Payload). Your view will be the main actor for
retrieving the <span class="caps">HTTP</span> response and returning the <span class="caps">HTTP</span> request. However, using this middleware mechanism Django allows you to
enable other actors (the middleware) that will universally modify the <span class="caps">HTTP</span> request before passing it to your view and will also
modify the view’s <span class="caps">HTTP</span> respone before sending it back to the client.</p>
<p>Actually, a list of middleware called … <tt class="docutils literal"><span class="caps">MIDDLEWARE</span></tt> is <a class="reference external" href="https://docs.djangoproject.com/en/2.0/topics/http/middleware/#activating-middleware">defined by default</a> in the <tt class="docutils literal">settings.py</tt> of all new Django
projects; these are used to add various capabilities
that are universally needed, for example session support, various security enablers, django message support and others. You
can easily attach your own middleware to that list to add extra functionality. Notice that the order of the middleware in the <tt class="docutils literal"><span class="caps">MIDDLEWARE</span></tt>
list actually matters. Middleware later in the list will be executed after the ones previous in the list; we’ll see some consequences of this later.</p>
<p>Now the time has come to take a quick look at how to implement a middleware, <a class="reference external" href="https://docs.djangoproject.com/en/2.0/topics/http/middleware/#writing-your-own-middleware">taken from the Django docs</a>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">SimpleMiddleware</span><span class="p">:</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">get_response</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">get_response</span> <span class="o">=</span> <span class="n">get_response</span>
<span class="c1"># One-time configuration and initialization.</span>
<span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="c1"># Code to be executed for each request before</span>
<span class="c1"># the view (and later middleware) are called.</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_response</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="c1"># Code to be executed for each request/response after</span>
<span class="c1"># the view is called.</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>Actually you can implement the middleware as a nested function however I prefer the classy version. The comments should be really enlightening:
When your project is started the constructor (<tt class="docutils literal">__init__</tt>) will be called once, for example if you want to read a configuration setting from the database
then you should do it in the <tt class="docutils literal">__init__</tt> to avoid calling the database everytime your middleware is executed (i.e for <em>every</em> request). The <tt class="docutils literal">__call__</tt> is
a special method that gets translated to calling this class instance as a function, i.e if you do something like:</p>
<div class="highlight"><pre><span></span><span class="n">sm</span> <span class="o">=</span> <span class="n">SimpleMiddleware</span><span class="p">()</span>
<span class="n">sm</span><span class="p">()</span>
</pre></div>
<p>Then <tt class="docutils literal">sm()</tt> will execute the <tt class="docutils literal">__call__</tt>; there are various similar <a class="reference external" href="http://www.diveintopython3.net/special-method-names.html">python special methods</a>, for example <tt class="docutils literal">__len__</tt>, <tt class="docutils literal">__eq__</tt> etc</p>
<p>Now, as you can see the <tt class="docutils literal">__call__</tt> special method has four parts:</p>
<ul class="simple">
<li>Code that is executed <em>before</em> the <tt class="docutils literal">self.get_response()</tt> method is called; here you should modify the request object. Middleware will reach this point in the order they are listed.</li>
<li>The actual call to <tt class="docutils literal">self.get_response()</tt></li>
<li>Code that is executed <em>after</em> the <tt class="docutils literal">self.get_response()</tt> method is called; here you should modify the response object. Middleware will reach this point in the reverse order they are listed.</li>
<li>Returning the response to be used by the next middleware</li>
</ul>
<p>Notice that <tt class="docutils literal">get_response</tt> will call the next middleware; while the <tt class="docutils literal">get_response</tt> for the last middleware will actually call the view. Then
the view will return a response which could be modified (if needed) by the middlewares in the opposite order of their definition list.</p>
<p>As an example, let’s define two simple middlewares:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">M1</span><span class="p">:</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">get_response</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">get_response</span> <span class="o">=</span> <span class="n">get_response</span>
<span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"M1 before response"</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_response</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"M1 after response"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
<span class="k">class</span> <span class="nc">M2</span><span class="p">:</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">get_response</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">get_response</span> <span class="o">=</span> <span class="n">get_response</span>
<span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"M2 before response"</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_response</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"M2 after response"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>When you define <tt class="docutils literal"><span class="caps">MIDDLEWARE</span> = ['M1', 'M2']</tt> you’ll see the following:</p>
<div class="highlight"><pre><span></span><span class="c1"># Got the request</span>
<span class="n">M1</span> <span class="n">before</span> <span class="n">response</span>
<span class="n">M2</span> <span class="n">before</span> <span class="n">response</span>
<span class="c1"># The view is rendered to the response now</span>
<span class="n">M2</span> <span class="n">after</span> <span class="n">response</span>
<span class="n">M1</span> <span class="n">after</span> <span class="n">response</span>
<span class="c1"># Return the response</span>
</pre></div>
<p>Please notice a middleware may not call <tt class="docutils literal">self.get_response</tt> to continue the chain but return directly a response (for example a 403 Forbiden response).</p>
<p>After this quick introduction to how middleware works, let’s take a look at a skeleton for the time-delay middleware:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">time</span>
<span class="k">class</span> <span class="nc">TimeDelayMiddleware</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">get_response</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">get_response</span> <span class="o">=</span> <span class="n">get_response</span>
<span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_response</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>This is really simple, I’ve just added an extra line to the previous middleware. This line adds a one-second delay to all responses. I’ve
added it before <tt class="docutils literal">self.get_response</tt> - because this delay does not depend on anything, I could have added it after <tt class="docutils literal">self.get_response</tt>
without changes in the behavior. Also, the order of this middleware in the <tt class="docutils literal"><span class="caps">MIDDLEWARE</span></tt> list doesn’t matter since it doesn’t depend on
other middleware (it just needs to run to add the delay).</p>
<p>This middleware may have a little more functionality, for example to configure the delay from the settings or add the delay only for
specific urls (by checking the <tt class="docutils literal">request.path</tt>).
Here’s how these extra features could be implemented:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="k">class</span> <span class="nc">TimeDelayMiddleware</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">get_response</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">get_response</span> <span class="o">=</span> <span class="n">get_response</span>
<span class="bp">self</span><span class="o">.</span><span class="n">delay</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">REQUEST_TIME_DELAY</span>
<span class="k">def</span> <span class="fm">__call__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="k">if</span> <span class="s1">'/api/'</span> <span class="ow">in</span> <span class="n">request</span><span class="o">.</span><span class="n">path</span><span class="p">:</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">delay</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_response</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>The above will add the delay only to requests whose path contains <tt class="docutils literal">'/api'</tt>. Another case is if you want to
only add the delay for <tt class="docutils literal"><span class="caps">POST</span></tt> requests by checking that <tt class="docutils literal">request.method == '<span class="caps">POST</span>'</tt>.</p>
<p>Now, to install this middleware, you can configure your <tt class="docutils literal"><span class="caps">MIDDLEWARE</span></tt> like this in your <tt class="docutils literal">settings.py</tt>
(let’s say that you have an application named <tt class="docutils literal">core</tt> containing a module named <tt class="docutils literal">middleware</tt>):</p>
<div class="highlight"><pre><span></span><span class="n">MIDDLEWARE</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'django.middleware.security.SecurityMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.sessions.middleware.SessionMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.common.CommonMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.csrf.CsrfViewMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.auth.middleware.AuthenticationMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.messages.middleware.MessageMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.clickjacking.XFrameOptionsMiddleware'</span><span class="p">,</span>
<span class="s1">'core.middleware.TimeDelayMiddleware'</span><span class="p">,</span>
<span class="p">]</span>
</pre></div>
<p>The other middleware are the default ones in Django. One more thing to consider is that if
you have a single settings.py this middleware will be called; one way to override the delay
is to check for settings.<span class="caps">DEBUG</span> and only call <tt class="docutils literal">time.sleep</tt> when <tt class="docutils literal"><span class="caps">DEBUG</span> == True</tt>. However,
the proper way to do it is to have different settings for your development and production
environments and add the <tt class="docutils literal">TimeDelayMiddleware</tt> only to your development <tt class="docutils literal"><span class="caps">MIDDLEWARE</span></tt> list.
Having different settings for each development is a <a class="reference external" href="https://medium.com/@ayarshabeer/django-best-practice-settings-file-for-multiple-environments-6d71c6966ee2">common practice in Django</a> and I totally
recommend to use it.</p>
</div>
<div class="section" id="using-cbvs">
<h2>Using CBVs</h2>
<p>Another method to add a delay to the execution of a view is to implement a TimeDelayMixin and inherit
your Class Based View from it. As we’ve seen in the <a class="reference external" href="https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/"><span class="caps">CBV</span> guide</a>, the <tt class="docutils literal">dispatch</tt> method is the one
that is always called when your <span class="caps">CBV</span> is rendered, thus your <tt class="docutils literal">TimeDelayMixin</tt> could be implemented like this:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">time</span>
<span class="k">class</span> <span class="nc">TimeDelayMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>This is very simple (and you can use similar techniques as described for the middleware above to configure
the delay time or add the delay only when <tt class="docutils literal">settings.<span class="caps">DEBUG</span> == True</tt> etc) - to actually use it, just inherit your
view from this mixin, f.e:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DelayedSampleListView</span><span class="p">(</span><span class="n">TimeDelayMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Sample</span>
</pre></div>
<p>Now whenever you call your <tt class="docutils literal">DelayedSampleListView</tt> you’ll see it after the configured delay!</p>
<p>What is really interesting is that the <tt class="docutils literal">dispatch</tt> method actually exists (and has the same functionality) also
in Django Rest Framework CBVs, thus using the same mixin you can add the delay not only your normal CBVs but
also your <span class="caps">DRF</span> <span class="caps">API</span> views!</p>
</div>
Easy immutable objects in Javascript2018-04-05T12:20:00+03:002018-04-05T12:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2018-04-05:/2018/04/05/easy-immutable-objects/<p class="first last">How to avoid mutations in your objects and a poor man’s lens!</p>
<p>With the rise of <a class="reference external" href="https://redux.js.org">Redux</a> and other similar Javascript frameworks (e.g <a class="reference external" href="https://hyperapp.js.org">Hyperapp</a>) that
try to be a little more <em>functional</em> (functional as in functional programming), a
new problem was introduced to Javascript programmers (at least to those that weren’t
familair with functional programming): How to keep their application’s
state “immutable”.</p>
<p>Immutable means that the state should be an object that does not change (mutates) - instead
of changing it, Redux needs the state object to be <a class="reference external" href="https://redux.js.org/basics/reducers">created from the beginning</a>.</p>
<p>So when something happens in your application you need to discard
the existing state object and create a new one from scratch by modifying and copying the previous state values.
This is easy
in most toy-apps that are used when introducing the concept, for example if your state is <tt class="docutils literal">{counter: 0}</tt>
then you could just define the reducer for the <tt class="docutils literal"><span class="caps">ADD</span></tt> action (i.e when the user clicks the <tt class="docutils literal">+</tt> button) like this:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">reducer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="p">{</span><span class="nx">counter</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">},</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'ADD'</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s1">'counter'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">counter</span><span class="o">+</span><span class="mf">1</span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">default</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Unfortunately, your application will definitely have a much more complex state than this!</p>
<p>In the following, I’ll do a quick introduction on how to keep your state objects immutable
using modern Javascript techniques, I’ll present how complex it is to modify non-trivial
immutable objects and finally I’ll give you a quick recipe for modifying your non-trivial
immutable objects. If you want to play with the concepts I’ll introduce you can do it at a
<a class="reference external" href="https://repl.it/@spapas/JS-Drill-Down-objectarray-immutable">playground I’ve created on repl.it</a>.</p>
<p>Please keep in mind that this article has been written for <span class="caps">ES6</span> - take a look at my
<a class="reference external" href="https://spapas.github.io/2015/11/16/using-browserify-es6/">browserify with <span class="caps">ES6</span></a> article to see how you can also use it in your projects with Browserify.</p>
<p>Also, if you’d like to see a non-toy React + Redux application or you’d like a gentle
introduction to the concepts I talked about (state, reducers, actions etc)
you can follow along my <a class="reference external" href="https://spapas.github.io/2016/03/02/react-redux-tutorial/">React Redux tutorial</a>. This is a rather old article
(considering how quickly the Javascript framework state change) but the basic concepts
introduced there are true today.</p>
<div class="section" id="immutable-objects">
<h2>Immutable objects</h2>
<p>Let’s start our descent into avoiding mutations by supposing that you had
something a little more complex than the initial example, for example your state was like this:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'counter'</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'John'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'age'</span><span class="o">:</span><span class="w"> </span><span class="mf">36</span>
<span class="p">}</span>
</pre></div>
<p>If you continued the same example then your <tt class="docutils literal"><span class="caps">ADD</span></tt> reducer would need to return something like this</p>
<div class="highlight"><pre><span></span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'counter'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">counter</span><span class="o">+</span><span class="mf">1</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'age'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">age</span>
<span class="p">}</span>
</pre></div>
<p>This gets difficult and error prone very soon - and what happens if you later need to add another attribute to your state?</p>
<p>The correct way to implement this would be to enumerate all properties of state except ‘counter’, copy them to a new
object, and then assign counter+1 to the new object’s counter attribute. You could implement this by hand however,
thankfully, there’s the <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign">Object.assign</a> method! This method will copy all attributes from a list of objects
to an object which will return as a result and is defined like this:</p>
<div class="highlight"><pre><span></span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">target</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">sources</span><span class="p">)</span>
</pre></div>
<p>The <tt class="docutils literal">target</tt> parameter is the object that will retrieve all attributes from <tt class="docutils literal">sources</tt> (which is a variadic argument - you can
have as many sources as you want - even 0; in this case the target will be returned). For a quick example, running:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">o</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="s1">'a'</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">oo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">o</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="s1">'b'</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="p">,</span><span class="w"> </span><span class="s1">'c'</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="s1">'c'</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="p">})</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">oo</span><span class="p">,</span><span class="w"> </span><span class="nx">o</span><span class="o">===</span><span class="nx">oo</span><span class="p">)</span>
</pre></div>
<p>will return <tt class="docutils literal">{ a: 1, b: 2, c: 3 } true</tt> i.e the attributes ‘b’ and ‘c’ were copied to <tt class="docutils literal">o</tt> and it was assigned to <tt class="docutils literal">oo</tt> — notice
that o and oo are the same object (thus <tt class="docutils literal">o</tt> is modified now). Also, notice that the the attributes of objects to the right
have priority over the attributes of the objects to the left (<tt class="docutils literal">'c': 1</tt> was overriden by <tt class="docutils literal">'c': 3</tt>).</p>
<p>As you should have guessed by now, you should never pass the
state as the <tt class="docutils literal">target</tt> but instead you should create a new object, thus the <tt class="docutils literal"><span class="caps">ADD</span></tt> reducer should return the following:</p>
<div class="highlight"><pre><span></span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="s1">'counter'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">counter</span><span class="o">+</span><span class="mf">1</span><span class="p">)</span>
</pre></div>
<p>This means that it will create a new object which will copy all current attributes of state and increase the existing
<tt class="docutils literal">counter</tt> attribute.</p>
<p>I’d like to also add here that instead of using the <tt class="docutils literal">Object.assign</tt> method you could use the <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax">spread syntax</a>
to more or less do the same. The spread syntax on an object takes this object’s attributes and outputs them as key-value
dictionary pairs (for them to be used to initialize other objects). Thus, you can use the spread syntax to create an new object that has the same attributes
of another object like this:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">newState</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">}</span>
<span class="c1">// which is similar to</span>
<span class="nx">newState</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">)</span>
</pre></div>
<p>Of course you usually need to override some attributes, which can be passed directly to the newly created object,
for example for the <tt class="docutils literal"><span class="caps">ADD</span></tt> reducer:</p>
<div class="highlight"><pre><span></span><span class="k">return</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="s1">'counter'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">counter</span><span class="o">+</span><span class="mf">1</span><span class="w"> </span><span class="p">}</span>
</pre></div>
<p>Like <tt class="docutils literal">Object.assign</tt>, you can have as many sources as you want in your spread syntax thus nothing stops you from using <tt class="docutils literal">...</tt> multiple times to copy the attributes of multiple objects
for example you could define <tt class="docutils literal"><span class="caps">ADD</span></tt> like this:</p>
<div class="highlight"><pre><span></span><span class="k">return</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">...{</span><span class="s1">'counter'</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">counter</span><span class="o">+</span><span class="mf">1</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span>
</pre></div>
<p>The order is similar to Object.assign, i.e the attributes that follow will override the previous ones.</p>
<p>One final comment is that both <tt class="docutils literal">Object.assign</tt> and copying objects with the spread syntax will do a “shallow”
copy i.e it will copy only the outer object, not the objects its keys refer to. An example of this behavior is that
if you run the following:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="s1">'val'</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="w"> </span><span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="nx">a</span><span class="w"> </span><span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{...</span><span class="nx">x</span><span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span><span class="w"> </span><span class="nx">y</span><span class="p">)</span>
<span class="nx">x</span><span class="p">[</span><span class="s1">'val2'</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">4</span>
<span class="nx">y</span><span class="p">[</span><span class="s1">'val2'</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">5</span>
<span class="nx">a</span><span class="p">[</span><span class="s1">'val'</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">33</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span><span class="w"> </span><span class="nx">y</span><span class="p">)</span>
</pre></div>
<p>you’ll get:</p>
<div class="highlight"><pre><span></span><span class="p">{</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">val</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">val</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span>
<span class="p">{</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">val</span><span class="o">:</span><span class="w"> </span><span class="mf">33</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">val2</span><span class="o">:</span><span class="w"> </span><span class="mf">4</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">val</span><span class="o">:</span><span class="w"> </span><span class="mf">33</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">val2</span><span class="o">:</span><span class="w"> </span><span class="mf">5</span><span class="w"> </span><span class="p">}</span>
</pre></div>
<p>i.e <tt class="docutils literal">x</tt> and <tt class="docutils literal">y</tt> got a different <tt class="docutils literal">val2</tt> attribute since they not the same object, however both <tt class="docutils literal">x</tt> and <tt class="docutils literal">y</tt>
have a reference to the <em>same</em> <tt class="docutils literal">a</tt> thus when it’s <tt class="docutils literal">val</tt> attribute was changed this change appears to both <tt class="docutils literal">x</tt> and <tt class="docutils literal">y</tt>!</p>
<p>What the above means is that if you have a state object containing
other objects (or arrays) you will also need to copy these children
objects to keep your state immutable. We’ll see examples on this later.</p>
</div>
<div class="section" id="immutable-arrays">
<h2>Immutable arrays</h2>
<p>One thing we haven’t talked about yet is what happens if there’s an array in the state, for example your state is
<tt class="docutils literal">let <span class="pre">state=[]</span></tt> and you have and <tt class="docutils literal"><span class="caps">APPEND</span></tt> reducer that puts something in the end of that array. The naive (and wrong)
way to do it is to call <tt class="docutils literal">push</tt> directly to the state - this will mutate your state and is not be allowed!</p>
<p>You need to copy the array elements and the tool for this job is <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice">Array.slice</a>. This methods takes two optional arguments (<tt class="docutils literal">begin</tt>
and <tt class="docutils literal">end</tt>) that define the range of elements that will be copied; if you call it without arguments then it will copy
the whole array. Using slice, your <tt class="docutils literal"><span class="caps">APPEND</span></tt> reducer can be like this:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">newState</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">slice</span><span class="p">()</span>
<span class="nx">newState</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">'new element'</span><span class="p">)</span>
<span class="k">return</span><span class="w"> </span><span class="nx">newState</span>
</pre></div>
<p>Also, you could use the <cite>Array.concat</cite> method which will return a new array by copying all the elements of its arguments</p>
<div class="highlight"><pre><span></span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">concat</span><span class="p">([</span><span class="s1">'new element'</span><span class="p">])</span>
</pre></div>
<p>This will append <tt class="docutils literal">new element</tt> to a new object that will have the elements of state (it won’t modify the
existing state) and is easier if you have this exact requirement. The advantage of slice is that you can
use it to add/remove/modify elements from any place in the original array. For example, here’s how you can
add an element after the first element of an array:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s1">'a'</span><span class="p">,</span><span class="w"> </span><span class="s1">'b'</span><span class="p">,</span><span class="w"> </span><span class="s1">'c'</span><span class="w"> </span><span class="p">]</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">x</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="mf">1</span><span class="p">).</span><span class="nx">concat</span><span class="p">([</span><span class="s1">'second'</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="nx">x</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">1</span><span class="p">,</span><span class="mf">3</span><span class="p">))</span>
</pre></div>
<p>Now <tt class="docutils literal">y</tt> will be equal to <tt class="docutils literal">[ 'a', 'second', 'b', 'c' ]</tt>. So the above will get the first (0-th) element from the <tt class="docutils literal">x</tt>
array and concat it with another element (<tt class="docutils literal">second</tt>) and the remaining elements of <tt class="docutils literal">x</tt>. Remember that <tt class="docutils literal">x</tt> is not
modifyied since <tt class="docutils literal">concat</tt> will create a new array.</p>
<p>In a similar fashion to objects, instead of using concat it is much easier to use the spread syntax. The spread syntax for
an array will output its elements one after the other for them to be used by other arrays. Thus, continuing from the
previous example, <tt class="docutils literal"><span class="pre">[...x]</span></tt> will return a new array with the elements of <tt class="docutils literal">x</tt> (so it is similar to <tt class="docutils literal">x.slice()</tt> or <tt class="docutils literal">x.concat()</tt>),
thus to re-generate the previous example you’ll do something like</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">y</span><span class="o">=</span><span class="p">[...</span><span class="nx">x</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="mf">1</span><span class="p">),</span><span class="w"> </span><span class="s1">'second'</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">x</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">1</span><span class="p">,</span><span class="mf">3</span><span class="p">)]</span>
</pre></div>
<p>All three of concat, slice or the spread syntax will do a shallow copy (similar to how Object.assign works) so the same
conclusions from the previous section are true here: If you have arrays inside other arrays (or objects) you’ll need to copy the inner
arrays recursively.</p>
</div>
<div class="section" id="more-complex-cases">
<h2>More complex cases</h2>
<p>We’ll now take a look at some more complex cases and see how quickly it gets difficult because of the shallow copying.
Let’s suppose that our state is the following:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'user'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'first_name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'John'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'last_name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Doe'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'address'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'city'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Athens'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'country'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Greece'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'zip'</span><span class="o">:</span><span class="w"> </span><span class="s1">'12345'</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>and we want to assign a <tt class="docutils literal">group</tt> attribute to the state. This can be easily done with <tt class="docutils literal">assign</tt>:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">groups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[{</span>
<span class="w"> </span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'group1'</span>
<span class="p">}]</span>
<span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="nx">groups</span>
<span class="p">})</span>
</pre></div>
<p>or spread:</p>
<div class="highlight"><pre><span></span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="nx">groups</span>
<span class="p">}</span>
</pre></div>
<p>Notice that instead of <tt class="docutils literal">'groups': groups</tt> I could have used the <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Syntax">shorthand syntax</a> and written only <tt class="docutils literal">groups</tt> and it would still work
(i.e <tt class="docutils literal">state = <span class="pre">{...state,</span> groups}</tt> is the same). In all cases, the resulting state will be:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="s1">'user'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'first_name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'John'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'last_name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Doe'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'address'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'city'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Athens'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'country'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Greece'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'zip'</span><span class="o">:</span><span class="w"> </span><span class="s1">'12345'</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="p">[{</span>
<span class="w"> </span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'group1'</span>
<span class="w"> </span><span class="p">}]</span>
<span class="p">}</span>
</pre></div>
<p>From now on I’ll only use the spread syntax which is more compact.</p>
<p>Let’s try to change the user’s name. This is not as easy as the first example because we need to:</p>
<ul class="simple">
<li>Create a new copy of the <tt class="docutils literal">user</tt> object with the new first name</li>
<li>Create a new copy of the <tt class="docutils literal">state</tt> object with the new user object created above</li>
</ul>
<p>This can be done in two steps like this:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">user</span><span class="w"> </span><span class="o">=</span><span class="p">{...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'user'</span><span class="p">],</span><span class="w"> </span><span class="s1">'first_name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Jack'</span><span class="p">}</span>
<span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="nx">user</span><span class="p">}</span>
</pre></div>
<p>or in one step like this:</p>
<div class="highlight"><pre><span></span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="s1">'user'</span><span class="o">:</span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'user'</span><span class="p">],</span><span class="w"> </span><span class="s1">'first_name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Jack'</span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>The single step assignment is the combination of the two step described above. It is a little more complex
but it saves typing and is prefered because it allows the reducer function to have a single expression. Now
let’s try to modify the user’s zip code. We’ll do it in three steps first:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">address</span><span class="w"> </span><span class="o">=</span><span class="p">{...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'user'</span><span class="p">][</span><span class="s1">'address'</span><span class="p">],</span><span class="w"> </span><span class="s1">'zip'</span><span class="o">:</span><span class="w"> </span><span class="s1">'54321'</span><span class="p">}</span>
<span class="nx">user</span><span class="w"> </span><span class="o">=</span><span class="p">{...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'user'</span><span class="p">],</span><span class="w"> </span><span class="nx">address</span><span class="p">}</span>
<span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="nx">user</span><span class="p">}</span>
</pre></div>
<p>And now in one:</p>
<div class="highlight"><pre><span></span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="s1">'user'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'user'</span><span class="p">],</span><span class="w"> </span><span class="s1">'address'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'user'</span><span class="p">][</span><span class="s1">'address'</span><span class="p">],</span><span class="w"> </span><span class="s1">'zip'</span><span class="o">:</span><span class="w"> </span><span class="mf">54321</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}}</span>
</pre></div>
<p>Now, as can be seen in the above examples, modifying (without mutating) a compex state object
is not very easy - it needs much thinking and is too error prone! This will be even more
apparent when we also get the array modifications into the equation, for example by adding another
two groups:</p>
<div class="highlight"><pre><span></span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="nx">groups</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'groups'</span><span class="p">].</span><span class="nx">slice</span><span class="p">(),</span>
<span class="w"> </span><span class="p">{</span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">'group2'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">'group3'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</pre></div>
<p>The above copies the existing state and assigns to it a new <tt class="docutils literal">groups</tt> object by copying
the existing groups and appending two more groups to that array! The state now will be:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="nx">user</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">first_name</span><span class="o">:</span><span class="w"> </span><span class="s1">'Jack'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">last_name</span><span class="o">:</span><span class="w"> </span><span class="s1">'Doe'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">address</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">city</span><span class="o">:</span><span class="w"> </span><span class="s1">'Athens'</span><span class="p">,</span><span class="w"> </span><span class="nx">country</span><span class="o">:</span><span class="w"> </span><span class="s1">'Greece'</span><span class="p">,</span><span class="w"> </span><span class="nx">zip</span><span class="o">:</span><span class="w"> </span><span class="mf">54321</span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">groups</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">'group1'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">'group2'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">'group3'</span><span class="p">,</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</pre></div>
<p>As a final examply, how can we add the missing <tt class="docutils literal">id</tt> attribute to the first group?
Following the above techniques:</p>
<div class="highlight"><pre><span></span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="nx">groups</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'groups'</span><span class="p">][</span><span class="mf">0</span><span class="p">],</span><span class="w"> </span><span class="s1">'id'</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">},</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">[</span><span class="s1">'groups'</span><span class="p">].</span><span class="nx">slice</span><span class="p">(</span><span class="mf">1</span><span class="p">)</span>
<span class="w"> </span><span class="p">]</span>
<span class="p">}</span>
</pre></div>
<p>One more time what the above does?</p>
<ul class="simple">
<li>Creates a new object and copies all existing properties of state to it</li>
<li>Creates a new array which assigns it to the new state’s <tt class="docutils literal">groups</tt> attribute</li>
<li>For the first element of that array it copies all attributes of the first element of state[‘groups’] and assings it an <tt class="docutils literal">id=1</tt> attribute</li>
<li>For the remaining elements of that array it copies all elements of state[‘groups] after the first one</li>
</ul>
<p>Now think what would happen if we had an even more complex state with 3 or 4 nested levels!</p>
</div>
<div class="section" id="immutability-s-little-helpers">
<h2>Immutability’s little helpers</h2>
<p>As you’ve seen from the previous examples, using immutable objects is not as easy as seems from
the toy examples. Actually, drilling down into complex immutable
objects and returning new ones that have
some values changed is a well-known problem in the functional world and has already a solution
called “lenses”. This is a funny name but it more or less means that you use a magnifying lens to look at
exactly the value you want and modify or retrieve it. The problem with lenses is that although they solve
the problem I mention is that if you want to use them you’ll need to dive deep into functional
programming and also you’ll need to include an extra library to your project (even if you only
want this specific capability).</p>
<p>For completeness, here’s the <a class="reference external" href="http://ramdajs.com/docs/#lens">the docs on lens</a> from <a class="reference external" href="http://ramdajs.com">Ramda</a> which is a well known Javascript functional library.
This needs you to understand what is <tt class="docutils literal">prop</tt>, what is <tt class="docutils literal">assoc</tt> and then how to use the lens with <tt class="docutils literal">view</tt>,
<tt class="docutils literal">set</tt> and <tt class="docutils literal">over</tt>. For me, these are way too much things to remember for such a specific thing. Also, notice
that the minified version of Ramda is around 45 kb which is not small. Yes, if I wanted
to fully use Ramda or a similar library I’d be delighted to use all these techniques and include it as a
dependency - however most people prefer to stick with more familiar (and more procedural) concepts.</p>
<p>The helpers I’m going to present here are more or less a poor man’s lens, i.e you will be able to use the basic
functionality of a lens but…</p>
<ul class="simple">
<li>without the peculiar syntax and</li>
<li>without the need to learn more functional concepts than what you’ll want and</li>
<li>without the need to include any more external dependencies</li>
</ul>
<p>Pretty good deal, no?</p>
<p>In any case, a lens has two parts, a get and a set. The get will be used to drill down and retrieve a value from a
complex object while the set will be used to drill down and assign a value to a complex object. The set does not
modify the object but returns a new one. The get lens is not really needed since you can easily drill down to an
object using the good old index syntax but I’ll include it here for completenes.</p>
<p>We’ll start with the get which seems easier. For this, I’ll just create a function that will take an object and
a path inside that object as parameter and retrieve the value at that path. The path could be either a string of the form
‘a.0.c.d’ or an array [‘a’, ‘0’, ‘c’, ‘d’] - for numerical indeces we’ll consider an array at that point.</p>
<p>Thus, for the object <tt class="docutils literal">{'a': <span class="pre">[{'b':</span> {'c': {'d': 32} <span class="pre">}}]}</span></tt> when the lens getter is called with either
<tt class="docutils literal">'a.0.b.c'</tt> or [‘a’, 0, ‘b’, ‘c’] as the path, it should return <tt class="docutils literal">{'d': 32}</tt>.</p>
<p>To implement the get helper I will use a functional concept, <tt class="docutils literal">reduce</tt>. I’ve already explained this concept
in my <a class="reference external" href="https://spapas.github.io/2016/03/02/react-redux-tutorial/#interlude-so-what-s-a-reducer">previous react-redux tutorial</a> so I urge you to read that article for more info. Using reduce we
can apply one by one accumulatively the members of the path to the initial object and the result will be
the value of that path. Here’s the implementation of <tt class="docutils literal">pget</tt> (from property get):</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">objgetter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">accumulator</span><span class="p">,</span><span class="w"> </span><span class="nx">currentValue</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">accumulator</span><span class="p">[</span><span class="nx">currentValue</span><span class="p">];</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">pget</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">path</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="p">(</span><span class="ow">typeof</span><span class="w"> </span><span class="nx">path</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'string'</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">path</span><span class="w"> </span><span class="ow">instanceof</span><span class="w"> </span><span class="nb">String</span><span class="p">)</span><span class="o">?</span><span class="nx">path</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s1">'.'</span><span class="p">)</span><span class="o">:</span><span class="nx">path</span>
<span class="p">).</span><span class="nx">reduce</span><span class="p">(</span><span class="nx">objgetter</span><span class="p">,</span><span class="w"> </span><span class="nx">obj</span><span class="p">)</span>
</pre></div>
<p>I have defined an objgetter reducer function that gets an accumulated object and the current
value of the path and just returns the <tt class="docutils literal">currentValue</tt> index of that accumulated object. Finally,
for the get lens (named <tt class="docutils literal">pget</tt>) I just check to see if the path is a string or an array (if it’s
a string I split it on dots) and then I “reduce” the path using the objgetter defined above and
starting by the original object as the initial value. To understand how it is working, let’s try calling it
for an object:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">s1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="s1">'a'</span><span class="o">:</span><span class="w"> </span><span class="p">[{</span><span class="s1">'b'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="s1">'c'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="s1">'d'</span><span class="o">:</span><span class="w"> </span><span class="mf">32</span><span class="p">}</span><span class="w"> </span><span class="p">}}]}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">pget</span><span class="p">(</span><span class="nx">s1</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="s1">'a'</span><span class="p">,</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="s1">'b'</span><span class="p">,</span><span class="w"> </span><span class="s1">'c'</span><span class="p">]))</span>
</pre></div>
<p>The above <tt class="docutils literal">pget</tt> will call <tt class="docutils literal">reduce</tt> on the passed array using the defined <tt class="docutils literal">objgetter</tt> above
as the reducer function and <tt class="docutils literal">s1</tt> as the original object. So, the reducer function will be called with
the following values each time:</p>
<table border="1" class="docutils">
<colgroup>
<col width="68%" />
<col width="32%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">accumulator</th>
<th class="head">currentvalue</th>
</tr>
</thead>
<tbody valign="top">
<tr><td><tt class="docutils literal">s1</tt></td>
<td><tt class="docutils literal">'a'</tt></td>
</tr>
<tr><td><tt class="docutils literal"><span class="pre">s1['a']</span></tt></td>
<td><tt class="docutils literal">0</tt></td>
</tr>
<tr><td><tt class="docutils literal"><span class="pre">s1['a'][0]</span></tt></td>
<td><tt class="docutils literal">'b'</tt></td>
</tr>
<tr><td><tt class="docutils literal"><span class="pre">s1['a'][0]['b']</span></tt></td>
<td><tt class="docutils literal">'c'</tt></td>
</tr>
<tr><td><tt class="docutils literal"><span class="pre">s1['a'][0]['b']['c']</span></tt></td>
<td> </td>
</tr>
</tbody>
</table>
<p>Thus the result will be exactly what we wanted <tt class="docutils literal">{'d' :32}</tt>. An interesting thing is that it’s working
fine without the need to differentiate between arrays and objects because of how index access <tt class="docutils literal">[]</tt> works.</p>
<p>Continuing for the set lens (which will be more difficult), I’ll first represent a simple version that
works only with objects and an array path but displays the main idea of how this will work: It uses recursion i.e it will call itself to gradually build the new object. Here’s how it is implemented:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">pset0</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">path</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">path</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">length</span><span class="o">==</span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="nx">idx</span><span class="p">]</span><span class="o">:</span><span class="w"> </span><span class="nx">val</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">remaining</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">path</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">1</span><span class="p">)</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">obj</span><span class="p">,</span>
<span class="w"> </span><span class="p">[</span><span class="nx">idx</span><span class="p">]</span><span class="o">:</span><span class="w"> </span><span class="nx">pset0</span><span class="p">(...[</span><span class="nx">obj</span><span class="p">[</span><span class="nx">idx</span><span class="p">]],</span><span class="w"> </span><span class="nx">remaining</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>As already explained, I have assumed that the path is an array of indeces and that the <tt class="docutils literal">obj</tt> is a complex object
(no arrays in it please); the function returns a new object with the old object’s value at the path be replaced
with <tt class="docutils literal">val</tt>. This function checks to see if the path has only one element, if yes it will assign the value to that
attribute of the object it retrieve. If not, it will call itself recursively by skipping the current index and assign the return value to the
current index of the curent object. Let’s see how it works for the following call:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">s2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="nx">a0</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">b0</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">c0</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="o">:</span><span class="w"> </span><span class="mf">3</span><span class="p">}}}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">pset0</span><span class="p">(</span><span class="nx">s2</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="s1">'a'</span><span class="p">,</span><span class="w"> </span><span class="s1">'b'</span><span class="p">,</span><span class="w"> </span><span class="s1">'c'</span><span class="p">],</span><span class="w"> </span><span class="mf">4</span><span class="p">))</span>
</pre></div>
<table border="1" class="docutils">
<colgroup>
<col width="7%" />
<col width="34%" />
<col width="59%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head"># Call</th>
<th class="head">Call parameters</th>
<th class="head">Return</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>1</td>
<td>pset0(s2, [‘a’, ‘b’, ‘c’], 4)</td>
<td>{…s2, [‘b’]: pset0(s2[‘a’], [‘b’, ‘c’], 4) }</td>
</tr>
<tr><td>2</td>
<td>pset0(s2[‘a’], [‘b’, ‘c’], 4)</td>
<td>{…s2[‘a’], [‘c’]: pset0(s2[‘a’][‘b’], [‘c’], 4) }</td>
</tr>
<tr><td>3</td>
<td>pset0(s2[‘a’][‘b’], [‘c’], 4)</td>
<td>{…s2[‘a’][‘b’], [‘c’]: 4}</td>
</tr>
</tbody>
</table>
<p>Thus, the first time it will be called it will return a new object with the attributes of <tt class="docutils literal">s2</tt>
but overriding its <tt class="docutils literal">'b'</tt> index with the return of the second call. The second call will return
a new object with the attributes of <tt class="docutils literal"><span class="pre">s2['a']</span></tt> but override it’s <tt class="docutils literal">'c'</tt> index with the return
of the third call. Finally, the 3rd call will return an object with the attributes of <tt class="docutils literal"><span class="pre">s2['a']['b']</span></tt>
and setting the <tt class="docutils literal">'c'</tt> index to <tt class="docutils literal">4</tt>. The result will be as expected equal to:</p>
<div class="highlight"><pre><span></span><span class="p">{</span><span class="nx">a0</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">a</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">b0</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="nx">c0</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="o">:</span><span class="w"> </span><span class="mf">4</span><span class="w"> </span><span class="p">}}}</span>
</pre></div>
<p>Now that we’ve understood the logic we can extend the above function with the following extras:</p>
<ul class="simple">
<li>support for arrays in the object using numerical indeces</li>
<li>support for array (<tt class="docutils literal">['a', 'b']</tt>) or string path (<tt class="docutils literal">'a.b'</tt>) parameter</li>
<li>support for a direct value to set on the path or a function that will be applied on that value</li>
</ul>
<p>Here’s the resulting set lens:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">pset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">path</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">parts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="ow">typeof</span><span class="w"> </span><span class="nx">path</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'string'</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">path</span><span class="w"> </span><span class="ow">instanceof</span><span class="w"> </span><span class="nb">String</span><span class="p">)</span><span class="o">?</span><span class="nx">path</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s1">'.'</span><span class="p">)</span><span class="o">:</span><span class="nx">path</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">cset</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">cidx</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">newval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">val</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="ow">typeof</span><span class="w"> </span><span class="nx">val</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s2">"function"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">newval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">val</span><span class="p">(</span><span class="nx">obj</span><span class="p">[</span><span class="nx">cidx</span><span class="p">])</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">obj</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">...</span><span class="nx">obj</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">cidx</span><span class="o">*</span><span class="mf">1</span><span class="p">),</span>
<span class="w"> </span><span class="nx">newval</span><span class="p">,</span>
<span class="w"> </span><span class="p">...</span><span class="nx">obj</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">cidx</span><span class="o">*</span><span class="mf">1</span><span class="o">+</span><span class="mf">1</span><span class="p">)</span>
<span class="w"> </span><span class="p">]</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">...</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="p">[</span><span class="nx">cidx</span><span class="p">]</span><span class="o">:</span><span class="w"> </span><span class="nx">newval</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">pidx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">parts</span><span class="p">[</span><span class="mf">0</span><span class="p">]</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">parts</span><span class="p">.</span><span class="nx">length</span><span class="o">==</span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">cset</span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">pidx</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">remaining</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">parts</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">1</span><span class="p">)</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">cset</span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span><span class="w"> </span><span class="nx">pidx</span><span class="p">,</span><span class="w"> </span><span class="nx">pset</span><span class="p">(</span><span class="nx">obj</span><span class="p">[</span><span class="nx">pidx</span><span class="p">],</span><span class="w"> </span><span class="nx">remaining</span><span class="p">,</span><span class="w"> </span><span class="nx">val</span><span class="p">))</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>It may seem a little complex but I think it’s easy to be understood: The parts in the beginning
will just check to see if the path is an array or a string and split the string to its parts.
The <tt class="docutils literal">cset</tt> function that follows is a local function that is used to make the copy of the object
or array and set the new value. Here’s how it is working: It will first check to see if the <tt class="docutils literal">val</tt>
parameter is a function or a not. If it is a function it will apply this function to the object’s index
to get the <tt class="docutils literal">newvalue</tt> else it will just use <tt class="docutils literal">val</tt> as the <tt class="docutils literal">newvalue</tt>. After that it checks if the
object it got is an array or not. If it is an array it will do the slice trick we saw before to copy
the elements of the array except the <tt class="docutils literal">newval</tt> which will put it at the index (notice that the index
at that point must be numerical but that’s up to you to assert). If the current <tt class="docutils literal">obj</tt> is not an array
then it must be an object thus it uses the spread syntax to copy the object’s attributes and reassign
the current index to <tt class="docutils literal">newval</tt>.</p>
<p>The last part of <tt class="docutils literal">pset</tt> is similar to the <tt class="docutils literal">pset0</tt> it just uses <tt class="docutils literal">cset</tt> to do the new object/array
generation instead of doing it in place like <tt class="docutils literal">pset0</tt> - as already explained, <tt class="docutils literal">pset</tt> is called recursively
until only one element remains on the path in which case the <tt class="docutils literal">newval</tt> will be assigned to the current index of
the current <tt class="docutils literal">obj</tt>.</p>
<p>Let’s try to use <tt class="docutils literal">pset</tt> for the following rather complex state:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">state2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'users'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'results'</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Sera'</span><span class="p">,</span><span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">'g1'</span><span class="p">,</span><span class="w"> </span><span class="s1">'g2'</span><span class="p">,</span><span class="w"> </span><span class="s1">'g3'</span><span class="p">]},</span>
<span class="w"> </span><span class="p">{</span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'John'</span><span class="p">,</span><span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">'g1'</span><span class="p">,</span><span class="w"> </span><span class="s1">'g2'</span><span class="p">,</span><span class="w"> </span><span class="s1">'g3'</span><span class="p">]},</span>
<span class="w"> </span><span class="p">{</span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Joe'</span><span class="p">,</span><span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="p">[]}</span>
<span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="s1">'pagination'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'total'</span><span class="o">:</span><span class="w"> </span><span class="mf">100</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'perpage'</span><span class="o">:</span><span class="w"> </span><span class="mf">5</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'number'</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s1">'groups'</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'results'</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">]</span>
<span class="w"> </span><span class="p">,</span>
<span class="w"> </span><span class="s1">'total'</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Let’s call it three times one after the other to change various attributes:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">new_state2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pset</span><span class="p">(</span>
<span class="w"> </span><span class="nx">pset</span><span class="p">(</span>
<span class="w"> </span><span class="nx">pset</span><span class="p">(</span>
<span class="w"> </span><span class="nx">pset</span><span class="p">(</span><span class="nx">state2</span><span class="p">,</span><span class="w"> </span><span class="s2">"users.results.2.groups.0"</span><span class="p">,</span><span class="w"> </span><span class="s1">'aa'</span><span class="p">),</span>
<span class="w"> </span><span class="s2">"users.results.0.name"</span><span class="p">,</span><span class="w"> </span><span class="nx">x</span><span class="p">=></span><span class="nx">x</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()),</span>
<span class="w"> </span><span class="s2">"users.total"</span><span class="p">,</span><span class="w"> </span><span class="nx">x</span><span class="p">=></span><span class="nx">x</span><span class="o">+</span><span class="mf">1</span><span class="p">),</span>
<span class="s1">'users.results.1.name'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Jack'</span><span class="p">)</span>
</pre></div>
<p>And here’s the result:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="s2">"users"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"results"</span><span class="o">:</span><span class="w"> </span><span class="p">[{</span>
<span class="w"> </span><span class="s2">"name"</span><span class="o">:</span><span class="w"> </span><span class="s2">"SERA"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"groups"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"g1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"g2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"g3"</span><span class="p">]</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"name"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Jack"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"groups"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"g1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"g2"</span><span class="p">,</span><span class="w"> </span><span class="s2">"g3"</span><span class="p">]</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"name"</span><span class="o">:</span><span class="w"> </span><span class="s2">"Joe"</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"groups"</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"aa"</span><span class="p">]</span>
<span class="w"> </span><span class="p">}],</span>
<span class="w"> </span><span class="s2">"pagination"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"total"</span><span class="o">:</span><span class="w"> </span><span class="mf">101</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"perpage"</span><span class="o">:</span><span class="w"> </span><span class="mf">5</span><span class="p">,</span>
<span class="w"> </span><span class="s2">"number"</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="s2">"groups"</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s2">"results"</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span>
<span class="w"> </span><span class="s2">"total"</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>This should be self explanatory.</p>
<p>I’ve published the above immutable little helpers as an npm package: <a class="reference external" href="https://www.npmjs.com/package/poor-man-lens">https://www.npmjs.com/package/poor-man-lens</a> (yes I
decided to use the poor man lens name instead of the immutable little helpers) - they are too simple and could be easily
copied and pasted to your project but I’ve seen even smaller npm packages and I wanted to try to see if it is easy to
publish a package to npm (answer: it is very easy - easier than python’s pip). Also there’s a github repository for
these utils in case somebody wants to contribute anything or look at the source: <a class="reference external" href="https://github.com/spapas/poor-man-lens">https://github.com/spapas/poor-man-lens</a>.</p>
<p>Notice that this package has been written in <span class="caps">ES5</span> (and actually has a polyfil for Object.assign) thus you should probably
be able to use it anywhere you want, even directly from the browser by directly including the <tt class="docutils literal">pml.js</tt> file.</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Using the above techniques you should be able to easily keep your state objects immutable. For simple cases you
can stick to the spread syntax or Object.assign / Array.slice but for more complex cases you may want to consider
either copying directly the pset and pget utils I explained above or just using the <a class="reference external" href="https://www.npmjs.com/package/poor-man-lens">poor-man-lens npm package</a>.</p>
</div>
A comprehensive Django CBV guide2018-03-19T12:20:00+02:002018-03-19T12:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2018-03-19:/2018/03/19/comprehensive-django-cbv-guide/<p class="first last">A comprehensive guide to Django CBVs - from neophyte to more advanced</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#a-gentle-introduction-to-cbvs" id="toc-entry-1">A gentle introduction to CBVs</a><ul>
<li><a class="reference internal" href="#hand-made-cbvs" id="toc-entry-2">Hand-made CBVs</a></li>
<li><a class="reference internal" href="#is-this-really-dry" id="toc-entry-3">Is this really <span class="caps">DRY</span> ?</a></li>
<li><a class="reference internal" href="#re-using-view-functionality" id="toc-entry-4">Re-using view functionality</a></li>
<li><a class="reference internal" href="#interlude-an-mro-primer" id="toc-entry-5">Interlude: An <span class="caps">MRO</span> primer</a></li>
<li><a class="reference internal" href="#using-mixins-for-code-reuse" id="toc-entry-6">Using mixins for code-reuse</a></li>
<li><a class="reference internal" href="#the-super-situation" id="toc-entry-7">The super situation</a></li>
<li><a class="reference internal" href="#using-super-in-our-hierarchy" id="toc-entry-8">Using super in our hierarchy</a></li>
<li><a class="reference internal" href="#testing-all-this" id="toc-entry-9">Testing all this</a></li>
</ul>
</li>
<li><a class="reference internal" href="#a-high-level-overview-of-cbvs" id="toc-entry-10">A high level overview of CBVs</a><ul>
<li><a class="reference internal" href="#taking-a-look-at-the-view" id="toc-entry-11">Taking a look at the View</a></li>
<li><a class="reference internal" href="#redirectview-and-templateview" id="toc-entry-12">RedirectView and TemplateView</a></li>
<li><a class="reference internal" href="#the-formview" id="toc-entry-13">The FormView</a></li>
<li><a class="reference internal" href="#the-listview-and-detailview" id="toc-entry-14">The ListView and DetailView</a></li>
<li><a class="reference internal" href="#the-createview" id="toc-entry-15">The CreateView</a></li>
<li><a class="reference internal" href="#the-updateview-and-deleteview" id="toc-entry-16">The UpdateView and DeleteView</a></li>
<li><a class="reference internal" href="#access-control-mixins" id="toc-entry-17">Access control mixins</a></li>
<li><a class="reference internal" href="#some-other-cbvs" id="toc-entry-18">Some other CBVs</a></li>
</ul>
</li>
<li><a class="reference internal" href="#real-world-use-cases" id="toc-entry-19">Real world use cases</a><ul>
<li><a class="reference internal" href="#do-something-when-a-valid-form-is-submitted" id="toc-entry-20">Do something when a valid form is submitted</a></li>
<li><a class="reference internal" href="#change-the-queryset-of-the-cbv" id="toc-entry-21">Change the queryset of the <span class="caps">CBV</span></a></li>
<li><a class="reference internal" href="#allow-each-user-to-list-view-edit-delete-only-his-own-items" id="toc-entry-22">Allow each user to list/view/edit/delete only his own items</a></li>
<li><a class="reference internal" href="#configure-the-form-s-initial-values-from-get-parameters" id="toc-entry-23">Configure the form’s initial values from <span class="caps">GET</span> parameters</a></li>
<li><a class="reference internal" href="#pass-extra-kwargs-to-the-formview-form" id="toc-entry-24">Pass extra kwargs to the FormView form</a></li>
<li><a class="reference internal" href="#add-values-to-the-context" id="toc-entry-25">Add values to the context</a></li>
<li><a class="reference internal" href="#add-a-simple-filter-to-a-listview" id="toc-entry-26">Add a simple filter to a ListView</a></li>
<li><a class="reference internal" href="#support-for-success-messages" id="toc-entry-27">Support for success messages</a></li>
<li><a class="reference internal" href="#implement-a-quick-moderation" id="toc-entry-28">Implement a quick moderation</a></li>
<li><a class="reference internal" href="#allow-access-to-a-view-if-a-user-has-one-out-of-a-group-of-permissions" id="toc-entry-29">Allow access to a view if a user has one out of a group of permissions</a></li>
<li><a class="reference internal" href="#disable-a-view-based-on-some-condition" id="toc-entry-30">Disable a view based on some condition</a></li>
<li><a class="reference internal" href="#output-non-html-views" id="toc-entry-31">Output non-html views</a></li>
<li><a class="reference internal" href="#use-one-templateview-for-multiple-html-templates" id="toc-entry-32">Use one TemplateView for multiple html templates</a></li>
<li><a class="reference internal" href="#implement-a-partial-ajax-view" id="toc-entry-33">Implement a partial Ajax view</a></li>
<li><a class="reference internal" href="#add-a-dynamic-filter-and-or-table-to-the-context" id="toc-entry-34">Add a dynamic filter and/or table to the context</a></li>
<li><a class="reference internal" href="#configure-forms-for-your-views" id="toc-entry-35">Configure forms for your views</a></li>
<li><a class="reference internal" href="#display-a-different-form-for-create-and-update" id="toc-entry-36">Display a different form for Create and Update</a></li>
<li><a class="reference internal" href="#only-allow-specific-http-methods-for-a-view" id="toc-entry-37">Only allow specific <span class="caps">HTTP</span> methods for a view</a></li>
<li><a class="reference internal" href="#create-an-umbrella-view-for-multiple-models" id="toc-entry-38">Create an umbrella View for multiple models</a></li>
</ul>
</li>
<li><a class="reference internal" href="#a-heavy-cbv-user-project" id="toc-entry-39">A heavy <span class="caps">CBV</span> user project</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-40">Conclusion</a></li>
</ul>
</div>
<p>Class Based Views (<span class="caps">CBV</span>) is one of my favourite things about Django. During my
first Django projects (using Django 1.4 around 6 years ago) I was mainly using
functional views — that’s what the tutorial recommended then anyway. However,
slowly in my next projects I started reducing the amount of functional views
and embracing CBVs, slowly understanding their usage and usefulness. Right now,
I more or less only use CBVs for my views; even if sometimes it seems more work
to use a <span class="caps">CBV</span> instead of a functional one I know that sometime in the future I’d
be glad that I did it since I’ll want to re-use some view functionality and
CBVs are more or less the only way to have <span class="caps">DRY</span> views in Django.</p>
<p>I’ve heard various rants about them, mainly that they are too complex and difficult to
understand and use, however I believe that they are not really difficult when
you start from the basics. Even if it is a little work to become comfortable with
the <span class="caps">CBV</span> logic, when they are used properly they will greatly improve your Django experience
so it is definitely worth it.</p>
<p>Notice
that to properly understand CBVs you must have a good understanding of how
Python’s (multiple) inheritance and <span class="caps">MRO</span> work. Yes, this is a rather complex and
confusing thing but I’ll try to also explain this as good as I can to the first chapter
of this article so if you follow along you shouldn’t have any problems.</p>
<p>This guide has four parts:</p>
<ul class="simple">
<li>A gentle introduction to how CBVs are working and to the the problems that do solve. For this we’ll implement
our own simple Custom Class Based View variant and take a look at python’s inheritance model.</li>
<li>A high level overview of the real Django CBVs using <a class="reference external" href="http://ccbv.co.uk`"><span class="caps">CBV</span> inspector</a> as our guide.</li>
<li>A number of use cases where CBVs can be used to elegantly solve real world problems</li>
<li>Describing the usage of some of the previous use cases to a real Django application</li>
</ul>
<p>I’ve implemented an accompanying project to this article which you can find at <a class="reference external" href="https://github.com/spapas/cbv-tutorial">https://github.com/spapas/cbv-tutorial</a>.
This project has two separate parts. One that is the implementation of the Custom Class Based View variant
to see how it is working and the other is the application that contains the usage of the various <span class="caps">CBV</span> use cases.</p>
<div class="section" id="a-gentle-introduction-to-cbvs">
<h2>A gentle introduction to CBVs</h2>
<p>In this part of the guide we’ll do a gentle introduction to how CBVs work by implementing
our own class based views variant - along with it we’ll introduce and try to understand
some concepts of python (multiple) inheritance and how it applies to CBVs.</p>
<p>Before continuing, let’s talk about the concept of the “view” in Django:
Django is considered an <span class="caps">MVT</span> (Model View Template) framework - the View as
conceived by Django is not the same as the <span class="caps">MVC</span>-View. A Django View is more or
less a way to define the data that the Template (which is cloased to the <span class="caps">MVC</span>-View)
will display, so the Django View (with the help of the Django Framework) is similar
to the <span class="caps">MVC</span>-Controller.</p>
<p>In any case, traditionally a view in Django is a normal python function that takes a single parameter,
the <a class="reference external" href="https://docs.djangoproject.com/en/1.11/ref/request-response/#django.http.HttpRequest">request</a> object and must return a <a class="reference external" href="https://docs.djangoproject.com/en/1.11/ref/request-response/#django.http.HttpResponse">response</a> object (notice that if the
view uses request parameters for example the id of an object to be edited
they will also be passed to the function). The responsibility of the
view function is to properly parse the request parameters and construct the
response object - as can be understood there is a lot of work that need to be
done for each view (for example check if the method is <span class="caps">GET</span> or <span class="caps">POST</span>, if the user
has access to that page, retrieve objects from the database, crate a context dict
and pass it to the template to be rendered etc).</p>
<p>Now, since functional views are simple python functions it is <em>not</em> easy to override,
reuse or extend their behaviour. There are more or less two methods for this: Use function
decorators or pass extra parameters when adding the view to your urls. I’d like
to point out here that there’s a third method for code-reuse: Extracting
functionality to common and re-usable functions or classes that will be called from the
functional views but this is not something specific to Django views but a general
concept of good programming style which you should follow anyway.</p>
<p>The first one uses <a class="reference external" href="https://wiki.python.org/moin/PythonDecorators">python decorators</a> to create a functional view that wraps the
initial one. The new view is called before the initial one, adds some functionality
(for example check if the current user has access, modify request parameters etc),
calls the initial one which will return a response object, modify the response if needed
and then return that. This is how <a class="reference external" href="https://docs.djangoproject.com/en/2.0/topics/auth/default/#the-login-required-decorator">login_required</a> works. Notice that by using
decorators you can change things before and after the original view runs but
you can’t do anything about the way the original view works.</p>
<p>For the second one (adding extra view parameters) you must
write your function view in a way which allows it to be reused, for example instead
of hard-coding the template name allow it to be passed as a parameter or instead
of using a specific form class for a form make it configurable through a parameter. Then,
when you add this function to your urls you will pass different parameters
depending on how you want to configure your view. Using this method you can
override the original function behaviour however there’s a limit to the number of
parameters you can allow your function views to have and notice that these
function views cannot be further overridden. The <a class="reference external" href="https://docs.djangoproject.com/en/2.0/topics/auth/default/#django.contrib.auth.views.login">login</a> authentication view (which
is now deprecated in favour of a <span class="caps">CBV</span> one)
is using this technique, for example you can pass it
the template name that will be used, a custom authentication form etc.</p>
<p>It should be obvious that both these methods have severe limitations and do not allow you to be as <span class="caps">DRY</span> as
you should be. When using the wrapped views you can’t actually
change the functionality of the original view (since that original function needs
to be called) but only do things before and after calling it. Also, using the
parameters will lead to spaghetti code with multiple if / else conditions in order
to take into account the various cases that may arise. All the above lead to
very reduced re-usability and DRYness of functional views - usually the best thing
you can do is to gather the common things in external normal python functions (not view functions) that could be
re-used from other functional views as already discussed.</p>
<p>Class based views solve the above problem of non-<span class="caps">DRY</span>-ness by using the well known
concept of <span class="caps">OO</span> inheritance: The view is defined from a class which has methods
for implementing the view functionality - you inherit from that class and override
the parts you want so the inherited class based view will use the overridden methods instead
of the original ones. You can also create re-usable classes (mixins) that offer a specific
functionality to your class based view by implementing some of the methods of the
original class. Each one of your class based views can inherit its functionality from
multiple mixins thus allowing you to define a single class for each thing you need
and re-using it everywhere. Notice of course that this is possible only if the
CBVs are <em>properly implemented</em> to allow overriding their functionality. We’ll see
how this is possible in the next section.</p>
<div class="section" id="hand-made-cbvs">
<h3>Hand-made CBVs</h3>
<p>To make things more clear we’ll start implementing our own class based views hierarchy. Here’s
a rather naive first try:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">CustomClassView</span><span class="p">:</span>
<span class="n">context</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">header</span> <span class="o">=</span> <span class="s1">''</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span> <span class="o">=</span> <span class="n">kwargs</span>
<span class="k">for</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span><span class="n">v</span><span class="p">)</span> <span class="ow">in</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="nb">setattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">"""</span>
<span class="s2"> <html></span>
<span class="s2"> <body></span>
<span class="s2"> <h1></span><span class="si">{header}</span><span class="s2"></h1></span>
<span class="s2"> </span><span class="si">{body}</span>
<span class="s2"> </body></span>
<span class="s2"> </html></span>
<span class="s2"> """</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">header</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">header</span><span class="p">,</span> <span class="n">body</span><span class="o">=</span><span class="s1">'<br />'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">context</span><span class="p">),</span>
<span class="p">)</span>
<span class="nd">@classmethod</span>
<span class="k">def</span> <span class="nf">as_view</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">view</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="p">):</span>
<span class="n">instance</span> <span class="o">=</span> <span class="bp">cls</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">instance</span><span class="o">.</span><span class="n">render</span><span class="p">())</span>
<span class="k">return</span> <span class="n">view</span>
</pre></div>
<p>This class can be used to render a simple <span class="caps">HTML</span> template with a custom header and
a list of items in the body (named <tt class="docutils literal">context</tt>). There are two things to notice here: The <tt class="docutils literal">__init__</tt> method (which
will be called as the object’s constructor) will assign all the keyword arguments (<tt class="docutils literal">kwargs</tt>) it receives
as instance attributes (for example <tt class="docutils literal"><span class="pre">CustomClassView(header='hello')</span></tt> will create
an instance with <tt class="docutils literal">'hello'</tt> as its <tt class="docutils literal">header</tt> attribute). The <tt class="docutils literal">as_view</tt> is a class method
(i.e it can be called directly on the <em>class</em> without the need to instantiate an object
for example you can call <tt class="docutils literal">CustomClassView.as_view()</tt> ) that
defines and returns a traditional functional view (named <tt class="docutils literal">view</tt>) that will be used to
actually serve the view. The returned
functional view is very simple - it just instantiates a new instance (object)
of <tt class="docutils literal">CustomClassView</tt> passing
the <tt class="docutils literal">kwargs</tt> it got in the constructor and then returns a normal <tt class="docutils literal">HttpResponse</tt> with
the instance’s <tt class="docutils literal">render()</tt> result. This <tt class="docutils literal">render()</tt> method will just output some html
using the instance’s header and context to fill it.</p>
<p>Notice that the instance of the <tt class="docutils literal">CustomClassView</tt> inside the <tt class="docutils literal">as_view</tt> class method
is not created using
<tt class="docutils literal"><span class="pre">CustomClassView(**kwargs)</span></tt> but using <tt class="docutils literal"><span class="pre">cls(**kwargs)</span></tt> - <tt class="docutils literal">cls</tt> is the name of the
class that <tt class="docutils literal">as_view</tt> was called on and is actually passed as a parameter for
class methods (in a similar manner to how <tt class="docutils literal">self</tt> is passed to instance methods).
This is important to instantiate an object instance of the proper class.</p>
<p>For example, if you created a class that inherited from <tt class="docutils literal">CustomClassView</tt>
and called its <tt class="docutils literal">as_view</tt> method then when you use the <tt class="docutils literal">cls</tt> parameter to instantiate
the object it will correctly create an object of the <em>inherited</em> class and not the <em>base</em> one
(if on the other hand you had used <tt class="docutils literal"><span class="pre">CustomClassView(**kwargs)</span></tt> to instantiate the instance
then the <tt class="docutils literal">as_view</tt> method of the inheriting classes would instantiate instances of
<tt class="docutils literal">CustomClassView</tt> so inheritance wouldn’t really work!).</p>
<p>To add the above class method in your urls, just use its <tt class="docutils literal">as_view()</tt> as you’d
normally use a functional view:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">include</span><span class="p">,</span> <span class="n">url</span>
<span class="kn">from</span> <span class="nn">.</span> <span class="kn">import</span> <span class="n">views</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ccv-empty/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">CustomClassView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ccv-empty'</span><span class="p">),</span>
<span class="c1"># ... other urls</span>
<span class="p">]</span>
</pre></div>
<p>This doesn’t actually render anything since both header and context are empty on
the created instance — remember that <tt class="docutils literal">as_view</tt> returns a functional view that
instantiates a <tt class="docutils literal">CustomClassView</tt> object and returns an <tt class="docutils literal">HttpResponse</tt> filling it
with the object’s <tt class="docutils literal">render()</tt> results. To add some output we can either
create another class that inherits from <tt class="docutils literal">CustomClassView</tt> or
initialize the attributes from the constructor of the class (using the kwargs functionality described above).</p>
<p>The inherited class can just override the values of the attributes:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">InheritsCustomClassView</span><span class="p">(</span><span class="n">CustomClassView</span><span class="p">,</span> <span class="p">):</span>
<span class="n">header</span> <span class="o">=</span> <span class="s2">"Hi"</span>
<span class="n">context</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'test'</span><span class="p">,</span> <span class="s1">'test2'</span> <span class="p">]</span>
</pre></div>
<p>And then just add the inherited class to your urls as before:</p>
<div class="highlight"><pre><span></span><span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ccv-inherits/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">InheritsCustomClassView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ccv-inherits'</span><span class="p">),</span>
</pre></div>
<p>The <tt class="docutils literal">as_view()</tt> method will create an instance of <tt class="docutils literal">InheritsCustomClassView</tt> that has
the values configured in the class as attributes and return
its <tt class="docutils literal">render()</tt> output as response.</p>
<p>The other way to configure the attributes of the class is to
pass them to the <tt class="docutils literal">as_view</tt> class method (which in turn will pass them to the instances
constructor which will set the attributes in the instance). Here’s an example:</p>
<div class="highlight"><pre><span></span><span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^ccv-with-values/$'</span><span class="p">,</span> <span class="n">views</span><span class="o">.</span><span class="n">CustomClassView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(</span><span class="n">header</span><span class="o">=</span><span class="s1">'Hello'</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="p">[</span><span class="s1">'hello'</span><span class="p">,</span> <span class="s1">'world'</span><span class="p">,</span> <span class="p">],</span> <span class="n">footer</span><span class="o">=</span><span class="s1">'Bye'</span><span class="p">,</span> <span class="p">),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'ccv-with-values'</span><span class="p">),</span>
</pre></div>
<p>The above will create a <tt class="docutils literal">CustomClassView</tt> instance with the provided values as its attributes.</p>
<p>Although this method of configuration is used in normal django CBVs (for example
setting the <tt class="docutils literal">template_name</tt> in a <tt class="docutils literal">TemplateView</tt>) I recommend you avoid using it because passing parameters
to the <tt class="docutils literal">as_view</tt> method pollutes the urls.py with configuration
that (at least in my opinion) should <em>not</em> be there (and there’s no reason to have to take a look at both
your urls.py and your views.py to understand the behavior of your views) and also, even for very simple views I know that after some time I’ll need
to add some functionality that cannot be implemented by passing the parameters so I prefer to bite the
bullet and define all my views as inherited classes so it will be easy for me to further customize them later (we’ll
see how this is done in a second). Thus, even if you have</p>
<p>In any case, I won’t discuss passing parameters to the <tt class="docutils literal">as_view</tt> method any more,
so from now on any class based views I define will be added to urls py using <tt class="docutils literal">ClassName.as_view()</tt> without any
parameters to the <tt class="docutils literal">as_view()</tt> class method.</p>
</div>
<div class="section" id="is-this-really-dry">
<h3>Is this really <span class="caps">DRY</span> ?</h3>
<p>Let’s now suppose that we wanted to allow our class based view to print something on the header even if no header is provided
when you configure it. The only way to do it would be to re-define the <tt class="docutils literal">render</tt> method like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">header</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">else</span> <span class="s2">"DEFAULT HEADER"</span>
<span class="k">return</span> <span class="s2">"""</span>
<span class="s2"> <html></span>
<span class="s2"> <body></span>
<span class="s2"> <h1></span><span class="si">{header}</span><span class="s2"></h1></span>
<span class="s2"> </span><span class="si">{body}</span>
<span class="s2"> </body></span>
<span class="s2"> </html></span>
<span class="s2"> """</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">header</span><span class="o">=</span><span class="n">header</span><span class="p">,</span> <span class="n">body</span><span class="o">=</span><span class="s1">'<br />'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">context</span><span class="p">),</span>
<span class="p">)</span>
</pre></div>
<p>This is definitely not the <span class="caps">DRY</span> way to do it because you would need to re-define the whole <tt class="docutils literal">render</tt> method. Think
what would happen if
you wanted to print <tt class="docutils literal">"<span class="caps">ANOTHER</span> <span class="caps">DEFAULT</span> <span class="caps">HEADER</span>"</tt> as a default header for some other view - once again re-defining
<tt class="docutils literal">render</tt>! In fact, the above
<tt class="docutils literal">CustomClassView</tt> is naively implemented because it does not allow proper customization through inheritance. The
same problems for the header arise also when you need modify the body; for
example, if you wanted to add an index number before displaying the items of the list then you’d need to again re-implement the
whole <tt class="docutils literal">render</tt> method.</p>
<p>If that was our only option then we could just stick to functional views. However, we can do
much better if we define the class based view in such a way that allows inherited classes to override methods that
define specific parts of the functionality. To do this the class-based-view must be properly implemented so each
part of its functionality is implemented by a different method.</p>
<p>Here’s how we could improve the <tt class="docutils literal">CustomClassView</tt> to make it more <span class="caps">DRY</span>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">BetterCustomClassView</span><span class="p">(</span><span class="n">CustomClassView</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"Better Custom Class View"</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">else</span> <span class="s2">""</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span> <span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">else</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">render_context</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_context</span><span class="p">()</span>
<span class="k">if</span> <span class="n">context</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">'<br />'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
<span class="k">return</span> <span class="s2">""</span>
<span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">"""</span>
<span class="s2"> <html></span>
<span class="s2"> <body></span>
<span class="s2"> <h1></span><span class="si">{header}</span><span class="s2"></h1></span>
<span class="s2"> </span><span class="si">{body}</span>
<span class="s2"> </body></span>
<span class="s2"> </html></span>
<span class="s2"> """</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">header</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_header</span><span class="p">(),</span> <span class="n">body</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">render_context</span><span class="p">(),</span>
<span class="p">)</span>
</pre></div>
<p>So what happens here? First of all we inherit from <tt class="docutils literal">CustomClassView</tt> to keep the
<tt class="docutils literal">as_view</tt> method which doesn’t need changing. Beyond this, the render
uses methods (<tt class="docutils literal">get_header</tt> and <tt class="docutils literal">render_context</tt>) to retrieve the values from the header and the body - this means
that we could re-define these methods to an inherited class in order to override
what these methods will return. Beyond <tt class="docutils literal">get_header</tt> and <tt class="docutils literal">render_contex</tt> I’ve added
a <tt class="docutils literal">get_context</tt> method that is used by <tt class="docutils literal">render_context</tt> to make this <span class="caps">CBV</span> even
more re-usable. For example I may
need to configure the context (add/remove items from the context i.e have a <span class="caps">CBV</span>
that adds a last item with the number of list items to the list to be displayed). Of course this could
be done from <tt class="docutils literal">render_context</tt> <em>but</em> this means that I would need to define my new functionality
(modifying the context items) <em>and</em> re-defining the context list formatting. It is much
better (in my opinion always) to keep properly separated these things.</p>
<p>Now, the above is a first try that I created to mainly fulfil my requirement of
having a default header and some more examples I will discuss later (and keep
everything simple enough). You could
extract more functionality as methods-for-overriding, for example the render
method could be written like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">render</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template</span><span class="p">()</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">header</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_header</span><span class="p">(),</span> <span class="n">body</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">render_context</span><span class="p">(),</span>
<span class="p">)</span>
</pre></div>
<p>and add a <tt class="docutils literal">get_template</tt> method that will return the actual html template. There’s no
hard rules here on what functionality should be extracted to a method (so it could
be overridden) however I recommend to follow the <span class="caps">YAGNI</span> rule (i.e implement everything
as normal and when you see that some functionality needs to be overridden then refactor
your code to extract it to a separate method).</p>
<p>Let’s see an example of adding the default header functionality by overriding <tt class="docutils literal">get_header</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultHeaderBetterCustomClassView</span><span class="p">(</span><span class="n">BetterCustomClassView</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">else</span> <span class="s2">"DEFAULT HEADER"</span>
</pre></div>
<p>Classes inheriting from <tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> can choose to not
actually define a header attribute so <tt class="docutils literal">"<span class="caps">DEFAULT</span> <span class="caps">HEADER</span>"</tt> will be printed instead. Keep in
mind that for <tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> to be actually useful you’ll need to
have more than one classes that need this default-header functionality (or else you could
just set the header attribute of your class to <tt class="docutils literal">"<span class="caps">DEFAULT</span> <span class="caps">HEADER</span>"</tt> - this is not
user generated input, this is your source code!).</p>
</div>
<div class="section" id="re-using-view-functionality">
<h3>Re-using view functionality</h3>
<p>We have come now to a crucial point in this chapter, so please stick with me. Let’s say that you have
<em>more than one</em> class based views that contain a header attribute. You want to include
the default header functionality on all of them so that if any view instantiated from these
class based views doesn’t define a header
the default string will be output (I know that this may be a rather trivial example but I want
to keep everything simple to make following easy - instead of the default header the functionality
you want to override may be adding stuff to the context or filtering the objects you’ll retrieve
from the database).</p>
<p>To re-use this default header functionality from multiple classes you have <em>two</em> options:
Either inherit all classes that need this functionality from <tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> or
extract the custom <tt class="docutils literal">get_header</tt> method to a <em>mixin</em> and inherit from the mixin. A mixin is a class not
related to the class based view hierarchy we are using - the mixin inherits from object (or from another
mixin) and just defines the methods and attributes that need to be overridden. When the mixin is <em>mixed</em>
with the ancestors of a class its functionality will be used by that class (we’ll see how shortly). So
the mixin will only define <tt class="docutils literal">get_header</tt> and not all other methods like
<tt class="docutils literal">render</tt>, <tt class="docutils literal">get_context</tt> etc. Using the
<tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> is enough for some cases but for the general case
of re-using the functionality you’ll need to create the mixin. Let’s see why:</p>
<p>Suppose that you have a base class that renders the header and context as <span class="caps">JSON</span> instead of the <span class="caps">HTML</span>
template, something like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">JsonCustomClassView</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">else</span> <span class="s2">""</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">else</span> <span class="p">[]</span>
<span class="nd">@classmethod</span>
<span class="k">def</span> <span class="nf">as_view</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">view</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="p">):</span>
<span class="n">instance</span> <span class="o">=</span> <span class="bp">cls</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span>
<span class="s1">'header'</span><span class="p">:</span> <span class="n">instance</span><span class="o">.</span><span class="n">get_header</span><span class="p">(),</span>
<span class="s1">'context'</span><span class="p">:</span> <span class="n">instance</span><span class="o">.</span><span class="n">get_context</span><span class="p">(),</span>
<span class="p">}))</span>
<span class="k">return</span> <span class="n">view</span>
</pre></div>
<p>Notice that this class does not inherit from our previous hierarchy (i.e does not
inherit from BetterCustomClassView) but from object since it provides
its own <tt class="docutils literal">as_view</tt> method. How could we re-use default header functionality
in this class (without having to re-implement it)? One solution would be to create a class that
inherits from both <tt class="docutils literal">JsonCustomClassView</tt> and <tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> using something like</p>
<div class="highlight"><pre><span></span><span class="c1"># OPTION 1</span>
<span class="k">class</span> <span class="nc">DefaultHeaderJsonCustomClassView</span><span class="p">(</span><span class="n">DefaultHeaderBetterCustomClassView</span><span class="p">,</span> <span class="n">JsonCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
<span class="c1"># OR</span>
<span class="c1"># OPTION 2</span>
<span class="k">class</span> <span class="nc">JsonDefaultHeaderCustomClassView</span><span class="p">(</span><span class="n">JsonCustomClassView</span><span class="p">,</span> <span class="n">DefaultHeaderBetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>What will happen here? Notice that the methods <tt class="docutils literal">get_header</tt> and <tt class="docutils literal">as_view</tt> exist in <em>both</em> ancestor classes! So
which one will be used in each case? Actually, there’s a (rather complex) rule for that called
<span class="caps">MRO</span> (Method Resolution Order). The <span class="caps">MRO</span> is also what can be used to know which <tt class="docutils literal">get_header</tt>
and <tt class="docutils literal">as_view</tt> will be used in each case in the previous example.</p>
</div>
<div class="section" id="interlude-an-mro-primer">
<h3>Interlude: An <span class="caps">MRO</span> primer</h3>
<p>What is <span class="caps">MRO</span>? For every class that Python sees, it tries to create a <em>list</em> (<span class="caps">MRO</span> list) of ancestor classes containing that class as
the first element and its ancestors in a specific order I’ll discuss in the next paragraph. When a method
of an object of that specific class needs to be
called, then the method will be searched in the <span class="caps">MRO</span> list (from the first element of the <span class="caps">MRO</span> list i.e. starting with the class itself) - when a class is found
in the list that defines the method then that method instance (i.e. the method defined in this class) will be called and the search will stop (careful readers: I haven’t
yet talked about <em>super</em> so please be patient).</p>
<p>Now, how is the <span class="caps">MRO</span> list created? As I explained, the first element
is the class itself. The second element is the <span class="caps">MRO</span> of the <em>leftmost</em> ancestor of that object (so <span class="caps">MRO</span> will
run recursively on each ancestor), the third element will be the <span class="caps">MRO</span> of the ancestor right next to the leftmost
ancestor etc. There is one extra and important rule: When a class is found multiple times in the <span class="caps">MRO</span> list (for example
if some elements have a common ancestor) then <em>only the last occurrence in the list will be kept</em> - so each class
will exist only once in the <span class="caps">MRO</span> list. The above rule implies that the
rightmost element in every <span class="caps">MRO</span> list will always be object - please make sure you
understand why before continuing.</p>
<p>Thus, the <span class="caps">MRO</span> list for <tt class="docutils literal">DefaultHeaderJsonCustomClassView</tt> defined in the previous section
is (remember, start
with the class to the left and add the <span class="caps">MRO</span> of each of its ancestors starting
from the leftmost one):
<tt class="docutils literal">[DefaultHeaderJsonCustomClassView, DefaultHeaderBetterCustomClassView, BetterCustomClassView, CustomClassView, JsonCustomClassView, object]</tt>, while
for <tt class="docutils literal">JsonDefaultHeaderCustomClassView</tt> is
<tt class="docutils literal">[JsonDefaultHeaderCustomClassView, JsonCustomClassView, DefaultHeaderBetterCustomClassView, BetterCustomClassView, CustomClassView, object]</tt>. What this
means is that for <tt class="docutils literal">DefaultHeaderJsonCustomClassView</tt> the <tt class="docutils literal">CustomClassView.as_view()</tt> and <tt class="docutils literal">DefaultHeaderBetterCustomClassView.get_header()</tt> will be used (thus
we will not get the <span class="caps">JSON</span> output) and for <tt class="docutils literal">JsonDefaultHeaderCustomClassView</tt> the <tt class="docutils literal">JsonCustomClassView.as_view()</tt> and <tt class="docutils literal">JsonCustomClassView.get_header()</tt>
will be used (so we won’t get the default header functionality) - i.e none of those two options will result to the desired behaviour.</p>
<p>Let’s try an example that has the same base class twice in the hierarchy (actually the previous examples also had a class twice in
the hierarchy - <tt class="docutils literal">object</tt> but let’s be more explicit). For this, we’ll create a
<tt class="docutils literal">DefaultContextBetterCustomClassView</tt> that returns a default context if the context is empty
(similar to the default header functionality).</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultContextBetterCustomClassView</span><span class="p">(</span><span class="n">BetterCustomClassView</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">else</span> <span class="p">[</span><span class="s2">"DEFAULT CONTEXT"</span><span class="p">]</span>
</pre></div>
<p>Now we’ll create a class that inherits from both <tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> and <tt class="docutils literal">DefaultContextBetterCustomClassView</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultHeaderContextCustomClassView</span><span class="p">(</span><span class="n">DefaultHeaderBetterCustomClassView</span><span class="p">,</span> <span class="n">DefaultContextBetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>Let’s do the <span class="caps">MRO</span> for the <tt class="docutils literal">DefaultHeaderContextCustomClassView</tt> class:</p>
<p>Initially, the <span class="caps">MRO</span> will be the following:</p>
<pre class="code literal-block">
Starting with the initial class
1. DefaultHeaderContextCustomClassView
Follows the leftmost class (DefaultHeaderBetterCustomClassView) MRO
2. DefaultHeaderBetterCustomClassView, 3. BetterCustomClassView, 4. CustomClassView, 5. object
And finally the next class (DefaultContextBetterCustomClassView) MRO
6. DefaultContextBetterCustomClassView, 7. BetterCustomClassView, 8. CustomClassView, 9. object
</pre>
<p>Notice that classes <tt class="docutils literal">BetterCustomClassView</tt>, <tt class="docutils literal">CustomClassView</tt> and <tt class="docutils literal">object</tt> are repeated two times
(on place 3,4,5 and 7,8,9) thus <em>only</em> their last (rightmost) occurrences will be kept in the list. So the
resulting <span class="caps">MRO</span> is the following (3,4,5 are removed):</p>
<p><tt class="docutils literal">[DefaultHeaderContextCustomClassView, DefaultHeaderBetterCustomClassView, DefaultContextBetterCustomClassView, BetterCustomClassView, CustomClassView, object]</tt></p>
<p>One funny thing here is that the <tt class="docutils literal">DefaultHeaderContextCustomClassView</tt> <em>will actually work</em> properly because the
<tt class="docutils literal">get_header</tt> will be found in <tt class="docutils literal">DefaultHeaderBetterCustomClassView</tt> and the
<tt class="docutils literal">get_context</tt> will be found in <tt class="docutils literal">DefaultContextBetterCustomClassView</tt> so this
result to the correct functionality.</p>
<p>Yes it does work but at what cost? Do you really want to do the mental exercise
of finding out the <span class="caps">MRO</span> for each class you define to see which method will be actually used? Also, what would happen if the
<tt class="docutils literal">DefaultHeaderContextCustomClassView</tt> class also had a <tt class="docutils literal">get_context</tt> method defined
(hint: that <tt class="docutils literal">get_context</tt> would be used and the <tt class="docutils literal">get_context</tt> of <tt class="docutils literal">DefaultContextBetterCustomClassView</tt>
would be ignored).</p>
<p>Before finishing this interlude, I’d like to make a confession: The Python <span class="caps">MRO</span> algorithm is not as simple as
than the procedure I described. It uses an algorithm called <a class="reference external" href="https://en.wikipedia.org/wiki/C3_linearization">C3 linearization</a> which seems way too complex
to start explaining or understanding if you not a <span class="caps">CS</span> student. What you’ll need to remember is that the
procedure I described works fine in normal cases when you don’t try to do something stupid. Here’s a
<a class="reference external" href="https://medium.com/technology-nineleaps/python-method-resolution-order-4fd41d2fcc">post that explains the theory more</a>. However if you follow along my recommendations below you won’t
have any problems with <span class="caps">MRO</span>, actually you won’t really need to use the <span class="caps">MRO</span> that much to understand
the method calling hierarchy.</p>
</div>
<div class="section" id="using-mixins-for-code-reuse">
<h3>Using mixins for code-reuse</h3>
<p>The above explanation of <span class="caps">MRO</span> should convince you that you should avoid
mixing hierarchies of classes - if you are not convinced then wait until I introduce <tt class="docutils literal">super()</tt>
in the next section and I guarantee that you’ll be!</p>
<p>So, that’s why I
propose implementing common functionality that needs to be re-used between
classes only with mixins (hint: that’s also what Django does). Each re-usable functionality
will be implemented in its own mixin; class views that need to implement that
functionality will just inherit from the mixin along with the base class view. Each
one of the view classes you define should inherit from <em>one and only one</em> other class
view and any number of mixins you want. Make sure that the view class is rightmost in
the ancestors list and the mixins are to the left of it (so that they will properly override
its behaviour; remember that the methods of the ancestors to the left are searched first
in the <span class="caps">MRO</span> list — and the methods of the defined class have of course the highest priority
since it goes first in the <span class="caps">MRO</span> list).</p>
<p>Let’s try implementing the proposed mixins for a default header and context:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultHeaderMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">else</span> <span class="s2">"DEFAULT HEADER"</span>
<span class="k">class</span> <span class="nc">DefaultContextMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">else</span> <span class="p">[</span><span class="s2">"DEFAULT CONTEXT"</span><span class="p">]</span>
</pre></div>
<p>and all the proposed use cases using the base class view and the mixins:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultHeaderMixinBetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">DefaultHeaderMixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">DefaultContextMixinBetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">DefaultContextMixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">DefaultHeaderContextMixinBetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">DefaultHeaderMixin</span><span class="p">,</span> <span class="n">mixins</span><span class="o">.</span><span class="n">DefaultContextMixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">JsonDefaultHeaderMixinCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">DefaultHeaderMixin</span><span class="p">,</span> <span class="n">JsonCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>I believe that the above definitions are self-documented and it is very easy to know which
method of the resulting class will be called each time: Start from the main class and if
the method is not found there continue from left to right to the ancestor list; since the mixins
do only one thing and do it well you’ll know what each class does simply by looking at its definition.</p>
</div>
<div class="section" id="the-super-situation">
<h3>The super situation</h3>
<p>The final (and most complex) thing and extension I’d like to discuss for our custom class based views is the case
where you want to use the functionality of more than one mixins for the <em>same thing</em>. For example, let’s suppose
that we had a mixin that added some data to the context and a different mixing that added
some different data to the context. Both would use the <tt class="docutils literal">get_context</tt> method
and you’d like to have the context data of both of them to your context. But
this is not possible using the implementations above because when a
<tt class="docutils literal">get_context</tt> is found in the <span class="caps">MRO</span> list it will be called and the <span class="caps">MRO</span> search
will finish there!</p>
<p>So how could we add the functionality of both these mixins to a class based view? This is the same problem as
if we wanted to inherit from a mixin (or a class view) and override one of its methods
but <em>also</em> call its parent (overridden) method for example to get its output and use it as the base
of the output for the overridden method. Both these situations (re-use
functionality of two mixins with the same method or re-use functionality
from a parent method you override) are the same because what stays in the end is
the <span class="caps">MRO</span> list. For example say we we had the following base class</p>
<pre class="code literal-block">
class V:pass
</pre>
<p>and we wanted to override it either using mixins or by using normal inheritance.</p>
<p>When using mixins for example like this:</p>
<pre class="code literal-block">
class M1:pass
class M2:pass
class MIXIN(M2, M1, V):pass
</pre>
<p>we’ll have the following <span class="caps">MRO</span>:</p>
<pre class="code literal-block">
# MIXIN.mro()
# [MIXIN, M2, M1, V, object, ]
</pre>
<p>while when using inheritance like this:</p>
<pre class="code literal-block">
class M1V(V):pass
class M2M1V(M1V):pass
class INHERITANCE(M2M1V):pass
</pre>
<p>we’ll have the following <span class="caps">MRO</span>:</p>
<blockquote>
# <span class="caps">INHERITANCE</span>.mro()
# [<span class="caps">INHERITANCE</span>, <span class="caps">M2M1V</span>, <span class="caps">M1V</span>, V, object ]</blockquote>
<p>As we can see in both cases the base class V is the last one (just next to object)
and between this class and the one that needs the functionality (<tt class="docutils literal"><span class="caps">MIXIN</span></tt> in the first
case and <tt class="docutils literal"><span class="caps">INHERITANCE</span></tt> in the second case) there are
the classes that will define the extra functionality that needs to be re-used: <tt class="docutils literal">M2</tt> and <tt class="docutils literal">M1</tt> (start from
left to right) in the first case and <tt class="docutils literal"><span class="caps">M2M1V</span></tt> and <tt class="docutils literal"><span class="caps">M1V</span></tt> (follow the inheritance hierarchy)
in the second case. So in both cases when calling a method they will be searched the same way using
the <span class="caps">MRO</span> list and when the method is found it will be executed and the search will stop.</p>
<p>But what if we needed to re-use some method from <tt class="docutils literal">V</tt> (or from some other ancestor) and
a class on the left of the <span class="caps">MRO</span> list has the same method?
The answer, as you should have guessed by now if you have some Python knowledge is <tt class="docutils literal">super()</tt>.</p>
<p>The <tt class="docutils literal">super</tt> method can be used by a class method to call a method of <em>its ancestors</em> respecting
the <span class="caps">MRO</span>. Thus, running <tt class="docutils literal"><span class="pre">super().x()</span></tt> from a method instance will try to find method <tt class="docutils literal">x()</tt>
on the <span class="caps">MRO</span> ancestors of this instance <em>even if the instance defines the “x()“ method</em> i.e it will
not search the first element of the <span class="caps">MRO</span> list. Notice
that if the <tt class="docutils literal">x()</tt> method does not exist in the headless-<span class="caps">MRO</span> chain you’ll get an attribute error.
So, usually, you’ll can <tt class="docutils literal"><span class="pre">super().x()</span></tt> from <em>inside</em> the <tt class="docutils literal">x()</tt> method to call your parent’s (as
specified by the <span class="caps">MRO</span> list) same-named method and retrieve its output.</p>
<p>Let’s take a closer look at how <tt class="docutils literal">super()</tt> works using a simple example. For this, we’ll define a method calld <tt class="docutils literal">x()</tt> on all classes
of the previous example:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">V</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From V"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">M1</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From M1"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">M2</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From M2"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">MIXIN</span><span class="p">(</span><span class="n">M2</span><span class="p">,</span> <span class="n">M1</span><span class="p">,</span> <span class="n">V</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From MIXIN"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">M1V</span><span class="p">(</span><span class="n">V</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From M1V"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">M2M1V</span><span class="p">(</span><span class="n">M1V</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From M2M1V"</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">INHERITANCE</span><span class="p">(</span><span class="n">M2M1V</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">x</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"From INHERITANCE"</span><span class="p">)</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"MIXIN OUTPUT"</span><span class="p">)</span>
<span class="n">MIXIN</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
<span class="nb">print</span> <span class="p">(</span><span class="s2">"INHERITANCE OUTPUT"</span><span class="p">)</span>
<span class="n">INHERITANCE</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
</pre></div>
<p>Here’s the output:</p>
<pre class="code literal-block">
MIXIN OUTPUT
From V
From M1
From M2
From MIXIN
INHERITANCE OUTPUT
From V
From M1V
From M2M1V
From INHERITANCE
</pre>
<p>Notice when each message is printed: Because x() first calls its <tt class="docutils literal">super()</tt> method
and then it prints the message in both cases first the <tt class="docutils literal">From V</tt> message is printed
from the base class and then from the following classes in the hierarchy (as per the <span class="caps">MRO</span>)
ending with the class of the instance (either <tt class="docutils literal"><span class="caps">MIXIN</span></tt> or <tt class="docutils literal"><span class="caps">INHERITANCE</span></tt>). Also the
print order is the same in both cases as we’ve already explained. Please make
sure you understand why the output is like this before continuing.</p>
</div>
<div class="section" id="using-super-in-our-hierarchy">
<h3>Using super in our hierarchy</h3>
<p>Using super and mixins it is easy to mix and match functionality to create new
classes. Of course, super can be used without mixins when overriding a method from
a class you inherit from and want to also call your ancestor’s method.</p>
<p>Here’s how we could add a prefix to the header:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">HeaderPrefixMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="s2">"PREFIX: "</span> <span class="o">+</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_header</span><span class="p">()</span>
</pre></div>
<p>and here’s how it could be used:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">HeaderPrefixBetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">HeaderPrefixMixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="n">header</span><span class="o">=</span><span class="s1">'Hello!'</span>
</pre></div>
<p>This will retrieve the header from the ancestor and properly print the header displaying both <span class="caps">PREFIX</span> and Hello.
What if we wanted to re-use the default header mixin? First let’s change <tt class="docutils literal">DefaultHeaderMixin</tt>
to properly use <tt class="docutils literal">super()</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultHeaderSuperMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_header</span><span class="p">()</span> <span class="k">if</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_header</span><span class="p">()</span> <span class="k">else</span> <span class="s2">"DEFAULT HEADER"</span>
</pre></div>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">HeaderPrefixDefaultBetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">HeaderPrefixMixin</span><span class="p">,</span> <span class="n">mixins</span><span class="o">.</span><span class="n">DefaultHeaderSuperMixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>Notice the order of the ancestor classes. The <tt class="docutils literal">get_header()</tt> of <tt class="docutils literal">HeaderPrefixMixin</tt> will be called which
will call the <tt class="docutils literal">get_header()</tt> of
<tt class="docutils literal">DefaultHeaderSuperMixin</tt> (which will call the <tt class="docutils literal">get_header()</tt> of <tt class="docutils literal">BetterCustomClassView</tt> returning <tt class="docutils literal">None</tt>).
So the result will be <tt class="docutils literal">"<span class="caps">PREFIX</span>: <span class="caps">DEFAULT</span> <span class="caps">HEADER</span>"</tt>. However if instead we had defined this class like</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">HeaderPrefixDefaultBetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">DefaultHeaderSuperMixin</span><span class="p">,</span> <span class="n">mixins</span><span class="o">.</span><span class="n">HeaderPrefixMixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>the result would be <tt class="docutils literal">"<span class="caps">PREFIX</span>: "</tt> (<span class="caps">DEFAULT</span> <span class="caps">HEADER</span> won’t be printed). Can you understand why?</p>
<p>One thing to keep in mind is that most probably you’ll need to call <tt class="docutils literal">super()</tt> and return its output when you override a method.
Even if you think that you don’t need to call it for this view or mixin, you may need it later from some other view or mixin that
inherits from this view. Also notice that <tt class="docutils literal">super()</tt> may not return anything but may have some
side-effects in your class (for example set a <tt class="docutils literal">self</tt> attribute) which you won’t get if you don’t call it!</p>
<p>For another example of super, let’s define a couple of mixins that add things to the context:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ExtraContext1Mixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context</span><span class="p">()</span>
<span class="n">ctx</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">'data1'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">ctx</span>
<span class="k">class</span> <span class="nc">ExtraContext2Mixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context</span><span class="p">()</span>
<span class="n">ctx</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="s1">'data2'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
<p>The first one retrieves the ancestor context list and appends <tt class="docutils literal">'data1'</tt> to the
it while the second one will insert <tt class="docutils literal">'data2'</tt> to the start of the list. To use
these mixins just add them to the ancestor list of your class hierarchy as usually.
One interesting thing to notice here is that because of how <tt class="docutils literal">get_context</tt> is
defined we’ll get the same output no matter the order of the mixins in the hierarchy
since <tt class="docutils literal">ExtraContext1Mixin</tt> will append <tt class="docutils literal">data1</tt> to the end of the context list and
the <tt class="docutils literal">ExtraContext2Mixin</tt> will insert <tt class="docutils literal">data2</tt> to the start of the context list.</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ExtraContext12BetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">ExtraContext1Mixin</span><span class="p">,</span> <span class="n">mixins</span><span class="o">.</span><span class="n">ExtraContext2Mixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">ExtraContext21BetterCustomClassView</span><span class="p">(</span><span class="n">mixins</span><span class="o">.</span><span class="n">ExtraContext2Mixin</span><span class="p">,</span> <span class="n">mixins</span><span class="o">.</span><span class="n">ExtraContext1Mixin</span><span class="p">,</span> <span class="n">BetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>If instead both of these mixins appended the item to the end of the list, then
the output would be different depending on the ancestor order.
Of course, since we’ve already defined <tt class="docutils literal">HeaderPrefixMixin</tt> and <tt class="docutils literal">DefaultHeaderSuperMixin</tt> nothing stops us
from using all those mixins together!</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AllTogetherNowBetterCustomClassView</span><span class="p">(</span>
<span class="n">mixins</span><span class="o">.</span><span class="n">HeaderPrefixMixin</span><span class="p">,</span>
<span class="n">mixins</span><span class="o">.</span><span class="n">DefaultHeaderSuperMixin</span><span class="p">,</span>
<span class="n">mixins</span><span class="o">.</span><span class="n">ExtraContext1Mixin</span><span class="p">,</span>
<span class="n">mixins</span><span class="o">.</span><span class="n">ExtraContext2Mixin</span><span class="p">,</span>
<span class="n">BetterCustomClassView</span>
<span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>This will have the desired behaviour of adding a prefix to the header, having a default header if not one was defined
and adding the extra context from both mixins!</p>
</div>
<div class="section" id="testing-all-this">
<h3>Testing all this</h3>
<p>In the accompanying project at <a class="reference external" href="https://github.com/spapas/cbv-tutorial">https://github.com/spapas/cbv-tutorial</a> you can take a look at how this
custom <span class="caps">CBV</span> hierarchy works by running it and taking a look at the <tt class="docutils literal">core</tt> project (visit <a class="reference external" href="http://127.0.0.1:8001/non-django-cbv/">http://127.0.0.1:8001/non-django-cbv/</a>).
There you can take a look at the views.py and mixins.py to see all the views and mixins we’ve discussed in this chapter.</p>
</div>
</div>
<div class="section" id="a-high-level-overview-of-cbvs">
<h2>A high level overview of CBVs</h2>
<p>After the previous rather long (but I hope gentle enough) introduction to implementing
our own class based view hierarchy using inheritance, mixins, <span class="caps">MRO</span>, method overriding
and <tt class="docutils literal">super</tt> we can now start talking about the Django Class Based Views (CBVs). Our
guide will be the <a class="reference external" href="http://ccbv.co.uk`"><span class="caps">CBV</span> inspector</a> application which displays all classes and mixins
that Django CBVs are using along with their methods and attributes. Using this application
and after reading this article you should be able to quickly and definitely know
which method or attribute you need to define to each one of your mixins or views.</p>
<p>To use <span class="caps">CBV</span> inspector, just click on a class name (for example <tt class="docutils literal">CreateView</tt>); you will
immediately see its <span class="caps">MRO</span> ancestors, its list of attributes (and the ancestor class that defines
each one) and finally a list of methods that this class and all its ancestors define.
Of course when a method is defined by multiple classes the <span class="caps">MRO</span> ordering will be used -
super is used when the functionality of the ancestor classes is also used. The <span class="caps">CBV</span>
inspector (and our project) has Python 3 syntax. If you want to follow along with
Python 2 (I don’t recommend it though since Django 2.0 only supports Python 3.x) use the following
syntax to call super for method <tt class="docutils literal">x()</tt>:</p>
<div class="highlight"><pre><span></span><span class="nb">super</span><span class="p">(</span><span class="n">ClassName</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
</pre></div>
<p>this is the same as calling</p>
<div class="highlight"><pre><span></span><span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">x</span><span class="p">()</span>
</pre></div>
<p>in Python 3.x.</p>
<div class="section" id="taking-a-look-at-the-view">
<h3>Taking a look at the View</h3>
<p>In any case, our travel starts from the central <span class="caps">CBV</span> class which is (intuitively) called … <a class="reference external" href="https://ccbv.co.uk/View">View</a>!</p>
<p>This class is used as the base in Django’s <span class="caps">CBV</span> hierarchy (similar to how <tt class="docutils literal">CustomClassView</tt>
was used in our own hierarchy). It has only one attribute
(<tt class="docutils literal">http_method_names</tt>) and a very small number of methods. The most important method is the
<tt class="docutils literal">as_view</tt> class method (which is similar to the one we defined in the previous section).
The <tt class="docutils literal">as_view</tt> will instantiate an instance object of the <tt class="docutils literal">View</tt> class
(actually the class that inherits from <tt class="docutils literal">View</tt>) and use this object to properly generate a functional view.</p>
<p>The <tt class="docutils literal">View</tt> class cannot be used as it is
but it must be inherited by a child class. The child class needs to define a method
that has the same name as each http method that is supported - for example if
only <span class="caps">HTTP</span> <span class="caps">GET</span> and <span class="caps">HTTP</span> <span class="caps">POST</span> are supported then the inherited class must define a
<tt class="docutils literal">get</tt> and a <tt class="docutils literal">post</tt> method; these methods are called from the functional view
through a method called <tt class="docutils literal">dispatch</tt> and need to return a proper response object. So,
we have two central methods here: The <tt class="docutils literal">as_view</tt> class method that creates the
object instance and returns its view function and <tt class="docutils literal">dispatch</tt> that will call
the proper named class method depending on the <span class="caps">HTTP</span> method (i.e post, get, put
etc). One thing to keep from this discussion is that you shouldn’t ever need to
mess with <tt class="docutils literal">as_view</tt> but, because <tt class="docutils literal">dispatch</tt> is the only instance method that is
guaranteed to run every time the class based view will run, you will frequently
need to override it especially to control access control.</p>
<p>As an example, we can implemented the <tt class="docutils literal">BetterCustomClassView</tt> from the first
section using <tt class="docutils literal">View</tt> as its ancestor:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DjangoBetterCustomClassView</span><span class="p">(</span><span class="n">View</span><span class="p">,</span> <span class="p">):</span>
<span class="n">header</span> <span class="o">=</span> <span class="s1">''</span>
<span class="n">context</span> <span class="o">=</span><span class="s1">''</span>
<span class="k">def</span> <span class="nf">get_header</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">header</span> <span class="k">else</span> <span class="s2">""</span>
<span class="k">def</span> <span class="nf">get_context</span><span class="p">(</span><span class="bp">self</span> <span class="p">,</span> <span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">context</span> <span class="k">else</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">render_context</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_context</span><span class="p">()</span>
<span class="k">if</span> <span class="n">context</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">'<br />'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
<span class="k">return</span> <span class="s2">""</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">resp</span> <span class="o">=</span> <span class="s2">"""</span>
<span class="s2"> <html></span>
<span class="s2"> <body></span>
<span class="s2"> <h1></span><span class="si">{header}</span><span class="s2"></h1></span>
<span class="s2"> </span><span class="si">{body}</span>
<span class="s2"> </body></span>
<span class="s2"> </html></span>
<span class="s2"> """</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
<span class="n">header</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_header</span><span class="p">(),</span> <span class="n">body</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">render_context</span><span class="p">(),</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">resp</span><span class="p">)</span>
</pre></div>
<p>This method won’t print anything but of course it could use the mixins from
before to have some default values:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DefaultHeaderContextDjangoBetterCustomClassView</span><span class="p">(</span><span class="n">DefaultHeaderMixin</span><span class="p">,</span> <span class="n">DefaultContextMixin</span><span class="p">,</span> <span class="n">DjangoBetterCustomClassView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>Of course instead of using our mixins and render methods it would be much better
to use the proper ones defined by Django - that’s what we’re going to do from
now on I just wanted to make clear that there’s nothing special in Django’s <span class="caps">CBV</span>
hierarchy and can be overridden as we’d like.</p>
</div>
<div class="section" id="redirectview-and-templateview">
<h3>RedirectView and TemplateView</h3>
<p>Continuing our tour of Django CBVs I’d like to talk a little about the classes
that the <span class="caps">CBV</span> Inspector puts in the same level as <tt class="docutils literal">View</tt> (<span class="caps">GENERIC</span> <span class="caps">BASE</span>):
<a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/base/#redirectview">RedirectView</a> and <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/base/#templateview">TemplateView</a>. Both inherit directly from <tt class="docutils literal">View</tt> and, the
first one defines a <tt class="docutils literal">get</tt> method that returns a redirect to another page
while the latter one renders and returns a Django template in the <tt class="docutils literal">get</tt>
method.</p>
<p>The <tt class="docutils literal">RedirectView</tt> inherits directly from view and has attributes like <tt class="docutils literal">url</tt>
(to use a static url)
or <tt class="docutils literal">pattern_name</tt> (to use one of the patterns define in your urls.py)
to define where it should redirect. These attributes are
used by the <tt class="docutils literal">get_redirect_url</tt> which will generate the actual url to redirect
to and can be overriden for example to redirect to a different location depending
on the current user.</p>
<p>The <tt class="docutils literal">TemplateView</tt> on the other hand inherits from <tt class="docutils literal">View</tt> and two more classes (actually
these are mixins) beyond <tt class="docutils literal">View</tt>: <tt class="docutils literal">TemplateResponseMixin</tt> and
<tt class="docutils literal">ContextMixin</tt>. If you take a look at them you’ll see that the
<tt class="docutils literal">TemplateResponseMixin</tt> defines some template-related attributes (most important being
the <tt class="docutils literal">template_name</tt>) and two
methods: One that retrieves the template that will be used to render this View
(<tt class="docutils literal">get_template_names</tt>)
and one that actually renders the template (<tt class="docutils literal">render_to_response</tt>) using a
<a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/template-response/#django.template.response.TemplateResponse">TemplateResponse</a> instance. The
<tt class="docutils literal">ContextMixin</tt> provides the <tt class="docutils literal">get_context_data</tt> that is
passed to the template to be rendered and should be overridden if you want to
pass more context variables.</p>
<p>We can already see many opportunities of reusing and overriding
functionality and improving our <span class="caps">DRY</span> score, for example: Create a catch all RedirectView
that depending on the remainder of the url it will redirect to a different page,
create a mixin that appends some things to the context of all CBVs using it, use dynamic templates
based on some other condition (that’s actually what Detail/List/UpdateView
are doing), render a template to a different output than Html (for example a
text file) etc. I’ll try to present examples for these in the next section.</p>
</div>
<div class="section" id="the-formview">
<h3>The FormView</h3>
<p>The next view we’re going to talk about is <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-editing/#formview">FormView</a>. This is a view that can be
used whenever we want to display a form (<em>not</em> a form related to a Model i.e for
Create/Update/Delete, for these cases there are specific CBVs we’ll see later).
It is interesting to take a look at the list of its
ancestors: <tt class="docutils literal">TemplateResponseMixin</tt>, <tt class="docutils literal">BaseFormView</tt>, <tt class="docutils literal">FormMixin</tt>, <tt class="docutils literal">ContextMixin</tt>, <tt class="docutils literal">ProcessFormView</tt> and <tt class="docutils literal">View</tt>.
We are familiar with <tt class="docutils literal">TemplateResponseMixin</tt>, <tt class="docutils literal">ContextMixin</tt> and <tt class="docutils literal">View</tt> but not with
the others. Before discussing these classes let’s take a look at the FormView
hierarchy, courtesy of <a class="reference external" href="http://ccbv.co.uk">http://ccbv.co.uk</a> and <a class="reference external" href="http://yuml.me">http://yuml.me</a>:</p>
<img src="https://yuml.me/diagram/plain;/class/[TemplateResponseMixin%7Bbg:white%7D]%5E-[FormView%7Bbg:green%7D],%20[BaseFormView%7Bbg:white%7D]%5E-[FormView%7Bbg:green%7D],%20[FormMixin%7Bbg:white%7D]%5E-[BaseFormView%7Bbg:white%7D],%20[ContextMixin%7Bbg:white%7D]%5E-[FormMixin%7Bbg:white%7D],%20[ProcessFormView%7Bbg:white%7D]%5E-[BaseFormView%7Bbg:white%7D],%20[View%7Bbg:lightblue%7D]%5E-[ProcessFormView%7Bbg:white%7D].svg" alt="FormView"><p>The above diagram should make everything easier: The <tt class="docutils literal">FormMixin</tt> inherits
from <tt class="docutils literal">ContextMixin</tt> and overrides its <tt class="docutils literal">get_context_data</tt> method to add the
form to the view. Beyond this, it adds some attributes and methods for proper form handling, for
example the <tt class="docutils literal">form_class</tt> (attribute when the form class will be the same always) and
<tt class="docutils literal">get_form_class()</tt> (method when the form class will be dynamic for example
depending on the logged in user), <tt class="docutils literal">initial</tt> and <tt class="docutils literal">get_initial()</tt> (same logic as before for
the form’s initial values), <tt class="docutils literal">form_valid()</tt> and <tt class="docutils literal">form_invalid()</tt> to define
what should happen when the form is valid or invalid, <tt class="docutils literal">get_form_kwargs</tt> to pass
some keyword arguments to the form’s constructor etc. Notice that FormMixin
does not define any form handling logic (i.e check if the form is valid and call
its <tt class="docutils literal">form_valid()</tt> method) — this logic is defined in the <tt class="docutils literal">ProcessFormView</tt>
which inherits from <tt class="docutils literal">View</tt> and defines proper <tt class="docutils literal">get()</tt> (just render the form)
and <tt class="docutils literal">post()</tt> (check if the form is valid and call <tt class="docutils literal">form_valid</tt> else call <tt class="docutils literal">form_invalid</tt>) methods.</p>
<p>One interesting here is to notice here is that Django defines both the <tt class="docutils literal">FormMixin</tt> and <tt class="docutils literal">ProcessFormView</tt>.
The <tt class="docutils literal">FormMixin</tt> offers the basic Form elements (the form class, initial data
etc) and could be re-used in a different flow beyond the one offered by
<tt class="docutils literal">ProcessFormView</tt> (for example display the form as a <span class="caps">JSON</span> object instead of a
Django template). On the other hand, <tt class="docutils literal">ProcessFormView</tt> is required in order to
define the <tt class="docutils literal">get</tt> and <tt class="docutils literal">post</tt> methods that are needed from the <tt class="docutils literal">View</tt>. These
methods can’t be overridden in the FormMixin since that would mean that the mixin
would behave as a view!</p>
<p>Finally, the <tt class="docutils literal">BaseFormView</tt> class is used to
inherit from <tt class="docutils literal">ProcessFormView</tt> and <tt class="docutils literal">FormMixin</tt>. It does not do anything
more than providing a base class that other classes that want to use the form
functionality (i.e both the <tt class="docutils literal">ProcessFormView</tt> and <tt class="docutils literal">FormMixin</tt>) will inherit from.</p>
</div>
<div class="section" id="the-listview-and-detailview">
<h3>The ListView and DetailView</h3>
<p>Next in our Django <span class="caps">CBV</span> tour is the <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#listview">ListView</a>. The <tt class="docutils literal">ListView</tt> is used to render multiple
objects in a template, for example in a list or table. Here’s a diagram of the class
hierarchy (courtesy of <a class="reference external" href="http://ccbv.co.uk">http://ccbv.co.uk</a> and <a class="reference external" href="http://yuml.me">http://yuml.me</a>):</p>
<img src="https://yuml.me/diagram/plain;/class/[MultipleObjectTemplateResponseMixin%7Bbg:white%7D]%5E-[ListView%7Bbg:green%7D],%20[TemplateResponseMixin%7Bbg:white%7D]%5E-[MultipleObjectTemplateResponseMixin%7Bbg:white%7D],%20[BaseListView%7Bbg:white%7D]%5E-[ListView%7Bbg:green%7D],%20[MultipleObjectMixin%7Bbg:white%7D]%5E-[BaseListView%7Bbg:white%7D],%20[ContextMixin%7Bbg:white%7D]%5E-[MultipleObjectMixin%7Bbg:white%7D],%20[View%7Bbg:lightblue%7D]%5E-[BaseListView%7Bbg:white%7D].svg" alt="ListView"><p>The <tt class="docutils literal">MultipleObjectMixin</tt> is used make a query to the database (either using a
model or a queryset) and pass the results to the context. It also supports
custom ordering (<tt class="docutils literal">get_ordering()</tt>) and pagination (<tt class="docutils literal">paginate_queryset()</tt>).
However, the most important method of this mixin is <tt class="docutils literal">get_queryset()</tt>. This
method checks to see if the <tt class="docutils literal">queryset</tt> or <tt class="docutils literal">model</tt> attribute are defined
(<tt class="docutils literal">queryset</tt> will be checked first so it has priority if both are defined) and
returns a queryset result (taking into account the ordering). This queryset
result will be used by the <tt class="docutils literal">get_context_data()</tt> method of this mixin to
actually put it to the context by saving to a context variable named <tt class="docutils literal">object_list</tt>.
Notice that you can set the <tt class="docutils literal">context_object_name</tt> attribute to add and extra
another variable to the context with the queryset beyond <tt class="docutils literal">object_list</tt> (for
example if you have an <tt class="docutils literal">ArticleLsitView</tt> you can set <tt class="docutils literal">context_object_name = articles</tt> to
be able to do <tt class="docutils literal">{% for article in articles %}</tt> in your context instead of
<tt class="docutils literal">{% for article in object_list %}</tt>).</p>
<p>The <tt class="docutils literal">MultipleObjectMixin</tt> can be used and
overridden when we need to put multiple objects in a View. This mixin is
inherited (along with <tt class="docutils literal">View</tt>) from <tt class="docutils literal">BaseListView</tt> that adds a proper <tt class="docutils literal">get</tt>
method to call <tt class="docutils literal">get_context_data</tt> and pass the result to the template.</p>
<p>As we can also see, Django uses the <tt class="docutils literal">MultipleObjectTemplateResponseMixin</tt> that
inherits from <tt class="docutils literal">TemplateResponseMixin</tt> to render the template. This mixin does
some magic with the queryset or model to define a
template name (so you won’t need to define it yourself) - that’s from where the
<tt class="docutils literal">app_label/app_model_list.html</tt> default template name is created.</p>
<p>Similar to the <tt class="docutils literal">ListView</tt> is the <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#detailview">DetailView</a> which has the same class hierarchy as the <tt class="docutils literal">ListView</tt> with two differences:
It uses <tt class="docutils literal">SingleObjectMixin</tt> instead of <tt class="docutils literal">MultipleOjbectMixin</tt>,
<tt class="docutils literal">SingleObjectTemplateResponseMixin</tt> instead of <tt class="docutils literal">MultipleObjectTemplateResponseMixin</tt>
and <tt class="docutils literal">BaseDetailView</tt> instead of <tt class="docutils literal">BaseListView</tt>. The
<tt class="docutils literal">SingleObjectMixin</tt> will use the <tt class="docutils literal">get_queryset()</tt> (in a similar manner to the <tt class="docutils literal">get_queryset()</tt> of
<tt class="docutils literal">MultipleObjectMixin</tt>) method to return a single object (so all attributes and methods
concerning ordering or pagination are missing) but instead has the <tt class="docutils literal">get_object()</tt> method which
will pick and return a single object from that queryset (using a <tt class="docutils literal">pk</tt> or <tt class="docutils literal">slug</tt> parameter). This object
will be put to the context of this view by the <tt class="docutils literal">get_context_data</tt>. The <tt class="docutils literal">BaseDetailView</tt> just
defines a proper <tt class="docutils literal">get</tt> to call the <tt class="docutils literal">get_context_data</tt> (of <tt class="docutils literal">SingleObjectMixin</tt>) and finally
the <tt class="docutils literal">SingleObjectTemplateResponseMixin</tt> will automatically generate the template name (i.e generate
<tt class="docutils literal">app_label/app_model_detail.html</tt>).</p>
</div>
<div class="section" id="the-createview">
<h3>The CreateView</h3>
<p>The next Django <span class="caps">CBV</span> we’ll talk about is <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#createview">CreateView</a>. This class is used to create a new instance
of a model. It has a rather complex hierarchy diagram but we’ve already discussed most of these classes:</p>
<img src="https://yuml.me/diagram/plain;/class/[SingleObjectTemplateResponseMixin%7Bbg:white%7D]%5E-[CreateView%7Bbg:green%7D],%20[TemplateResponseMixin%7Bbg:white%7D]%5E-[SingleObjectTemplateResponseMixin%7Bbg:white%7D],%20[BaseCreateView%7Bbg:white%7D]%5E-[CreateView%7Bbg:green%7D],%20[ModelFormMixin%7Bbg:white%7D]%5E-[BaseCreateView%7Bbg:white%7D],%20[FormMixin%7Bbg:white%7D]%5E-[ModelFormMixin%7Bbg:white%7D],%20[ContextMixin%7Bbg:white%7D]%5E-[FormMixin%7Bbg:white%7D],%20[SingleObjectMixin%7Bbg:white%7D]%5E-[ModelFormMixin%7Bbg:white%7D],%20[ContextMixin%7Bbg:white%7D]%5E-[SingleObjectMixin%7Bbg:white%7D],%20[ProcessFormView%7Bbg:white%7D]%5E-[BaseCreateView%7Bbg:white%7D],%20[View%7Bbg:lightblue%7D]%5E-[ProcessFormView%7Bbg:white%7D].svg" /><p>As we can see the <tt class="docutils literal">CreateView</tt> inherits from <tt class="docutils literal">BaseCreateView</tt> and <tt class="docutils literal">SingleObjectTemplateResponseMixin</tt>. The
<tt class="docutils literal">SingleObjectTemplateResponseMixin</tt> is mainly used to define the template names that will be searched for
(i.e <tt class="docutils literal">app_label/app_model_form.html</tt>), while the <tt class="docutils literal">BaseCreateView</tt>
is used to combine the functionality of <tt class="docutils literal">ProcessFormView</tt> (that handles the basic form workflow as we have
already discussed) and <tt class="docutils literal">ModelFormMixin</tt>. The <tt class="docutils literal">ModelFormMixin</tt> is a rather complex mixin that inherits from
both <tt class="docutils literal">SingleObjectMixin</tt> and <tt class="docutils literal">FormMixin</tt>. The <tt class="docutils literal">SingleObjectMixin</tt> functionality is not really used by <tt class="docutils literal">CreateView</tt>
(since no object will need to be retrieved for the <tt class="docutils literal">CreateView</tt>) however the <tt class="docutils literal">ModelFormMixin</tt> is also used
by <tt class="docutils literal">UpdateView</tt> that’s why <tt class="docutils literal">ModelFormMixin</tt> also inherits from it (to retrieve the object that will be updated).</p>
<p><tt class="docutils literal">ModelFormMixin</tt> mixin adds functionality
for handling forms related to models and object instances. More specifically it adds functionality for:
* creating a form class (if one is not provided) by the configured model / queryset. If you don’t provide the form class (by using the <tt class="docutils literal">form_class</tt> attribute) then you need to configure the fields that the generated form will display by passing an array of field names through the <tt class="docutils literal">fields</tt> attribute
* overrides the <tt class="docutils literal">form_valid</tt> in order to save the object instance of the form
* fixes <tt class="docutils literal">get_success_url</tt> to redirect to the saved object’s absolute_url when the object is saved
* pass the current object to be updated (that was retrieving through the <tt class="docutils literal">SingleObjectMixin</tt>) -if there is a current object- to the form as the <tt class="docutils literal">instance</tt> attribute</p>
</div>
<div class="section" id="the-updateview-and-deleteview">
<h3>The UpdateView and DeleteView</h3>
<p>The <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#updateview">UpdateView</a> class is almost identical to the <tt class="docutils literal">CreateView</tt> - the only difference is that
<tt class="docutils literal">UpdateView</tt> inherits from <tt class="docutils literal">BaseUpdateView</tt> (and <tt class="docutils literal">SingleObjectTemplateResponseMixin</tt>) instead
of <tt class="docutils literal">BaseCreateView</tt>. The <tt class="docutils literal">BaseUpdateView</tt> overrides the <tt class="docutils literal">get</tt> and <tt class="docutils literal">post</tt> methods of
<tt class="docutils literal">ProcessFormView</tt> to retrieve the object (using <tt class="docutils literal">SingleObjectMixin</tt>‘s <tt class="docutils literal">get_object()</tt>)
and assign it to an instance variable - this will then be picked up by the <tt class="docutils literal">ModelFormMixin</tt> and used
properly in the form as explained before. One thing I notice here is that it seems that the hierarchy would
be better if the <tt class="docutils literal">ModelFormMixin</tt> inherited <em>only</em> from <tt class="docutils literal">FormMixin</tt> (instead of both from
<tt class="docutils literal">FormMixin</tt> and <tt class="docutils literal">SingleObjectMixin</tt>) and <tt class="docutils literal">BaseUpdateView</tt> inheriting from <tt class="docutils literal">ProcessFormView</tt>,
<tt class="docutils literal">ModelForMixin</tt> <em>and</em> <tt class="docutils literal">SingleObjectMixin</tt>. This way the <tt class="docutils literal">BaseCreateView</tt> wouldn’t get the
non-needed <tt class="docutils literal">SingleObjectMixin</tt> functionality. I am not sure why Django is implemented this way
(i.e the <tt class="docutils literal">ModelFormMixin</tt> also inheriting from <tt class="docutils literal">SingleObjectMixin</tt> thus passing this non-needed
functionality to <tt class="docutils literal">BaseCreateView</tt>) — if a reader has a clue I’d like to know it.</p>
<p>In any way, I’d like to also present the <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#deleteview">DeleteView</a> which is more or less the same as the <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/class-based-views/generic-display/#detailview">DetailView</a>
with the addition of the <tt class="docutils literal">DeleteMixin</tt> in the mix. The <tt class="docutils literal">DeleteMixin</tt> adds a <tt class="docutils literal">post()</tt> method
that will delete the object when called and makes <tt class="docutils literal">success_url</tt> required (since there would be no
object to redirect to after this view is posted).</p>
</div>
<div class="section" id="access-control-mixins">
<h3>Access control mixins</h3>
<p>Another small hierarchy of class based views (actually these are all mixins) are the authentication ones which
can be used to control access to a view.
These are <tt class="docutils literal">AcessMixin</tt>, <tt class="docutils literal">LoginRequiredMixin</tt>, <tt class="docutils literal">PermissionRequiredMixin</tt> and <tt class="docutils literal">UserPassesTestMixin</tt>.
The <tt class="docutils literal">AccessMixin</tt> provides some basic functionality (i.e what to do when the user does not have access
to the view, find out the login url to redirect him etc) and is used as a base for the other three. These
three override the <tt class="docutils literal">dispatch()</tt> method of <tt class="docutils literal">View</tt> to check if the user has the specific rights (i.e
if he has logged in for <tt class="docutils literal">LoginRequiredMixin</tt>, if he has the defined permissions for <tt class="docutils literal">PermissionRequiredMixin</tt>
or if he passes the provided test in <tt class="docutils literal">UserPassesTextMixin</tt>). If the user has the rights the view will proceed
as normally (call super’s dispatch) else the access denied functionality from <tt class="docutils literal">AccessMixin</tt> will be implemented.</p>
</div>
<div class="section" id="some-other-cbvs">
<h3>Some other CBVs</h3>
<p>Beyond the class based views I discussed in this section, Django also has a bunch of CBVs related
to account views (<tt class="docutils literal">LoginView</tt>, <tt class="docutils literal">LogoutView</tt>, <tt class="docutils literal">PasswordChangeView</tt> etc) and Dates (<tt class="docutils literal">DateDetailView</tt>, <tt class="docutils literal">YearArchiveView</tt> etc).
I won’t go into detail about these since they follow the same concepts and use most of the mixins
we’ve discussed before. Using the <span class="caps">CBV</span> Inspector you should be able to follow along and decide the methods you need
to override for your needs.</p>
<p>Also, most well written Django packages will define their own CBVs that inherit
from the Django CBVs - with the knowledge you acquired here you will be able to follow along on their source code to understand how everything works.</p>
</div>
</div>
<div class="section" id="real-world-use-cases">
<h2>Real world use cases</h2>
<p>In this section I am going to present a number of use cases demonstrating the usefulness of Django CBVs. In most of
these examples I am going to override one of the methods of the mixins I discussed in the previous section. There
are <em>two</em> methods you can use for integrating the following use cases to your application.</p>
<p>Create your own class inheriting from one of the Django CBVs and add to it directly the method to override. For example,
if you wanted to override the <tt class="docutils literal">get_queryset()</tt> method a <tt class="docutils literal">ListView</tt> you would do a:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">GetQuerysetOverrideListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">return</span> <span class="n">qs</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">status</span><span class="o">=</span><span class="s1">'PUBLISHED'</span><span class="p">)</span>
</pre></div>
<p>This is useful if you know that you aren’t going to need the overriden <tt class="docutils literal">get_queryset</tt> functionality to a different
method and following the <span class="caps">YAGNI</span> principle (or if you know that even if you need it you could inherit from <tt class="docutils literal">GetQuerysetOverrideListView</tt>
i.e in another ListView).
However, if you know that there may be more CBVs that would need their
queryset filtered by <tt class="docutils literal"><span class="pre">status='<span class="caps">PUBLISHED</span>'</span></tt> then you should add a mixin that would be used by your CBVs:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">GetQuerysetOverrideMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">return</span> <span class="n">qs</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">status</span><span class="o">=</span><span class="s1">'PUBLISHED'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">GetQuerysetOverrideListView</span><span class="p">(</span><span class="n">GetQuerysetOverrideMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>Now, one thing that needs some discussion here is that the method <tt class="docutils literal">get_queryset</tt> is provided by a mixin (in fact
it is provided by two mixins: <tt class="docutils literal">MultipleObjectMixin</tt> for <tt class="docutils literal">ListView</tt> and <tt class="docutils literal">SingleObjectMixin</tt> for <tt class="docutils literal">DetailView</tt>,
<tt class="docutils literal">UpdateView</tt> and <tt class="docutils literal">DeleteView</tt>). Because of how <span class="caps">MRO</span> works, I won’t need to inherit <tt class="docutils literal">GetQuerysetOverrideMixin</tt> from
<tt class="docutils literal">MultipleObjectMixin</tt> (or <tt class="docutils literal">SingleObjectMixin</tt> but let’s ignore that for now) but I can just inherit from object
and make sure that, as already discussed, put the mixin <em>before</em> (to the left) of the <span class="caps">CBV</span>. Notice that even if I had
defined <tt class="docutils literal">GetQuerysetOverrideMixin</tt> as <tt class="docutils literal">GetQuerysetOverrideMixin(MultipleObjectMixin)</tt> the <tt class="docutils literal">MultipleObjectMixin</tt> class would
be found <em>twice</em> in the <span class="caps">MRO</span> list so only the rightmost instance would remain. So the <span class="caps">MRO</span> for both <tt class="docutils literal">GetQuerysetOverrideMixin(object, )</tt>
and <tt class="docutils literal">GetQuerysetOverrideMixin(MultipleObjectMixin)</tt> <em>would be the same</em>! Also, inheriting directly from object makes
our <tt class="docutils literal">GetQuerysetOverrideMixin</tt> more <span class="caps">DRY</span> since if it inherited from <tt class="docutils literal">MultipleObjectMixin</tt> we’d need to create <em>another</em>
version of it that would inherit from <tt class="docutils literal">SingleObjectMixin</tt>; this is because <tt class="docutils literal">get_queryset</tt> exists in both these mixins.</p>
<p>For some of the following use cases I am also going to use the following models for user generated content (articles and uploaded files):</p>
<div class="highlight"><pre><span></span><span class="n">STATUS_CHOICES</span> <span class="o">=</span> <span class="p">(</span>
<span class="p">(</span><span class="s1">'DRAFT'</span><span class="p">,</span> <span class="s1">'Draft'</span><span class="p">,</span> <span class="p">),</span>
<span class="p">(</span><span class="s1">'PUBLISHED'</span><span class="p">,</span> <span class="s1">'Published'</span><span class="p">,</span> <span class="p">),</span>
<span class="p">(</span><span class="s1">'REMOVED'</span><span class="p">,</span> <span class="s1">'Removed'</span><span class="p">,</span> <span class="p">),</span>
<span class="p">)</span>
<span class="k">class</span> <span class="nc">Category</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="p">)</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">permissions</span> <span class="o">=</span> <span class="p">(</span>
<span class="p">(</span><span class="s2">"publisher_access"</span><span class="p">,</span> <span class="s2">"Publisher Access"</span><span class="p">),</span>
<span class="p">(</span><span class="s2">"admin_access"</span><span class="p">,</span> <span class="s2">"Admin Access"</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">class</span> <span class="nc">AbstractGeneralInfo</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">status</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">16</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">STATUS_CHOICES</span><span class="p">,</span> <span class="p">)</span>
<span class="n">category</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'category'</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="p">)</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="p">)</span>
<span class="n">created_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s1">'</span><span class="si">%(class)s</span><span class="s1">_created_by'</span><span class="p">,</span> <span class="p">)</span>
<span class="n">modified_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="p">)</span>
<span class="n">modified_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s1">'</span><span class="si">%(class)s</span><span class="s1">_modified_by'</span><span class="p">,</span> <span class="p">)</span>
<span class="n">owned_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="o">.</span><span class="n">PROTECT</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s1">'</span><span class="si">%(class)s</span><span class="s1">_owned_by'</span><span class="p">,</span> <span class="p">)</span>
<span class="n">published_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">class</span> <span class="nc">Article</span><span class="p">(</span><span class="n">AbstractGeneralInfo</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="p">)</span>
<span class="n">content</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">Document</span><span class="p">(</span><span class="n">AbstractGeneralInfo</span><span class="p">):</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="p">)</span>
<span class="n">file</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">FileField</span><span class="p">()</span>
</pre></div>
<p>All this can be found on the accompanying project <a class="reference external" href="https://github.com/spapas/cbv-tutorial">https://github.com/spapas/cbv-tutorial</a> on the djangocbv app (visit <a class="reference external" href="http://127.0.0.1:8001/djangocbv/">http://127.0.0.1:8001/djangocbv/</a>).</p>
<div class="section" id="do-something-when-a-valid-form-is-submitted">
<h3>Do something when a valid form is submitted</h3>
<p>When a form is submitted and the form is valid
the <tt class="docutils literal">form_valid</tt> method of <tt class="docutils literal">ModelForMixin</tt> (and <tt class="docutils literal">FormMixin</tt>) will be called. This method
can be overridden to do various things before (or after) the form is saved. For example,
you may want have a field whose value is calculated from other fields in the form or you want to
create an extra object. Let’s see a generic example of overriding a <tt class="docutils literal">CreateView</tt> or <tt class="docutils literal">UpdateView</tt> with comments:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">,</span> <span class="p">):</span>
<span class="c1"># let's calculate a field value</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">calculated_field</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'data1'</span><span class="p">]</span> <span class="o">+</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'data2'</span><span class="p">]</span>
<span class="c1"># save the form by calling super().form_valid(); keep the return value - it is the value of get_success_url</span>
<span class="n">redirect_to</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="c1"># For Create or UpdateView, the just-saved object will be assigned to self.object</span>
<span class="n">logger</span><span class="o">.</span><span class="n">log</span><span class="p">(</span><span class="s2">"Created an object with id </span><span class="si">{0}</span><span class="s2">"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">object</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="c1"># return the redirect</span>
<span class="k">return</span> <span class="n">redirect_to</span>
</pre></div>
<p>This is rather complex so I’ll also explain it: The form_valid gets the actual form which, since is validated
has a <tt class="docutils literal">cleaned_data</tt> dictionary of values. This form also has an <tt class="docutils literal">instance</tt> attribute which is the object
that this form is bound to - notice that a normal <tt class="docutils literal">Form</tt> won’t have an instance only a <tt class="docutils literal">ModelForm</tt>.
This can be used to modify the instance of this form as needed - before saving it. When you want to actually save
the instance you call <tt class="docutils literal"><span class="pre">super().form_valid()</span></tt> passing it the modified form (and instance). This method does
three things</p>
<ul class="simple">
<li>It saves the instance to the database</li>
<li>It assigns the saved object to the <tt class="docutils literal">object</tt> instance attribute (so you can refer to it by <tt class="docutils literal">self.instance</tt>)</li>
<li>It uses <tt class="docutils literal">get_redirect_url</tt> to retrieve the location where you should redirect after the form is submitted</li>
</ul>
<p>Thus in this example we save <tt class="docutils literal">redirect_to</tt> to return it also from our method also and then can use <tt class="docutils literal">self.object.id</tt>
to log the id of the current object.</p>
<p>On a more specific example, notice the <tt class="docutils literal">Article</tt> and <tt class="docutils literal">Document</tt> models which both inherit (abstract)
from <tt class="docutils literal">AbstractGeneralInfo</tt> have a <tt class="docutils literal">created_by</tt> and
a <tt class="docutils literal">modified_by</tt> field. These fields have to be filled automatically from the current logged in user. Now, there are various
options to do that but what I vote for is using an <tt class="docutils literal">AuditableMixin</tt> as I have already described in <a class="reference external" href="https://spapas.github.io/2015/01/21/django-model-auditing/#adding-simple-auditing-functionality-ourselves">my Django model auditing article</a>.</p>
<p>To replicate the functionality we’ll create an <tt class="docutils literal">AuditableMixin</tt> like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AuditableMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,):</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">,</span> <span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">created_by</span><span class="p">:</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">created_by</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">modified_by</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>This mixin can be used by both the create and update view of both <tt class="docutils literal">Article</tt> and <tt class="docutils literal">Document</tt>. So all four of these
classes will share the same functionality. Notice that the <tt class="docutils literal">form_valid</tt> method is overridden - the <tt class="docutils literal">created_by</tt>
of the form’s instance (which is the object that was edited, remember how <tt class="docutils literal">ModelFormMixin</tt> works) will by set
to the current user if it is null (so it will be only set once) while the <tt class="docutils literal">modified_by</tt> will be set always to the
current user. Finally we call <tt class="docutils literal"><span class="pre">super().form_valid</span></tt> and return its response so
that the form will be actually saved and the redirect will go to the proper success url. To use it for example for the
<tt class="docutils literal">Article</tt>, <tt class="docutils literal">CreateView</tt> should be defined like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ArticleCreateView</span><span class="p">(</span><span class="n">AuditableMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
</pre></div>
</div>
<div class="section" id="change-the-queryset-of-the-cbv">
<h3>Change the queryset of the <span class="caps">CBV</span></h3>
<p>All CBVs that inherit from <tt class="docutils literal">SingleObjectMixin</tt> or <tt class="docutils literal">MultipleObjectMixin</tt> (<tt class="docutils literal">ListView</tt>, <tt class="docutils literal">DetailView</tt>, <tt class="docutils literal">UpdateView</tt> and <tt class="docutils literal">DeleteView</tt>)
have a <tt class="docutils literal">model</tt> and a <tt class="docutils literal">queryset</tt> property that can be used (either one or the other) to define the queryset that will be used for
querying the database for that CBVs results. This queryset can be further dynamically refined by overriding the <tt class="docutils literal">get_queryset()</tt> method.
What I usually do is that I define the <tt class="docutils literal">model</tt> attribute and then override <tt class="docutils literal">get_querset</tt> in order to dynamically modify the queryset.</p>
<p>For example, let’s say that I wanted to add a count of articles and documents per each category. Here’s how the <tt class="docutils literal">CategoryListView</tt> could be done:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">CategoryListView</span><span class="p">(</span><span class="n">ExportCsvMixin</span><span class="p">,</span> <span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Category</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'categories'</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">return</span> <span class="n">qs</span><span class="o">.</span><span class="n">annotate</span><span class="p">(</span><span class="n">article_cnt</span><span class="o">=</span><span class="n">Count</span><span class="p">(</span><span class="s1">'article'</span><span class="p">),</span> <span class="n">document_cnt</span><span class="o">=</span><span class="n">Count</span><span class="p">(</span><span class="s1">'document'</span><span class="p">))</span>
</pre></div>
<p>Notice that I also use some more mixins for this <tt class="docutils literal">ListView</tt> (they’ll be explained later). The <tt class="docutils literal">get_queryset</tt> adds
the annotation to the <tt class="docutils literal">super()</tt> queryset (which will be <tt class="docutils literal">Category.objects.all()</tt>). One final comment is that instead
of this, I could have more or less the same functionality by implementing <tt class="docutils literal">CategoryListView</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">CategoryListView</span><span class="p">(</span><span class="n">ExportCsvMixin</span><span class="p">,</span> <span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'categories'</span>
<span class="n">query</span> <span class="o">=</span> <span class="n">Category</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">annotate</span><span class="p">(</span><span class="n">article_cnt</span><span class="o">=</span><span class="n">Count</span><span class="p">(</span><span class="s1">'article'</span><span class="p">),</span> <span class="n">document_cnt</span><span class="o">=</span><span class="n">Count</span><span class="p">(</span><span class="s1">'document'</span><span class="p">))</span>
</pre></div>
<p>This has the same functionality (return all categories with the number of articles and documents for each one) and
saves some typing from overriding the <tt class="docutils literal">get_queryset</tt> method. However as I said most of the time I use the model
attribute and override the <tt class="docutils literal">get_queryset</tt> method because it seems more explicit and descriptive to me and most of
the time I’ll need to add some more filtering (based on the current user, based on some query parameter etc) that
can only be implemented on the <tt class="docutils literal">get_queryset</tt>.</p>
</div>
<div class="section" id="allow-each-user-to-list-view-edit-delete-only-his-own-items">
<h3>Allow each user to list/view/edit/delete only his own items</h3>
<p>Continuing from the previous example of modifying the queryset, let’s suppose that we want to allow each
user to be able to list the items (articles and
documents) he has created and view/edit/delete them. We also want to allow admins and publishers to view/edit everything.</p>
<p>Since the <tt class="docutils literal">Article</tt> and <tt class="docutils literal">Document</tt> models both have an <tt class="docutils literal">owned_by</tt> element we can use use this to filter
the results returned by <tt class="docutils literal">get_queryset()</tt>. For example, here’s a mixin that checks if the current user is
admin or publisher. If he is a publisher then he will just return the <tt class="docutils literal">super()</tt> queryset. If however he is a simple
user it will return only the results that are owned by him with <tt class="docutils literal">qs.filter(owned_by=self.request.user)</tt>.</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">LimitAccessMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="s1">'djangocbv.admin_access'</span><span class="p">)</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="s1">'djangocbv.publisher_access'</span><span class="p">)</span> <span class="p">:</span>
<span class="k">return</span> <span class="n">qs</span>
<span class="k">return</span> <span class="n">qs</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">owned_by</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">)</span>
</pre></div>
<p>Another similar mixin that is used is the <tt class="docutils literal">HideRemovedMixin</tt> that, for simple users, excludes from the queryset the objects that
are removed:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">HideRemovedMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="s1">'djangocbv.admin_access'</span><span class="p">)</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="s1">'djangocbv.publisher_access'</span><span class="p">):</span>
<span class="k">return</span> <span class="n">qs</span>
<span class="k">return</span> <span class="n">qs</span><span class="o">.</span><span class="n">exclude</span><span class="p">(</span><span class="n">status</span><span class="o">=</span><span class="s1">'REMOVED'</span><span class="p">)</span>
</pre></div>
<p>One thing that needs a little discussion is that for both of these mixins I am using <tt class="docutils literal">get_queryset</tt> to implement access control to
allow using the same mixin for views that inherit from both <tt class="docutils literal">SingleObjectMixin</tt> and <tt class="docutils literal">MultipleObjectMixin</tt> (since the <tt class="docutils literal">get_queryset</tt> is
used in both of them). This
means that when a user tries to access an object that has not access to he’ll get a nice 404 error.</p>
<p>Beyond this, instead of filtering the queryset,
for views inheriting from <tt class="docutils literal">SingleObjectMixin</tt> (i.e <tt class="docutils literal">DetailView</tt>, <tt class="docutils literal">UpdateView</tt> and <tt class="docutils literal">DeleteView</tt>)
we could have overridden the <tt class="docutils literal">get_object</tt> method to raise an access denied exception. Here’s how <tt class="docutils literal">get_object</tt> could be
overridden to raise a 403 Forbidden status when a user tries to access an object that does not belong to him:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.core.exceptions</span> <span class="kn">import</span> <span class="n">PermissionDenied</span>
<span class="k">def</span> <span class="nf">get_object</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="n">obj</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="k">if</span> <span class="n">obj</span><span class="o">.</span><span class="n">owned_by</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">PermissionDenied</span>
<span class="k">return</span> <span class="n">obj</span>
</pre></div>
</div>
<div class="section" id="configure-the-form-s-initial-values-from-get-parameters">
<h3>Configure the form’s initial values from <span class="caps">GET</span> parameters</h3>
<p>Sometimes we want to have a <tt class="docutils literal">CreateView</tt> with some fields already filled. I usually
implement this by passing the proper parameters to the <span class="caps">URL</span> (i.e by calling it as /create_view?category_id=2)
and then using the following mixin to override the <tt class="docutils literal">FormMixin</tt> <tt class="docutils literal">get_initial</tt> method in order to
return the form’s initial data from it:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">SetInitialMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,):</span>
<span class="k">def</span> <span class="nf">get_initial</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">initial</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">SetInitialMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_initial</span><span class="p">()</span>
<span class="n">initial</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">dict</span><span class="p">())</span>
<span class="k">return</span> <span class="n">initial</span>
</pre></div>
<p>So if the /article_create url can be used to initialte the <tt class="docutils literal">CreateView</tt> for the article,
using <tt class="docutils literal"><span class="pre">/article_create?category_id=3</span></tt> will show the CreateView with the Category with id=3
pre-selected in the category field!</p>
</div>
<div class="section" id="pass-extra-kwargs-to-the-formview-form">
<h3>Pass extra kwargs to the FormView form</h3>
<p>This is a very common requirement. The form may need to be modified by an external condition,
for example the current user or something that can be calculated from the view. Here’s a
sample mixin that passes the current request (which also includes the user) to the form:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">RequestArgMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_form_kwargs</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">kwargs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">RequestArgMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_form_kwargs</span><span class="p">()</span>
<span class="n">kwargs</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="s1">'request'</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">})</span>
<span class="k">return</span> <span class="n">kwargs</span>
</pre></div>
<p>Please notice that the form has to properly handle the extra kwarg in its constructor,
before calling the super’s constructor. For
example, here’s how a form that can accept the request could be implemented:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">RequestForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s1">'request'</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>We use <tt class="docutils literal">pop</tt> to remove the request from the received <tt class="docutils literal">kwargs</tt> and only then we call the
parent constructor.</p>
</div>
<div class="section" id="add-values-to-the-context">
<h3>Add values to the context</h3>
<p>To add values to the context of a <span class="caps">CBV</span> we override the <tt class="docutils literal">get_context_data()</tt> method. Here’s
a mixin that adds a list of categories to all CBVs using it:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">CategoriesContextMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'categories'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Category</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
<p>Notice that the mixin calls super to get the context data of its ancestors and appends to it. This
mean that if we also had a mixin that f.e added the current logged in user to the context (this isn’t really
needed since there’s a context processor for this but anyway) then when a <span class="caps">CBV</span> inherited from both of
them then the data of both of them would be added to the context.</p>
<p>As a general comment there are three other methods the same functionality could be achieved:</p>
<ul class="simple">
<li>Just override the <tt class="docutils literal">get_context_data</tt> of the <span class="caps">CBV</span> you want to add extra data to its context</li>
<li>Add a template tag that will bring the needed data to the template</li>
<li>Use a context processor to bring the data to all templates</li>
</ul>
<p>As can be understood, each of the above methods has certain advantages and disadvantages. For
example, if the extra data will query the database then the context processor method will add
one extra query for all page loads (even if the data is not needed). On the other hand,
the template tag will query the database only on specific views but it makes debugging and
reasoning about your template more difficult since if you have a lot of template tags you’ll have
various context variables appearing from thing air!</p>
<p>One final comment is that overriding the <tt class="docutils literal">get_context_data</tt> method will probably be the most
common thing you’re going to do when using CBVs (you’ll definitely need to add things to the context)
so try to remember the following 3 needed lines:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="c1"># ... here we add stuff to the ctx</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
</div>
<div class="section" id="add-a-simple-filter-to-a-listview">
<h3>Add a simple filter to a ListView</h3>
<p>For filtering I recommend using the excellent <a class="reference external" href="https://github.com/carltongibson/django-filter">django-filter</a> package as I’ve already
presented in <a class="reference external" href="https://spapas.github.io/2017/10/11/essential-django-packages/">my essential Django package list</a>. Here’s how a mixin can be created that
adds a filter to the context:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AddFilterMixin</span><span class="p">:</span>
<span class="n">filter_class</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">filter_class</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="s2">"Please define filter_class when using AddFilterMixin"</span><span class="p">)</span>
<span class="nb">filter</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">filter_class</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">,</span> <span class="n">queryset</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">())</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'filter'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">filter</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">context_object_name</span><span class="p">:</span>
<span class="n">ctx</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">context_object_name</span><span class="p">]</span> <span class="o">=</span> <span class="nb">filter</span><span class="o">.</span><span class="n">qs</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
<p>Notice that the <tt class="docutils literal">get_context_data</tt> checks to see if the <tt class="docutils literal">filter_class</tt> attribute has been
defined (if not it will raise a useful explanation). It will then instantiate the filter class
passing it the <tt class="docutils literal">self.request.<span class="caps">GET</span></tt> and the current queryset (<tt class="docutils literal">self.get_queryset()</tt>) - so for
example any extra filtering you are doing to the queryset (for example only show content owned by the
current user) will be also used. Finally, pass the filter to the context and assign the
contect_object_name to the filtered queryset.</p>
<p>Here’s for example how this mixin is used for <tt class="docutils literal">ArticleListView</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ArticleListView</span><span class="p">(</span><span class="n">AddFilterMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'articles'</span>
<span class="n">filter_class</span> <span class="o">=</span> <span class="n">ArticleFilter</span>
</pre></div>
<p>And then just add the following to the <tt class="docutils literal">article_list.html</tt> template:</p>
<div class="highlight"><pre><span></span><span class="o"><</span><span class="n">form</span> <span class="n">method</span><span class="o">=</span><span class="s1">'GET'</span><span class="o">></span>
<span class="p">{{</span> <span class="nb">filter</span><span class="o">.</span><span class="n">form</span> <span class="p">}}</span>
<span class="o"><</span><span class="nb">input</span> <span class="nb">type</span><span class="o">=</span><span class="s1">'submit'</span> <span class="n">value</span><span class="o">=</span><span class="s1">'Filter'</span> <span class="o">/></span>
<span class="o"></</span><span class="n">form</span><span class="o">></span>
<span class="p">{</span><span class="o">%</span> <span class="k">for</span> <span class="n">article</span> <span class="ow">in</span> <span class="n">articles</span> <span class="o">%</span><span class="p">}</span>
<span class="n">Display</span> <span class="n">article</span> <span class="n">info</span> <span class="o">-</span> <span class="n">only</span> <span class="n">filtered</span> <span class="n">articles</span> <span class="n">will</span> <span class="n">be</span> <span class="n">here</span>
<span class="p">{</span><span class="o">%</span> <span class="n">endfor</span> <span class="o">%</span><span class="p">}</span>
</pre></div>
</div>
<div class="section" id="support-for-success-messages">
<h3>Support for success messages</h3>
<p>Django has a very useful <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/contrib/messages/">messages framework</a> which can be used to add flash messages
to a view. A flash message is a message that persists in the sesion until it is viewed
by the user. So, for example when a user edits an object and saves it, he’ll be redirected
to the success page - if you have configured a flash message to inform the user that the
save was ok then he’ll see this message once and then if he reloads the page it will
be removed.</p>
<p>Here’s a mixin that can be used to support flash messages using Django’s message framework:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">SuccessMessageMixin</span><span class="p">:</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">''</span>
<span class="k">def</span> <span class="nf">get_success_message</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">success_message</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">messages</span><span class="o">.</span><span class="n">success</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_success_message</span><span class="p">())</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>This mixin overrides the <tt class="docutils literal">form_valid</tt> and adds the message using <tt class="docutils literal">get_success_message</tt> - this
can be overriden if you want to have a dynamic message or just set the <tt class="docutils literal">success_message</tt> attribute
for a static message, for example something like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">SuccesMessageArticleCreateView</span><span class="p">(</span><span class="n">SuccessMessageMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Object was created!'</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
</pre></div>
<p>I’d like to once again point out here that since the <tt class="docutils literal"><span class="pre">super().form_valid(form)</span></tt> method is properly used
then if a <span class="caps">CBV</span> uses multiple mixins that override form_valid (for example if your <span class="caps">CBV</span> overrides both
<tt class="docutils literal">SuccessMessageMixin</tt> and <tt class="docutils literal">AuditableMixin</tt> then the form_valid of <em>both</em> will be called so you’ll
get both the created_by/modified_by values set to the current user and the success message!</p>
<p>Notice that Django actually provides an implementation of <a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/contrib/messages/#adding-messages-in-class-based-views">a message mixin</a> which can be used instead
of the proposed implementation here (I didn’t know it until recently that’s why I am using this to some
projects and I also present it here).</p>
</div>
<div class="section" id="implement-a-quick-moderation">
<h3>Implement a quick moderation</h3>
<p>It is easy to implement some moderation to our model publishing. For example, let’s suppose that we only
allow publishers to publish a model. Here’s how it can be done:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="k">if</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">status</span> <span class="o">!=</span> <span class="s1">'REMOVED'</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="s1">'djangocbv.publisher_access'</span><span class="p">):</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'PUBLISHED'</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'DRAFT'</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>So, first of all we make sure that the object is not <tt class="docutils literal"><span class="caps">REMOVED</span></tt> (if it is
remove it we don’t do anything else). Next we check if the current user has
<tt class="docutils literal">publisher_access</tt> if yes we change the object’s status to <tt class="docutils literal"><span class="caps">PUBLISHED</span></tt> - on any
other case we change its status to <tt class="docutils literal"><span class="caps">DRAFT</span></tt>. Notice that this means that whenever a
publisher saves the object it will be published and whenever a non-publisher saves it
it will be made a draft. We then call our ancestor’s <tt class="docutils literal">form_valid</tt> to save the object
and return to success url.</p>
<p>I’d like to repeat here that this mixin, since it calls super, can work concurrently
with any other mixins that override <tt class="docutils literal">form_valid</tt> (and also call their super method
of course), for example it can be used together with the audit (auto-fill created_by
and moderated_by) and the success mixin we defined previously!</p>
</div>
<div class="section" id="allow-access-to-a-view-if-a-user-has-one-out-of-a-group-of-permissions">
<h3>Allow access to a view if a user has one out of a group of permissions</h3>
<p>For this we’ll need to use the authentication mixins functionality. We could implement
this by overriding <tt class="docutils literal">PermissionRequiredMixin</tt> or by overriding <tt class="docutils literal">UserPassesTestMixin</tt>.</p>
<p>Using <tt class="docutils literal">PermissionRequiredMixin</tt> is not very easy because the way it works
it will allow access if the user has <em>all</em> permissions from the group (not only one as is the requirement).
Of course you could override its <tt class="docutils literal">has_permission</tt> method to change the way it checks if
the user has the permissions (i.e make sure it has one permission instead of all):</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AnyPermissionRequiredMixin</span><span class="p">(</span><span class="n">PermissionRequiredMixin</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">has_permission</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">perms</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_permission_required</span><span class="p">()</span>
<span class="k">return</span> <span class="nb">any</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="n">perm</span><span class="p">)</span> <span class="k">for</span> <span class="n">perm</span> <span class="ow">in</span> <span class="n">perms</span><span class="p">)</span>
</pre></div>
<p>Also we could implement our mixin using <tt class="docutils literal">UserPassesTestMixin</tt> as its base:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AnyPermissionRequiredAlternativeMixin</span><span class="p">(</span><span class="n">UserPassesTestMixin</span><span class="p">):</span>
<span class="n">permissions</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">test_func</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">any</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">has_perm</span><span class="p">(</span><span class="n">perm</span><span class="p">)</span> <span class="k">for</span> <span class="n">perm</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">permissions</span><span class="p">)</span>
</pre></div>
<p>The functionality is very simple: If the user has one of the list of the configured permissions then the test will pass (so he’ll have access to the view).
If instead the user has none of the permissions then he won’t be able to access the view.</p>
<p>Notice that for the above implementations we inherited from <tt class="docutils literal">PermissionRequiredMixin</tt> or <tt class="docutils literal">UserPassesTextMixin</tt> to keep their functionality - if we had inherited
these mixins from object then we’d need to inherit our CBVs from both <tt class="docutils literal">AnyPermissionRequiredMixin</tt> and <tt class="docutils literal">PermissionRequiredMixin</tt> or
<tt class="docutils literal">AnyPermissionRequiredAlternativeMixin</tt> and <tt class="docutils literal">UserPassesTestMixin</tt> (with the correct <span class="caps">MRO</span> order of course).</p>
<p>Now, the whole permission cheking functionality can be even more <span class="caps">DRY</span>. Let’s suppose that we know that there are a couple of views which should only
be visible to users having either the <tt class="docutils literal">app.admin</tt> or <tt class="docutils literal">app.curator</tt> permission. Instead of inheriting all these views from <tt class="docutils literal">AnyPermissionRequiredMixin</tt>
and configuring the permissions list to each one, the <span class="caps">DRY</span> way to implement this is to add yet another mixin from which the CBVs will actually inhert:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AdminOrPublisherPermissionRequiredMixin</span><span class="p">(</span><span class="n">AnyPermissionRequiredMixin</span><span class="p">):</span>
<span class="n">permissions</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'djangocbv.admin_access'</span><span class="p">,</span> <span class="s1">'djangocbv.publisher_access'</span><span class="p">]</span>
</pre></div>
</div>
<div class="section" id="disable-a-view-based-on-some-condition">
<h3>Disable a view based on some condition</h3>
<p>There are times you want to disable a view based on an arbitrary condition - for example example make the view
disabled before a specific date. Here’s a simple mixin that overrides <tt class="docutils literal">dispatch</tt> to do this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DisabledDateMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="n">the_date</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">date</span><span class="p">(</span><span class="mi">2018</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="n">datetime</span><span class="o">.</span><span class="n">date</span><span class="o">.</span><span class="n">today</span><span class="p">()</span> <span class="o"><</span> <span class="n">the_date</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">PermissionDenied</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>You can even disable a view completely in case you want to keep it in your urls.py using this mixin:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DisabledDateMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">PermissionDenied</span>
</pre></div>
</div>
<div class="section" id="output-non-html-views">
<h3>Output non-html views</h3>
<p>I’ve written a whole article about this, please take a look at my <a class="reference external" href="https://spapas.github.io/2014/09/15/django-non-html-responses/">Django non-<span class="caps">HTML</span> responses</a> article.</p>
<p>Also, notice that is very easy to create a mixin that will output a view to <span class="caps">PDF</span> - I have already written
an <a class="reference external" href="https://spapas.github.io/2015/11/27/pdf-in-django/#using-a-cbv">essential guide for outputting PDFs in Django</a> so I am just going to refer you to this article for
(much more) information!</p>
<p>Finally, let’s take a look at a generic Mixin that you can use to add <span class="caps">CSV</span> exporting capabilities to a
<tt class="docutils literal">ListView</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ExportCsvMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'csv'</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'text/csv'</span><span class="p">)</span>
<span class="n">response</span><span class="p">[</span><span class="s1">'Content-Disposition'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'attachment; filename="export.csv"'</span>
<span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">writer</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="k">for</span> <span class="n">idx</span><span class="p">,</span> <span class="n">o</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">context</span><span class="p">[</span><span class="s1">'object_list'</span><span class="p">]):</span>
<span class="k">if</span> <span class="n">idx</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="c1"># Write headers</span>
<span class="n">writer</span><span class="o">.</span><span class="n">writerow</span><span class="p">(</span><span class="n">k</span> <span class="k">for</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span><span class="n">v</span><span class="p">)</span> <span class="ow">in</span> <span class="n">o</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">k</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">'_'</span><span class="p">))</span>
<span class="n">writer</span><span class="o">.</span><span class="n">writerow</span><span class="p">(</span><span class="n">v</span> <span class="k">for</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span><span class="n">v</span><span class="p">)</span> <span class="ow">in</span> <span class="n">o</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">k</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">'_'</span><span class="p">))</span>
<span class="k">return</span> <span class="n">response</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">)</span>
</pre></div>
<p>As you can see this mixin overrides the <tt class="docutils literal">render_to_response</tt> method. It will check if there’s a
<tt class="docutils literal">csv</tt> key to the <tt class="docutils literal"><span class="caps">GET</span></tt> queryset dictionary, thus the url must be called with <tt class="docutils literal"><span class="pre">?csv=true</span></tt> or something similar. You
can just add this link to your template:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">a</span> <span class="na">class</span><span class="o">=</span><span class="s">'button'</span> <span class="na">href</span><span class="o">=</span><span class="s">'?csv=true'</span><span class="p">></span>Export csv<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</pre></div>
<p>So if the view needs to be exported to <span class="caps">CSV</span>, it will create a new <tt class="docutils literal">HttpResponse</tt> object with the correct content type.
The next line will add a header that (<tt class="docutils literal"><span class="pre">Content-Disposition</span></tt>) will mark the response as an attachment and give it a default file name.
We then crate a new <tt class="docutils literal">csv.writer</tt> passing the just-created response as the place to write the csv. The <tt class="docutils literal">for</tt> loop that follows
enumerates the <tt class="docutils literal">object_list</tt> value of the context (remember that this is added by the <tt class="docutils literal">MultipleObjectMixin</tt> and contains the
result of the <tt class="docutils literal">ListView</tt>). It will then use the object’s <tt class="docutils literal">__dict__</tt> attribute to write the headers (for the first time) and then
write the values of all objects.</p>
<p>As another simple example, let’s create a quick <span class="caps">JSON</span> output mixin for our DetailViews:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">JsonDetailMixin</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'json'</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/json'</span><span class="p">)</span>
<span class="n">response</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="nb">dict</span><span class="p">(</span> <span class="p">(</span><span class="n">k</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span><span class="n">v</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">object</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">items</span><span class="p">()</span> <span class="p">)))</span>
<span class="k">return</span> <span class="n">response</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">)</span>
</pre></div>
<p>If you add this to a view inheriting from <tt class="docutils literal">DetailView</tt> and pass it the <tt class="docutils literal"><span class="pre">?json=true</span></tt> query parameter
you’ll get a <span class="caps">JSON</span> response!</p>
</div>
<div class="section" id="use-one-templateview-for-multiple-html-templates">
<h3>Use one TemplateView for multiple html templates</h3>
<p>Using a <tt class="docutils literal">TemplateView</tt> you could display an html template without much problem just by
settings the <tt class="docutils literal">template</tt> attribute of your class. What if you wanted to have a single
<tt class="docutils literal">TemplateView</tt> that would display many templates based on the query path? Simple, just
override <tt class="docutils literal">get_template_names</tt> to return a different template based on the path. For example,
using this view:</p>
<div class="highlight"><pre><span></span>class DynamicTemplateView(TemplateView):
def get_template_names(self):
what = self.kwargs['what']
return '{0}.html'.format(what)
</pre></div>
<p>You can render any template you have depending on the value of the <tt class="docutils literal">what</tt> kwarg. To allow
only specific template names you can either add a check to the above implementation (i.e that
what is <tt class="docutils literal">help</tt> or <tt class="docutils literal">about</tt>) or you may do it to the urls.py if you use a regular expression. Thus,
to only allow <tt class="docutils literal">help.html</tt> and <tt class="docutils literal">about.html</tt> to be rendered with this method add it to your urls like this:</p>
<div class="highlight"><pre><span></span>re_path(r'^show/(?P<span class="p"><</span><span class="nt">what</span><span class="p">></span>help|about)/', views.DynamicTemplateView.as_view(), name='template-show'),
</pre></div>
<p>Finally, to use it to render the <tt class="docutils literal">help.html</tt> you’ll just call it like <a href=’{% url “template-show” “help” %}’>Help</a></p>
<p>Notice that of course instead of creating the <tt class="docutils literal">DynamicTemplateView</tt> you could just dump these html files in your
static folder and return them using the static files functionality. However the extra thing that the <tt class="docutils literal">DynamicTemplateView</tt>
brings to you is that this is a full Django template thus you can use template tags, filters, your context variables, inherit
from your site-base and even override <tt class="docutils literal">get_context_data</tt> to add extra info to the template! All this is not possible with
static files!</p>
</div>
<div class="section" id="implement-a-partial-ajax-view">
<h3>Implement a partial Ajax view</h3>
<p>Overriding <tt class="docutils literal">get_template_names</tt> can also be used to create a <span class="caps">DRY</span> Ajax view
of your data! For example, let’s say that you have a <tt class="docutils literal">DetailView</tt> for one of your models that
has overriden the <tt class="docutils literal">get_template_names</tt> like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_template_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">is_ajax</span><span class="p">()</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'ajax_partial'</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">'core/partial/data_ajax.html'</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">()</span>
</pre></div>
<p>and you have also defined a normal template for classic request response viewing and an ajax template
that contains only the specific data for this instances (i.e it does not containg html body, headers, footers etc,
only a <div> with the instance’s data). Notice I’m using either the <tt class="docutils literal">is_ajax</tt> method or I directly passed <span class="caps">GET</span>
value (<tt class="docutils literal">ajax_partial</tt>) - this is needed because sometimes <tt class="docutils literal">is_ajax</tt> is not working as expected (depending on
how you’re going to do the request), also this way you can easily test the partial ajax view through your browser
by passing it <tt class="docutils literal"><span class="pre">?ajax_partia=true</span></tt>.</p>
<p>Using this technique you can create an Ajax view of your data just by requesting the DetailView through an
Ajax call and dumping the response you get to a modal dialog (for example) - no need for fancy <span class="caps">REST</span> APIs. Also as
a bonus, the classic DetailView will work normally, so you can have the Ajax view to give a summary of the instance’s
data (i.e have a subset of the info on the Ajax template) and the normal view to display everything.</p>
</div>
<div class="section" id="add-a-dynamic-filter-and-or-table-to-the-context">
<h3>Add a dynamic filter and/or table to the context</h3>
<p>If you have a lot of similar models you can add a mixin that dynamically creates tables and a filters
for these models - take a look at my <a class="reference external" href="https://spapas.github.io/2015/10/05/django-dynamic-tables-similar-models/">dynamic tables and filters for similar models</a> article!</p>
</div>
<div class="section" id="configure-forms-for-your-views">
<h3>Configure forms for your views</h3>
<p>As I’ve already explained if you are using a <tt class="docutils literal">FormView</tt> you’ll need to set a <tt class="docutils literal">form_class</tt> for
your view (needed by <tt class="docutils literal">FormMixin</tt>) while, for an Update or <tt class="docutils literal">CreateView</tt> which use the <tt class="docutils literal">ModelFormMixin</tt>
you can either set the <tt class="docutils literal"><span class="pre">form-class</span></tt> or directly configure the instance’s fields that will be displayed
to the form using the <tt class="docutils literal">fields</tt> attribute.</p>
<p>For example, let’s say that you have a rather generic <tt class="docutils literal">FormView</tt> that will display a different form
depending on the user permissions. Here’s how you could do this to return a <tt class="docutils literal">SuperForm</tt> if the
current user is a superuser and a <tt class="docutils literal">SimpleForm</tt> in other cases:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_form_class</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_superuser</span><span class="p">:</span>
<span class="k">return</span> <span class="n">SuperForm</span>
<span class="k">return</span> <span class="n">SimpleForm</span>
</pre></div>
</div>
<div class="section" id="display-a-different-form-for-create-and-update">
<h3>Display a different form for Create and Update</h3>
<p>There are various ways you can do this (for example you can just declare a different <tt class="docutils literal">form_class</tt> for your
<tt class="docutils literal">Create</tt> and <tt class="docutils literal">UpdateView</tt>) but I think that the most <span class="caps">DRY</span> one, especially if the create and update form are
similar is to pass an <tt class="docutils literal">is_create</tt> argument to the form which it will then be used to properly configure the form.</p>
<p>Thus, on your <tt class="docutils literal">CreateView</tt> you’ll add this <tt class="docutils literal">get_form_kwargs</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_form_kwargs</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">kwargs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">MyCreateView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_form_kwargs</span><span class="p">()</span>
<span class="n">kwargs</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="s1">'is_create'</span><span class="p">:</span> <span class="kc">True</span><span class="p">})</span>
<span class="k">return</span> <span class="n">kwargs</span>
</pre></div>
<p>while on your <tt class="docutils literal">UpdateView</tt> you’ll add this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_form_kwargs</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">kwargs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">MyUpdateView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_form_kwargs</span><span class="p">()</span>
<span class="n">kwargs</span><span class="o">.</span><span class="n">update</span><span class="p">({</span><span class="s1">'is_create'</span><span class="p">:</span> <span class="kc">False</span><span class="p">})</span>
<span class="k">return</span> <span class="n">kwargs</span>
</pre></div>
<p>Please notice that the form has to properly handle the extra kwarg in its constructor as I’ve already explained previously.</p>
</div>
<div class="section" id="only-allow-specific-http-methods-for-a-view">
<h3>Only allow specific <span class="caps">HTTP</span> methods for a view</h3>
<p>Let’s say that you want to create an <tt class="docutils literal">UnpublishView</tt> i.e a view that will change the status of your content
to <tt class="docutils literal"><span class="caps">DRAFT</span></tt>. Since this view will change your model instance it must be called through <tt class="docutils literal"><span class="caps">POST</span></tt>, however
you may not want to display an individual form for this view, just a button that when called will display
a client-side (Javascript) prompt and if the user clicks it it will immediately do a <tt class="docutils literal"><span class="caps">POST</span></tt> request
by submitting the form. The best way to create this is to just implement an <tt class="docutils literal">UpdateView</tt> for your model
and change its form valid to change the status to <tt class="docutils literal"><span class="caps">DRAFT</span></tt>, something like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">,</span> <span class="p">):</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">status</span> <span class="o">=</span> <span class="s1">'DRAFT'</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>Beyond this, you’ll need to add a <tt class="docutils literal">fields = []</tt> attribute to your <tt class="docutils literal">UpdateView</tt> to denote that you won’t
need to update any fields from the model (since you’ll update the status directly) and finally, to only allow
this view to be called through an http <tt class="docutils literal"><span class="caps">POST</span></tt> method add the following attribute:</p>
<div class="highlight"><pre><span></span><span class="n">http_method_names</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'post'</span><span class="p">,]</span>
</pre></div>
</div>
<div class="section" id="create-an-umbrella-view-for-multiple-models">
<h3>Create an umbrella View for multiple models</h3>
<p>Let’s say that you have a couple of models (called <tt class="docutils literal">Type1</tt> and <tt class="docutils literal">Type2</tt> that are more or less the same and
you want to quickly create a <tt class="docutils literal">ListView</tt> for both of them but you’d like to create just one <tt class="docutils literal">ListView</tt> and
separate them by their url. Here’s how it could be done:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">UmbrellaListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="n">template_name</span><span class="o">=</span><span class="s1">'umbrella_list.html'</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">=</span> <span class="n">kwargs</span><span class="p">[</span><span class="s1">'kind'</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s1">'type1'</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">queryset</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Type1</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">kind</span> <span class="o">==</span> <span class="s1">'type2'</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">queryset</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Type2</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">UmbrellaListView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>Notice that for this to work properly you must setup your urls like this:</p>
<div class="highlight"><pre><span></span><span class="o">...</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^list/(?P<kind>type1|type2)/$'</span><span class="p">,</span> <span class="n">UmbrellaListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">()</span> <span class="p">)</span> <span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s1">'umbrella_list'</span> <span class="p">),</span>
<span class="o">...</span>
</pre></div>
</div>
</div>
<div class="section" id="a-heavy-cbv-user-project">
<h2>A heavy <span class="caps">CBV</span> user project</h2>
<p>In this small chapter I’d like to present a bunch of mixins and views that I’ve defined to the
accompanying project (<a class="reference external" href="https://github.com/spapas/cbv-tutorial">https://github.com/spapas/cbv-tutorial</a>).</p>
<p>Let’s start with the mixins (I won’t show the mixins I’ve already talked about in the previous chapter):</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">SetOwnerIfNeeded</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">,</span> <span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">owned_by_id</span><span class="p">:</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">owned_by</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ChangeStatusMixin</span><span class="p">:</span>
<span class="n">new_status</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">,</span> <span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">new_status</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="s2">"Please define new_status when using ChangeStatusMixin"</span><span class="p">)</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">status</span> <span class="o">=</span> <span class="n">new_status</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ContentCreateMixin</span><span class="p">(</span><span class="n">SuccessMessageMixin</span><span class="p">,</span>
<span class="n">AuditableMixin</span><span class="p">,</span>
<span class="n">SetOwnerIfNeeded</span><span class="p">,</span>
<span class="n">RequestArgMixin</span><span class="p">,</span>
<span class="n">SetInitialMixin</span><span class="p">,</span>
<span class="n">ModerationMixin</span><span class="p">,</span>
<span class="n">LoginRequiredMixin</span><span class="p">):</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Object successfully created!'</span>
<span class="k">class</span> <span class="nc">ContentUpdateMixin</span><span class="p">(</span><span class="n">SuccessMessageMixin</span><span class="p">,</span>
<span class="n">AuditableMixin</span><span class="p">,</span>
<span class="n">SetOwnerIfNeeded</span><span class="p">,</span>
<span class="n">RequestArgMixin</span><span class="p">,</span>
<span class="n">SetInitialMixin</span><span class="p">,</span>
<span class="n">ModerationMixin</span><span class="p">,</span>
<span class="n">LimitAccessMixin</span><span class="p">,</span>
<span class="n">LoginRequiredMixin</span><span class="p">):</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Object successfully updated!'</span>
<span class="k">class</span> <span class="nc">ContentListMixin</span><span class="p">(</span><span class="n">ExportCsvMixin</span><span class="p">,</span> <span class="n">AddFilterMixin</span><span class="p">,</span> <span class="n">HideRemovedMixin</span><span class="p">,</span> <span class="p">):</span>
<span class="k">pass</span>
<span class="k">class</span> <span class="nc">ContentRemoveMixin</span><span class="p">(</span><span class="n">SuccessMessageMixin</span>
<span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span>
<span class="n">AuditableMixin</span><span class="p">,</span>
<span class="n">HideRemovedMixin</span><span class="p">,</span>
<span class="n">ChangeStatusMixin</span><span class="p">,):</span>
<span class="n">http_method_names</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'post'</span><span class="p">,]</span>
<span class="n">new_status</span> <span class="o">=</span> <span class="s1">'REMOVED'</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Object successfully removed!'</span>
<span class="k">class</span> <span class="nc">ContentUnpublishMixin</span><span class="p">(</span><span class="n">SuccessMessageMixin</span>
<span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span>
<span class="n">AuditableMixin</span><span class="p">,</span>
<span class="n">UnpublishSuccessMessageMixin</span><span class="p">,</span>
<span class="n">ChangeStatusMixin</span><span class="p">,):</span>
<span class="n">http_method_names</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'post'</span><span class="p">,]</span>
<span class="n">new_status</span> <span class="o">=</span> <span class="s1">'DRAFT'</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Object successfully unpublished!'</span>
</pre></div>
<p>The <tt class="docutils literal">SetOwnerIfNeeded</tt> and <tt class="docutils literal">ChangeStatusMixin</tt> are simple mixins that override <tt class="docutils literal">form_valid</tt> to
introduce some functionality before saving the object).</p>
<p>The mixins that follow are used to group functionality of
other mixins together and will be inherited by the views. Thus, <tt class="docutils literal">ContentCreateMixin</tt> has the mixin functionality needed to create something (for example
an <tt class="docutils literal">Article</tt> or a <tt class="docutils literal">Document</tt>) i.e show a success message, add auditing information, set the object’s owner,
pass the request to the form, set the form’s initial values, do some moderation and only allow logged in users. On
a similar fashion, the <tt class="docutils literal">ContentUpdateMixin</tt> collects the functionality needed to update something and is similar to
<tt class="docutils literal">ContentCreateMixin</tt> (with the difference that it also as the <tt class="docutils literal">LimitAccessMixin</tt> to only allow simple users to
edit their own content). The <tt class="docutils literal">ContentListMixin</tt> adds functionality for export to <span class="caps">CSV</span>, simple filter and hiding removed things.</p>
<p>Finally, the <tt class="docutils literal">ContentRemoveMixin</tt> and <tt class="docutils literal">ContentUnpublishMixin</tt> are used to implement Views for removing and unpublishing
an object. Both of them inherit from <tt class="docutils literal">ChangeStatusMixin</tt> - one setting
the <tt class="docutils literal">new_status</tt> to <tt class="docutils literal"><span class="caps">REMOVED</span></tt> the other to <tt class="docutils literal"><span class="caps">DRAFT</span></tt>.</p>
<p>Notice that they share much functionality so I could
remove both <tt class="docutils literal">ContentRemoveMixin</tt> and <tt class="docutils literal">ContentUnpublishMixin</tt> and add a single <tt class="docutils literal">ContentChangeStatusMixin</tt> like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ContentChangeStatusMixin</span><span class="p">(</span><span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span>
<span class="n">AuditableMixin</span><span class="p">,</span>
<span class="n">UnpublishSuccessMessageMixin</span><span class="p">,</span>
<span class="n">ChangeStatusMixin</span><span class="p">,):</span>
<span class="n">http_method_names</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'post'</span><span class="p">,]</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[]</span>
</pre></div>
<p>Thus the <tt class="docutils literal">new_status</tt> attribute wouldn’t be there so the views inheriting from this <tt class="docutils literal">ContentChangeStatusMixin</tt>
(i.e <tt class="docutils literal">*RemoveView</tt> and <tt class="docutils literal">*UnpublishView</tt>) would need to define the <tt class="docutils literal">new_status</tt> field themselves.
This is definitely valid (and more <span class="caps">DRY</span>) but less explicit than the way I’ve implemented this - i.e you may
wanted to not allow publishers to remove objects, only admins (so you could implement that differently in the <tt class="docutils literal">get_queryset</tt>
or <tt class="docutils literal">dispatch</tt> method of <tt class="docutils literal">ContentRemoveMixin</tt> and <tt class="docutils literal">ContentUpdateMixin</tt>) this is easier if you have both the
<tt class="docutils literal">ContentRemoveMixin</tt> and <tt class="docutils literal">ContentUnpublishMixin</tt>.</p>
<p>Now let’s take a look at the views:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">CategoryListView</span><span class="p">(</span><span class="n">ExportCsvMixin</span><span class="p">,</span> <span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Category</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'categories'</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">return</span> <span class="n">qs</span><span class="o">.</span><span class="n">annotate</span><span class="p">(</span><span class="n">article_cnt</span><span class="o">=</span><span class="n">Count</span><span class="p">(</span><span class="s1">'article'</span><span class="p">),</span> <span class="n">document_cnt</span><span class="o">=</span><span class="n">Count</span><span class="p">(</span><span class="s1">'document'</span><span class="p">))</span>
<span class="k">class</span> <span class="nc">CategoryCreateView</span><span class="p">(</span><span class="n">SuccessMessageMixin</span><span class="p">,</span> <span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Category</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">]</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Category created!'</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'category-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">CategoryUpdateView</span><span class="p">(</span><span class="n">SuccessMessageMixin</span><span class="p">,</span> <span class="n">AdminOrPublisherPermissionRequiredMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Category</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">]</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Category updated!'</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'category-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">CategoryDetailView</span><span class="p">(</span><span class="n">CategoriesContextMixin</span><span class="p">,</span> <span class="n">DetailView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Category</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'category'</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'article_number'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Article</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">category</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">object</span><span class="p">)</span><span class="o">.</span><span class="n">count</span><span class="p">()</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'document_number'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Document</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">category</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">object</span><span class="p">)</span><span class="o">.</span><span class="n">count</span><span class="p">()</span>
<span class="k">return</span> <span class="n">ctx</span>
<span class="k">class</span> <span class="nc">ArticleListView</span><span class="p">(</span><span class="n">ContentListMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'articles'</span>
<span class="n">filter_class</span> <span class="o">=</span> <span class="n">ArticleFilter</span>
<span class="k">class</span> <span class="nc">ArticleCreateView</span><span class="p">(</span><span class="n">ContentCreateMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">ArticleForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'article-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleUpdateView</span><span class="p">(</span><span class="n">ContentUpdateMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">ArticleForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'article-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleDetailView</span><span class="p">(</span><span class="n">HideRemovedMixin</span><span class="p">,</span> <span class="n">JsonDetailMixin</span><span class="p">,</span> <span class="n">DetailView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'article'</span>
<span class="k">def</span> <span class="nf">get_template_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">is_ajax</span><span class="p">()</span> <span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'partial'</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">'djangocbv/_article_content_partial.html'</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">ArticleRemoveView</span><span class="p">(</span><span class="n">ContentRemoveMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'article-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ArticleUnpublishView</span><span class="p">(</span><span class="n">ContentUnpublishMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'article-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">DocumentListView</span><span class="p">(</span><span class="n">ContentListMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Document</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'documents'</span>
<span class="n">filter_class</span> <span class="o">=</span> <span class="n">DocumentFilter</span>
<span class="k">class</span> <span class="nc">DocumentCreateView</span><span class="p">(</span><span class="n">ContentCreateMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Document</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">DocumentForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'document-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">DocumentUpdateView</span><span class="p">(</span><span class="n">ContentUpdateMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Document</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">DocumentForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'document-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">DocumentDetailView</span><span class="p">(</span><span class="n">HideRemovedMixin</span><span class="p">,</span> <span class="n">JsonDetailMixin</span><span class="p">,</span> <span class="n">DetailView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Document</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'document'</span>
<span class="k">class</span> <span class="nc">DocumentRemoveView</span><span class="p">(</span><span class="n">ContentRemoveMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Document</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'document-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">DocumentUnpublishView</span><span class="p">(</span><span class="n">ContentUnpublishMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Document</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'document-list'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">DynamicTemplateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_template_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">what</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="p">[</span><span class="s1">'what'</span><span class="p">]</span>
<span class="k">return</span> <span class="s1">'</span><span class="si">{0}</span><span class="s1">.html'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">what</span><span class="p">)</span>
</pre></div>
<p>As you will see there are</p>
<ul class="simple">
<li>4 views related to categories (Create, Detail, Update and List)</li>
<li>6 views related to articles (Create, Detail, Update, List, Unpublish and Remove) and</li>
<li>another 6 views related to documents (same as articles).</li>
</ul>
<p>The views for <tt class="docutils literal">Article</tt> and <tt class="docutils literal">Document</tt> are more or less the same: They inherit
from the corresponding mixin we defined previously (<tt class="docutils literal">CreateContentMixin</tt>, <tt class="docutils literal">UpdateContentMixin</tt> etc) add a
redirect to their corresponding list view (I am using <tt class="docutils literal">redirect_lazy</tt> there because <tt class="docutils literal">redirect</tt> wouldn’t work
because it will lead to a cyclic dependency between urls and views) and define their corresponding form and model.
For the the DetailViews I don’t use a group mixin like the others but I just add the <tt class="docutils literal">HideRemovedMixin</tt> and
<tt class="docutils literal">JsonDetailMixin</tt> directly to their ancestor list. This is to make clear that the group mixins (<tt class="docutils literal">ContentCreateMixin</tt> etc)
are optional and I could for example define <tt class="docutils literal">ArticleCreateView</tt> like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">ArticleCreateView</span><span class="p">(</span><span class="n">SuccessMessageMixin</span><span class="p">,</span>
<span class="n">AuditableMixin</span><span class="p">,</span>
<span class="n">SetOwnerIfNeeded</span><span class="p">,</span>
<span class="n">RequestArgMixin</span><span class="p">,</span>
<span class="n">SetInitialMixin</span><span class="p">,</span>
<span class="n">ModerationMixin</span><span class="p">,</span>
<span class="n">LoginRequiredMixin</span><span class="p">,</span>
<span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Article</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">ArticleForm</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s1">'article-list'</span><span class="p">)</span>
<span class="n">success_message</span> <span class="o">=</span> <span class="s1">'Object successfully created!'</span>
</pre></div>
<p>What’s the advantage of using the <tt class="docutils literal">ContentCreateMixin</tt> then? Well since the same mixins are used from <tt class="docutils literal">DocumentCreateView</tt>
I won’t need to re-define this list there. Also if for example I want to allow users that have a specific permission to create
articles and document I will remove the <tt class="docutils literal">LoginRequireMixin</tt> and add the <tt class="docutils literal">AllowCreateContentMixin</tt> only to the <tt class="docutils literal">ContentCreateMixin</tt>
and not to both <tt class="docutils literal">ArticleCreateView</tt> and <tt class="docutils literal">DocumentCreateView</tt> (and I won’t be in danger of forgetting to change it somewhere).
Of course all this depends on your requirements and how <span class="caps">DRY</span> you need to be.</p>
<p>The category related views are simpler and override their mixins directly. Finally there’s a <tt class="docutils literal">DynamicTemplateView</tt> to
display templates based on their filename as discussed previously.</p>
<p>Before continuing, please try to understand how much more <span class="caps">DRY</span> this project is when compared to using a traditional
functional one (or if not using mixins). For example, there’s a <tt class="docutils literal">RequestArgMixin</tt> that is used by all views that create/update content. If <span class="caps">INHERITANCE</span>
didn’t use that mixin I’d need to re-define the same functionality (pass the current request to the form’s constructor) to
4 views (Article/Document Create/Update). Or for the <tt class="docutils literal">AuditableMixin</tt> I’d need to remember to upgrade the created/modified by
to 8 views (Article/Document Create/Update/Unpublish/Remove)!</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>The previous discussion should convince you how much more <span class="caps">DRY</span> your views will be when using CBVs and how
much quicker will be to create your views. Also, if
you followed closely the first and second chapters you should be able to understand everything that is
needed for CBVs and be able to properly understand which method or attribute you need to override to
implement some specific functionality. Finally, the list of examples in the third chapter should help
you get started in all your <span class="caps">CBV</span> needs - if you have some specific question about CBVs or you’d like
another use case added to the list feel free to ask and I’ll try to add it!</p>
<p><em>Update 21/04/2020</em> As per commenter’s Ahmed I. Elsayed comment, there are sites with a similar
functionality with the <span class="caps">CBV</span> inspector for the Django Rest Framework (<a class="reference external" href="http://www.cdrf.co/">http://www.cdrf.co/</a>) and
Django Forms (<a class="reference external" href="https://cdf.9vo.lt/">https://cdf.9vo.lt/</a>)! They are an excellent resource for reference!!</p>
</div>
Easy downloading youtube videos and mp3s using youtube-dl and python2018-03-06T12:20:00+02:002018-03-06T12:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2018-03-06:/2018/03/06/easy-youtube-mp3-downloading/<p class="first last">Download videos (and convert them to mp3s) from youtube using python youtube-dl and ffmpeg!</p>
<p>In this article I am going to present you with an easy (and advertisement/malware free) way to download
videos from youtube, converting them to mp3 if needed. Also, I will give some more useful hints, for
example how to download multiple mp3s using a script, how to break a long mp3 to same-length parts
so you could quickly skip tracks when you play it in your stereo etc.</p>
<p>I am going to give specific instructions for Windows users - however everything I’ll propose should also be applicable
to <span class="caps">OSX</span> or Linux users with minor modifications.</p>
<p>The tools we are going to use are:</p>
<ul class="simple">
<li><a class="reference external" href="https://rg3.github.io/youtube-dl/">youtube-dl</a> which is a python library for downloading videos from youtube (and some other sites)</li>
<li><a class="reference external" href="https://www.ffmpeg.org">ffmpeg</a> which is a passepartout video/audio editing library</li>
</ul>
<div class="section" id="installation">
<h2>Installation</h2>
<p>To install youtube-dl I recommend installing it in your global python (2 or 3) package list using pip. Please read my
<a class="reference external" href="https://spapas.github.io/2017/12/20/python-2-3-windows/">previous</a> article</p>
<p>to see how you should install and use Python 2 or 3
on Windows. Following the instructions from there, you can do the following to install youtube-dl in your global
Python 3 packages:</p>
<pre class="code literal-block">
py -3 -m pip install youtube-dl
</pre>
<p>To run youtube-dl you’ll write something like <tt class="docutils literal">py <span class="pre">-3</span> <span class="pre">-m</span> youtube_dl url_or_video_id</tt> (notice the underscore instead of dash since
dashes are not supported in module names in python). For example try something like this <tt class="docutils literal">py <span class="pre">-3</span> <span class="pre">-m</span> youtube_dl YgtL4S7Hrwo</tt> and you’ll
be rewarded with the 2016 Pycon talk from Guido van Rossum! If you find this a little too much too type don’t be afraid, I will give
some hints later about how to improve this.</p>
<p>To upgrade your youtube-dl installation you should do something like this:</p>
<pre class="code literal-block">
py -3 -m pip install -U youtube-dl
</pre>
<p>Notice that you must frequently upgrade your youtube-dl installation because sometimes youtube changes the requirements
for viewing / downloading videos and your old version will not work. So if for some reason something is not correct when
you use youtube-dl always try updating first.</p>
<p>If you wanted you could also create a virtual environment (see instructions on previously mentioned article) and install youtube-dl locally there using <tt class="docutils literal">pip install <span class="pre">youtube-dl</span></tt>
however I prefer to install it on the global packages to be really easy for me to call it from a command prompt I open. Notice
that if you install youtube-dl in a virtualenv, after you activate that virtualenv you’ll be able to run it just by typing <tt class="docutils literal"><span class="pre">youtube-dl</span></tt>.</p>
<p>Finally, If for some reason you don’t want want to mess with python and all this (or it just seems greek to you) then you
may go on and directly download a <a class="reference external" href="https://yt-dl.org/latest/youtube-dl.exe">youtube-dl windows executable</a>. Just put it in your path and you should be good to go.</p>
<p>To install ffmpeg, I recommend <a class="reference external" href="https://ffmpeg.zeranoe.com/builds/">downloading the Windows build from here</a> (select the correct Windows architecture of your system and
always static linking - the version doesn’t matter). This will get you a zip - we are mainly interested to the three files under the
<tt class="docutils literal">bin</tt> folder of that zip which should be copied to a directory under your path:</p>
<ul class="simple">
<li>ffmpeg is the passepartout video converting toot that we are going to use</li>
<li>ffprobe will print some information about a file (about its container and video/audio streams)</li>
<li>ffplay will play the file — not really recommended there are better tools but it is invaluable
for testing; if it can be played be ffplay then ffmpeg will be able to properly read your file</li>
</ul>
<p>Notice I recommend copying things to a directory in your path. This is recommended and will save you from repeatedly typing the same things
over and over. Also, later I will propose a bunch of <span class="caps">DOS</span> batch (.bat) files that can also be copied to that directory and help you even
more in you youtube video downloading. To add a directory to the <span class="caps">PATH</span>, just press Windows+Pause Break, Advanced System Settings, Advanced,
Environment Variables, edit the “Path” User variable (for your user) and append the directory there.</p>
</div>
<div class="section" id="using-youtube-dl">
<h2>Using youtube-dl</h2>
<p>As I’ve already explained before, to run youtube-dl you’ll either write something like <tt class="docutils literal">py <span class="pre">-3</span> <span class="pre">-m</span> youtube_dl</tt> (if you’ve installed it
to your global python packages) or run youtube-dl if you’ve downloaded the pre-built exe or have installed it in a virtualenv. To save
you from some keystrokes, you can create a batch file that will run and pass any more parameters to it, something like this:</p>
<pre class="code literal-block">
py -3 -m youtube_dl %*
</pre>
<p>(the %* will capture the remaining command line) so to get the previous video just run <tt class="docutils literal">getvideo YgtL4S7Hrwo</tt>
(or getvideo <a class="reference external" href="https://www.youtube.com/watch?v=YgtL4S7Hrwo">https://www.youtube.com/watch?v=YgtL4S7Hrwo</a> - works the same with the video id or the complete url).</p>
<p>One thing I’d like to mention here is that youtube-dl works fine with playlists and even channels. For example,
to download all videos from PyCon 2017 just do this:</p>
<p><tt class="docutils literal">getvideo <span class="pre">https://www.youtube.com/channel/UCrJhliKNQ8g0qoE_zvL8eVg/feed</span></tt> and you should see something like:</p>
<pre class="code literal-block">
E:\>py -3 -m youtube_dl https://www.youtube.com/channel/UCrJhliKNQ8g0qoE_zvL8eVg/feed
[youtube:channel] UCrJhliKNQ8g0qoE_zvL8eVg: Downloading channel page
[youtube:playlist] UUrJhliKNQ8g0qoE_zvL8eVg: Downloading webpage
[download] Downloading playlist: Uploads from PyCon 2017
[youtube:playlist] UUrJhliKNQ8g0qoE_zvL8eVg: Downloading page #1
[youtube:playlist] playlist Uploads from PyCon 2017: Downloading 143 videos
[download] Downloading video 1 of 143
[youtube] AjFfsOA7AQI: Downloading webpage
[youtube] AjFfsOA7AQI: Downloading video info webpage
[youtube] AjFfsOA7AQI: Extracting video information
WARNING: Requested formats are incompatible for merge and will be merged into mkv.
[download] Destination: Final remarks and conference close - Pycon 2017-AjFfsOA7AQI.f137.mp4
[download] 2.9% of 34.49MiB at 940.52KiB/s ETA 00:36
</pre>
<p>This is gonna take some time …</p>
<p>Now, youtube-dl has <a class="reference external" href="https://github.com/rg3/youtube-dl/blob/master/README.md#options">many options</a> and can be configured with <a class="reference external" href="https://github.com/rg3/youtube-dl/blob/master/README.md#configuration">default values</a> depending on your
requirements. I won’t go into detail about these except on some things I usually use, if you
need some help feel free to ask me.</p>
<p>When you download a video, youtube-dl will try to download the best quality possible for that video,
however a video may have various different formats that can be queries by passing the option <tt class="docutils literal"><span class="pre">--list-formats</span></tt>
to ffmpeg, for example here’s the output from the previously mentioned video:</p>
<pre class="code literal-block">
E:\>getvideo YgtL4S7Hrwo --list-formats
[youtube] YgtL4S7Hrwo: Downloading webpage
[youtube] YgtL4S7Hrwo: Downloading video info webpage
[youtube] YgtL4S7Hrwo: Extracting video information
[info] Available formats for YgtL4S7Hrwo:
format code extension resolution note
249 webm audio only DASH audio 53k , opus @ 50k, 15.14MiB
250 webm audio only DASH audio 72k , opus @ 70k, 20.29MiB
171 webm audio only DASH audio 111k , vorbis@128k, 29.42MiB
140 m4a audio only DASH audio 130k , m4a_dash container, mp4a.40.2@128k, 38.38MiB
251 webm audio only DASH audio 130k , opus @160k, 36.94MiB
278 webm 256x144 144p 58k , webm container, vp9, 30fps, video only, 11.01MiB
242 webm 426x240 240p 88k , vp9, 30fps, video only, 12.40MiB
160 mp4 256x144 144p 120k , avc1.4d400c, 30fps, video only, 33.64MiB
243 webm 640x360 360p 153k , vp9, 30fps, video only, 23.48MiB
134 mp4 640x360 360p 230k , avc1.4d401e, 30fps, video only, 28.91MiB
133 mp4 426x240 240p 260k , avc1.4d4015, 30fps, video only, 74.75MiB
244 webm 854x480 480p 289k , vp9, 30fps, video only, 39.31MiB
135 mp4 854x480 480p 488k , avc1.4d401f, 30fps, video only, 56.43MiB
247 webm 1280x720 720p 945k , vp9, 30fps, video only, 102.45MiB
136 mp4 1280x720 720p 1074k , avc1.4d401f, 30fps, video only, 116.72MiB
17 3gp 176x144 small , mp4v.20.3, mp4a.40.2@ 24k
36 3gp 320x180 small , mp4v.20.3, mp4a.40.2
43 webm 640x360 medium , vp8.0, vorbis@128k
18 mp4 640x360 medium , avc1.42001E, mp4a.40.2@ 96k
22 mp4 1280x720 hd720 , avc1.64001F, mp4a.40.2@192k (best)
</pre>
<p>As you can see, each has an id and defines an extension (container) and info about its video and audio stream.
You can download a <em>specific</em> format by using the -f command line otpion. For example , to download the audio-only
format with the worst audio quality use <tt class="docutils literal"><span class="pre">C:\Users\serafeim>getvideo</span> YgtL4S7Hrwo <span class="pre">-f</span> 249</tt>. Notice that there are
formats with audio ony and other formats with vide only. To download the worst format possible (resulting in the
smallest file size of course ) you can pass the <tt class="docutils literal"><span class="pre">-f</span> worst</tt> command line (there’s also a <tt class="docutils literal"><span class="pre">-f</span> best</tt> command line
which is used by default).</p>
<p>Another thing I’d like to point out here is that you can define an <a class="reference external" href="https://github.com/rg3/youtube-dl/blob/master/README.md#output-template">output template</a> using the <tt class="docutils literal"><span class="pre">-o</span></tt> option that
will format the name of the output file of your video using the provided options. There are <a class="reference external" href="https://github.com/rg3/youtube-dl/blob/master/README.md#output-template-examples">many examples in the docs</a>
so I won’t go into any more details here.</p>
<p>Another cool option is the -a that will help you download all videos from a file. For example, if you have a file
named <tt class="docutils literal">videos.txt</tt> with the following contsnts:</p>
<pre class="code literal-block">
AjFfsOA7AQI
3dDtACSYVx0
G17E4Muylis
</pre>
<p>running <tt class="docutils literal">getvideo <span class="pre">-a</span> videos.txt <span class="pre">-f</span> worst</tt></p>
<p>will get you all three videos in their worst quality. If you don’t want to create files then you can use something
like this:</p>
<pre class="code literal-block">
for %i in (AjFfsOA7AQI 3dDtACSYVx0 G17E4Muylis) do getvideo %i -f worst
</pre>
<p>and it will run getvideo for all three files.</p>
<p>Some more options I’d like to recommend using are:</p>
<ul class="simple">
<li><tt class="docutils literal"><span class="pre">--restrict-filenames</span></tt> to avoid strange filenames</li>
<li><tt class="docutils literal"><span class="pre">--ignore-errors</span></tt> to ignore errors when download multiple files (from a playlist or a channel) - this is really
useful because if you have a play with missing items youtube-dl will stop downloading the remaining files when it
encounters the missing one</li>
</ul>
<p>If you want to always use these options you may add them to your configuration file (<tt class="docutils literal"><span class="pre">C:\Users\<user</span> <span class="pre">name>\youtube-dl.conf</span></tt>)
or to the getvideo.bat defined above i.e getvideo.bat will be:</p>
<pre class="code literal-block">
py -3 -m youtube_dl --restrict-filenames --ignore-errors %*
</pre>
</div>
<div class="section" id="extracting-mp3s">
<h2>Extracting mp3s</h2>
<p>The next step in this trip is to understand how to extract mp3s from videos that are downloaded from youtube. If you’ve
payed attention you’d know that by now you can download audio-only formats from youtube - however they are in a format
called <a class="reference external" href="https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP"><span class="caps">DASH</span></a> which most probably is <em>not</em> playable by your car stereo (<span class="caps">DASH</span> is specialized for streaming audio through <span class="caps">HTTP</span>).</p>
<p>Thus, the proper way to get mp3s is to post-process
the downloaded file using ffmpeg to convert it to mp3. This could be done manually (by doing something
<tt class="docutils literal">ffmpeg <span class="pre">-i</span> input out.mp3</tt> — ffmpeg is smart enough to know how to convert per extension)
but thankfully youtube-dl offers the <tt class="docutils literal"><span class="pre">-x</span></tt> (and friend) parameters to make this automatic. Using <tt class="docutils literal"><span class="pre">-x</span></tt> tells
youtube-dl to extract the audio from the video (notice that youtube-dl is smart enough to download one of the audio-only
formats so you don’t have ). Using -x alone may result in a different audio format (for example .ogg) so to force conversion to mp3
you should also add the <tt class="docutils literal"><span class="pre">--audio-format</span> mp3</tt> parameter. Thus, to download an mp3 you can use the following command
line (continuing from the previous examples):</p>
<pre class="code literal-block">
py -3 -m youtube_dl --restrict-filenames --ignore-errors -x --audio-format mp3 AjFfsOA7AQI
</pre>
<p>or even better, create a <tt class="docutils literal">getmp3.bat</tt> batch file that will be used to retrieve an mp3:</p>
<pre class="code literal-block">
py -3 -m youtube_dl --restrict-filenames --ignore-errors -x --audio-format mp3 %1
</pre>
<p>Please notice that also in this case youtube-dl is smart enough to download an audio-only format thus
you won’t need to select it by hand using <tt class="docutils literal"><span class="pre">-f</span></tt> to save bandwith.</p>
</div>
<div class="section" id="splitting-the-mp3-file-to-parts">
<h2>Splitting the mp3 file to parts</h2>
<p>Some people would like to split their large mp3 files to same-length segments. Of course it would be better
for the file to be split by silence to individual songs (if the file contains songs) but these methods usually
don’t work that good so I prefer the same length segments. To do that using ffmpeg you just need to add the
following parameters:</p>
<pre class="code literal-block">
ffmpeg -i input.mp3 -segment_time 180 -f segment out.%03d.mp3"
</pre>
<p>The segment time is in seconds (so each segment will be 3 minutes) while the output files will have a name like
<tt class="docutils literal">out.001.mp3, out.002.mp3</tt> etc.</p>
<p>What if you’d like to make the segmentation automatic? For this, I recommend writing a batch file with two
commands - one to download the mp3 and a second one to call ffmpeg to segment the file. Notice that you
could use the <tt class="docutils literal"><span class="pre">--postprocessor-args</span> <span class="caps">ARGS</span></tt> command line parameter to pass the required arguments to youtube-dl
so it will be done in one command however I’d like to have a little more control thus I prefer two commands (if
you decide to use <tt class="docutils literal"><span class="pre">--postprocessor-args</span> <span class="caps">ARGS</span></tt> keep in mind that args must be inside double quotes “”).</p>
<p>Since we are going to use two commands, we need to feed the output file of youtube-dl to ffmpeg and specify a
name for the ffmpeg output file-segments. The easiest way to do that is to just pass two parameters to the batch
file - one for the video to download and one for its name. Copy the following to a file named <tt class="docutils literal">getmp3seg.bat</tt>:</p>
<pre class="code literal-block">
py -3 -m youtube_dl %1 -x --audio-format mp3 --audio-quality 128k -o %2.%%(ext)s"
ffmpeg -i %2.mp3 -segment_time 180 -f segment %2.%%03d.mp3
del %2.mp3
</pre>
<p>You can then call it like this: <tt class="docutils literal">getmp3seg AjFfsOA7AQI test</tt>. The first line will download and covert
the video to mp3 and put it in a file named <tt class="docutils literal">test.mp3</tt> (the %2 is the test, the %% is used to
escape the % and the %(ext)s is the extensions - this is needed if you use something like -o %2.mp3
youtube-dl will be confused when trying to convert the file to mp3 and will not work). The 2nd line
will segment the file to 180 second seconds (notice that here also we need to escape %) and the third
line will delete the original mp3. This leaves us with the following 4 files (the video was around 10 minutes): <tt class="docutils literal">test.000.mp3,
test.001.mp3, test.002.mp3, test.003.mp3</tt>.</p>
<p>One final thing I’d like to present here is a (more complex) script that you can use to download a video
and segmentize it only if it is more than 360 seconds. For this, we will also use the <a class="reference external" href="http://ibiblio.org/mp3info/">mp3info</a> util which can
be downloaded directly from the homepage and copied to the path. So copy the following to a script named <tt class="docutils literal">getmp3seg2.bat</tt>:</p>
<pre class="code literal-block">
@echo off
IF "%2"=="" GOTO HAVE_1
py -3 -m youtube_dl %1 -x --audio-format mp3 -o %2.%%(ext)s"
FOR /f %%i IN ('mp3info -p "%%S" %2.mp3') DO SET koko=%%i
IF %koko% GTR 360 (
ECHO greater than or equal to 360
ffmpeg -i %2.mp3 -segment_time 180 -f segment %2.%%03d.mp3
del %2.mp3
) else (
ECHO less than 360
)
GOTO :eof
:HAVE_1
ECHO Please call this file with video id and title
</pre>
<p>This is a little more complex - I’ll explain it quickly: <tt class="docutils literal">@echo off</tt> is used to suppress non needed output.
The <tt class="docutils literal"><span class="caps">IF</span></tt> following makes sure that you have two parameters. The next line downloads the file and converts it to mp3. The <tt class="docutils literal"><span class="caps">FOR</span></tt>
loop is a little strange but it’s result will be to retrieve the output of <tt class="docutils literal">mp3info <span class="pre">-o</span> "%S" title.mp3</tt> (which is the
duration in seconds of that mp3) and assign it to the <tt class="docutils literal">koko</tt> variable. The next <tt class="docutils literal"><span class="caps">IF</span></tt> checks if <tt class="docutils literal">koko</tt> is greater than (<tt class="docutils literal"><span class="caps">GTR</span></tt>)
360 seconds and if yes will run the conversion code we discussed before - else it will just output that it is less than 360 seconds.</p>
<p>Finally, there’s a <tt class="docutils literal"><span class="caps">GOTO</span>: eof</tt> line to skip printing the error message when the batch is called with less than two parameters.</p>
</div>
<div class="section" id="using-youtube-dl-from-python">
<h2>Using youtube-dl from python</h2>
<p>Integrating with youtube-dl from python is easy. Of course, you could just go on and directly call the command line
however you can have more control. The most important class is <tt class="docutils literal">youtube_dl.YoutubeDL.YoutubeDL</tt>. You instantiate
an object of this class class passing the parameters you’d like and call its <tt class="docutils literal">download()</tt> instance method passing
a list of urls. Here’s a small script that downloads the input video ids:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">youtube_dl</span> <span class="kn">import</span> <span class="n">YoutubeDL</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">></span> <span class="mi">1</span><span class="p">:</span>
<span class="n">ydl_opts</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">ydl</span> <span class="o">=</span> <span class="n">YoutubeDL</span><span class="p">(</span><span class="n">ydl_opts</span><span class="p">)</span>
<span class="n">ydl</span><span class="o">.</span><span class="n">download</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:])</span>
<span class="k">else</span><span class="p">:</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Enter list of urls to download"</span><span class="p">)</span>
<span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</pre></div>
<p>Save it in a file named getvideo.py and run it like <tt class="docutils literal">py <span class="pre">-3</span> getvideo.py AjFfsOA7AQI 3dDtACSYVx0 G17E4Muylis</tt> to download all three videos!</p>
</div>
<div class="section" id="fixing-your-unicode-names">
<h2>Fixing your unicode names</h2>
<p>The last thing I’d like to talk about concerns people that want to download videos with Unicode characters in their titles (for example Greek).</p>
<p>Let’s suppose that you want to download the file vFVNOaUPRow which is piano music from a well-know greek composer. If you get it without parameters
(for example <tt class="docutils literal">py <span class="pre">-3</span> <span class="pre">-m</span> youtube_dl <span class="pre">-x</span> <span class="pre">--audio-format</span> mp3 vFVNOaUPRow</tt>)
you’ll get the following output file: <tt class="docutils literal">Ο Μάνος Χατζιδάκις. παίζει 11 κομμάτια στο <span class="pre">πιάνο-vFVNOaUPRow.f247.mp3</span></tt> (notice the greek characters) while, if
you add the <tt class="docutils literal"><span class="pre">--restrict-filenames</span></tt> I mentioned before you’ll get <tt class="docutils literal"><span class="pre">_11-vFVNOaUPRow.f247.mp3</span></tt> (notice that the greek characters have been removed
since they are not safe).</p>
<p>So if you use the <tt class="docutils literal"><span class="pre">--restrict-filenames</span></tt> parameter you’ll get an output that contains <em>only</em> the video id (and any safe characters it may find) while
if you don’t use it you’ll get the normal title of the video. However, most stereos <em>do not</em> display unicode characters properly so if I get this
file to my car I’ll see garbage and I won’t be able to identify it — I will be able to listen it but not see its name!</p>
<p>To fix that, I propose transliterating the unicode characters using the <a class="reference external" href="https://pypi.python.org/pypi/Unidecode">unidecode</a> library. Just install it using <tt class="docutils literal">pip</tt>. Then you can the following
script to rename all mp3 files in a directory to using english characters only:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">os</span><span class="o">,</span> <span class="nn">unidecode</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="k">for</span> <span class="n">file</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">listdir</span><span class="p">(</span><span class="s1">'.'</span><span class="p">):</span>
<span class="k">if</span> <span class="n">file</span><span class="o">.</span><span class="n">endswith</span><span class="p">(</span><span class="s1">'mp3'</span><span class="p">):</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Renaming </span><span class="si">{0}</span><span class="s2"> to </span><span class="si">{1}</span><span class="s2">"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">unidecode</span><span class="o">.</span><span class="n">unidecode</span><span class="p">(</span><span class="n">file</span><span class="p">)))</span>
<span class="n">os</span><span class="o">.</span><span class="n">rename</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">unidecode</span><span class="o">.</span><span class="n">unidecode</span><span class="p">(</span><span class="n">file</span><span class="p">))</span>
</pre></div>
<p>Copy this to a file named transliterate.py and run it in a directory containing mp3 files (<tt class="docutils literal">py <span class="pre">-3</span> transliterate.py</tt>) to rename them to non-unicode characters.</p>
</div>
Authentication for django-rest-framework with django-rest-auth2018-03-01T22:40:00+02:002018-03-01T22:40:00+02:00Serafeim Papastefanostag:spapas.github.io,2018-03-01:/2018/03/01/django-rest-auth/<p class="first last">How to authenticate with django-rest-auth</p>
<p><strong>Update: 25/08/2021</strong> Please notice that I’ve written
a <a class="reference external" href="https://spapas.github.io/2021/08/25/django-token-rest-auth/">a new article</a>
concerning Token authentication in django rest framework which
I recommend to read instead of this one since most info here is deprecated.</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p>Most of the times I need authentication with any <span class="caps">REST</span> APIs defined through <a class="reference external" href="http://www.django-rest-framework.org">django-rest-framework</a>
I will use <a class="reference external" href="http://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication">SessionAuthentication</a> method. This method uses the session cookie (which is set through the
normal Django login and logout views)
to check out if there’s an authenticated user and get his username. This method works only in the
same session (browser window) as the one that actually did the login but this should be enough for most cases.</p>
<p>However, sometimes instead of using the normal Django login/logout views, you’ll want
to authentication through <span class="caps">REST</span> end-points, for example for using them with SPAs (where
you don’t want to use the traditional views for authentication but through <span class="caps">REST</span> end-points) or
because you have implemented a mobile (or desktop) application that needs to authenticate with
your script.</p>
<p>There are various ways this could be done but one of the simplest is using <a class="reference external" href="https://github.com/Tivix/django-rest-auth">django-rest-auth</a>.
This project adds a number of <span class="caps">REST</span> end-points to your project that can be used for user login
and registration (and even social login when combined with <a class="reference external" href="https://github.com/pennersr/django-allauth">django-allauth</a>).
In the following I am going to write a simple tutorial on how to actually use django-rest-auth to
authenticate with django-rest-framework using the provided <span class="caps">REST</span> end points and how to call a
<span class="caps">REST</span> <span class="caps">API</span> as an authenticated user.</p>
<p>Before continuing with the tutorial, let’s take a look at what we’ll build here:</p>
<img alt="Our project" src="/images/rest-auth.gif" style="width: 640px;" />
<p>This is a single html page (styled with <a class="reference external" href="https://picturepan2.github.io/spectre/">spectre.css</a>) that checks if the user is logged in
and either displays the login or logout button (using javascript). When you click the login you’ll get a modal in which you
can enter your credentials which will be submitted through <span class="caps">REST</span> to the django-rest-auth endpoint and
depending on the response will set a javascript variable (and a corresponding session/local storage key).
Then you can use the “Test auth” button that works only on authenticated users and returns their username.
Finally, notice that after you log out the “test auth” button returns a 403 access denied.</p>
<p>If you want to play with this project yourself, you can clone it here <a class="reference external" href="https://github.com/spapas/rest_authenticate">https://github.com/spapas/rest_authenticate</a>.
Just create a venv, install requirements, create a superuser and you should be good to go!</p>
</div>
<div class="section" id="some-theory">
<h2>Some theory</h2>
<p>After you log in with Django, your authentication information is saved to the “session”_. The session is a bucket of information
that the Django application saves about your visit — to distinguish between different visitors a cookie with a unique
value named <tt class="docutils literal">sessionid</tt> will be used. So, your web browser will send this cookie with each page request thus allowing Django
to know which bucket of information is yours (and if you’ve authenticated know who are you). This is not a Django
related concept but a general one (supported by most if not all <span class="caps">HTTP</span> frameworks) and is used to add state to an otherwise
stateless medium (<span class="caps">HTTP</span>).</p>
<p>Since the <tt class="docutils literal">sessionid</tt> cookie is sent not only with traditional but also with Ajax request it can be used to authenticate
<span class="caps">REST</span> requests after you’ve logged in. This is what is used by default in django-rest-framework and as I said in the
introduction it is a very good solution for most use cases: You login to django and you can go ahead and call the <span class="caps">REST</span>
<span class="caps">API</span> through Ajax; the <tt class="docutils literal">sessionid</tt> cookie will be sent along with the request and you’ll be authenticated.</p>
<p>Now, although the session authentication is nice for using in browsers, you may need to access your <span class="caps">API</span> through a desktop
or a mobile application where, setting the cookies yourself is not the optimal solution. Also, you may have an <span class="caps">SPA</span> that needs
to access an <span class="caps">API</span> in a different domain; using <a class="reference external" href="https://stackoverflow.com/questions/3342140/cross-domain-cookies">using cookies for this is not easy</a> - if possible at all.</p>
<p>For such cases, django-rest-framework
offers a different authentication method called <tt class="docutils literal">TokenAuthentication_</tt>. Using this method, each user of the Django application
is correlated with a random string (Token) which is passed along with the request at its header thus the Django app can authenticate
the user using this token! One thing that may seem strange is that since both the session cookie and a token are
set through <span class="caps">HTTP</span> Headers why all the fuss about tokens? Why not just use the session cookie and be done with it. Well, there are
various reasons - here’s a <cite>rather extensive article</cite> explaining some. Some of the reasons are that a token can be valid forever
while the session is something ephemeral - beyond authorization information, sessions may keep various other data for a web
application and are expired after some time to save space. Also, since tokens are used for exactly this (authentication) they
are much easier to use and reason about. Finally, as I’ve already explained, sharing cookies by multiple sites is not something
you’d like to do.</p>
</div>
<div class="section" id="installation-configuration">
<h2>Installation <span class="amp">&</span> configuration</h2>
<p>To install django-rest-auth just follow <a class="reference external" href="http://django-rest-auth.readthedocs.io/en/latest/installation.html#installation">the instructions here</a> i.e just add
<tt class="docutils literal">'rest_framework', 'rest_framework.authtoken'</tt> and <tt class="docutils literal">'rest_auth'</tt> to your <cite>INSTALLED_APPS</cite> in
<tt class="docutils literal">settings.py</tt> and run migrate.</p>
<p>Since I won’t be adding any other apps to this project (no models are actually needed), I’ve added
two directories <tt class="docutils literal">static</tt> and <tt class="docutils literal">templates</tt> to put static files and templates there. This is configured
by adding the <tt class="docutils literal">'<span class="caps">DIRS</span>'</tt> attribte to <tt class="docutils literal"><span class="caps">TEMPLATES</span></tt>, like this:</p>
<div class="highlight"><pre><span></span><span class="n">TEMPLATES</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="s1">'BACKEND'</span><span class="p">:</span> <span class="s1">'django.template.backends.django.DjangoTemplates'</span><span class="p">,</span>
<span class="s1">'DIRS'</span><span class="p">:</span> <span class="p">[</span>
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s1">'templates'</span><span class="p">),</span>
<span class="p">],</span>
<span class="o">//</span> <span class="o">...</span>
</pre></div>
<p>and adding the <cite>STATICFILES_DIRS</cite> setting:</p>
<div class="highlight"><pre><span></span><span class="n">STATICFILES_DIRS</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s2">"static"</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>The remaining setting are the default as were created by <tt class="docutils literal"><span class="pre">django-admin</span> startproject</tt>.</p>
<p>I have included the the following urls to <tt class="docutils literal">urls.py</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'admin/'</span><span class="p">,</span> <span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'test_auth/'</span><span class="p">,</span> <span class="n">TestAuthView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'test_auth'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'rest-auth/logout/'</span><span class="p">,</span> <span class="n">LogoutViewEx</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'rest_logout'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">'rest-auth/login/'</span><span class="p">,</span> <span class="n">LoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'rest_login'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">path</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="n">HomeTemplateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'home'</span><span class="p">,</span> <span class="p">),</span>
<span class="p">]</span> <span class="o">+</span> <span class="n">static</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">STATIC_URL</span><span class="p">,</span> <span class="n">document_root</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">STATIC_ROOT</span><span class="p">)</span>
</pre></div>
<p>These are: The django-admin, a test_auth view (that works only for authenticated users and returns their username),
<em>a view (LogoutViewEx) that overrides the rest-auth <span class="caps">REST</span> logout-view</em> (I’ll explain why this is needed in a minute),
the rest-auth <span class="caps">REST</span> login-view, the home template view (which is the only view implemented) and finally a mapping
of your static files to the <tt class="docutils literal">STATIC_URL</tt>.</p>
</div>
<div class="section" id="the-views">
<h2>The views</h2>
<p>There are three views in this application - the <tt class="docutils literal">HomeTemplateView</tt>, the <tt class="docutils literal">TestAuthView</tt>
and the <tt class="docutils literal">LogoutViewEx</tt> view that overrides the normal <tt class="docutils literal">LogoutView</tt> of <tt class="docutils literal"><span class="pre">django-rest-auth</span></tt>.
The first one is
a simple <tt class="docutils literal">TemplateView</tt> that just
displays an html page and loads the client side code - we’ll talk about it later in the front-side section.</p>
<p>The <tt class="docutils literal">TestAuthView</tt> is implemented like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">TestAuthView</span><span class="p">(</span><span class="n">APIView</span><span class="p">):</span>
<span class="n">authentication_classes</span> <span class="o">=</span> <span class="p">(</span><span class="n">authentication</span><span class="o">.</span><span class="n">TokenAuthentication</span><span class="p">,)</span>
<span class="n">permission_classes</span> <span class="o">=</span> <span class="p">(</span><span class="n">permissions</span><span class="o">.</span><span class="n">IsAuthenticated</span><span class="p">,)</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="k">return</span> <span class="n">Response</span><span class="p">(</span><span class="s2">"Hello </span><span class="si">{0}</span><span class="s2">!"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">))</span>
</pre></div>
<p>This is very simple however I’d like to make a few comments about the above. First of all you see that
I’ve defined <tt class="docutils literal">authentication_classes</tt> and <tt class="docutils literal">permission_classes</tt>. These options define</p>
<ul class="simple">
<li>which method will be used for authenticating access to the <span class="caps">REST</span> view i.e finding out if the user
requesting access has logged in and if yes what’s his username (in our case the <tt class="docutils literal">TokenAuthentication</tt> will be used)</li>
<li>if the user is authorized (has permission) to call this <span class="caps">REST</span> view (in our case only authenticated users will be allowed)</li>
</ul>
<p>The authentication and permission clases can be set globally
in your <tt class="docutils literal">settings.py</tt> using <tt class="docutils literal"><span class="pre">REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES']</span></tt> and
<tt class="docutils literal"><span class="pre">REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES']</span></tt>
or defined per-class like this. If I wanted to have the same authentication and permission classes defined
in my <tt class="docutils literal">settings.py</tt> so I wouldn’t need to set these options per-class I’d add the following to my <tt class="docutils literal">settings.py</tt>:</p>
<div class="highlight"><pre><span></span><span class="n">REST_FRAMEWORK</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'DEFAULT_AUTHENTICATION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.authentication.TokenAuthentication'</span><span class="p">,</span>
<span class="p">),</span>
<span class="s1">'DEFAULT_PERMISSION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.permissions.IsAuthenticated'</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">}</span>
</pre></div>
<p>Finally, keep in mind that you haven’t defined these in your views or your settings, they will have the
following <a class="reference external" href="http://www.django-rest-framework.org/api-guide/settings/#default_authentication_classes">default</a> <a class="reference external" href="http://www.django-rest-framework.org/api-guide/settings/#default_permission_classes">values</a>:</p>
<div class="highlight"><pre><span></span><span class="n">REST_FRAMEWORK</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'DEFAULT_AUTHENTICATION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.authentication.SessionAuthentication'</span><span class="p">,</span>
<span class="s1">'rest_framework.authentication.BasicAuthentication'</span>
<span class="p">),</span>
<span class="s1">'DEFAULT_PERMISSION_CLASSES'</span><span class="p">:</span> <span class="p">(</span>
<span class="s1">'rest_framework.permissions.AllowAny'</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">}</span>
</pre></div>
<p>The above mean that if you don’t define authentication and permission classes anywhere then the <span class="caps">REST</span>
views will use either session authentication (i.e the user has logged in normally using
the Django login views as explained before) or basic authentication
(the request provides the credentials in the header using traditional <span class="caps">HTTP</span> Basic authentication)
and also that all users (logged in or not) will be allowed to call all APIs (this is
probably not something you want).</p>
<p>The <tt class="docutils literal">TokenAuthentication</tt> that we are using instead means that for every user there must be a valid token which will be provided
for each request he does. The tokens are normal object instances of <tt class="docutils literal">rest_framework.authtoken.models.Token</tt>
and you can take a look at them (or even add one) through the Django admin (auth token - tokens). You can also
even do whatever you normally would do to an object instance, for example:</p>
<div class="highlight"><pre><span></span><span class="o">>>></span> <span class="p">[</span> <span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">user</span><span class="p">,</span> <span class="n">x</span><span class="o">.</span><span class="n">key</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">Token</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()]</span>
<span class="p">[(</span><span class="o"><</span><span class="n">User</span><span class="p">:</span> <span class="n">root</span><span class="o">></span><span class="p">,</span> <span class="s1">'db4dcc1b9d00d1af74fb3cb41e1f9e673208485b'</span><span class="p">)]</span>
</pre></div>
<p>To authenticate with a token (using <a class="reference external" href="http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication">TokenAuthentication</a>), you must add an extra header to your request with the format
<tt class="docutils literal">Authorization: Token token</tt> for example in the previous case <tt class="docutils literal">root</tt> would add
<tt class="docutils literal">Authorization: Token db4dcc1b9d00d1af74fb3cb41e1f9e673208485b</tt>. To do this you’ll need something
client-side code which we’ll see in the next section.</p>
<p>To do it with <a class="reference external" href="https://curl.haxx.se">curl</a> you can just do something like this:</p>
<div class="highlight"><pre><span></span>curl<span class="w"> </span>http://127.0.0.1:8000/test_auth/<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization:Token db4dcc1b9d00d1af74fb3cb41e1f9e673208485b"</span>
</pre></div>
<p>Try it with a valid and invalid token and without providing a token at all and see the response each time.</p>
<p>So, django-rest-framework provides the model (Token) and the mechanism (add the extra Authentication header) for
authentication with Tokens. What it does not provide is a simple way to create/remove tokens for users: This
is where <tt class="docutils literal"><span class="pre">django-rest-auth</span></tt> comes to the rescue! Its login and logout <span class="caps">REST</span> views will automatically
create (and delete) tokens for the users that are logging in. They will also authenticate the user
normally (using sessions) - this means that if a user logs in using the login <span class="caps">REST</span> endpoint he’ll then
be logged in normally to the site and be able to access non-<span class="caps">REST</span> parts of the site (for example the django-admin).
Also, if the user logs in through the django-rest-auth <span class="caps">REST</span> end point and if you have are using <tt class="docutils literal">SessionAuthentication</tt>
to one of your views then he’ll be able to authenticate to these views <em>without</em> the need to pass the token (can
you understand why?).</p>
<p>Finally, let’s take a look at the <tt class="docutils literal">LogoutViewEx</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">LogoutViewEx</span><span class="p">(</span><span class="n">LogoutView</span><span class="p">):</span>
<span class="n">authentication_classes</span> <span class="o">=</span> <span class="p">(</span><span class="n">authentication</span><span class="o">.</span><span class="n">TokenAuthentication</span><span class="p">,)</span>
</pre></div>
<p>This class only defines the authentication_classes attribute. Is this really needed? Well, it depends on
you project. If you take a look at the source code of <tt class="docutils literal">LogoutView</tt> (<a class="reference external" href="https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/views.py#L99">https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/views.py#L99</a>)
you’ll see that it does not define <tt class="docutils literal">authentication_classes</tt>. This, as we’ve already discussed, means that it will
fall-back to whatever you have defined in the settings (or the defaults of django-rest-framework). So, if you haven’t
defined anything in the settings then you’ll get the by default the
SessionAuthentication and BasicAuthentication methods (hint: <em>not</em> the <tt class="docutils literal">TokenAuthentication</tt>). This means that you won’t be able to
logout when you pass the token (but <em>will</em> be able to logout from the web-app after you login - why?). So to make everything
crystal and be able to reason better about the behavior I specifically define the <tt class="docutils literal">LogoutViewEx</tt> to use the <tt class="docutils literal">TokenAuthentication</tt> - that’s
what you’d use if you developed a mobile or desktop app anyway.</p>
</div>
<div class="section" id="the-client-side-scripts">
<h2>The client side scripts</h2>
<p>I’ve included all client-side code to a <tt class="docutils literal">home.html</tt> template that is loaded
from the <tt class="docutils literal">HomeTemplateView</tt>. The client-side code has been implemented only with jQuery because I think
this is the library that most people are familiar with - and is really easy to be understood even if you
are not familiar with it. It more or less consists of four sections in html:</p>
<ul class="simple">
<li>A user-is-logged-in section that displays the username and the logout button</li>
<li>A user-is-not-logged-in section that displays a message and the login button</li>
<li>A test-auth section that displays a button for calling the <tt class="docutils literal">TestAuthView</tt> defined previously and outputs its response</li>
<li>The login modal</li>
</ul>
<p>Here’s the html (using spectre.css for styling):</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"container grid-lg"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>Test<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"non-logged-in"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
You have to log-in!
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'loginButton'</span><span class="p">></span>Login<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"logged-in"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
Welcome <span class="p"><</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">'span-username'</span><span class="p">></</span><span class="nt">span</span><span class="p">></span>!
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'logoutButton'</span><span class="p">></span>Logout<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"columns"</span> <span class="na">id</span><span class="o">=</span><span class="s">"test"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-3'</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'testAuthButton'</span><span class="p">></span>Test auth<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'column col-9'</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'test-auth-response'</span> <span class="p">></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal"</span> <span class="na">id</span><span class="o">=</span><span class="s">"login-modal"</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#close"</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-overlay close-modal"</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">"Close"</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-container"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-header"</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#close"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-clear float-right close-modal"</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">"Close"</span><span class="p">></</span><span class="nt">a</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-title h5"</span><span class="p">></span>Please login<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-body"</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"content"</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span><span class="p">></span>
{% csrf_token %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-label"</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-username"</span><span class="p">></span>Username<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-input"</span> <span class="na">type</span><span class="o">=</span><span class="s">"text"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-username"</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">"Name"</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-label"</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-password"</span><span class="p">></span>Password<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-input"</span> <span class="na">type</span><span class="o">=</span><span class="s">"password"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-password"</span> <span class="na">placeholder</span><span class="o">=</span><span class="s">"Password"</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-group"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-checkbox"</span> <span class="na">for</span><span class="o">=</span><span class="s">"input-local-storage"</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"checkbox"</span> <span class="na">id</span><span class="o">=</span><span class="s">"input-local-storage"</span> <span class="p">/></span> <span class="p"><</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">"form-icon"</span><span class="p">></</span><span class="nt">i</span><span class="p">></span> Use local storage (remember me)
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">'label label-error mt-1 d-invisible'</span> <span class="na">id</span><span class="o">=</span><span class="s">'modal-error'</span><span class="p">></span>
Unable to login!
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"modal-footer"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-primary"</span> <span class="na">id</span><span class="o">=</span><span class="s">'loginOkButton'</span> <span class="p">></span>Ok<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"#close"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn close-modal"</span> <span class="p">></span>Close<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>The html is very simple and I don’t think I need to explain much - notice that the <cite>#logged-in</cite> and
<cite>#non-logged-in</cite> sections are mutually exclusive (I use <tt class="docutils literal">$.show()</tt> and <tt class="docutils literal">$.hide()</tt> to show and hide them) but the <cite>#test</cite> section is always displayed
so you’ll be able to call the test <span class="caps">REST</span> <span class="caps">API</span> when you are and are not authenticated. For the modal
to be displayed you need to add an <tt class="docutils literal">active</tt> class to its <tt class="docutils literal">#modal</tt> container.</p>
<p>For the javascript, let’s take a look at some initialization stuff:</p>
<div class="highlight"><pre><span></span><span class="kd">var</span><span class="w"> </span><span class="nx">g_urls</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="s1">'login'</span><span class="o">:</span><span class="w"> </span><span class="s1">'{% url "rest_login" %}'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'logout'</span><span class="o">:</span><span class="w"> </span><span class="s1">'{% url "rest_logout" %}'</span><span class="p">,</span>
<span class="w"> </span><span class="s1">'test_auth'</span><span class="o">:</span><span class="w"> </span><span class="s1">'{% url "test_auth" %}'</span><span class="p">,</span>
<span class="p">};</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">getCookie</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">cookieValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">cookie</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">cookie</span><span class="w"> </span><span class="o">!==</span><span class="w"> </span><span class="s1">''</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">cookies</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">cookie</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s1">';'</span><span class="p">);</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="w"> </span><span class="o"><</span><span class="w"> </span><span class="nx">cookies</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="w"> </span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">cookie</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">jQuery</span><span class="p">.</span><span class="nx">trim</span><span class="p">(</span><span class="nx">cookies</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
<span class="w"> </span><span class="c1">// Does this cookie string begin with the name we want?</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">cookie</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">name</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="p">(</span><span class="nx">name</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">'='</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">cookieValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">cookie</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="nx">name</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">));</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">cookieValue</span><span class="p">;</span>
<span class="p">};</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">g_csrftoken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">getCookie</span><span class="p">(</span><span class="s1">'csrftoken'</span><span class="p">);</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">initLogin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#non-logged-in'</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#logged-in'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#span-username'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">username</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">remember_me</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">,</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">,</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#non-logged-in'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#logged-in'</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#span-username'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s1">''</span><span class="p">);</span>
<span class="w"> </span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="w"> </span><span class="nx">sessionStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s2">"auth"</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">""</span><span class="p">);</span>
<span class="p">};</span>
</pre></div>
<p>First of all, I define a <tt class="docutils literal">g_urls</tt> window/global object that will keep the required <span class="caps">REST</span> <span class="caps">URLS</span> (login/logout and test auth). These
are retrieved from Django using the <tt class="docutils literal">{% url %}</tt> template tag and are not hard-coded.
After that, I check to see if the user has authenticated before. Notice that because
this is client-side code, I need to do that every time the page loads or else the <span class="caps">JS</span> won’t be initialized properly! The user login
information is stored to an object named <tt class="docutils literal">g_auth</tt> and contains two attributes: <tt class="docutils literal">username</tt>, <tt class="docutils literal">key</tt> (token) and <tt class="docutils literal">remember_me</tt>.</p>
<p>To keep the login information I use either a key named <tt class="docutils literal">auth</tt> to either the <tt class="docutils literal">localStorage</tt> or the <tt class="docutils literal">sessionStorage</tt>. The <tt class="docutils literal">sessionStorage</tt> is used to save
info for the current browser tab (<em>not</em> window) while the <tt class="docutils literal">localStorage</tt> saves info for ever (until somebody deletes it). Thus,
<tt class="docutils literal">localStorage</tt> can be used for implementing a “remember me” functionality. Notice that instead of using the session/local storage
I could instead integrate the user login information with the Django back-end. To do this I’d need to see if the current user has
a session login and if yes pass his username and token to Javascript. These values would then be read by the login initialization
code. I’m leaving this as an exercise for attentive readers.</p>
<p>Getting the login information from the session probably is a better solution for web-apps however I think that using the local or session storage emulate better a more
general (and completely stateless) behaviour especially considering that the <span class="caps">API</span> may be used for mobible/desktop apps.</p>
<p>In any case, after you’ve initialized the <tt class="docutils literal">g_auth</tt> object you’ll need to read the <span class="caps">CSRF</span> cookie. By default Django requires
<a class="reference external" href="https://docs.djangoproject.com/en/2.0/ref/csrf/"><span class="caps">CSRF</span> protection</a> for all <tt class="docutils literal"><span class="caps">POST</span></tt> requests (we do a <span class="caps">POST</span> request for login and logout). What happens here is that for pages that may
need to do a <tt class="docutils literal"><span class="caps">POST</span></tt> request, Django will set a cookie (<span class="caps">CSRF</span> cookie) in its initial response. You’ll need to read that cookie and submit its
value along with the rest of your form fields when you do the <span class="caps">POST</span>. So the <tt class="docutils literal">getCookie</tt> function is just used to set the <tt class="docutils literal">g_csrftoken</tt> with the value
of the <span class="caps">CSRF</span> cookie.</p>
<p>The final function we define here (which is called a little later) checks to see if there is login information and hides/displays the correct
things in html. It will also set the local or session storage (depending on remember me value).</p>
<p>After that, we have some client side code that is inside the <tt class="docutils literal">$()</tt> function which will be called after the page has completely loaded:</p>
<div class="highlight"><pre><span></span><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">initLogin</span><span class="p">();</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#loginButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#login-modal'</span><span class="p">).</span><span class="nx">addClass</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'.close-modal'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#login-modal'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#testAuthButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="nx">g_urls</span><span class="p">.</span><span class="nx">test_auth</span><span class="p">,</span>
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">"GET"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">beforeSend</span><span class="o">:</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Token "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">"<span class='label label-success'>Ok! Response: "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#test-auth-response'</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="s2">"<span class='label label-error'>Fail! Response: "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">responseText</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" (status: "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span><span class="o">+</span><span class="s2">")</span>"</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="c1">// continuing below ...</span>
</pre></div>
<p>The first thing happening here is to call the <tt class="docutils literal">initLogin</tt> function to properly intiialize the page and then we add a couple of
handlers to the click buttons of the <tt class="docutils literal">#loginButton</tt> (which just displays the modal by adding the <tt class="docutils literal">active</tt> class ),
<tt class="docutils literal"><span class="pre">.close-modal</span></tt> class (there are multiple
ways to close the modal thus I use a class which just removes that <tt class="docutils literal">active</tt> class) and finally to the <tt class="docutils literal">#testAuthButton</tt>. This
button will do a <tt class="docutils literal"><span class="caps">GET</span></tt> request to the <tt class="docutils literal">g_urls.test_auth</tt> we defined before. The important thing to notice here is that we add
a <tt class="docutils literal">beforeSend</tt> attribute to the <tt class="docutils literal">$.ajax</tt> request which, if <tt class="docutils literal">g_auth</tt> is defined adds an <tt class="docutils literal">Authorization</tt> header with the token
in the form that django-rest-framework <tt class="docutils literal">TokenAuthentication</tt> expects and as we’ve already discussed above:</p>
<div class="highlight"><pre><span></span><span class="nx">beforeSend</span><span class="o">:</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">g_auth</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Token "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>If this ajax call returns ok (<tt class="docutils literal">done</tt> part) we just add the <tt class="docutils literal">Ok</tt> to a green label else if there’s an error (<tt class="docutils literal">fail</tt> part)
we add the response text and status to a red label. You can try clicking the button and you see that only if you’ve logged in
you will succeed in this call.</p>
<p>Let’s now take a look at the <tt class="docutils literal">#loginOkbutton</tt> click handler (inside the modal):</p>
<div class="highlight"><pre><span></span><span class="nx">$</span><span class="p">(</span><span class="s1">'#loginOkButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#input-username'</span><span class="p">).</span><span class="nx">val</span><span class="p">();</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">password</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#input-password'</span><span class="p">).</span><span class="nx">val</span><span class="p">();</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">remember_me</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#input-local-storage'</span><span class="p">).</span><span class="nx">prop</span><span class="p">(</span><span class="s1">'checked'</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">username</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">password</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Will try to login with "</span><span class="p">,</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span><span class="w"> </span><span class="nx">password</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#modal-error'</span><span class="p">).</span><span class="nx">addClass</span><span class="p">(</span><span class="s1">'d-invisible'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="nx">g_urls</span><span class="p">.</span><span class="nx">login</span><span class="p">,</span>
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">"POST"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">username</span><span class="o">:</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span>
<span class="w"> </span><span class="nx">password</span><span class="o">:</span><span class="w"> </span><span class="nx">password</span><span class="p">,</span>
<span class="w"> </span><span class="nx">csrfmiddlewaretoken</span><span class="o">:</span><span class="w"> </span><span class="nx">g_csrftoken</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"DONE: "</span><span class="p">,</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">username</span><span class="o">:</span><span class="w"> </span><span class="nx">username</span><span class="p">,</span>
<span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="nx">data</span><span class="p">.</span><span class="nx">key</span><span class="p">,</span>
<span class="w"> </span><span class="nx">remember_me</span><span class="o">:</span><span class="w"> </span><span class="nx">remember_me</span>
<span class="w"> </span><span class="p">};</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#login-modal'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'active'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">initLogin</span><span class="p">();</span>
<span class="w"> </span><span class="c1">// CAREFUL! csrf token is rotated after login: https://docs.djangoproject.com/en/1.7/releases/1.5.2/#bugfixes</span>
<span class="w"> </span><span class="nx">g_csrftoken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">getCookie</span><span class="p">(</span><span class="s1">'csrftoken'</span><span class="p">);</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"FAIL"</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#modal-error'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'d-invisible'</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#modal-error'</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">'d-invisible'</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">});</span>
</pre></div>
<p>All three user inputs (<tt class="docutils literal">username, password, remember_me</tt>) are read from the form and if both username and
password have been defined an Ajax request will be done to the <tt class="docutils literal">g_urls.login</tt> url. We pass
<tt class="docutils literal">username, password</tt> <em>and</em> <tt class="docutils literal">g_csrftoken</tt> (as discussed before) as the request data. Now, if there’s an
error (<tt class="docutils literal">fail</tt>) I just display a generic message (by removing it’s <cite>d-invisible</cite> class) while, if the
request was Ok I retrieve the <tt class="docutils literal">key</tt> (token) from the response, initialize the <tt class="docutils literal">g_auth</tt> object with the
<tt class="docutils literal">username</tt>, <tt class="docutils literal">key</tt> and <tt class="docutils literal">remember_me</tt> values and call <tt class="docutils literal">initLogin</tt> to show the correct divs and save
to the session/local storage.</p>
<p>It is important to keep in mind that with the line <tt class="docutils literal">g_csrftoken = <span class="pre">getCookie('csrftoken')</span></tt>
we re-read the <span class="caps">CSRF</span> cookie. This is needed because, as you can see in the mentioned link in the comment,
after Django logs in, the csrf cookie value is rotated for security reasons so it must be re-read here (or else
the <tt class="docutils literal">logout</tt> that is also a <span class="caps">POST</span> request will not work).</p>
<p>Finally, here’s the code for logout (still inside the <tt class="docutils literal">$(function () {</tt>):</p>
<div class="highlight"><pre><span></span><span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="s1">'#logoutButton'</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Trying to logout"</span><span class="p">);</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">url</span><span class="o">:</span><span class="w"> </span><span class="nx">g_urls</span><span class="p">.</span><span class="nx">logout</span><span class="p">,</span>
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s2">"POST"</span><span class="p">,</span>
<span class="w"> </span><span class="nx">beforeSend</span><span class="o">:</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Token "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">g_auth</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">csrfmiddlewaretoken</span><span class="o">:</span><span class="w"> </span><span class="nx">g_csrftoken</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"DONE: "</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="nx">g_auth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">null</span><span class="p">;</span>
<span class="w"> </span><span class="nx">initLogin</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">fail</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"FAIL: "</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">});</span><span class="w"> </span><span class="c1">// End of $(function () {</span>
</pre></div>
<p>The code here is very simple - just do a <tt class="docutils literal"><span class="caps">POST</span></tt> to the <tt class="docutils literal">g_urls.logout</tt> and if everything is ok delete the <tt class="docutils literal">g_auth</tt> values
and call <tt class="docutils literal">initLogin()</tt> to show the correct divs and remove the <tt class="docutils literal">auth</tt> key from local/session storage. Notice that when
you <tt class="docutils literal"><span class="caps">POST</span></tt> to the <tt class="docutils literal">logout</tt> <span class="caps">REST</span> end-point, you need to also add the <tt class="docutils literal">Authorization</tt> header with the token or else
(since we’ve defined only <tt class="docutils literal">TokenAuthentication</tt> for the <tt class="docutils literal">authentication_classes</tt> for the <tt class="docutils literal">LogoutViewEx</tt> class)
there won’t be any way to correlate the request with the user and log him out!</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>Using the info presented on this article you should be able to properly login and logout to Django using <span class="caps">REST</span> and
also call <span class="caps">REST</span> end-points as an authenticated used. I recommend using the <tt class="docutils literal">curl</tt> utility to try to call the rest
end point with various parameters to see the response. Also, you change the <tt class="docutils literal">LogoutViewEx</tt> with the
default django-rest-auth <tt class="docutils literal">LogoutView</tt> and then try logging out through the web-app <em>and</em> through curl and see
what happens when you try to access the test-auth end-point.</p>
<p>Finally, the above project can be easily modified to use
<tt class="docutils literal">SessionAuthentication</tt> instead of <tt class="docutils literal">TokenAuthentication</tt> (so you won’t need <tt class="docutils literal"><span class="pre">django-rest-auth</span></tt> at all) - I’m
leaving it as an exercise to the reader.</p>
</div>
Using both Python 2 and 3 in Windows2017-12-20T11:20:00+02:002017-12-20T11:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2017-12-20:/2017/12/20/python-2-3-windows/<p class="first last">How to install and use both Python 2.x and 3.x on Windows</p>
<p>The release of Django 2.0 was a milestone in the history of Python since it
completely dropped support for Python 2.x. I am a long time user of Django
and, loyal to the philosophy of “if it is working don’t change” I was always
using Python 2.x.</p>
<p>Of course right now this needs to change since new applications will need to
be developed to the latest version of Django (and Python). I am using
Windows for development (almost exclusively) and what I wanted was to
be able to create and use virtual environments for both my old projects
(using Python 2) and new projects (using Python 3).</p>
<p>The above requirement is not as straight-forward as I’d like - actually it is
if you know what you need to do and which tools you must use. That’s why I
decided to write a quick step by step tutorial on how to use both versions
of Python in your Windows environment. Notice that I am using Windows 10,
Python 2.7.14 an Python 3.6.4.</p>
<p>First of all, let’s download both versions of Python
from the <a class="reference external" href="https://www.python.org/downloads/">Python download page</a>. I downloaded the files
python-2.7.14.msi and python-3.6.4.exe (not sure why the one is .msi and the other
is .exe it doesn’t matter anyway).</p>
<p>Firstly I am installing Python 2.7.14 and selecting:</p>
<ul class="simple">
<li>Install for all users</li>
<li>Install to default folder</li>
<li>Press next to following screen (install default customizations)</li>
<li>This won’t add the python.exe of Python 2.7 to path</li>
</ul>
<p>Next I am install Python 3.6.4:</p>
<ul class="simple">
<li>Make sure to click “Install launcher for all users (recommended)”</li>
<li>I also check “Add Python 3.6to <span class="caps">PATH</span>” (to add the Python 3.6 executable to path)</li>
<li>I then just click “Install Now” (this will put Python 3.6 to c:)</li>
</ul>
<p>Right now if you open a terminal window (Windows+r, cmd.exe) and run Python you will
initiate the Python 3.6 interpreter. This is useful for just dropping in a Python interpreter.</p>
<p>Remember the “Install launcher for all users (recommended)” we clicked before? This installs
the python launcher which is used to select between Python versions.
If you run it without parameters the Python 3.6 interpreter will by started. You can pass
the -2 parameter to start the python 2.x interpreter or -3 to explicitly declare the
python 3.x interpreter:</p>
<pre class="code literal-block">
C:\Users\Serafeim>py -2
Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> ^Z
C:\Users\Serafeim>py -3
Python 3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:04:45) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> ^Z
</pre>
<p>With py we can easily start the Python interpreter we want. This is not enough though - we
need to use virtualenv and create proper virtual environments for our projects. To do this
you can add the -m option to py to run a module with the proper python version. For example,
to start an http server with Python 2 you would use the module <tt class="docutils literal">SimpleHTTPServer</tt> while
for python 3 you would use <tt class="docutils literal">http.server</tt> (<a class="reference external" href="https://stackoverflow.com/questions/7943751/what-is-the-python-3-equivalent-of-python-m-simplehttpserver">as per this</a>):</p>
<pre class="code literal-block">
C:\progr\py\pelican\spapas.github.io>py -2 -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
C:\progr\py\pelican\spapas.github.io>py -3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
</pre>
<p>Now, to create the virtual environments we’ll use the virtualenv module which is installed
by default in Python 2.x (try running <tt class="docutils literal">py <span class="pre">-2</span> <span class="pre">-m</span> pip freeze</tt> to see the list of installed
packages for Python 2.x) and the venv module which is included in the Python 3.x core. So
to create a virtualenv for Python 2 we’ll run <tt class="docutils literal">py <span class="pre">-2</span> <span class="pre">-m</span> virtualenv <span class="pre">name-of-virtualenv</span></tt>
and for Python 3 <tt class="docutils literal">py <span class="pre">-3</span> <span class="pre">-m</span> venv <span class="pre">name-of-virtualenv</span></tt>.</p>
<pre class="code literal-block">
C:\progr\py>py -2 -m virtualenv venv-2
New python executable in C:\progr\py\venv-2\Scripts\python.exe
Installing setuptools, pip, wheel...done.
C:\progr\py>py -3 -m venv venv-3
C:\progr\py>venv-2\Scripts\activate
(venv-2) C:\progr\py>python -V
Python 2.7.14
(venv-2) C:\progr\py>deactivate
C:\progr\py>venv-3\Scripts\activate
(venv-3) C:\progr\py>python -V
Python 3.6.4
</pre>
<p>That’s how easy it is to have both Python 2.7 and Python 3.6 in your Windows!</p>
My essential django package list2017-10-11T14:20:00+03:002017-10-11T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2017-10-11:/2017/10/11/essential-django-packages/<p class="first last">A list of packages (add-ons) that I use in most of my projects</p>
<p>In this article I’d like to present a list of django packages (add-ons) that I use
in most of my projects. I am using django for more than 5 years as my day to
day work tool to develop applications for the public sector organization I work for.
So please keep in mind that these packages are targeted to the “enterpripse”
audience and some things that target public (open access) web apps may be missing.</p>
<p>Some of these packages are included in
<a class="reference external" href="https://github.com/spapas/cookiecutter-django-starter">a django cookiecutter</a> I am using to start new projects and also you can find
example usage of some of these packages in my <a class="reference external" href="https://github.com/spapas/mailer_server">Mailer Server</a> project.
I’ll also be happy to answer any
questions about these, preferably in <a class="reference external" href="https://stackoverflow.com">stackoverflow</a> if the questions are fit for that site.</p>
<p>Finally, before going to the package list, notice that most of the following are django-related and I won’t include any
generic python packages (with the exception of xhtml2pdf which I discuss later). There
are a bunch of python-generic packages that I usually use (xlrd, xlwt, requests, raven, unidecode,
database connectors etc) but these won’t be discussed here.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#packages-i-use-in-all-my-projects" id="toc-entry-1">Packages I use in all my projects</a><ul>
<li><a class="reference internal" href="#django-tables2" id="toc-entry-2">django-tables2</a></li>
<li><a class="reference internal" href="#django-filter" id="toc-entry-3">django-filter</a></li>
<li><a class="reference internal" href="#django-crispy-forms" id="toc-entry-4">django-crispy-forms</a></li>
<li><a class="reference internal" href="#django-extensions" id="toc-entry-5">django-extensions</a></li>
<li><a class="reference internal" href="#django-autocomplete-light" id="toc-entry-6">django-autocomplete-light</a></li>
<li><a class="reference internal" href="#django-reversion" id="toc-entry-7">django-reversion</a></li>
<li><a class="reference internal" href="#django-compressor" id="toc-entry-8">django-compressor</a></li>
<li><a class="reference internal" href="#django-debug-toolbar" id="toc-entry-9">django-debug-toolbar</a></li>
</ul>
</li>
<li><a class="reference internal" href="#packages-i-use-when-i-need-their-functionality" id="toc-entry-10">Packages I use when I need their functionality</a><ul>
<li><a class="reference internal" href="#xhtml2pdf" id="toc-entry-11">xhtml2pdf</a></li>
<li><a class="reference internal" href="#django-auth-ldap" id="toc-entry-12">django-auth-ldap</a></li>
<li><a class="reference internal" href="#django-localflavor" id="toc-entry-13">django-localflavor</a></li>
<li><a class="reference internal" href="#django-constance" id="toc-entry-14">django-constance</a></li>
<li><a class="reference internal" href="#django-rq" id="toc-entry-15">django-rq</a></li>
<li><a class="reference internal" href="#django-rules-light" id="toc-entry-16">django-rules-light</a></li>
<li><a class="reference internal" href="#django-generic-scaffold" id="toc-entry-17">django-generic-scaffold</a></li>
<li><a class="reference internal" href="#django-taggit" id="toc-entry-18">django-taggit</a></li>
<li><a class="reference internal" href="#django-sendfile" id="toc-entry-19">django-sendfile</a></li>
<li><a class="reference internal" href="#django-reversion-compare" id="toc-entry-20">django-reversion-compare</a></li>
<li><a class="reference internal" href="#django-simple-history" id="toc-entry-21">django-simple-history</a></li>
<li><a class="reference internal" href="#django-import-export" id="toc-entry-22">django-import-export</a></li>
<li><a class="reference internal" href="#django-extra-views" id="toc-entry-23">django-extra-views</a></li>
<li><a class="reference internal" href="#easy-thumbnails" id="toc-entry-24">easy-thumbnails</a></li>
<li><a class="reference internal" href="#django-rest-framework" id="toc-entry-25">django-rest-framework</a></li>
<li><a class="reference internal" href="#django-waffle" id="toc-entry-26">django-waffle</a></li>
<li><a class="reference internal" href="#django-allauth" id="toc-entry-27">django-allauth</a></li>
<li><a class="reference internal" href="#django-modeltranslation" id="toc-entry-28">django-modeltranslation</a></li>
<li><a class="reference internal" href="#django-widget-tweaks" id="toc-entry-29">django-widget-tweaks</a></li>
<li><a class="reference internal" href="#django-simple-captcha" id="toc-entry-30">django-simple-captcha</a></li>
<li><a class="reference internal" href="#wagtail" id="toc-entry-31">wagtail</a></li>
</ul>
</li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-32">Conclusion</a></li>
</ul>
</div>
<div class="section" id="packages-i-use-in-all-my-projects">
<h2>Packages I use in all my projects</h2>
<p>These packages are used more or less in all my projects - I just install them when I
create a new project (or create the project through the cookiecutter I mention above)/</p>
<div class="section" id="django-tables2">
<h3>django-tables2</h3>
<p><a class="reference external" href="https://github.com/bradleyayers/django-tables2">django-tables2</a>, along with django-filter (following) are the two packages
I always use in my projets. You create a <cite>tables.py</cite> module inside your applications
where you define a bunch of tables using more or less the same methodology as with
django-forms: For a <cite>Book</cite> model, Create a <cite>BookTable</cite> class, relate it with the model
and, if needed override some of the columns it automatically generates. For example you
can configure a column that will behave as a link to a detail view of the book, a column
that will behave as a date (with the proper formatting) or even a column that will display
a custom django template snippet.</p>
<p>You’ll then be
able to configure the data (queryset) of this table and pass it your views context. Finally
use <tt class="docutils literal">{% render_table table %}</tt> to actually display your table. What
you’ll get? The actual html table (with a nice style that can be configured if needed),
free pagination and free column-header sorting. After you get the hang of it, you’ll be
able to add tables for your models in a couple of minutes - this is <span class="caps">DRY</span> for me. It also
offers some <a class="reference external" href="http://django-tables2.readthedocs.io/en/latest/pages/generic-mixins.html">class based views and mixins</a> for even more <span class="caps">DRY</span> adding tables to your views.</p>
<p>Finally, please notice that since version 1.8,
django-tables2 <a class="reference external" href="https://django-tables2.readthedocs.io/en/latest/pages/export.html">supports exporting data</a> using <a class="reference external" href="https://github.com/kennethreitz/tablib">tablib</a>, an old requirement of most users
of this library.</p>
</div>
<div class="section" id="django-filter">
<h3>django-filter</h3>
<p><a class="reference external" href="https://github.com/carltongibson/django-filter">django-filter</a> is used to create filters for your data. It plays well with
django-tables2 but you can use it to create filters for any kind of listing you want. It
works similar to django-tables2: Create a <tt class="docutils literal">filters.py</tt> module in your application and define
the a <cite>BookFilter</cite> class there by relating it with the <cite>Book</cite> model, specifying the fields
of the model you want to filter with and maybe override some of the default options. New versions
have some great built-in configuration functionality by customizing which method will be used for
filtering or even adding multiple filter methods - for example you could define your
<tt class="docutils literal">BookFilter</tt> like this:</p>
<pre class="code literal-block">
class BookFilter(django_filters.FilterSet):
class Meta:
model = Book
fields = {
'name': ['icontains'],
'author__last_name': ['icontains', 'startswith'],
'publication_date': ['year', 'month', ],
}
</pre>
<p>which will give you the following filter form fields:</p>
<ul class="simple">
<li>an ilike ‘%value%’ for the book name</li>
<li>an ilike ‘%value%’ for the author name</li>
<li>a like ‘value%’ for the author name</li>
<li>a year(publication_date) = value for the publication_date</li>
<li>a month(publication_date) = value for the publication_date</li>
</ul>
<p>and their (<span class="caps">AND</span>) combinations!</p>
<p>The <cite>BookFilter</cite> can be used to create a filter form in your template and then in your views
pass to it the initial queryset along with <cite>request.<span class="caps">GET</span></cite> (which will contain the filter values)
to return the filtered data (and usually pass it to the table). I’ve created a sample project that
uses both django-tables2 and django-filters for you to use: <a class="reference external" href="https://github.com/spapas/django_table_filtering">https://github.com/spapas/django_table_filtering</a>.
Also, I’ve written an article which describes a technique for <a class="reference external" href="https://spapas.github.io/2015/10/05/django-dynamic-tables-similar-models/">automatically creating a filter-table view</a>.</p>
</div>
<div class="section" id="django-crispy-forms">
<h3>django-crispy-forms</h3>
<p>The forms that are created by default by django-forms are very basic and not styled properly.
To overcome this and have better styles for my forms, I always use <a class="reference external" href="https://github.com/django-crispy-forms/django-crispy-forms">django-crispy-forms</a>. It actually has two modes: Using the crispy template filter and using
the crispy template tag. Using the crispy template filter is very simple - just take a plain old django form and
render it in your template like this <cite>{{ form|crispy }}</cite>. If the django-crispy-forms has been configured correctly
(with the correct template pack) the form you’ll get will be much nicer than the django-default one. This is completely
automatic, you don’t need to do anything else!</p>
<p>Now, if you have some special requirements from a form, for example
multi-column rendering, adding tabs, accordions etc then you’ll need to use the <cite>{% crispy %}</cite> template tag. To use this
you must create the layout of your form in the form’s costructor using the FormHelper django-crispy-forms <span class="caps">API</span>. This may seem cumbersome
at first (why not just create the form’s layout in the django template) but using a class to define your form’s layout
has other advantages, for example all the form layout is in the actual form (not in the template) you can control
programatically the layout of the form (f.e display some fields only for administrators), you can use inheritance and
virtual methods to override how a form is rendered etc.</p>
<p>To help you understand how a <tt class="docutils literal">FormHelper</tt> looks like, here’s a form that is used to edit an access Card for visitors that
displays all fields horizontally inside a panel (I am using Bootstrap for all styling and layout purposes):</p>
<pre class="code literal-block">
class CardForm(ModelForm):
class Meta:
model = Card
fields = ['card_number', 'card_text', 'enabled']
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Fieldset(
'',
HTML(u"""<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Add card</h3>
</div>
<div class="panel-body">"""),
Div(
Div('card_number', css_class='col-md-4'),
Div('card_text', css_class='col-md-4'),
Div('enable',css_class='col-md-4'),
css_class='row'
),
),
FormActions(
Submit('submit', u'Save', css_class='btn btn-success'),
HTML(u"<a class='btn btn-primary' href='{% url \"card_list\" %}'>Return</a>" ),
),
HTML(u"</div></div>"),
)
super(CardForm, self).__init__(*args, **kwargs)
</pre>
<p>Notice that forms that will be rendered with a <tt class="docutils literal">FormHelper</tt> actually contain their <form> tag (you don’t need
to write it yourself like with plain django forms) so you have to define their method (post in this example) and submit button.</p>
</div>
<div class="section" id="django-extensions">
<h3>django-extensions</h3>
<p><a class="reference external" href="https://github.com/django-extensions/django-extensions">django-extensions</a> is a swiss-army-knife of django tools. I use it <em>always</em> in my projects because of the <cite>runserver_plus</cite>
and <cite>shell_plus</cite> commands. The first uses the <a class="reference external" href="https://spapas.github.io/2016/06/07/django-werkzeug-debugger/">Werkzeug debugger with django</a> which makes django development an absolute joy
(open a python shell wherever you want in your code and start writing commands)!
The second opens a <em>better</em> shell (your models and a bunch of django stuff are auto-imported, a better shall will be used if found etc).</p>
<p>The
<cite>runserver_plus</cite> and <cite>shell_plus</cite> alone will be more than enough for me to use this however it adds some more usefull management
commands like: <cite>admin_generator</cite> to quickly create an admin.py for your app, <cite>graph_models</cite> to generate a graphviz dot file of your models,
<cite>update_permissions</cite> to synchronize the list of permissions if you have added one to an existing model and <a class="reference external" href="http://django-extensions.readthedocs.io/en/latest/command_extensions.html">many, many others</a>. Take a look
at them and you’ll probably find more useful things!</p>
</div>
<div class="section" id="django-autocomplete-light">
<h3>django-autocomplete-light</h3>
<p><a class="reference external" href="https://github.com/yourlabs/django-autocomplete-light">django-autocomplete-light</a> is the best auto-complete library for django, especially after v3 was released (which greatly reduces the magic and
uses normal CBVs for configuring the querysets). You will create
an AutocompleteView for a model (similar to your other class based views) and then automatically use this view through a widget in the admin
or in your own forms. It is fully configurable (both the results and the selection templates), supports many to many fields, creating new instances
and even autocompleteing django-taggit tags! If for some reason it seems that it is not working please keep in mind that you need to includ
jquery <em>and</em> <tt class="docutils literal">{{ form.media }}</tt> to your templates or else the required client side code won’t be executed.</p>
<p>I think it is an essential for all cases because dropdowns with lots of choices have a very bad user experience - and the same is true with
many to many fields (you could use the checkboxes widget to improve their behavior a little but you will have bad behavior when there are many choices).</p>
</div>
<div class="section" id="django-reversion">
<h3>django-reversion</h3>
<p><a class="reference external" href="https://github.com/etianen/django-reversion">django-reversion</a> is a really important package for my projects. When it is configured properly (by adding a a reversion-middleware), it offers full auditing
for all changes in the your model instances you select (and properly groups them in case of changes to multiple instances in a simple request).
It saves a <span class="caps">JSON</span> representation
of all the versions of an instance in your database. Keep in mind that this may increase your database size but if you need full auditing
then is is probably the best way to do it. I have written an article about <a class="reference external" href="https://spapas.github.io/2015/01/21/django-model-auditing/">django model auditing</a> that discusses this package and django-simple-history
(following) more.</p>
</div>
<div class="section" id="django-compressor">
<h3>django-compressor</h3>
<p><a class="reference external" href="https://github.com/django-compressor/django-compressor/">django-compressor</a> is package that combines and minifies your css and javascript (both files and line snippets) into static files. There are
other tools for this but I have never used them since django-compressor satisfies my needs. Although I’ve
<a class="reference external" href="https://spapas.github.io/2015/05/27/using-browserify-watchify/">written about browserify</a> and friends from the node-js world I don’t recommend using such tools in django to combine and minify your
javascript and css <em>unless</em> you specifically require them.</p>
<p>It has an online and an offline mode. For the online mode, when a request is done it will check if the compressed file exist and if not it will
create it. This may lead to problems with permissions if your application server user cannot write to your static folders and also
your users will see exceptions if for some reason you have included a file that cannot be found. For the
offline mode, you need to run a management command that will create the static files <em>while deploying</em> the applications - this mode is
recommended because any missing files problems etc will be resolved while deploying the app.</p>
</div>
<div class="section" id="django-debug-toolbar">
<h3>django-debug-toolbar</h3>
<p><a class="reference external" href="https://github.com/jazzband/django-debug-toolbar">django-debug-toolbar</a>: This is a well known package for debugging django apps that is always included in the development configuration of my projects.
It has various panels that help you debug your application but, at least for me, the most helpful is the one that displays you all <span class="caps">SQL</span> queries that
are executed for a page load. Because of how the django orm is working it will go on and follow all relations something that will lead to
hundreds of queries. For example, let’s say that you have simple Book model with a foreign key to an Author model that has N instances in your
database. If you do a <tt class="docutils literal">Book.objects.all()</tt>
and want display the author name for each book in a template then you’ll always do <tt class="docutils literal">N+1</tt> queries to the database! This is really easy to miss because
in the django you’ll just do <tt class="docutils literal">{% for book in books <span class="pre">%}{{</span> <span class="pre">book.name}},</span> {{ book.author.name <span class="pre">}}{%</span> endif %}</tt> — however the <tt class="docutils literal">{{ book.author.name }}</tt> will go
on and do an extra <span class="caps">SQL</span> query!!! Such cases are easily resolved by using <a class="reference external" href="https://docs.djangoproject.com/en/1.11/ref/models/querysets/#select-related">select_related</a> (and <a class="reference external" href="https://docs.djangoproject.com/en/1.11/ref/models/querysets/#prefetch-related">prefetch_related</a>) but you must be sure to use select_related
for all your queries (and if you add some extra things to your template you must remember to also add them to your select_related clause for the query).</p>
<p>So, what I recommend before going to production is to visit all your pages using django-debug-toolbar and take a quick look at the number of <span class="caps">SQL</span>
queries. If you see something that does not make sense (for example you see more than 10 queries) then you’ll need to think about the problem I just
mentioned. Please notice that this, at least for me, is not premature optimization - this is not actually optimization! This is about writing correct
code. Let’s suppose that you could not use the django orm anymore and you had to use plain old <span class="caps">SQL</span> queries. Would you write <tt class="docutils literal"><span class="caps">SELECT</span> * <span class="caps">FROM</span> books</tt> and
then for each row do another <tt class="docutils literal"><span class="caps">SELECT</span> * <span class="caps">FROM</span> authors <span class="caps">WHERE</span> id=?</tt> passing the author of each book <em>or</em> do only <tt class="docutils literal">select * from books b <span class="caps">LEFT</span> <span class="caps">JOIN</span>
authors a on b.author_id = a.id</tt>?</p>
</div>
</div>
<div class="section" id="packages-i-use-when-i-need-their-functionality">
<h2>Packages I use when I need their functionality</h2>
<p>The packages following are also essential to me but only when I need their functionality. I don’t use them in all my projects
but, when I need the capabilities they offer then I will use these packages (and not some others). For example, if I need to
render PDFs in my applications then I will use the xhtml2pdf, if I need to login through <span class="caps">LDAP</span> I will use django-auth-ldap etc.</p>
<div class="section" id="xhtml2pdf">
<h3>xhtml2pdf</h3>
<p><a class="reference external" href="https://github.com/xhtml2pdf/xhtml2pdf">xhtml2pdf</a> is the package I use for creating <span class="caps">PDF</span>’s with django as I’ve alreadly discussed in the <a class="reference external" href="https://spapas.github.io/2015/11/27/pdf-in-django/">PDFs in Django</a> article (this is not
a django-specific package like most others I discuss here but it plays really good with django). You create a
normal django template, add some styling to it and dump it to html. Notice that there’s a <a class="reference external" href="https://github.com/chrisglass/django-xhtml2pdf">django-xhtml2pdf</a> project but has not been recently updated
and after all as you can see in my article it is easy to just call xhtml2pdf directly. The xhtml2pdf library is actually a wrapper around the
excellent <a class="reference external" href="http://www.reportlab.com">reportlab</a> library which does the low-level pdf output.</p>
<p>Notice that the xhtml2pdf library had some maintenance problems
(that’s why some people are suggesting other <span class="caps">PDF</span> solutions like WeasyPrint) however they seem to have been fixed now. Also,
I have found out that, at least for my needs (using Windows as my development environment), other soltuons are much inferior to xhtml2pdf.
I urge you to try xhtml2pdf first and only if you find that it does not cover your needs (and have asked
me about your problem) try the other solutions.</p>
</div>
<div class="section" id="django-auth-ldap">
<h3>django-auth-ldap</h3>
<p><a class="reference external" href="https://bitbucket.org/psagers/django-auth-ldap/">django-auth-ldap</a> is the package you’ll want to use if your organization uses <span class="caps">LDAP</span> (or Active Directory) and you want to use it for
logging in. Just configure your <span class="caps">LDAP</span> server settings,
add the ldap authenticator and you’ll be ready to go. Please notice that this package is a django wrapper of the python-ldap package
which actually provides the <span class="caps">LDAP</span> connection.</p>
</div>
<div class="section" id="django-localflavor">
<h3>django-localflavor</h3>
<p><a class="reference external" href="https://github.com/django/django-localflavor">django-localflavor</a> offers useful stuff for various countries, mainly form fields with the correct validation and lists of choices.
For example, for my country (Greece) you’ll get a <tt class="docutils literal">GRPhoneNumberField</tt>, a <tt class="docutils literal">GRPostalCodeField</tt> and a <tt class="docutils literal">GRTaxNumberCodeField</tt>. Use it instead of re-implementing
the behavior.</p>
</div>
<div class="section" id="django-constance">
<h3>django-constance</h3>
<p><a class="reference external" href="https://github.com/jazzband/django-constance">django-constance</a> is a simple package that enables you to add quick-configurable settings in your application. To change the settings.py file
you need to edit the source and restart the application - for most installations this is a full re-deployment of the application. Fully
re-deploying the app just to change a setting is not very good practice (depending on the setting of course but if it is a business setting
it usually should be done by business users and not by administrators).</p>
<p>That’s where django-constance comes to help you. You can define some
extra settings which can be changed through the django admin and their new value will be available immediately. Also you can configure where
these settings will be saved. One option is the database but this is not recommended - instead you can use redis so that the settings values will be
available much quicker!</p>
</div>
<div class="section" id="django-rq">
<h3>django-rq</h3>
<p><a class="reference external" href="https://github.com/ui/django-rq">django-rq</a> is a django wrapper for the <a class="reference external" href="https://github.com/nvie/rq">rq</a> library. I use it when I need asynchronous tasks (which is on almost all of my projects). More
info can be found on the two articles I have writtten about django rq (<a class="reference external" href="https://spapas.github.io/2015/01/27/async-tasks-with-django-rq/">asynchronous tasks in django</a> and <a class="reference external" href="https://spapas.github.io/2015/09/01/django-rq-redux/">django-rq redux</a>).</p>
</div>
<div class="section" id="django-rules-light">
<h3>django-rules-light</h3>
<p>One of the least known packages from those I discuss here, <a class="reference external" href="https://github.com/yourlabs/django-rules-light">django-rules-light</a> is one of the most useful when is needed. This package
allows you to define complex rules for doing actions on model instances. Each rule is a function that gets the user that wants to do the action
and the object that the user wants to action on. The function returns True or False to allow or not allow the action. You can then use these in
both your code to programatically check if the user can do the the action and your templates to decide what buttons and options you will display.
There are also various helper methods for CBVs that make everything easier.</p>
<p>To properly understand the value of django-rules-light you need to have some more complex than usual action rules. For example if your actions
for an object are view / edit and all your users can view and edit their own objects then you don’t really need this package. However, if your administrators
can view all objects and your object can be finalized so no changes are allowed unless an administrator tries to change it then you’ll greatly benefit
from using it!</p>
</div>
<div class="section" id="django-generic-scaffold">
<h3>django-generic-scaffold</h3>
<p><a class="reference external" href="https://github.com/spapas/django-generic-scaffold">django-generic-scaffold</a> is a package I have created that can be used to quickly (and DRYly) create <span class="caps">CRUD</span> CBVs for your models. I usually don’t want to
give access to the django-admin to non-technical users however sometimes I want to quickly create the required CBVs for them (list, detail, create, edit
delete). Using django-generic-scaffold you can just create a scaffold which is related with a Model and all the views will be automagically created -
you only need to link them to your urls.py. The created CBVs are fully configurable by adding extra mixins or even changing the parent class of each <span class="caps">CBV</span>.</p>
<p>Notice that this package does not create any source files - instead all CBVs are created on-the-fly using <tt class="docutils literal">type</tt>. For example, to create <span class="caps">CRUD</span> CBVs for
a Book model you’ll do this in scaffolding.py:</p>
<pre class="code literal-block">
class BookCrudManager(CrudManager):
model = models.Book
prefix = 'books'
</pre>
<p>and then in your urls.py you’ll just append the generated urls to your url list:</p>
<pre class="code literal-block">
book_crud = BookCrudManager()
urlpatterns += book_crud.get_url_patterns()
</pre>
<p>Now you can visit the corresponding views (for example /books or /bookscreate - depends on the prefix) to add/view/edit etc your books!</p>
</div>
<div class="section" id="django-taggit">
<h3>django-taggit</h3>
<p><a class="reference external" href="https://github.com/alex/django-taggit">django-taggit</a> is the library you’ll want to use if you have to use tags with your models. A tag is a synonym for keyword, i.e adding some
words/phrases to your instances that are used to categorise and desribe em. The relation between your to-be-tagged-model and your tags is
many to many. To use it, you just add <cite>tags = TaggableManager()</cite> to your
model and you are ready to go! Of course it will need some more configuration to be included in django admin and django forms but thankfully,
autocomplete-lights can be <a class="reference external" href="https://django-autocomplete-light.readthedocs.io/en/master/taggit.html">integrated with django-taggit</a>!</p>
</div>
<div class="section" id="django-sendfile">
<h3>django-sendfile</h3>
<p><a class="reference external" href="https://github.com/johnsensible/django-sendfile">django-sendfile</a> is a very important - at least to - me library. Sometimes, user uploaded files (media in django) should not be visible to all users
so you’ll need to implement some access control through your django app. However, it is important to <em>not</em> serve these media files through your application
server (uwsi, gunicorn etc) but use a web server (nginx, apache ect) for serving them. This is needed because your application server’s purpose is
not serving files from the disk - keep in mind that the application server usually has a specified amount of workers (usually analogous to the number
of CPUs of your server, for example 4 workers ) - think what will happen if some large media files are server through these workers to users with
a slow connection! With 4 such concurrent connections your application won’t be able to serve any other content!</p>
<p>So this package (along with the support of X-Sendfile from the web servers) helps you fulfill the above requirements:
It allowes you to check permissions to your media through your django application <em>but</em> then offload the serving of your media files to the web server.
More info about
<a class="reference external" href="https://stackoverflow.com/q/7296642/119071">django-sendfile can be found on this <span class="caps">SO</span> answer</a> but with a few words, with django-sendfile you create a view that checks if a file is allowed to be served
and, if yes, instruct the web server to actually serve that file by appending a specific header to the response.</p>
</div>
<div class="section" id="django-reversion-compare">
<h3>django-reversion-compare</h3>
<p><a class="reference external" href="https://github.com/jedie/django-reversion-compare">django-reversion-compare</a> is an addon to django-reversion. It allows you to compare two different versions of a model instance
and highlights their differences using various smart algorithms (so if you have a long text field you won’t only see that these are different but
you’ll also see where exactly they differ, with output similar to the one you get when using diff).</p>
</div>
<div class="section" id="django-simple-history">
<h3>django-simple-history</h3>
<p><a class="reference external" href="https://github.com/treyhunner/django-simple-history">django-simple-history</a> has similar capabilities with django-reversion - (auditing and keeping versions of models) with a very important
difference: While django-reversion keeps a <span class="caps">JSON</span> representation of each version in the database (making querying very difficult), django-simple-history
creates an extra, history table for each model instance you want to track and adds each change as a new row to that table. As can be understood this
will make the history table really huge but has the advantage that you can easily query for old values. I usually use django-reversion unless I know
that I will need the history querying.</p>
</div>
<div class="section" id="django-import-export">
<h3>django-import-export</h3>
<p><a class="reference external" href="https://github.com/django-import-export/django-import-export">django-import-export</a> can be used to enchance your models with mass import and export capabilities (from example from/to <span class="caps">CSV</span>).
You will add an ModelResource class that describes (and configures) how your Model should be imported/exported. The ModelResource class can
then be easily used in your views and, more importantly, it is integrated to the django-admin. I have to confess that I have not used django-import-export
for <em>importing</em> data because I prefer implementing my own views for that (to have better control over the whole process and because the data
I usually need to import does not usually map 1 to 1 with a model but I need to create more model instances etc). However I am using the export
capabilities of django-import-export in various projects with great success, especially the admin integration which easily fulfills the exporting
data capabilities of most users.</p>
</div>
<div class="section" id="django-extra-views">
<h3>django-extra-views</h3>
<p>Beyond django-forms, django supports a feature called <a class="reference external" href="https://docs.djangoproject.com/en/1.11/topics/forms/formsets/">Formsets</a> which allows you to add/edit multiple object instances in
a single view (by displaying all instances one after the other). The classic request/response cycle of django is preserved in Formsets, so
your form instances will be submitted all together when you submit the form. The logic extension to the Formset is the <a class="reference external" href="https://docs.djangoproject.com/en/1.11/topics/forms/modelforms/#model-formsets">ModelFormset</a> i.e each form
in a Formset is a ModelForm and <a class="reference external" href="https://docs.djangoproject.com/en/1.11/topics/forms/modelforms/#inline-formsets">InlineFormSet</a> where you have a Parent model that has a bunch of children and you are editing the Parent <em>and</em>
its children in a single Form. For example, you have a School and a Student model where each Student has a ForeignKey to School. The usual case
would be to edit the Student model and select her school (through a drop down or even-better if you use django-autocomplete-light through a proper
autocompelte widget). However, you may for some reason want to edit the School and display (and edit) the list of its Students — that’s where you’ll
use an InlineFormSet!</p>
<p>The above features (Formsets, Modelformsets and Inlineformsets) are not supported natively by django CBVs — that’s where <a class="reference external" href="https://github.com/AndrewIngram/django-extra-views">django-extra-views</a> comes
to the foreground. You can use the corresponding CBVs of django-extra-views to support the multiple-form workflows described above. Using these CBVs
are more or less similar to using the good-old django FormView.</p>
</div>
<div class="section" id="easy-thumbnails">
<h3>easy-thumbnails</h3>
<p><a class="reference external" href="https://github.com/SmileyChris/easy-thumbnails">easy-thumbnails</a> is a great library if you want to support thumbnails. Actually, thumbnails is not 100% correct - this package can be used to
generate and manage various versions of your original images, for example you may have a small version of the image that will be used as a thumbnail,
a larger version that will be used in a gallery-carousel view and an even larger version (but not the original one which could be huge) that will be used when
the user clicks on the gallery to view a larger version of the image. To do that you define the image configurations you support in your settings.py and then
you have access
to your thumbnails both in your templates and in your views. Notice that a specific thumbnail congfiguration for an image will be created only once since
the generated images are saved so each thumbnail will be generated on the first request it contains it and will be reused in the following such requests.</p>
</div>
<div class="section" id="django-rest-framework">
<h3>django-rest-framework</h3>
<p><a class="reference external" href="https://github.com/encode/django-rest-framework">django-rest-framework</a> is definitely the best package for creating web APIs with django. I don’t recommend using it if you want to create a quick <span class="caps">JSON</span> search
<span class="caps">API</span> (take a look at <a class="reference external" href="https://spapas.github.io/2014/09/15/django-non-html-responses/">django non-<span class="caps">HTML</span> responses</a>) but if you want to go the <span class="caps">SPA</span> way or if you want to create multiple APIs for various models then
this is the way to go. Integrating it to your project will need some effort (that’s why I don’t recommend it for quick and dirty APIs) because you’ll
need to create a serializers.py which will define the serializers (more or less the fields) for the models you’ll want to expose through your <span class="caps">API</span> and
then create the views (or the viewsets which are families of views for example list, detail, delete, update, create) and add them to your urls.py. You’ll
also need to configure authentication, authorization and probably filtering and pagination. This may seem like a lot of work but the result are
excellent - you’ll get a full <span class="caps">REST</span> <span class="caps">API</span> supporting create, list, detail, update, delete for any complex configuration of your models.
You can take a look at a sample application in my <a class="reference external" href="https://github.com/spapas/react-tutorial">React tutorial</a> repository (yes this is a repository that has a tutorial for React and
friends but the back-end is in django and django-rest-framework).</p>
<p>django-rest-framework integrates nicely with django-filter (mentioned above) to re-use the filters you have created for your model listings
in your <span class="caps">REST</span> APIs - <span class="caps">DRY</span> at its best!</p>
</div>
<div class="section" id="django-waffle">
<h3>django-waffle</h3>
<p><a class="reference external" href="https://github.com/jsocol/django-waffle">django-waffle</a> is described as a feature flipper. What does this mean? Let’s say that you want to control at will when a specific view will be enabled
- this is the library you’ll want to use. Or you have developed a new feature and you want to give access to it only on a subset of users for a pilot run of
the feature - once again you should use django flipper. It offers a nice admin interface where you can configure the flags that will be used for the various
feature enabling/disabling (and if they are active or not) and various template tags and functions that you can use to test if the features should be activated
or not.</p>
</div>
<div class="section" id="django-allauth">
<h3>django-allauth</h3>
<p><a class="reference external" href="https://github.com/pennersr/django-allauth">django-allauth</a> should be used in two cases: When you want a <em>better</em> user registration workflow than the default (non-existant) one or you want
to integrate your application with an external OAuth provider (i.e allow your users to login through their facebook, google, twitter, github etc accounts). I
have mainly used this package for the first case and I can confirm that it works great and you can create as complex flows as you want (for example, in
one of my projects I have the following user registration-activation flow: A user registers using a custom form and using his email as username, he receives
an email with a confirmation link,
after he has confirmed his email he receivs a custom message to wait for his account activation and the administrators of the application are notified,
the administrators enable the new user’s account after checking some things and only then he’ll be able to log-in). One thing that must be noticed about
django-allauth is that it (in my opition) does not have very good documentation but there are lots of answers about <a class="reference external" href="https://stackoverflow.com/questions/tagged/django-allauth">django-allauth in stackoverflow</a> and the source code
is very clear so you can always use the source as documentation for this package.</p>
</div>
<div class="section" id="django-modeltranslation">
<h3>django-modeltranslation</h3>
<p>The <a class="reference external" href="https://github.com/deschler/django-modeltranslation">django-modeltranslation</a> library is the library I recommend for when you want to have translations to your models. To use it
you add a translation.py file where you declare the models and their fields that should be translated. The, depending on which languages you have configured
in your settings.py after you run makemigrations and migrate you’ll see that django-modeltranslation will have included extra fields to the database, each one
with the corresponding language name (for example if you have added a field <tt class="docutils literal">name</tt> to the translations and have english and greek as language,
django-modeltranslation will add the fields <tt class="docutils literal">name_en</tt> and <tt class="docutils literal">name_el</tt> to your table). You can the edit the i18n fields (using forms or the django admin) and
depending on the current language of your site when you use <tt class="docutils literal">name</tt> you’ll get either <tt class="docutils literal">name_el</tt> or <tt class="docutils literal">name_en</tt>.</p>
</div>
<div class="section" id="django-widget-tweaks">
<h3>django-widget-tweaks</h3>
<p>If for some reason you don’t want to do django-crispy-forms, or you have a form in which you want to do a specific layout change but without
fully implementing the FormHelper then you can actually render the form in <span class="caps">HTML</span> and output the fields one by one. One thing that cannot
be done though is passing custom options to the rendered form field. When you do a <tt class="docutils literal">{{ form.field }}</tt> to your template django will render
the form field using its default options - yes this can be overriden using <a class="reference external" href="https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#base-widget-classes">custom widgets</a> but I don’t recommend it for example if you only
want to add a class to the rendered <tt class="docutils literal"><input></tt>!</p>
<p>Instead, you can use <a class="reference external" href="https://github.com/jazzband/django-widget-tweaks">django-widget-tweaks</a> to pass some specific class names or attributes to
the rendered form fields - so if you use <tt class="docutils literal">{{ <span class="pre">form.field|add_class:"myclass"</span> }}</tt> the rendered <tt class="docutils literal"><input></tt> will have a <tt class="docutils literal">myclass</tt> css class.</p>
</div>
<div class="section" id="django-simple-captcha">
<h3>django-simple-captcha</h3>
<p>Use <a class="reference external" href="https://github.com/mbi/django-simple-captcha">django-simple-captcha</a> to add (configurable) captchas to your django forms. This is a very simple package that does not have any requirements
beyond the Pillow library for the captcha image generator. The generated captchas are simple images with some added noise so it won’t integrate
<a class="reference external" href="https://www.google.com/recaptcha/intro/">reCAPTCHA</a> with which you may be more familiar. I deliberatly propose this package for captchas so you won’t need to integrate with Google services.</p>
</div>
<div class="section" id="wagtail">
<h3>wagtail</h3>
<p><a class="reference external" href="https://github.com/wagtail/wagtail">wagtail</a> is a great django <span class="caps">CMS</span>. I use it when I need to create a <span class="caps">CMS</span> or I need to add <span class="caps">CMS</span> like capabilities to a project. It has many
capabilities, too many to be listed here. I urge you to try it if you need a <span class="caps">CMS</span>!</p>
</div>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>The above packages should cover most of your django needs. I have listed only packages with good documentation,
that have been recently updated
and work with new django versions and should be fairly easy to integrate with your projects. If you need anything more or want to take a general
look at some of the packages that have are availablie I recommend starting
with the <a class="reference external" href="https://djangopackages.org">django packages</a> site.</p>
<p>One important thing to notice here is that some of the above packages are not really complex and their functionality can be re-implemented
by you in a couple of hours. For example, you could replicate the functionality of django-constance by adding a config dict and
a couple of methods (and template tags) of storing and retrieving the keys of that dict with redis. Or add some custom clean methods to your
forms instead of using the form fields from django-localflavor. Also, some of these packages have similar functionality and can be used (along
with a little custom code) to replicate the functionality of other packages, for exmaple instead of using django-waffle you could use
django-constance to configure if the features should be enabled or disabled and django-rules-light to control if the users have access to the feature.
Also, you could probably use django-waffle for access control, i.e allow only admins to access a specific views.</p>
<p>Please don’t do this. This violates <span class="caps">DRY</span> and violates being disciplined.
Each package has its purpose and being <span class="caps">DRY</span> means that you use it for its purpose, <em>not</em> re-implementing it and <em>not</em> re-using it for other purposes.
When somebody (or you after some months) sees that package in requirements or INSTALLED_APPS he will conclude that you are using it for
its intented purpose and thank you because you have saved him some time - please don’t make him waste his time by needing to read your source code to understand
any smart tricks or reinventing the wheel.</p>
</div>
Automatically create a category table in Postgresql by extracting unique table values2017-07-04T09:05:00+03:002017-07-04T09:05:00+03:00Serafeim Papastefanostag:spapas.github.io,2017-07-04:/2017/07/04/postgresql-auto-create-category-column/<p class="first last">A postgresql script to help you automatically create a new category table by extracting its values from a table and generate the relations</p>
<p>Recently I was given an <span class="caps">SQL</span> table containing some useful data. The problem with that table was that it was was non-properly normalized but was completely flat,
i.e all its columns contained varchar values while, some of them should have instead contained foreign keys to other tables. Here’s an example of how this
table looked like:</p>
<table border="1" class="docutils">
<colgroup>
<col width="14%" />
<col width="25%" />
<col width="28%" />
<col width="33%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head" colspan="4">Parts</th>
</tr>
<tr><th class="head"><span class="caps">ID</span></th>
<th class="head">Serial</th>
<th class="head">Category</th>
<th class="head">Manufacturer</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>1</td>
<td>423431</td>
<td>Monitor</td>
<td><span class="caps">LG</span></td>
</tr>
<tr><td>2</td>
<td>534552</td>
<td>Monitor</td>
<td><span class="caps">LG</span></td>
</tr>
<tr><td>3</td>
<td>435634</td>
<td>Printer</td>
<td><span class="caps">HP</span></td>
</tr>
<tr><td>4</td>
<td>534234</td>
<td>Printer</td>
<td>Samsung</td>
</tr>
<tr><td>5</td>
<td>234212</td>
<td>Monitor</td>
<td>Samsung</td>
</tr>
<tr><td>6</td>
<td>123123</td>
<td>Monitor</td>
<td><span class="caps">LG</span></td>
</tr>
</tbody>
</table>
<p>The normalized version of this table should have been instead like this:</p>
<table border="1" class="docutils">
<colgroup>
<col width="13%" />
<col width="23%" />
<col width="28%" />
<col width="38%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head" colspan="4">Parts</th>
</tr>
<tr><th class="head"><span class="caps">ID</span></th>
<th class="head">Serial</th>
<th class="head">Category_id</th>
<th class="head">Manufacturer_id</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>1</td>
<td>423431</td>
<td>1</td>
<td>1</td>
</tr>
<tr><td>2</td>
<td>534552</td>
<td>1</td>
<td>1</td>
</tr>
<tr><td>3</td>
<td>435634</td>
<td>2</td>
<td>2</td>
</tr>
<tr><td>4</td>
<td>534234</td>
<td>2</td>
<td>3</td>
</tr>
<tr><td>5</td>
<td>234212</td>
<td>1</td>
<td>3</td>
</tr>
<tr><td>6</td>
<td>123123</td>
<td>1</td>
<td>2</td>
</tr>
</tbody>
</table>
<p>with the following extra tables that contain the category values with proper foreign keys:</p>
<table border="1" class="docutils">
<colgroup>
<col width="22%" />
<col width="78%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head" colspan="2">Category</th>
</tr>
<tr><th class="head"><span class="caps">ID</span></th>
<th class="head">Name</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>1</td>
<td>Monitor</td>
</tr>
<tr><td>2</td>
<td>Printer</td>
</tr>
</tbody>
</table>
<table border="1" class="docutils">
<colgroup>
<col width="17%" />
<col width="83%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head" colspan="2">Manufacturer</th>
</tr>
<tr><th class="head"><span class="caps">ID</span></th>
<th class="head">Name</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>1</td>
<td>Monitor</td>
</tr>
<tr><td>2</td>
<td>Printer</td>
</tr>
</tbody>
</table>
<p>The normalized version should be used instead of the flat one <a class="reference external" href="http://www.imdb.com/title/tt0057012/quotes?item=qt0454452">for reasons which at this moment must be all too obvious</a>.</p>
<p>Having such non-normalized tables is also a common problem I experience with Django: When creating a model
I usually define some <tt class="docutils literal">CharField</tt> with pre-defined <tt class="docutils literal">choices</tt> which “will never change”. Sometime during the
development (if I am lucky) or when the project is live for years I will be informed that the choices need not
only to be changed but must be changed by the users of the site without the need to change the source code! Or
that the choices/categories have properties (beyond their name) that need to be defined and used in the project. Both
of these cases mean that these categories need to be extracted from simple strings to full Django models (i.e get
normalized in their own table)!</p>
<p>In this post I will present a function written in <span class="caps">PL</span>/pgsql that will automatically normalize a column from a
flat table like the previous. Specifically, using the previous example,
if you have a table named <tt class="docutils literal">part</tt> that has a non-normalized
column named <tt class="docutils literal">category</tt> then when you call <tt class="docutils literal">select <span class="pre">export_relation('part',</span> 'category')</tt> the following
will happen:</p>
<ul class="simple">
<li>A new table named <tt class="docutils literal">part_category</tt> will be created. This table will contain two columns <tt class="docutils literal">id</tt> and <tt class="docutils literal">name</tt>, with <tt class="docutils literal">id</tt> being the primary key and <tt class="docutils literal">name</tt> having a unique constraint. If the table exists it will be dropped and re-ccreated</li>
<li>A new column named <tt class="docutils literal">category_id</tt> will be added to <tt class="docutils literal">part</tt>. This column will be a foreign key to the new table <tt class="docutils literal">part_category</tt>.</li>
<li><dl class="first docutils">
<dt>For each unique value <tt class="docutils literal">v</tt> of <tt class="docutils literal">category</tt>:</dt>
<dd><ul class="first last">
<li>Insert a new record in <tt class="docutils literal">part_category</tt> with <tt class="docutils literal">v</tt> in the <tt class="docutils literal">name</tt> field of the table and save the inserted id to <tt class="docutils literal">current_id</tt></li>
<li>Set <tt class="docutils literal">current_id</tt> to <tt class="docutils literal">category_id</tt> to all rows of <tt class="docutils literal">part</tt> where <tt class="docutils literal">category</tt> has the value of <tt class="docutils literal">v</tt></li>
</ul>
</dd>
</dl>
</li>
</ul>
<p>Before diving in to the <span class="caps">PL</span>/pgSQL script that does the above changes to the table I’d like to notice that I am
not very experienced with <span class="caps">PL</span>/pgSQL since I rarerly use it
(I actually avoid writing code in the database) however, because the case I described is ideal for using a database script
I’ve bitten the bullet and implemented it.</p>
<p>Beyond it’s actual functionality, this script can be used as a reference/cookbook for common <span class="caps">PL</span>/pgSQL tasks:</p>
<ul class="simple">
<li>Create/define a <span class="caps">PL</span>/pgSQL function (stored procedure)</li>
<li>Declare variables</li>
<li>Assign values to variables</li>
<li>Execute <span class="caps">SQL</span> commands with variable defined table / column names</li>
<li>Log process in <span class="caps">PL</span>/pgSQL</li>
<li>Executing code conditionally</li>
<li>Loop through the rows of a query</li>
<li>Save the primary key of an inserted row</li>
</ul>
<p>The script works however I feel that more experienced <span class="caps">PL</span>/pgSQL developers would write things different - if you have any
proposals please comment out and I’ll be happy to incorporate them to the script.</p>
<p>Now, let’s now take a look at the actual script:</p>
<div class="highlight"><pre><span></span><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="k">FUNCTION</span><span class="w"> </span><span class="n">export_relation</span><span class="p">(</span><span class="n">table_name</span><span class="w"> </span><span class="nb">varchar</span><span class="p">,</span><span class="w"> </span><span class="n">column_name</span><span class="w"> </span><span class="nb">varchar</span><span class="p">)</span>
<span class="k">RETURNS</span><span class="w"> </span><span class="nb">void</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">$$</span>
<span class="k">DECLARE</span>
<span class="w"> </span><span class="n">val</span><span class="w"> </span><span class="nb">varchar</span><span class="p">;</span>
<span class="w"> </span><span class="n">res</span><span class="w"> </span><span class="nb">boolean</span><span class="p">;</span>
<span class="w"> </span><span class="n">new_table_name</span><span class="w"> </span><span class="nb">varchar</span><span class="p">;</span>
<span class="w"> </span><span class="n">new_column_name</span><span class="w"> </span><span class="nb">varchar</span><span class="p">;</span>
<span class="w"> </span><span class="n">current_id</span><span class="w"> </span><span class="nb">int</span><span class="p">;</span>
<span class="k">BEGIN</span>
<span class="w"> </span><span class="n">new_table_name</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="n">table_name</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">'_'</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">column_name</span><span class="p">;</span>
<span class="w"> </span><span class="n">new_column_name</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="n">column_name</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">'_id'</span><span class="w"> </span><span class="p">;</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'drop table if exists %s cascade'</span><span class="p">,</span><span class="w"> </span><span class="n">new_table_name</span><span class="p">);</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'CREATE TABLE %s (id serial NOT NULL, name character varying(255) NOT NULL, CONSTRAINT %s_pkey PRIMARY KEY (id), CONSTRAINT %s_unique UNIQUE (name))WITH ( OIDS=FALSE)'</span><span class="p">,</span>
<span class="w"> </span><span class="n">new_table_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_table_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_table_name</span>
<span class="w"> </span><span class="p">);</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="p">(</span><span class="s1">'select exists(SELECT column_name FROM information_schema.columns WHERE table_name=''%s'' and column_name=''%s'') as x'</span><span class="p">,</span><span class="w"> </span><span class="n">table_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_column_name</span><span class="p">)</span><span class="w"> </span><span class="k">into</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">res</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="k">false</span><span class="w"> </span><span class="k">then</span>
<span class="w"> </span><span class="k">raise</span><span class="w"> </span><span class="k">notice</span><span class="w"> </span><span class="s1">'Creating colum'</span><span class="p">;</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'ALTER TABLE %s ADD COLUMN %s integer null'</span><span class="p">,</span><span class="w"> </span><span class="n">table_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_column_name</span><span class="p">);</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'ALTER TABLE %s ADD CONSTRAINT fk_%s FOREIGN KEY (%s) REFERENCES %s(id)'</span><span class="p">,</span><span class="w"> </span><span class="n">table_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_column_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_column_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_table_name</span><span class="p">);</span>
<span class="w"> </span><span class="k">end</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">;</span>
<span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="n">val</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'select %s from %s group by(%s)'</span><span class="p">,</span><span class="w"> </span><span class="n">column_name</span><span class="p">,</span><span class="w"> </span><span class="n">table_name</span><span class="p">,</span><span class="w"> </span><span class="n">column_name</span><span class="p">)</span><span class="w"> </span><span class="k">LOOP</span>
<span class="w"> </span><span class="k">RAISE</span><span class="w"> </span><span class="k">NOTICE</span><span class="w"> </span><span class="s1">'Inserting new value %s ...'</span><span class="p">,</span><span class="w"> </span><span class="n">val</span><span class="p">;</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'insert into %s(name) values (''%s'') returning id'</span><span class="p">,</span><span class="w"> </span><span class="n">new_table_name</span><span class="p">,</span><span class="w"> </span><span class="n">val</span><span class="p">)</span><span class="w"> </span><span class="k">into</span><span class="w"> </span><span class="n">current_id</span><span class="p">;</span>
<span class="w"> </span><span class="k">raise</span><span class="w"> </span><span class="k">notice</span><span class="w"> </span><span class="s1">'Created ID %'</span><span class="p">,</span><span class="w"> </span><span class="n">current_id</span><span class="p">;</span>
<span class="w"> </span><span class="k">execute</span><span class="w"> </span><span class="n">format</span><span class="w"> </span><span class="p">(</span><span class="s1">'update %s set %s = %s where %s = ''%s'''</span><span class="p">,</span><span class="w"> </span><span class="n">table_name</span><span class="p">,</span><span class="w"> </span><span class="n">new_column_name</span><span class="p">,</span><span class="n">current_id</span><span class="w"> </span><span class="p">,</span><span class="w"> </span><span class="n">column_name</span><span class="p">,</span><span class="w"> </span><span class="n">val</span><span class="w"> </span><span class="p">);</span>
<span class="w"> </span><span class="k">END</span><span class="w"> </span><span class="k">LOOP</span><span class="p">;</span>
<span class="w"> </span><span class="cm">/* Uncomment this if you want to drop the flat column</span>
<span class="cm"> raise notice 'Dropping colmn';</span>
<span class="cm"> execute format ('alter table %s drop column %s', table_name, column_name);</span>
<span class="cm"> */</span>
<span class="k">END</span><span class="p">;</span>
<span class="s">$$</span><span class="w"> </span><span class="k">LANGUAGE</span><span class="w"> </span><span class="n">plpgsql</span><span class="p">;</span>
</pre></div>
<p>The first line creates or updates the script. You can just copy over this script to an <span class="caps">SQL</span> window and run it as many times as you like (making changes
between runs) and the script will be always updated. The function that is created is actually a procedure since it returns <tt class="docutils literal">void</tt> and takes two parameters.
The <tt class="docutils literal"><span class="caps">DECLARE</span></tt> section that follows contains all the variables that are used in the script:</p>
<ul class="simple">
<li><tt class="docutils literal">val</tt> is the current value of the category when looping through their values</li>
<li><tt class="docutils literal">res</tt> is a boolean variable used for a conditional</li>
<li><tt class="docutils literal">new_table_name</tt> is the name of the table that will be created</li>
<li><tt class="docutils literal">new_column_name</tt> is the name of the column that will be added to the old table</li>
<li><tt class="docutils literal">current_id</tt> is the id of the last inserted value in the new table</li>
</ul>
<p>After the <tt class="docutils literal"><span class="caps">BEGIN</span></tt> the actual procedure starts: First the values of <tt class="docutils literal">new_table_name</tt> and <tt class="docutils literal">new_column_name</tt> are initialized to be used throughout the code and
then the new table is dropped (if exists) and re-created. Noticce the <tt class="docutils literal">execute format (parameter)</tt> function that executes the <span class="caps">SQL</span> contained in the parameter which
is a string and is constructed using the variables we’ve defined. The next line checks if the old table has the new column (i.e category_id) and saves
the result in the <tt class="docutils literal">res</tt> variable to check if the new column exists and if not add it to the old table.</p>
<p>A loop enumerating all unique values of the category column of the old table is then executed. Notice that <tt class="docutils literal">val</tt> will contain a single value since the <span class="caps">SQL</span> that is executed
will return a single column (that’s why it is declared as varchar). If we returned more than one column from the select the val could be declared as <tt class="docutils literal">record</tt> and access its
properties through dot notation (<tt class="docutils literal">val.prop1</tt> etc). The value is inserted to the newly created table using a <tt class="docutils literal">insert into table values () returning id</tt> <span class="caps">SQL</span> syntax
(so that the new id will be returned - this is an insert/select hybrid command) and saved to the <tt class="docutils literal">current_id</tt> variable. The <tt class="docutils literal">current_id</tt> variable then is used to update
the new column that was added to the old table with the proper foreign key value.</p>
<p>Notice that I’ve a commented out code in the end - if you want you can uncomment it and the old (flat) column will be dropped - so in my examply the <tt class="docutils literal">category</tt> column will be
removed since I will have <tt class="docutils literal">category_id</tt> to find out the name of each category. I recommend to uncomment this and actually drop the column since when you have both <tt class="docutils literal">category</tt>
and <tt class="docutils literal">category_id</tt> the values of these two columns are going to get out of sync and since you’ll have duplicate information your table will be even more non-normalized. You can
of course keep the column to make sure that the script works as you want since if the column is not dropped you can easily return to the previous state of the database by
removeing the new table and column.</p>
<p>To call it just run <tt class="docutils literal">select <span class="pre">export_relation('part',</span> 'category')</tt> and you should see some debug info in the messages tab. When the script is finished you’ll have the
<tt class="docutils literal">part_category</tt> table and <tt class="docutils literal">category_id</tt> column in the <tt class="docutils literal">part</tt> table.</p>
Creating custom components for ag-grid2017-01-03T09:55:00+02:002017-01-03T09:55:00+02:00Serafeim Papastefanostag:spapas.github.io,2017-01-03:/2017/01/03/ag-grid-custom-components/<p class="first last">How to create custom components (renderers and editors) for the excellent ag-grid grid library</p>
<p>Recently I had to integrate an application with a javascript (client-side) excel-like grid. After some research
I found out that <a class="reference external" href="https://www.ag-grid.com/">ag-grid</a> is (at least for me) the best javascript grid library. It has an open source version with a <span class="caps">MIT</span> license
so it can safely be used by all your projects and a commercial, enteprise version that includes a bunch of
extra features. The open source version should be sufficient for most projects however I really recommend
buying the commercial version to support this very useful library.</p>
<p>The greatest characteristic of ag-grid in my opinion is its openess and <span class="caps">API</span> that enables you to
extend it to support all your needs! An example of this <span class="caps">API</span> will be presented in this article where
I will implement two components that can be included in your grid:</p>
<ul class="simple">
<li>An array editor through which you will be able to create cells that can be used to insert a list of values. For example, if you have a grid of employees, you will be able to add a “children” cell that will contain the names of each employee’s children properly separated (one line per child)</li>
<li>An object editor through which you will be able to create cells that can be used to insert flat objects. For example, for the grid of employees, you may add an “address” cell that, when edited will be expanded to seperate fields for Address, City, Zip code and Country.</li>
</ul>
<div class="section" id="a-common-ground">
<h2>A common ground</h2>
<p>First of all, let’s create a simple example that will act as a common ground for our components:</p>
<div class="jsfiddle"><iframe width="100%" height="300" src="http://jsfiddle.net/6vmvdmzj/6/embedded/js,resources,html,css,result/light/" allowfullscreen="allowfullscreen" frameborder="0"></iframe></div><p>As you can see, I have defined a bunch of columns for the grid and then create a new grid passing it the <tt class="docutils literal">myGrid</tt> div
and the <tt class="docutils literal">gridOptions</tt> (which are kept to a minimum). Finally, there’s an event handler for button click that adds an
empty row:</p>
<div class="highlight"><pre><span></span><span class="kd">var</span><span class="w"> </span><span class="nx">columns</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">headerName</span><span class="o">:</span><span class="w"> </span><span class="s1">'ID'</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="o">:</span><span class="w"> </span><span class="s1">'id'</span><span class="p">,</span><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="mf">50</span><span class="p">,</span><span class="w"> </span><span class="nx">editable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">headerName</span><span class="o">:</span><span class="w"> </span><span class="s1">'Name'</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="o">:</span><span class="w"> </span><span class="s1">'name'</span><span class="p">,</span><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="mf">100</span><span class="p">,</span><span class="w"> </span><span class="nx">editable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">headerName</span><span class="o">:</span><span class="w"> </span><span class="s2">"Address"</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="o">:</span><span class="w"> </span><span class="s2">"address"</span><span class="p">,</span><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="mf">200</span><span class="p">,</span><span class="w"> </span><span class="nx">editable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">headerName</span><span class="o">:</span><span class="w"> </span><span class="s2">"Children"</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="o">:</span><span class="w"> </span><span class="s2">"children"</span><span class="p">,</span><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="mf">200</span><span class="p">,</span><span class="w"> </span><span class="nx">editable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">];</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">gridOptions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">columnDefs</span><span class="o">:</span><span class="w"> </span><span class="nx">columns</span><span class="p">,</span>
<span class="w"> </span><span class="nx">rowData</span><span class="o">:</span><span class="w"> </span><span class="p">[]</span>
<span class="p">};</span>
<span class="ow">new</span><span class="w"> </span><span class="nx">agGrid</span><span class="p">.</span><span class="nx">Grid</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#myGrid'</span><span class="p">),</span><span class="w"> </span><span class="nx">gridOptions</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">'#addRow'</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">"click"</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">gridOptions</span><span class="p">.</span><span class="nx">api</span><span class="p">.</span><span class="nx">addItems</span><span class="p">([{}]);</span>
<span class="p">});</span>
</pre></div>
<p>You may be able to edit both the address and the children of each employee in the above example however this is
not intuitive. The editors will be able to enter any kind of address they want and add the children seperated by commas,
spaces, dashes or whatever they want. Of course you could add validators to enforce some formatting for these fields
however I think that using custom components has a much better user experience.</p>
<p>Please notice that the above example along with the following components will be implemented in pure (no-framework) javascript. Integrating
ag-grid with a javascript framework like angular or react in an <span class="caps">SPA</span> should not be difficult however I find it easier to adjust my <span class="caps">SPA</span>
so that the grid component is seperate and does not need interoperability with other <span class="caps">SPA</span> components since all components like renderers
and editors will be integrated to the grid!</p>
<p>Also, using pure javascript for your
custom components makes them faster than adding another layer of indirection through react as can be seen on the on <a class="reference external" href="https://www.ag-grid.com/best-react-data-grid/index.php">ag-grid react integration</a>:</p>
<blockquote>
If you do use React, be aware that you are adding an extra layer of indirection into ag-Grid. ag-Grid’s internal framework is already highly tuned to work incredibly fast and does not require React or anything else to make it faster. If you are looking for a lightning fast grid, even if you are using React and the ag-grid-react component, consider using plain ag-Grid Components (as explained on the pages for rendering etc) inside ag-Grid instead of creating React counterparts.</blockquote>
</div>
<div class="section" id="editors-and-renderers">
<h2>Editors and renderers</h2>
<p>A custom column in ag-grid actually has two distinctive parts: An object that is used for <a class="reference external" href="https://www.ag-grid.com/javascript-grid-cell-rendering/">rendering</a> and an object that is used
for <a class="reference external" href="https://www.ag-grid.com/javascript-grid-cell-editing/">editing</a> the cell value. You can have columns with the built in editor and a custom renderer, columns with the built in renderer
and a custom editor and cells with custom editors and rendererers. I urge you to read the <a class="reference external" href="https://www.ag-grid.com/documentation-main/documentation.php">documentation</a> on both renderers and
editors in order to understand most of the decisions I have made for the implemented components.</p>
<p>A renderer can be:</p>
<ul class="simple">
<li>a function that receives the value of the cell and returns either an <span class="caps">HTML</span> string or a complete <span class="caps">DOM</span> object</li>
<li>a class that provides methods for returning the <span class="caps">HTML</span> string or <span class="caps">DOM</span> object for the cell</li>
</ul>
<p>The function is much easier to use however biting the bullet and providing a renderer class is better for non-trivial rendering.
This is because the function will be called each time the cell needs to be <em>refreshed</em> (refer to the docs on what refreshing means)
while, the class provides a specific <tt class="docutils literal">refresh()</tt> method that is called instead. This way, using the class you can generate the <span class="caps">DOM</span> structure
for the cell once, when it is first created and then when its value changes you’ll only call its refresh method to update the value. We’ll
see how this works later.</p>
<p>An editor is a class that should provide methods for returning the <span class="caps">DOM</span> structure for the cell editing (for example an <tt class="docutils literal"><input></tt> field)
and the current value of the field.</p>
<p>Both renderer and editor classes can be attached to columns using <tt class="docutils literal">cellEditor</tt> and <tt class="docutils literal">cellRenderer</tt> column properties. You also may
pass per-column properties to each cell using the <tt class="docutils literal">cellEditorParams</tt> and <tt class="docutils literal">cellRendererParams</tt> propertie. For example, you may have
a renderer for booleans that displays icons for true/false and you want to use different icons depending on the column type, or you
may want to create a validation-editor that receives a function and accepts the value you enter only if the function returns true - the
valid function could be different for different column types.</p>
</div>
<div class="section" id="creating-the-object-cell-editor">
<h2>Creating the object cell editor</h2>
<p>The first component we’ll present here is an object renderer/editor. This component will receiver a list of fields and will
allow the user to edit them in a popup grouped together. Here’s a fiddle with the Address of each employee using the
object editing component:</p>
<div class="jsfiddle"><iframe width="100%" height="300" src="http://jsfiddle.net/0o85uywr/4/embedded/js,resources,html,css,result/light/" allowfullscreen="allowfullscreen" frameborder="0"></iframe></div><p>To integrate it with the ag-grid I’ve added an addressFields list containg the fields of the object like this:</p>
<div class="highlight"><pre><span></span><span class="kd">var</span><span class="w"> </span><span class="nx">addressFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'address'</span><span class="p">,</span><span class="w"> </span><span class="s1">'label'</span><span class="o">:</span><span class="w"> </span><span class="s1">'Address'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'zip'</span><span class="p">,</span><span class="w"> </span><span class="s1">'label'</span><span class="o">:</span><span class="w"> </span><span class="s1">'ZIP'</span><span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="s1">'city'</span><span class="p">,</span><span class="w"> </span><span class="s1">'label'</span><span class="o">:</span><span class="w"> </span><span class="s1">'City'</span><span class="w"> </span><span class="p">},</span>
<span class="p">]</span>
</pre></div>
<p>and then passed this as a parameter to both the renderer and editor for the address field:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="nx">headerName</span><span class="o">:</span><span class="w"> </span><span class="s2">"Address"</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="o">:</span><span class="w"> </span><span class="s2">"address"</span><span class="p">,</span><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="mf">200</span><span class="p">,</span><span class="w"> </span><span class="nx">editable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nx">cellRenderer</span><span class="o">:</span><span class="w"> </span><span class="nx">ObjectCellRenderer</span><span class="p">,</span>
<span class="w"> </span><span class="nx">cellEditor</span><span class="o">:</span><span class="w"> </span><span class="nx">ObjectEditor</span><span class="p">,</span>
<span class="w"> </span><span class="nx">cellEditorParams</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">fields</span><span class="o">:</span><span class="w"> </span><span class="nx">addressFields</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">cellRendererParams</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">fields</span><span class="o">:</span><span class="w"> </span><span class="nx">addressFields</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">ObjectEditor</tt> and <tt class="docutils literal">ObjectCellRenderer</tt> are the actual editor and renderer of the component. I will start by representing the renderer first:</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">ObjectCellRenderer</span><span class="p">()</span><span class="w"> </span><span class="p">{}</span>
<span class="nx">ObjectCellRenderer</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Create the DOM element to display</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'span'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">innerHTML</span><span class="o">=</span><span class="s1">''</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">refresh</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span>
<span class="p">};</span>
</pre></div>
<p>The ObjectCellRender is an javascript object to which we define an <tt class="docutils literal">init</tt> method. This method will be called by ag-grid when
the component is first created, passing it a params object with various useful params, like the user-defined parameters (from <tt class="docutils literal">cellRendererParams</tt>)
and the actual value of othe cell. We just create an empty span <span class="caps">DOM</span> element that will be used to display the value of the object and call <tt class="docutils literal">refresh</tt>.</p>
<div class="highlight"><pre><span></span><span class="nx">ObjectCellRenderer</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">refresh</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">''</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// If we have a value build the representation</span>
<span class="w"> </span><span class="k">for</span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="o">=</span><span class="mf">0</span><span class="p">;</span><span class="nx">i</span><span class="o"><</span><span class="nx">params</span><span class="p">.</span><span class="nx">fields</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">fields</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">label</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">': '</span><span class="p">;</span>
<span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">[</span><span class="nx">params</span><span class="p">.</span><span class="nx">fields</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">name</span><span class="p">]</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' '</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="c1">// Put representation to the DOM element</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">innerHTML</span><span class="o">=</span><span class="nx">res</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">ObjectCellRenderer</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getGui</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<p>The <tt class="docutils literal">refresh</tt> method generates the text value of the cell (that will be put inside the span we created in init). It first checks if the <tt class="docutils literal">value</tt> attribute
of <tt class="docutils literal">params</tt> is defined and if yes, it appends the label of each object attribute (which we pass through <tt class="docutils literal">cellRendererParams.fields.label</tt>) along with its value
(which is retrieved from the <tt class="docutils literal">params.value</tt> using <tt class="docutils literal">cellRendererParams.fields.name</tt>). Notice ag-grid puts the result of the <tt class="docutils literal">getGui</tt> method in the cell - so
we just return the span we create. Also, we created the span element in init but filled it in refresh - to avoid it creating the same element lots of times (this would
be more imporntant of course on more expensive operations).</p>
<p>Now let’s continue with <tt class="docutils literal">ObjectEditor</tt>:</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">ObjectEditor</span><span class="p">()</span><span class="w"> </span><span class="p">{}</span>
<span class="c1">// Surpress some keypresses</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">onKeyDown</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">which</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">keyCode</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">37</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="c1">// left</span>
<span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">39</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="c1">// right</span>
<span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">9</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// tab</span>
<span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">stopPropagation</span><span class="p">();</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="nx">ObjectEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Create the container DOM element</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'div'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">style</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"border-radius: 15px; border: 1px solid grey;background: #e6e6e6;padding: 10px; "</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">onkeydown</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">onKeyDown</span>
<span class="w"> </span><span class="c1">// Create the object-editing form</span>
<span class="w"> </span><span class="k">for</span><span class="p">(</span><span class="nx">i</span><span class="o">=</span><span class="mf">0</span><span class="p">;</span><span class="nx">i</span><span class="o"><</span><span class="nx">params</span><span class="p">.</span><span class="nx">fields</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">field</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">fields</span><span class="p">[</span><span class="nx">i</span><span class="p">];</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'label'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">label</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">field</span><span class="p">.</span><span class="nx">label</span><span class="o">+</span><span class="s1">': '</span><span class="p">;</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">input</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'input'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">[</span><span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="p">];</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">label</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">input</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'br'</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="c1">// Create a save button</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">saveButton</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'button'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">saveButton</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">createTextNode</span><span class="p">(</span><span class="s1">'Ok'</span><span class="p">))</span>
<span class="w"> </span><span class="nx">saveButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">stopEditing</span><span class="p">();</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">saveButton</span><span class="p">);</span>
<span class="p">};</span>
</pre></div>
<p>The <tt class="docutils literal">init</tt> function of ObjectEditor ise used to create a container div element that will hold the actual input elements. Then, using the fields
that were passed as a parameter to the editor it creates a <tt class="docutils literal">label</tt>, an <tt class="docutils literal">input</tt> and a <tt class="docutils literal">br</tt> element and inserts them one by one to the container div. The input is
instantiated with the current value of each attribute while its name is taken from the name of the corresponding field (from the fields parameter).
Finally, a saveButton is created that will stop the editing when clicked.</p>
<div class="highlight"><pre><span></span><span class="nx">ObjectEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getGui</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">ObjectEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">afterGuiAttached</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s1">'input'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">inputs</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">focus</span><span class="p">();</span>
<span class="p">};</span>
<span class="nx">ObjectEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span>
<span class="w"> </span><span class="c1">// Create the cell value (an object) from the inputs values</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s1">'input'</span><span class="p">);</span>
<span class="w"> </span><span class="k">for</span><span class="p">(</span><span class="nx">j</span><span class="o">=</span><span class="mf">0</span><span class="p">;</span><span class="nx">j</span><span class="o"><</span><span class="nx">inputs</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">j</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">res</span><span class="p">[</span><span class="nx">inputs</span><span class="p">[</span><span class="nx">j</span><span class="p">].</span><span class="nx">name</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">inputs</span><span class="p">[</span><span class="nx">j</span><span class="p">].</span><span class="nx">value</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/^\s+|\s+$/g</span><span class="p">,</span><span class="w"> </span><span class="s2">""</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">res</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">ObjectEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">destroy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="p">};</span>
<span class="nx">ObjectEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">isPopup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<p>The other methods of ObjectEditor are simpler: <tt class="docutils literal">getGui</tt> actually returns the container we built in the <tt class="docutils literal">init</tt>, <tt class="docutils literal">afterGuiAttached</tt>
is called when the component is attached to the <span class="caps">DOM</span> and focuses on the first input element, <tt class="docutils literal">getValue</tt> enumerates the input elements,
takes their value (and names) and return an object with the name/value pairs, <tt class="docutils literal">destroy</tt> dosn’t do anything however it must be defined
and can be used for cleaning up if needed and <tt class="docutils literal">isPopup</tt> returns true to display the container as a popup instead of inline.</p>
</div>
<div class="section" id="creating-the-array-like-cell-editor">
<h2>Creating the array-like cell editor</h2>
<p>The multi-line renderer/editor will allow a cell to contain a list of values. Here’s the complete fiddle where the “children” column
is using the multi-line component:</p>
<div class="jsfiddle"><iframe width="100%" height="300" src="http://jsfiddle.net/a6s7r4tq/1/embedded/js,resources,html,css,result/light/" allowfullscreen="allowfullscreen" frameborder="0"></iframe></div><p>To integrate it with ag-grid we just need to use the corresponding editor and renderer:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="nx">headerName</span><span class="o">:</span><span class="w"> </span><span class="s2">"Children"</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="o">:</span><span class="w"> </span><span class="s2">"children"</span><span class="p">,</span><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="mf">200</span><span class="p">,</span><span class="w"> </span><span class="nx">editable</span><span class="o">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nx">cellRenderer</span><span class="o">:</span><span class="w"> </span><span class="nx">MultiLineCellRenderer</span><span class="p">,</span>
<span class="w"> </span><span class="nx">cellEditor</span><span class="o">:</span><span class="w"> </span><span class="nx">MultiLineEditor</span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">MultiLineCellRenderer</tt> is similar to the <tt class="docutils literal">ObjectCellRenderer</tt>. A span/container element is created
at the <tt class="docutils literal">init</tt> method and the <tt class="docutils literal">refresh</tt> method is called to fill it. The <tt class="docutils literal">refresh</tt> method outputs the
number of items in the list (i.e it writes N items) and uses the span’s <tt class="docutils literal">title</tt> to display a tooltip with
the values of the items:</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">MultiLineCellRenderer</span><span class="p">()</span><span class="w"> </span><span class="p">{}</span>
<span class="nx">MultiLineCellRenderer</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'span'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">title</span><span class="o">=</span><span class="s1">''</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">innerHTML</span><span class="o">=</span><span class="s1">''</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">refresh</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">MultiLineCellRenderer</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">refresh</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="kc">undefined</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'0 items'</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">''</span>
<span class="w"> </span><span class="c1">// Create the tooltip for the cell</span>
<span class="w"> </span><span class="k">for</span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="o">=</span><span class="mf">0</span><span class="p">;</span><span class="nx">i</span><span class="o"><</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">(</span><span class="nx">i</span><span class="o">+</span><span class="mf">1</span><span class="p">)</span><span class="o">+</span><span class="s1">': '</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
<span class="w"> </span><span class="nx">res</span><span class="o">+=</span><span class="s1">'\n'</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">res</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">' item'</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">length</span><span class="o">==</span><span class="mf">1</span><span class="o">?</span><span class="s2">""</span><span class="o">:</span><span class="s2">"s"</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
<span class="nx">MultiLineCellRenderer</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getGui</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">span</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<p>The logic of the <tt class="docutils literal">MultiLineEditor</tt> is also similar to the <tt class="docutils literal">ObjectEditor</tt>:</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">MultiLineEditor</span><span class="p">()</span><span class="w"> </span><span class="p">{}</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">onKeyDown</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">which</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">keyCode</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">37</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="c1">// left</span>
<span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">39</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="c1">// right</span>
<span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mf">9</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c1">// tab</span>
<span class="w"> </span><span class="nx">event</span><span class="p">.</span><span class="nx">stopPropagation</span><span class="p">();</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
<span class="c1">// Used to append list items (their value along with the remove button)</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">addLine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">li</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'li'</span><span class="p">);</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">span</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'span'</span><span class="p">);</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">removeButton</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'button'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">removeButton</span><span class="p">.</span><span class="nx">style</span><span class="o">=</span><span class="s1">'margin-left: 5px; text-align: right; '</span>
<span class="w"> </span><span class="nx">removeButton</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Remove'</span>
<span class="w"> </span><span class="nx">span</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">val</span><span class="p">;</span>
<span class="w"> </span><span class="nx">li</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">span</span><span class="p">)</span>
<span class="w"> </span><span class="nx">li</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">removeButton</span><span class="p">)</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">ul</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">li</span><span class="p">);</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">that</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">;</span>
<span class="w"> </span><span class="nx">removeButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">that</span><span class="p">.</span><span class="nx">ul</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">}</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">init</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">that</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'div'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">style</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"border-radius: 15px; border: 1px solid grey;background: #e6e6e6;padding: 10px;"</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">ul</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'ol'</span><span class="p">);</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">for</span><span class="p">(</span><span class="nx">i</span><span class="o">=</span><span class="mf">0</span><span class="p">;</span><span class="nx">i</span><span class="o"><</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">addLine</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">value</span><span class="p">[</span><span class="nx">i</span><span class="p">]);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">ul</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">input</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'input'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">input</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">addButton</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'button'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">addButton</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Add'</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">addButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">val</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">that</span><span class="p">.</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">val</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">val</span><span class="o">==</span><span class="kc">undefined</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">val</span><span class="o">==</span><span class="s1">''</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="p">;</span>
<span class="w"> </span><span class="nx">that</span><span class="p">.</span><span class="nx">addLine</span><span class="p">(</span><span class="nx">val</span><span class="p">,</span><span class="w"> </span><span class="nx">that</span><span class="p">.</span><span class="nx">ul</span><span class="p">);</span>
<span class="w"> </span><span class="nx">that</span><span class="p">.</span><span class="nx">input</span><span class="p">.</span><span class="nx">value</span><span class="o">=</span><span class="s1">''</span><span class="p">;</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">addButton</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">saveButton</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">'button'</span><span class="p">);</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">saveButton</span><span class="p">.</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Ok'</span><span class="p">;</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">saveButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">params</span><span class="p">.</span><span class="nx">stopEditing</span><span class="p">();</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">saveButton</span><span class="p">);</span>
<span class="p">};</span>
</pre></div>
<p>A <tt class="docutils literal">div</tt> element that will display the popup editor is created first. In this container we add a <tt class="docutils literal">ol</tt> element and then,
if there are values in the list they will be
appended in that <tt class="docutils literal">ol</tt> using the <tt class="docutils literal">addLine</tt> method. This method creates a <tt class="docutils literal">li</tt> element the value of each list item along with a
remove <tt class="docutils literal">button</tt> which removes the corresponding <tt class="docutils literal">li</tt> element when clicked (that <tt class="docutils literal">that=this</tt> is needed because
the click callback function of the button has a different <tt class="docutils literal">this</tt> than the <tt class="docutils literal">addLine</tt> method so <tt class="docutils literal">that</tt> needs
to be used instead).</p>
<p>After the list of the items, there’s an <tt class="docutils literal">input</tt> whose value will be inserted to the list when clicking the
add <tt class="docutils literal">button</tt>. The same <tt class="docutils literal">addLine</tt> function we used when initializing the component is used here to append the
input’s value to the <tt class="docutils literal">ol</tt>. Finally the save <tt class="docutils literal">button</tt> stops the list editing:</p>
<div class="highlight"><pre><span></span><span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getGui</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">afterGuiAttached</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">inputs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s1">'input'</span><span class="p">);</span>
<span class="w"> </span><span class="nx">inputs</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">focus</span><span class="p">();</span>
<span class="p">};</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[];</span>
<span class="w"> </span><span class="c1">// The value is the text of all the span items of the li items</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">items</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">ul</span><span class="p">.</span><span class="nx">getElementsByTagName</span><span class="p">(</span><span class="s1">'span'</span><span class="p">);</span>
<span class="w"> </span><span class="k">for</span><span class="p">(</span><span class="kd">var</span><span class="w"> </span><span class="nx">i</span><span class="o">=</span><span class="mf">0</span><span class="p">;</span><span class="nx">i</span><span class="o"><</span><span class="nx">items</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span><span class="nx">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">res</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">items</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">innerHTML</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">res</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">destroy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="p">};</span>
<span class="nx">MultiLineEditor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">isPopup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>
<p>For the value of this component, we just enumerate through the <tt class="docutils literal">li span</tt> items of the list we created through <tt class="docutils literal">addLine</tt>
and add it to a normal javascript array.</p>
</div>
<div class="section" id="some-more-discussion-about-the-components">
<h2>Some more discussion about the components</h2>
<p>The above components can be used as they are however I think that their greatest value is that they should show-off some of
the capabilities of ag-grid and used as templates to build up your own stuff. Beyond styling or changing the layout
of the multi-line and the object editor, I can think of a great number of extensions for them. Some examples, left as
an excerise to the reader:</p>
<ul class="simple">
<li>The renderer components are rather simple, you may create any <span class="caps">DOM</span> structure you want in them</li>
<li>Pass a function as a parameter to the object or multi-line editor that will be used as a validator for the values (i.e not allow nulls or enforce a specific format) - then you can add another “error” <tt class="docutils literal">span</tt> that will be displayed if the validation does not pass</li>
<li>Make the object editor smarter by passing the type of each attribute (i.e text, number, boolean, date) and check that each input corresponds to that type</li>
<li>Pass a min/max number of list items for the multi-line editor</li>
<li>The multi-line editor doesn’t really need to have only text values for each item. You could combine the multi-line editor with the object editor so that each line has a specific, object-like structure. For example, for each child we may have two attributes, name and date of birth</li>
</ul>
</div>
Getting a logical backup of all databases of your Postgresql server2016-11-02T15:10:00+02:002016-11-02T15:10:00+02:00Serafeim Papastefanostag:spapas.github.io,2016-11-02:/2016/11/02/postgresql-backup/<p class="first last">A script to help you backup your postgresql databases</p>
<p>In this small post I will present a small bash script that could be used to create logical backups of all the databases in a Postgresql server along
with some other goodies. More specifically, the script will:</p>
<ul class="simple">
<li>Create two files at /tmp to output information (one for debugging and one with info for sending it through email at the end)</li>
<li>Create a backup directory with the current date</li>
<li>Create a list of all databases found on the server</li>
<li>For each database, vacuum and analyze it, backup it, gzip it and put it in the backup directory</li>
<li>Write info about the backup in the info log file</li>
<li>Do the same for global objects</li>
<li>Send an email when the backup is finished with the info log</li>
</ul>
<p>Now, in my system I’m using an external folder at <tt class="docutils literal">/mnt/backupdb</tt> to put my backups. You may either use the same technique or connect remotely to a
postgresql database (so you need to change the parameters of <tt class="docutils literal">vacuumdb</tt>, <tt class="docutils literal">pg_dump</tt> and <tt class="docutils literal">pg_dumpall</tt> to define the server and credentials to connect to)
and put the backups to a local disc.</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/sh</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span>><span class="w"> </span>/tmp/db_backup.log
<span class="nb">echo</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span>><span class="w"> </span>/tmp/db_backup_info.log
<span class="nv">date_str</span><span class="o">=</span><span class="k">$(</span>date<span class="w"> </span>+<span class="s2">"%Y%m%d_%H%M%S"</span><span class="k">)</span>
<span class="nv">backup_dir</span><span class="o">=</span>/mnt/backupdb/pg_backup.<span class="nv">$date_str</span>
mkdir<span class="w"> </span><span class="nv">$backup_dir</span>
<span class="nb">pushd</span><span class="w"> </span><span class="nv">$backup_dir</span><span class="w"> </span>><span class="w"> </span>/dev/null
<span class="nv">dbs</span><span class="o">=</span><span class="sb">`</span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>psql<span class="w"> </span>-Upostgres<span class="w"> </span>-lt<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-v<span class="w"> </span>:<span class="w"> </span><span class="p">|</span><span class="w"> </span>cut<span class="w"> </span>-d<span class="w"> </span><span class="se">\|</span><span class="w"> </span>-f<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-v<span class="w"> </span>template<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-v<span class="w"> </span>-e<span class="w"> </span><span class="s1">'^\s*$'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-e<span class="w"> </span><span class="s1">'s/ *$//'</span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span><span class="s1">'\n'</span><span class="w"> </span><span class="s1">' '</span><span class="sb">`</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Will backup: </span><span class="nv">$dbs</span><span class="s2"> to </span><span class="nv">$backup_dir</span><span class="s2">"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="k">for</span><span class="w"> </span>db<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nv">$dbs</span><span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Starting backup for </span><span class="nv">$db</span><span class="s2">"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="w"> </span><span class="nv">filename</span><span class="o">=</span><span class="nv">$db</span>.<span class="nv">$date_str</span>.sql.gz
<span class="w"> </span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>vacuumdb<span class="w"> </span>--analyze<span class="w"> </span>-Upostgres<span class="w"> </span><span class="nv">$db</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup.log
<span class="w"> </span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>pg_dump<span class="w"> </span>-Upostgres<span class="w"> </span>-v<span class="w"> </span><span class="nv">$db</span><span class="w"> </span>-F<span class="w"> </span>p<span class="w"> </span><span class="m">2</span>>><span class="w"> </span>/tmp/db_backup.log<span class="w"> </span><span class="p">|</span><span class="w"> </span>gzip<span class="w"> </span>><span class="w"> </span><span class="nv">$filename</span>
<span class="w"> </span><span class="nv">size</span><span class="o">=</span><span class="sb">`</span>stat<span class="w"> </span><span class="nv">$filename</span><span class="w"> </span>--printf<span class="o">=</span><span class="s2">"%s"</span><span class="sb">`</span>
<span class="w"> </span><span class="nv">kb_size</span><span class="o">=</span><span class="sb">`</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"scale=2; </span><span class="nv">$size</span><span class="s2"> / 1024.0"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>bc<span class="sb">`</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Finished backup for </span><span class="nv">$db</span><span class="s2"> - size is </span><span class="nv">$kb_size</span><span class="s2"> KB"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="k">done</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Backing up global objects"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="nv">filename</span><span class="o">=</span>global.<span class="nv">$date_str</span>.sql.gz
sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>pg_dumpall<span class="w"> </span>-Upostgres<span class="w"> </span>-v<span class="w"> </span>-g<span class="w"> </span><span class="m">2</span>>><span class="w"> </span>/tmp/db_backup.log<span class="w"> </span><span class="p">|</span><span class="w"> </span>gzip<span class="w"> </span>><span class="w"> </span><span class="nv">$filename</span>
<span class="nv">size</span><span class="o">=</span><span class="sb">`</span>stat<span class="w"> </span><span class="nv">$filename</span><span class="w"> </span>--printf<span class="o">=</span><span class="s2">"%s"</span><span class="sb">`</span>
<span class="nv">kb_size</span><span class="o">=</span><span class="sb">`</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"scale=2; </span><span class="nv">$size</span><span class="s2"> / 1024.0"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>bc<span class="sb">`</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Finished backup for global - size is </span><span class="nv">$kb_size</span><span class="s2"> KB"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Ok!"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
mail<span class="w"> </span>-s<span class="w"> </span><span class="s2">"Backup results"</span><span class="w"> </span>spapas@mymail.foo.bar<span class="w"> </span><<span class="w"> </span>/tmp/db_backup_info.log
<span class="nb">popd</span><span class="w"> </span>><span class="w"> </span>/dev/null
</pre></div>
<p>Let’s explain a bit the above script: The two first lines (echo …) will just clear out the two files <tt class="docutils literal">/tmp/db_backup.log</tt> and <tt class="docutils literal">/tmp/db_backup_info.log</tt>. The first
will contain debug info from the commands and the second one will contain our info that will be sent through an email at the end of the backup. After that, we initialize
<tt class="docutils literal">date_str</tt> with the current date in the form <tt class="docutils literal">20161102_145011</tt> and the backup_dir with the correct directory to save the backups to. We then create the backup directory
and switch to it with <tt class="docutils literal">pushd</tt>.</p>
<p>The following, rather long command will assign the names of the databases to the <tt class="docutils literal">dbs</tt> variable. So how is it working? <tt class="docutils literal">psql <span class="pre">-lt</span></tt> lists the names of the databases, but lists
also more non-needed information which we remove with the following commands (grep, cut etc). The sed removes whitespace and the tr concatenates individual lines to a single line
so dbs will have a value like ‘db1 db2 …’. For each one of these files then we assign its name and date to a filename and then, after we execute vacuumdb we use pg_dump with gzip to actually
create the backup and output it to the file. The other two lines (size and kb_size) are used to calculate the size of the backup file (to be sure that something is actually created) - you’ll
need to install bc for that. The same process is followed the to backup global objects (usernames etc) using <tt class="docutils literal">pg_dumpall <span class="pre">-g</span></tt>. Finally, we send a mail with a subject of “Backup results”
and body the contents of <tt class="docutils literal">/tmp/db_backup_info.log</tt>.</p>
<p>I’ve saved this file to <tt class="docutils literal">/var/lib/pgsql/db_backup_all.sh</tt>. To run I propose using cron — just edit your crontab (through <tt class="docutils literal">vi /etc/crontab</tt>) and add the line</p>
<pre class="code literal-block">
15 2 * * * root /usr/bin/bash /var/lib/pgsql/db_backup_all.sh
</pre>
<p>This will run the backup every night at 2.15. Uses the root user to have access rights to the backup folder. One thing to be careful about is that on Redhat/Centos distributions,
the above won’t work because sudo requires a tty to work and cron doesn’t have one. To fix this, comment out the line</p>
<pre class="code literal-block">
Defaults requiretty
</pre>
<p>of your /etc/sudoers file.</p>
<p><strong>Update 02/12/2016:</strong> Here’s a little better version of the above script that</p>
<ol class="arabic simple">
<li>Create two files for each database, one with <span class="caps">SQL</span> script backup, one with binary backup. Although with <span class="caps">SQL</span> backup you can check out the backup and maybe do changes before applying it, the binary backup is a more foolproof method of restoring everything to your database! Also, instead of restoring the database through <tt class="docutils literal">psql</tt> (as required by the <span class="caps">SQL</span> script backup), using the binary backup you can restore through the <tt class="docutils literal">pg_restore</tt> tool.</li>
<li>Adds a function to output the file size (so the script is more <span class="caps">DRY</span>)</li>
</ol>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/sh</span>
<span class="k">function</span><span class="w"> </span>output_file_size<span class="w"> </span><span class="o">{</span>
<span class="w"> </span><span class="nv">size</span><span class="o">=</span><span class="sb">`</span>stat<span class="w"> </span><span class="nv">$1</span><span class="w"> </span>--printf<span class="o">=</span><span class="s2">"%s"</span><span class="sb">`</span>
<span class="w"> </span><span class="nv">kb_size</span><span class="o">=</span><span class="sb">`</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"scale=2; </span><span class="nv">$size</span><span class="s2"> / 1024.0"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>bc<span class="sb">`</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Finished backup for </span><span class="nv">$2</span><span class="s2"> - size is </span><span class="nv">$kb_size</span><span class="s2"> KB"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="o">}</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span>><span class="w"> </span>/tmp/db_backup.log
<span class="nb">echo</span><span class="w"> </span><span class="s2">""</span><span class="w"> </span>><span class="w"> </span>/tmp/db_backup_info.log
<span class="nv">date_str</span><span class="o">=</span><span class="k">$(</span>date<span class="w"> </span>+<span class="s2">"%Y%m%d_%H%M%S"</span><span class="k">)</span>
<span class="nv">backup_dir</span><span class="o">=</span>/mnt/backupdb/dbpg/pg_backup.<span class="nv">$date_str</span>
mkdir<span class="w"> </span><span class="nv">$backup_dir</span>
<span class="nb">pushd</span><span class="w"> </span><span class="nv">$backup_dir</span><span class="w"> </span>><span class="w"> </span>/dev/null
<span class="nv">dbs</span><span class="o">=</span><span class="sb">`</span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>psql<span class="w"> </span>-Upostgres<span class="w"> </span>-lt<span class="w"> </span><span class="p">|</span><span class="w"> </span>cut<span class="w"> </span>-d<span class="w"> </span><span class="se">\|</span><span class="w"> </span>-f<span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-v<span class="w"> </span>template<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-v<span class="w"> </span>-e<span class="w"> </span><span class="s1">'^\s*$'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-e<span class="w"> </span><span class="s1">'s/ *$//'</span><span class="p">|</span><span class="w"> </span>tr<span class="w"> </span><span class="s1">'\n'</span><span class="w"> </span><span class="s1">' '</span><span class="sb">`</span>
<span class="c1">#dbs='dgul hrms mailer_server missions postgres'</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Will backup: </span><span class="nv">$dbs</span><span class="s2"> to </span><span class="nv">$backup_dir</span><span class="s2">"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="k">for</span><span class="w"> </span>db<span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="nv">$dbs</span><span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Starting backup for </span><span class="nv">$db</span><span class="s2">"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="w"> </span><span class="nv">filename</span><span class="o">=</span><span class="nv">$db</span>.<span class="nv">$date_str</span>.sql.gz
<span class="w"> </span><span class="nv">filename_binary</span><span class="o">=</span><span class="nv">$db</span>.<span class="nv">$date_str</span>.bak.gz
<span class="w"> </span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>vacuumdb<span class="w"> </span>--analyze<span class="w"> </span>-Upostgres<span class="w"> </span><span class="nv">$db</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup.log
<span class="w"> </span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>pg_dump<span class="w"> </span>-Upostgres<span class="w"> </span>-v<span class="w"> </span><span class="nv">$db</span><span class="w"> </span>-F<span class="w"> </span>p<span class="w"> </span><span class="m">2</span>>><span class="w"> </span>/tmp/db_backup.log<span class="w"> </span><span class="p">|</span><span class="w"> </span>gzip<span class="w"> </span>><span class="w"> </span><span class="nv">$filename</span>
<span class="w"> </span>sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>pg_dump<span class="w"> </span>-Upostgres<span class="w"> </span>-v<span class="w"> </span><span class="nv">$db</span><span class="w"> </span>-F<span class="w"> </span>c<span class="w"> </span><span class="m">2</span>>><span class="w"> </span>/tmp/db_backup.log<span class="w"> </span><span class="p">|</span><span class="w"> </span>gzip<span class="w"> </span>><span class="w"> </span><span class="nv">$filename_binary</span>
<span class="w"> </span>output_file_size<span class="w"> </span><span class="nv">$filename</span><span class="w"> </span><span class="s2">"</span><span class="nv">$db</span><span class="s2"> sql"</span>
<span class="w"> </span>output_file_size<span class="w"> </span><span class="nv">$filename_binary</span><span class="w"> </span><span class="s2">"</span><span class="nv">$db</span><span class="s2"> bin"</span>
<span class="k">done</span>
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Backing up global objects"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
<span class="nv">filename</span><span class="o">=</span>global.<span class="nv">$date_str</span>.sql.gz
sudo<span class="w"> </span>-u<span class="w"> </span>postgres<span class="w"> </span>pg_dumpall<span class="w"> </span>-Upostgres<span class="w"> </span>-v<span class="w"> </span>-g<span class="w"> </span><span class="m">2</span>>><span class="w"> </span>/tmp/db_backup.log<span class="w"> </span><span class="p">|</span><span class="w"> </span>gzip<span class="w"> </span>><span class="w"> </span><span class="nv">$filename</span>
output_file_size<span class="w"> </span><span class="nv">$filename</span><span class="w"> </span>global
<span class="nb">echo</span><span class="w"> </span><span class="s2">"Ok!"</span><span class="w"> </span>>><span class="w"> </span>/tmp/db_backup_info.log
mail<span class="w"> </span>-s<span class="w"> </span><span class="s2">"Backup results"</span><span class="w"> </span>spapas@hcg.gr<span class="w"> </span><<span class="w"> </span>/tmp/db_backup_info.log
<span class="nb">popd</span><span class="w"> </span>><span class="w"> </span>/dev/null
</pre></div>
A pandas pivot_table primer2016-09-21T11:20:00+03:002016-09-21T11:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2016-09-21:/2016/09/21/pandas-pivot-table-primer/<p>A primer on how to use the very useful pivot_table command of a pandas dataframe.</p><!DOCTYPE html>
<html>
<head><meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notebook</title><script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js"></script>
<style type="text/css">
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: var(--jp-cell-editor-active-background) }
.highlight { background: var(--jp-cell-editor-background); color: var(--jp-mirror-editor-variable-color) }
.highlight .c { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment */
.highlight .err { color: var(--jp-mirror-editor-error-color) } /* Error */
.highlight .k { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword */
.highlight .o { color: var(--jp-mirror-editor-operator-color); font-weight: bold } /* Operator */
.highlight .p { color: var(--jp-mirror-editor-punctuation-color) } /* Punctuation */
.highlight .ch { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment.Multiline */
.highlight .cp { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment.Preproc */
.highlight .cpf { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment.Single */
.highlight .cs { color: var(--jp-mirror-editor-comment-color); font-style: italic } /* Comment.Special */
.highlight .kc { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: var(--jp-mirror-editor-keyword-color); font-weight: bold } /* Keyword.Type */
.highlight .m { color: var(--jp-mirror-editor-number-color) } /* Literal.Number */
.highlight .s { color: var(--jp-mirror-editor-string-color) } /* Literal.String */
.highlight .ow { color: var(--jp-mirror-editor-operator-color); font-weight: bold } /* Operator.Word */
.highlight .pm { color: var(--jp-mirror-editor-punctuation-color) } /* Punctuation.Marker */
.highlight .w { color: var(--jp-mirror-editor-variable-color) } /* Text.Whitespace */
.highlight .mb { color: var(--jp-mirror-editor-number-color) } /* Literal.Number.Bin */
.highlight .mf { color: var(--jp-mirror-editor-number-color) } /* Literal.Number.Float */
.highlight .mh { color: var(--jp-mirror-editor-number-color) } /* Literal.Number.Hex */
.highlight .mi { color: var(--jp-mirror-editor-number-color) } /* Literal.Number.Integer */
.highlight .mo { color: var(--jp-mirror-editor-number-color) } /* Literal.Number.Oct */
.highlight .sa { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Affix */
.highlight .sb { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Backtick */
.highlight .sc { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Char */
.highlight .dl { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Delimiter */
.highlight .sd { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Doc */
.highlight .s2 { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Double */
.highlight .se { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Escape */
.highlight .sh { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Heredoc */
.highlight .si { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Interpol */
.highlight .sx { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Other */
.highlight .sr { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Regex */
.highlight .s1 { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Single */
.highlight .ss { color: var(--jp-mirror-editor-string-color) } /* Literal.String.Symbol */
.highlight .il { color: var(--jp-mirror-editor-number-color) } /* Literal.Number.Integer.Long */
</style>
<style type="text/css">
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*
* Mozilla scrollbar styling
*/
/* use standard opaque scrollbars for most nodes */
[data-jp-theme-scrollbars='true'] {
scrollbar-color: rgb(var(--jp-scrollbar-thumb-color))
var(--jp-scrollbar-background-color);
}
/* for code nodes, use a transparent style of scrollbar. These selectors
* will match lower in the tree, and so will override the above */
[data-jp-theme-scrollbars='true'] .CodeMirror-hscrollbar,
[data-jp-theme-scrollbars='true'] .CodeMirror-vscrollbar {
scrollbar-color: rgba(var(--jp-scrollbar-thumb-color), 0.5) transparent;
}
/* tiny scrollbar */
.jp-scrollbar-tiny {
scrollbar-color: rgba(var(--jp-scrollbar-thumb-color), 0.5) transparent;
scrollbar-width: thin;
}
/*
* Webkit scrollbar styling
*/
/* use standard opaque scrollbars for most nodes */
[data-jp-theme-scrollbars='true'] ::-webkit-scrollbar,
[data-jp-theme-scrollbars='true'] ::-webkit-scrollbar-corner {
background: var(--jp-scrollbar-background-color);
}
[data-jp-theme-scrollbars='true'] ::-webkit-scrollbar-thumb {
background: rgb(var(--jp-scrollbar-thumb-color));
border: var(--jp-scrollbar-thumb-margin) solid transparent;
background-clip: content-box;
border-radius: var(--jp-scrollbar-thumb-radius);
}
[data-jp-theme-scrollbars='true'] ::-webkit-scrollbar-track:horizontal {
border-left: var(--jp-scrollbar-endpad) solid
var(--jp-scrollbar-background-color);
border-right: var(--jp-scrollbar-endpad) solid
var(--jp-scrollbar-background-color);
}
[data-jp-theme-scrollbars='true'] ::-webkit-scrollbar-track:vertical {
border-top: var(--jp-scrollbar-endpad) solid
var(--jp-scrollbar-background-color);
border-bottom: var(--jp-scrollbar-endpad) solid
var(--jp-scrollbar-background-color);
}
/* for code nodes, use a transparent style of scrollbar */
[data-jp-theme-scrollbars='true'] .CodeMirror-hscrollbar::-webkit-scrollbar,
[data-jp-theme-scrollbars='true'] .CodeMirror-vscrollbar::-webkit-scrollbar,
[data-jp-theme-scrollbars='true']
.CodeMirror-hscrollbar::-webkit-scrollbar-corner,
[data-jp-theme-scrollbars='true']
.CodeMirror-vscrollbar::-webkit-scrollbar-corner {
background-color: transparent;
}
[data-jp-theme-scrollbars='true']
.CodeMirror-hscrollbar::-webkit-scrollbar-thumb,
[data-jp-theme-scrollbars='true']
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
background: rgba(var(--jp-scrollbar-thumb-color), 0.5);
border: var(--jp-scrollbar-thumb-margin) solid transparent;
background-clip: content-box;
border-radius: var(--jp-scrollbar-thumb-radius);
}
[data-jp-theme-scrollbars='true']
.CodeMirror-hscrollbar::-webkit-scrollbar-track:horizontal {
border-left: var(--jp-scrollbar-endpad) solid transparent;
border-right: var(--jp-scrollbar-endpad) solid transparent;
}
[data-jp-theme-scrollbars='true']
.CodeMirror-vscrollbar::-webkit-scrollbar-track:vertical {
border-top: var(--jp-scrollbar-endpad) solid transparent;
border-bottom: var(--jp-scrollbar-endpad) solid transparent;
}
/* tiny scrollbar */
.jp-scrollbar-tiny::-webkit-scrollbar,
.jp-scrollbar-tiny::-webkit-scrollbar-corner {
background-color: transparent;
height: 4px;
width: 4px;
}
.jp-scrollbar-tiny::-webkit-scrollbar-thumb {
background: rgba(var(--jp-scrollbar-thumb-color), 0.5);
}
.jp-scrollbar-tiny::-webkit-scrollbar-track:horizontal {
border-left: 0px solid transparent;
border-right: 0px solid transparent;
}
.jp-scrollbar-tiny::-webkit-scrollbar-track:vertical {
border-top: 0px solid transparent;
border-bottom: 0px solid transparent;
}
/*
* Phosphor
*/
.lm-ScrollBar[data-orientation='horizontal'] {
min-height: 16px;
max-height: 16px;
min-width: 45px;
border-top: 1px solid #a0a0a0;
}
.lm-ScrollBar[data-orientation='vertical'] {
min-width: 16px;
max-width: 16px;
min-height: 45px;
border-left: 1px solid #a0a0a0;
}
.lm-ScrollBar-button {
background-color: #f0f0f0;
background-position: center center;
min-height: 15px;
max-height: 15px;
min-width: 15px;
max-width: 15px;
}
.lm-ScrollBar-button:hover {
background-color: #dadada;
}
.lm-ScrollBar-button.lm-mod-active {
background-color: #cdcdcd;
}
.lm-ScrollBar-track {
background: #f0f0f0;
}
.lm-ScrollBar-thumb {
background: #cdcdcd;
}
.lm-ScrollBar-thumb:hover {
background: #bababa;
}
.lm-ScrollBar-thumb.lm-mod-active {
background: #a0a0a0;
}
.lm-ScrollBar[data-orientation='horizontal'] .lm-ScrollBar-thumb {
height: 100%;
min-width: 15px;
border-left: 1px solid #a0a0a0;
border-right: 1px solid #a0a0a0;
}
.lm-ScrollBar[data-orientation='vertical'] .lm-ScrollBar-thumb {
width: 100%;
min-height: 15px;
border-top: 1px solid #a0a0a0;
border-bottom: 1px solid #a0a0a0;
}
.lm-ScrollBar[data-orientation='horizontal']
.lm-ScrollBar-button[data-action='decrement'] {
background-image: var(--jp-icon-caret-left);
background-size: 17px;
}
.lm-ScrollBar[data-orientation='horizontal']
.lm-ScrollBar-button[data-action='increment'] {
background-image: var(--jp-icon-caret-right);
background-size: 17px;
}
.lm-ScrollBar[data-orientation='vertical']
.lm-ScrollBar-button[data-action='decrement'] {
background-image: var(--jp-icon-caret-up);
background-size: 17px;
}
.lm-ScrollBar[data-orientation='vertical']
.lm-ScrollBar-button[data-action='increment'] {
background-image: var(--jp-icon-caret-down);
background-size: 17px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-Widget, /* </DEPRECATED> */
.lm-Widget {
box-sizing: border-box;
position: relative;
overflow: hidden;
cursor: default;
}
/* <DEPRECATED> */
.p-Widget.p-mod-hidden, /* </DEPRECATED> */
.lm-Widget.lm-mod-hidden {
display: none !important;
}
.lm-AccordionPanel[data-orientation='horizontal'] > .lm-AccordionPanel-title {
/* Title is rotated for horizontal accordion panel using <span class="caps">CSS</span> */
display: block;
transform-origin: top left;
transform: rotate(-90deg) translate(-100%);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-CommandPalette, /* </DEPRECATED> */
.lm-CommandPalette {
display: flex;
flex-direction: column;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* <DEPRECATED> */
.p-CommandPalette-search, /* </DEPRECATED> */
.lm-CommandPalette-search {
flex: 0 0 auto;
}
/* <DEPRECATED> */
.p-CommandPalette-content, /* </DEPRECATED> */
.lm-CommandPalette-content {
flex: 1 1 auto;
margin: 0;
padding: 0;
min-height: 0;
overflow: auto;
list-style-type: none;
}
/* <DEPRECATED> */
.p-CommandPalette-header, /* </DEPRECATED> */
.lm-CommandPalette-header {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* <DEPRECATED> */
.p-CommandPalette-item, /* </DEPRECATED> */
.lm-CommandPalette-item {
display: flex;
flex-direction: row;
}
/* <DEPRECATED> */
.p-CommandPalette-itemIcon, /* </DEPRECATED> */
.lm-CommandPalette-itemIcon {
flex: 0 0 auto;
}
/* <DEPRECATED> */
.p-CommandPalette-itemContent, /* </DEPRECATED> */
.lm-CommandPalette-itemContent {
flex: 1 1 auto;
overflow: hidden;
}
/* <DEPRECATED> */
.p-CommandPalette-itemShortcut, /* </DEPRECATED> */
.lm-CommandPalette-itemShortcut {
flex: 0 0 auto;
}
/* <DEPRECATED> */
.p-CommandPalette-itemLabel, /* </DEPRECATED> */
.lm-CommandPalette-itemLabel {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.lm-close-icon {
border: 1px solid transparent;
background-color: transparent;
position: absolute;
z-index: 1;
right: 3%;
top: 0;
bottom: 0;
margin: auto;
padding: 7px 0;
display: none;
vertical-align: middle;
outline: 0;
cursor: pointer;
}
.lm-close-icon:after {
content: 'X';
display: block;
width: 15px;
height: 15px;
text-align: center;
color: #000;
font-weight: normal;
font-size: 12px;
cursor: pointer;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-DockPanel, /* </DEPRECATED> */
.lm-DockPanel {
z-index: 0;
}
/* <DEPRECATED> */
.p-DockPanel-widget, /* </DEPRECATED> */
.lm-DockPanel-widget {
z-index: 0;
}
/* <DEPRECATED> */
.p-DockPanel-tabBar, /* </DEPRECATED> */
.lm-DockPanel-tabBar {
z-index: 1;
}
/* <DEPRECATED> */
.p-DockPanel-handle, /* </DEPRECATED> */
.lm-DockPanel-handle {
z-index: 2;
}
/* <DEPRECATED> */
.p-DockPanel-handle.p-mod-hidden, /* </DEPRECATED> */
.lm-DockPanel-handle.lm-mod-hidden {
display: none !important;
}
/* <DEPRECATED> */
.p-DockPanel-handle:after, /* </DEPRECATED> */
.lm-DockPanel-handle:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
}
/* <DEPRECATED> */
.p-DockPanel-handle[data-orientation='horizontal'],
/* </DEPRECATED> */
.lm-DockPanel-handle[data-orientation='horizontal'] {
cursor: ew-resize;
}
/* <DEPRECATED> */
.p-DockPanel-handle[data-orientation='vertical'],
/* </DEPRECATED> */
.lm-DockPanel-handle[data-orientation='vertical'] {
cursor: ns-resize;
}
/* <DEPRECATED> */
.p-DockPanel-handle[data-orientation='horizontal']:after,
/* </DEPRECATED> */
.lm-DockPanel-handle[data-orientation='horizontal']:after {
left: 50%;
min-width: 8px;
transform: translateX(-50%);
}
/* <DEPRECATED> */
.p-DockPanel-handle[data-orientation='vertical']:after,
/* </DEPRECATED> */
.lm-DockPanel-handle[data-orientation='vertical']:after {
top: 50%;
min-height: 8px;
transform: translateY(-50%);
}
/* <DEPRECATED> */
.p-DockPanel-overlay, /* </DEPRECATED> */
.lm-DockPanel-overlay {
z-index: 3;
box-sizing: border-box;
pointer-events: none;
}
/* <DEPRECATED> */
.p-DockPanel-overlay.p-mod-hidden, /* </DEPRECATED> */
.lm-DockPanel-overlay.lm-mod-hidden {
display: none !important;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-Menu, /* </DEPRECATED> */
.lm-Menu {
z-index: 10000;
position: absolute;
white-space: nowrap;
overflow-x: hidden;
overflow-y: auto;
outline: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* <DEPRECATED> */
.p-Menu-content, /* </DEPRECATED> */
.lm-Menu-content {
margin: 0;
padding: 0;
display: table;
list-style-type: none;
}
/* <DEPRECATED> */
.p-Menu-item, /* </DEPRECATED> */
.lm-Menu-item {
display: table-row;
}
/* <DEPRECATED> */
.p-Menu-item.p-mod-hidden,
.p-Menu-item.p-mod-collapsed,
/* </DEPRECATED> */
.lm-Menu-item.lm-mod-hidden,
.lm-Menu-item.lm-mod-collapsed {
display: none !important;
}
/* <DEPRECATED> */
.p-Menu-itemIcon,
.p-Menu-itemSubmenuIcon,
/* </DEPRECATED> */
.lm-Menu-itemIcon,
.lm-Menu-itemSubmenuIcon {
display: table-cell;
text-align: center;
}
/* <DEPRECATED> */
.p-Menu-itemLabel, /* </DEPRECATED> */
.lm-Menu-itemLabel {
display: table-cell;
text-align: left;
}
/* <DEPRECATED> */
.p-Menu-itemShortcut, /* </DEPRECATED> */
.lm-Menu-itemShortcut {
display: table-cell;
text-align: right;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-MenuBar, /* </DEPRECATED> */
.lm-MenuBar {
outline: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* <DEPRECATED> */
.p-MenuBar-content, /* </DEPRECATED> */
.lm-MenuBar-content {
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
list-style-type: none;
}
/* <DEPRECATED> */
.p--MenuBar-item, /* </DEPRECATED> */
.lm-MenuBar-item {
box-sizing: border-box;
}
/* <DEPRECATED> */
.p-MenuBar-itemIcon,
.p-MenuBar-itemLabel,
/* </DEPRECATED> */
.lm-MenuBar-itemIcon,
.lm-MenuBar-itemLabel {
display: inline-block;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-ScrollBar, /* </DEPRECATED> */
.lm-ScrollBar {
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* <DEPRECATED> */
.p-ScrollBar[data-orientation='horizontal'],
/* </DEPRECATED> */
.lm-ScrollBar[data-orientation='horizontal'] {
flex-direction: row;
}
/* <DEPRECATED> */
.p-ScrollBar[data-orientation='vertical'],
/* </DEPRECATED> */
.lm-ScrollBar[data-orientation='vertical'] {
flex-direction: column;
}
/* <DEPRECATED> */
.p-ScrollBar-button, /* </DEPRECATED> */
.lm-ScrollBar-button {
box-sizing: border-box;
flex: 0 0 auto;
}
/* <DEPRECATED> */
.p-ScrollBar-track, /* </DEPRECATED> */
.lm-ScrollBar-track {
box-sizing: border-box;
position: relative;
overflow: hidden;
flex: 1 1 auto;
}
/* <DEPRECATED> */
.p-ScrollBar-thumb, /* </DEPRECATED> */
.lm-ScrollBar-thumb {
box-sizing: border-box;
position: absolute;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-SplitPanel-child, /* </DEPRECATED> */
.lm-SplitPanel-child {
z-index: 0;
}
/* <DEPRECATED> */
.p-SplitPanel-handle, /* </DEPRECATED> */
.lm-SplitPanel-handle {
z-index: 1;
}
/* <DEPRECATED> */
.p-SplitPanel-handle.p-mod-hidden, /* </DEPRECATED> */
.lm-SplitPanel-handle.lm-mod-hidden {
display: none !important;
}
/* <DEPRECATED> */
.p-SplitPanel-handle:after, /* </DEPRECATED> */
.lm-SplitPanel-handle:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
}
/* <DEPRECATED> */
.p-SplitPanel[data-orientation='horizontal'] > .p-SplitPanel-handle,
/* </DEPRECATED> */
.lm-SplitPanel[data-orientation='horizontal'] > .lm-SplitPanel-handle {
cursor: ew-resize;
}
/* <DEPRECATED> */
.p-SplitPanel[data-orientation='vertical'] > .p-SplitPanel-handle,
/* </DEPRECATED> */
.lm-SplitPanel[data-orientation='vertical'] > .lm-SplitPanel-handle {
cursor: ns-resize;
}
/* <DEPRECATED> */
.p-SplitPanel[data-orientation='horizontal'] > .p-SplitPanel-handle:after,
/* </DEPRECATED> */
.lm-SplitPanel[data-orientation='horizontal'] > .lm-SplitPanel-handle:after {
left: 50%;
min-width: 8px;
transform: translateX(-50%);
}
/* <DEPRECATED> */
.p-SplitPanel[data-orientation='vertical'] > .p-SplitPanel-handle:after,
/* </DEPRECATED> */
.lm-SplitPanel[data-orientation='vertical'] > .lm-SplitPanel-handle:after {
top: 50%;
min-height: 8px;
transform: translateY(-50%);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-TabBar, /* </DEPRECATED> */
.lm-TabBar {
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* <DEPRECATED> */
.p-TabBar[data-orientation='horizontal'], /* </DEPRECATED> */
.lm-TabBar[data-orientation='horizontal'] {
flex-direction: row;
align-items: flex-end;
}
/* <DEPRECATED> */
.p-TabBar[data-orientation='vertical'], /* </DEPRECATED> */
.lm-TabBar[data-orientation='vertical'] {
flex-direction: column;
align-items: flex-end;
}
/* <DEPRECATED> */
.p-TabBar-content, /* </DEPRECATED> */
.lm-TabBar-content {
margin: 0;
padding: 0;
display: flex;
flex: 1 1 auto;
list-style-type: none;
}
/* <DEPRECATED> */
.p-TabBar[data-orientation='horizontal'] > .p-TabBar-content,
/* </DEPRECATED> */
.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-content {
flex-direction: row;
}
/* <DEPRECATED> */
.p-TabBar[data-orientation='vertical'] > .p-TabBar-content,
/* </DEPRECATED> */
.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-content {
flex-direction: column;
}
/* <DEPRECATED> */
.p-TabBar-tab, /* </DEPRECATED> */
.lm-TabBar-tab {
display: flex;
flex-direction: row;
box-sizing: border-box;
overflow: hidden;
touch-action: none; /* Disable native Drag/Drop */
}
/* <DEPRECATED> */
.p-TabBar-tabIcon,
.p-TabBar-tabCloseIcon,
/* </DEPRECATED> */
.lm-TabBar-tabIcon,
.lm-TabBar-tabCloseIcon {
flex: 0 0 auto;
}
/* <DEPRECATED> */
.p-TabBar-tabLabel, /* </DEPRECATED> */
.lm-TabBar-tabLabel {
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
}
.lm-TabBar-tabInput {
user-select: all;
width: 100%;
box-sizing: border-box;
}
/* <DEPRECATED> */
.p-TabBar-tab.p-mod-hidden, /* </DEPRECATED> */
.lm-TabBar-tab.lm-mod-hidden {
display: none !important;
}
.lm-TabBar-addButton.lm-mod-hidden {
display: none !important;
}
/* <DEPRECATED> */
.p-TabBar.p-mod-dragging .p-TabBar-tab, /* </DEPRECATED> */
.lm-TabBar.lm-mod-dragging .lm-TabBar-tab {
position: relative;
}
/* <DEPRECATED> */
.p-TabBar.p-mod-dragging[data-orientation='horizontal'] .p-TabBar-tab,
/* </DEPRECATED> */
.lm-TabBar.lm-mod-dragging[data-orientation='horizontal'] .lm-TabBar-tab {
left: 0;
transition: left 150ms ease;
}
/* <DEPRECATED> */
.p-TabBar.p-mod-dragging[data-orientation='vertical'] .p-TabBar-tab,
/* </DEPRECATED> */
.lm-TabBar.lm-mod-dragging[data-orientation='vertical'] .lm-TabBar-tab {
top: 0;
transition: top 150ms ease;
}
/* <DEPRECATED> */
.p-TabBar.p-mod-dragging .p-TabBar-tab.p-mod-dragging,
/* </DEPRECATED> */
.lm-TabBar.lm-mod-dragging .lm-TabBar-tab.lm-mod-dragging {
transition: none;
}
.lm-TabBar-tabLabel .lm-TabBar-tabInput {
user-select: all;
width: 100%;
box-sizing: border-box;
background: inherit;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
.p-TabPanel-tabBar, /* </DEPRECATED> */
.lm-TabPanel-tabBar {
z-index: 1;
}
/* <DEPRECATED> */
.p-TabPanel-stackedPanel, /* </DEPRECATED> */
.lm-TabPanel-stackedPanel {
z-index: 0;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
@charset "<span class="caps">UTF</span>-8";
html{
-webkit-box-sizing:border-box;
box-sizing:border-box; }
*,
*::before,
*::after{
-webkit-box-sizing:inherit;
box-sizing:inherit; }
body{
font-size:14px;
font-weight:400;
letter-spacing:0;
line-height:1.28581;
text-transform:none;
color:#182026;
font-family:-apple-system, "BlinkMacSystemFont", "Segoe <span class="caps">UI</span>", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Open Sans", "Helvetica Neue", "Icons16", sans-serif; }
p{
margin-bottom:10px;
margin-top:0; }
small{
font-size:12px; }
strong{
font-weight:600; }
::-moz-selection{
background:rgba(125, 188, 255, 0.6); }
::selection{
background:rgba(125, 188, 255, 0.6); }
.bp3-heading{
color:#182026;
font-weight:600;
margin:0 0 10px;
padding:0; }
.bp3-dark .bp3-heading{
color:#f5f8fa; }
h1.bp3-heading, .bp3-running-text h1{
font-size:36px;
line-height:40px; }
h2.bp3-heading, .bp3-running-text h2{
font-size:28px;
line-height:32px; }
h3.bp3-heading, .bp3-running-text h3{
font-size:22px;
line-height:25px; }
h4.bp3-heading, .bp3-running-text h4{
font-size:18px;
line-height:21px; }
h5.bp3-heading, .bp3-running-text h5{
font-size:16px;
line-height:19px; }
h6.bp3-heading, .bp3-running-text h6{
font-size:14px;
line-height:16px; }
.bp3-ui-text{
font-size:14px;
font-weight:400;
letter-spacing:0;
line-height:1.28581;
text-transform:none; }
.bp3-monospace-text{
font-family:monospace;
text-transform:none; }
.bp3-text-muted{
color:#5c7080; }
.bp3-dark .bp3-text-muted{
color:#a7b6c2; }
.bp3-text-disabled{
color:rgba(92, 112, 128, 0.6); }
.bp3-dark .bp3-text-disabled{
color:rgba(167, 182, 194, 0.6); }
.bp3-text-overflow-ellipsis{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal; }
.bp3-running-text{
font-size:14px;
line-height:1.5; }
.bp3-running-text h1{
color:#182026;
font-weight:600;
margin-bottom:20px;
margin-top:40px; }
.bp3-dark .bp3-running-text h1{
color:#f5f8fa; }
.bp3-running-text h2{
color:#182026;
font-weight:600;
margin-bottom:20px;
margin-top:40px; }
.bp3-dark .bp3-running-text h2{
color:#f5f8fa; }
.bp3-running-text h3{
color:#182026;
font-weight:600;
margin-bottom:20px;
margin-top:40px; }
.bp3-dark .bp3-running-text h3{
color:#f5f8fa; }
.bp3-running-text h4{
color:#182026;
font-weight:600;
margin-bottom:20px;
margin-top:40px; }
.bp3-dark .bp3-running-text h4{
color:#f5f8fa; }
.bp3-running-text h5{
color:#182026;
font-weight:600;
margin-bottom:20px;
margin-top:40px; }
.bp3-dark .bp3-running-text h5{
color:#f5f8fa; }
.bp3-running-text h6{
color:#182026;
font-weight:600;
margin-bottom:20px;
margin-top:40px; }
.bp3-dark .bp3-running-text h6{
color:#f5f8fa; }
.bp3-running-text hr{
border:none;
border-bottom:1px solid rgba(16, 22, 26, 0.15);
margin:20px 0; }
.bp3-dark .bp3-running-text hr{
border-color:rgba(255, 255, 255, 0.15); }
.bp3-running-text p{
margin:0 0 10px;
padding:0; }
.bp3-text-large{
font-size:16px; }
.bp3-text-small{
font-size:12px; }
a{
color:#106ba3;
text-decoration:none; }
a:hover{
color:#106ba3;
cursor:pointer;
text-decoration:underline; }
a .bp3-icon, a .bp3-icon-standard, a .bp3-icon-large{
color:inherit; }
a code,
.bp3-dark a code{
color:inherit; }
.bp3-dark a,
.bp3-dark a:hover{
color:#48aff0; }
.bp3-dark a .bp3-icon, .bp3-dark a .bp3-icon-standard, .bp3-dark a .bp3-icon-large,
.bp3-dark a:hover .bp3-icon,
.bp3-dark a:hover .bp3-icon-standard,
.bp3-dark a:hover .bp3-icon-large{
color:inherit; }
.bp3-running-text code, .bp3-code{
font-family:monospace;
text-transform:none;
background:rgba(255, 255, 255, 0.7);
border-radius:3px;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2);
color:#5c7080;
font-size:smaller;
padding:2px 5px; }
.bp3-dark .bp3-running-text code, .bp3-running-text .bp3-dark code, .bp3-dark .bp3-code{
background:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
color:#a7b6c2; }
.bp3-running-text a > code, a > .bp3-code{
color:#137cbd; }
.bp3-dark .bp3-running-text a > code, .bp3-running-text .bp3-dark a > code, .bp3-dark a > .bp3-code{
color:inherit; }
.bp3-running-text pre, .bp3-code-block{
font-family:monospace;
text-transform:none;
background:rgba(255, 255, 255, 0.7);
border-radius:3px;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.15);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.15);
color:#182026;
display:block;
font-size:13px;
line-height:1.4;
margin:10px 0;
padding:13px 15px 12px;
word-break:break-all;
word-wrap:break-word; }
.bp3-dark .bp3-running-text pre, .bp3-running-text .bp3-dark pre, .bp3-dark .bp3-code-block{
background:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-running-text pre > code, .bp3-code-block > code{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:inherit;
font-size:inherit;
padding:0; }
.bp3-running-text kbd, .bp3-key{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
background:#ffffff;
border-radius:3px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
color:#5c7080;
display:-webkit-inline-box;
display:-ms-inline-flexbox;
display:inline-flex;
font-family:inherit;
font-size:12px;
height:24px;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
line-height:24px;
min-width:24px;
padding:3px 6px;
vertical-align:middle; }
.bp3-running-text kbd .bp3-icon, .bp3-key .bp3-icon, .bp3-running-text kbd .bp3-icon-standard, .bp3-key .bp3-icon-standard, .bp3-running-text kbd .bp3-icon-large, .bp3-key .bp3-icon-large{
margin-right:5px; }
.bp3-dark .bp3-running-text kbd, .bp3-running-text .bp3-dark kbd, .bp3-dark .bp3-key{
background:#394b59;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
color:#a7b6c2; }
.bp3-running-text blockquote, .bp3-blockquote{
border-left:solid 4px rgba(167, 182, 194, 0.5);
margin:0 0 10px;
padding:0 20px; }
.bp3-dark .bp3-running-text blockquote, .bp3-running-text .bp3-dark blockquote, .bp3-dark .bp3-blockquote{
border-color:rgba(115, 134, 148, 0.5); }
.bp3-running-text ul,
.bp3-running-text ol, .bp3-list{
margin:10px 0;
padding-left:30px; }
.bp3-running-text ul li:not(:last-child), .bp3-running-text ol li:not(:last-child), .bp3-list li:not(:last-child){
margin-bottom:5px; }
.bp3-running-text ul ol, .bp3-running-text ol ol, .bp3-list ol,
.bp3-running-text ul ul,
.bp3-running-text ol ul,
.bp3-list ul{
margin-top:5px; }
.bp3-list-unstyled{
list-style:none;
margin:0;
padding:0; }
.bp3-list-unstyled li{
padding:0; }
.bp3-rtl{
text-align:right; }
.bp3-dark{
color:#f5f8fa; }
:focus{
outline:rgba(19, 124, 189, 0.6) auto 2px;
outline-offset:2px;
-moz-outline-radius:6px; }
.bp3-focus-disabled :focus{
outline:none !important; }
.bp3-focus-disabled :focus ~ .bp3-control-indicator{
outline:none !important; }
.bp3-alert{
max-width:400px;
padding:20px; }
.bp3-alert-body{
display:-webkit-box;
display:-ms-flexbox;
display:flex; }
.bp3-alert-body .bp3-icon{
font-size:40px;
margin-right:20px;
margin-top:0; }
.bp3-alert-contents{
word-break:break-word; }
.bp3-alert-footer{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:reverse;
-ms-flex-direction:row-reverse;
flex-direction:row-reverse;
margin-top:10px; }
.bp3-alert-footer .bp3-button{
margin-left:10px; }
.bp3-breadcrumbs{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
cursor:default;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-ms-flex-wrap:wrap;
flex-wrap:wrap;
height:30px;
list-style:none;
margin:0;
padding:0; }
.bp3-breadcrumbs > li{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex; }
.bp3-breadcrumbs > li::after{
background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.71 7.29l-4-4a1.003 1.003 0 00-1.42 1.42L8.59 8 5.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l4-4c.18-.18.29-.43.29-.71 0-.28-.11-.53-.29-.71z' fill='%235C7080'/%3e%3c/svg%3e");
content:"";
display:block;
height:16px;
margin:0 5px;
width:16px; }
.bp3-breadcrumbs > li:last-of-type::after{
display:none; }
.bp3-breadcrumb,
.bp3-breadcrumb-current,
.bp3-breadcrumbs-collapsed{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-inline-box;
display:-ms-inline-flexbox;
display:inline-flex;
font-size:16px; }
.bp3-breadcrumb,
.bp3-breadcrumbs-collapsed{
color:#5c7080; }
.bp3-breadcrumb:hover{
text-decoration:none; }
.bp3-breadcrumb.bp3-disabled{
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-breadcrumb .bp3-icon{
margin-right:5px; }
.bp3-breadcrumb-current{
color:inherit;
font-weight:600; }
.bp3-breadcrumb-current .bp3-input{
font-size:inherit;
font-weight:inherit;
vertical-align:baseline; }
.bp3-breadcrumbs-collapsed{
background:#ced9e0;
border:none;
border-radius:3px;
cursor:pointer;
margin-right:2px;
padding:1px 5px;
vertical-align:text-bottom; }
.bp3-breadcrumbs-collapsed::before{
background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cg fill='%235C7080'%3e%3ccircle cx='2' cy='8.03' r='2'/%3e%3ccircle cx='14' cy='8.03' r='2'/%3e%3ccircle cx='8' cy='8.03' r='2'/%3e%3c/g%3e%3c/svg%3e") center no-repeat;
content:"";
display:block;
height:16px;
width:16px; }
.bp3-breadcrumbs-collapsed:hover{
background:#bfccd6;
color:#182026;
text-decoration:none; }
.bp3-dark .bp3-breadcrumb,
.bp3-dark .bp3-breadcrumbs-collapsed{
color:#a7b6c2; }
.bp3-dark .bp3-breadcrumbs > li::after{
color:#a7b6c2; }
.bp3-dark .bp3-breadcrumb.bp3-disabled{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-breadcrumb-current{
color:#f5f8fa; }
.bp3-dark .bp3-breadcrumbs-collapsed{
background:rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-breadcrumbs-collapsed:hover{
background:rgba(16, 22, 26, 0.6);
color:#f5f8fa; }
.bp3-button{
display:-webkit-inline-box;
display:-ms-inline-flexbox;
display:inline-flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
border:none;
border-radius:3px;
cursor:pointer;
font-size:14px;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
padding:5px 10px;
text-align:left;
vertical-align:middle;
min-height:30px;
min-width:30px; }
.bp3-button > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-button > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-button::before,
.bp3-button > *{
margin-right:7px; }
.bp3-button:empty::before,
.bp3-button > :last-child{
margin-right:0; }
.bp3-button:empty{
padding:0 !important; }
.bp3-button:disabled, .bp3-button.bp3-disabled{
cursor:not-allowed; }
.bp3-button.bp3-fill{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
width:100%; }
.bp3-button.bp3-align-right,
.bp3-align-right .bp3-button{
text-align:right; }
.bp3-button.bp3-align-left,
.bp3-align-left .bp3-button{
text-align:left; }
.bp3-button:not([class*="bp3-intent-"]){
background-color:#f5f8fa;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.8)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
color:#182026; }
.bp3-button:not([class*="bp3-intent-"]):hover{
background-clip:padding-box;
background-color:#ebf1f5;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); }
.bp3-button:not([class*="bp3-intent-"]):active, .bp3-button:not([class*="bp3-intent-"]).bp3-active{
background-color:#d8e1e8;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-button:not([class*="bp3-intent-"]):disabled, .bp3-button:not([class*="bp3-intent-"]).bp3-disabled{
background-color:rgba(206, 217, 224, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
outline:none; }
.bp3-button:not([class*="bp3-intent-"]):disabled.bp3-active, .bp3-button:not([class*="bp3-intent-"]):disabled.bp3-active:hover, .bp3-button:not([class*="bp3-intent-"]).bp3-disabled.bp3-active, .bp3-button:not([class*="bp3-intent-"]).bp3-disabled.bp3-active:hover{
background:rgba(206, 217, 224, 0.7); }
.bp3-button.bp3-intent-primary{
background-color:#137cbd;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
color:#ffffff; }
.bp3-button.bp3-intent-primary:hover, .bp3-button.bp3-intent-primary:active, .bp3-button.bp3-intent-primary.bp3-active{
color:#ffffff; }
.bp3-button.bp3-intent-primary:hover{
background-color:#106ba3;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-primary:active, .bp3-button.bp3-intent-primary.bp3-active{
background-color:#0e5a8a;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-primary:disabled, .bp3-button.bp3-intent-primary.bp3-disabled{
background-color:rgba(19, 124, 189, 0.5);
background-image:none;
border-color:transparent;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(255, 255, 255, 0.6); }
.bp3-button.bp3-intent-success{
background-color:#0f9960;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
color:#ffffff; }
.bp3-button.bp3-intent-success:hover, .bp3-button.bp3-intent-success:active, .bp3-button.bp3-intent-success.bp3-active{
color:#ffffff; }
.bp3-button.bp3-intent-success:hover{
background-color:#0d8050;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-success:active, .bp3-button.bp3-intent-success.bp3-active{
background-color:#0a6640;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-success:disabled, .bp3-button.bp3-intent-success.bp3-disabled{
background-color:rgba(15, 153, 96, 0.5);
background-image:none;
border-color:transparent;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(255, 255, 255, 0.6); }
.bp3-button.bp3-intent-warning{
background-color:#d9822b;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
color:#ffffff; }
.bp3-button.bp3-intent-warning:hover, .bp3-button.bp3-intent-warning:active, .bp3-button.bp3-intent-warning.bp3-active{
color:#ffffff; }
.bp3-button.bp3-intent-warning:hover{
background-color:#bf7326;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-warning:active, .bp3-button.bp3-intent-warning.bp3-active{
background-color:#a66321;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-warning:disabled, .bp3-button.bp3-intent-warning.bp3-disabled{
background-color:rgba(217, 130, 43, 0.5);
background-image:none;
border-color:transparent;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(255, 255, 255, 0.6); }
.bp3-button.bp3-intent-danger{
background-color:#db3737;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
color:#ffffff; }
.bp3-button.bp3-intent-danger:hover, .bp3-button.bp3-intent-danger:active, .bp3-button.bp3-intent-danger.bp3-active{
color:#ffffff; }
.bp3-button.bp3-intent-danger:hover{
background-color:#c23030;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-danger:active, .bp3-button.bp3-intent-danger.bp3-active{
background-color:#a82a2a;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-button.bp3-intent-danger:disabled, .bp3-button.bp3-intent-danger.bp3-disabled{
background-color:rgba(219, 55, 55, 0.5);
background-image:none;
border-color:transparent;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(255, 255, 255, 0.6); }
.bp3-button[class*="bp3-intent-"] .bp3-button-spinner .bp3-spinner-head{
stroke:#ffffff; }
.bp3-button.bp3-large,
.bp3-large .bp3-button{
min-height:40px;
min-width:40px;
font-size:16px;
padding:5px 15px; }
.bp3-button.bp3-large::before,
.bp3-button.bp3-large > *,
.bp3-large .bp3-button::before,
.bp3-large .bp3-button > *{
margin-right:10px; }
.bp3-button.bp3-large:empty::before,
.bp3-button.bp3-large > :last-child,
.bp3-large .bp3-button:empty::before,
.bp3-large .bp3-button > :last-child{
margin-right:0; }
.bp3-button.bp3-small,
.bp3-small .bp3-button{
min-height:24px;
min-width:24px;
padding:0 7px; }
.bp3-button.bp3-loading{
position:relative; }
.bp3-button.bp3-loading[class*="bp3-icon-"]::before{
visibility:hidden; }
.bp3-button.bp3-loading .bp3-button-spinner{
margin:0;
position:absolute; }
.bp3-button.bp3-loading > :not(.bp3-button-spinner){
visibility:hidden; }
.bp3-button[class*="bp3-icon-"]::before{
font-family:"Icons16", sans-serif;
font-size:16px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
color:#5c7080; }
.bp3-button .bp3-icon, .bp3-button .bp3-icon-standard, .bp3-button .bp3-icon-large{
color:#5c7080; }
.bp3-button .bp3-icon.bp3-align-right, .bp3-button .bp3-icon-standard.bp3-align-right, .bp3-button .bp3-icon-large.bp3-align-right{
margin-left:7px; }
.bp3-button .bp3-icon:first-child:last-child,
.bp3-button .bp3-spinner + .bp3-icon:last-child{
margin:0 -7px; }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]){
background-color:#394b59;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.05)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]):hover, .bp3-dark .bp3-button:not([class*="bp3-intent-"]):active, .bp3-dark .bp3-button:not([class*="bp3-intent-"]).bp3-active{
color:#f5f8fa; }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]):hover{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]):active, .bp3-dark .bp3-button:not([class*="bp3-intent-"]).bp3-active{
background-color:#202b33;
background-image:none;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]):disabled, .bp3-dark .bp3-button:not([class*="bp3-intent-"]).bp3-disabled{
background-color:rgba(57, 75, 89, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]):disabled.bp3-active, .bp3-dark .bp3-button:not([class*="bp3-intent-"]).bp3-disabled.bp3-active{
background:rgba(57, 75, 89, 0.7); }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]) .bp3-button-spinner .bp3-spinner-head{
background:rgba(16, 22, 26, 0.5);
stroke:#8a9ba8; }
.bp3-dark .bp3-button:not([class*="bp3-intent-"])[class*="bp3-icon-"]::before{
color:#a7b6c2; }
.bp3-dark .bp3-button:not([class*="bp3-intent-"]) .bp3-icon, .bp3-dark .bp3-button:not([class*="bp3-intent-"]) .bp3-icon-standard, .bp3-dark .bp3-button:not([class*="bp3-intent-"]) .bp3-icon-large{
color:#a7b6c2; }
.bp3-dark .bp3-button[class*="bp3-intent-"]{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-button[class*="bp3-intent-"]:hover{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-button[class*="bp3-intent-"]:active, .bp3-dark .bp3-button[class*="bp3-intent-"].bp3-active{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-button[class*="bp3-intent-"]:disabled, .bp3-dark .bp3-button[class*="bp3-intent-"].bp3-disabled{
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(255, 255, 255, 0.3); }
.bp3-dark .bp3-button[class*="bp3-intent-"] .bp3-button-spinner .bp3-spinner-head{
stroke:#8a9ba8; }
.bp3-button:disabled::before,
.bp3-button:disabled .bp3-icon, .bp3-button:disabled .bp3-icon-standard, .bp3-button:disabled .bp3-icon-large, .bp3-button.bp3-disabled::before,
.bp3-button.bp3-disabled .bp3-icon, .bp3-button.bp3-disabled .bp3-icon-standard, .bp3-button.bp3-disabled .bp3-icon-large, .bp3-button[class*="bp3-intent-"]::before,
.bp3-button[class*="bp3-intent-"] .bp3-icon, .bp3-button[class*="bp3-intent-"] .bp3-icon-standard, .bp3-button[class*="bp3-intent-"] .bp3-icon-large{
color:inherit !important; }
.bp3-button.bp3-minimal{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-button.bp3-minimal:hover{
background:rgba(167, 182, 194, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026;
text-decoration:none; }
.bp3-button.bp3-minimal:active, .bp3-button.bp3-minimal.bp3-active{
background:rgba(115, 134, 148, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026; }
.bp3-button.bp3-minimal:disabled, .bp3-button.bp3-minimal:disabled:hover, .bp3-button.bp3-minimal.bp3-disabled, .bp3-button.bp3-minimal.bp3-disabled:hover{
background:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-button.bp3-minimal:disabled.bp3-active, .bp3-button.bp3-minimal:disabled:hover.bp3-active, .bp3-button.bp3-minimal.bp3-disabled.bp3-active, .bp3-button.bp3-minimal.bp3-disabled:hover.bp3-active{
background:rgba(115, 134, 148, 0.3); }
.bp3-dark .bp3-button.bp3-minimal{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:inherit; }
.bp3-dark .bp3-button.bp3-minimal:hover, .bp3-dark .bp3-button.bp3-minimal:active, .bp3-dark .bp3-button.bp3-minimal.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-button.bp3-minimal:hover{
background:rgba(138, 155, 168, 0.15); }
.bp3-dark .bp3-button.bp3-minimal:active, .bp3-dark .bp3-button.bp3-minimal.bp3-active{
background:rgba(138, 155, 168, 0.3);
color:#f5f8fa; }
.bp3-dark .bp3-button.bp3-minimal:disabled, .bp3-dark .bp3-button.bp3-minimal:disabled:hover, .bp3-dark .bp3-button.bp3-minimal.bp3-disabled, .bp3-dark .bp3-button.bp3-minimal.bp3-disabled:hover{
background:none;
color:rgba(167, 182, 194, 0.6);
cursor:not-allowed; }
.bp3-dark .bp3-button.bp3-minimal:disabled.bp3-active, .bp3-dark .bp3-button.bp3-minimal:disabled:hover.bp3-active, .bp3-dark .bp3-button.bp3-minimal.bp3-disabled.bp3-active, .bp3-dark .bp3-button.bp3-minimal.bp3-disabled:hover.bp3-active{
background:rgba(138, 155, 168, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-primary{
color:#106ba3; }
.bp3-button.bp3-minimal.bp3-intent-primary:hover, .bp3-button.bp3-minimal.bp3-intent-primary:active, .bp3-button.bp3-minimal.bp3-intent-primary.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#106ba3; }
.bp3-button.bp3-minimal.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.15);
color:#106ba3; }
.bp3-button.bp3-minimal.bp3-intent-primary:active, .bp3-button.bp3-minimal.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#106ba3; }
.bp3-button.bp3-minimal.bp3-intent-primary:disabled, .bp3-button.bp3-minimal.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(16, 107, 163, 0.5); }
.bp3-button.bp3-minimal.bp3-intent-primary:disabled.bp3-active, .bp3-button.bp3-minimal.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-primary .bp3-button-spinner .bp3-spinner-head{
stroke:#106ba3; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary{
color:#48aff0; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.2);
color:#48aff0; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary:active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#48aff0; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary:disabled, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(72, 175, 240, 0.5); }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary:disabled.bp3-active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-success{
color:#0d8050; }
.bp3-button.bp3-minimal.bp3-intent-success:hover, .bp3-button.bp3-minimal.bp3-intent-success:active, .bp3-button.bp3-minimal.bp3-intent-success.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#0d8050; }
.bp3-button.bp3-minimal.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.15);
color:#0d8050; }
.bp3-button.bp3-minimal.bp3-intent-success:active, .bp3-button.bp3-minimal.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#0d8050; }
.bp3-button.bp3-minimal.bp3-intent-success:disabled, .bp3-button.bp3-minimal.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(13, 128, 80, 0.5); }
.bp3-button.bp3-minimal.bp3-intent-success:disabled.bp3-active, .bp3-button.bp3-minimal.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-success .bp3-button-spinner .bp3-spinner-head{
stroke:#0d8050; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-success{
color:#3dcc91; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.2);
color:#3dcc91; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-success:active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#3dcc91; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-success:disabled, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(61, 204, 145, 0.5); }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-success:disabled.bp3-active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-warning{
color:#bf7326; }
.bp3-button.bp3-minimal.bp3-intent-warning:hover, .bp3-button.bp3-minimal.bp3-intent-warning:active, .bp3-button.bp3-minimal.bp3-intent-warning.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#bf7326; }
.bp3-button.bp3-minimal.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.15);
color:#bf7326; }
.bp3-button.bp3-minimal.bp3-intent-warning:active, .bp3-button.bp3-minimal.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#bf7326; }
.bp3-button.bp3-minimal.bp3-intent-warning:disabled, .bp3-button.bp3-minimal.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(191, 115, 38, 0.5); }
.bp3-button.bp3-minimal.bp3-intent-warning:disabled.bp3-active, .bp3-button.bp3-minimal.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-warning .bp3-button-spinner .bp3-spinner-head{
stroke:#bf7326; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning{
color:#ffb366; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.2);
color:#ffb366; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning:active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#ffb366; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning:disabled, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(255, 179, 102, 0.5); }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning:disabled.bp3-active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-danger{
color:#c23030; }
.bp3-button.bp3-minimal.bp3-intent-danger:hover, .bp3-button.bp3-minimal.bp3-intent-danger:active, .bp3-button.bp3-minimal.bp3-intent-danger.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#c23030; }
.bp3-button.bp3-minimal.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.15);
color:#c23030; }
.bp3-button.bp3-minimal.bp3-intent-danger:active, .bp3-button.bp3-minimal.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#c23030; }
.bp3-button.bp3-minimal.bp3-intent-danger:disabled, .bp3-button.bp3-minimal.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(194, 48, 48, 0.5); }
.bp3-button.bp3-minimal.bp3-intent-danger:disabled.bp3-active, .bp3-button.bp3-minimal.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-button.bp3-minimal.bp3-intent-danger .bp3-button-spinner .bp3-spinner-head{
stroke:#c23030; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger{
color:#ff7373; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.2);
color:#ff7373; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger:active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#ff7373; }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger:disabled, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(255, 115, 115, 0.5); }
.bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger:disabled.bp3-active, .bp3-dark .bp3-button.bp3-minimal.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-button.bp3-outlined{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
border:1px solid rgba(24, 32, 38, 0.2);
-webkit-box-sizing:border-box;
box-sizing:border-box; }
.bp3-button.bp3-outlined:hover{
background:rgba(167, 182, 194, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026;
text-decoration:none; }
.bp3-button.bp3-outlined:active, .bp3-button.bp3-outlined.bp3-active{
background:rgba(115, 134, 148, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026; }
.bp3-button.bp3-outlined:disabled, .bp3-button.bp3-outlined:disabled:hover, .bp3-button.bp3-outlined.bp3-disabled, .bp3-button.bp3-outlined.bp3-disabled:hover{
background:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-button.bp3-outlined:disabled.bp3-active, .bp3-button.bp3-outlined:disabled:hover.bp3-active, .bp3-button.bp3-outlined.bp3-disabled.bp3-active, .bp3-button.bp3-outlined.bp3-disabled:hover.bp3-active{
background:rgba(115, 134, 148, 0.3); }
.bp3-dark .bp3-button.bp3-outlined{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:inherit; }
.bp3-dark .bp3-button.bp3-outlined:hover, .bp3-dark .bp3-button.bp3-outlined:active, .bp3-dark .bp3-button.bp3-outlined.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-button.bp3-outlined:hover{
background:rgba(138, 155, 168, 0.15); }
.bp3-dark .bp3-button.bp3-outlined:active, .bp3-dark .bp3-button.bp3-outlined.bp3-active{
background:rgba(138, 155, 168, 0.3);
color:#f5f8fa; }
.bp3-dark .bp3-button.bp3-outlined:disabled, .bp3-dark .bp3-button.bp3-outlined:disabled:hover, .bp3-dark .bp3-button.bp3-outlined.bp3-disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-disabled:hover{
background:none;
color:rgba(167, 182, 194, 0.6);
cursor:not-allowed; }
.bp3-dark .bp3-button.bp3-outlined:disabled.bp3-active, .bp3-dark .bp3-button.bp3-outlined:disabled:hover.bp3-active, .bp3-dark .bp3-button.bp3-outlined.bp3-disabled.bp3-active, .bp3-dark .bp3-button.bp3-outlined.bp3-disabled:hover.bp3-active{
background:rgba(138, 155, 168, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-primary{
color:#106ba3; }
.bp3-button.bp3-outlined.bp3-intent-primary:hover, .bp3-button.bp3-outlined.bp3-intent-primary:active, .bp3-button.bp3-outlined.bp3-intent-primary.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#106ba3; }
.bp3-button.bp3-outlined.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.15);
color:#106ba3; }
.bp3-button.bp3-outlined.bp3-intent-primary:active, .bp3-button.bp3-outlined.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#106ba3; }
.bp3-button.bp3-outlined.bp3-intent-primary:disabled, .bp3-button.bp3-outlined.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(16, 107, 163, 0.5); }
.bp3-button.bp3-outlined.bp3-intent-primary:disabled.bp3-active, .bp3-button.bp3-outlined.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-primary .bp3-button-spinner .bp3-spinner-head{
stroke:#106ba3; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary{
color:#48aff0; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.2);
color:#48aff0; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary:active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#48aff0; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(72, 175, 240, 0.5); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary:disabled.bp3-active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-success{
color:#0d8050; }
.bp3-button.bp3-outlined.bp3-intent-success:hover, .bp3-button.bp3-outlined.bp3-intent-success:active, .bp3-button.bp3-outlined.bp3-intent-success.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#0d8050; }
.bp3-button.bp3-outlined.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.15);
color:#0d8050; }
.bp3-button.bp3-outlined.bp3-intent-success:active, .bp3-button.bp3-outlined.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#0d8050; }
.bp3-button.bp3-outlined.bp3-intent-success:disabled, .bp3-button.bp3-outlined.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(13, 128, 80, 0.5); }
.bp3-button.bp3-outlined.bp3-intent-success:disabled.bp3-active, .bp3-button.bp3-outlined.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-success .bp3-button-spinner .bp3-spinner-head{
stroke:#0d8050; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success{
color:#3dcc91; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.2);
color:#3dcc91; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success:active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#3dcc91; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(61, 204, 145, 0.5); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success:disabled.bp3-active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-warning{
color:#bf7326; }
.bp3-button.bp3-outlined.bp3-intent-warning:hover, .bp3-button.bp3-outlined.bp3-intent-warning:active, .bp3-button.bp3-outlined.bp3-intent-warning.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#bf7326; }
.bp3-button.bp3-outlined.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.15);
color:#bf7326; }
.bp3-button.bp3-outlined.bp3-intent-warning:active, .bp3-button.bp3-outlined.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#bf7326; }
.bp3-button.bp3-outlined.bp3-intent-warning:disabled, .bp3-button.bp3-outlined.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(191, 115, 38, 0.5); }
.bp3-button.bp3-outlined.bp3-intent-warning:disabled.bp3-active, .bp3-button.bp3-outlined.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-warning .bp3-button-spinner .bp3-spinner-head{
stroke:#bf7326; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning{
color:#ffb366; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.2);
color:#ffb366; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning:active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#ffb366; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(255, 179, 102, 0.5); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning:disabled.bp3-active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-danger{
color:#c23030; }
.bp3-button.bp3-outlined.bp3-intent-danger:hover, .bp3-button.bp3-outlined.bp3-intent-danger:active, .bp3-button.bp3-outlined.bp3-intent-danger.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#c23030; }
.bp3-button.bp3-outlined.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.15);
color:#c23030; }
.bp3-button.bp3-outlined.bp3-intent-danger:active, .bp3-button.bp3-outlined.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#c23030; }
.bp3-button.bp3-outlined.bp3-intent-danger:disabled, .bp3-button.bp3-outlined.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(194, 48, 48, 0.5); }
.bp3-button.bp3-outlined.bp3-intent-danger:disabled.bp3-active, .bp3-button.bp3-outlined.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-button.bp3-outlined.bp3-intent-danger .bp3-button-spinner .bp3-spinner-head{
stroke:#c23030; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger{
color:#ff7373; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.2);
color:#ff7373; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger:active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#ff7373; }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(255, 115, 115, 0.5); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger:disabled.bp3-active, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-button.bp3-outlined:disabled, .bp3-button.bp3-outlined.bp3-disabled, .bp3-button.bp3-outlined:disabled:hover, .bp3-button.bp3-outlined.bp3-disabled:hover{
border-color:rgba(92, 112, 128, 0.1); }
.bp3-dark .bp3-button.bp3-outlined{
border-color:rgba(255, 255, 255, 0.4); }
.bp3-dark .bp3-button.bp3-outlined:disabled, .bp3-dark .bp3-button.bp3-outlined:disabled:hover, .bp3-dark .bp3-button.bp3-outlined.bp3-disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-disabled:hover{
border-color:rgba(255, 255, 255, 0.2); }
.bp3-button.bp3-outlined.bp3-intent-primary{
border-color:rgba(16, 107, 163, 0.6); }
.bp3-button.bp3-outlined.bp3-intent-primary:disabled, .bp3-button.bp3-outlined.bp3-intent-primary.bp3-disabled{
border-color:rgba(16, 107, 163, 0.2); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary{
border-color:rgba(72, 175, 240, 0.6); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-primary.bp3-disabled{
border-color:rgba(72, 175, 240, 0.2); }
.bp3-button.bp3-outlined.bp3-intent-success{
border-color:rgba(13, 128, 80, 0.6); }
.bp3-button.bp3-outlined.bp3-intent-success:disabled, .bp3-button.bp3-outlined.bp3-intent-success.bp3-disabled{
border-color:rgba(13, 128, 80, 0.2); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success{
border-color:rgba(61, 204, 145, 0.6); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-success:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-success.bp3-disabled{
border-color:rgba(61, 204, 145, 0.2); }
.bp3-button.bp3-outlined.bp3-intent-warning{
border-color:rgba(191, 115, 38, 0.6); }
.bp3-button.bp3-outlined.bp3-intent-warning:disabled, .bp3-button.bp3-outlined.bp3-intent-warning.bp3-disabled{
border-color:rgba(191, 115, 38, 0.2); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning{
border-color:rgba(255, 179, 102, 0.6); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-warning.bp3-disabled{
border-color:rgba(255, 179, 102, 0.2); }
.bp3-button.bp3-outlined.bp3-intent-danger{
border-color:rgba(194, 48, 48, 0.6); }
.bp3-button.bp3-outlined.bp3-intent-danger:disabled, .bp3-button.bp3-outlined.bp3-intent-danger.bp3-disabled{
border-color:rgba(194, 48, 48, 0.2); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger{
border-color:rgba(255, 115, 115, 0.6); }
.bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger:disabled, .bp3-dark .bp3-button.bp3-outlined.bp3-intent-danger.bp3-disabled{
border-color:rgba(255, 115, 115, 0.2); }
a.bp3-button{
text-align:center;
text-decoration:none;
-webkit-transition:none;
transition:none; }
a.bp3-button, a.bp3-button:hover, a.bp3-button:active{
color:#182026; }
a.bp3-button.bp3-disabled{
color:rgba(92, 112, 128, 0.6); }
.bp3-button-text{
-webkit-box-flex:0;
-ms-flex:0 1 auto;
flex:0 1 auto; }
.bp3-button.bp3-align-left .bp3-button-text, .bp3-button.bp3-align-right .bp3-button-text,
.bp3-button-group.bp3-align-left .bp3-button-text,
.bp3-button-group.bp3-align-right .bp3-button-text{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto; }
.bp3-button-group{
display:-webkit-inline-box;
display:-ms-inline-flexbox;
display:inline-flex; }
.bp3-button-group .bp3-button{
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
position:relative;
z-index:4; }
.bp3-button-group .bp3-button:focus{
z-index:5; }
.bp3-button-group .bp3-button:hover{
z-index:6; }
.bp3-button-group .bp3-button:active, .bp3-button-group .bp3-button.bp3-active{
z-index:7; }
.bp3-button-group .bp3-button:disabled, .bp3-button-group .bp3-button.bp3-disabled{
z-index:3; }
.bp3-button-group .bp3-button[class*="bp3-intent-"]{
z-index:9; }
.bp3-button-group .bp3-button[class*="bp3-intent-"]:focus{
z-index:10; }
.bp3-button-group .bp3-button[class*="bp3-intent-"]:hover{
z-index:11; }
.bp3-button-group .bp3-button[class*="bp3-intent-"]:active, .bp3-button-group .bp3-button[class*="bp3-intent-"].bp3-active{
z-index:12; }
.bp3-button-group .bp3-button[class*="bp3-intent-"]:disabled, .bp3-button-group .bp3-button[class*="bp3-intent-"].bp3-disabled{
z-index:8; }
.bp3-button-group:not(.bp3-minimal) > .bp3-popover-wrapper:not(:first-child) .bp3-button,
.bp3-button-group:not(.bp3-minimal) > .bp3-button:not(:first-child){
border-bottom-left-radius:0;
border-top-left-radius:0; }
.bp3-button-group:not(.bp3-minimal) > .bp3-popover-wrapper:not(:last-child) .bp3-button,
.bp3-button-group:not(.bp3-minimal) > .bp3-button:not(:last-child){
border-bottom-right-radius:0;
border-top-right-radius:0;
margin-right:-1px; }
.bp3-button-group.bp3-minimal .bp3-button{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-button-group.bp3-minimal .bp3-button:hover{
background:rgba(167, 182, 194, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026;
text-decoration:none; }
.bp3-button-group.bp3-minimal .bp3-button:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-active{
background:rgba(115, 134, 148, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026; }
.bp3-button-group.bp3-minimal .bp3-button:disabled, .bp3-button-group.bp3-minimal .bp3-button:disabled:hover, .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled, .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled:hover{
background:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-button-group.bp3-minimal .bp3-button:disabled.bp3-active, .bp3-button-group.bp3-minimal .bp3-button:disabled:hover.bp3-active, .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled.bp3-active, .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled:hover.bp3-active{
background:rgba(115, 134, 148, 0.3); }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:inherit; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button:hover, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button:active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button:hover{
background:rgba(138, 155, 168, 0.15); }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button:active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-active{
background:rgba(138, 155, 168, 0.3);
color:#f5f8fa; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button:disabled, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button:disabled:hover, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled:hover{
background:none;
color:rgba(167, 182, 194, 0.6);
cursor:not-allowed; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button:disabled.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button:disabled:hover.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-disabled:hover.bp3-active{
background:rgba(138, 155, 168, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary{
color:#106ba3; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:hover, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#106ba3; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.15);
color:#106ba3; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#106ba3; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:disabled, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(16, 107, 163, 0.5); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:disabled.bp3-active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary .bp3-button-spinner .bp3-spinner-head{
stroke:#106ba3; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary{
color:#48aff0; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.2);
color:#48aff0; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#48aff0; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:disabled, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(72, 175, 240, 0.5); }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary:disabled.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success{
color:#0d8050; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:hover, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#0d8050; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.15);
color:#0d8050; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#0d8050; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:disabled, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(13, 128, 80, 0.5); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:disabled.bp3-active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success .bp3-button-spinner .bp3-spinner-head{
stroke:#0d8050; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success{
color:#3dcc91; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.2);
color:#3dcc91; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#3dcc91; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:disabled, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(61, 204, 145, 0.5); }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success:disabled.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning{
color:#bf7326; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:hover, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#bf7326; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.15);
color:#bf7326; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#bf7326; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:disabled, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(191, 115, 38, 0.5); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:disabled.bp3-active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning .bp3-button-spinner .bp3-spinner-head{
stroke:#bf7326; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning{
color:#ffb366; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.2);
color:#ffb366; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#ffb366; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:disabled, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(255, 179, 102, 0.5); }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning:disabled.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger{
color:#c23030; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:hover, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#c23030; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.15);
color:#c23030; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#c23030; }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:disabled, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(194, 48, 48, 0.5); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:disabled.bp3-active, .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger .bp3-button-spinner .bp3-spinner-head{
stroke:#c23030; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger{
color:#ff7373; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.2);
color:#ff7373; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#ff7373; }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:disabled, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(255, 115, 115, 0.5); }
.bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger:disabled.bp3-active, .bp3-dark .bp3-button-group.bp3-minimal .bp3-button.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-button-group .bp3-popover-wrapper,
.bp3-button-group .bp3-popover-target{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto; }
.bp3-button-group.bp3-fill{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
width:100%; }
.bp3-button-group .bp3-button.bp3-fill,
.bp3-button-group.bp3-fill .bp3-button:not(.bp3-fixed){
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto; }
.bp3-button-group.bp3-vertical{
-webkit-box-align:stretch;
-ms-flex-align:stretch;
align-items:stretch;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
vertical-align:top; }
.bp3-button-group.bp3-vertical.bp3-fill{
height:100%;
width:unset; }
.bp3-button-group.bp3-vertical .bp3-button{
margin-right:0 !important;
width:100%; }
.bp3-button-group.bp3-vertical:not(.bp3-minimal) > .bp3-popover-wrapper:first-child .bp3-button,
.bp3-button-group.bp3-vertical:not(.bp3-minimal) > .bp3-button:first-child{
border-radius:3px 3px 0 0; }
.bp3-button-group.bp3-vertical:not(.bp3-minimal) > .bp3-popover-wrapper:last-child .bp3-button,
.bp3-button-group.bp3-vertical:not(.bp3-minimal) > .bp3-button:last-child{
border-radius:0 0 3px 3px; }
.bp3-button-group.bp3-vertical:not(.bp3-minimal) > .bp3-popover-wrapper:not(:last-child) .bp3-button,
.bp3-button-group.bp3-vertical:not(.bp3-minimal) > .bp3-button:not(:last-child){
margin-bottom:-1px; }
.bp3-button-group.bp3-align-left .bp3-button{
text-align:left; }
.bp3-dark .bp3-button-group:not(.bp3-minimal) > .bp3-popover-wrapper:not(:last-child) .bp3-button,
.bp3-dark .bp3-button-group:not(.bp3-minimal) > .bp3-button:not(:last-child){
margin-right:1px; }
.bp3-dark .bp3-button-group.bp3-vertical > .bp3-popover-wrapper:not(:last-child) .bp3-button,
.bp3-dark .bp3-button-group.bp3-vertical > .bp3-button:not(:last-child){
margin-bottom:1px; }
.bp3-callout{
font-size:14px;
line-height:1.5;
background-color:rgba(138, 155, 168, 0.15);
border-radius:3px;
padding:10px 12px 9px;
position:relative;
width:100%; }
.bp3-callout[class*="bp3-icon-"]{
padding-left:40px; }
.bp3-callout[class*="bp3-icon-"]::before{
font-family:"Icons20", sans-serif;
font-size:20px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
color:#5c7080;
left:10px;
position:absolute;
top:10px; }
.bp3-callout.bp3-callout-icon{
padding-left:40px; }
.bp3-callout.bp3-callout-icon > .bp3-icon:first-child{
color:#5c7080;
left:10px;
position:absolute;
top:10px; }
.bp3-callout .bp3-heading{
line-height:20px;
margin-bottom:5px;
margin-top:0; }
.bp3-callout .bp3-heading:last-child{
margin-bottom:0; }
.bp3-dark .bp3-callout{
background-color:rgba(138, 155, 168, 0.2); }
.bp3-dark .bp3-callout[class*="bp3-icon-"]::before{
color:#a7b6c2; }
.bp3-callout.bp3-intent-primary{
background-color:rgba(19, 124, 189, 0.15); }
.bp3-callout.bp3-intent-primary[class*="bp3-icon-"]::before,
.bp3-callout.bp3-intent-primary > .bp3-icon:first-child,
.bp3-callout.bp3-intent-primary .bp3-heading{
color:#106ba3; }
.bp3-dark .bp3-callout.bp3-intent-primary{
background-color:rgba(19, 124, 189, 0.25); }
.bp3-dark .bp3-callout.bp3-intent-primary[class*="bp3-icon-"]::before,
.bp3-dark .bp3-callout.bp3-intent-primary > .bp3-icon:first-child,
.bp3-dark .bp3-callout.bp3-intent-primary .bp3-heading{
color:#48aff0; }
.bp3-callout.bp3-intent-success{
background-color:rgba(15, 153, 96, 0.15); }
.bp3-callout.bp3-intent-success[class*="bp3-icon-"]::before,
.bp3-callout.bp3-intent-success > .bp3-icon:first-child,
.bp3-callout.bp3-intent-success .bp3-heading{
color:#0d8050; }
.bp3-dark .bp3-callout.bp3-intent-success{
background-color:rgba(15, 153, 96, 0.25); }
.bp3-dark .bp3-callout.bp3-intent-success[class*="bp3-icon-"]::before,
.bp3-dark .bp3-callout.bp3-intent-success > .bp3-icon:first-child,
.bp3-dark .bp3-callout.bp3-intent-success .bp3-heading{
color:#3dcc91; }
.bp3-callout.bp3-intent-warning{
background-color:rgba(217, 130, 43, 0.15); }
.bp3-callout.bp3-intent-warning[class*="bp3-icon-"]::before,
.bp3-callout.bp3-intent-warning > .bp3-icon:first-child,
.bp3-callout.bp3-intent-warning .bp3-heading{
color:#bf7326; }
.bp3-dark .bp3-callout.bp3-intent-warning{
background-color:rgba(217, 130, 43, 0.25); }
.bp3-dark .bp3-callout.bp3-intent-warning[class*="bp3-icon-"]::before,
.bp3-dark .bp3-callout.bp3-intent-warning > .bp3-icon:first-child,
.bp3-dark .bp3-callout.bp3-intent-warning .bp3-heading{
color:#ffb366; }
.bp3-callout.bp3-intent-danger{
background-color:rgba(219, 55, 55, 0.15); }
.bp3-callout.bp3-intent-danger[class*="bp3-icon-"]::before,
.bp3-callout.bp3-intent-danger > .bp3-icon:first-child,
.bp3-callout.bp3-intent-danger .bp3-heading{
color:#c23030; }
.bp3-dark .bp3-callout.bp3-intent-danger{
background-color:rgba(219, 55, 55, 0.25); }
.bp3-dark .bp3-callout.bp3-intent-danger[class*="bp3-icon-"]::before,
.bp3-dark .bp3-callout.bp3-intent-danger > .bp3-icon:first-child,
.bp3-dark .bp3-callout.bp3-intent-danger .bp3-heading{
color:#ff7373; }
.bp3-running-text .bp3-callout{
margin:20px 0; }
.bp3-card{
background-color:#ffffff;
border-radius:3px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.15), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.15), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0);
padding:20px;
-webkit-transition:-webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:-webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), box-shadow 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), box-shadow 200ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 200ms cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-card.bp3-dark,
.bp3-dark .bp3-card{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0); }
.bp3-elevation-0{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.15), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.15), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0); }
.bp3-elevation-0.bp3-dark,
.bp3-dark .bp3-elevation-0{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0); }
.bp3-elevation-1{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-elevation-1.bp3-dark,
.bp3-dark .bp3-elevation-1{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-elevation-2{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 1px 1px rgba(16, 22, 26, 0.2), 0 2px 6px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 1px 1px rgba(16, 22, 26, 0.2), 0 2px 6px rgba(16, 22, 26, 0.2); }
.bp3-elevation-2.bp3-dark,
.bp3-dark .bp3-elevation-2{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.4), 0 2px 6px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.4), 0 2px 6px rgba(16, 22, 26, 0.4); }
.bp3-elevation-3{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2); }
.bp3-elevation-3.bp3-dark,
.bp3-dark .bp3-elevation-3{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4); }
.bp3-elevation-4{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2); }
.bp3-elevation-4.bp3-dark,
.bp3-dark .bp3-elevation-4{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4); }
.bp3-card.bp3-interactive:hover{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
cursor:pointer; }
.bp3-card.bp3-interactive:hover.bp3-dark,
.bp3-dark .bp3-card.bp3-interactive:hover{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4); }
.bp3-card.bp3-interactive:active{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
opacity:0.9;
-webkit-transition-duration:0;
transition-duration:0; }
.bp3-card.bp3-interactive:active.bp3-dark,
.bp3-dark .bp3-card.bp3-interactive:active{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-collapse{
height:0;
overflow-y:hidden;
-webkit-transition:height 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:height 200ms cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-collapse .bp3-collapse-body{
-webkit-transition:-webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:-webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-collapse .bp3-collapse-body[aria-hidden="true"]{
display:none; }
.bp3-context-menu .bp3-popover-target{
display:block; }
.bp3-context-menu-popover-target{
position:fixed; }
.bp3-divider{
border-bottom:1px solid rgba(16, 22, 26, 0.15);
border-right:1px solid rgba(16, 22, 26, 0.15);
margin:5px; }
.bp3-dark .bp3-divider{
border-color:rgba(16, 22, 26, 0.4); }
.bp3-dialog-container{
opacity:1;
-webkit-transform:scale(1);
transform:scale(1);
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
min-height:100%;
pointer-events:none;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
width:100%; }
.bp3-dialog-container.bp3-overlay-enter > .bp3-dialog, .bp3-dialog-container.bp3-overlay-appear > .bp3-dialog{
opacity:0;
-webkit-transform:scale(0.5);
transform:scale(0.5); }
.bp3-dialog-container.bp3-overlay-enter-active > .bp3-dialog, .bp3-dialog-container.bp3-overlay-appear-active > .bp3-dialog{
opacity:1;
-webkit-transform:scale(1);
transform:scale(1);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:opacity, transform;
transition-property:opacity, transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11);
transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11); }
.bp3-dialog-container.bp3-overlay-exit > .bp3-dialog{
opacity:1;
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-dialog-container.bp3-overlay-exit-active > .bp3-dialog{
opacity:0;
-webkit-transform:scale(0.5);
transform:scale(0.5);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:opacity, transform;
transition-property:opacity, transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11);
transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11); }
.bp3-dialog{
background:#ebf1f5;
border-radius:6px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
margin:30px 0;
padding-bottom:20px;
pointer-events:all;
-webkit-user-select:text;
-moz-user-select:text;
-ms-user-select:text;
user-select:text;
width:500px; }
.bp3-dialog:focus{
outline:0; }
.bp3-dialog.bp3-dark,
.bp3-dark .bp3-dialog{
background:#293742;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dialog-header{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
background:#ffffff;
border-radius:6px 6px 0 0;
-webkit-box-shadow:0 1px 0 rgba(16, 22, 26, 0.15);
box-shadow:0 1px 0 rgba(16, 22, 26, 0.15);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
min-height:40px;
padding-left:20px;
padding-right:5px;
z-index:30; }
.bp3-dialog-header .bp3-icon-large,
.bp3-dialog-header .bp3-icon{
color:#5c7080;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
margin-right:10px; }
.bp3-dialog-header .bp3-heading{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
line-height:inherit;
margin:0; }
.bp3-dialog-header .bp3-heading:last-child{
margin-right:20px; }
.bp3-dark .bp3-dialog-header{
background:#30404d;
-webkit-box-shadow:0 1px 0 rgba(16, 22, 26, 0.4);
box-shadow:0 1px 0 rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-dialog-header .bp3-icon-large,
.bp3-dark .bp3-dialog-header .bp3-icon{
color:#a7b6c2; }
.bp3-dialog-body{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
line-height:18px;
margin:20px; }
.bp3-dialog-footer{
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
margin:0 20px; }
.bp3-dialog-footer-actions{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-pack:end;
-ms-flex-pack:end;
justify-content:flex-end; }
.bp3-dialog-footer-actions .bp3-button{
margin-left:10px; }
.bp3-multistep-dialog-panels{
display:-webkit-box;
display:-ms-flexbox;
display:flex; }
.bp3-multistep-dialog-left-panel{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:1;
-ms-flex:1;
flex:1;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column; }
.bp3-dark .bp3-multistep-dialog-left-panel{
background:#202b33; }
.bp3-multistep-dialog-right-panel{
background-color:#f5f8fa;
border-left:1px solid rgba(16, 22, 26, 0.15);
border-radius:0 0 6px 0;
-webkit-box-flex:3;
-ms-flex:3;
flex:3;
min-width:0; }
.bp3-dark .bp3-multistep-dialog-right-panel{
background-color:#293742;
border-left:1px solid rgba(16, 22, 26, 0.4); }
.bp3-multistep-dialog-footer{
background-color:#ffffff;
border-radius:0 0 6px 0;
border-top:1px solid rgba(16, 22, 26, 0.15);
padding:10px; }
.bp3-dark .bp3-multistep-dialog-footer{
background:#30404d;
border-top:1px solid rgba(16, 22, 26, 0.4); }
.bp3-dialog-step-container{
background-color:#f5f8fa;
border-bottom:1px solid rgba(16, 22, 26, 0.15); }
.bp3-dark .bp3-dialog-step-container{
background:#293742;
border-bottom:1px solid rgba(16, 22, 26, 0.4); }
.bp3-dialog-step-container.bp3-dialog-step-viewed{
background-color:#ffffff; }
.bp3-dark .bp3-dialog-step-container.bp3-dialog-step-viewed{
background:#30404d; }
.bp3-dialog-step{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
background-color:#f5f8fa;
border-radius:6px;
cursor:not-allowed;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
margin:4px;
padding:6px 14px; }
.bp3-dark .bp3-dialog-step{
background:#293742; }
.bp3-dialog-step-viewed .bp3-dialog-step{
background-color:#ffffff;
cursor:pointer; }
.bp3-dark .bp3-dialog-step-viewed .bp3-dialog-step{
background:#30404d; }
.bp3-dialog-step:hover{
background-color:#f5f8fa; }
.bp3-dark .bp3-dialog-step:hover{
background:#293742; }
.bp3-dialog-step-icon{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
background-color:rgba(92, 112, 128, 0.6);
border-radius:50%;
color:#ffffff;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
height:25px;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
width:25px; }
.bp3-dark .bp3-dialog-step-icon{
background-color:rgba(167, 182, 194, 0.6); }
.bp3-active.bp3-dialog-step-viewed .bp3-dialog-step-icon{
background-color:#2b95d6; }
.bp3-dialog-step-viewed .bp3-dialog-step-icon{
background-color:#8a9ba8; }
.bp3-dialog-step-title{
color:rgba(92, 112, 128, 0.6);
-webkit-box-flex:1;
-ms-flex:1;
flex:1;
padding-left:10px; }
.bp3-dark .bp3-dialog-step-title{
color:rgba(167, 182, 194, 0.6); }
.bp3-active.bp3-dialog-step-viewed .bp3-dialog-step-title{
color:#2b95d6; }
.bp3-dialog-step-viewed:not(.bp3-active) .bp3-dialog-step-title{
color:#182026; }
.bp3-dark .bp3-dialog-step-viewed:not(.bp3-active) .bp3-dialog-step-title{
color:#f5f8fa; }
.bp3-drawer{
background:#ffffff;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
margin:0;
padding:0; }
.bp3-drawer:focus{
outline:0; }
.bp3-drawer.bp3-position-top{
height:50%;
left:0;
right:0;
top:0; }
.bp3-drawer.bp3-position-top.bp3-overlay-enter, .bp3-drawer.bp3-position-top.bp3-overlay-appear{
-webkit-transform:translateY(-100%);
transform:translateY(-100%); }
.bp3-drawer.bp3-position-top.bp3-overlay-enter-active, .bp3-drawer.bp3-position-top.bp3-overlay-appear-active{
-webkit-transform:translateY(0);
transform:translateY(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-top.bp3-overlay-exit{
-webkit-transform:translateY(0);
transform:translateY(0); }
.bp3-drawer.bp3-position-top.bp3-overlay-exit-active{
-webkit-transform:translateY(-100%);
transform:translateY(-100%);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-bottom{
bottom:0;
height:50%;
left:0;
right:0; }
.bp3-drawer.bp3-position-bottom.bp3-overlay-enter, .bp3-drawer.bp3-position-bottom.bp3-overlay-appear{
-webkit-transform:translateY(100%);
transform:translateY(100%); }
.bp3-drawer.bp3-position-bottom.bp3-overlay-enter-active, .bp3-drawer.bp3-position-bottom.bp3-overlay-appear-active{
-webkit-transform:translateY(0);
transform:translateY(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-bottom.bp3-overlay-exit{
-webkit-transform:translateY(0);
transform:translateY(0); }
.bp3-drawer.bp3-position-bottom.bp3-overlay-exit-active{
-webkit-transform:translateY(100%);
transform:translateY(100%);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-left{
bottom:0;
left:0;
top:0;
width:50%; }
.bp3-drawer.bp3-position-left.bp3-overlay-enter, .bp3-drawer.bp3-position-left.bp3-overlay-appear{
-webkit-transform:translateX(-100%);
transform:translateX(-100%); }
.bp3-drawer.bp3-position-left.bp3-overlay-enter-active, .bp3-drawer.bp3-position-left.bp3-overlay-appear-active{
-webkit-transform:translateX(0);
transform:translateX(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-left.bp3-overlay-exit{
-webkit-transform:translateX(0);
transform:translateX(0); }
.bp3-drawer.bp3-position-left.bp3-overlay-exit-active{
-webkit-transform:translateX(-100%);
transform:translateX(-100%);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-right{
bottom:0;
right:0;
top:0;
width:50%; }
.bp3-drawer.bp3-position-right.bp3-overlay-enter, .bp3-drawer.bp3-position-right.bp3-overlay-appear{
-webkit-transform:translateX(100%);
transform:translateX(100%); }
.bp3-drawer.bp3-position-right.bp3-overlay-enter-active, .bp3-drawer.bp3-position-right.bp3-overlay-appear-active{
-webkit-transform:translateX(0);
transform:translateX(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-position-right.bp3-overlay-exit{
-webkit-transform:translateX(0);
transform:translateX(0); }
.bp3-drawer.bp3-position-right.bp3-overlay-exit-active{
-webkit-transform:translateX(100%);
transform:translateX(100%);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical){
bottom:0;
right:0;
top:0;
width:50%; }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical).bp3-overlay-enter, .bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical).bp3-overlay-appear{
-webkit-transform:translateX(100%);
transform:translateX(100%); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical).bp3-overlay-enter-active, .bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical).bp3-overlay-appear-active{
-webkit-transform:translateX(0);
transform:translateX(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical).bp3-overlay-exit{
-webkit-transform:translateX(0);
transform:translateX(0); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right):not(.bp3-vertical).bp3-overlay-exit-active{
-webkit-transform:translateX(100%);
transform:translateX(100%);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical{
bottom:0;
height:50%;
left:0;
right:0; }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical.bp3-overlay-enter, .bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical.bp3-overlay-appear{
-webkit-transform:translateY(100%);
transform:translateY(100%); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical.bp3-overlay-enter-active, .bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical.bp3-overlay-appear-active{
-webkit-transform:translateY(0);
transform:translateY(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical.bp3-overlay-exit{
-webkit-transform:translateY(0);
transform:translateY(0); }
.bp3-drawer:not(.bp3-position-top):not(.bp3-position-bottom):not(.bp3-position-left):not(
.bp3-position-right).bp3-vertical.bp3-overlay-exit-active{
-webkit-transform:translateY(100%);
transform:translateY(100%);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-drawer.bp3-dark,
.bp3-dark .bp3-drawer{
background:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-drawer-header{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
border-radius:0;
-webkit-box-shadow:0 1px 0 rgba(16, 22, 26, 0.15);
box-shadow:0 1px 0 rgba(16, 22, 26, 0.15);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
min-height:40px;
padding:5px;
padding-left:20px;
position:relative; }
.bp3-drawer-header .bp3-icon-large,
.bp3-drawer-header .bp3-icon{
color:#5c7080;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
margin-right:10px; }
.bp3-drawer-header .bp3-heading{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
line-height:inherit;
margin:0; }
.bp3-drawer-header .bp3-heading:last-child{
margin-right:20px; }
.bp3-dark .bp3-drawer-header{
-webkit-box-shadow:0 1px 0 rgba(16, 22, 26, 0.4);
box-shadow:0 1px 0 rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-drawer-header .bp3-icon-large,
.bp3-dark .bp3-drawer-header .bp3-icon{
color:#a7b6c2; }
.bp3-drawer-body{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
line-height:18px;
overflow:auto; }
.bp3-drawer-footer{
-webkit-box-shadow:inset 0 1px 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 0 1px 0 rgba(16, 22, 26, 0.15);
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
padding:10px 20px;
position:relative; }
.bp3-dark .bp3-drawer-footer{
-webkit-box-shadow:inset 0 1px 0 rgba(16, 22, 26, 0.4);
box-shadow:inset 0 1px 0 rgba(16, 22, 26, 0.4); }
.bp3-editable-text{
cursor:text;
display:inline-block;
max-width:100%;
position:relative;
vertical-align:top;
white-space:nowrap; }
.bp3-editable-text::before{
bottom:-3px;
left:-3px;
position:absolute;
right:-3px;
top:-3px;
border-radius:3px;
content:"";
-webkit-transition:background-color 100ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:background-color 100ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:background-color 100ms cubic-bezier(0.4, 1, 0.75, 0.9), box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:background-color 100ms cubic-bezier(0.4, 1, 0.75, 0.9), box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-editable-text:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15); }
.bp3-editable-text.bp3-editable-text-editing::before{
background-color:#ffffff;
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-editable-text.bp3-disabled::before{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-editable-text.bp3-intent-primary .bp3-editable-text-input,
.bp3-editable-text.bp3-intent-primary .bp3-editable-text-content{
color:#137cbd; }
.bp3-editable-text.bp3-intent-primary:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(19, 124, 189, 0.4);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(19, 124, 189, 0.4); }
.bp3-editable-text.bp3-intent-primary.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-editable-text.bp3-intent-success .bp3-editable-text-input,
.bp3-editable-text.bp3-intent-success .bp3-editable-text-content{
color:#0f9960; }
.bp3-editable-text.bp3-intent-success:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px rgba(15, 153, 96, 0.4);
box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px rgba(15, 153, 96, 0.4); }
.bp3-editable-text.bp3-intent-success.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-editable-text.bp3-intent-warning .bp3-editable-text-input,
.bp3-editable-text.bp3-intent-warning .bp3-editable-text-content{
color:#d9822b; }
.bp3-editable-text.bp3-intent-warning:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px rgba(217, 130, 43, 0.4);
box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px rgba(217, 130, 43, 0.4); }
.bp3-editable-text.bp3-intent-warning.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-editable-text.bp3-intent-danger .bp3-editable-text-input,
.bp3-editable-text.bp3-intent-danger .bp3-editable-text-content{
color:#db3737; }
.bp3-editable-text.bp3-intent-danger:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px rgba(219, 55, 55, 0.4);
box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px rgba(219, 55, 55, 0.4); }
.bp3-editable-text.bp3-intent-danger.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-editable-text:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(255, 255, 255, 0.15);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(255, 255, 255, 0.15); }
.bp3-dark .bp3-editable-text.bp3-editable-text-editing::before{
background-color:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-editable-text.bp3-disabled::before{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-editable-text.bp3-intent-primary .bp3-editable-text-content{
color:#48aff0; }
.bp3-dark .bp3-editable-text.bp3-intent-primary:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(72, 175, 240, 0), 0 0 0 0 rgba(72, 175, 240, 0), inset 0 0 0 1px rgba(72, 175, 240, 0.4);
box-shadow:0 0 0 0 rgba(72, 175, 240, 0), 0 0 0 0 rgba(72, 175, 240, 0), inset 0 0 0 1px rgba(72, 175, 240, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-primary.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #48aff0, 0 0 0 3px rgba(72, 175, 240, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #48aff0, 0 0 0 3px rgba(72, 175, 240, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-success .bp3-editable-text-content{
color:#3dcc91; }
.bp3-dark .bp3-editable-text.bp3-intent-success:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(61, 204, 145, 0), 0 0 0 0 rgba(61, 204, 145, 0), inset 0 0 0 1px rgba(61, 204, 145, 0.4);
box-shadow:0 0 0 0 rgba(61, 204, 145, 0), 0 0 0 0 rgba(61, 204, 145, 0), inset 0 0 0 1px rgba(61, 204, 145, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-success.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #3dcc91, 0 0 0 3px rgba(61, 204, 145, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #3dcc91, 0 0 0 3px rgba(61, 204, 145, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-warning .bp3-editable-text-content{
color:#ffb366; }
.bp3-dark .bp3-editable-text.bp3-intent-warning:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(255, 179, 102, 0), 0 0 0 0 rgba(255, 179, 102, 0), inset 0 0 0 1px rgba(255, 179, 102, 0.4);
box-shadow:0 0 0 0 rgba(255, 179, 102, 0), 0 0 0 0 rgba(255, 179, 102, 0), inset 0 0 0 1px rgba(255, 179, 102, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-warning.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #ffb366, 0 0 0 3px rgba(255, 179, 102, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #ffb366, 0 0 0 3px rgba(255, 179, 102, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-danger .bp3-editable-text-content{
color:#ff7373; }
.bp3-dark .bp3-editable-text.bp3-intent-danger:hover::before{
-webkit-box-shadow:0 0 0 0 rgba(255, 115, 115, 0), 0 0 0 0 rgba(255, 115, 115, 0), inset 0 0 0 1px rgba(255, 115, 115, 0.4);
box-shadow:0 0 0 0 rgba(255, 115, 115, 0), 0 0 0 0 rgba(255, 115, 115, 0), inset 0 0 0 1px rgba(255, 115, 115, 0.4); }
.bp3-dark .bp3-editable-text.bp3-intent-danger.bp3-editable-text-editing::before{
-webkit-box-shadow:0 0 0 1px #ff7373, 0 0 0 3px rgba(255, 115, 115, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #ff7373, 0 0 0 3px rgba(255, 115, 115, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-editable-text-input,
.bp3-editable-text-content{
color:inherit;
display:inherit;
font:inherit;
letter-spacing:inherit;
max-width:inherit;
min-width:inherit;
position:relative;
resize:none;
text-transform:inherit;
vertical-align:top; }
.bp3-editable-text-input{
background:none;
border:none;
-webkit-box-shadow:none;
box-shadow:none;
padding:0;
white-space:pre-wrap;
width:100%; }
.bp3-editable-text-input::-webkit-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-editable-text-input::-moz-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-editable-text-input:-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-editable-text-input::-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-editable-text-input::placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-editable-text-input:focus{
outline:none; }
.bp3-editable-text-input::-ms-clear{
display:none; }
.bp3-editable-text-content{
overflow:hidden;
padding-right:2px;
text-overflow:ellipsis;
white-space:pre; }
.bp3-editable-text-editing > .bp3-editable-text-content{
left:0;
position:absolute;
visibility:hidden; }
.bp3-editable-text-placeholder > .bp3-editable-text-content{
color:rgba(92, 112, 128, 0.6); }
.bp3-dark .bp3-editable-text-placeholder > .bp3-editable-text-content{
color:rgba(167, 182, 194, 0.6); }
.bp3-editable-text.bp3-multiline{
display:block; }
.bp3-editable-text.bp3-multiline .bp3-editable-text-content{
overflow:auto;
white-space:pre-wrap;
word-wrap:break-word; }
.bp3-divider{
border-bottom:1px solid rgba(16, 22, 26, 0.15);
border-right:1px solid rgba(16, 22, 26, 0.15);
margin:5px; }
.bp3-dark .bp3-divider{
border-color:rgba(16, 22, 26, 0.4); }
.bp3-control-group{
-webkit-transform:translateZ(0);
transform:translateZ(0);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:stretch;
-ms-flex-align:stretch;
align-items:stretch; }
.bp3-control-group > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-control-group > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-control-group .bp3-button,
.bp3-control-group .bp3-html-select,
.bp3-control-group .bp3-input,
.bp3-control-group .bp3-select{
position:relative; }
.bp3-control-group .bp3-input{
border-radius:inherit;
z-index:2; }
.bp3-control-group .bp3-input:focus{
border-radius:3px;
z-index:14; }
.bp3-control-group .bp3-input[class*="bp3-intent"]{
z-index:13; }
.bp3-control-group .bp3-input[class*="bp3-intent"]:focus{
z-index:15; }
.bp3-control-group .bp3-input[readonly], .bp3-control-group .bp3-input:disabled, .bp3-control-group .bp3-input.bp3-disabled{
z-index:1; }
.bp3-control-group .bp3-input-group[class*="bp3-intent"] .bp3-input{
z-index:13; }
.bp3-control-group .bp3-input-group[class*="bp3-intent"] .bp3-input:focus{
z-index:15; }
.bp3-control-group .bp3-button,
.bp3-control-group .bp3-html-select select,
.bp3-control-group .bp3-select select{
-webkit-transform:translateZ(0);
transform:translateZ(0);
border-radius:inherit;
z-index:4; }
.bp3-control-group .bp3-button:focus,
.bp3-control-group .bp3-html-select select:focus,
.bp3-control-group .bp3-select select:focus{
z-index:5; }
.bp3-control-group .bp3-button:hover,
.bp3-control-group .bp3-html-select select:hover,
.bp3-control-group .bp3-select select:hover{
z-index:6; }
.bp3-control-group .bp3-button:active,
.bp3-control-group .bp3-html-select select:active,
.bp3-control-group .bp3-select select:active{
z-index:7; }
.bp3-control-group .bp3-button[readonly], .bp3-control-group .bp3-button:disabled, .bp3-control-group .bp3-button.bp3-disabled,
.bp3-control-group .bp3-html-select select[readonly],
.bp3-control-group .bp3-html-select select:disabled,
.bp3-control-group .bp3-html-select select.bp3-disabled,
.bp3-control-group .bp3-select select[readonly],
.bp3-control-group .bp3-select select:disabled,
.bp3-control-group .bp3-select select.bp3-disabled{
z-index:3; }
.bp3-control-group .bp3-button[class*="bp3-intent"],
.bp3-control-group .bp3-html-select select[class*="bp3-intent"],
.bp3-control-group .bp3-select select[class*="bp3-intent"]{
z-index:9; }
.bp3-control-group .bp3-button[class*="bp3-intent"]:focus,
.bp3-control-group .bp3-html-select select[class*="bp3-intent"]:focus,
.bp3-control-group .bp3-select select[class*="bp3-intent"]:focus{
z-index:10; }
.bp3-control-group .bp3-button[class*="bp3-intent"]:hover,
.bp3-control-group .bp3-html-select select[class*="bp3-intent"]:hover,
.bp3-control-group .bp3-select select[class*="bp3-intent"]:hover{
z-index:11; }
.bp3-control-group .bp3-button[class*="bp3-intent"]:active,
.bp3-control-group .bp3-html-select select[class*="bp3-intent"]:active,
.bp3-control-group .bp3-select select[class*="bp3-intent"]:active{
z-index:12; }
.bp3-control-group .bp3-button[class*="bp3-intent"][readonly], .bp3-control-group .bp3-button[class*="bp3-intent"]:disabled, .bp3-control-group .bp3-button[class*="bp3-intent"].bp3-disabled,
.bp3-control-group .bp3-html-select select[class*="bp3-intent"][readonly],
.bp3-control-group .bp3-html-select select[class*="bp3-intent"]:disabled,
.bp3-control-group .bp3-html-select select[class*="bp3-intent"].bp3-disabled,
.bp3-control-group .bp3-select select[class*="bp3-intent"][readonly],
.bp3-control-group .bp3-select select[class*="bp3-intent"]:disabled,
.bp3-control-group .bp3-select select[class*="bp3-intent"].bp3-disabled{
z-index:8; }
.bp3-control-group .bp3-input-group > .bp3-icon,
.bp3-control-group .bp3-input-group > .bp3-button,
.bp3-control-group .bp3-input-group > .bp3-input-left-container,
.bp3-control-group .bp3-input-group > .bp3-input-action{
z-index:16; }
.bp3-control-group .bp3-select::after,
.bp3-control-group .bp3-html-select::after,
.bp3-control-group .bp3-select > .bp3-icon,
.bp3-control-group .bp3-html-select > .bp3-icon{
z-index:17; }
.bp3-control-group .bp3-select:focus-within{
z-index:5; }
.bp3-control-group:not(.bp3-vertical) > *:not(.bp3-divider){
margin-right:-1px; }
.bp3-control-group:not(.bp3-vertical) > .bp3-divider:not(:first-child){
margin-left:6px; }
.bp3-dark .bp3-control-group:not(.bp3-vertical) > *:not(.bp3-divider){
margin-right:0; }
.bp3-dark .bp3-control-group:not(.bp3-vertical) > .bp3-button + .bp3-button{
margin-left:1px; }
.bp3-control-group .bp3-popover-wrapper,
.bp3-control-group .bp3-popover-target{
border-radius:inherit; }
.bp3-control-group > :first-child{
border-radius:3px 0 0 3px; }
.bp3-control-group > :last-child{
border-radius:0 3px 3px 0;
margin-right:0; }
.bp3-control-group > :only-child{
border-radius:3px;
margin-right:0; }
.bp3-control-group .bp3-input-group .bp3-button{
border-radius:3px; }
.bp3-control-group .bp3-numeric-input:not(:first-child) .bp3-input-group{
border-bottom-left-radius:0;
border-top-left-radius:0; }
.bp3-control-group.bp3-fill{
width:100%; }
.bp3-control-group > .bp3-fill{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto; }
.bp3-control-group.bp3-fill > *:not(.bp3-fixed){
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto; }
.bp3-control-group.bp3-vertical{
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column; }
.bp3-control-group.bp3-vertical > *{
margin-top:-1px; }
.bp3-control-group.bp3-vertical > :first-child{
border-radius:3px 3px 0 0;
margin-top:0; }
.bp3-control-group.bp3-vertical > :last-child{
border-radius:0 0 3px 3px; }
.bp3-control{
cursor:pointer;
display:block;
margin-bottom:10px;
position:relative;
text-transform:none; }
.bp3-control input:checked ~ .bp3-control-indicator{
background-color:#137cbd;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
color:#ffffff; }
.bp3-control:hover input:checked ~ .bp3-control-indicator{
background-color:#106ba3;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2); }
.bp3-control input:not(:disabled):active:checked ~ .bp3-control-indicator{
background:#0e5a8a;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-control input:disabled:checked ~ .bp3-control-indicator{
background:rgba(19, 124, 189, 0.5);
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-control input:checked ~ .bp3-control-indicator{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control:hover input:checked ~ .bp3-control-indicator{
background-color:#106ba3;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control input:not(:disabled):active:checked ~ .bp3-control-indicator{
background-color:#0e5a8a;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-control input:disabled:checked ~ .bp3-control-indicator{
background:rgba(14, 90, 138, 0.5);
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-control:not(.bp3-align-right){
padding-left:26px; }
.bp3-control:not(.bp3-align-right) .bp3-control-indicator{
margin-left:-26px; }
.bp3-control.bp3-align-right{
padding-right:26px; }
.bp3-control.bp3-align-right .bp3-control-indicator{
margin-right:-26px; }
.bp3-control.bp3-disabled{
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-control.bp3-inline{
display:inline-block;
margin-right:20px; }
.bp3-control input{
left:0;
opacity:0;
position:absolute;
top:0;
z-index:-1; }
.bp3-control .bp3-control-indicator{
background-clip:padding-box;
background-color:#f5f8fa;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.8)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
border:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
cursor:pointer;
display:inline-block;
font-size:16px;
height:1em;
margin-right:10px;
margin-top:-3px;
position:relative;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
vertical-align:middle;
width:1em; }
.bp3-control .bp3-control-indicator::before{
content:"";
display:block;
height:1em;
width:1em; }
.bp3-control:hover .bp3-control-indicator{
background-color:#ebf1f5; }
.bp3-control input:not(:disabled):active ~ .bp3-control-indicator{
background:#d8e1e8;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-control input:disabled ~ .bp3-control-indicator{
background:rgba(206, 217, 224, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
cursor:not-allowed; }
.bp3-control input:focus ~ .bp3-control-indicator{
outline:rgba(19, 124, 189, 0.6) auto 2px;
outline-offset:2px;
-moz-outline-radius:6px; }
.bp3-control.bp3-align-right .bp3-control-indicator{
float:right;
margin-left:10px;
margin-top:1px; }
.bp3-control.bp3-large{
font-size:16px; }
.bp3-control.bp3-large:not(.bp3-align-right){
padding-left:30px; }
.bp3-control.bp3-large:not(.bp3-align-right) .bp3-control-indicator{
margin-left:-30px; }
.bp3-control.bp3-large.bp3-align-right{
padding-right:30px; }
.bp3-control.bp3-large.bp3-align-right .bp3-control-indicator{
margin-right:-30px; }
.bp3-control.bp3-large .bp3-control-indicator{
font-size:20px; }
.bp3-control.bp3-large.bp3-align-right .bp3-control-indicator{
margin-top:0; }
.bp3-control.bp3-checkbox input:indeterminate ~ .bp3-control-indicator{
background-color:#137cbd;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.1)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
color:#ffffff; }
.bp3-control.bp3-checkbox:hover input:indeterminate ~ .bp3-control-indicator{
background-color:#106ba3;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 -1px 0 rgba(16, 22, 26, 0.2); }
.bp3-control.bp3-checkbox input:not(:disabled):active:indeterminate ~ .bp3-control-indicator{
background:#0e5a8a;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-control.bp3-checkbox input:disabled:indeterminate ~ .bp3-control-indicator{
background:rgba(19, 124, 189, 0.5);
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-control.bp3-checkbox input:indeterminate ~ .bp3-control-indicator{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control.bp3-checkbox:hover input:indeterminate ~ .bp3-control-indicator{
background-color:#106ba3;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control.bp3-checkbox input:not(:disabled):active:indeterminate ~ .bp3-control-indicator{
background-color:#0e5a8a;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-control.bp3-checkbox input:disabled:indeterminate ~ .bp3-control-indicator{
background:rgba(14, 90, 138, 0.5);
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-control.bp3-checkbox .bp3-control-indicator{
border-radius:3px; }
.bp3-control.bp3-checkbox input:checked ~ .bp3-control-indicator::before{
background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill-rule='evenodd' clip-rule='evenodd' d='M12 5c-.28 0-.53.11-.71.29L7 9.59l-2.29-2.3a1.003 1.003 0 00-1.42 1.42l3 3c.18.18.43.29.71.29s.53-.11.71-.29l5-5A1.003 1.003 0 0012 5z' fill='white'/%3e%3c/svg%3e"); }
.bp3-control.bp3-checkbox input:indeterminate ~ .bp3-control-indicator::before{
background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill-rule='evenodd' clip-rule='evenodd' d='M11 7H5c-.55 0-1 .45-1 1s.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1z' fill='white'/%3e%3c/svg%3e"); }
.bp3-control.bp3-radio .bp3-control-indicator{
border-radius:50%; }
.bp3-control.bp3-radio input:checked ~ .bp3-control-indicator::before{
background-image:radial-gradient(#ffffff, #ffffff 28%, transparent 32%); }
.bp3-control.bp3-radio input:checked:disabled ~ .bp3-control-indicator::before{
opacity:0.5; }
.bp3-control.bp3-radio input:focus ~ .bp3-control-indicator{
-moz-outline-radius:16px; }
.bp3-control.bp3-switch input ~ .bp3-control-indicator{
background:rgba(167, 182, 194, 0.5); }
.bp3-control.bp3-switch:hover input ~ .bp3-control-indicator{
background:rgba(115, 134, 148, 0.5); }
.bp3-control.bp3-switch input:not(:disabled):active ~ .bp3-control-indicator{
background:rgba(92, 112, 128, 0.5); }
.bp3-control.bp3-switch input:disabled ~ .bp3-control-indicator{
background:rgba(206, 217, 224, 0.5); }
.bp3-control.bp3-switch input:disabled ~ .bp3-control-indicator::before{
background:rgba(255, 255, 255, 0.8); }
.bp3-control.bp3-switch input:checked ~ .bp3-control-indicator{
background:#137cbd; }
.bp3-control.bp3-switch:hover input:checked ~ .bp3-control-indicator{
background:#106ba3; }
.bp3-control.bp3-switch input:checked:not(:disabled):active ~ .bp3-control-indicator{
background:#0e5a8a; }
.bp3-control.bp3-switch input:checked:disabled ~ .bp3-control-indicator{
background:rgba(19, 124, 189, 0.5); }
.bp3-control.bp3-switch input:checked:disabled ~ .bp3-control-indicator::before{
background:rgba(255, 255, 255, 0.8); }
.bp3-control.bp3-switch:not(.bp3-align-right){
padding-left:38px; }
.bp3-control.bp3-switch:not(.bp3-align-right) .bp3-control-indicator{
margin-left:-38px; }
.bp3-control.bp3-switch.bp3-align-right{
padding-right:38px; }
.bp3-control.bp3-switch.bp3-align-right .bp3-control-indicator{
margin-right:-38px; }
.bp3-control.bp3-switch .bp3-control-indicator{
border:none;
border-radius:1.75em;
-webkit-box-shadow:none !important;
box-shadow:none !important;
min-width:1.75em;
-webkit-transition:background-color 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:background-color 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
width:auto; }
.bp3-control.bp3-switch .bp3-control-indicator::before{
background:#ffffff;
border-radius:50%;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.2);
height:calc(1em - 4px);
left:0;
margin:2px;
position:absolute;
-webkit-transition:left 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:left 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
width:calc(1em - 4px); }
.bp3-control.bp3-switch input:checked ~ .bp3-control-indicator::before{
left:calc(100% - 1em); }
.bp3-control.bp3-switch.bp3-large:not(.bp3-align-right){
padding-left:45px; }
.bp3-control.bp3-switch.bp3-large:not(.bp3-align-right) .bp3-control-indicator{
margin-left:-45px; }
.bp3-control.bp3-switch.bp3-large.bp3-align-right{
padding-right:45px; }
.bp3-control.bp3-switch.bp3-large.bp3-align-right .bp3-control-indicator{
margin-right:-45px; }
.bp3-dark .bp3-control.bp3-switch input ~ .bp3-control-indicator{
background:rgba(16, 22, 26, 0.5); }
.bp3-dark .bp3-control.bp3-switch:hover input ~ .bp3-control-indicator{
background:rgba(16, 22, 26, 0.7); }
.bp3-dark .bp3-control.bp3-switch input:not(:disabled):active ~ .bp3-control-indicator{
background:rgba(16, 22, 26, 0.9); }
.bp3-dark .bp3-control.bp3-switch input:disabled ~ .bp3-control-indicator{
background:rgba(57, 75, 89, 0.5); }
.bp3-dark .bp3-control.bp3-switch input:disabled ~ .bp3-control-indicator::before{
background:rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control.bp3-switch input:checked ~ .bp3-control-indicator{
background:#137cbd; }
.bp3-dark .bp3-control.bp3-switch:hover input:checked ~ .bp3-control-indicator{
background:#106ba3; }
.bp3-dark .bp3-control.bp3-switch input:checked:not(:disabled):active ~ .bp3-control-indicator{
background:#0e5a8a; }
.bp3-dark .bp3-control.bp3-switch input:checked:disabled ~ .bp3-control-indicator{
background:rgba(14, 90, 138, 0.5); }
.bp3-dark .bp3-control.bp3-switch input:checked:disabled ~ .bp3-control-indicator::before{
background:rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control.bp3-switch .bp3-control-indicator::before{
background:#394b59;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control.bp3-switch input:checked ~ .bp3-control-indicator::before{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-control.bp3-switch .bp3-switch-inner-text{
font-size:0.7em;
text-align:center; }
.bp3-control.bp3-switch .bp3-control-indicator-child:first-child{
line-height:0;
margin-left:0.5em;
margin-right:1.2em;
visibility:hidden; }
.bp3-control.bp3-switch .bp3-control-indicator-child:last-child{
line-height:1em;
margin-left:1.2em;
margin-right:0.5em;
visibility:visible; }
.bp3-control.bp3-switch input:checked ~ .bp3-control-indicator .bp3-control-indicator-child:first-child{
line-height:1em;
visibility:visible; }
.bp3-control.bp3-switch input:checked ~ .bp3-control-indicator .bp3-control-indicator-child:last-child{
line-height:0;
visibility:hidden; }
.bp3-dark .bp3-control{
color:#f5f8fa; }
.bp3-dark .bp3-control.bp3-disabled{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-control .bp3-control-indicator{
background-color:#394b59;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.05)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-control:hover .bp3-control-indicator{
background-color:#30404d; }
.bp3-dark .bp3-control input:not(:disabled):active ~ .bp3-control-indicator{
background:#202b33;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-control input:disabled ~ .bp3-control-indicator{
background:rgba(57, 75, 89, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
cursor:not-allowed; }
.bp3-dark .bp3-control.bp3-checkbox input:disabled:checked ~ .bp3-control-indicator, .bp3-dark .bp3-control.bp3-checkbox input:disabled:indeterminate ~ .bp3-control-indicator{
color:rgba(167, 182, 194, 0.6); }
.bp3-file-input{
cursor:pointer;
display:inline-block;
height:30px;
position:relative; }
.bp3-file-input input{
margin:0;
min-width:200px;
opacity:0; }
.bp3-file-input input:disabled + .bp3-file-upload-input,
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input{
background:rgba(206, 217, 224, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
resize:none; }
.bp3-file-input input:disabled + .bp3-file-upload-input::after,
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input::after{
background-color:rgba(206, 217, 224, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
outline:none; }
.bp3-file-input input:disabled + .bp3-file-upload-input::after.bp3-active, .bp3-file-input input:disabled + .bp3-file-upload-input::after.bp3-active:hover,
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input::after.bp3-active,
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input::after.bp3-active:hover{
background:rgba(206, 217, 224, 0.7); }
.bp3-dark .bp3-file-input input:disabled + .bp3-file-upload-input, .bp3-dark
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input{
background:rgba(57, 75, 89, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-input input:disabled + .bp3-file-upload-input::after, .bp3-dark
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input::after{
background-color:rgba(57, 75, 89, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-input input:disabled + .bp3-file-upload-input::after.bp3-active, .bp3-dark
.bp3-file-input input.bp3-disabled + .bp3-file-upload-input::after.bp3-active{
background:rgba(57, 75, 89, 0.7); }
.bp3-file-input.bp3-file-input-has-selection .bp3-file-upload-input{
color:#182026; }
.bp3-dark .bp3-file-input.bp3-file-input-has-selection .bp3-file-upload-input{
color:#f5f8fa; }
.bp3-file-input.bp3-fill{
width:100%; }
.bp3-file-input.bp3-large,
.bp3-large .bp3-file-input{
height:40px; }
.bp3-file-input .bp3-file-upload-input-custom-text::after{
content:attr(bp3-button-text); }
.bp3-file-upload-input{
-webkit-appearance:none;
-moz-appearance:none;
appearance:none;
background:#ffffff;
border:none;
border-radius:3px;
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
color:#182026;
font-size:14px;
font-weight:400;
height:30px;
line-height:30px;
outline:none;
padding:0 10px;
-webkit-transition:-webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:-webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
vertical-align:middle;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
color:rgba(92, 112, 128, 0.6);
left:0;
padding-right:80px;
position:absolute;
right:0;
top:0;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none; }
.bp3-file-upload-input::-webkit-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-file-upload-input::-moz-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-file-upload-input:-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-file-upload-input::-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-file-upload-input::placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-file-upload-input:focus, .bp3-file-upload-input.bp3-active{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-file-upload-input[type="search"], .bp3-file-upload-input.bp3-round{
border-radius:30px;
-webkit-box-sizing:border-box;
box-sizing:border-box;
padding-left:10px; }
.bp3-file-upload-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.15);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.15); }
.bp3-file-upload-input:disabled, .bp3-file-upload-input.bp3-disabled{
background:rgba(206, 217, 224, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
resize:none; }
.bp3-file-upload-input::after{
background-color:#f5f8fa;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.8)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
color:#182026;
min-height:24px;
min-width:24px;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
border-radius:3px;
content:"Browse";
line-height:24px;
margin:3px;
position:absolute;
right:0;
text-align:center;
top:0;
width:70px; }
.bp3-file-upload-input::after:hover{
background-clip:padding-box;
background-color:#ebf1f5;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); }
.bp3-file-upload-input::after:active, .bp3-file-upload-input::after.bp3-active{
background-color:#d8e1e8;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-file-upload-input::after:disabled, .bp3-file-upload-input::after.bp3-disabled{
background-color:rgba(206, 217, 224, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
outline:none; }
.bp3-file-upload-input::after:disabled.bp3-active, .bp3-file-upload-input::after:disabled.bp3-active:hover, .bp3-file-upload-input::after.bp3-disabled.bp3-active, .bp3-file-upload-input::after.bp3-disabled.bp3-active:hover{
background:rgba(206, 217, 224, 0.7); }
.bp3-file-upload-input:hover::after{
background-clip:padding-box;
background-color:#ebf1f5;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); }
.bp3-file-upload-input:active::after{
background-color:#d8e1e8;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-large .bp3-file-upload-input{
font-size:16px;
height:40px;
line-height:40px;
padding-right:95px; }
.bp3-large .bp3-file-upload-input[type="search"], .bp3-large .bp3-file-upload-input.bp3-round{
padding:0 15px; }
.bp3-large .bp3-file-upload-input::after{
min-height:30px;
min-width:30px;
line-height:30px;
margin:5px;
width:85px; }
.bp3-dark .bp3-file-upload-input{
background:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input::-webkit-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input::-moz-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input:-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input::-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input::placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input:focus{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-file-upload-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-file-upload-input:disabled, .bp3-dark .bp3-file-upload-input.bp3-disabled{
background:rgba(57, 75, 89, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input::after{
background-color:#394b59;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.05)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dark .bp3-file-upload-input::after:hover, .bp3-dark .bp3-file-upload-input::after:active, .bp3-dark .bp3-file-upload-input::after.bp3-active{
color:#f5f8fa; }
.bp3-dark .bp3-file-upload-input::after:hover{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-file-upload-input::after:active, .bp3-dark .bp3-file-upload-input::after.bp3-active{
background-color:#202b33;
background-image:none;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-file-upload-input::after:disabled, .bp3-dark .bp3-file-upload-input::after.bp3-disabled{
background-color:rgba(57, 75, 89, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-file-upload-input::after:disabled.bp3-active, .bp3-dark .bp3-file-upload-input::after.bp3-disabled.bp3-active{
background:rgba(57, 75, 89, 0.7); }
.bp3-dark .bp3-file-upload-input::after .bp3-button-spinner .bp3-spinner-head{
background:rgba(16, 22, 26, 0.5);
stroke:#8a9ba8; }
.bp3-dark .bp3-file-upload-input:hover::after{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-file-upload-input:active::after{
background-color:#202b33;
background-image:none;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-file-upload-input::after{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); }
.bp3-form-group{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
margin:0 0 15px; }
.bp3-form-group label.bp3-label{
margin-bottom:5px; }
.bp3-form-group .bp3-control{
margin-top:7px; }
.bp3-form-group .bp3-form-helper-text{
color:#5c7080;
font-size:12px;
margin-top:5px; }
.bp3-form-group.bp3-intent-primary .bp3-form-helper-text{
color:#106ba3; }
.bp3-form-group.bp3-intent-success .bp3-form-helper-text{
color:#0d8050; }
.bp3-form-group.bp3-intent-warning .bp3-form-helper-text{
color:#bf7326; }
.bp3-form-group.bp3-intent-danger .bp3-form-helper-text{
color:#c23030; }
.bp3-form-group.bp3-inline{
-webkit-box-align:start;
-ms-flex-align:start;
align-items:flex-start;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row; }
.bp3-form-group.bp3-inline.bp3-large label.bp3-label{
line-height:40px;
margin:0 10px 0 0; }
.bp3-form-group.bp3-inline label.bp3-label{
line-height:30px;
margin:0 10px 0 0; }
.bp3-form-group.bp3-disabled .bp3-label,
.bp3-form-group.bp3-disabled .bp3-text-muted,
.bp3-form-group.bp3-disabled .bp3-form-helper-text{
color:rgba(92, 112, 128, 0.6) !important; }
.bp3-dark .bp3-form-group.bp3-intent-primary .bp3-form-helper-text{
color:#48aff0; }
.bp3-dark .bp3-form-group.bp3-intent-success .bp3-form-helper-text{
color:#3dcc91; }
.bp3-dark .bp3-form-group.bp3-intent-warning .bp3-form-helper-text{
color:#ffb366; }
.bp3-dark .bp3-form-group.bp3-intent-danger .bp3-form-helper-text{
color:#ff7373; }
.bp3-dark .bp3-form-group .bp3-form-helper-text{
color:#a7b6c2; }
.bp3-dark .bp3-form-group.bp3-disabled .bp3-label,
.bp3-dark .bp3-form-group.bp3-disabled .bp3-text-muted,
.bp3-dark .bp3-form-group.bp3-disabled .bp3-form-helper-text{
color:rgba(167, 182, 194, 0.6) !important; }
.bp3-input-group{
display:block;
position:relative; }
.bp3-input-group .bp3-input{
position:relative;
width:100%; }
.bp3-input-group .bp3-input:not(:first-child){
padding-left:30px; }
.bp3-input-group .bp3-input:not(:last-child){
padding-right:30px; }
.bp3-input-group .bp3-input-action,
.bp3-input-group > .bp3-input-left-container,
.bp3-input-group > .bp3-button,
.bp3-input-group > .bp3-icon{
position:absolute;
top:0; }
.bp3-input-group .bp3-input-action:first-child,
.bp3-input-group > .bp3-input-left-container:first-child,
.bp3-input-group > .bp3-button:first-child,
.bp3-input-group > .bp3-icon:first-child{
left:0; }
.bp3-input-group .bp3-input-action:last-child,
.bp3-input-group > .bp3-input-left-container:last-child,
.bp3-input-group > .bp3-button:last-child,
.bp3-input-group > .bp3-icon:last-child{
right:0; }
.bp3-input-group .bp3-button{
min-height:24px;
min-width:24px;
margin:3px;
padding:0 7px; }
.bp3-input-group .bp3-button:empty{
padding:0; }
.bp3-input-group > .bp3-input-left-container,
.bp3-input-group > .bp3-icon{
z-index:1; }
.bp3-input-group > .bp3-input-left-container > .bp3-icon,
.bp3-input-group > .bp3-icon{
color:#5c7080; }
.bp3-input-group > .bp3-input-left-container > .bp3-icon:empty,
.bp3-input-group > .bp3-icon:empty{
font-family:"Icons16", sans-serif;
font-size:16px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased; }
.bp3-input-group > .bp3-input-left-container > .bp3-icon,
.bp3-input-group > .bp3-icon,
.bp3-input-group .bp3-input-action > .bp3-spinner{
margin:7px; }
.bp3-input-group .bp3-tag{
margin:5px; }
.bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:not(:hover):not(:focus),
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:not(:hover):not(:focus){
color:#5c7080; }
.bp3-dark .bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:not(:hover):not(:focus), .bp3-dark
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:not(:hover):not(:focus){
color:#a7b6c2; }
.bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:not(:hover):not(:focus) .bp3-icon, .bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:not(:hover):not(:focus) .bp3-icon-standard, .bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:not(:hover):not(:focus) .bp3-icon-large,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:not(:hover):not(:focus) .bp3-icon,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:not(:hover):not(:focus) .bp3-icon-standard,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:not(:hover):not(:focus) .bp3-icon-large{
color:#5c7080; }
.bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:disabled,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:disabled{
color:rgba(92, 112, 128, 0.6) !important; }
.bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:disabled .bp3-icon, .bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:disabled .bp3-icon-standard, .bp3-input-group .bp3-input:not(:focus) + .bp3-button.bp3-minimal:disabled .bp3-icon-large,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:disabled .bp3-icon,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:disabled .bp3-icon-standard,
.bp3-input-group .bp3-input:not(:focus) + .bp3-input-action .bp3-button.bp3-minimal:disabled .bp3-icon-large{
color:rgba(92, 112, 128, 0.6) !important; }
.bp3-input-group.bp3-disabled{
cursor:not-allowed; }
.bp3-input-group.bp3-disabled .bp3-icon{
color:rgba(92, 112, 128, 0.6); }
.bp3-input-group.bp3-large .bp3-button{
min-height:30px;
min-width:30px;
margin:5px; }
.bp3-input-group.bp3-large > .bp3-input-left-container > .bp3-icon,
.bp3-input-group.bp3-large > .bp3-icon,
.bp3-input-group.bp3-large .bp3-input-action > .bp3-spinner{
margin:12px; }
.bp3-input-group.bp3-large .bp3-input{
font-size:16px;
height:40px;
line-height:40px; }
.bp3-input-group.bp3-large .bp3-input[type="search"], .bp3-input-group.bp3-large .bp3-input.bp3-round{
padding:0 15px; }
.bp3-input-group.bp3-large .bp3-input:not(:first-child){
padding-left:40px; }
.bp3-input-group.bp3-large .bp3-input:not(:last-child){
padding-right:40px; }
.bp3-input-group.bp3-small .bp3-button{
min-height:20px;
min-width:20px;
margin:2px; }
.bp3-input-group.bp3-small .bp3-tag{
min-height:20px;
min-width:20px;
margin:2px; }
.bp3-input-group.bp3-small > .bp3-input-left-container > .bp3-icon,
.bp3-input-group.bp3-small > .bp3-icon,
.bp3-input-group.bp3-small .bp3-input-action > .bp3-spinner{
margin:4px; }
.bp3-input-group.bp3-small .bp3-input{
font-size:12px;
height:24px;
line-height:24px;
padding-left:8px;
padding-right:8px; }
.bp3-input-group.bp3-small .bp3-input[type="search"], .bp3-input-group.bp3-small .bp3-input.bp3-round{
padding:0 12px; }
.bp3-input-group.bp3-small .bp3-input:not(:first-child){
padding-left:24px; }
.bp3-input-group.bp3-small .bp3-input:not(:last-child){
padding-right:24px; }
.bp3-input-group.bp3-fill{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
width:100%; }
.bp3-input-group.bp3-round .bp3-button,
.bp3-input-group.bp3-round .bp3-input,
.bp3-input-group.bp3-round .bp3-tag{
border-radius:30px; }
.bp3-dark .bp3-input-group .bp3-icon{
color:#a7b6c2; }
.bp3-dark .bp3-input-group.bp3-disabled .bp3-icon{
color:rgba(167, 182, 194, 0.6); }
.bp3-input-group.bp3-intent-primary .bp3-input{
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px #137cbd, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px #137cbd, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-primary .bp3-input:focus{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-primary .bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #137cbd;
box-shadow:inset 0 0 0 1px #137cbd; }
.bp3-input-group.bp3-intent-primary .bp3-input:disabled, .bp3-input-group.bp3-intent-primary .bp3-input.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input-group.bp3-intent-primary > .bp3-icon{
color:#106ba3; }
.bp3-dark .bp3-input-group.bp3-intent-primary > .bp3-icon{
color:#48aff0; }
.bp3-input-group.bp3-intent-success .bp3-input{
-webkit-box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px #0f9960, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px #0f9960, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-success .bp3-input:focus{
-webkit-box-shadow:0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-success .bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #0f9960;
box-shadow:inset 0 0 0 1px #0f9960; }
.bp3-input-group.bp3-intent-success .bp3-input:disabled, .bp3-input-group.bp3-intent-success .bp3-input.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input-group.bp3-intent-success > .bp3-icon{
color:#0d8050; }
.bp3-dark .bp3-input-group.bp3-intent-success > .bp3-icon{
color:#3dcc91; }
.bp3-input-group.bp3-intent-warning .bp3-input{
-webkit-box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px #d9822b, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px #d9822b, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-warning .bp3-input:focus{
-webkit-box-shadow:0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-warning .bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #d9822b;
box-shadow:inset 0 0 0 1px #d9822b; }
.bp3-input-group.bp3-intent-warning .bp3-input:disabled, .bp3-input-group.bp3-intent-warning .bp3-input.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input-group.bp3-intent-warning > .bp3-icon{
color:#bf7326; }
.bp3-dark .bp3-input-group.bp3-intent-warning > .bp3-icon{
color:#ffb366; }
.bp3-input-group.bp3-intent-danger .bp3-input{
-webkit-box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px #db3737, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px #db3737, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-danger .bp3-input:focus{
-webkit-box-shadow:0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input-group.bp3-intent-danger .bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #db3737;
box-shadow:inset 0 0 0 1px #db3737; }
.bp3-input-group.bp3-intent-danger .bp3-input:disabled, .bp3-input-group.bp3-intent-danger .bp3-input.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input-group.bp3-intent-danger > .bp3-icon{
color:#c23030; }
.bp3-dark .bp3-input-group.bp3-intent-danger > .bp3-icon{
color:#ff7373; }
.bp3-input{
-webkit-appearance:none;
-moz-appearance:none;
appearance:none;
background:#ffffff;
border:none;
border-radius:3px;
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
color:#182026;
font-size:14px;
font-weight:400;
height:30px;
line-height:30px;
outline:none;
padding:0 10px;
-webkit-transition:-webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:-webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-box-shadow 100ms cubic-bezier(0.4, 1, 0.75, 0.9);
vertical-align:middle; }
.bp3-input::-webkit-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input::-moz-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input:-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input::-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input::placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input:focus, .bp3-input.bp3-active{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input[type="search"], .bp3-input.bp3-round{
border-radius:30px;
-webkit-box-sizing:border-box;
box-sizing:border-box;
padding-left:10px; }
.bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.15);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.15); }
.bp3-input:disabled, .bp3-input.bp3-disabled{
background:rgba(206, 217, 224, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
resize:none; }
.bp3-input.bp3-large{
font-size:16px;
height:40px;
line-height:40px; }
.bp3-input.bp3-large[type="search"], .bp3-input.bp3-large.bp3-round{
padding:0 15px; }
.bp3-input.bp3-small{
font-size:12px;
height:24px;
line-height:24px;
padding-left:8px;
padding-right:8px; }
.bp3-input.bp3-small[type="search"], .bp3-input.bp3-small.bp3-round{
padding:0 12px; }
.bp3-input.bp3-fill{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
width:100%; }
.bp3-dark .bp3-input{
background:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dark .bp3-input::-webkit-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-input::-moz-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-input:-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-input::-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-input::placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-input:focus{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input:disabled, .bp3-dark .bp3-input.bp3-disabled{
background:rgba(57, 75, 89, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-input.bp3-intent-primary{
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px #137cbd, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px #137cbd, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-primary:focus{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-primary[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #137cbd;
box-shadow:inset 0 0 0 1px #137cbd; }
.bp3-input.bp3-intent-primary:disabled, .bp3-input.bp3-intent-primary.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-input.bp3-intent-primary{
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px #137cbd, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px #137cbd, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-primary:focus{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-primary[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #137cbd;
box-shadow:inset 0 0 0 1px #137cbd; }
.bp3-dark .bp3-input.bp3-intent-primary:disabled, .bp3-dark .bp3-input.bp3-intent-primary.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input.bp3-intent-success{
-webkit-box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px #0f9960, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px #0f9960, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-success:focus{
-webkit-box-shadow:0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-success[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #0f9960;
box-shadow:inset 0 0 0 1px #0f9960; }
.bp3-input.bp3-intent-success:disabled, .bp3-input.bp3-intent-success.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-input.bp3-intent-success{
-webkit-box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px #0f9960, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), 0 0 0 0 rgba(15, 153, 96, 0), inset 0 0 0 1px #0f9960, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-success:focus{
-webkit-box-shadow:0 0 0 1px #0f9960, 0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #0f9960, 0 0 0 1px #0f9960, 0 0 0 3px rgba(15, 153, 96, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-success[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #0f9960;
box-shadow:inset 0 0 0 1px #0f9960; }
.bp3-dark .bp3-input.bp3-intent-success:disabled, .bp3-dark .bp3-input.bp3-intent-success.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input.bp3-intent-warning{
-webkit-box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px #d9822b, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px #d9822b, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-warning:focus{
-webkit-box-shadow:0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-warning[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #d9822b;
box-shadow:inset 0 0 0 1px #d9822b; }
.bp3-input.bp3-intent-warning:disabled, .bp3-input.bp3-intent-warning.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-input.bp3-intent-warning{
-webkit-box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px #d9822b, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), 0 0 0 0 rgba(217, 130, 43, 0), inset 0 0 0 1px #d9822b, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-warning:focus{
-webkit-box-shadow:0 0 0 1px #d9822b, 0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #d9822b, 0 0 0 1px #d9822b, 0 0 0 3px rgba(217, 130, 43, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-warning[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #d9822b;
box-shadow:inset 0 0 0 1px #d9822b; }
.bp3-dark .bp3-input.bp3-intent-warning:disabled, .bp3-dark .bp3-input.bp3-intent-warning.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input.bp3-intent-danger{
-webkit-box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px #db3737, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px #db3737, inset 0 0 0 1px rgba(16, 22, 26, 0.15), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-danger:focus{
-webkit-box-shadow:0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-input.bp3-intent-danger[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #db3737;
box-shadow:inset 0 0 0 1px #db3737; }
.bp3-input.bp3-intent-danger:disabled, .bp3-input.bp3-intent-danger.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-input.bp3-intent-danger{
-webkit-box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px #db3737, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), 0 0 0 0 rgba(219, 55, 55, 0), inset 0 0 0 1px #db3737, inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-danger:focus{
-webkit-box-shadow:0 0 0 1px #db3737, 0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #db3737, 0 0 0 1px #db3737, 0 0 0 3px rgba(219, 55, 55, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-input.bp3-intent-danger[readonly]{
-webkit-box-shadow:inset 0 0 0 1px #db3737;
box-shadow:inset 0 0 0 1px #db3737; }
.bp3-dark .bp3-input.bp3-intent-danger:disabled, .bp3-dark .bp3-input.bp3-intent-danger.bp3-disabled{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-input::-ms-clear{
display:none; }
textarea.bp3-input{
max-width:100%;
padding:10px; }
textarea.bp3-input, textarea.bp3-input.bp3-large, textarea.bp3-input.bp3-small{
height:auto;
line-height:inherit; }
textarea.bp3-input.bp3-small{
padding:8px; }
.bp3-dark textarea.bp3-input{
background:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), 0 0 0 0 rgba(19, 124, 189, 0), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dark textarea.bp3-input::-webkit-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark textarea.bp3-input::-moz-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark textarea.bp3-input:-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark textarea.bp3-input::-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark textarea.bp3-input::placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark textarea.bp3-input:focus{
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark textarea.bp3-input[readonly]{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark textarea.bp3-input:disabled, .bp3-dark textarea.bp3-input.bp3-disabled{
background:rgba(57, 75, 89, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
label.bp3-label{
display:block;
margin-bottom:15px;
margin-top:0; }
label.bp3-label .bp3-html-select,
label.bp3-label .bp3-input,
label.bp3-label .bp3-select,
label.bp3-label .bp3-slider,
label.bp3-label .bp3-popover-wrapper{
display:block;
margin-top:5px;
text-transform:none; }
label.bp3-label .bp3-button-group{
margin-top:5px; }
label.bp3-label .bp3-select select,
label.bp3-label .bp3-html-select select{
font-weight:400;
vertical-align:top;
width:100%; }
label.bp3-label.bp3-disabled,
label.bp3-label.bp3-disabled .bp3-text-muted{
color:rgba(92, 112, 128, 0.6); }
label.bp3-label.bp3-inline{
line-height:30px; }
label.bp3-label.bp3-inline .bp3-html-select,
label.bp3-label.bp3-inline .bp3-input,
label.bp3-label.bp3-inline .bp3-input-group,
label.bp3-label.bp3-inline .bp3-select,
label.bp3-label.bp3-inline .bp3-popover-wrapper{
display:inline-block;
margin:0 0 0 5px;
vertical-align:top; }
label.bp3-label.bp3-inline .bp3-button-group{
margin:0 0 0 5px; }
label.bp3-label.bp3-inline .bp3-input-group .bp3-input{
margin-left:0; }
label.bp3-label.bp3-inline.bp3-large{
line-height:40px; }
label.bp3-label:not(.bp3-inline) .bp3-popover-target{
display:block; }
.bp3-dark label.bp3-label{
color:#f5f8fa; }
.bp3-dark label.bp3-label.bp3-disabled,
.bp3-dark label.bp3-label.bp3-disabled .bp3-text-muted{
color:rgba(167, 182, 194, 0.6); }
.bp3-numeric-input .bp3-button-group.bp3-vertical > .bp3-button{
-webkit-box-flex:1;
-ms-flex:1 1 14px;
flex:1 1 14px;
min-height:0;
padding:0;
width:30px; }
.bp3-numeric-input .bp3-button-group.bp3-vertical > .bp3-button:first-child{
border-radius:0 3px 0 0; }
.bp3-numeric-input .bp3-button-group.bp3-vertical > .bp3-button:last-child{
border-radius:0 0 3px 0; }
.bp3-numeric-input .bp3-button-group.bp3-vertical:first-child > .bp3-button:first-child{
border-radius:3px 0 0 0; }
.bp3-numeric-input .bp3-button-group.bp3-vertical:first-child > .bp3-button:last-child{
border-radius:0 0 0 3px; }
.bp3-numeric-input.bp3-large .bp3-button-group.bp3-vertical > .bp3-button{
width:40px; }
form{
display:block; }
.bp3-html-select select,
.bp3-select select{
display:-webkit-inline-box;
display:-ms-inline-flexbox;
display:inline-flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
border:none;
border-radius:3px;
cursor:pointer;
font-size:14px;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
padding:5px 10px;
text-align:left;
vertical-align:middle;
background-color:#f5f8fa;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.8)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
color:#182026;
-moz-appearance:none;
-webkit-appearance:none;
border-radius:3px;
height:30px;
padding:0 25px 0 10px;
width:100%; }
.bp3-html-select select > *, .bp3-select select > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-html-select select > .bp3-fill, .bp3-select select > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-html-select select::before,
.bp3-select select::before, .bp3-html-select select > *, .bp3-select select > *{
margin-right:7px; }
.bp3-html-select select:empty::before,
.bp3-select select:empty::before,
.bp3-html-select select > :last-child,
.bp3-select select > :last-child{
margin-right:0; }
.bp3-html-select select:hover,
.bp3-select select:hover{
background-clip:padding-box;
background-color:#ebf1f5;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); }
.bp3-html-select select:active,
.bp3-select select:active, .bp3-html-select select.bp3-active,
.bp3-select select.bp3-active{
background-color:#d8e1e8;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-html-select select:disabled,
.bp3-select select:disabled, .bp3-html-select select.bp3-disabled,
.bp3-select select.bp3-disabled{
background-color:rgba(206, 217, 224, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
outline:none; }
.bp3-html-select select:disabled.bp3-active,
.bp3-select select:disabled.bp3-active, .bp3-html-select select:disabled.bp3-active:hover,
.bp3-select select:disabled.bp3-active:hover, .bp3-html-select select.bp3-disabled.bp3-active,
.bp3-select select.bp3-disabled.bp3-active, .bp3-html-select select.bp3-disabled.bp3-active:hover,
.bp3-select select.bp3-disabled.bp3-active:hover{
background:rgba(206, 217, 224, 0.7); }
.bp3-html-select.bp3-minimal select,
.bp3-select.bp3-minimal select{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-html-select.bp3-minimal select:hover,
.bp3-select.bp3-minimal select:hover{
background:rgba(167, 182, 194, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026;
text-decoration:none; }
.bp3-html-select.bp3-minimal select:active,
.bp3-select.bp3-minimal select:active, .bp3-html-select.bp3-minimal select.bp3-active,
.bp3-select.bp3-minimal select.bp3-active{
background:rgba(115, 134, 148, 0.3);
-webkit-box-shadow:none;
box-shadow:none;
color:#182026; }
.bp3-html-select.bp3-minimal select:disabled,
.bp3-select.bp3-minimal select:disabled, .bp3-html-select.bp3-minimal select:disabled:hover,
.bp3-select.bp3-minimal select:disabled:hover, .bp3-html-select.bp3-minimal select.bp3-disabled,
.bp3-select.bp3-minimal select.bp3-disabled, .bp3-html-select.bp3-minimal select.bp3-disabled:hover,
.bp3-select.bp3-minimal select.bp3-disabled:hover{
background:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-html-select.bp3-minimal select:disabled.bp3-active,
.bp3-select.bp3-minimal select:disabled.bp3-active, .bp3-html-select.bp3-minimal select:disabled:hover.bp3-active,
.bp3-select.bp3-minimal select:disabled:hover.bp3-active, .bp3-html-select.bp3-minimal select.bp3-disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-disabled.bp3-active, .bp3-html-select.bp3-minimal select.bp3-disabled:hover.bp3-active,
.bp3-select.bp3-minimal select.bp3-disabled:hover.bp3-active{
background:rgba(115, 134, 148, 0.3); }
.bp3-dark .bp3-html-select.bp3-minimal select, .bp3-html-select.bp3-minimal .bp3-dark select,
.bp3-dark .bp3-select.bp3-minimal select, .bp3-select.bp3-minimal .bp3-dark select{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:inherit; }
.bp3-dark .bp3-html-select.bp3-minimal select:hover, .bp3-html-select.bp3-minimal .bp3-dark select:hover,
.bp3-dark .bp3-select.bp3-minimal select:hover, .bp3-select.bp3-minimal .bp3-dark select:hover, .bp3-dark .bp3-html-select.bp3-minimal select:active, .bp3-html-select.bp3-minimal .bp3-dark select:active,
.bp3-dark .bp3-select.bp3-minimal select:active, .bp3-select.bp3-minimal .bp3-dark select:active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-html-select.bp3-minimal select:hover, .bp3-html-select.bp3-minimal .bp3-dark select:hover,
.bp3-dark .bp3-select.bp3-minimal select:hover, .bp3-select.bp3-minimal .bp3-dark select:hover{
background:rgba(138, 155, 168, 0.15); }
.bp3-dark .bp3-html-select.bp3-minimal select:active, .bp3-html-select.bp3-minimal .bp3-dark select:active,
.bp3-dark .bp3-select.bp3-minimal select:active, .bp3-select.bp3-minimal .bp3-dark select:active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-active{
background:rgba(138, 155, 168, 0.3);
color:#f5f8fa; }
.bp3-dark .bp3-html-select.bp3-minimal select:disabled, .bp3-html-select.bp3-minimal .bp3-dark select:disabled,
.bp3-dark .bp3-select.bp3-minimal select:disabled, .bp3-select.bp3-minimal .bp3-dark select:disabled, .bp3-dark .bp3-html-select.bp3-minimal select:disabled:hover, .bp3-html-select.bp3-minimal .bp3-dark select:disabled:hover,
.bp3-dark .bp3-select.bp3-minimal select:disabled:hover, .bp3-select.bp3-minimal .bp3-dark select:disabled:hover, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-disabled, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-disabled:hover, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-disabled:hover,
.bp3-dark .bp3-select.bp3-minimal select.bp3-disabled:hover, .bp3-select.bp3-minimal .bp3-dark select.bp3-disabled:hover{
background:none;
color:rgba(167, 182, 194, 0.6);
cursor:not-allowed; }
.bp3-dark .bp3-html-select.bp3-minimal select:disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select:disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select:disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select:disabled.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select:disabled:hover.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select:disabled:hover.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select:disabled:hover.bp3-active, .bp3-select.bp3-minimal .bp3-dark select:disabled:hover.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-disabled.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-disabled:hover.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-disabled:hover.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-disabled:hover.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-disabled:hover.bp3-active{
background:rgba(138, 155, 168, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-primary,
.bp3-select.bp3-minimal select.bp3-intent-primary{
color:#106ba3; }
.bp3-html-select.bp3-minimal select.bp3-intent-primary:hover,
.bp3-select.bp3-minimal select.bp3-intent-primary:hover, .bp3-html-select.bp3-minimal select.bp3-intent-primary:active,
.bp3-select.bp3-minimal select.bp3-intent-primary:active, .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-primary.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#106ba3; }
.bp3-html-select.bp3-minimal select.bp3-intent-primary:hover,
.bp3-select.bp3-minimal select.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.15);
color:#106ba3; }
.bp3-html-select.bp3-minimal select.bp3-intent-primary:active,
.bp3-select.bp3-minimal select.bp3-intent-primary:active, .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#106ba3; }
.bp3-html-select.bp3-minimal select.bp3-intent-primary:disabled,
.bp3-select.bp3-minimal select.bp3-intent-primary:disabled, .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-disabled,
.bp3-select.bp3-minimal select.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(16, 107, 163, 0.5); }
.bp3-html-select.bp3-minimal select.bp3-intent-primary:disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-primary:disabled.bp3-active, .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-primary .bp3-button-spinner .bp3-spinner-head, .bp3-select.bp3-minimal select.bp3-intent-primary .bp3-button-spinner .bp3-spinner-head{
stroke:#106ba3; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary{
color:#48aff0; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary:hover, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary:hover,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary:hover, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary:hover{
background:rgba(19, 124, 189, 0.2);
color:#48aff0; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary:active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary:active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary:active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary:active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary.bp3-active{
background:rgba(19, 124, 189, 0.3);
color:#48aff0; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary:disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary:disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary:disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary:disabled, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary.bp3-disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary.bp3-disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary.bp3-disabled{
background:none;
color:rgba(72, 175, 240, 0.5); }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary:disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary:disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary:disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary:disabled.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-primary.bp3-disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-primary.bp3-disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-primary.bp3-disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-primary.bp3-disabled.bp3-active{
background:rgba(19, 124, 189, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-success,
.bp3-select.bp3-minimal select.bp3-intent-success{
color:#0d8050; }
.bp3-html-select.bp3-minimal select.bp3-intent-success:hover,
.bp3-select.bp3-minimal select.bp3-intent-success:hover, .bp3-html-select.bp3-minimal select.bp3-intent-success:active,
.bp3-select.bp3-minimal select.bp3-intent-success:active, .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-success.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#0d8050; }
.bp3-html-select.bp3-minimal select.bp3-intent-success:hover,
.bp3-select.bp3-minimal select.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.15);
color:#0d8050; }
.bp3-html-select.bp3-minimal select.bp3-intent-success:active,
.bp3-select.bp3-minimal select.bp3-intent-success:active, .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#0d8050; }
.bp3-html-select.bp3-minimal select.bp3-intent-success:disabled,
.bp3-select.bp3-minimal select.bp3-intent-success:disabled, .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-disabled,
.bp3-select.bp3-minimal select.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(13, 128, 80, 0.5); }
.bp3-html-select.bp3-minimal select.bp3-intent-success:disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-success:disabled.bp3-active, .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-success .bp3-button-spinner .bp3-spinner-head, .bp3-select.bp3-minimal select.bp3-intent-success .bp3-button-spinner .bp3-spinner-head{
stroke:#0d8050; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success{
color:#3dcc91; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success:hover, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success:hover,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success:hover, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success:hover{
background:rgba(15, 153, 96, 0.2);
color:#3dcc91; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success:active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success:active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success:active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success:active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success.bp3-active{
background:rgba(15, 153, 96, 0.3);
color:#3dcc91; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success:disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success:disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success:disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success:disabled, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success.bp3-disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success.bp3-disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success.bp3-disabled{
background:none;
color:rgba(61, 204, 145, 0.5); }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success:disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success:disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success:disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success:disabled.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-success.bp3-disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-success.bp3-disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-success.bp3-disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-success.bp3-disabled.bp3-active{
background:rgba(15, 153, 96, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-warning,
.bp3-select.bp3-minimal select.bp3-intent-warning{
color:#bf7326; }
.bp3-html-select.bp3-minimal select.bp3-intent-warning:hover,
.bp3-select.bp3-minimal select.bp3-intent-warning:hover, .bp3-html-select.bp3-minimal select.bp3-intent-warning:active,
.bp3-select.bp3-minimal select.bp3-intent-warning:active, .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-warning.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#bf7326; }
.bp3-html-select.bp3-minimal select.bp3-intent-warning:hover,
.bp3-select.bp3-minimal select.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.15);
color:#bf7326; }
.bp3-html-select.bp3-minimal select.bp3-intent-warning:active,
.bp3-select.bp3-minimal select.bp3-intent-warning:active, .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#bf7326; }
.bp3-html-select.bp3-minimal select.bp3-intent-warning:disabled,
.bp3-select.bp3-minimal select.bp3-intent-warning:disabled, .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-disabled,
.bp3-select.bp3-minimal select.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(191, 115, 38, 0.5); }
.bp3-html-select.bp3-minimal select.bp3-intent-warning:disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-warning:disabled.bp3-active, .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-warning .bp3-button-spinner .bp3-spinner-head, .bp3-select.bp3-minimal select.bp3-intent-warning .bp3-button-spinner .bp3-spinner-head{
stroke:#bf7326; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning{
color:#ffb366; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning:hover, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning:hover,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning:hover, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning:hover{
background:rgba(217, 130, 43, 0.2);
color:#ffb366; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning:active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning:active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning:active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning:active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning.bp3-active{
background:rgba(217, 130, 43, 0.3);
color:#ffb366; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning:disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning:disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning:disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning:disabled, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning.bp3-disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning.bp3-disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning.bp3-disabled{
background:none;
color:rgba(255, 179, 102, 0.5); }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning:disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning:disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning:disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning:disabled.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-warning.bp3-disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-warning.bp3-disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-warning.bp3-disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-warning.bp3-disabled.bp3-active{
background:rgba(217, 130, 43, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-danger,
.bp3-select.bp3-minimal select.bp3-intent-danger{
color:#c23030; }
.bp3-html-select.bp3-minimal select.bp3-intent-danger:hover,
.bp3-select.bp3-minimal select.bp3-intent-danger:hover, .bp3-html-select.bp3-minimal select.bp3-intent-danger:active,
.bp3-select.bp3-minimal select.bp3-intent-danger:active, .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-danger.bp3-active{
background:none;
-webkit-box-shadow:none;
box-shadow:none;
color:#c23030; }
.bp3-html-select.bp3-minimal select.bp3-intent-danger:hover,
.bp3-select.bp3-minimal select.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.15);
color:#c23030; }
.bp3-html-select.bp3-minimal select.bp3-intent-danger:active,
.bp3-select.bp3-minimal select.bp3-intent-danger:active, .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#c23030; }
.bp3-html-select.bp3-minimal select.bp3-intent-danger:disabled,
.bp3-select.bp3-minimal select.bp3-intent-danger:disabled, .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-disabled,
.bp3-select.bp3-minimal select.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(194, 48, 48, 0.5); }
.bp3-html-select.bp3-minimal select.bp3-intent-danger:disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-danger:disabled.bp3-active, .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-disabled.bp3-active,
.bp3-select.bp3-minimal select.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-html-select.bp3-minimal select.bp3-intent-danger .bp3-button-spinner .bp3-spinner-head, .bp3-select.bp3-minimal select.bp3-intent-danger .bp3-button-spinner .bp3-spinner-head{
stroke:#c23030; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger{
color:#ff7373; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger:hover, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger:hover,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger:hover, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger:hover{
background:rgba(219, 55, 55, 0.2);
color:#ff7373; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger:active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger:active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger:active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger:active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger.bp3-active{
background:rgba(219, 55, 55, 0.3);
color:#ff7373; }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger:disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger:disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger:disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger:disabled, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-disabled, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger.bp3-disabled,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger.bp3-disabled, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger.bp3-disabled{
background:none;
color:rgba(255, 115, 115, 0.5); }
.bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger:disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger:disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger:disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger:disabled.bp3-active, .bp3-dark .bp3-html-select.bp3-minimal select.bp3-intent-danger.bp3-disabled.bp3-active, .bp3-html-select.bp3-minimal .bp3-dark select.bp3-intent-danger.bp3-disabled.bp3-active,
.bp3-dark .bp3-select.bp3-minimal select.bp3-intent-danger.bp3-disabled.bp3-active, .bp3-select.bp3-minimal .bp3-dark select.bp3-intent-danger.bp3-disabled.bp3-active{
background:rgba(219, 55, 55, 0.3); }
.bp3-html-select.bp3-large select,
.bp3-select.bp3-large select{
font-size:16px;
height:40px;
padding-right:35px; }
.bp3-dark .bp3-html-select select, .bp3-dark .bp3-select select{
background-color:#394b59;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.05)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dark .bp3-html-select select:hover, .bp3-dark .bp3-select select:hover, .bp3-dark .bp3-html-select select:active, .bp3-dark .bp3-select select:active, .bp3-dark .bp3-html-select select.bp3-active, .bp3-dark .bp3-select select.bp3-active{
color:#f5f8fa; }
.bp3-dark .bp3-html-select select:hover, .bp3-dark .bp3-select select:hover{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-html-select select:active, .bp3-dark .bp3-select select:active, .bp3-dark .bp3-html-select select.bp3-active, .bp3-dark .bp3-select select.bp3-active{
background-color:#202b33;
background-image:none;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-html-select select:disabled, .bp3-dark .bp3-select select:disabled, .bp3-dark .bp3-html-select select.bp3-disabled, .bp3-dark .bp3-select select.bp3-disabled{
background-color:rgba(57, 75, 89, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-html-select select:disabled.bp3-active, .bp3-dark .bp3-select select:disabled.bp3-active, .bp3-dark .bp3-html-select select.bp3-disabled.bp3-active, .bp3-dark .bp3-select select.bp3-disabled.bp3-active{
background:rgba(57, 75, 89, 0.7); }
.bp3-dark .bp3-html-select select .bp3-button-spinner .bp3-spinner-head, .bp3-dark .bp3-select select .bp3-button-spinner .bp3-spinner-head{
background:rgba(16, 22, 26, 0.5);
stroke:#8a9ba8; }
.bp3-html-select select:disabled,
.bp3-select select:disabled{
background-color:rgba(206, 217, 224, 0.5);
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-html-select .bp3-icon,
.bp3-select .bp3-icon, .bp3-select::after{
color:#5c7080;
pointer-events:none;
position:absolute;
right:7px;
top:7px; }
.bp3-html-select .bp3-disabled.bp3-icon,
.bp3-select .bp3-disabled.bp3-icon, .bp3-disabled.bp3-select::after{
color:rgba(92, 112, 128, 0.6); }
.bp3-html-select,
.bp3-select{
display:inline-block;
letter-spacing:normal;
position:relative;
vertical-align:middle; }
.bp3-html-select select::-ms-expand,
.bp3-select select::-ms-expand{
display:none; }
.bp3-html-select .bp3-icon,
.bp3-select .bp3-icon{
color:#5c7080; }
.bp3-html-select .bp3-icon:hover,
.bp3-select .bp3-icon:hover{
color:#182026; }
.bp3-dark .bp3-html-select .bp3-icon, .bp3-dark
.bp3-select .bp3-icon{
color:#a7b6c2; }
.bp3-dark .bp3-html-select .bp3-icon:hover, .bp3-dark
.bp3-select .bp3-icon:hover{
color:#f5f8fa; }
.bp3-html-select.bp3-large::after,
.bp3-html-select.bp3-large .bp3-icon,
.bp3-select.bp3-large::after,
.bp3-select.bp3-large .bp3-icon{
right:12px;
top:12px; }
.bp3-html-select.bp3-fill,
.bp3-html-select.bp3-fill select,
.bp3-select.bp3-fill,
.bp3-select.bp3-fill select{
width:100%; }
.bp3-dark .bp3-html-select option, .bp3-dark
.bp3-select option{
background-color:#30404d;
color:#f5f8fa; }
.bp3-dark .bp3-html-select option:disabled, .bp3-dark
.bp3-select option:disabled{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-html-select::after, .bp3-dark
.bp3-select::after{
color:#a7b6c2; }
.bp3-select::after{
font-family:"Icons16", sans-serif;
font-size:16px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
content:""; }
.bp3-running-text table, table.bp3-html-table{
border-spacing:0;
font-size:14px; }
.bp3-running-text table th, table.bp3-html-table th,
.bp3-running-text table td,
table.bp3-html-table td{
padding:11px;
text-align:left;
vertical-align:top; }
.bp3-running-text table th, table.bp3-html-table th{
color:#182026;
font-weight:600; }
.bp3-running-text table td,
table.bp3-html-table td{
color:#182026; }
.bp3-running-text table tbody tr:first-child th, table.bp3-html-table tbody tr:first-child th,
.bp3-running-text table tbody tr:first-child td,
table.bp3-html-table tbody tr:first-child td,
.bp3-running-text table tfoot tr:first-child th,
table.bp3-html-table tfoot tr:first-child th,
.bp3-running-text table tfoot tr:first-child td,
table.bp3-html-table tfoot tr:first-child td{
-webkit-box-shadow:inset 0 1px 0 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 0 1px 0 0 rgba(16, 22, 26, 0.15); }
.bp3-dark .bp3-running-text table th, .bp3-running-text .bp3-dark table th, .bp3-dark table.bp3-html-table th{
color:#f5f8fa; }
.bp3-dark .bp3-running-text table td, .bp3-running-text .bp3-dark table td, .bp3-dark table.bp3-html-table td{
color:#f5f8fa; }
.bp3-dark .bp3-running-text table tbody tr:first-child th, .bp3-running-text .bp3-dark table tbody tr:first-child th, .bp3-dark table.bp3-html-table tbody tr:first-child th,
.bp3-dark .bp3-running-text table tbody tr:first-child td,
.bp3-running-text .bp3-dark table tbody tr:first-child td,
.bp3-dark table.bp3-html-table tbody tr:first-child td,
.bp3-dark .bp3-running-text table tfoot tr:first-child th,
.bp3-running-text .bp3-dark table tfoot tr:first-child th,
.bp3-dark table.bp3-html-table tfoot tr:first-child th,
.bp3-dark .bp3-running-text table tfoot tr:first-child td,
.bp3-running-text .bp3-dark table tfoot tr:first-child td,
.bp3-dark table.bp3-html-table tfoot tr:first-child td{
-webkit-box-shadow:inset 0 1px 0 0 rgba(255, 255, 255, 0.15);
box-shadow:inset 0 1px 0 0 rgba(255, 255, 255, 0.15); }
table.bp3-html-table.bp3-html-table-condensed th,
table.bp3-html-table.bp3-html-table-condensed td, table.bp3-html-table.bp3-small th,
table.bp3-html-table.bp3-small td{
padding-bottom:6px;
padding-top:6px; }
table.bp3-html-table.bp3-html-table-striped tbody tr:nth-child(odd) td{
background:rgba(191, 204, 214, 0.15); }
table.bp3-html-table.bp3-html-table-bordered th:not(:first-child){
-webkit-box-shadow:inset 1px 0 0 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 1px 0 0 0 rgba(16, 22, 26, 0.15); }
table.bp3-html-table.bp3-html-table-bordered tbody tr td,
table.bp3-html-table.bp3-html-table-bordered tfoot tr td{
-webkit-box-shadow:inset 0 1px 0 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 0 1px 0 0 rgba(16, 22, 26, 0.15); }
table.bp3-html-table.bp3-html-table-bordered tbody tr td:not(:first-child),
table.bp3-html-table.bp3-html-table-bordered tfoot tr td:not(:first-child){
-webkit-box-shadow:inset 1px 1px 0 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 1px 1px 0 0 rgba(16, 22, 26, 0.15); }
table.bp3-html-table.bp3-html-table-bordered.bp3-html-table-striped tbody tr:not(:first-child) td{
-webkit-box-shadow:none;
box-shadow:none; }
table.bp3-html-table.bp3-html-table-bordered.bp3-html-table-striped tbody tr:not(:first-child) td:not(:first-child){
-webkit-box-shadow:inset 1px 0 0 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 1px 0 0 0 rgba(16, 22, 26, 0.15); }
table.bp3-html-table.bp3-interactive tbody tr:hover td{
background-color:rgba(191, 204, 214, 0.3);
cursor:pointer; }
table.bp3-html-table.bp3-interactive tbody tr:active td{
background-color:rgba(191, 204, 214, 0.4); }
.bp3-dark table.bp3-html-table{ }
.bp3-dark table.bp3-html-table.bp3-html-table-striped tbody tr:nth-child(odd) td{
background:rgba(92, 112, 128, 0.15); }
.bp3-dark table.bp3-html-table.bp3-html-table-bordered th:not(:first-child){
-webkit-box-shadow:inset 1px 0 0 0 rgba(255, 255, 255, 0.15);
box-shadow:inset 1px 0 0 0 rgba(255, 255, 255, 0.15); }
.bp3-dark table.bp3-html-table.bp3-html-table-bordered tbody tr td,
.bp3-dark table.bp3-html-table.bp3-html-table-bordered tfoot tr td{
-webkit-box-shadow:inset 0 1px 0 0 rgba(255, 255, 255, 0.15);
box-shadow:inset 0 1px 0 0 rgba(255, 255, 255, 0.15); }
.bp3-dark table.bp3-html-table.bp3-html-table-bordered tbody tr td:not(:first-child),
.bp3-dark table.bp3-html-table.bp3-html-table-bordered tfoot tr td:not(:first-child){
-webkit-box-shadow:inset 1px 1px 0 0 rgba(255, 255, 255, 0.15);
box-shadow:inset 1px 1px 0 0 rgba(255, 255, 255, 0.15); }
.bp3-dark table.bp3-html-table.bp3-html-table-bordered.bp3-html-table-striped tbody tr:not(:first-child) td{
-webkit-box-shadow:inset 1px 0 0 0 rgba(255, 255, 255, 0.15);
box-shadow:inset 1px 0 0 0 rgba(255, 255, 255, 0.15); }
.bp3-dark table.bp3-html-table.bp3-html-table-bordered.bp3-html-table-striped tbody tr:not(:first-child) td:first-child{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark table.bp3-html-table.bp3-interactive tbody tr:hover td{
background-color:rgba(92, 112, 128, 0.3);
cursor:pointer; }
.bp3-dark table.bp3-html-table.bp3-interactive tbody tr:active td{
background-color:rgba(92, 112, 128, 0.4); }
.bp3-key-combo{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center; }
.bp3-key-combo > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-key-combo > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-key-combo::before,
.bp3-key-combo > *{
margin-right:5px; }
.bp3-key-combo:empty::before,
.bp3-key-combo > :last-child{
margin-right:0; }
.bp3-hotkey-dialog{
padding-bottom:0;
top:40px; }
.bp3-hotkey-dialog .bp3-dialog-body{
margin:0;
padding:0; }
.bp3-hotkey-dialog .bp3-hotkey-label{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1; }
.bp3-hotkey-column{
margin:auto;
max-height:80vh;
overflow-y:auto;
padding:30px; }
.bp3-hotkey-column .bp3-heading{
margin-bottom:20px; }
.bp3-hotkey-column .bp3-heading:not(:first-child){
margin-top:40px; }
.bp3-hotkey{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-pack:justify;
-ms-flex-pack:justify;
justify-content:space-between;
margin-left:0;
margin-right:0; }
.bp3-hotkey:not(:last-child){
margin-bottom:10px; }
.bp3-icon{
display:inline-block;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
vertical-align:text-bottom; }
.bp3-icon:not(:empty)::before{
content:"" !important;
content:unset !important; }
.bp3-icon > svg{
display:block; }
.bp3-icon > svg:not([fill]){
fill:currentColor; }
.bp3-icon.bp3-intent-primary, .bp3-icon-standard.bp3-intent-primary, .bp3-icon-large.bp3-intent-primary{
color:#106ba3; }
.bp3-dark .bp3-icon.bp3-intent-primary, .bp3-dark .bp3-icon-standard.bp3-intent-primary, .bp3-dark .bp3-icon-large.bp3-intent-primary{
color:#48aff0; }
.bp3-icon.bp3-intent-success, .bp3-icon-standard.bp3-intent-success, .bp3-icon-large.bp3-intent-success{
color:#0d8050; }
.bp3-dark .bp3-icon.bp3-intent-success, .bp3-dark .bp3-icon-standard.bp3-intent-success, .bp3-dark .bp3-icon-large.bp3-intent-success{
color:#3dcc91; }
.bp3-icon.bp3-intent-warning, .bp3-icon-standard.bp3-intent-warning, .bp3-icon-large.bp3-intent-warning{
color:#bf7326; }
.bp3-dark .bp3-icon.bp3-intent-warning, .bp3-dark .bp3-icon-standard.bp3-intent-warning, .bp3-dark .bp3-icon-large.bp3-intent-warning{
color:#ffb366; }
.bp3-icon.bp3-intent-danger, .bp3-icon-standard.bp3-intent-danger, .bp3-icon-large.bp3-intent-danger{
color:#c23030; }
.bp3-dark .bp3-icon.bp3-intent-danger, .bp3-dark .bp3-icon-standard.bp3-intent-danger, .bp3-dark .bp3-icon-large.bp3-intent-danger{
color:#ff7373; }
span.bp3-icon-standard{
font-family:"Icons16", sans-serif;
font-size:16px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
display:inline-block; }
span.bp3-icon-large{
font-family:"Icons20", sans-serif;
font-size:20px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
display:inline-block; }
span.bp3-icon:empty{
font-family:"Icons20";
font-size:inherit;
font-style:normal;
font-weight:400;
line-height:1; }
span.bp3-icon:empty::before{
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased; }
.bp3-icon-add::before{
content:""; }
.bp3-icon-add-column-left::before{
content:""; }
.bp3-icon-add-column-right::before{
content:""; }
.bp3-icon-add-row-bottom::before{
content:""; }
.bp3-icon-add-row-top::before{
content:""; }
.bp3-icon-add-to-artifact::before{
content:""; }
.bp3-icon-add-to-folder::before{
content:""; }
.bp3-icon-airplane::before{
content:""; }
.bp3-icon-align-center::before{
content:""; }
.bp3-icon-align-justify::before{
content:""; }
.bp3-icon-align-left::before{
content:""; }
.bp3-icon-align-right::before{
content:""; }
.bp3-icon-alignment-bottom::before{
content:""; }
.bp3-icon-alignment-horizontal-center::before{
content:""; }
.bp3-icon-alignment-left::before{
content:""; }
.bp3-icon-alignment-right::before{
content:""; }
.bp3-icon-alignment-top::before{
content:""; }
.bp3-icon-alignment-vertical-center::before{
content:""; }
.bp3-icon-annotation::before{
content:""; }
.bp3-icon-application::before{
content:""; }
.bp3-icon-applications::before{
content:""; }
.bp3-icon-archive::before{
content:""; }
.bp3-icon-arrow-bottom-left::before{
content:"↙"; }
.bp3-icon-arrow-bottom-right::before{
content:"↘"; }
.bp3-icon-arrow-down::before{
content:"↓"; }
.bp3-icon-arrow-left::before{
content:"←"; }
.bp3-icon-arrow-right::before{
content:"→"; }
.bp3-icon-arrow-top-left::before{
content:"↖"; }
.bp3-icon-arrow-top-right::before{
content:"↗"; }
.bp3-icon-arrow-up::before{
content:"↑"; }
.bp3-icon-arrows-horizontal::before{
content:"↔"; }
.bp3-icon-arrows-vertical::before{
content:"↕"; }
.bp3-icon-asterisk::before{
content:"*"; }
.bp3-icon-automatic-updates::before{
content:""; }
.bp3-icon-badge::before{
content:""; }
.bp3-icon-ban-circle::before{
content:""; }
.bp3-icon-bank-account::before{
content:""; }
.bp3-icon-barcode::before{
content:""; }
.bp3-icon-blank::before{
content:""; }
.bp3-icon-blocked-person::before{
content:""; }
.bp3-icon-bold::before{
content:""; }
.bp3-icon-book::before{
content:""; }
.bp3-icon-bookmark::before{
content:""; }
.bp3-icon-box::before{
content:""; }
.bp3-icon-briefcase::before{
content:""; }
.bp3-icon-bring-data::before{
content:""; }
.bp3-icon-build::before{
content:""; }
.bp3-icon-calculator::before{
content:""; }
.bp3-icon-calendar::before{
content:""; }
.bp3-icon-camera::before{
content:""; }
.bp3-icon-caret-down::before{
content:"⌄"; }
.bp3-icon-caret-left::before{
content:"〈"; }
.bp3-icon-caret-right::before{
content:"〉"; }
.bp3-icon-caret-up::before{
content:"⌃"; }
.bp3-icon-cell-tower::before{
content:""; }
.bp3-icon-changes::before{
content:""; }
.bp3-icon-chart::before{
content:""; }
.bp3-icon-chat::before{
content:""; }
.bp3-icon-chevron-backward::before{
content:""; }
.bp3-icon-chevron-down::before{
content:""; }
.bp3-icon-chevron-forward::before{
content:""; }
.bp3-icon-chevron-left::before{
content:""; }
.bp3-icon-chevron-right::before{
content:""; }
.bp3-icon-chevron-up::before{
content:""; }
.bp3-icon-circle::before{
content:""; }
.bp3-icon-circle-arrow-down::before{
content:""; }
.bp3-icon-circle-arrow-left::before{
content:""; }
.bp3-icon-circle-arrow-right::before{
content:""; }
.bp3-icon-circle-arrow-up::before{
content:""; }
.bp3-icon-citation::before{
content:""; }
.bp3-icon-clean::before{
content:""; }
.bp3-icon-clipboard::before{
content:""; }
.bp3-icon-cloud::before{
content:"☁"; }
.bp3-icon-cloud-download::before{
content:""; }
.bp3-icon-cloud-upload::before{
content:""; }
.bp3-icon-code::before{
content:""; }
.bp3-icon-code-block::before{
content:""; }
.bp3-icon-cog::before{
content:""; }
.bp3-icon-collapse-all::before{
content:""; }
.bp3-icon-column-layout::before{
content:""; }
.bp3-icon-comment::before{
content:""; }
.bp3-icon-comparison::before{
content:""; }
.bp3-icon-compass::before{
content:""; }
.bp3-icon-compressed::before{
content:""; }
.bp3-icon-confirm::before{
content:""; }
.bp3-icon-console::before{
content:""; }
.bp3-icon-contrast::before{
content:""; }
.bp3-icon-control::before{
content:""; }
.bp3-icon-credit-card::before{
content:""; }
.bp3-icon-cross::before{
content:"✗"; }
.bp3-icon-crown::before{
content:""; }
.bp3-icon-cube::before{
content:""; }
.bp3-icon-cube-add::before{
content:""; }
.bp3-icon-cube-remove::before{
content:""; }
.bp3-icon-curved-range-chart::before{
content:""; }
.bp3-icon-cut::before{
content:""; }
.bp3-icon-dashboard::before{
content:""; }
.bp3-icon-data-lineage::before{
content:""; }
.bp3-icon-database::before{
content:""; }
.bp3-icon-delete::before{
content:""; }
.bp3-icon-delta::before{
content:"Δ"; }
.bp3-icon-derive-column::before{
content:""; }
.bp3-icon-desktop::before{
content:""; }
.bp3-icon-diagnosis::before{
content:""; }
.bp3-icon-diagram-tree::before{
content:""; }
.bp3-icon-direction-left::before{
content:""; }
.bp3-icon-direction-right::before{
content:""; }
.bp3-icon-disable::before{
content:""; }
.bp3-icon-document::before{
content:""; }
.bp3-icon-document-open::before{
content:""; }
.bp3-icon-document-share::before{
content:""; }
.bp3-icon-dollar::before{
content:"$"; }
.bp3-icon-dot::before{
content:"•"; }
.bp3-icon-double-caret-horizontal::before{
content:""; }
.bp3-icon-double-caret-vertical::before{
content:""; }
.bp3-icon-double-chevron-down::before{
content:""; }
.bp3-icon-double-chevron-left::before{
content:""; }
.bp3-icon-double-chevron-right::before{
content:""; }
.bp3-icon-double-chevron-up::before{
content:""; }
.bp3-icon-doughnut-chart::before{
content:""; }
.bp3-icon-download::before{
content:""; }
.bp3-icon-drag-handle-horizontal::before{
content:""; }
.bp3-icon-drag-handle-vertical::before{
content:""; }
.bp3-icon-draw::before{
content:""; }
.bp3-icon-drive-time::before{
content:""; }
.bp3-icon-duplicate::before{
content:""; }
.bp3-icon-edit::before{
content:"✎"; }
.bp3-icon-eject::before{
content:"⏏"; }
.bp3-icon-endorsed::before{
content:""; }
.bp3-icon-envelope::before{
content:"✉"; }
.bp3-icon-equals::before{
content:""; }
.bp3-icon-eraser::before{
content:""; }
.bp3-icon-error::before{
content:""; }
.bp3-icon-euro::before{
content:"€"; }
.bp3-icon-exchange::before{
content:""; }
.bp3-icon-exclude-row::before{
content:""; }
.bp3-icon-expand-all::before{
content:""; }
.bp3-icon-export::before{
content:""; }
.bp3-icon-eye-off::before{
content:""; }
.bp3-icon-eye-on::before{
content:""; }
.bp3-icon-eye-open::before{
content:""; }
.bp3-icon-fast-backward::before{
content:""; }
.bp3-icon-fast-forward::before{
content:""; }
.bp3-icon-feed::before{
content:""; }
.bp3-icon-feed-subscribed::before{
content:""; }
.bp3-icon-film::before{
content:""; }
.bp3-icon-filter::before{
content:""; }
.bp3-icon-filter-keep::before{
content:""; }
.bp3-icon-filter-list::before{
content:""; }
.bp3-icon-filter-open::before{
content:""; }
.bp3-icon-filter-remove::before{
content:""; }
.bp3-icon-flag::before{
content:"⚑"; }
.bp3-icon-flame::before{
content:""; }
.bp3-icon-flash::before{
content:""; }
.bp3-icon-floppy-disk::before{
content:""; }
.bp3-icon-flow-branch::before{
content:""; }
.bp3-icon-flow-end::before{
content:""; }
.bp3-icon-flow-linear::before{
content:""; }
.bp3-icon-flow-review::before{
content:""; }
.bp3-icon-flow-review-branch::before{
content:""; }
.bp3-icon-flows::before{
content:""; }
.bp3-icon-folder-close::before{
content:""; }
.bp3-icon-folder-new::before{
content:""; }
.bp3-icon-folder-open::before{
content:""; }
.bp3-icon-folder-shared::before{
content:""; }
.bp3-icon-folder-shared-open::before{
content:""; }
.bp3-icon-follower::before{
content:""; }
.bp3-icon-following::before{
content:""; }
.bp3-icon-font::before{
content:""; }
.bp3-icon-fork::before{
content:""; }
.bp3-icon-form::before{
content:""; }
.bp3-icon-full-circle::before{
content:""; }
.bp3-icon-full-stacked-chart::before{
content:""; }
.bp3-icon-fullscreen::before{
content:""; }
.bp3-icon-function::before{
content:""; }
.bp3-icon-gantt-chart::before{
content:""; }
.bp3-icon-geolocation::before{
content:""; }
.bp3-icon-geosearch::before{
content:""; }
.bp3-icon-git-branch::before{
content:""; }
.bp3-icon-git-commit::before{
content:""; }
.bp3-icon-git-merge::before{
content:""; }
.bp3-icon-git-new-branch::before{
content:""; }
.bp3-icon-git-pull::before{
content:""; }
.bp3-icon-git-push::before{
content:""; }
.bp3-icon-git-repo::before{
content:""; }
.bp3-icon-glass::before{
content:""; }
.bp3-icon-globe::before{
content:""; }
.bp3-icon-globe-network::before{
content:""; }
.bp3-icon-graph::before{
content:""; }
.bp3-icon-graph-remove::before{
content:""; }
.bp3-icon-greater-than::before{
content:""; }
.bp3-icon-greater-than-or-equal-to::before{
content:""; }
.bp3-icon-grid::before{
content:""; }
.bp3-icon-grid-view::before{
content:""; }
.bp3-icon-group-objects::before{
content:""; }
.bp3-icon-grouped-bar-chart::before{
content:""; }
.bp3-icon-hand::before{
content:""; }
.bp3-icon-hand-down::before{
content:""; }
.bp3-icon-hand-left::before{
content:""; }
.bp3-icon-hand-right::before{
content:""; }
.bp3-icon-hand-up::before{
content:""; }
.bp3-icon-header::before{
content:""; }
.bp3-icon-header-one::before{
content:""; }
.bp3-icon-header-two::before{
content:""; }
.bp3-icon-headset::before{
content:""; }
.bp3-icon-heart::before{
content:"♥"; }
.bp3-icon-heart-broken::before{
content:""; }
.bp3-icon-heat-grid::before{
content:""; }
.bp3-icon-heatmap::before{
content:""; }
.bp3-icon-help::before{
content:"?"; }
.bp3-icon-helper-management::before{
content:""; }
.bp3-icon-highlight::before{
content:""; }
.bp3-icon-history::before{
content:""; }
.bp3-icon-home::before{
content:"⌂"; }
.bp3-icon-horizontal-bar-chart::before{
content:""; }
.bp3-icon-horizontal-bar-chart-asc::before{
content:""; }
.bp3-icon-horizontal-bar-chart-desc::before{
content:""; }
.bp3-icon-horizontal-distribution::before{
content:""; }
.bp3-icon-id-number::before{
content:""; }
.bp3-icon-image-rotate-left::before{
content:""; }
.bp3-icon-image-rotate-right::before{
content:""; }
.bp3-icon-import::before{
content:""; }
.bp3-icon-inbox::before{
content:""; }
.bp3-icon-inbox-filtered::before{
content:""; }
.bp3-icon-inbox-geo::before{
content:""; }
.bp3-icon-inbox-search::before{
content:""; }
.bp3-icon-inbox-update::before{
content:""; }
.bp3-icon-info-sign::before{
content:"ℹ"; }
.bp3-icon-inheritance::before{
content:""; }
.bp3-icon-inner-join::before{
content:""; }
.bp3-icon-insert::before{
content:""; }
.bp3-icon-intersection::before{
content:""; }
.bp3-icon-ip-address::before{
content:""; }
.bp3-icon-issue::before{
content:""; }
.bp3-icon-issue-closed::before{
content:""; }
.bp3-icon-issue-new::before{
content:""; }
.bp3-icon-italic::before{
content:""; }
.bp3-icon-join-table::before{
content:""; }
.bp3-icon-key::before{
content:""; }
.bp3-icon-key-backspace::before{
content:""; }
.bp3-icon-key-command::before{
content:""; }
.bp3-icon-key-control::before{
content:""; }
.bp3-icon-key-delete::before{
content:""; }
.bp3-icon-key-enter::before{
content:""; }
.bp3-icon-key-escape::before{
content:""; }
.bp3-icon-key-option::before{
content:""; }
.bp3-icon-key-shift::before{
content:""; }
.bp3-icon-key-tab::before{
content:""; }
.bp3-icon-known-vehicle::before{
content:""; }
.bp3-icon-lab-test::before{
content:""; }
.bp3-icon-label::before{
content:""; }
.bp3-icon-layer::before{
content:""; }
.bp3-icon-layers::before{
content:""; }
.bp3-icon-layout::before{
content:""; }
.bp3-icon-layout-auto::before{
content:""; }
.bp3-icon-layout-balloon::before{
content:""; }
.bp3-icon-layout-circle::before{
content:""; }
.bp3-icon-layout-grid::before{
content:""; }
.bp3-icon-layout-group-by::before{
content:""; }
.bp3-icon-layout-hierarchy::before{
content:""; }
.bp3-icon-layout-linear::before{
content:""; }
.bp3-icon-layout-skew-grid::before{
content:""; }
.bp3-icon-layout-sorted-clusters::before{
content:""; }
.bp3-icon-learning::before{
content:""; }
.bp3-icon-left-join::before{
content:""; }
.bp3-icon-less-than::before{
content:""; }
.bp3-icon-less-than-or-equal-to::before{
content:""; }
.bp3-icon-lifesaver::before{
content:""; }
.bp3-icon-lightbulb::before{
content:""; }
.bp3-icon-link::before{
content:""; }
.bp3-icon-list::before{
content:"☰"; }
.bp3-icon-list-columns::before{
content:""; }
.bp3-icon-list-detail-view::before{
content:""; }
.bp3-icon-locate::before{
content:""; }
.bp3-icon-lock::before{
content:""; }
.bp3-icon-log-in::before{
content:""; }
.bp3-icon-log-out::before{
content:""; }
.bp3-icon-manual::before{
content:""; }
.bp3-icon-manually-entered-data::before{
content:""; }
.bp3-icon-map::before{
content:""; }
.bp3-icon-map-create::before{
content:""; }
.bp3-icon-map-marker::before{
content:""; }
.bp3-icon-maximize::before{
content:""; }
.bp3-icon-media::before{
content:""; }
.bp3-icon-menu::before{
content:""; }
.bp3-icon-menu-closed::before{
content:""; }
.bp3-icon-menu-open::before{
content:""; }
.bp3-icon-merge-columns::before{
content:""; }
.bp3-icon-merge-links::before{
content:""; }
.bp3-icon-minimize::before{
content:""; }
.bp3-icon-minus::before{
content:"−"; }
.bp3-icon-mobile-phone::before{
content:""; }
.bp3-icon-mobile-video::before{
content:""; }
.bp3-icon-moon::before{
content:""; }
.bp3-icon-more::before{
content:""; }
.bp3-icon-mountain::before{
content:""; }
.bp3-icon-move::before{
content:""; }
.bp3-icon-mugshot::before{
content:""; }
.bp3-icon-multi-select::before{
content:""; }
.bp3-icon-music::before{
content:""; }
.bp3-icon-new-drawing::before{
content:""; }
.bp3-icon-new-grid-item::before{
content:""; }
.bp3-icon-new-layer::before{
content:""; }
.bp3-icon-new-layers::before{
content:""; }
.bp3-icon-new-link::before{
content:""; }
.bp3-icon-new-object::before{
content:""; }
.bp3-icon-new-person::before{
content:""; }
.bp3-icon-new-prescription::before{
content:""; }
.bp3-icon-new-text-box::before{
content:""; }
.bp3-icon-ninja::before{
content:""; }
.bp3-icon-not-equal-to::before{
content:""; }
.bp3-icon-notifications::before{
content:""; }
.bp3-icon-notifications-updated::before{
content:""; }
.bp3-icon-numbered-list::before{
content:""; }
.bp3-icon-numerical::before{
content:""; }
.bp3-icon-office::before{
content:""; }
.bp3-icon-offline::before{
content:""; }
.bp3-icon-oil-field::before{
content:""; }
.bp3-icon-one-column::before{
content:""; }
.bp3-icon-outdated::before{
content:""; }
.bp3-icon-page-layout::before{
content:""; }
.bp3-icon-panel-stats::before{
content:""; }
.bp3-icon-panel-table::before{
content:""; }
.bp3-icon-paperclip::before{
content:""; }
.bp3-icon-paragraph::before{
content:""; }
.bp3-icon-path::before{
content:""; }
.bp3-icon-path-search::before{
content:""; }
.bp3-icon-pause::before{
content:""; }
.bp3-icon-people::before{
content:""; }
.bp3-icon-percentage::before{
content:""; }
.bp3-icon-person::before{
content:""; }
.bp3-icon-phone::before{
content:"☎"; }
.bp3-icon-pie-chart::before{
content:""; }
.bp3-icon-pin::before{
content:""; }
.bp3-icon-pivot::before{
content:""; }
.bp3-icon-pivot-table::before{
content:""; }
.bp3-icon-play::before{
content:""; }
.bp3-icon-plus::before{
content:"+"; }
.bp3-icon-polygon-filter::before{
content:""; }
.bp3-icon-power::before{
content:""; }
.bp3-icon-predictive-analysis::before{
content:""; }
.bp3-icon-prescription::before{
content:""; }
.bp3-icon-presentation::before{
content:""; }
.bp3-icon-print::before{
content:"⎙"; }
.bp3-icon-projects::before{
content:""; }
.bp3-icon-properties::before{
content:""; }
.bp3-icon-property::before{
content:""; }
.bp3-icon-publish-function::before{
content:""; }
.bp3-icon-pulse::before{
content:""; }
.bp3-icon-random::before{
content:""; }
.bp3-icon-record::before{
content:""; }
.bp3-icon-redo::before{
content:""; }
.bp3-icon-refresh::before{
content:""; }
.bp3-icon-regression-chart::before{
content:""; }
.bp3-icon-remove::before{
content:""; }
.bp3-icon-remove-column::before{
content:""; }
.bp3-icon-remove-column-left::before{
content:""; }
.bp3-icon-remove-column-right::before{
content:""; }
.bp3-icon-remove-row-bottom::before{
content:""; }
.bp3-icon-remove-row-top::before{
content:""; }
.bp3-icon-repeat::before{
content:""; }
.bp3-icon-reset::before{
content:""; }
.bp3-icon-resolve::before{
content:""; }
.bp3-icon-rig::before{
content:""; }
.bp3-icon-right-join::before{
content:""; }
.bp3-icon-ring::before{
content:""; }
.bp3-icon-rotate-document::before{
content:""; }
.bp3-icon-rotate-page::before{
content:""; }
.bp3-icon-satellite::before{
content:""; }
.bp3-icon-saved::before{
content:""; }
.bp3-icon-scatter-plot::before{
content:""; }
.bp3-icon-search::before{
content:""; }
.bp3-icon-search-around::before{
content:""; }
.bp3-icon-search-template::before{
content:""; }
.bp3-icon-search-text::before{
content:""; }
.bp3-icon-segmented-control::before{
content:""; }
.bp3-icon-select::before{
content:""; }
.bp3-icon-selection::before{
content:"⦿"; }
.bp3-icon-send-to::before{
content:""; }
.bp3-icon-send-to-graph::before{
content:""; }
.bp3-icon-send-to-map::before{
content:""; }
.bp3-icon-series-add::before{
content:""; }
.bp3-icon-series-configuration::before{
content:""; }
.bp3-icon-series-derived::before{
content:""; }
.bp3-icon-series-filtered::before{
content:""; }
.bp3-icon-series-search::before{
content:""; }
.bp3-icon-settings::before{
content:""; }
.bp3-icon-share::before{
content:""; }
.bp3-icon-shield::before{
content:""; }
.bp3-icon-shop::before{
content:""; }
.bp3-icon-shopping-cart::before{
content:""; }
.bp3-icon-signal-search::before{
content:""; }
.bp3-icon-sim-card::before{
content:""; }
.bp3-icon-slash::before{
content:""; }
.bp3-icon-small-cross::before{
content:""; }
.bp3-icon-small-minus::before{
content:""; }
.bp3-icon-small-plus::before{
content:""; }
.bp3-icon-small-tick::before{
content:""; }
.bp3-icon-snowflake::before{
content:""; }
.bp3-icon-social-media::before{
content:""; }
.bp3-icon-sort::before{
content:""; }
.bp3-icon-sort-alphabetical::before{
content:""; }
.bp3-icon-sort-alphabetical-desc::before{
content:""; }
.bp3-icon-sort-asc::before{
content:""; }
.bp3-icon-sort-desc::before{
content:""; }
.bp3-icon-sort-numerical::before{
content:""; }
.bp3-icon-sort-numerical-desc::before{
content:""; }
.bp3-icon-split-columns::before{
content:""; }
.bp3-icon-square::before{
content:""; }
.bp3-icon-stacked-chart::before{
content:""; }
.bp3-icon-star::before{
content:"★"; }
.bp3-icon-star-empty::before{
content:"☆"; }
.bp3-icon-step-backward::before{
content:""; }
.bp3-icon-step-chart::before{
content:""; }
.bp3-icon-step-forward::before{
content:""; }
.bp3-icon-stop::before{
content:""; }
.bp3-icon-stopwatch::before{
content:""; }
.bp3-icon-strikethrough::before{
content:""; }
.bp3-icon-style::before{
content:""; }
.bp3-icon-swap-horizontal::before{
content:""; }
.bp3-icon-swap-vertical::before{
content:""; }
.bp3-icon-symbol-circle::before{
content:""; }
.bp3-icon-symbol-cross::before{
content:""; }
.bp3-icon-symbol-diamond::before{
content:""; }
.bp3-icon-symbol-square::before{
content:""; }
.bp3-icon-symbol-triangle-down::before{
content:""; }
.bp3-icon-symbol-triangle-up::before{
content:""; }
.bp3-icon-tag::before{
content:""; }
.bp3-icon-take-action::before{
content:""; }
.bp3-icon-taxi::before{
content:""; }
.bp3-icon-text-highlight::before{
content:""; }
.bp3-icon-th::before{
content:""; }
.bp3-icon-th-derived::before{
content:""; }
.bp3-icon-th-disconnect::before{
content:""; }
.bp3-icon-th-filtered::before{
content:""; }
.bp3-icon-th-list::before{
content:""; }
.bp3-icon-thumbs-down::before{
content:""; }
.bp3-icon-thumbs-up::before{
content:""; }
.bp3-icon-tick::before{
content:"✓"; }
.bp3-icon-tick-circle::before{
content:""; }
.bp3-icon-time::before{
content:"⏲"; }
.bp3-icon-timeline-area-chart::before{
content:""; }
.bp3-icon-timeline-bar-chart::before{
content:""; }
.bp3-icon-timeline-events::before{
content:""; }
.bp3-icon-timeline-line-chart::before{
content:""; }
.bp3-icon-tint::before{
content:""; }
.bp3-icon-torch::before{
content:""; }
.bp3-icon-tractor::before{
content:""; }
.bp3-icon-train::before{
content:""; }
.bp3-icon-translate::before{
content:""; }
.bp3-icon-trash::before{
content:""; }
.bp3-icon-tree::before{
content:""; }
.bp3-icon-trending-down::before{
content:""; }
.bp3-icon-trending-up::before{
content:""; }
.bp3-icon-truck::before{
content:""; }
.bp3-icon-two-columns::before{
content:""; }
.bp3-icon-unarchive::before{
content:""; }
.bp3-icon-underline::before{
content:"⎁"; }
.bp3-icon-undo::before{
content:"⎌"; }
.bp3-icon-ungroup-objects::before{
content:""; }
.bp3-icon-unknown-vehicle::before{
content:""; }
.bp3-icon-unlock::before{
content:""; }
.bp3-icon-unpin::before{
content:""; }
.bp3-icon-unresolve::before{
content:""; }
.bp3-icon-updated::before{
content:""; }
.bp3-icon-upload::before{
content:""; }
.bp3-icon-user::before{
content:""; }
.bp3-icon-variable::before{
content:""; }
.bp3-icon-vertical-bar-chart-asc::before{
content:""; }
.bp3-icon-vertical-bar-chart-desc::before{
content:""; }
.bp3-icon-vertical-distribution::before{
content:""; }
.bp3-icon-video::before{
content:""; }
.bp3-icon-volume-down::before{
content:""; }
.bp3-icon-volume-off::before{
content:""; }
.bp3-icon-volume-up::before{
content:""; }
.bp3-icon-walk::before{
content:""; }
.bp3-icon-warning-sign::before{
content:""; }
.bp3-icon-waterfall-chart::before{
content:""; }
.bp3-icon-widget::before{
content:""; }
.bp3-icon-widget-button::before{
content:""; }
.bp3-icon-widget-footer::before{
content:""; }
.bp3-icon-widget-header::before{
content:""; }
.bp3-icon-wrench::before{
content:""; }
.bp3-icon-zoom-in::before{
content:""; }
.bp3-icon-zoom-out::before{
content:""; }
.bp3-icon-zoom-to-fit::before{
content:""; }
.bp3-submenu > .bp3-popover-wrapper{
display:block; }
.bp3-submenu .bp3-popover-target{
display:block; }
.bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-menu-item{ }
.bp3-submenu.bp3-popover{
-webkit-box-shadow:none;
box-shadow:none;
padding:0 5px; }
.bp3-submenu.bp3-popover > .bp3-popover-content{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-submenu.bp3-popover, .bp3-submenu.bp3-popover.bp3-dark{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-dark .bp3-submenu.bp3-popover > .bp3-popover-content, .bp3-submenu.bp3-popover.bp3-dark > .bp3-popover-content{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4); }
.bp3-menu{
background:#ffffff;
border-radius:3px;
color:#182026;
list-style:none;
margin:0;
min-width:180px;
padding:5px;
text-align:left; }
.bp3-menu-divider{
border-top:1px solid rgba(16, 22, 26, 0.15);
display:block;
margin:5px; }
.bp3-dark .bp3-menu-divider{
border-top-color:rgba(255, 255, 255, 0.15); }
.bp3-menu-item{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:start;
-ms-flex-align:start;
align-items:flex-start;
border-radius:2px;
color:inherit;
line-height:20px;
padding:5px 7px;
text-decoration:none;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none; }
.bp3-menu-item > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-menu-item > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-menu-item::before,
.bp3-menu-item > *{
margin-right:7px; }
.bp3-menu-item:empty::before,
.bp3-menu-item > :last-child{
margin-right:0; }
.bp3-menu-item > .bp3-fill{
word-break:break-word; }
.bp3-menu-item:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-menu-item{
background-color:rgba(167, 182, 194, 0.3);
cursor:pointer;
text-decoration:none; }
.bp3-menu-item.bp3-disabled{
background-color:inherit;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-dark .bp3-menu-item{
color:inherit; }
.bp3-dark .bp3-menu-item:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-menu-item{
background-color:rgba(138, 155, 168, 0.15);
color:inherit; }
.bp3-dark .bp3-menu-item.bp3-disabled{
background-color:inherit;
color:rgba(167, 182, 194, 0.6); }
.bp3-menu-item.bp3-intent-primary{
color:#106ba3; }
.bp3-menu-item.bp3-intent-primary .bp3-icon{
color:inherit; }
.bp3-menu-item.bp3-intent-primary::before, .bp3-menu-item.bp3-intent-primary::after,
.bp3-menu-item.bp3-intent-primary .bp3-menu-item-label{
color:#106ba3; }
.bp3-menu-item.bp3-intent-primary:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item, .bp3-menu-item.bp3-intent-primary.bp3-active{
background-color:#137cbd; }
.bp3-menu-item.bp3-intent-primary:active{
background-color:#106ba3; }
.bp3-menu-item.bp3-intent-primary:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item, .bp3-menu-item.bp3-intent-primary:hover::before, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item::before, .bp3-menu-item.bp3-intent-primary:hover::after, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item::after,
.bp3-menu-item.bp3-intent-primary:hover .bp3-menu-item-label,
.bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item .bp3-menu-item-label, .bp3-menu-item.bp3-intent-primary:active, .bp3-menu-item.bp3-intent-primary:active::before, .bp3-menu-item.bp3-intent-primary:active::after,
.bp3-menu-item.bp3-intent-primary:active .bp3-menu-item-label, .bp3-menu-item.bp3-intent-primary.bp3-active, .bp3-menu-item.bp3-intent-primary.bp3-active::before, .bp3-menu-item.bp3-intent-primary.bp3-active::after,
.bp3-menu-item.bp3-intent-primary.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-menu-item.bp3-intent-success{
color:#0d8050; }
.bp3-menu-item.bp3-intent-success .bp3-icon{
color:inherit; }
.bp3-menu-item.bp3-intent-success::before, .bp3-menu-item.bp3-intent-success::after,
.bp3-menu-item.bp3-intent-success .bp3-menu-item-label{
color:#0d8050; }
.bp3-menu-item.bp3-intent-success:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item, .bp3-menu-item.bp3-intent-success.bp3-active{
background-color:#0f9960; }
.bp3-menu-item.bp3-intent-success:active{
background-color:#0d8050; }
.bp3-menu-item.bp3-intent-success:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item, .bp3-menu-item.bp3-intent-success:hover::before, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item::before, .bp3-menu-item.bp3-intent-success:hover::after, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item::after,
.bp3-menu-item.bp3-intent-success:hover .bp3-menu-item-label,
.bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item .bp3-menu-item-label, .bp3-menu-item.bp3-intent-success:active, .bp3-menu-item.bp3-intent-success:active::before, .bp3-menu-item.bp3-intent-success:active::after,
.bp3-menu-item.bp3-intent-success:active .bp3-menu-item-label, .bp3-menu-item.bp3-intent-success.bp3-active, .bp3-menu-item.bp3-intent-success.bp3-active::before, .bp3-menu-item.bp3-intent-success.bp3-active::after,
.bp3-menu-item.bp3-intent-success.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-menu-item.bp3-intent-warning{
color:#bf7326; }
.bp3-menu-item.bp3-intent-warning .bp3-icon{
color:inherit; }
.bp3-menu-item.bp3-intent-warning::before, .bp3-menu-item.bp3-intent-warning::after,
.bp3-menu-item.bp3-intent-warning .bp3-menu-item-label{
color:#bf7326; }
.bp3-menu-item.bp3-intent-warning:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item, .bp3-menu-item.bp3-intent-warning.bp3-active{
background-color:#d9822b; }
.bp3-menu-item.bp3-intent-warning:active{
background-color:#bf7326; }
.bp3-menu-item.bp3-intent-warning:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item, .bp3-menu-item.bp3-intent-warning:hover::before, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item::before, .bp3-menu-item.bp3-intent-warning:hover::after, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item::after,
.bp3-menu-item.bp3-intent-warning:hover .bp3-menu-item-label,
.bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item .bp3-menu-item-label, .bp3-menu-item.bp3-intent-warning:active, .bp3-menu-item.bp3-intent-warning:active::before, .bp3-menu-item.bp3-intent-warning:active::after,
.bp3-menu-item.bp3-intent-warning:active .bp3-menu-item-label, .bp3-menu-item.bp3-intent-warning.bp3-active, .bp3-menu-item.bp3-intent-warning.bp3-active::before, .bp3-menu-item.bp3-intent-warning.bp3-active::after,
.bp3-menu-item.bp3-intent-warning.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-menu-item.bp3-intent-danger{
color:#c23030; }
.bp3-menu-item.bp3-intent-danger .bp3-icon{
color:inherit; }
.bp3-menu-item.bp3-intent-danger::before, .bp3-menu-item.bp3-intent-danger::after,
.bp3-menu-item.bp3-intent-danger .bp3-menu-item-label{
color:#c23030; }
.bp3-menu-item.bp3-intent-danger:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item, .bp3-menu-item.bp3-intent-danger.bp3-active{
background-color:#db3737; }
.bp3-menu-item.bp3-intent-danger:active{
background-color:#c23030; }
.bp3-menu-item.bp3-intent-danger:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item, .bp3-menu-item.bp3-intent-danger:hover::before, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item::before, .bp3-menu-item.bp3-intent-danger:hover::after, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item::after,
.bp3-menu-item.bp3-intent-danger:hover .bp3-menu-item-label,
.bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item .bp3-menu-item-label, .bp3-menu-item.bp3-intent-danger:active, .bp3-menu-item.bp3-intent-danger:active::before, .bp3-menu-item.bp3-intent-danger:active::after,
.bp3-menu-item.bp3-intent-danger:active .bp3-menu-item-label, .bp3-menu-item.bp3-intent-danger.bp3-active, .bp3-menu-item.bp3-intent-danger.bp3-active::before, .bp3-menu-item.bp3-intent-danger.bp3-active::after,
.bp3-menu-item.bp3-intent-danger.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-menu-item::before{
font-family:"Icons16", sans-serif;
font-size:16px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
margin-right:7px; }
.bp3-menu-item::before,
.bp3-menu-item > .bp3-icon{
color:#5c7080;
margin-top:2px; }
.bp3-menu-item .bp3-menu-item-label{
color:#5c7080; }
.bp3-menu-item:hover, .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-menu-item{
color:inherit; }
.bp3-menu-item.bp3-active, .bp3-menu-item:active{
background-color:rgba(115, 134, 148, 0.3); }
.bp3-menu-item.bp3-disabled{
background-color:inherit !important;
color:rgba(92, 112, 128, 0.6) !important;
cursor:not-allowed !important;
outline:none !important; }
.bp3-menu-item.bp3-disabled::before,
.bp3-menu-item.bp3-disabled > .bp3-icon,
.bp3-menu-item.bp3-disabled .bp3-menu-item-label{
color:rgba(92, 112, 128, 0.6) !important; }
.bp3-large .bp3-menu-item{
font-size:16px;
line-height:22px;
padding:9px 7px; }
.bp3-large .bp3-menu-item .bp3-icon{
margin-top:3px; }
.bp3-large .bp3-menu-item::before{
font-family:"Icons20", sans-serif;
font-size:20px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
margin-right:10px;
margin-top:1px; }
button.bp3-menu-item{
background:none;
border:none;
text-align:left;
width:100%; }
.bp3-menu-header{
border-top:1px solid rgba(16, 22, 26, 0.15);
display:block;
margin:5px;
cursor:default;
padding-left:2px; }
.bp3-dark .bp3-menu-header{
border-top-color:rgba(255, 255, 255, 0.15); }
.bp3-menu-header:first-of-type{
border-top:none; }
.bp3-menu-header > h6{
color:#182026;
font-weight:600;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
line-height:17px;
margin:0;
padding:10px 7px 0 1px; }
.bp3-dark .bp3-menu-header > h6{
color:#f5f8fa; }
.bp3-menu-header:first-of-type > h6{
padding-top:0; }
.bp3-large .bp3-menu-header > h6{
font-size:18px;
padding-bottom:5px;
padding-top:15px; }
.bp3-large .bp3-menu-header:first-of-type > h6{
padding-top:0; }
.bp3-dark .bp3-menu{
background:#30404d;
color:#f5f8fa; }
.bp3-dark .bp3-menu-item{ }
.bp3-dark .bp3-menu-item.bp3-intent-primary{
color:#48aff0; }
.bp3-dark .bp3-menu-item.bp3-intent-primary .bp3-icon{
color:inherit; }
.bp3-dark .bp3-menu-item.bp3-intent-primary::before, .bp3-dark .bp3-menu-item.bp3-intent-primary::after,
.bp3-dark .bp3-menu-item.bp3-intent-primary .bp3-menu-item-label{
color:#48aff0; }
.bp3-dark .bp3-menu-item.bp3-intent-primary:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-primary.bp3-active{
background-color:#137cbd; }
.bp3-dark .bp3-menu-item.bp3-intent-primary:active{
background-color:#106ba3; }
.bp3-dark .bp3-menu-item.bp3-intent-primary:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-primary:hover::before, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item::before, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item::before, .bp3-dark .bp3-menu-item.bp3-intent-primary:hover::after, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item::after, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item::after,
.bp3-dark .bp3-menu-item.bp3-intent-primary:hover .bp3-menu-item-label,
.bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item .bp3-menu-item-label,
.bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-primary.bp3-menu-item .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-primary:active, .bp3-dark .bp3-menu-item.bp3-intent-primary:active::before, .bp3-dark .bp3-menu-item.bp3-intent-primary:active::after,
.bp3-dark .bp3-menu-item.bp3-intent-primary:active .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-primary.bp3-active, .bp3-dark .bp3-menu-item.bp3-intent-primary.bp3-active::before, .bp3-dark .bp3-menu-item.bp3-intent-primary.bp3-active::after,
.bp3-dark .bp3-menu-item.bp3-intent-primary.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-dark .bp3-menu-item.bp3-intent-success{
color:#3dcc91; }
.bp3-dark .bp3-menu-item.bp3-intent-success .bp3-icon{
color:inherit; }
.bp3-dark .bp3-menu-item.bp3-intent-success::before, .bp3-dark .bp3-menu-item.bp3-intent-success::after,
.bp3-dark .bp3-menu-item.bp3-intent-success .bp3-menu-item-label{
color:#3dcc91; }
.bp3-dark .bp3-menu-item.bp3-intent-success:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-success.bp3-active{
background-color:#0f9960; }
.bp3-dark .bp3-menu-item.bp3-intent-success:active{
background-color:#0d8050; }
.bp3-dark .bp3-menu-item.bp3-intent-success:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-success:hover::before, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item::before, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item::before, .bp3-dark .bp3-menu-item.bp3-intent-success:hover::after, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item::after, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item::after,
.bp3-dark .bp3-menu-item.bp3-intent-success:hover .bp3-menu-item-label,
.bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item .bp3-menu-item-label,
.bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-success.bp3-menu-item .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-success:active, .bp3-dark .bp3-menu-item.bp3-intent-success:active::before, .bp3-dark .bp3-menu-item.bp3-intent-success:active::after,
.bp3-dark .bp3-menu-item.bp3-intent-success:active .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-success.bp3-active, .bp3-dark .bp3-menu-item.bp3-intent-success.bp3-active::before, .bp3-dark .bp3-menu-item.bp3-intent-success.bp3-active::after,
.bp3-dark .bp3-menu-item.bp3-intent-success.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-dark .bp3-menu-item.bp3-intent-warning{
color:#ffb366; }
.bp3-dark .bp3-menu-item.bp3-intent-warning .bp3-icon{
color:inherit; }
.bp3-dark .bp3-menu-item.bp3-intent-warning::before, .bp3-dark .bp3-menu-item.bp3-intent-warning::after,
.bp3-dark .bp3-menu-item.bp3-intent-warning .bp3-menu-item-label{
color:#ffb366; }
.bp3-dark .bp3-menu-item.bp3-intent-warning:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-warning.bp3-active{
background-color:#d9822b; }
.bp3-dark .bp3-menu-item.bp3-intent-warning:active{
background-color:#bf7326; }
.bp3-dark .bp3-menu-item.bp3-intent-warning:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-warning:hover::before, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item::before, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item::before, .bp3-dark .bp3-menu-item.bp3-intent-warning:hover::after, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item::after, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item::after,
.bp3-dark .bp3-menu-item.bp3-intent-warning:hover .bp3-menu-item-label,
.bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item .bp3-menu-item-label,
.bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-warning.bp3-menu-item .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-warning:active, .bp3-dark .bp3-menu-item.bp3-intent-warning:active::before, .bp3-dark .bp3-menu-item.bp3-intent-warning:active::after,
.bp3-dark .bp3-menu-item.bp3-intent-warning:active .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-warning.bp3-active, .bp3-dark .bp3-menu-item.bp3-intent-warning.bp3-active::before, .bp3-dark .bp3-menu-item.bp3-intent-warning.bp3-active::after,
.bp3-dark .bp3-menu-item.bp3-intent-warning.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-dark .bp3-menu-item.bp3-intent-danger{
color:#ff7373; }
.bp3-dark .bp3-menu-item.bp3-intent-danger .bp3-icon{
color:inherit; }
.bp3-dark .bp3-menu-item.bp3-intent-danger::before, .bp3-dark .bp3-menu-item.bp3-intent-danger::after,
.bp3-dark .bp3-menu-item.bp3-intent-danger .bp3-menu-item-label{
color:#ff7373; }
.bp3-dark .bp3-menu-item.bp3-intent-danger:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-danger.bp3-active{
background-color:#db3737; }
.bp3-dark .bp3-menu-item.bp3-intent-danger:active{
background-color:#c23030; }
.bp3-dark .bp3-menu-item.bp3-intent-danger:hover, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item, .bp3-dark .bp3-menu-item.bp3-intent-danger:hover::before, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item::before, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item::before, .bp3-dark .bp3-menu-item.bp3-intent-danger:hover::after, .bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item::after, .bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item::after,
.bp3-dark .bp3-menu-item.bp3-intent-danger:hover .bp3-menu-item-label,
.bp3-dark .bp3-submenu .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item .bp3-menu-item-label,
.bp3-submenu .bp3-dark .bp3-popover-target.bp3-popover-open > .bp3-intent-danger.bp3-menu-item .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-danger:active, .bp3-dark .bp3-menu-item.bp3-intent-danger:active::before, .bp3-dark .bp3-menu-item.bp3-intent-danger:active::after,
.bp3-dark .bp3-menu-item.bp3-intent-danger:active .bp3-menu-item-label, .bp3-dark .bp3-menu-item.bp3-intent-danger.bp3-active, .bp3-dark .bp3-menu-item.bp3-intent-danger.bp3-active::before, .bp3-dark .bp3-menu-item.bp3-intent-danger.bp3-active::after,
.bp3-dark .bp3-menu-item.bp3-intent-danger.bp3-active .bp3-menu-item-label{
color:#ffffff; }
.bp3-dark .bp3-menu-item::before,
.bp3-dark .bp3-menu-item > .bp3-icon{
color:#a7b6c2; }
.bp3-dark .bp3-menu-item .bp3-menu-item-label{
color:#a7b6c2; }
.bp3-dark .bp3-menu-item.bp3-active, .bp3-dark .bp3-menu-item:active{
background-color:rgba(138, 155, 168, 0.3); }
.bp3-dark .bp3-menu-item.bp3-disabled{
color:rgba(167, 182, 194, 0.6) !important; }
.bp3-dark .bp3-menu-item.bp3-disabled::before,
.bp3-dark .bp3-menu-item.bp3-disabled > .bp3-icon,
.bp3-dark .bp3-menu-item.bp3-disabled .bp3-menu-item-label{
color:rgba(167, 182, 194, 0.6) !important; }
.bp3-dark .bp3-menu-divider,
.bp3-dark .bp3-menu-header{
border-color:rgba(255, 255, 255, 0.15); }
.bp3-dark .bp3-menu-header > h6{
color:#f5f8fa; }
.bp3-label .bp3-menu{
margin-top:5px; }
.bp3-navbar{
background-color:#ffffff;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.2);
height:50px;
padding:0 15px;
position:relative;
width:100%;
z-index:10; }
.bp3-navbar.bp3-dark,
.bp3-dark .bp3-navbar{
background-color:#394b59; }
.bp3-navbar.bp3-dark{
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-navbar{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 0 0 rgba(16, 22, 26, 0), 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-navbar.bp3-fixed-top{
left:0;
position:fixed;
right:0;
top:0; }
.bp3-navbar-heading{
font-size:16px;
margin-right:15px; }
.bp3-navbar-group{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
height:50px; }
.bp3-navbar-group.bp3-align-left{
float:left; }
.bp3-navbar-group.bp3-align-right{
float:right; }
.bp3-navbar-divider{
border-left:1px solid rgba(16, 22, 26, 0.15);
height:20px;
margin:0 10px; }
.bp3-dark .bp3-navbar-divider{
border-left-color:rgba(255, 255, 255, 0.15); }
.bp3-non-ideal-state{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
height:100%;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
text-align:center;
width:100%; }
.bp3-non-ideal-state > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-non-ideal-state > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-non-ideal-state::before,
.bp3-non-ideal-state > *{
margin-bottom:20px; }
.bp3-non-ideal-state:empty::before,
.bp3-non-ideal-state > :last-child{
margin-bottom:0; }
.bp3-non-ideal-state > *{
max-width:400px; }
.bp3-non-ideal-state-visual{
color:rgba(92, 112, 128, 0.6);
font-size:60px; }
.bp3-dark .bp3-non-ideal-state-visual{
color:rgba(167, 182, 194, 0.6); }
.bp3-overflow-list{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-ms-flex-wrap:nowrap;
flex-wrap:nowrap;
min-width:0; }
.bp3-overflow-list-spacer{
-ms-flex-negative:1;
flex-shrink:1;
width:1px; }
body.bp3-overlay-open{
overflow:hidden; }
.bp3-overlay{
bottom:0;
left:0;
position:static;
right:0;
top:0;
z-index:20; }
.bp3-overlay:not(.bp3-overlay-open){
pointer-events:none; }
.bp3-overlay.bp3-overlay-container{
overflow:hidden;
position:fixed; }
.bp3-overlay.bp3-overlay-container.bp3-overlay-inline{
position:absolute; }
.bp3-overlay.bp3-overlay-scroll-container{
overflow:auto;
position:fixed; }
.bp3-overlay.bp3-overlay-scroll-container.bp3-overlay-inline{
position:absolute; }
.bp3-overlay.bp3-overlay-inline{
display:inline;
overflow:visible; }
.bp3-overlay-content{
position:fixed;
z-index:20; }
.bp3-overlay-inline .bp3-overlay-content,
.bp3-overlay-scroll-container .bp3-overlay-content{
position:absolute; }
.bp3-overlay-backdrop{
bottom:0;
left:0;
position:fixed;
right:0;
top:0;
opacity:1;
background-color:rgba(16, 22, 26, 0.7);
overflow:auto;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
z-index:20; }
.bp3-overlay-backdrop.bp3-overlay-enter, .bp3-overlay-backdrop.bp3-overlay-appear{
opacity:0; }
.bp3-overlay-backdrop.bp3-overlay-enter-active, .bp3-overlay-backdrop.bp3-overlay-appear-active{
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:opacity;
transition-property:opacity;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-overlay-backdrop.bp3-overlay-exit{
opacity:1; }
.bp3-overlay-backdrop.bp3-overlay-exit-active{
opacity:0;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:opacity;
transition-property:opacity;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-overlay-backdrop:focus{
outline:none; }
.bp3-overlay-inline .bp3-overlay-backdrop{
position:absolute; }
.bp3-panel-stack{
overflow:hidden;
position:relative; }
.bp3-panel-stack-header{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
-webkit-box-shadow:0 1px rgba(16, 22, 26, 0.15);
box-shadow:0 1px rgba(16, 22, 26, 0.15);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-ms-flex-negative:0;
flex-shrink:0;
height:30px;
z-index:1; }
.bp3-dark .bp3-panel-stack-header{
-webkit-box-shadow:0 1px rgba(255, 255, 255, 0.15);
box-shadow:0 1px rgba(255, 255, 255, 0.15); }
.bp3-panel-stack-header > span{
-webkit-box-align:stretch;
-ms-flex-align:stretch;
align-items:stretch;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:1;
-ms-flex:1;
flex:1; }
.bp3-panel-stack-header .bp3-heading{
margin:0 5px; }
.bp3-button.bp3-panel-stack-header-back{
margin-left:5px;
padding-left:0;
white-space:nowrap; }
.bp3-button.bp3-panel-stack-header-back .bp3-icon{
margin:0 2px; }
.bp3-panel-stack-view{
bottom:0;
left:0;
position:absolute;
right:0;
top:0;
background-color:#ffffff;
border-right:1px solid rgba(16, 22, 26, 0.15);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
margin-right:-1px;
overflow-y:auto;
z-index:1; }
.bp3-dark .bp3-panel-stack-view{
background-color:#30404d; }
.bp3-panel-stack-view:nth-last-child(n + 4){
display:none; }
.bp3-panel-stack-push .bp3-panel-stack-enter, .bp3-panel-stack-push .bp3-panel-stack-appear{
-webkit-transform:translateX(100%);
transform:translateX(100%);
opacity:0; }
.bp3-panel-stack-push .bp3-panel-stack-enter-active, .bp3-panel-stack-push .bp3-panel-stack-appear-active{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack-push .bp3-panel-stack-exit{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1; }
.bp3-panel-stack-push .bp3-panel-stack-exit-active{
-webkit-transform:translateX(-50%);
transform:translateX(-50%);
opacity:0;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack-pop .bp3-panel-stack-enter, .bp3-panel-stack-pop .bp3-panel-stack-appear{
-webkit-transform:translateX(-50%);
transform:translateX(-50%);
opacity:0; }
.bp3-panel-stack-pop .bp3-panel-stack-enter-active, .bp3-panel-stack-pop .bp3-panel-stack-appear-active{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack-pop .bp3-panel-stack-exit{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1; }
.bp3-panel-stack-pop .bp3-panel-stack-exit-active{
-webkit-transform:translateX(100%);
transform:translateX(100%);
opacity:0;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack2{
overflow:hidden;
position:relative; }
.bp3-panel-stack2-header{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
-webkit-box-shadow:0 1px rgba(16, 22, 26, 0.15);
box-shadow:0 1px rgba(16, 22, 26, 0.15);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-ms-flex-negative:0;
flex-shrink:0;
height:30px;
z-index:1; }
.bp3-dark .bp3-panel-stack2-header{
-webkit-box-shadow:0 1px rgba(255, 255, 255, 0.15);
box-shadow:0 1px rgba(255, 255, 255, 0.15); }
.bp3-panel-stack2-header > span{
-webkit-box-align:stretch;
-ms-flex-align:stretch;
align-items:stretch;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:1;
-ms-flex:1;
flex:1; }
.bp3-panel-stack2-header .bp3-heading{
margin:0 5px; }
.bp3-button.bp3-panel-stack2-header-back{
margin-left:5px;
padding-left:0;
white-space:nowrap; }
.bp3-button.bp3-panel-stack2-header-back .bp3-icon{
margin:0 2px; }
.bp3-panel-stack2-view{
bottom:0;
left:0;
position:absolute;
right:0;
top:0;
background-color:#ffffff;
border-right:1px solid rgba(16, 22, 26, 0.15);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
margin-right:-1px;
overflow-y:auto;
z-index:1; }
.bp3-dark .bp3-panel-stack2-view{
background-color:#30404d; }
.bp3-panel-stack2-view:nth-last-child(n + 4){
display:none; }
.bp3-panel-stack2-push .bp3-panel-stack2-enter, .bp3-panel-stack2-push .bp3-panel-stack2-appear{
-webkit-transform:translateX(100%);
transform:translateX(100%);
opacity:0; }
.bp3-panel-stack2-push .bp3-panel-stack2-enter-active, .bp3-panel-stack2-push .bp3-panel-stack2-appear-active{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack2-push .bp3-panel-stack2-exit{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1; }
.bp3-panel-stack2-push .bp3-panel-stack2-exit-active{
-webkit-transform:translateX(-50%);
transform:translateX(-50%);
opacity:0;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack2-pop .bp3-panel-stack2-enter, .bp3-panel-stack2-pop .bp3-panel-stack2-appear{
-webkit-transform:translateX(-50%);
transform:translateX(-50%);
opacity:0; }
.bp3-panel-stack2-pop .bp3-panel-stack2-enter-active, .bp3-panel-stack2-pop .bp3-panel-stack2-appear-active{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-panel-stack2-pop .bp3-panel-stack2-exit{
-webkit-transform:translate(0%);
transform:translate(0%);
opacity:1; }
.bp3-panel-stack2-pop .bp3-panel-stack2-exit-active{
-webkit-transform:translateX(100%);
transform:translateX(100%);
opacity:0;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:400ms;
transition-duration:400ms;
-webkit-transition-property:opacity, -webkit-transform;
transition-property:opacity, -webkit-transform;
transition-property:transform, opacity;
transition-property:transform, opacity, -webkit-transform;
-webkit-transition-timing-function:ease;
transition-timing-function:ease; }
.bp3-popover{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
-webkit-transform:scale(1);
transform:scale(1);
border-radius:3px;
display:inline-block;
z-index:20; }
.bp3-popover .bp3-popover-arrow{
height:30px;
position:absolute;
width:30px; }
.bp3-popover .bp3-popover-arrow::before{
height:20px;
margin:5px;
width:20px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-top > .bp3-popover{
margin-bottom:17px;
margin-top:-17px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-top > .bp3-popover > .bp3-popover-arrow{
bottom:-11px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-top > .bp3-popover > .bp3-popover-arrow svg{
-webkit-transform:rotate(-90deg);
transform:rotate(-90deg); }
.bp3-tether-element-attached-left.bp3-tether-target-attached-right > .bp3-popover{
margin-left:17px; }
.bp3-tether-element-attached-left.bp3-tether-target-attached-right > .bp3-popover > .bp3-popover-arrow{
left:-11px; }
.bp3-tether-element-attached-left.bp3-tether-target-attached-right > .bp3-popover > .bp3-popover-arrow svg{
-webkit-transform:rotate(0);
transform:rotate(0); }
.bp3-tether-element-attached-top.bp3-tether-target-attached-bottom > .bp3-popover{
margin-top:17px; }
.bp3-tether-element-attached-top.bp3-tether-target-attached-bottom > .bp3-popover > .bp3-popover-arrow{
top:-11px; }
.bp3-tether-element-attached-top.bp3-tether-target-attached-bottom > .bp3-popover > .bp3-popover-arrow svg{
-webkit-transform:rotate(90deg);
transform:rotate(90deg); }
.bp3-tether-element-attached-right.bp3-tether-target-attached-left > .bp3-popover{
margin-left:-17px;
margin-right:17px; }
.bp3-tether-element-attached-right.bp3-tether-target-attached-left > .bp3-popover > .bp3-popover-arrow{
right:-11px; }
.bp3-tether-element-attached-right.bp3-tether-target-attached-left > .bp3-popover > .bp3-popover-arrow svg{
-webkit-transform:rotate(180deg);
transform:rotate(180deg); }
.bp3-tether-element-attached-middle > .bp3-popover > .bp3-popover-arrow{
top:50%;
-webkit-transform:translateY(-50%);
transform:translateY(-50%); }
.bp3-tether-element-attached-center > .bp3-popover > .bp3-popover-arrow{
right:50%;
-webkit-transform:translateX(50%);
transform:translateX(50%); }
.bp3-tether-element-attached-top.bp3-tether-target-attached-top > .bp3-popover > .bp3-popover-arrow{
top:-0.3934px; }
.bp3-tether-element-attached-right.bp3-tether-target-attached-right > .bp3-popover > .bp3-popover-arrow{
right:-0.3934px; }
.bp3-tether-element-attached-left.bp3-tether-target-attached-left > .bp3-popover > .bp3-popover-arrow{
left:-0.3934px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-bottom > .bp3-popover > .bp3-popover-arrow{
bottom:-0.3934px; }
.bp3-tether-element-attached-top.bp3-tether-element-attached-left > .bp3-popover{
-webkit-transform-origin:top left;
transform-origin:top left; }
.bp3-tether-element-attached-top.bp3-tether-element-attached-center > .bp3-popover{
-webkit-transform-origin:top center;
transform-origin:top center; }
.bp3-tether-element-attached-top.bp3-tether-element-attached-right > .bp3-popover{
-webkit-transform-origin:top right;
transform-origin:top right; }
.bp3-tether-element-attached-middle.bp3-tether-element-attached-left > .bp3-popover{
-webkit-transform-origin:center left;
transform-origin:center left; }
.bp3-tether-element-attached-middle.bp3-tether-element-attached-center > .bp3-popover{
-webkit-transform-origin:center center;
transform-origin:center center; }
.bp3-tether-element-attached-middle.bp3-tether-element-attached-right > .bp3-popover{
-webkit-transform-origin:center right;
transform-origin:center right; }
.bp3-tether-element-attached-bottom.bp3-tether-element-attached-left > .bp3-popover{
-webkit-transform-origin:bottom left;
transform-origin:bottom left; }
.bp3-tether-element-attached-bottom.bp3-tether-element-attached-center > .bp3-popover{
-webkit-transform-origin:bottom center;
transform-origin:bottom center; }
.bp3-tether-element-attached-bottom.bp3-tether-element-attached-right > .bp3-popover{
-webkit-transform-origin:bottom right;
transform-origin:bottom right; }
.bp3-popover .bp3-popover-content{
background:#ffffff;
color:inherit; }
.bp3-popover .bp3-popover-arrow::before{
-webkit-box-shadow:1px 1px 6px rgba(16, 22, 26, 0.2);
box-shadow:1px 1px 6px rgba(16, 22, 26, 0.2); }
.bp3-popover .bp3-popover-arrow-border{
fill:#10161a;
fill-opacity:0.1; }
.bp3-popover .bp3-popover-arrow-fill{
fill:#ffffff; }
.bp3-popover-enter > .bp3-popover, .bp3-popover-appear > .bp3-popover{
-webkit-transform:scale(0.3);
transform:scale(0.3); }
.bp3-popover-enter-active > .bp3-popover, .bp3-popover-appear-active > .bp3-popover{
-webkit-transform:scale(1);
transform:scale(1);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11);
transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11); }
.bp3-popover-exit > .bp3-popover{
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-popover-exit-active > .bp3-popover{
-webkit-transform:scale(0.3);
transform:scale(0.3);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11);
transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11); }
.bp3-popover .bp3-popover-content{
border-radius:3px;
position:relative; }
.bp3-popover.bp3-popover-content-sizing .bp3-popover-content{
max-width:350px;
padding:20px; }
.bp3-popover-target + .bp3-overlay .bp3-popover.bp3-popover-content-sizing{
width:350px; }
.bp3-popover.bp3-minimal{
margin:0 !important; }
.bp3-popover.bp3-minimal .bp3-popover-arrow{
display:none; }
.bp3-popover.bp3-minimal.bp3-popover{
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-popover-enter > .bp3-popover.bp3-minimal.bp3-popover, .bp3-popover-appear > .bp3-popover.bp3-minimal.bp3-popover{
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-popover-enter-active > .bp3-popover.bp3-minimal.bp3-popover, .bp3-popover-appear-active > .bp3-popover.bp3-minimal.bp3-popover{
-webkit-transform:scale(1);
transform:scale(1);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-popover-exit > .bp3-popover.bp3-minimal.bp3-popover{
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-popover-exit-active > .bp3-popover.bp3-minimal.bp3-popover{
-webkit-transform:scale(1);
transform:scale(1);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-popover.bp3-dark,
.bp3-dark .bp3-popover{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4); }
.bp3-popover.bp3-dark .bp3-popover-content,
.bp3-dark .bp3-popover .bp3-popover-content{
background:#30404d;
color:inherit; }
.bp3-popover.bp3-dark .bp3-popover-arrow::before,
.bp3-dark .bp3-popover .bp3-popover-arrow::before{
-webkit-box-shadow:1px 1px 6px rgba(16, 22, 26, 0.4);
box-shadow:1px 1px 6px rgba(16, 22, 26, 0.4); }
.bp3-popover.bp3-dark .bp3-popover-arrow-border,
.bp3-dark .bp3-popover .bp3-popover-arrow-border{
fill:#10161a;
fill-opacity:0.2; }
.bp3-popover.bp3-dark .bp3-popover-arrow-fill,
.bp3-dark .bp3-popover .bp3-popover-arrow-fill{
fill:#30404d; }
.bp3-popover-arrow::before{
border-radius:2px;
content:"";
display:block;
position:absolute;
-webkit-transform:rotate(45deg);
transform:rotate(45deg); }
.bp3-tether-pinned .bp3-popover-arrow{
display:none; }
.bp3-popover-backdrop{
background:rgba(255, 255, 255, 0); }
.bp3-transition-container{
opacity:1;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
z-index:20; }
.bp3-transition-container.bp3-popover-enter, .bp3-transition-container.bp3-popover-appear{
opacity:0; }
.bp3-transition-container.bp3-popover-enter-active, .bp3-transition-container.bp3-popover-appear-active{
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:opacity;
transition-property:opacity;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-transition-container.bp3-popover-exit{
opacity:1; }
.bp3-transition-container.bp3-popover-exit-active{
opacity:0;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:opacity;
transition-property:opacity;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-transition-container:focus{
outline:none; }
.bp3-transition-container.bp3-popover-leave .bp3-popover-content{
pointer-events:none; }
.bp3-transition-container[data-x-out-of-boundaries]{
display:none; }
span.bp3-popover-target{
display:inline-block; }
.bp3-popover-wrapper.bp3-fill{
width:100%; }
.bp3-portal{
left:0;
position:absolute;
right:0;
top:0; }
@-webkit-keyframes linear-progress-bar-stripes{
from{
background-position:0 0; }
to{
background-position:30px 0; } }
@keyframes linear-progress-bar-stripes{
from{
background-position:0 0; }
to{
background-position:30px 0; } }
.bp3-progress-bar{
background:rgba(92, 112, 128, 0.2);
border-radius:40px;
display:block;
height:8px;
overflow:hidden;
position:relative;
width:100%; }
.bp3-progress-bar .bp3-progress-meter{
background:linear-gradient(-45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%);
background-color:rgba(92, 112, 128, 0.8);
background-size:30px 30px;
border-radius:40px;
height:100%;
position:absolute;
-webkit-transition:width 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:width 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
width:100%; }
.bp3-progress-bar:not(.bp3-no-animation):not(.bp3-no-stripes) .bp3-progress-meter{
animation:linear-progress-bar-stripes 300ms linear infinite reverse; }
.bp3-progress-bar.bp3-no-stripes .bp3-progress-meter{
background-image:none; }
.bp3-dark .bp3-progress-bar{
background:rgba(16, 22, 26, 0.5); }
.bp3-dark .bp3-progress-bar .bp3-progress-meter{
background-color:#8a9ba8; }
.bp3-progress-bar.bp3-intent-primary .bp3-progress-meter{
background-color:#137cbd; }
.bp3-progress-bar.bp3-intent-success .bp3-progress-meter{
background-color:#0f9960; }
.bp3-progress-bar.bp3-intent-warning .bp3-progress-meter{
background-color:#d9822b; }
.bp3-progress-bar.bp3-intent-danger .bp3-progress-meter{
background-color:#db3737; }
@-webkit-keyframes skeleton-glow{
from{
background:rgba(206, 217, 224, 0.2);
border-color:rgba(206, 217, 224, 0.2); }
to{
background:rgba(92, 112, 128, 0.2);
border-color:rgba(92, 112, 128, 0.2); } }
@keyframes skeleton-glow{
from{
background:rgba(206, 217, 224, 0.2);
border-color:rgba(206, 217, 224, 0.2); }
to{
background:rgba(92, 112, 128, 0.2);
border-color:rgba(92, 112, 128, 0.2); } }
.bp3-skeleton{
-webkit-animation:1000ms linear infinite alternate skeleton-glow;
animation:1000ms linear infinite alternate skeleton-glow;
background:rgba(206, 217, 224, 0.2);
background-clip:padding-box !important;
border-color:rgba(206, 217, 224, 0.2) !important;
border-radius:2px;
-webkit-box-shadow:none !important;
box-shadow:none !important;
color:transparent !important;
cursor:default;
pointer-events:none;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none; }
.bp3-skeleton::before, .bp3-skeleton::after,
.bp3-skeleton *{
visibility:hidden !important; }
.bp3-slider{
height:40px;
min-width:150px;
width:100%;
cursor:default;
outline:none;
position:relative;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none; }
.bp3-slider:hover{
cursor:pointer; }
.bp3-slider:active{
cursor:-webkit-grabbing;
cursor:grabbing; }
.bp3-slider.bp3-disabled{
cursor:not-allowed;
opacity:0.5; }
.bp3-slider.bp3-slider-unlabeled{
height:16px; }
.bp3-slider-track,
.bp3-slider-progress{
height:6px;
left:0;
right:0;
top:5px;
position:absolute; }
.bp3-slider-track{
border-radius:3px;
overflow:hidden; }
.bp3-slider-progress{
background:rgba(92, 112, 128, 0.2); }
.bp3-dark .bp3-slider-progress{
background:rgba(16, 22, 26, 0.5); }
.bp3-slider-progress.bp3-intent-primary{
background-color:#137cbd; }
.bp3-slider-progress.bp3-intent-success{
background-color:#0f9960; }
.bp3-slider-progress.bp3-intent-warning{
background-color:#d9822b; }
.bp3-slider-progress.bp3-intent-danger{
background-color:#db3737; }
.bp3-slider-handle{
background-color:#f5f8fa;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.8)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0));
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
color:#182026;
border-radius:3px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.2);
cursor:pointer;
height:16px;
left:0;
position:absolute;
top:0;
width:16px; }
.bp3-slider-handle:hover{
background-clip:padding-box;
background-color:#ebf1f5;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); }
.bp3-slider-handle:active, .bp3-slider-handle.bp3-active{
background-color:#d8e1e8;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-slider-handle:disabled, .bp3-slider-handle.bp3-disabled{
background-color:rgba(206, 217, 224, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed;
outline:none; }
.bp3-slider-handle:disabled.bp3-active, .bp3-slider-handle:disabled.bp3-active:hover, .bp3-slider-handle.bp3-disabled.bp3-active, .bp3-slider-handle.bp3-disabled.bp3-active:hover{
background:rgba(206, 217, 224, 0.7); }
.bp3-slider-handle:focus{
z-index:1; }
.bp3-slider-handle:hover{
background-clip:padding-box;
background-color:#ebf1f5;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1);
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 1px 1px rgba(16, 22, 26, 0.2);
cursor:-webkit-grab;
cursor:grab;
z-index:2; }
.bp3-slider-handle.bp3-active{
background-color:#d8e1e8;
background-image:none;
-webkit-box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 2px rgba(16, 22, 26, 0.2);
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 1px rgba(16, 22, 26, 0.1);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 1px 1px rgba(16, 22, 26, 0.1);
cursor:-webkit-grabbing;
cursor:grabbing; }
.bp3-disabled .bp3-slider-handle{
background:#bfccd6;
-webkit-box-shadow:none;
box-shadow:none;
pointer-events:none; }
.bp3-dark .bp3-slider-handle{
background-color:#394b59;
background-image:-webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.05)), to(rgba(255, 255, 255, 0)));
background-image:linear-gradient(to bottom, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
color:#f5f8fa; }
.bp3-dark .bp3-slider-handle:hover, .bp3-dark .bp3-slider-handle:active, .bp3-dark .bp3-slider-handle.bp3-active{
color:#f5f8fa; }
.bp3-dark .bp3-slider-handle:hover{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-slider-handle:active, .bp3-dark .bp3-slider-handle.bp3-active{
background-color:#202b33;
background-image:none;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.6), inset 0 1px 2px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-slider-handle:disabled, .bp3-dark .bp3-slider-handle.bp3-disabled{
background-color:rgba(57, 75, 89, 0.5);
background-image:none;
-webkit-box-shadow:none;
box-shadow:none;
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-slider-handle:disabled.bp3-active, .bp3-dark .bp3-slider-handle.bp3-disabled.bp3-active{
background:rgba(57, 75, 89, 0.7); }
.bp3-dark .bp3-slider-handle .bp3-button-spinner .bp3-spinner-head{
background:rgba(16, 22, 26, 0.5);
stroke:#8a9ba8; }
.bp3-dark .bp3-slider-handle, .bp3-dark .bp3-slider-handle:hover{
background-color:#394b59; }
.bp3-dark .bp3-slider-handle.bp3-active{
background-color:#293742; }
.bp3-dark .bp3-disabled .bp3-slider-handle{
background:#5c7080;
border-color:#5c7080;
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-slider-handle .bp3-slider-label{
background:#394b59;
border-radius:3px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
color:#f5f8fa;
margin-left:8px; }
.bp3-dark .bp3-slider-handle .bp3-slider-label{
background:#e1e8ed;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
color:#394b59; }
.bp3-disabled .bp3-slider-handle .bp3-slider-label{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-slider-handle.bp3-start, .bp3-slider-handle.bp3-end{
width:8px; }
.bp3-slider-handle.bp3-start{
border-bottom-right-radius:0;
border-top-right-radius:0; }
.bp3-slider-handle.bp3-end{
border-bottom-left-radius:0;
border-top-left-radius:0;
margin-left:8px; }
.bp3-slider-handle.bp3-end .bp3-slider-label{
margin-left:0; }
.bp3-slider-label{
-webkit-transform:translate(-50%, 20px);
transform:translate(-50%, 20px);
display:inline-block;
font-size:12px;
line-height:1;
padding:2px 5px;
position:absolute;
vertical-align:top; }
.bp3-slider.bp3-vertical{
height:150px;
min-width:40px;
width:40px; }
.bp3-slider.bp3-vertical .bp3-slider-track,
.bp3-slider.bp3-vertical .bp3-slider-progress{
bottom:0;
height:auto;
left:5px;
top:0;
width:6px; }
.bp3-slider.bp3-vertical .bp3-slider-progress{
top:auto; }
.bp3-slider.bp3-vertical .bp3-slider-label{
-webkit-transform:translate(20px, 50%);
transform:translate(20px, 50%); }
.bp3-slider.bp3-vertical .bp3-slider-handle{
top:auto; }
.bp3-slider.bp3-vertical .bp3-slider-handle .bp3-slider-label{
margin-left:0;
margin-top:-8px; }
.bp3-slider.bp3-vertical .bp3-slider-handle.bp3-end, .bp3-slider.bp3-vertical .bp3-slider-handle.bp3-start{
height:8px;
margin-left:0;
width:16px; }
.bp3-slider.bp3-vertical .bp3-slider-handle.bp3-start{
border-bottom-right-radius:3px;
border-top-left-radius:0; }
.bp3-slider.bp3-vertical .bp3-slider-handle.bp3-start .bp3-slider-label{
-webkit-transform:translate(20px);
transform:translate(20px); }
.bp3-slider.bp3-vertical .bp3-slider-handle.bp3-end{
border-bottom-left-radius:0;
border-bottom-right-radius:0;
border-top-left-radius:3px;
margin-bottom:8px; }
@-webkit-keyframes pt-spinner-animation{
from{
-webkit-transform:rotate(0deg);
transform:rotate(0deg); }
to{
-webkit-transform:rotate(360deg);
transform:rotate(360deg); } }
@keyframes pt-spinner-animation{
from{
-webkit-transform:rotate(0deg);
transform:rotate(0deg); }
to{
-webkit-transform:rotate(360deg);
transform:rotate(360deg); } }
.bp3-spinner{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-pack:center;
-ms-flex-pack:center;
justify-content:center;
overflow:visible;
vertical-align:middle; }
.bp3-spinner svg{
display:block; }
.bp3-spinner path{
fill-opacity:0; }
.bp3-spinner .bp3-spinner-head{
stroke:rgba(92, 112, 128, 0.8);
stroke-linecap:round;
-webkit-transform-origin:center;
transform-origin:center;
-webkit-transition:stroke-dashoffset 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:stroke-dashoffset 200ms cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-spinner .bp3-spinner-track{
stroke:rgba(92, 112, 128, 0.2); }
.bp3-spinner-animation{
-webkit-animation:pt-spinner-animation 500ms linear infinite;
animation:pt-spinner-animation 500ms linear infinite; }
.bp3-no-spin > .bp3-spinner-animation{
-webkit-animation:none;
animation:none; }
.bp3-dark .bp3-spinner .bp3-spinner-head{
stroke:#8a9ba8; }
.bp3-dark .bp3-spinner .bp3-spinner-track{
stroke:rgba(16, 22, 26, 0.5); }
.bp3-spinner.bp3-intent-primary .bp3-spinner-head{
stroke:#137cbd; }
.bp3-spinner.bp3-intent-success .bp3-spinner-head{
stroke:#0f9960; }
.bp3-spinner.bp3-intent-warning .bp3-spinner-head{
stroke:#d9822b; }
.bp3-spinner.bp3-intent-danger .bp3-spinner-head{
stroke:#db3737; }
.bp3-tabs.bp3-vertical{
display:-webkit-box;
display:-ms-flexbox;
display:flex; }
.bp3-tabs.bp3-vertical > .bp3-tab-list{
-webkit-box-align:start;
-ms-flex-align:start;
align-items:flex-start;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column; }
.bp3-tabs.bp3-vertical > .bp3-tab-list .bp3-tab{
border-radius:3px;
padding:0 10px;
width:100%; }
.bp3-tabs.bp3-vertical > .bp3-tab-list .bp3-tab[aria-selected="true"]{
background-color:rgba(19, 124, 189, 0.2);
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-tabs.bp3-vertical > .bp3-tab-list .bp3-tab-indicator-wrapper .bp3-tab-indicator{
background-color:rgba(19, 124, 189, 0.2);
border-radius:3px;
bottom:0;
height:auto;
left:0;
right:0;
top:0; }
.bp3-tabs.bp3-vertical > .bp3-tab-panel{
margin-top:0;
padding-left:20px; }
.bp3-tab-list{
-webkit-box-align:end;
-ms-flex-align:end;
align-items:flex-end;
border:none;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
list-style:none;
margin:0;
padding:0;
position:relative; }
.bp3-tab-list > *:not(:last-child){
margin-right:20px; }
.bp3-tab{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
color:#182026;
cursor:pointer;
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
font-size:14px;
line-height:30px;
max-width:100%;
position:relative;
vertical-align:top; }
.bp3-tab a{
color:inherit;
display:block;
text-decoration:none; }
.bp3-tab-indicator-wrapper ~ .bp3-tab{
background-color:transparent !important;
-webkit-box-shadow:none !important;
box-shadow:none !important; }
.bp3-tab[aria-disabled="true"]{
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-tab[aria-selected="true"]{
border-radius:0;
-webkit-box-shadow:inset 0 -3px 0 #106ba3;
box-shadow:inset 0 -3px 0 #106ba3; }
.bp3-tab[aria-selected="true"], .bp3-tab:not([aria-disabled="true"]):hover{
color:#106ba3; }
.bp3-tab:focus{
-moz-outline-radius:0; }
.bp3-large > .bp3-tab{
font-size:16px;
line-height:40px; }
.bp3-tab-panel{
margin-top:20px; }
.bp3-tab-panel[aria-hidden="true"]{
display:none; }
.bp3-tab-indicator-wrapper{
left:0;
pointer-events:none;
position:absolute;
top:0;
-webkit-transform:translateX(0), translateY(0);
transform:translateX(0), translateY(0);
-webkit-transition:height, width, -webkit-transform;
transition:height, width, -webkit-transform;
transition:height, transform, width;
transition:height, transform, width, -webkit-transform;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-tab-indicator-wrapper .bp3-tab-indicator{
background-color:#106ba3;
bottom:0;
height:3px;
left:0;
position:absolute;
right:0; }
.bp3-tab-indicator-wrapper.bp3-no-animation{
-webkit-transition:none;
transition:none; }
.bp3-dark .bp3-tab{
color:#f5f8fa; }
.bp3-dark .bp3-tab[aria-disabled="true"]{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-tab[aria-selected="true"]{
-webkit-box-shadow:inset 0 -3px 0 #48aff0;
box-shadow:inset 0 -3px 0 #48aff0; }
.bp3-dark .bp3-tab[aria-selected="true"], .bp3-dark .bp3-tab:not([aria-disabled="true"]):hover{
color:#48aff0; }
.bp3-dark .bp3-tab-indicator{
background-color:#48aff0; }
.bp3-flex-expander{
-webkit-box-flex:1;
-ms-flex:1 1;
flex:1 1; }
.bp3-tag{
display:-webkit-inline-box;
display:-ms-inline-flexbox;
display:inline-flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
background-color:#5c7080;
border:none;
border-radius:3px;
-webkit-box-shadow:none;
box-shadow:none;
color:#f5f8fa;
font-size:12px;
line-height:16px;
max-width:100%;
min-height:20px;
min-width:20px;
padding:2px 6px;
position:relative; }
.bp3-tag.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-interactive:hover{
background-color:rgba(92, 112, 128, 0.85); }
.bp3-tag.bp3-interactive.bp3-active, .bp3-tag.bp3-interactive:active{
background-color:rgba(92, 112, 128, 0.7); }
.bp3-tag > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-tag > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-tag::before,
.bp3-tag > *{
margin-right:4px; }
.bp3-tag:empty::before,
.bp3-tag > :last-child{
margin-right:0; }
.bp3-tag:focus{
outline:rgba(19, 124, 189, 0.6) auto 2px;
outline-offset:0;
-moz-outline-radius:6px; }
.bp3-tag.bp3-round{
border-radius:30px;
padding-left:8px;
padding-right:8px; }
.bp3-dark .bp3-tag{
background-color:#bfccd6;
color:#182026; }
.bp3-dark .bp3-tag.bp3-interactive{
cursor:pointer; }
.bp3-dark .bp3-tag.bp3-interactive:hover{
background-color:rgba(191, 204, 214, 0.85); }
.bp3-dark .bp3-tag.bp3-interactive.bp3-active, .bp3-dark .bp3-tag.bp3-interactive:active{
background-color:rgba(191, 204, 214, 0.7); }
.bp3-dark .bp3-tag > .bp3-icon, .bp3-dark .bp3-tag .bp3-icon-standard, .bp3-dark .bp3-tag .bp3-icon-large{
fill:currentColor; }
.bp3-tag > .bp3-icon, .bp3-tag .bp3-icon-standard, .bp3-tag .bp3-icon-large{
fill:#ffffff; }
.bp3-tag.bp3-large,
.bp3-large .bp3-tag{
font-size:14px;
line-height:20px;
min-height:30px;
min-width:30px;
padding:5px 10px; }
.bp3-tag.bp3-large::before,
.bp3-tag.bp3-large > *,
.bp3-large .bp3-tag::before,
.bp3-large .bp3-tag > *{
margin-right:7px; }
.bp3-tag.bp3-large:empty::before,
.bp3-tag.bp3-large > :last-child,
.bp3-large .bp3-tag:empty::before,
.bp3-large .bp3-tag > :last-child{
margin-right:0; }
.bp3-tag.bp3-large.bp3-round,
.bp3-large .bp3-tag.bp3-round{
padding-left:12px;
padding-right:12px; }
.bp3-tag.bp3-intent-primary{
background:#137cbd;
color:#ffffff; }
.bp3-tag.bp3-intent-primary.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-intent-primary.bp3-interactive:hover{
background-color:rgba(19, 124, 189, 0.85); }
.bp3-tag.bp3-intent-primary.bp3-interactive.bp3-active, .bp3-tag.bp3-intent-primary.bp3-interactive:active{
background-color:rgba(19, 124, 189, 0.7); }
.bp3-tag.bp3-intent-success{
background:#0f9960;
color:#ffffff; }
.bp3-tag.bp3-intent-success.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-intent-success.bp3-interactive:hover{
background-color:rgba(15, 153, 96, 0.85); }
.bp3-tag.bp3-intent-success.bp3-interactive.bp3-active, .bp3-tag.bp3-intent-success.bp3-interactive:active{
background-color:rgba(15, 153, 96, 0.7); }
.bp3-tag.bp3-intent-warning{
background:#d9822b;
color:#ffffff; }
.bp3-tag.bp3-intent-warning.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-intent-warning.bp3-interactive:hover{
background-color:rgba(217, 130, 43, 0.85); }
.bp3-tag.bp3-intent-warning.bp3-interactive.bp3-active, .bp3-tag.bp3-intent-warning.bp3-interactive:active{
background-color:rgba(217, 130, 43, 0.7); }
.bp3-tag.bp3-intent-danger{
background:#db3737;
color:#ffffff; }
.bp3-tag.bp3-intent-danger.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-intent-danger.bp3-interactive:hover{
background-color:rgba(219, 55, 55, 0.85); }
.bp3-tag.bp3-intent-danger.bp3-interactive.bp3-active, .bp3-tag.bp3-intent-danger.bp3-interactive:active{
background-color:rgba(219, 55, 55, 0.7); }
.bp3-tag.bp3-fill{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
width:100%; }
.bp3-tag.bp3-minimal > .bp3-icon, .bp3-tag.bp3-minimal .bp3-icon-standard, .bp3-tag.bp3-minimal .bp3-icon-large{
fill:#5c7080; }
.bp3-tag.bp3-minimal:not([class*="bp3-intent-"]){
background-color:rgba(138, 155, 168, 0.2);
color:#182026; }
.bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive:hover{
background-color:rgba(92, 112, 128, 0.3); }
.bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive.bp3-active, .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive:active{
background-color:rgba(92, 112, 128, 0.4); }
.bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]){
color:#f5f8fa; }
.bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive{
cursor:pointer; }
.bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive:hover{
background-color:rgba(191, 204, 214, 0.3); }
.bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive.bp3-active, .bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]).bp3-interactive:active{
background-color:rgba(191, 204, 214, 0.4); }
.bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]) > .bp3-icon, .bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]) .bp3-icon-standard, .bp3-dark .bp3-tag.bp3-minimal:not([class*="bp3-intent-"]) .bp3-icon-large{
fill:#a7b6c2; }
.bp3-tag.bp3-minimal.bp3-intent-primary{
background-color:rgba(19, 124, 189, 0.15);
color:#106ba3; }
.bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive:hover{
background-color:rgba(19, 124, 189, 0.25); }
.bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive.bp3-active, .bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive:active{
background-color:rgba(19, 124, 189, 0.35); }
.bp3-tag.bp3-minimal.bp3-intent-primary > .bp3-icon, .bp3-tag.bp3-minimal.bp3-intent-primary .bp3-icon-standard, .bp3-tag.bp3-minimal.bp3-intent-primary .bp3-icon-large{
fill:#137cbd; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-primary{
background-color:rgba(19, 124, 189, 0.25);
color:#48aff0; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive{
cursor:pointer; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive:hover{
background-color:rgba(19, 124, 189, 0.35); }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive.bp3-active, .bp3-dark .bp3-tag.bp3-minimal.bp3-intent-primary.bp3-interactive:active{
background-color:rgba(19, 124, 189, 0.45); }
.bp3-tag.bp3-minimal.bp3-intent-success{
background-color:rgba(15, 153, 96, 0.15);
color:#0d8050; }
.bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive:hover{
background-color:rgba(15, 153, 96, 0.25); }
.bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive.bp3-active, .bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive:active{
background-color:rgba(15, 153, 96, 0.35); }
.bp3-tag.bp3-minimal.bp3-intent-success > .bp3-icon, .bp3-tag.bp3-minimal.bp3-intent-success .bp3-icon-standard, .bp3-tag.bp3-minimal.bp3-intent-success .bp3-icon-large{
fill:#0f9960; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-success{
background-color:rgba(15, 153, 96, 0.25);
color:#3dcc91; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive{
cursor:pointer; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive:hover{
background-color:rgba(15, 153, 96, 0.35); }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive.bp3-active, .bp3-dark .bp3-tag.bp3-minimal.bp3-intent-success.bp3-interactive:active{
background-color:rgba(15, 153, 96, 0.45); }
.bp3-tag.bp3-minimal.bp3-intent-warning{
background-color:rgba(217, 130, 43, 0.15);
color:#bf7326; }
.bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive:hover{
background-color:rgba(217, 130, 43, 0.25); }
.bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive.bp3-active, .bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive:active{
background-color:rgba(217, 130, 43, 0.35); }
.bp3-tag.bp3-minimal.bp3-intent-warning > .bp3-icon, .bp3-tag.bp3-minimal.bp3-intent-warning .bp3-icon-standard, .bp3-tag.bp3-minimal.bp3-intent-warning .bp3-icon-large{
fill:#d9822b; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-warning{
background-color:rgba(217, 130, 43, 0.25);
color:#ffb366; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive{
cursor:pointer; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive:hover{
background-color:rgba(217, 130, 43, 0.35); }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive.bp3-active, .bp3-dark .bp3-tag.bp3-minimal.bp3-intent-warning.bp3-interactive:active{
background-color:rgba(217, 130, 43, 0.45); }
.bp3-tag.bp3-minimal.bp3-intent-danger{
background-color:rgba(219, 55, 55, 0.15);
color:#c23030; }
.bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive{
cursor:pointer; }
.bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive:hover{
background-color:rgba(219, 55, 55, 0.25); }
.bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive.bp3-active, .bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive:active{
background-color:rgba(219, 55, 55, 0.35); }
.bp3-tag.bp3-minimal.bp3-intent-danger > .bp3-icon, .bp3-tag.bp3-minimal.bp3-intent-danger .bp3-icon-standard, .bp3-tag.bp3-minimal.bp3-intent-danger .bp3-icon-large{
fill:#db3737; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-danger{
background-color:rgba(219, 55, 55, 0.25);
color:#ff7373; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive{
cursor:pointer; }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive:hover{
background-color:rgba(219, 55, 55, 0.35); }
.bp3-dark .bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive.bp3-active, .bp3-dark .bp3-tag.bp3-minimal.bp3-intent-danger.bp3-interactive:active{
background-color:rgba(219, 55, 55, 0.45); }
.bp3-tag-remove{
background:none;
border:none;
color:inherit;
cursor:pointer;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
margin-bottom:-2px;
margin-right:-6px !important;
margin-top:-2px;
opacity:0.5;
padding:2px;
padding-left:0; }
.bp3-tag-remove:hover{
background:none;
opacity:0.8;
text-decoration:none; }
.bp3-tag-remove:active{
opacity:1; }
.bp3-tag-remove:empty::before{
font-family:"Icons16", sans-serif;
font-size:16px;
font-style:normal;
font-weight:400;
line-height:1;
-moz-osx-font-smoothing:grayscale;
-webkit-font-smoothing:antialiased;
content:""; }
.bp3-large .bp3-tag-remove{
margin-right:-10px !important;
padding:0 5px 0 0; }
.bp3-large .bp3-tag-remove:empty::before{
font-family:"Icons20", sans-serif;
font-size:20px;
font-style:normal;
font-weight:400;
line-height:1; }
.bp3-tag-input{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:start;
-ms-flex-align:start;
align-items:flex-start;
cursor:text;
height:auto;
line-height:inherit;
min-height:30px;
padding-left:5px;
padding-right:0; }
.bp3-tag-input > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-tag-input > .bp3-tag-input-values{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-tag-input .bp3-tag-input-icon{
color:#5c7080;
margin-left:2px;
margin-right:7px;
margin-top:7px; }
.bp3-tag-input .bp3-tag-input-values{
display:-webkit-box;
display:-ms-flexbox;
display:flex;
-webkit-box-orient:horizontal;
-webkit-box-direction:normal;
-ms-flex-direction:row;
flex-direction:row;
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
-ms-flex-item-align:stretch;
align-self:stretch;
-ms-flex-wrap:wrap;
flex-wrap:wrap;
margin-right:7px;
margin-top:5px;
min-width:0; }
.bp3-tag-input .bp3-tag-input-values > *{
-webkit-box-flex:0;
-ms-flex-positive:0;
flex-grow:0;
-ms-flex-negative:0;
flex-shrink:0; }
.bp3-tag-input .bp3-tag-input-values > .bp3-fill{
-webkit-box-flex:1;
-ms-flex-positive:1;
flex-grow:1;
-ms-flex-negative:1;
flex-shrink:1; }
.bp3-tag-input .bp3-tag-input-values::before,
.bp3-tag-input .bp3-tag-input-values > *{
margin-right:5px; }
.bp3-tag-input .bp3-tag-input-values:empty::before,
.bp3-tag-input .bp3-tag-input-values > :last-child{
margin-right:0; }
.bp3-tag-input .bp3-tag-input-values:first-child .bp3-input-ghost:first-child{
padding-left:5px; }
.bp3-tag-input .bp3-tag-input-values > *{
margin-bottom:5px; }
.bp3-tag-input .bp3-tag{
overflow-wrap:break-word; }
.bp3-tag-input .bp3-tag.bp3-active{
outline:rgba(19, 124, 189, 0.6) auto 2px;
outline-offset:0;
-moz-outline-radius:6px; }
.bp3-tag-input .bp3-input-ghost{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
line-height:20px;
width:80px; }
.bp3-tag-input .bp3-input-ghost:disabled, .bp3-tag-input .bp3-input-ghost.bp3-disabled{
cursor:not-allowed; }
.bp3-tag-input .bp3-button,
.bp3-tag-input .bp3-spinner{
margin:3px;
margin-left:0; }
.bp3-tag-input .bp3-button{
min-height:24px;
min-width:24px;
padding:0 7px; }
.bp3-tag-input.bp3-large{
height:auto;
min-height:40px; }
.bp3-tag-input.bp3-large::before,
.bp3-tag-input.bp3-large > *{
margin-right:10px; }
.bp3-tag-input.bp3-large:empty::before,
.bp3-tag-input.bp3-large > :last-child{
margin-right:0; }
.bp3-tag-input.bp3-large .bp3-tag-input-icon{
margin-left:5px;
margin-top:10px; }
.bp3-tag-input.bp3-large .bp3-input-ghost{
line-height:30px; }
.bp3-tag-input.bp3-large .bp3-button{
min-height:30px;
min-width:30px;
padding:5px 10px;
margin:5px;
margin-left:0; }
.bp3-tag-input.bp3-large .bp3-spinner{
margin:8px;
margin-left:0; }
.bp3-tag-input.bp3-active{
background-color:#ffffff;
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-tag-input.bp3-active.bp3-intent-primary{
-webkit-box-shadow:0 0 0 1px #106ba3, 0 0 0 3px rgba(16, 107, 163, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #106ba3, 0 0 0 3px rgba(16, 107, 163, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-tag-input.bp3-active.bp3-intent-success{
-webkit-box-shadow:0 0 0 1px #0d8050, 0 0 0 3px rgba(13, 128, 80, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #0d8050, 0 0 0 3px rgba(13, 128, 80, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-tag-input.bp3-active.bp3-intent-warning{
-webkit-box-shadow:0 0 0 1px #bf7326, 0 0 0 3px rgba(191, 115, 38, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #bf7326, 0 0 0 3px rgba(191, 115, 38, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-tag-input.bp3-active.bp3-intent-danger{
-webkit-box-shadow:0 0 0 1px #c23030, 0 0 0 3px rgba(194, 48, 48, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px #c23030, 0 0 0 3px rgba(194, 48, 48, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.2); }
.bp3-dark .bp3-tag-input .bp3-tag-input-icon, .bp3-tag-input.bp3-dark .bp3-tag-input-icon{
color:#a7b6c2; }
.bp3-dark .bp3-tag-input .bp3-input-ghost, .bp3-tag-input.bp3-dark .bp3-input-ghost{
color:#f5f8fa; }
.bp3-dark .bp3-tag-input .bp3-input-ghost::-webkit-input-placeholder, .bp3-tag-input.bp3-dark .bp3-input-ghost::-webkit-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-tag-input .bp3-input-ghost::-moz-placeholder, .bp3-tag-input.bp3-dark .bp3-input-ghost::-moz-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-tag-input .bp3-input-ghost:-ms-input-placeholder, .bp3-tag-input.bp3-dark .bp3-input-ghost:-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-tag-input .bp3-input-ghost::-ms-input-placeholder, .bp3-tag-input.bp3-dark .bp3-input-ghost::-ms-input-placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-tag-input .bp3-input-ghost::placeholder, .bp3-tag-input.bp3-dark .bp3-input-ghost::placeholder{
color:rgba(167, 182, 194, 0.6); }
.bp3-dark .bp3-tag-input.bp3-active, .bp3-tag-input.bp3-dark.bp3-active{
background-color:rgba(16, 22, 26, 0.3);
-webkit-box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #137cbd, 0 0 0 1px #137cbd, 0 0 0 3px rgba(19, 124, 189, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-tag-input.bp3-active.bp3-intent-primary, .bp3-tag-input.bp3-dark.bp3-active.bp3-intent-primary{
-webkit-box-shadow:0 0 0 1px #106ba3, 0 0 0 3px rgba(16, 107, 163, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #106ba3, 0 0 0 3px rgba(16, 107, 163, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-tag-input.bp3-active.bp3-intent-success, .bp3-tag-input.bp3-dark.bp3-active.bp3-intent-success{
-webkit-box-shadow:0 0 0 1px #0d8050, 0 0 0 3px rgba(13, 128, 80, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #0d8050, 0 0 0 3px rgba(13, 128, 80, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-tag-input.bp3-active.bp3-intent-warning, .bp3-tag-input.bp3-dark.bp3-active.bp3-intent-warning{
-webkit-box-shadow:0 0 0 1px #bf7326, 0 0 0 3px rgba(191, 115, 38, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #bf7326, 0 0 0 3px rgba(191, 115, 38, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-dark .bp3-tag-input.bp3-active.bp3-intent-danger, .bp3-tag-input.bp3-dark.bp3-active.bp3-intent-danger{
-webkit-box-shadow:0 0 0 1px #c23030, 0 0 0 3px rgba(194, 48, 48, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px #c23030, 0 0 0 3px rgba(194, 48, 48, 0.3), inset 0 0 0 1px rgba(16, 22, 26, 0.3), inset 0 1px 1px rgba(16, 22, 26, 0.4); }
.bp3-input-ghost{
background:none;
border:none;
-webkit-box-shadow:none;
box-shadow:none;
padding:0; }
.bp3-input-ghost::-webkit-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input-ghost::-moz-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input-ghost:-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input-ghost::-ms-input-placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input-ghost::placeholder{
color:rgba(92, 112, 128, 0.6);
opacity:1; }
.bp3-input-ghost:focus{
outline:none !important; }
.bp3-toast{
-webkit-box-align:start;
-ms-flex-align:start;
align-items:flex-start;
background-color:#ffffff;
border-radius:3px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
display:-webkit-box;
display:-ms-flexbox;
display:flex;
margin:20px 0 0;
max-width:500px;
min-width:300px;
pointer-events:all;
position:relative !important; }
.bp3-toast.bp3-toast-enter, .bp3-toast.bp3-toast-appear{
-webkit-transform:translateY(-40px);
transform:translateY(-40px); }
.bp3-toast.bp3-toast-enter-active, .bp3-toast.bp3-toast-appear-active{
-webkit-transform:translateY(0);
transform:translateY(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11);
transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11); }
.bp3-toast.bp3-toast-enter ~ .bp3-toast, .bp3-toast.bp3-toast-appear ~ .bp3-toast{
-webkit-transform:translateY(-40px);
transform:translateY(-40px); }
.bp3-toast.bp3-toast-enter-active ~ .bp3-toast, .bp3-toast.bp3-toast-appear-active ~ .bp3-toast{
-webkit-transform:translateY(0);
transform:translateY(0);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11);
transition-timing-function:cubic-bezier(0.54, 1.12, 0.38, 1.11); }
.bp3-toast.bp3-toast-exit{
opacity:1;
-webkit-filter:blur(0);
filter:blur(0); }
.bp3-toast.bp3-toast-exit-active{
opacity:0;
-webkit-filter:blur(10px);
filter:blur(10px);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:300ms;
transition-duration:300ms;
-webkit-transition-property:opacity, -webkit-filter;
transition-property:opacity, -webkit-filter;
transition-property:opacity, filter;
transition-property:opacity, filter, -webkit-filter;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-toast.bp3-toast-exit ~ .bp3-toast{
-webkit-transform:translateY(0);
transform:translateY(0); }
.bp3-toast.bp3-toast-exit-active ~ .bp3-toast{
-webkit-transform:translateY(-40px);
transform:translateY(-40px);
-webkit-transition-delay:50ms;
transition-delay:50ms;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-toast .bp3-button-group{
-webkit-box-flex:0;
-ms-flex:0 0 auto;
flex:0 0 auto;
padding:5px;
padding-left:0; }
.bp3-toast > .bp3-icon{
color:#5c7080;
margin:12px;
margin-right:0; }
.bp3-toast.bp3-dark,
.bp3-dark .bp3-toast{
background-color:#394b59;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4); }
.bp3-toast.bp3-dark > .bp3-icon,
.bp3-dark .bp3-toast > .bp3-icon{
color:#a7b6c2; }
.bp3-toast[class*="bp3-intent-"] a{
color:rgba(255, 255, 255, 0.7); }
.bp3-toast[class*="bp3-intent-"] a:hover{
color:#ffffff; }
.bp3-toast[class*="bp3-intent-"] > .bp3-icon{
color:#ffffff; }
.bp3-toast[class*="bp3-intent-"] .bp3-button, .bp3-toast[class*="bp3-intent-"] .bp3-button::before,
.bp3-toast[class*="bp3-intent-"] .bp3-button .bp3-icon, .bp3-toast[class*="bp3-intent-"] .bp3-button:active{
color:rgba(255, 255, 255, 0.7) !important; }
.bp3-toast[class*="bp3-intent-"] .bp3-button:focus{
outline-color:rgba(255, 255, 255, 0.5); }
.bp3-toast[class*="bp3-intent-"] .bp3-button:hover{
background-color:rgba(255, 255, 255, 0.15) !important;
color:#ffffff !important; }
.bp3-toast[class*="bp3-intent-"] .bp3-button:active{
background-color:rgba(255, 255, 255, 0.3) !important;
color:#ffffff !important; }
.bp3-toast[class*="bp3-intent-"] .bp3-button::after{
background:rgba(255, 255, 255, 0.3) !important; }
.bp3-toast.bp3-intent-primary{
background-color:#137cbd;
color:#ffffff; }
.bp3-toast.bp3-intent-success{
background-color:#0f9960;
color:#ffffff; }
.bp3-toast.bp3-intent-warning{
background-color:#d9822b;
color:#ffffff; }
.bp3-toast.bp3-intent-danger{
background-color:#db3737;
color:#ffffff; }
.bp3-toast-message{
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
padding:11px;
word-break:break-word; }
.bp3-toast-container{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box !important;
display:-ms-flexbox !important;
display:flex !important;
-webkit-box-orient:vertical;
-webkit-box-direction:normal;
-ms-flex-direction:column;
flex-direction:column;
left:0;
overflow:hidden;
padding:0 20px 20px;
pointer-events:none;
right:0;
z-index:40; }
.bp3-toast-container.bp3-toast-container-in-portal{
position:fixed; }
.bp3-toast-container.bp3-toast-container-inline{
position:absolute; }
.bp3-toast-container.bp3-toast-container-top{
top:0; }
.bp3-toast-container.bp3-toast-container-bottom{
bottom:0;
-webkit-box-orient:vertical;
-webkit-box-direction:reverse;
-ms-flex-direction:column-reverse;
flex-direction:column-reverse;
top:auto; }
.bp3-toast-container.bp3-toast-container-left{
-webkit-box-align:start;
-ms-flex-align:start;
align-items:flex-start; }
.bp3-toast-container.bp3-toast-container-right{
-webkit-box-align:end;
-ms-flex-align:end;
align-items:flex-end; }
.bp3-toast-container-bottom .bp3-toast.bp3-toast-enter:not(.bp3-toast-enter-active),
.bp3-toast-container-bottom .bp3-toast.bp3-toast-enter:not(.bp3-toast-enter-active) ~ .bp3-toast, .bp3-toast-container-bottom .bp3-toast.bp3-toast-appear:not(.bp3-toast-appear-active),
.bp3-toast-container-bottom .bp3-toast.bp3-toast-appear:not(.bp3-toast-appear-active) ~ .bp3-toast,
.bp3-toast-container-bottom .bp3-toast.bp3-toast-exit-active ~ .bp3-toast,
.bp3-toast-container-bottom .bp3-toast.bp3-toast-leave-active ~ .bp3-toast{
-webkit-transform:translateY(60px);
transform:translateY(60px); }
.bp3-tooltip{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 2px 4px rgba(16, 22, 26, 0.2), 0 8px 24px rgba(16, 22, 26, 0.2);
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-tooltip .bp3-popover-arrow{
height:22px;
position:absolute;
width:22px; }
.bp3-tooltip .bp3-popover-arrow::before{
height:14px;
margin:4px;
width:14px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-top > .bp3-tooltip{
margin-bottom:11px;
margin-top:-11px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-top > .bp3-tooltip > .bp3-popover-arrow{
bottom:-8px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-top > .bp3-tooltip > .bp3-popover-arrow svg{
-webkit-transform:rotate(-90deg);
transform:rotate(-90deg); }
.bp3-tether-element-attached-left.bp3-tether-target-attached-right > .bp3-tooltip{
margin-left:11px; }
.bp3-tether-element-attached-left.bp3-tether-target-attached-right > .bp3-tooltip > .bp3-popover-arrow{
left:-8px; }
.bp3-tether-element-attached-left.bp3-tether-target-attached-right > .bp3-tooltip > .bp3-popover-arrow svg{
-webkit-transform:rotate(0);
transform:rotate(0); }
.bp3-tether-element-attached-top.bp3-tether-target-attached-bottom > .bp3-tooltip{
margin-top:11px; }
.bp3-tether-element-attached-top.bp3-tether-target-attached-bottom > .bp3-tooltip > .bp3-popover-arrow{
top:-8px; }
.bp3-tether-element-attached-top.bp3-tether-target-attached-bottom > .bp3-tooltip > .bp3-popover-arrow svg{
-webkit-transform:rotate(90deg);
transform:rotate(90deg); }
.bp3-tether-element-attached-right.bp3-tether-target-attached-left > .bp3-tooltip{
margin-left:-11px;
margin-right:11px; }
.bp3-tether-element-attached-right.bp3-tether-target-attached-left > .bp3-tooltip > .bp3-popover-arrow{
right:-8px; }
.bp3-tether-element-attached-right.bp3-tether-target-attached-left > .bp3-tooltip > .bp3-popover-arrow svg{
-webkit-transform:rotate(180deg);
transform:rotate(180deg); }
.bp3-tether-element-attached-middle > .bp3-tooltip > .bp3-popover-arrow{
top:50%;
-webkit-transform:translateY(-50%);
transform:translateY(-50%); }
.bp3-tether-element-attached-center > .bp3-tooltip > .bp3-popover-arrow{
right:50%;
-webkit-transform:translateX(50%);
transform:translateX(50%); }
.bp3-tether-element-attached-top.bp3-tether-target-attached-top > .bp3-tooltip > .bp3-popover-arrow{
top:-0.22183px; }
.bp3-tether-element-attached-right.bp3-tether-target-attached-right > .bp3-tooltip > .bp3-popover-arrow{
right:-0.22183px; }
.bp3-tether-element-attached-left.bp3-tether-target-attached-left > .bp3-tooltip > .bp3-popover-arrow{
left:-0.22183px; }
.bp3-tether-element-attached-bottom.bp3-tether-target-attached-bottom > .bp3-tooltip > .bp3-popover-arrow{
bottom:-0.22183px; }
.bp3-tether-element-attached-top.bp3-tether-element-attached-left > .bp3-tooltip{
-webkit-transform-origin:top left;
transform-origin:top left; }
.bp3-tether-element-attached-top.bp3-tether-element-attached-center > .bp3-tooltip{
-webkit-transform-origin:top center;
transform-origin:top center; }
.bp3-tether-element-attached-top.bp3-tether-element-attached-right > .bp3-tooltip{
-webkit-transform-origin:top right;
transform-origin:top right; }
.bp3-tether-element-attached-middle.bp3-tether-element-attached-left > .bp3-tooltip{
-webkit-transform-origin:center left;
transform-origin:center left; }
.bp3-tether-element-attached-middle.bp3-tether-element-attached-center > .bp3-tooltip{
-webkit-transform-origin:center center;
transform-origin:center center; }
.bp3-tether-element-attached-middle.bp3-tether-element-attached-right > .bp3-tooltip{
-webkit-transform-origin:center right;
transform-origin:center right; }
.bp3-tether-element-attached-bottom.bp3-tether-element-attached-left > .bp3-tooltip{
-webkit-transform-origin:bottom left;
transform-origin:bottom left; }
.bp3-tether-element-attached-bottom.bp3-tether-element-attached-center > .bp3-tooltip{
-webkit-transform-origin:bottom center;
transform-origin:bottom center; }
.bp3-tether-element-attached-bottom.bp3-tether-element-attached-right > .bp3-tooltip{
-webkit-transform-origin:bottom right;
transform-origin:bottom right; }
.bp3-tooltip .bp3-popover-content{
background:#394b59;
color:#f5f8fa; }
.bp3-tooltip .bp3-popover-arrow::before{
-webkit-box-shadow:1px 1px 6px rgba(16, 22, 26, 0.2);
box-shadow:1px 1px 6px rgba(16, 22, 26, 0.2); }
.bp3-tooltip .bp3-popover-arrow-border{
fill:#10161a;
fill-opacity:0.1; }
.bp3-tooltip .bp3-popover-arrow-fill{
fill:#394b59; }
.bp3-popover-enter > .bp3-tooltip, .bp3-popover-appear > .bp3-tooltip{
-webkit-transform:scale(0.8);
transform:scale(0.8); }
.bp3-popover-enter-active > .bp3-tooltip, .bp3-popover-appear-active > .bp3-tooltip{
-webkit-transform:scale(1);
transform:scale(1);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-popover-exit > .bp3-tooltip{
-webkit-transform:scale(1);
transform:scale(1); }
.bp3-popover-exit-active > .bp3-tooltip{
-webkit-transform:scale(0.8);
transform:scale(0.8);
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:100ms;
transition-duration:100ms;
-webkit-transition-property:-webkit-transform;
transition-property:-webkit-transform;
transition-property:transform;
transition-property:transform, -webkit-transform;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-tooltip .bp3-popover-content{
padding:10px 12px; }
.bp3-tooltip.bp3-dark,
.bp3-dark .bp3-tooltip{
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4); }
.bp3-tooltip.bp3-dark .bp3-popover-content,
.bp3-dark .bp3-tooltip .bp3-popover-content{
background:#e1e8ed;
color:#394b59; }
.bp3-tooltip.bp3-dark .bp3-popover-arrow::before,
.bp3-dark .bp3-tooltip .bp3-popover-arrow::before{
-webkit-box-shadow:1px 1px 6px rgba(16, 22, 26, 0.4);
box-shadow:1px 1px 6px rgba(16, 22, 26, 0.4); }
.bp3-tooltip.bp3-dark .bp3-popover-arrow-border,
.bp3-dark .bp3-tooltip .bp3-popover-arrow-border{
fill:#10161a;
fill-opacity:0.2; }
.bp3-tooltip.bp3-dark .bp3-popover-arrow-fill,
.bp3-dark .bp3-tooltip .bp3-popover-arrow-fill{
fill:#e1e8ed; }
.bp3-tooltip.bp3-intent-primary .bp3-popover-content{
background:#137cbd;
color:#ffffff; }
.bp3-tooltip.bp3-intent-primary .bp3-popover-arrow-fill{
fill:#137cbd; }
.bp3-tooltip.bp3-intent-success .bp3-popover-content{
background:#0f9960;
color:#ffffff; }
.bp3-tooltip.bp3-intent-success .bp3-popover-arrow-fill{
fill:#0f9960; }
.bp3-tooltip.bp3-intent-warning .bp3-popover-content{
background:#d9822b;
color:#ffffff; }
.bp3-tooltip.bp3-intent-warning .bp3-popover-arrow-fill{
fill:#d9822b; }
.bp3-tooltip.bp3-intent-danger .bp3-popover-content{
background:#db3737;
color:#ffffff; }
.bp3-tooltip.bp3-intent-danger .bp3-popover-arrow-fill{
fill:#db3737; }
.bp3-tooltip-indicator{
border-bottom:dotted 1px;
cursor:help; }
.bp3-tree .bp3-icon, .bp3-tree .bp3-icon-standard, .bp3-tree .bp3-icon-large{
color:#5c7080; }
.bp3-tree .bp3-icon.bp3-intent-primary, .bp3-tree .bp3-icon-standard.bp3-intent-primary, .bp3-tree .bp3-icon-large.bp3-intent-primary{
color:#137cbd; }
.bp3-tree .bp3-icon.bp3-intent-success, .bp3-tree .bp3-icon-standard.bp3-intent-success, .bp3-tree .bp3-icon-large.bp3-intent-success{
color:#0f9960; }
.bp3-tree .bp3-icon.bp3-intent-warning, .bp3-tree .bp3-icon-standard.bp3-intent-warning, .bp3-tree .bp3-icon-large.bp3-intent-warning{
color:#d9822b; }
.bp3-tree .bp3-icon.bp3-intent-danger, .bp3-tree .bp3-icon-standard.bp3-intent-danger, .bp3-tree .bp3-icon-large.bp3-intent-danger{
color:#db3737; }
.bp3-tree-node-list{
list-style:none;
margin:0;
padding-left:0; }
.bp3-tree-root{
background-color:transparent;
cursor:default;
padding-left:0;
position:relative; }
.bp3-tree-node-content-0{
padding-left:0px; }
.bp3-tree-node-content-1{
padding-left:23px; }
.bp3-tree-node-content-2{
padding-left:46px; }
.bp3-tree-node-content-3{
padding-left:69px; }
.bp3-tree-node-content-4{
padding-left:92px; }
.bp3-tree-node-content-5{
padding-left:115px; }
.bp3-tree-node-content-6{
padding-left:138px; }
.bp3-tree-node-content-7{
padding-left:161px; }
.bp3-tree-node-content-8{
padding-left:184px; }
.bp3-tree-node-content-9{
padding-left:207px; }
.bp3-tree-node-content-10{
padding-left:230px; }
.bp3-tree-node-content-11{
padding-left:253px; }
.bp3-tree-node-content-12{
padding-left:276px; }
.bp3-tree-node-content-13{
padding-left:299px; }
.bp3-tree-node-content-14{
padding-left:322px; }
.bp3-tree-node-content-15{
padding-left:345px; }
.bp3-tree-node-content-16{
padding-left:368px; }
.bp3-tree-node-content-17{
padding-left:391px; }
.bp3-tree-node-content-18{
padding-left:414px; }
.bp3-tree-node-content-19{
padding-left:437px; }
.bp3-tree-node-content-20{
padding-left:460px; }
.bp3-tree-node-content{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex;
height:30px;
padding-right:5px;
width:100%; }
.bp3-tree-node-content:hover{
background-color:rgba(191, 204, 214, 0.4); }
.bp3-tree-node-caret,
.bp3-tree-node-caret-none{
min-width:30px; }
.bp3-tree-node-caret{
color:#5c7080;
cursor:pointer;
padding:7px;
-webkit-transform:rotate(0deg);
transform:rotate(0deg);
-webkit-transition:-webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:-webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9);
transition:transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9), -webkit-transform 200ms cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-tree-node-caret:hover{
color:#182026; }
.bp3-dark .bp3-tree-node-caret{
color:#a7b6c2; }
.bp3-dark .bp3-tree-node-caret:hover{
color:#f5f8fa; }
.bp3-tree-node-caret.bp3-tree-node-caret-open{
-webkit-transform:rotate(90deg);
transform:rotate(90deg); }
.bp3-tree-node-caret.bp3-icon-standard::before{
content:""; }
.bp3-tree-node-icon{
margin-right:7px;
position:relative; }
.bp3-tree-node-label{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
word-wrap:normal;
-webkit-box-flex:1;
-ms-flex:1 1 auto;
flex:1 1 auto;
position:relative;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none; }
.bp3-tree-node-label span{
display:inline; }
.bp3-tree-node-secondary-label{
padding:0 5px;
-webkit-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none; }
.bp3-tree-node-secondary-label .bp3-popover-wrapper,
.bp3-tree-node-secondary-label .bp3-popover-target{
-webkit-box-align:center;
-ms-flex-align:center;
align-items:center;
display:-webkit-box;
display:-ms-flexbox;
display:flex; }
.bp3-tree-node.bp3-disabled .bp3-tree-node-content{
background-color:inherit;
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-tree-node.bp3-disabled .bp3-tree-node-caret,
.bp3-tree-node.bp3-disabled .bp3-tree-node-icon{
color:rgba(92, 112, 128, 0.6);
cursor:not-allowed; }
.bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content{
background-color:#137cbd; }
.bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content,
.bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content .bp3-icon, .bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content .bp3-icon-standard, .bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content .bp3-icon-large{
color:#ffffff; }
.bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content .bp3-tree-node-caret::before{
color:rgba(255, 255, 255, 0.7); }
.bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content .bp3-tree-node-caret:hover::before{
color:#ffffff; }
.bp3-dark .bp3-tree-node-content:hover{
background-color:rgba(92, 112, 128, 0.3); }
.bp3-dark .bp3-tree .bp3-icon, .bp3-dark .bp3-tree .bp3-icon-standard, .bp3-dark .bp3-tree .bp3-icon-large{
color:#a7b6c2; }
.bp3-dark .bp3-tree .bp3-icon.bp3-intent-primary, .bp3-dark .bp3-tree .bp3-icon-standard.bp3-intent-primary, .bp3-dark .bp3-tree .bp3-icon-large.bp3-intent-primary{
color:#137cbd; }
.bp3-dark .bp3-tree .bp3-icon.bp3-intent-success, .bp3-dark .bp3-tree .bp3-icon-standard.bp3-intent-success, .bp3-dark .bp3-tree .bp3-icon-large.bp3-intent-success{
color:#0f9960; }
.bp3-dark .bp3-tree .bp3-icon.bp3-intent-warning, .bp3-dark .bp3-tree .bp3-icon-standard.bp3-intent-warning, .bp3-dark .bp3-tree .bp3-icon-large.bp3-intent-warning{
color:#d9822b; }
.bp3-dark .bp3-tree .bp3-icon.bp3-intent-danger, .bp3-dark .bp3-tree .bp3-icon-standard.bp3-intent-danger, .bp3-dark .bp3-tree .bp3-icon-large.bp3-intent-danger{
color:#db3737; }
.bp3-dark .bp3-tree-node.bp3-tree-node-selected > .bp3-tree-node-content{
background-color:#137cbd; }
.bp3-omnibar{
-webkit-filter:blur(0);
filter:blur(0);
opacity:1;
background-color:#ffffff;
border-radius:3px;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.1), 0 4px 8px rgba(16, 22, 26, 0.2), 0 18px 46px 6px rgba(16, 22, 26, 0.2);
left:calc(50% - 250px);
top:20vh;
width:500px;
z-index:21; }
.bp3-omnibar.bp3-overlay-enter, .bp3-omnibar.bp3-overlay-appear{
-webkit-filter:blur(20px);
filter:blur(20px);
opacity:0.2; }
.bp3-omnibar.bp3-overlay-enter-active, .bp3-omnibar.bp3-overlay-appear-active{
-webkit-filter:blur(0);
filter:blur(0);
opacity:1;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:opacity, -webkit-filter;
transition-property:opacity, -webkit-filter;
transition-property:filter, opacity;
transition-property:filter, opacity, -webkit-filter;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-omnibar.bp3-overlay-exit{
-webkit-filter:blur(0);
filter:blur(0);
opacity:1; }
.bp3-omnibar.bp3-overlay-exit-active{
-webkit-filter:blur(20px);
filter:blur(20px);
opacity:0.2;
-webkit-transition-delay:0;
transition-delay:0;
-webkit-transition-duration:200ms;
transition-duration:200ms;
-webkit-transition-property:opacity, -webkit-filter;
transition-property:opacity, -webkit-filter;
transition-property:filter, opacity;
transition-property:filter, opacity, -webkit-filter;
-webkit-transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9);
transition-timing-function:cubic-bezier(0.4, 1, 0.75, 0.9); }
.bp3-omnibar .bp3-input{
background-color:transparent;
border-radius:0; }
.bp3-omnibar .bp3-input, .bp3-omnibar .bp3-input:focus{
-webkit-box-shadow:none;
box-shadow:none; }
.bp3-omnibar .bp3-menu{
background-color:transparent;
border-radius:0;
-webkit-box-shadow:inset 0 1px 0 rgba(16, 22, 26, 0.15);
box-shadow:inset 0 1px 0 rgba(16, 22, 26, 0.15);
max-height:calc(60vh - 40px);
overflow:auto; }
.bp3-omnibar .bp3-menu:empty{
display:none; }
.bp3-dark .bp3-omnibar, .bp3-omnibar.bp3-dark{
background-color:#30404d;
-webkit-box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4);
box-shadow:0 0 0 1px rgba(16, 22, 26, 0.2), 0 4px 8px rgba(16, 22, 26, 0.4), 0 18px 46px 6px rgba(16, 22, 26, 0.4); }
.bp3-omnibar-overlay .bp3-overlay-backdrop{
background-color:rgba(16, 22, 26, 0.2); }
.bp3-select-popover .bp3-popover-content{
padding:5px; }
.bp3-select-popover .bp3-input-group{
margin-bottom:0; }
.bp3-select-popover .bp3-menu{
max-height:300px;
max-width:400px;
overflow:auto;
padding:0; }
.bp3-select-popover .bp3-menu:not(:first-child){
padding-top:5px; }
.bp3-multi-select{
min-width:150px; }
.bp3-multi-select-popover .bp3-menu{
max-height:300px;
max-width:400px;
overflow:auto; }
.bp3-select-popover .bp3-popover-content{
padding:5px; }
.bp3-select-popover .bp3-input-group{
margin-bottom:0; }
.bp3-select-popover .bp3-menu{
max-height:300px;
max-width:400px;
overflow:auto;
padding:0; }
.bp3-select-popover .bp3-menu:not(:first-child){
padding-top:5px; }
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/* This file was auto-generated by ensureUiComponents() in @jupyterlab/buildutils */
/**
* (<span class="caps">DEPRECATED</span>) Support for consuming icons as <span class="caps">CSS</span> background images
*/
/* Icons urls */
:root {
--jp-icon-add-above: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEzN18xOTQ5MikiPgo8cGF0aCBjbGFzcz0ianAtaWNvbjMiIGQ9Ik00Ljc1IDQuOTMwNjZINi42MjVWNi44MDU2NkM2LjYyNSA3LjAxMTkxIDYuNzkzNzUgNy4xODA2NiA3IDcuMTgwNjZDNy4yMDYyNSA3LjE4MDY2IDcuMzc1IDcuMDExOTEgNy4zNzUgNi44MDU2NlY0LjkzMDY2SDkuMjVDOS40NTYyNSA0LjkzMDY2IDkuNjI1IDQuNzYxOTEgOS42MjUgNC41NTU2NkM5LjYyNSA0LjM0OTQxIDkuNDU2MjUgNC4xODA2NiA5LjI1IDQuMTgwNjZINy4zNzVWMi4zMDU2NkM3LjM3NSAyLjA5OTQxIDcuMjA2MjUgMS45MzA2NiA3IDEuOTMwNjZDNi43OTM3NSAxLjkzMDY2IDYuNjI1IDIuMDk5NDEgNi42MjUgMi4zMDU2NlY0LjE4MDY2SDQuNzVDNC41NDM3NSA0LjE4MDY2IDQuMzc1IDQuMzQ5NDEgNC4zNzUgNC41NTU2NkM0LjM3NSA0Ljc2MTkxIDQuNTQzNzUgNC45MzA2NiA0Ljc1IDQuOTMwNjZaIiBmaWxsPSIjNjE2MTYxIiBzdHJva2U9IiM2MTYxNjEiIHN0cm9rZS13aWR0aD0iMC43Ii8+CjwvZz4KPHBhdGggY2xhc3M9ImpwLWljb24zIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTExLjUgOS41VjExLjVMMi41IDExLjVWOS41TDExLjUgOS41Wk0xMiA4QzEyLjU1MjMgOCAxMyA4LjQ0NzcyIDEzIDlWMTJDMTMgMTIuNTUyMyAxMi41NTIzIDEzIDEyIDEzTDIgMTNDMS40NDc3MiAxMyAxIDEyLjU1MjMgMSAxMlY5QzEgOC40NDc3MiAxLjQ0NzcxIDggMiA4TDEyIDhaIiBmaWxsPSIjNjE2MTYxIi8+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzEzN18xOTQ5MiI+CjxyZWN0IGNsYXNzPSJqcC1pY29uMyIgd2lkdGg9IjYiIGhlaWdodD0iNiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDEwIDEuNTU1NjYpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==);
--jp-icon-add-below: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzEzN18xOTQ5OCkiPgo8cGF0aCBjbGFzcz0ianAtaWNvbjMiIGQ9Ik05LjI1IDEwLjA2OTNMNy4zNzUgMTAuMDY5M0w3LjM3NSA4LjE5NDM0QzcuMzc1IDcuOTg4MDkgNy4yMDYyNSA3LjgxOTM0IDcgNy44MTkzNEM2Ljc5Mzc1IDcuODE5MzQgNi42MjUgNy45ODgwOSA2LjYyNSA4LjE5NDM0TDYuNjI1IDEwLjA2OTNMNC43NSAxMC4wNjkzQzQuNTQzNzUgMTAuMDY5MyA0LjM3NSAxMC4yMzgxIDQuMzc1IDEwLjQ0NDNDNC4zNzUgMTAuNjUwNiA0LjU0Mzc1IDEwLjgxOTMgNC43NSAxMC44MTkzTDYuNjI1IDEwLjgxOTNMNi42MjUgMTIuNjk0M0M2LjYyNSAxMi45MDA2IDYuNzkzNzUgMTMuMDY5MyA3IDEzLjA2OTNDNy4yMDYyNSAxMy4wNjkzIDcuMzc1IDEyLjkwMDYgNy4zNzUgMTIuNjk0M0w3LjM3NSAxMC44MTkzTDkuMjUgMTAuODE5M0M5LjQ1NjI1IDEwLjgxOTMgOS42MjUgMTAuNjUwNiA5LjYyNSAxMC40NDQzQzkuNjI1IDEwLjIzODEgOS40NTYyNSAxMC4wNjkzIDkuMjUgMTAuMDY5M1oiIGZpbGw9IiM2MTYxNjEiIHN0cm9rZT0iIzYxNjE2MSIgc3Ryb2tlLXdpZHRoPSIwLjciLz4KPC9nPgo8cGF0aCBjbGFzcz0ianAtaWNvbjMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMi41IDUuNUwyLjUgMy41TDExLjUgMy41TDExLjUgNS41TDIuNSA1LjVaTTIgN0MxLjQ0NzcyIDcgMSA2LjU1MjI4IDEgNkwxIDNDMSAyLjQ0NzcyIDEuNDQ3NzIgMiAyIDJMMTIgMkMxMi41NTIzIDIgMTMgMi40NDc3MiAxMyAzTDEzIDZDMTMgNi41NTIyOSAxMi41NTIzIDcgMTIgN0wyIDdaIiBmaWxsPSIjNjE2MTYxIi8+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzEzN18xOTQ5OCI+CjxyZWN0IGNsYXNzPSJqcC1pY29uMyIgd2lkdGg9IjYiIGhlaWdodD0iNiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0ibWF0cml4KDEgMS43NDg0NmUtMDcgMS43NDg0NmUtMDcgLTEgNCAxMy40NDQzKSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=);
--jp-icon-add: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTE5IDEzaC02djZoLTJ2LTZINXYtMmg2VjVoMnY2aDZ2MnoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-bell: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE2IDE2IiB2ZXJzaW9uPSIxLjEiPgogICA8cGF0aCBjbGFzcz0ianAtaWNvbjIganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjMzMzMzMzIgogICAgICBkPSJtOCAwLjI5Yy0xLjQgMC0yLjcgMC43My0zLjYgMS44LTEuMiAxLjUtMS40IDMuNC0xLjUgNS4yLTAuMTggMi4yLTAuNDQgNC0yLjMgNS4zbDAuMjggMS4zaDVjMC4wMjYgMC42NiAwLjMyIDEuMSAwLjcxIDEuNSAwLjg0IDAuNjEgMiAwLjYxIDIuOCAwIDAuNTItMC40IDAuNi0xIDAuNzEtMS41aDVsMC4yOC0xLjNjLTEuOS0wLjk3LTIuMi0zLjMtMi4zLTUuMy0wLjEzLTEuOC0wLjI2LTMuNy0xLjUtNS4yLTAuODUtMS0yLjItMS44LTMuNi0xLjh6bTAgMS40YzAuODggMCAxLjkgMC41NSAyLjUgMS4zIDAuODggMS4xIDEuMSAyLjcgMS4yIDQuNCAwLjEzIDEuNyAwLjIzIDMuNiAxLjMgNS4yaC0xMGMxLjEtMS42IDEuMi0zLjQgMS4zLTUuMiAwLjEzLTEuNyAwLjMtMy4zIDEuMi00LjQgMC41OS0wLjcyIDEuNi0xLjMgMi41LTEuM3ptLTAuNzQgMTJoMS41Yy0wLjAwMTUgMC4yOCAwLjAxNSAwLjc5LTAuNzQgMC43OS0wLjczIDAuMDAxNi0wLjcyLTAuNTMtMC43NC0wLjc5eiIgLz4KPC9zdmc+Cg==);
--jp-icon-bug-dot: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiM2MTYxNjEiPgogICAgICAgIDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTcuMTkgOEgyMFYxMEgxNy45MUMxNy45NiAxMC4zMyAxOCAxMC42NiAxOCAxMVYxMkgyMFYxNEgxOC41SDE4VjE0LjAyNzVDMTUuNzUgMTQuMjc2MiAxNCAxNi4xODM3IDE0IDE4LjVDMTQgMTkuMjA4IDE0LjE2MzUgMTkuODc3OSAxNC40NTQ5IDIwLjQ3MzlDMTMuNzA2MyAyMC44MTE3IDEyLjg3NTcgMjEgMTIgMjFDOS43OCAyMSA3Ljg1IDE5Ljc5IDYuODEgMThINFYxNkg2LjA5QzYuMDQgMTUuNjcgNiAxNS4zNCA2IDE1VjE0SDRWMTJINlYxMUM2IDEwLjY2IDYuMDQgMTAuMzMgNi4wOSAxMEg0VjhINi44MUM3LjI2IDcuMjIgNy44OCA2LjU1IDguNjIgNi4wNEw3IDQuNDFMOC40MSAzTDEwLjU5IDUuMTdDMTEuMDQgNS4wNiAxMS41MSA1IDEyIDVDMTIuNDkgNSAxMi45NiA1LjA2IDEzLjQyIDUuMTdMMTUuNTkgM0wxNyA0LjQxTDE1LjM3IDYuMDRDMTYuMTIgNi41NSAxNi43NCA3LjIyIDE3LjE5IDhaTTEwIDE2SDE0VjE0SDEwVjE2Wk0xMCAxMkgxNFYxMEgxMFYxMloiIGZpbGw9IiM2MTYxNjEiLz4KICAgICAgICA8cGF0aCBkPSJNMjIgMTguNUMyMiAyMC40MzMgMjAuNDMzIDIyIDE4LjUgMjJDMTYuNTY3IDIyIDE1IDIwLjQzMyAxNSAxOC41QzE1IDE2LjU2NyAxNi41NjcgMTUgMTguNSAxNUMyMC40MzMgMTUgMjIgMTYuNTY3IDIyIDE4LjVaIiBmaWxsPSIjNjE2MTYxIi8+CiAgICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-bug: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIj4KICAgIDxwYXRoIGQ9Ik0yMCA4aC0yLjgxYy0uNDUtLjc4LTEuMDctMS40NS0xLjgyLTEuOTZMMTcgNC40MSAxNS41OSAzbC0yLjE3IDIuMTdDMTIuOTYgNS4wNiAxMi40OSA1IDEyIDVjLS40OSAwLS45Ni4wNi0xLjQxLjE3TDguNDEgMyA3IDQuNDFsMS42MiAxLjYzQzcuODggNi41NSA3LjI2IDcuMjIgNi44MSA4SDR2MmgyLjA5Yy0uMDUuMzMtLjA5LjY2LS4wOSAxdjFINHYyaDJ2MWMwIC4zNC4wNC42Ny4wOSAxSDR2MmgyLjgxYzEuMDQgMS43OSAyLjk3IDMgNS4xOSAzczQuMTUtMS4yMSA1LjE5LTNIMjB2LTJoLTIuMDljLjA1LS4zMy4wOS0uNjYuMDktMXYtMWgydi0yaC0ydi0xYzAtLjM0LS4wNC0uNjctLjA5LTFIMjBWOHptLTYgOGgtNHYtMmg0djJ6bTAtNGgtNHYtMmg0djJ6Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-build: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTE0LjkgMTcuNDVDMTYuMjUgMTcuNDUgMTcuMzUgMTYuMzUgMTcuMzUgMTVDMTcuMzUgMTMuNjUgMTYuMjUgMTIuNTUgMTQuOSAxMi41NUMxMy41NCAxMi41NSAxMi40NSAxMy42NSAxMi40NSAxNUMxMi40NSAxNi4zNSAxMy41NCAxNy40NSAxNC45IDE3LjQ1Wk0yMC4xIDE1LjY4TDIxLjU4IDE2Ljg0QzIxLjcxIDE2Ljk1IDIxLjc1IDE3LjEzIDIxLjY2IDE3LjI5TDIwLjI2IDE5LjcxQzIwLjE3IDE5Ljg2IDIwIDE5LjkyIDE5LjgzIDE5Ljg2TDE4LjA5IDE5LjE2QzE3LjczIDE5LjQ0IDE3LjMzIDE5LjY3IDE2LjkxIDE5Ljg1TDE2LjY0IDIxLjdDMTYuNjIgMjEuODcgMTYuNDcgMjIgMTYuMyAyMkgxMy41QzEzLjMyIDIyIDEzLjE4IDIxLjg3IDEzLjE1IDIxLjdMMTIuODkgMTkuODVDMTIuNDYgMTkuNjcgMTIuMDcgMTkuNDQgMTEuNzEgMTkuMTZMOS45NjAwMiAxOS44NkM5LjgxMDAyIDE5LjkyIDkuNjIwMDIgMTkuODYgOS41NDAwMiAxOS43MUw4LjE0MDAyIDE3LjI5QzguMDUwMDIgMTcuMTMgOC4wOTAwMiAxNi45NSA4LjIyMDAyIDE2Ljg0TDkuNzAwMDIgMTUuNjhMOS42NTAwMSAxNUw5LjcwMDAyIDE0LjMxTDguMjIwMDIgMTMuMTZDOC4wOTAwMiAxMy4wNSA4LjA1MDAyIDEyLjg2IDguMTQwMDIgMTIuNzFMOS41NDAwMiAxMC4yOUM5LjYyMDAyIDEwLjEzIDkuODEwMDIgMTAuMDcgOS45NjAwMiAxMC4xM0wxMS43MSAxMC44NEMxMi4wNyAxMC41NiAxMi40NiAxMC4zMiAxMi44OSAxMC4xNUwxMy4xNSA4LjI4OTk4QzEzLjE4IDguMTI5OTggMTMuMzIgNy45OTk5OCAxMy41IDcuOTk5OThIMTYuM0MxNi40NyA3Ljk5OTk4IDE2LjYyIDguMTI5OTggMTYuNjQgOC4yODk5OEwxNi45MSAxMC4xNUMxNy4zMyAxMC4zMiAxNy43MyAxMC41NiAxOC4wOSAxMC44NEwxOS44MyAxMC4xM0MyMCAxMC4wNyAyMC4xNyAxMC4xMyAyMC4yNiAxMC4yOUwyMS42NiAxMi43MUMyMS43NSAxMi44NiAyMS43MSAxMy4wNSAyMS41OCAxMy4xNkwyMC4xIDE0LjMxTDIwLjE1IDE1TDIwLjEgMTUuNjhaIi8+CiAgICA8cGF0aCBkPSJNNy4zMjk2NiA3LjQ0NDU0QzguMDgzMSA3LjAwOTU0IDguMzM5MzIgNi4wNTMzMiA3LjkwNDMyIDUuMjk5ODhDNy40NjkzMiA0LjU0NjQzIDYuNTA4MSA0LjI4MTU2IDUuNzU0NjYgNC43MTY1NkM1LjM5MTc2IDQuOTI2MDggNS4xMjY5NSA1LjI3MTE4IDUuMDE4NDkgNS42NzU5NEM0LjkxMDA0IDYuMDgwNzEgNC45NjY4MiA2LjUxMTk4IDUuMTc2MzQgNi44NzQ4OEM1LjYxMTM0IDcuNjI4MzIgNi41NzYyMiA3Ljg3OTU0IDcuMzI5NjYgNy40NDQ1NFpNOS42NTcxOCA0Ljc5NTkzTDEwLjg2NzIgNC45NTE3OUMxMC45NjI4IDQuOTc3NDEgMTEuMDQwMiA1LjA3MTMzIDExLjAzODIgNS4xODc5M0wxMS4wMzg4IDYuOTg4OTNDMTEuMDQ1NSA3LjEwMDU0IDEwLjk2MTYgNy4xOTUxOCAxMC44NTUgNy4yMTA1NEw5LjY2MDAxIDcuMzgwODNMOS4yMzkxNSA4LjEzMTg4TDkuNjY5NjEgOS4yNTc0NUM5LjcwNzI5IDkuMzYyNzEgOS42NjkzNCA5LjQ3Njk5IDkuNTc0MDggOS41MzE5OUw4LjAxNTIzIDEwLjQzMkM3LjkxMTMxIDEwLjQ5MiA3Ljc5MzM3IDEwLjQ2NzcgNy43MjEwNSAxMC4zODI0TDYuOTg3NDggOS40MzE4OEw2LjEwOTMxIDkuNDMwODNMNS4zNDcwNCAxMC4zOTA1QzUuMjg5MDkgMTAuNDcwMiA1LjE3MzgzIDEwLjQ5MDUgNS4wNzE4NyAxMC40MzM5TDMuNTEyNDUgOS41MzI5M0MzLjQxMDQ5IDkuNDc2MzMgMy4zNzY0NyA5LjM1NzQxIDMuNDEwNzUgOS4yNTY3OUwzLjg2MzQ3IDguMTQwOTNMMy42MTc0OSA3Ljc3NDg4TDMuNDIzNDcgNy4zNzg4M0wyLjIzMDc1IDcuMjEyOTdDMi4xMjY0NyA3LjE5MjM1IDIuMDQwNDkgNy4xMDM0MiAyLjA0MjQ1IDYuOTg2ODJMMi4wNDE4NyA1LjE4NTgyQzIuMDQzODMgNS4wNjkyMiAyLjExOTA5IDQuOTc5NTggMi4yMTcwNCA0Ljk2OTIyTDMuNDIwNjUgNC43OTM5M0wzLjg2NzQ5IDQuMDI3ODhMMy40MTEwNSAyLjkxNzMxQzMuMzczMzcgMi44MTIwNCAzLjQxMTMxIDIuNjk3NzYgMy41MTUyMyAyLjYzNzc2TDUuMDc0MDggMS43Mzc3NkM1LjE2OTM0IDEuNjgyNzYgNS4yODcyOSAxLjcwNzA0IDUuMzU5NjEgMS43OTIzMUw2LjExOTE1IDIuNzI3ODhMNi45ODAwMSAyLjczODkzTDcuNzI0OTYgMS43ODkyMkM3Ljc5MTU2IDEuNzA0NTggNy45MTU0OCAxLjY3OTIyIDguMDA4NzkgMS43NDA4Mkw5LjU2ODIxIDIuNjQxODJDOS42NzAxNyAyLjY5ODQyIDkuNzEyODUgMi44MTIzNCA5LjY4NzIzIDIuOTA3OTdMOS4yMTcxOCA0LjAzMzgzTDkuNDYzMTYgNC4zOTk4OEw5LjY1NzE4IDQuNzk1OTNaIi8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-caret-down-empty-thin: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIwIDIwIj4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSIgc2hhcGUtcmVuZGVyaW5nPSJnZW9tZXRyaWNQcmVjaXNpb24iPgoJCTxwb2x5Z29uIGNsYXNzPSJzdDEiIHBvaW50cz0iOS45LDEzLjYgMy42LDcuNCA0LjQsNi42IDkuOSwxMi4yIDE1LjQsNi43IDE2LjEsNy40ICIvPgoJPC9nPgo8L3N2Zz4K);
--jp-icon-caret-down-empty: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiIHNoYXBlLXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIj4KICAgIDxwYXRoIGQ9Ik01LjIsNS45TDksOS43bDMuOC0zLjhsMS4yLDEuMmwtNC45LDVsLTQuOS01TDUuMiw1Ljl6Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-caret-down: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiIHNoYXBlLXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIj4KICAgIDxwYXRoIGQ9Ik01LjIsNy41TDksMTEuMmwzLjgtMy44SDUuMnoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-caret-left: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE4IDE4Ij4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSIgc2hhcGUtcmVuZGVyaW5nPSJnZW9tZXRyaWNQcmVjaXNpb24iPgoJCTxwYXRoIGQ9Ik0xMC44LDEyLjhMNy4xLDlsMy44LTMuOGwwLDcuNkgxMC44eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-caret-right: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiIHNoYXBlLXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIj4KICAgIDxwYXRoIGQ9Ik03LjIsNS4yTDEwLjksOWwtMy44LDMuOFY1LjJINy4yeiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-caret-up-empty-thin: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIwIDIwIj4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSIgc2hhcGUtcmVuZGVyaW5nPSJnZW9tZXRyaWNQcmVjaXNpb24iPgoJCTxwb2x5Z29uIGNsYXNzPSJzdDEiIHBvaW50cz0iMTUuNCwxMy4zIDkuOSw3LjcgNC40LDEzLjIgMy42LDEyLjUgOS45LDYuMyAxNi4xLDEyLjYgIi8+Cgk8L2c+Cjwvc3ZnPgo=);
--jp-icon-caret-up: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE4IDE4Ij4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSIgc2hhcGUtcmVuZGVyaW5nPSJnZW9tZXRyaWNQcmVjaXNpb24iPgoJCTxwYXRoIGQ9Ik01LjIsMTAuNUw5LDYuOGwzLjgsMy44SDUuMnoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-case-sensitive: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIwIDIwIj4KICA8ZyBjbGFzcz0ianAtaWNvbjIiIGZpbGw9IiM0MTQxNDEiPgogICAgPHJlY3QgeD0iMiIgeT0iMiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+CiAgPC9nPgogIDxnIGNsYXNzPSJqcC1pY29uLWFjY2VudDIiIGZpbGw9IiNGRkYiPgogICAgPHBhdGggZD0iTTcuNiw4aDAuOWwzLjUsOGgtMS4xTDEwLDE0SDZsLTAuOSwySDRMNy42LDh6IE04LDkuMUw2LjQsMTNoMy4yTDgsOS4xeiIvPgogICAgPHBhdGggZD0iTTE2LjYsOS44Yy0wLjIsMC4xLTAuNCwwLjEtMC43LDAuMWMtMC4yLDAtMC40LTAuMS0wLjYtMC4yYy0wLjEtMC4xLTAuMi0wLjQtMC4yLTAuNyBjLTAuMywwLjMtMC42LDAuNS0wLjksMC43Yy0wLjMsMC4xLTAuNywwLjItMS4xLDAuMmMtMC4zLDAtMC41LDAtMC43LTAuMWMtMC4yLTAuMS0wLjQtMC4yLTAuNi0wLjNjLTAuMi0wLjEtMC4zLTAuMy0wLjQtMC41IGMtMC4xLTAuMi0wLjEtMC40LTAuMS0wLjdjMC0wLjMsMC4xLTAuNiwwLjItMC44YzAuMS0wLjIsMC4zLTAuNCwwLjQtMC41QzEyLDcsMTIuMiw2LjksMTIuNSw2LjhjMC4yLTAuMSwwLjUtMC4xLDAuNy0wLjIgYzAuMy0wLjEsMC41LTAuMSwwLjctMC4xYzAuMiwwLDAuNC0wLjEsMC42LTAuMWMwLjIsMCwwLjMtMC4xLDAuNC0wLjJjMC4xLTAuMSwwLjItMC4yLDAuMi0wLjRjMC0xLTEuMS0xLTEuMy0xIGMtMC40LDAtMS40LDAtMS40LDEuMmgtMC45YzAtMC40LDAuMS0wLjcsMC4yLTFjMC4xLTAuMiwwLjMtMC40LDAuNS0wLjZjMC4yLTAuMiwwLjUtMC4zLDAuOC0wLjNDMTMuMyw0LDEzLjYsNCwxMy45LDQgYzAuMywwLDAuNSwwLDAuOCwwLjFjMC4zLDAsMC41LDAuMSwwLjcsMC4yYzAuMiwwLjEsMC40LDAuMywwLjUsMC41QzE2LDUsMTYsNS4yLDE2LDUuNnYyLjljMCwwLjIsMCwwLjQsMCwwLjUgYzAsMC4xLDAuMSwwLjIsMC4zLDAuMmMwLjEsMCwwLjIsMCwwLjMsMFY5Ljh6IE0xNS4yLDYuOWMtMS4yLDAuNi0zLjEsMC4yLTMuMSwxLjRjMCwxLjQsMy4xLDEsMy4xLTAuNVY2Ljl6Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-check: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIj4KICAgIDxwYXRoIGQ9Ik05IDE2LjE3TDQuODMgMTJsLTEuNDIgMS40MUw5IDE5IDIxIDdsLTEuNDEtMS40MXoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-circle-empty: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTEyIDJDNi40NyAyIDIgNi40NyAyIDEyczQuNDcgMTAgMTAgMTAgMTAtNC40NyAxMC0xMFMxNy41MyAyIDEyIDJ6bTAgMThjLTQuNDEgMC04LTMuNTktOC04czMuNTktOCA4LTggOCAzLjU5IDggOC0zLjU5IDgtOCA4eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-circle: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTggMTgiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPGNpcmNsZSBjeD0iOSIgY3k9IjkiIHI9IjgiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-clear: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8bWFzayBpZD0iZG9udXRIb2xlIj4KICAgIDxyZWN0IHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0id2hpdGUiIC8+CiAgICA8Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSI4IiBmaWxsPSJibGFjayIvPgogIDwvbWFzaz4KCiAgPGcgY2xhc3M9ImpwLWljb24zIiBmaWxsPSIjNjE2MTYxIj4KICAgIDxyZWN0IGhlaWdodD0iMTgiIHdpZHRoPSIyIiB4PSIxMSIgeT0iMyIgdHJhbnNmb3JtPSJyb3RhdGUoMzE1LCAxMiwgMTIpIi8+CiAgICA8Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIgbWFzaz0idXJsKCNkb251dEhvbGUpIi8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-close: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbi1ub25lIGpwLWljb24tc2VsZWN0YWJsZS1pbnZlcnNlIGpwLWljb24zLWhvdmVyIiBmaWxsPSJub25lIj4KICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjExIi8+CiAgPC9nPgoKICA8ZyBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIGpwLWljb24tYWNjZW50Mi1ob3ZlciIgZmlsbD0iIzYxNjE2MSI+CiAgICA8cGF0aCBkPSJNMTkgNi40MUwxNy41OSA1IDEyIDEwLjU5IDYuNDEgNSA1IDYuNDEgMTAuNTkgMTIgNSAxNy41OSA2LjQxIDE5IDEyIDEzLjQxIDE3LjU5IDE5IDE5IDE3LjU5IDEzLjQxIDEyeiIvPgogIDwvZz4KCiAgPGcgY2xhc3M9ImpwLWljb24tbm9uZSBqcC1pY29uLWJ1c3kiIGZpbGw9Im5vbmUiPgogICAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iNyIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-code: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyOCAyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CgkJPHBhdGggZD0iTTExLjQgMTguNkw2LjggMTRMMTEuNCA5LjRMMTAgOEw0IDE0TDEwIDIwTDExLjQgMTguNlpNMTYuNiAxOC42TDIxLjIgMTRMMTYuNiA5LjRMMTggOEwyNCAxNEwxOCAyMEwxNi42IDE4LjZWMTguNloiLz4KCTwvZz4KPC9zdmc+Cg==);
--jp-icon-console: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIwMCAyMDAiPgogIDxnIGNsYXNzPSJqcC1jb25zb2xlLWljb24tYmFja2dyb3VuZC1jb2xvciBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiMwMjg4RDEiPgogICAgPHBhdGggZD0iTTIwIDE5LjhoMTYwdjE1OS45SDIweiIvPgogIDwvZz4KICA8ZyBjbGFzcz0ianAtY29uc29sZS1pY29uLWNvbG9yIGpwLWljb24tc2VsZWN0YWJsZS1pbnZlcnNlIiBmaWxsPSIjZmZmIj4KICAgIDxwYXRoIGQ9Ik0xMDUgMTI3LjNoNDB2MTIuOGgtNDB6TTUxLjEgNzdMNzQgOTkuOWwtMjMuMyAyMy4zIDEwLjUgMTAuNSAyMy4zLTIzLjNMOTUgOTkuOSA4NC41IDg5LjQgNjEuNiA2Ni41eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-copy: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTggMTgiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTExLjksMUgzLjJDMi40LDEsMS43LDEuNywxLjcsMi41djEwLjJoMS41VjIuNWg4LjdWMXogTTE0LjEsMy45aC04Yy0wLjgsMC0xLjUsMC43LTEuNSwxLjV2MTAuMmMwLDAuOCwwLjcsMS41LDEuNSwxLjVoOCBjMC44LDAsMS41LTAuNywxLjUtMS41VjUuNEMxNS41LDQuNiwxNC45LDMuOSwxNC4xLDMuOXogTTE0LjEsMTUuNWgtOFY1LjRoOFYxNS41eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-copyright: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCI+CiAgPGcgY2xhc3M9ImpwLWljb24zIiBmaWxsPSIjNjE2MTYxIj4KICAgIDxwYXRoIGQ9Ik0xMS44OCw5LjE0YzEuMjgsMC4wNiwxLjYxLDEuMTUsMS42MywxLjY2aDEuNzljLTAuMDgtMS45OC0xLjQ5LTMuMTktMy40NS0zLjE5QzkuNjQsNy42MSw4LDksOCwxMi4xNCBjMCwxLjk0LDAuOTMsNC4yNCwzLjg0LDQuMjRjMi4yMiwwLDMuNDEtMS42NSwzLjQ0LTIuOTVoLTEuNzljLTAuMDMsMC41OS0wLjQ1LDEuMzgtMS42MywxLjQ0QzEwLjU1LDE0LjgzLDEwLDEzLjgxLDEwLDEyLjE0IEMxMCw5LjI1LDExLjI4LDkuMTYsMTEuODgsOS4xNHogTTEyLDJDNi40OCwyLDIsNi40OCwyLDEyczQuNDgsMTAsMTAsMTBzMTAtNC40OCwxMC0xMFMxNy41MiwyLDEyLDJ6IE0xMiwyMGMtNC40MSwwLTgtMy41OS04LTggczMuNTktOCw4LThzOCwzLjU5LDgsOFMxNi40MSwyMCwxMiwyMHoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-cut: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTkuNjQgNy42NGMuMjMtLjUuMzYtMS4wNS4zNi0xLjY0IDAtMi4yMS0xLjc5LTQtNC00UzIgMy43OSAyIDZzMS43OSA0IDQgNGMuNTkgMCAxLjE0LS4xMyAxLjY0LS4zNkwxMCAxMmwtMi4zNiAyLjM2QzcuMTQgMTQuMTMgNi41OSAxNCA2IDE0Yy0yLjIxIDAtNCAxLjc5LTQgNHMxLjc5IDQgNCA0IDQtMS43OSA0LTRjMC0uNTktLjEzLTEuMTQtLjM2LTEuNjRMMTIgMTRsNyA3aDN2LTFMOS42NCA3LjY0ek02IDhjLTEuMSAwLTItLjg5LTItMnMuOS0yIDItMiAyIC44OSAyIDItLjkgMi0yIDJ6bTAgMTJjLTEuMSAwLTItLjg5LTItMnMuOS0yIDItMiAyIC44OSAyIDItLjkgMi0yIDJ6bTYtNy41Yy0uMjggMC0uNS0uMjItLjUtLjVzLjIyLS41LjUtLjUuNS4yMi41LjUtLjIyLjUtLjUuNXpNMTkgM2wtNiA2IDIgMiA3LTdWM3oiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-delete: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCI+CiAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIiAvPgogICAgPHBhdGggY2xhc3M9ImpwLWljb24zIiBmaWxsPSIjNjI2MjYyIiBkPSJNNiAxOWMwIDEuMS45IDIgMiAyaDhjMS4xIDAgMi0uOSAyLTJWN0g2djEyek0xOSA0aC0zLjVsLTEtMWgtNWwtMSAxSDV2MmgxNFY0eiIgLz4KPC9zdmc+Cg==);
--jp-icon-download: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTE5IDloLTRWM0g5djZINWw3IDcgNy03ek01IDE4djJoMTR2LTJINXoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-duplicate: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggY2xhc3M9ImpwLWljb24zIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTIuNzk5OTggMC44NzVIOC44OTU4MkM5LjIwMDYxIDAuODc1IDkuNDQ5OTggMS4xMzkxNCA5LjQ0OTk4IDEuNDYxOThDOS40NDk5OCAxLjc4NDgyIDkuMjAwNjEgMi4wNDg5NiA4Ljg5NTgyIDIuMDQ4OTZIMy4zNTQxNUMzLjA0OTM2IDIuMDQ4OTYgMi43OTk5OCAyLjMxMzEgMi43OTk5OCAyLjYzNTk0VjkuNjc5NjlDMi43OTk5OCAxMC4wMDI1IDIuNTUwNjEgMTAuMjY2NyAyLjI0NTgyIDEwLjI2NjdDMS45NDEwMyAxMC4yNjY3IDEuNjkxNjUgMTAuMDAyNSAxLjY5MTY1IDkuNjc5NjlWMi4wNDg5NkMxLjY5MTY1IDEuNDAzMjggMi4xOTA0IDAuODc1IDIuNzk5OTggMC44NzVaTTUuMzY2NjUgMTEuOVY0LjU1SDExLjA4MzNWMTEuOUg1LjM2NjY1Wk00LjE0MTY1IDQuMTQxNjdDNC4xNDE2NSAzLjY5MDYzIDQuNTA3MjggMy4zMjUgNC45NTgzMiAzLjMyNUgxMS40OTE3QzExLjk0MjcgMy4zMjUgMTIuMzA4MyAzLjY5MDYzIDEyLjMwODMgNC4xNDE2N1YxMi4zMDgzQzEyLjMwODMgMTIuNzU5NCAxMS45NDI3IDEzLjEyNSAxMS40OTE3IDEzLjEyNUg0Ljk1ODMyQzQuNTA3MjggMTMuMTI1IDQuMTQxNjUgMTIuNzU5NCA0LjE0MTY1IDEyLjMwODNWNC4xNDE2N1oiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggY2xhc3M9ImpwLWljb24zIiBkPSJNOS40MzU3NCA4LjI2NTA3SDguMzY0MzFWOS4zMzY1QzguMzY0MzEgOS40NTQzNSA4LjI2Nzg4IDkuNTUwNzggOC4xNTAwMiA5LjU1MDc4QzguMDMyMTcgOS41NTA3OCA3LjkzNTc0IDkuNDU0MzUgNy45MzU3NCA5LjMzNjVWOC4yNjUwN0g2Ljg2NDMxQzYuNzQ2NDUgOC4yNjUwNyA2LjY1MDAyIDguMTY4NjQgNi42NTAwMiA4LjA1MDc4QzYuNjUwMDIgNy45MzI5MiA2Ljc0NjQ1IDcuODM2NSA2Ljg2NDMxIDcuODM2NUg3LjkzNTc0VjYuNzY1MDdDNy45MzU3NCA2LjY0NzIxIDguMDMyMTcgNi41NTA3OCA4LjE1MDAyIDYuNTUwNzhDOC4yNjc4OCA2LjU1MDc4IDguMzY0MzEgNi42NDcyMSA4LjM2NDMxIDYuNzY1MDdWNy44MzY1SDkuNDM1NzRDOS41NTM2IDcuODM2NSA5LjY1MDAyIDcuOTMyOTIgOS42NTAwMiA4LjA1MDc4QzkuNjUwMDIgOC4xNjg2NCA5LjU1MzYgOC4yNjUwNyA5LjQzNTc0IDguMjY1MDdaIiBmaWxsPSIjNjE2MTYxIiBzdHJva2U9IiM2MTYxNjEiIHN0cm9rZS13aWR0aD0iMC41Ii8+Cjwvc3ZnPgo=);
--jp-icon-edit: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTMgMTcuMjVWMjFoMy43NUwxNy44MSA5Ljk0bC0zLjc1LTMuNzVMMyAxNy4yNXpNMjAuNzEgNy4wNGMuMzktLjM5LjM5LTEuMDIgMC0xLjQxbC0yLjM0LTIuMzRjLS4zOS0uMzktMS4wMi0uMzktMS40MSAwbC0xLjgzIDEuODMgMy43NSAzLjc1IDEuODMtMS44M3oiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-ellipses: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPGNpcmNsZSBjeD0iNSIgY3k9IjEyIiByPSIyIi8+CiAgICA8Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIyIi8+CiAgICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjEyIiByPSIyIi8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-extension: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTIwLjUgMTFIMTlWN2MwLTEuMS0uOS0yLTItMmgtNFYzLjVDMTMgMi4xMiAxMS44OCAxIDEwLjUgMVM4IDIuMTIgOCAzLjVWNUg0Yy0xLjEgMC0xLjk5LjktMS45OSAydjMuOEgzLjVjMS40OSAwIDIuNyAxLjIxIDIuNyAyLjdzLTEuMjEgMi43LTIuNyAyLjdIMlYyMGMwIDEuMS45IDIgMiAyaDMuOHYtMS41YzAtMS40OSAxLjIxLTIuNyAyLjctMi43IDEuNDkgMCAyLjcgMS4yMSAyLjcgMi43VjIySDE3YzEuMSAwIDItLjkgMi0ydi00aDEuNWMxLjM4IDAgMi41LTEuMTIgMi41LTIuNVMyMS44OCAxMSAyMC41IDExeiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-fast-forward: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTQgMThsOC41LTZMNCA2djEyem05LTEydjEybDguNS02TDEzIDZ6Ii8+CiAgICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-file-upload: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTkgMTZoNnYtNmg0bC03LTctNyA3aDR6bS00IDJoMTR2Mkg1eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-file: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8cGF0aCBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIiBkPSJNMTkuMyA4LjJsLTUuNS01LjVjLS4zLS4zLS43LS41LTEuMi0uNUgzLjljLS44LjEtMS42LjktMS42IDEuOHYxNC4xYzAgLjkuNyAxLjYgMS42IDEuNmgxNC4yYy45IDAgMS42LS43IDEuNi0xLjZWOS40Yy4xLS41LS4xLS45LS40LTEuMnptLTUuOC0zLjNsMy40IDMuNmgtMy40VjQuOXptMy45IDEyLjdINC43Yy0uMSAwLS4yIDAtLjItLjJWNC43YzAtLjIuMS0uMy4yLS4zaDcuMnY0LjRzMCAuOC4zIDEuMWMuMy4zIDEuMS4zIDEuMS4zaDQuM3Y3LjJzLS4xLjItLjIuMnoiLz4KPC9zdmc+Cg==);
--jp-icon-filter-list: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTEwIDE4aDR2LTJoLTR2MnpNMyA2djJoMThWNkgzem0zIDdoMTJ2LTJINnYyeiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-folder-favorite: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iIzAwMDAwMCI+CiAgPHBhdGggZD0iTTAgMGgyNHYyNEgwVjB6IiBmaWxsPSJub25lIi8+PHBhdGggY2xhc3M9ImpwLWljb24zIGpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iIzYxNjE2MSIgZD0iTTIwIDZoLThsLTItMkg0Yy0xLjEgMC0yIC45LTIgMnYxMmMwIDEuMS45IDIgMiAyaDE2YzEuMSAwIDItLjkgMi0yVjhjMC0xLjEtLjktMi0yLTJ6bS0yLjA2IDExTDE1IDE1LjI4IDEyLjA2IDE3bC43OC0zLjMzLTIuNTktMi4yNCAzLjQxLS4yOUwxNSA4bDEuMzQgMy4xNCAzLjQxLjI5LTIuNTkgMi4yNC43OCAzLjMzeiIvPgo8L3N2Zz4K);
--jp-icon-folder: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIiBkPSJNMTAgNEg0Yy0xLjEgMC0xLjk5LjktMS45OSAyTDIgMThjMCAxLjEuOSAyIDIgMmgxNmMxLjEgMCAyLS45IDItMlY4YzAtMS4xLS45LTItMi0yaC04bC0yLTJ6Ii8+Cjwvc3ZnPgo=);
--jp-icon-home: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iIzAwMDAwMCI+CiAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGNsYXNzPSJqcC1pY29uMyBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiM2MTYxNjEiIGQ9Ik0xMCAyMHYtNmg0djZoNXYtOGgzTDEyIDMgMiAxMmgzdjh6Ii8+Cjwvc3ZnPgo=);
--jp-icon-html5: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDUxMiA1MTIiPgogIDxwYXRoIGNsYXNzPSJqcC1pY29uMCBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiMwMDAiIGQ9Ik0xMDguNCAwaDIzdjIyLjhoMjEuMlYwaDIzdjY5aC0yM1Y0NmgtMjF2MjNoLTIzLjJNMjA2IDIzaC0yMC4zVjBoNjMuN3YyM0gyMjl2NDZoLTIzbTUzLjUtNjloMjQuMWwxNC44IDI0LjNMMzEzLjIgMGgyNC4xdjY5aC0yM1YzNC44bC0xNi4xIDI0LjgtMTYuMS0yNC44VjY5aC0yMi42bTg5LjItNjloMjN2NDYuMmgzMi42VjY5aC01NS42Ii8+CiAgPHBhdGggY2xhc3M9ImpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iI2U0NGQyNiIgZD0iTTEwNy42IDQ3MWwtMzMtMzcwLjRoMzYyLjhsLTMzIDM3MC4yTDI1NS43IDUxMiIvPgogIDxwYXRoIGNsYXNzPSJqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiNmMTY1MjkiIGQ9Ik0yNTYgNDgwLjVWMTMxaDE0OC4zTDM3NiA0NDciLz4KICA8cGF0aCBjbGFzcz0ianAtaWNvbi1zZWxlY3RhYmxlLWludmVyc2UiIGZpbGw9IiNlYmViZWIiIGQ9Ik0xNDIgMTc2LjNoMTE0djQ1LjRoLTY0LjJsNC4yIDQ2LjVoNjB2NDUuM0gxNTQuNG0yIDIyLjhIMjAybDMuMiAzNi4zIDUwLjggMTMuNnY0Ny40bC05My4yLTI2Ii8+CiAgPHBhdGggY2xhc3M9ImpwLWljb24tc2VsZWN0YWJsZS1pbnZlcnNlIiBmaWxsPSIjZmZmIiBkPSJNMzY5LjYgMTc2LjNIMjU1Ljh2NDUuNGgxMDkuNm0tNC4xIDQ2LjVIMjU1Ljh2NDUuNGg1NmwtNS4zIDU5LTUwLjcgMTMuNnY0Ny4ybDkzLTI1LjgiLz4KPC9zdmc+Cg==);
--jp-icon-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8cGF0aCBjbGFzcz0ianAtaWNvbi1icmFuZDQganAtaWNvbi1zZWxlY3RhYmxlLWludmVyc2UiIGZpbGw9IiNGRkYiIGQ9Ik0yLjIgMi4yaDE3LjV2MTcuNUgyLjJ6Ii8+CiAgPHBhdGggY2xhc3M9ImpwLWljb24tYnJhbmQwIGpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iIzNGNTFCNSIgZD0iTTIuMiAyLjJ2MTcuNWgxNy41bC4xLTE3LjVIMi4yem0xMi4xIDIuMmMxLjIgMCAyLjIgMSAyLjIgMi4ycy0xIDIuMi0yLjIgMi4yLTIuMi0xLTIuMi0yLjIgMS0yLjIgMi4yLTIuMnpNNC40IDE3LjZsMy4zLTguOCAzLjMgNi42IDIuMi0zLjIgNC40IDUuNEg0LjR6Ii8+Cjwvc3ZnPgo=);
--jp-icon-inspector: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBjbGFzcz0ianAtaW5zcGVjdG9yLWljb24tY29sb3IganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIiBkPSJNMjAgNEg0Yy0xLjEgMC0xLjk5LjktMS45OSAyTDIgMThjMCAxLjEuOSAyIDIgMmgxNmMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yem0tNSAxNEg0di00aDExdjR6bTAtNUg0VjloMTF2NHptNSA1aC00VjloNHY5eiIvPgo8L3N2Zz4K);
--jp-icon-json: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8ZyBjbGFzcz0ianAtanNvbi1pY29uLWNvbG9yIGpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iI0Y5QTgyNSI+CiAgICA8cGF0aCBkPSJNMjAuMiAxMS44Yy0xLjYgMC0xLjcuNS0xLjcgMSAwIC40LjEuOS4xIDEuMy4xLjUuMS45LjEgMS4zIDAgMS43LTEuNCAyLjMtMy41IDIuM2gtLjl2LTEuOWguNWMxLjEgMCAxLjQgMCAxLjQtLjggMC0uMyAwLS42LS4xLTEgMC0uNC0uMS0uOC0uMS0xLjIgMC0xLjMgMC0xLjggMS4zLTItMS4zLS4yLTEuMy0uNy0xLjMtMiAwLS40LjEtLjguMS0xLjIuMS0uNC4xLS43LjEtMSAwLS44LS40LS43LTEuNC0uOGgtLjVWNC4xaC45YzIuMiAwIDMuNS43IDMuNSAyLjMgMCAuNC0uMS45LS4xIDEuMy0uMS41LS4xLjktLjEgMS4zIDAgLjUuMiAxIDEuNyAxdjEuOHpNMS44IDEwLjFjMS42IDAgMS43LS41IDEuNy0xIDAtLjQtLjEtLjktLjEtMS4zLS4xLS41LS4xLS45LS4xLTEuMyAwLTEuNiAxLjQtMi4zIDMuNS0yLjNoLjl2MS45aC0uNWMtMSAwLTEuNCAwLTEuNC44IDAgLjMgMCAuNi4xIDEgMCAuMi4xLjYuMSAxIDAgMS4zIDAgMS44LTEuMyAyQzYgMTEuMiA2IDExLjcgNiAxM2MwIC40LS4xLjgtLjEgMS4yLS4xLjMtLjEuNy0uMSAxIDAgLjguMy44IDEuNC44aC41djEuOWgtLjljLTIuMSAwLTMuNS0uNi0zLjUtMi4zIDAtLjQuMS0uOS4xLTEuMy4xLS41LjEtLjkuMS0xLjMgMC0uNS0uMi0xLTEuNy0xdi0xLjl6Ii8+CiAgICA8Y2lyY2xlIGN4PSIxMSIgY3k9IjEzLjgiIHI9IjIuMSIvPgogICAgPGNpcmNsZSBjeD0iMTEiIGN5PSI4LjIiIHI9IjIuMSIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-julia: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDMyNSAzMDAiPgogIDxnIGNsYXNzPSJqcC1icmFuZDAganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjY2IzYzMzIj4KICAgIDxwYXRoIGQ9Ik0gMTUwLjg5ODQzOCAyMjUgQyAxNTAuODk4NDM4IDI2Ni40MjE4NzUgMTE3LjMyMDMxMiAzMDAgNzUuODk4NDM4IDMwMCBDIDM0LjQ3NjU2MiAzMDAgMC44OTg0MzggMjY2LjQyMTg3NSAwLjg5ODQzOCAyMjUgQyAwLjg5ODQzOCAxODMuNTc4MTI1IDM0LjQ3NjU2MiAxNTAgNzUuODk4NDM4IDE1MCBDIDExNy4zMjAzMTIgMTUwIDE1MC44OTg0MzggMTgzLjU3ODEyNSAxNTAuODk4NDM4IDIyNSIvPgogIDwvZz4KICA8ZyBjbGFzcz0ianAtYnJhbmQwIGpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iIzM4OTgyNiI+CiAgICA8cGF0aCBkPSJNIDIzNy41IDc1IEMgMjM3LjUgMTE2LjQyMTg3NSAyMDMuOTIxODc1IDE1MCAxNjIuNSAxNTAgQyAxMjEuMDc4MTI1IDE1MCA4Ny41IDExNi40MjE4NzUgODcuNSA3NSBDIDg3LjUgMzMuNTc4MTI1IDEyMS4wNzgxMjUgMCAxNjIuNSAwIEMgMjAzLjkyMTg3NSAwIDIzNy41IDMzLjU3ODEyNSAyMzcuNSA3NSIvPgogIDwvZz4KICA8ZyBjbGFzcz0ianAtYnJhbmQwIGpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iIzk1NThiMiI+CiAgICA8cGF0aCBkPSJNIDMyNC4xMDE1NjIgMjI1IEMgMzI0LjEwMTU2MiAyNjYuNDIxODc1IDI5MC41MjM0MzggMzAwIDI0OS4xMDE1NjIgMzAwIEMgMjA3LjY3OTY4OCAzMDAgMTc0LjEwMTU2MiAyNjYuNDIxODc1IDE3NC4xMDE1NjIgMjI1IEMgMTc0LjEwMTU2MiAxODMuNTc4MTI1IDIwNy42Nzk2ODggMTUwIDI0OS4xMDE1NjIgMTUwIEMgMjkwLjUyMzQzOCAxNTAgMzI0LjEwMTU2MiAxODMuNTc4MTI1IDMyNC4xMDE1NjIgMjI1Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-jupyter-favicon: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUyIiBoZWlnaHQ9IjE2NSIgdmlld0JveD0iMCAwIDE1MiAxNjUiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgPGcgY2xhc3M9ImpwLWp1cHl0ZXItaWNvbi1jb2xvciIgZmlsbD0iI0YzNzcyNiI+CiAgICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjA3ODk0NywgMTEwLjU4MjkyNykiIGQ9Ik03NS45NDIyODQyLDI5LjU4MDQ1NjEgQzQzLjMwMjM5NDcsMjkuNTgwNDU2MSAxNC43OTY3ODMyLDE3LjY1MzQ2MzQgMCwwIEM1LjUxMDgzMjExLDE1Ljg0MDY4MjkgMTUuNzgxNTM4OSwyOS41NjY3NzMyIDI5LjM5MDQ5NDcsMzkuMjc4NDE3MSBDNDIuOTk5Nyw0OC45ODk4NTM3IDU5LjI3MzcsNTQuMjA2NzgwNSA3NS45NjA1Nzg5LDU0LjIwNjc4MDUgQzkyLjY0NzQ1NzksNTQuMjA2NzgwNSAxMDguOTIxNDU4LDQ4Ljk4OTg1MzcgMTIyLjUzMDY2MywzOS4yNzg0MTcxIEMxMzYuMTM5NDUzLDI5LjU2Njc3MzIgMTQ2LjQxMDI4NCwxNS44NDA2ODI5IDE1MS45MjExNTgsMCBDMTM3LjA4Nzg2OCwxNy42NTM0NjM0IDEwOC41ODI1ODksMjkuNTgwNDU2MSA3NS45NDIyODQyLDI5LjU4MDQ1NjEgTDc1Ljk0MjI4NDIsMjkuNTgwNDU2MSBaIiAvPgogICAgPHBhdGggdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMzczNjgsIDAuNzA0ODc4KSIgZD0iTTc1Ljk3ODQ1NzksMjQuNjI2NDA3MyBDMTA4LjYxODc2MywyNC42MjY0MDczIDEzNy4xMjQ0NTgsMzYuNTUzNDQxNSAxNTEuOTIxMTU4LDU0LjIwNjc4MDUgQzE0Ni40MTAyODQsMzguMzY2MjIyIDEzNi4xMzk0NTMsMjQuNjQwMTMxNyAxMjIuNTMwNjYzLDE0LjkyODQ4NzggQzEwOC45MjE0NTgsNS4yMTY4NDM5IDkyLjY0NzQ1NzksMCA3NS45NjA1Nzg5LDAgQzU5LjI3MzcsMCA0Mi45OTk3LDUuMjE2ODQzOSAyOS4zOTA0OTQ3LDE0LjkyODQ4NzggQzE1Ljc4MTUzODksMjQuNjQwMTMxNyA1LjUxMDgzMjExLDM4LjM2NjIyMiAwLDU0LjIwNjc4MDUgQzE0LjgzMzA4MTYsMzYuNTg5OTI5MyA0My4zMzg1Njg0LDI0LjYyNjQwNzMgNzUuOTc4NDU3OSwyNC42MjY0MDczIEw3NS45Nzg0NTc5LDI0LjYyNjQwNzMgWiIgLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-jupyter: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzkiIGhlaWdodD0iNTEiIHZpZXdCb3g9IjAgMCAzOSA1MSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTYzOCAtMjI4MSkiPgogICAgIDxnIGNsYXNzPSJqcC1qdXB5dGVyLWljb24tY29sb3IiIGZpbGw9IiNGMzc3MjYiPgogICAgICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNjM5Ljc0IDIzMTEuOTgpIiBkPSJNIDE4LjI2NDYgNy4xMzQxMUMgMTAuNDE0NSA3LjEzNDExIDMuNTU4NzIgNC4yNTc2IDAgMEMgMS4zMjUzOSAzLjgyMDQgMy43OTU1NiA3LjEzMDgxIDcuMDY4NiA5LjQ3MzAzQyAxMC4zNDE3IDExLjgxNTIgMTQuMjU1NyAxMy4wNzM0IDE4LjI2OSAxMy4wNzM0QyAyMi4yODIzIDEzLjA3MzQgMjYuMTk2MyAxMS44MTUyIDI5LjQ2OTQgOS40NzMwM0MgMzIuNzQyNCA3LjEzMDgxIDM1LjIxMjYgMy44MjA0IDM2LjUzOCAwQyAzMi45NzA1IDQuMjU3NiAyNi4xMTQ4IDcuMTM0MTEgMTguMjY0NiA3LjEzNDExWiIvPgogICAgICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNjM5LjczIDIyODUuNDgpIiBkPSJNIDE4LjI3MzMgNS45MzkzMUMgMjYuMTIzNSA1LjkzOTMxIDMyLjk3OTMgOC44MTU4MyAzNi41MzggMTMuMDczNEMgMzUuMjEyNiA5LjI1MzAzIDMyLjc0MjQgNS45NDI2MiAyOS40Njk0IDMuNjAwNEMgMjYuMTk2MyAxLjI1ODE4IDIyLjI4MjMgMCAxOC4yNjkgMEMgMTQuMjU1NyAwIDEwLjM0MTcgMS4yNTgxOCA3LjA2ODYgMy42MDA0QyAzLjc5NTU2IDUuOTQyNjIgMS4zMjUzOSA5LjI1MzAzIDAgMTMuMDczNEMgMy41Njc0NSA4LjgyNDYzIDEwLjQyMzIgNS45MzkzMSAxOC4yNzMzIDUuOTM5MzFaIi8+CiAgICA8L2c+CiAgICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNjY5LjMgMjI4MS4zMSkiIGQ9Ik0gNS44OTM1MyAyLjg0NEMgNS45MTg4OSAzLjQzMTY1IDUuNzcwODUgNC4wMTM2NyA1LjQ2ODE1IDQuNTE2NDVDIDUuMTY1NDUgNS4wMTkyMiA0LjcyMTY4IDUuNDIwMTUgNC4xOTI5OSA1LjY2ODUxQyAzLjY2NDMgNS45MTY4OCAzLjA3NDQ0IDYuMDAxNTEgMi40OTgwNSA1LjkxMTcxQyAxLjkyMTY2IDUuODIxOSAxLjM4NDYzIDUuNTYxNyAwLjk1NDg5OCA1LjE2NDAxQyAwLjUyNTE3IDQuNzY2MzMgMC4yMjIwNTYgNC4yNDkwMyAwLjA4MzkwMzcgMy42Nzc1N0MgLTAuMDU0MjQ4MyAzLjEwNjExIC0wLjAyMTIzIDIuNTA2MTcgMC4xNzg3ODEgMS45NTM2NEMgMC4zNzg3OTMgMS40MDExIDAuNzM2ODA5IDAuOTIwODE3IDEuMjA3NTQgMC41NzM1MzhDIDEuNjc4MjYgMC4yMjYyNTkgMi4yNDA1NSAwLjAyNzU5MTkgMi44MjMyNiAwLjAwMjY3MjI5QyAzLjYwMzg5IC0wLjAzMDcxMTUgNC4zNjU3MyAwLjI0OTc4OSA0Ljk0MTQyIDAuNzgyNTUxQyA1LjUxNzExIDEuMzE1MzEgNS44NTk1NiAyLjA1Njc2IDUuODkzNTMgMi44NDRaIi8+CiAgICAgIDxwYXRoIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2MzkuOCAyMzIzLjgxKSIgZD0iTSA3LjQyNzg5IDMuNTgzMzhDIDcuNDYwMDggNC4zMjQzIDcuMjczNTUgNS4wNTgxOSA2Ljg5MTkzIDUuNjkyMTNDIDYuNTEwMzEgNi4zMjYwNyA1Ljk1MDc1IDYuODMxNTYgNS4yODQxMSA3LjE0NDZDIDQuNjE3NDcgNy40NTc2MyAzLjg3MzcxIDcuNTY0MTQgMy4xNDcwMiA3LjQ1MDYzQyAyLjQyMDMyIDcuMzM3MTIgMS43NDMzNiA3LjAwODcgMS4yMDE4NCA2LjUwNjk1QyAwLjY2MDMyOCA2LjAwNTIgMC4yNzg2MSA1LjM1MjY4IDAuMTA1MDE3IDQuNjMyMDJDIC0wLjA2ODU3NTcgMy45MTEzNSAtMC4wMjYyMzYxIDMuMTU0OTQgMC4yMjY2NzUgMi40NTg1NkMgMC40Nzk1ODcgMS43NjIxNyAwLjkzMTY5NyAxLjE1NzEzIDEuNTI1NzYgMC43MjAwMzNDIDIuMTE5ODMgMC4yODI5MzUgMi44MjkxNCAwLjAzMzQzOTUgMy41NjM4OSAwLjAwMzEzMzQ0QyA0LjU0NjY3IC0wLjAzNzQwMzMgNS41MDUyOSAwLjMxNjcwNiA2LjIyOTYxIDAuOTg3ODM1QyA2Ljk1MzkzIDEuNjU4OTYgNy4zODQ4NCAyLjU5MjM1IDcuNDI3ODkgMy41ODMzOEwgNy40Mjc4OSAzLjU4MzM4WiIvPgogICAgICA8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNjM4LjM2IDIyODYuMDYpIiBkPSJNIDIuMjc0NzEgNC4zOTYyOUMgMS44NDM2MyA0LjQxNTA4IDEuNDE2NzEgNC4zMDQ0NSAxLjA0Nzk5IDQuMDc4NDNDIDAuNjc5MjY4IDMuODUyNCAwLjM4NTMyOCAzLjUyMTE0IDAuMjAzMzcxIDMuMTI2NTZDIDAuMDIxNDEzNiAyLjczMTk4IC0wLjA0MDM3OTggMi4yOTE4MyAwLjAyNTgxMTYgMS44NjE4MUMgMC4wOTIwMDMxIDEuNDMxOCAwLjI4MzIwNCAxLjAzMTI2IDAuNTc1MjEzIDAuNzEwODgzQyAwLjg2NzIyMiAwLjM5MDUxIDEuMjQ2OTEgMC4xNjQ3MDggMS42NjYyMiAwLjA2MjA1OTJDIDIuMDg1NTMgLTAuMDQwNTg5NyAyLjUyNTYxIC0wLjAxNTQ3MTQgMi45MzA3NiAwLjEzNDIzNUMgMy4zMzU5MSAwLjI4Mzk0MSAzLjY4NzkyIDAuNTUxNTA1IDMuOTQyMjIgMC45MDMwNkMgNC4xOTY1MiAxLjI1NDYyIDQuMzQxNjkgMS42NzQzNiA0LjM1OTM1IDIuMTA5MTZDIDQuMzgyOTkgMi42OTEwNyA0LjE3Njc4IDMuMjU4NjkgMy43ODU5NyAzLjY4NzQ2QyAzLjM5NTE2IDQuMTE2MjQgMi44NTE2NiA0LjM3MTE2IDIuMjc0NzEgNC4zOTYyOUwgMi4yNzQ3MSA0LjM5NjI5WiIvPgogICAgPC9nPgogIDwvZz4+Cjwvc3ZnPgo=);
--jp-icon-jupyterlab-wordmark: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIHZpZXdCb3g9IjAgMCAxODYwLjggNDc1Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjIiIGZpbGw9IiM0RTRFNEUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQ4MC4xMzY0MDEsIDY0LjI3MTQ5MykiPgogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMDAwMDAsIDU4Ljg3NTU2NikiPgogICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjA4NzYwMywgMC4xNDAyOTQpIj4KICAgICAgICA8cGF0aCBkPSJNLTQyNi45LDE2OS44YzAsNDguNy0zLjcsNjQuNy0xMy42LDc2LjRjLTEwLjgsMTAtMjUsMTUuNS0zOS43LDE1LjVsMy43LDI5IGMyMi44LDAuMyw0NC44LTcuOSw2MS45LTIzLjFjMTcuOC0xOC41LDI0LTQ0LjEsMjQtODMuM1YwSC00Mjd2MTcwLjFMLTQyNi45LDE2OS44TC00MjYuOSwxNjkuOHoiLz4KICAgICAgPC9nPgogICAgPC9nPgogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTU1LjA0NTI5NiwgNTYuODM3MTA0KSI+CiAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuNTYyNDUzLCAxLjc5OTg0MikiPgogICAgICAgIDxwYXRoIGQ9Ik0tMzEyLDE0OGMwLDIxLDAsMzkuNSwxLjcsNTUuNGgtMzEuOGwtMi4xLTMzLjNoLTAuOGMtNi43LDExLjYtMTYuNCwyMS4zLTI4LDI3LjkgYy0xMS42LDYuNi0yNC44LDEwLTM4LjIsOS44Yy0zMS40LDAtNjktMTcuNy02OS04OVYwaDM2LjR2MTEyLjdjMCwzOC43LDExLjYsNjQuNyw0NC42LDY0LjdjMTAuMy0wLjIsMjAuNC0zLjUsMjguOS05LjQgYzguNS01LjksMTUuMS0xNC4zLDE4LjktMjMuOWMyLjItNi4xLDMuMy0xMi41LDMuMy0xOC45VjAuMmgzNi40VjE0OEgtMzEyTC0zMTIsMTQ4eiIvPgogICAgICA8L2c+CiAgICA8L2c+CiAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzOTAuMDEzMzIyLCA1My40Nzk2MzgpIj4KICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS43MDY0NTgsIDAuMjMxNDI1KSI+CiAgICAgICAgPHBhdGggZD0iTS00NzguNiw3MS40YzAtMjYtMC44LTQ3LTEuNy02Ni43aDMyLjdsMS43LDM0LjhoMC44YzcuMS0xMi41LDE3LjUtMjIuOCwzMC4xLTI5LjcgYzEyLjUtNywyNi43LTEwLjMsNDEtOS44YzQ4LjMsMCw4NC43LDQxLjcsODQuNywxMDMuM2MwLDczLjEtNDMuNywxMDkuMi05MSwxMDkuMmMtMTIuMSwwLjUtMjQuMi0yLjItMzUtNy44IGMtMTAuOC01LjYtMTkuOS0xMy45LTI2LjYtMjQuMmgtMC44VjI5MWgtMzZ2LTIyMEwtNDc4LjYsNzEuNEwtNDc4LjYsNzEuNHogTS00NDIuNiwxMjUuNmMwLjEsNS4xLDAuNiwxMC4xLDEuNywxNS4xIGMzLDEyLjMsOS45LDIzLjMsMTkuOCwzMS4xYzkuOSw3LjgsMjIuMSwxMi4xLDM0LjcsMTIuMWMzOC41LDAsNjAuNy0zMS45LDYwLjctNzguNWMwLTQwLjctMjEuMS03NS42LTU5LjUtNzUuNiBjLTEyLjksMC40LTI1LjMsNS4xLTM1LjMsMTMuNGMtOS45LDguMy0xNi45LDE5LjctMTkuNiwzMi40Yy0xLjUsNC45LTIuMywxMC0yLjUsMTUuMVYxMjUuNkwtNDQyLjYsMTI1LjZMLTQ0Mi42LDEyNS42eiIvPgogICAgICA8L2c+CiAgICA8L2c+CiAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2MDYuNzQwNzI2LCA1Ni44MzcxMDQpIj4KICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC43NTEyMjYsIDEuOTg5Mjk5KSI+CiAgICAgICAgPHBhdGggZD0iTS00NDAuOCwwbDQzLjcsMTIwLjFjNC41LDEzLjQsOS41LDI5LjQsMTIuOCw0MS43aDAuOGMzLjctMTIuMiw3LjktMjcuNywxMi44LTQyLjQgbDM5LjctMTE5LjJoMzguNUwtMzQ2LjksMTQ1Yy0yNiw2OS43LTQzLjcsMTA1LjQtNjguNiwxMjcuMmMtMTIuNSwxMS43LTI3LjksMjAtNDQuNiwyMy45bC05LjEtMzEuMSBjMTEuNy0zLjksMjIuNS0xMC4xLDMxLjgtMTguMWMxMy4yLTExLjEsMjMuNy0yNS4yLDMwLjYtNDEuMmMxLjUtMi44LDIuNS01LjcsMi45LTguOGMtMC4zLTMuMy0xLjItNi42LTIuNS05LjdMLTQ4MC4yLDAuMSBoMzkuN0wtNDQwLjgsMEwtNDQwLjgsMHoiLz4KICAgICAgPC9nPgogICAgPC9nPgogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODIyLjc0ODEwNCwgMC4wMDAwMDApIj4KICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMS40NjQwNTAsIDAuMzc4OTE0KSI+CiAgICAgICAgPHBhdGggZD0iTS00MTMuNywwdjU4LjNoNTJ2MjguMmgtNTJWMTk2YzAsMjUsNywzOS41LDI3LjMsMzkuNWM3LjEsMC4xLDE0LjItMC43LDIxLjEtMi41IGwxLjcsMjcuN2MtMTAuMywzLjctMjEuMyw1LjQtMzIuMiw1Yy03LjMsMC40LTE0LjYtMC43LTIxLjMtMy40Yy02LjgtMi43LTEyLjktNi44LTE3LjktMTIuMWMtMTAuMy0xMC45LTE0LjEtMjktMTQuMS01Mi45IFY4Ni41aC0zMVY1OC4zaDMxVjkuNkwtNDEzLjcsMEwtNDEzLjcsMHoiLz4KICAgICAgPC9nPgogICAgPC9nPgogICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTc0LjQzMzI4NiwgNTMuNDc5NjM4KSI+CiAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuOTkwMDM0LCAwLjYxMDMzOSkiPgogICAgICAgIDxwYXRoIGQ9Ik0tNDQ1LjgsMTEzYzAuOCw1MCwzMi4yLDcwLjYsNjguNiw3MC42YzE5LDAuNiwzNy45LTMsNTUuMy0xMC41bDYuMiwyNi40IGMtMjAuOSw4LjktNDMuNSwxMy4xLTY2LjIsMTIuNmMtNjEuNSwwLTk4LjMtNDEuMi05OC4zLTEwMi41Qy00ODAuMiw0OC4yLTQ0NC43LDAtMzg2LjUsMGM2NS4yLDAsODIuNyw1OC4zLDgyLjcsOTUuNyBjLTAuMSw1LjgtMC41LDExLjUtMS4yLDE3LjJoLTE0MC42SC00NDUuOEwtNDQ1LjgsMTEzeiBNLTMzOS4yLDg2LjZjMC40LTIzLjUtOS41LTYwLjEtNTAuNC02MC4xIGMtMzYuOCwwLTUyLjgsMzQuNC01NS43LDYwLjFILTMzOS4yTC0zMzkuMiw4Ni42TC0zMzkuMiw4Ni42eiIvPgogICAgICA8L2c+CiAgICA8L2c+CiAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjAxLjk2MTA1OCwgNTMuNDc5NjM4KSI+CiAgICAgIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEuMTc5NjQwLCAwLjcwNTA2OCkiPgogICAgICAgIDxwYXRoIGQ9Ik0tNDc4LjYsNjhjMC0yMy45LTAuNC00NC41LTEuNy02My40aDMxLjhsMS4yLDM5LjloMS43YzkuMS0yNy4zLDMxLTQ0LjUsNTUuMy00NC41IGMzLjUtMC4xLDcsMC40LDEwLjMsMS4ydjM0LjhjLTQuMS0wLjktOC4yLTEuMy0xMi40LTEuMmMtMjUuNiwwLTQzLjcsMTkuNy00OC43LDQ3LjRjLTEsNS43LTEuNiwxMS41LTEuNywxNy4ydjEwOC4zaC0zNlY2OCBMLTQ3OC42LDY4eiIvPgogICAgICA8L2c+CiAgICA8L2c+CiAgPC9nPgoKICA8ZyBjbGFzcz0ianAtaWNvbi13YXJuMCIgZmlsbD0iI0YzNzcyNiI+CiAgICA8cGF0aCBkPSJNMTM1Mi4zLDMyNi4yaDM3VjI4aC0zN1YzMjYuMnogTTE2MDQuOCwzMjYuMmMtMi41LTEzLjktMy40LTMxLjEtMy40LTQ4Ljd2LTc2IGMwLTQwLjctMTUuMS04My4xLTc3LjMtODMuMWMtMjUuNiwwLTUwLDcuMS02Ni44LDE4LjFsOC40LDI0LjRjMTQuMy05LjIsMzQtMTUuMSw1My0xNS4xYzQxLjYsMCw0Ni4yLDMwLjIsNDYuMiw0N3Y0LjIgYy03OC42LTAuNC0xMjIuMywyNi41LTEyMi4zLDc1LjZjMCwyOS40LDIxLDU4LjQsNjIuMiw1OC40YzI5LDAsNTAuOS0xNC4zLDYyLjItMzAuMmgxLjNsMi45LDI1LjZIMTYwNC44eiBNMTU2NS43LDI1Ny43IGMwLDMuOC0wLjgsOC0yLjEsMTEuOGMtNS45LDE3LjItMjIuNywzNC00OS4yLDM0Yy0xOC45LDAtMzQuOS0xMS4zLTM0LjktMzUuM2MwLTM5LjUsNDUuOC00Ni42LDg2LjItNDUuOFYyNTcuN3ogTTE2OTguNSwzMjYuMiBsMS43LTMzLjZoMS4zYzE1LjEsMjYuOSwzOC43LDM4LjIsNjguMSwzOC4yYzQ1LjQsMCw5MS4yLTM2LjEsOTEuMi0xMDguOGMwLjQtNjEuNy0zNS4zLTEwMy43LTg1LjctMTAzLjcgYy0zMi44LDAtNTYuMywxNC43LTY5LjMsMzcuNGgtMC44VjI4aC0zNi42djI0NS43YzAsMTguMS0wLjgsMzguNi0xLjcsNTIuNUgxNjk4LjV6IE0xNzA0LjgsMjA4LjJjMC01LjksMS4zLTEwLjksMi4xLTE1LjEgYzcuNi0yOC4xLDMxLjEtNDUuNCw1Ni4zLTQ1LjRjMzkuNSwwLDYwLjUsMzQuOSw2MC41LDc1LjZjMCw0Ni42LTIzLjEsNzguMS02MS44LDc4LjFjLTI2LjksMC00OC4zLTE3LjYtNTUuNS00My4zIGMtMC44LTQuMi0xLjctOC44LTEuNy0xMy40VjIwOC4yeiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-kernel: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxwYXRoIGNsYXNzPSJqcC1pY29uMiIgZmlsbD0iIzYxNjE2MSIgZD0iTTE1IDlIOXY2aDZWOXptLTIgNGgtMnYtMmgydjJ6bTgtMlY5aC0yVjdjMC0xLjEtLjktMi0yLTJoLTJWM2gtMnYyaC0yVjNIOXYySDdjLTEuMSAwLTIgLjktMiAydjJIM3YyaDJ2MkgzdjJoMnYyYzAgMS4xLjkgMiAyIDJoMnYyaDJ2LTJoMnYyaDJ2LTJoMmMxLjEgMCAyLS45IDItMnYtMmgydi0yaC0ydi0yaDJ6bS00IDZIN1Y3aDEwdjEweiIvPgo8L3N2Zz4K);
--jp-icon-keyboard: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIiBkPSJNMjAgNUg0Yy0xLjEgMC0xLjk5LjktMS45OSAyTDIgMTdjMCAxLjEuOSAyIDIgMmgxNmMxLjEgMCAyLS45IDItMlY3YzAtMS4xLS45LTItMi0yem0tOSAzaDJ2MmgtMlY4em0wIDNoMnYyaC0ydi0yek04IDhoMnYySDhWOHptMCAzaDJ2Mkg4di0yem0tMSAySDV2LTJoMnYyem0wLTNINVY4aDJ2MnptOSA3SDh2LTJoOHYyem0wLTRoLTJ2LTJoMnYyem0wLTNoLTJWOGgydjJ6bTMgM2gtMnYtMmgydjJ6bTAtM2gtMlY4aDJ2MnoiLz4KPC9zdmc+Cg==);
--jp-icon-launch: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMzIgMzIiIHdpZHRoPSIzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIj4KICAgIDxwYXRoIGQ9Ik0yNiwyOEg2YTIuMDAyNywyLjAwMjcsMCwwLDEtMi0yVjZBMi4wMDI3LDIuMDAyNywwLDAsMSw2LDRIMTZWNkg2VjI2SDI2VjE2aDJWMjZBMi4wMDI3LDIuMDAyNywwLDAsMSwyNiwyOFoiLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjAgMiAyMCA0IDI2LjU4NiA0IDE4IDEyLjU4NiAxOS40MTQgMTQgMjggNS40MTQgMjggMTIgMzAgMTIgMzAgMiAyMCAyIi8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-launcher: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIiBkPSJNMTkgMTlINVY1aDdWM0g1YTIgMiAwIDAwLTIgMnYxNGEyIDIgMCAwMDIgMmgxNGMxLjEgMCAyLS45IDItMnYtN2gtMnY3ek0xNCAzdjJoMy41OWwtOS44MyA5LjgzIDEuNDEgMS40MUwxOSA2LjQxVjEwaDJWM2gtN3oiLz4KPC9zdmc+Cg==);
--jp-icon-line-form: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxwYXRoIGZpbGw9IndoaXRlIiBkPSJNNS44OCA0LjEyTDEzLjc2IDEybC03Ljg4IDcuODhMOCAyMmwxMC0xMEw4IDJ6Ii8+Cjwvc3ZnPgo=);
--jp-icon-link: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTMuOSAxMmMwLTEuNzEgMS4zOS0zLjEgMy4xLTMuMWg0VjdIN2MtMi43NiAwLTUgMi4yNC01IDVzMi4yNCA1IDUgNWg0di0xLjlIN2MtMS43MSAwLTMuMS0xLjM5LTMuMS0zLjF6TTggMTNoOHYtMkg4djJ6bTktNmgtNHYxLjloNGMxLjcxIDAgMy4xIDEuMzkgMy4xIDMuMXMtMS4zOSAzLjEtMy4xIDMuMWgtNFYxN2g0YzIuNzYgMCA1LTIuMjQgNS01cy0yLjI0LTUtNS01eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-list: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxwYXRoIGNsYXNzPSJqcC1pY29uMiBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiM2MTYxNjEiIGQ9Ik0xOSA1djE0SDVWNWgxNG0xLjEtMkgzLjljLS41IDAtLjkuNC0uOS45djE2LjJjMCAuNC40LjkuOS45aDE2LjJjLjQgMCAuOS0uNS45LS45VjMuOWMwLS41LS41LS45LS45LS45ek0xMSA3aDZ2MmgtNlY3em0wIDRoNnYyaC02di0yem0wIDRoNnYyaC02ek03IDdoMnYySDd6bTAgNGgydjJIN3ptMCA0aDJ2Mkg3eiIvPgo8L3N2Zz4=);
--jp-icon-listings-info: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MC45NzggNTAuOTc4IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MC45NzggNTAuOTc4OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+Cgk8Zz4KCQk8cGF0aCBzdHlsZT0iZmlsbDojMDEwMDAyOyIgZD0iTTQzLjUyLDcuNDU4QzM4LjcxMSwyLjY0OCwzMi4zMDcsMCwyNS40ODksMEMxOC42NywwLDEyLjI2NiwyLjY0OCw3LjQ1OCw3LjQ1OAoJCQljLTkuOTQzLDkuOTQxLTkuOTQzLDI2LjExOSwwLDM2LjA2MmM0LjgwOSw0LjgwOSwxMS4yMTIsNy40NTYsMTguMDMxLDcuNDU4YzAsMCwwLjAwMSwwLDAuMDAyLDAKCQkJYzYuODE2LDAsMTMuMjIxLTIuNjQ4LDE4LjAyOS03LjQ1OGM0LjgwOS00LjgwOSw3LjQ1Ny0xMS4yMTIsNy40NTctMTguMDNDNTAuOTc3LDE4LjY3LDQ4LjMyOCwxMi4yNjYsNDMuNTIsNy40NTh6CgkJCSBNNDIuMTA2LDQyLjEwNWMtNC40MzIsNC40MzEtMTAuMzMyLDYuODcyLTE2LjYxNSw2Ljg3MmgtMC4wMDJjLTYuMjg1LTAuMDAxLTEyLjE4Ny0yLjQ0MS0xNi42MTctNi44NzIKCQkJYy05LjE2Mi05LjE2My05LjE2Mi0yNC4wNzEsMC0zMy4yMzNDMTMuMzAzLDQuNDQsMTkuMjA0LDIsMjUuNDg5LDJjNi4yODQsMCwxMi4xODYsMi40NCwxNi42MTcsNi44NzIKCQkJYzQuNDMxLDQuNDMxLDYuODcxLDEwLjMzMiw2Ljg3MSwxNi42MTdDNDguOTc3LDMxLjc3Miw0Ni41MzYsMzcuNjc1LDQyLjEwNiw0Mi4xMDV6Ii8+CgkJPHBhdGggc3R5bGU9ImZpbGw6IzAxMDAwMjsiIGQ9Ik0yMy41NzgsMzIuMjE4Yy0wLjAyMy0xLjczNCwwLjE0My0zLjA1OSwwLjQ5Ni0zLjk3MmMwLjM1My0wLjkxMywxLjExLTEuOTk3LDIuMjcyLTMuMjUzCgkJCWMwLjQ2OC0wLjUzNiwwLjkyMy0xLjA2MiwxLjM2Ny0xLjU3NWMwLjYyNi0wLjc1MywxLjEwNC0xLjQ3OCwxLjQzNi0yLjE3NWMwLjMzMS0wLjcwNywwLjQ5NS0xLjU0MSwwLjQ5NS0yLjUKCQkJYzAtMS4wOTYtMC4yNi0yLjA4OC0wLjc3OS0yLjk3OWMtMC41NjUtMC44NzktMS41MDEtMS4zMzYtMi44MDYtMS4zNjljLTEuODAyLDAuMDU3LTIuOTg1LDAuNjY3LTMuNTUsMS44MzIKCQkJYy0wLjMwMSwwLjUzNS0wLjUwMywxLjE0MS0wLjYwNywxLjgxNGMtMC4xMzksMC43MDctMC4yMDcsMS40MzItMC4yMDcsMi4xNzRoLTIuOTM3Yy0wLjA5MS0yLjIwOCwwLjQwNy00LjExNCwxLjQ5My01LjcxOQoJCQljMS4wNjItMS42NCwyLjg1NS0yLjQ4MSw1LjM3OC0yLjUyN2MyLjE2LDAuMDIzLDMuODc0LDAuNjA4LDUuMTQxLDEuNzU4YzEuMjc4LDEuMTYsMS45MjksMi43NjQsMS45NSw0LjgxMQoJCQljMCwxLjE0Mi0wLjEzNywyLjExMS0wLjQxLDIuOTExYy0wLjMwOSwwLjg0NS0wLjczMSwxLjU5My0xLjI2OCwyLjI0M2MtMC40OTIsMC42NS0xLjA2OCwxLjMxOC0xLjczLDIuMDAyCgkJCWMtMC42NSwwLjY5Ny0xLjMxMywxLjQ3OS0xLjk4NywyLjM0NmMtMC4yMzksMC4zNzctMC40MjksMC43NzctMC41NjUsMS4xOTljLTAuMTYsMC45NTktMC4yMTcsMS45NTEtMC4xNzEsMi45NzkKCQkJQzI2LjU4OSwzMi4yMTgsMjMuNTc4LDMyLjIxOCwyMy41NzgsMzIuMjE4eiBNMjMuNTc4LDM4LjIydi0zLjQ4NGgzLjA3NnYzLjQ4NEgyMy41Nzh6Ii8+Cgk8L2c+Cjwvc3ZnPgo=);
--jp-icon-markdown: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8cGF0aCBjbGFzcz0ianAtaWNvbi1jb250cmFzdDAganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjN0IxRkEyIiBkPSJNNSAxNC45aDEybC02LjEgNnptOS40LTYuOGMwLTEuMy0uMS0yLjktLjEtNC41LS40IDEuNC0uOSAyLjktMS4zIDQuM2wtMS4zIDQuM2gtMkw4LjUgNy45Yy0uNC0xLjMtLjctMi45LTEtNC4zLS4xIDEuNi0uMSAzLjItLjIgNC42TDcgMTIuNEg0LjhsLjctMTFoMy4zTDEwIDVjLjQgMS4yLjcgMi43IDEgMy45LjMtMS4yLjctMi42IDEtMy45bDEuMi0zLjdoMy4zbC42IDExaC0yLjRsLS4zLTQuMnoiLz4KPC9zdmc+Cg==);
--jp-icon-move-down: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggY2xhc3M9ImpwLWljb24zIiBkPSJNMTIuNDcxIDcuNTI4OTlDMTIuNzYzMiA3LjIzNjg0IDEyLjc2MzIgNi43NjMxNiAxMi40NzEgNi40NzEwMVY2LjQ3MTAxQzEyLjE3OSA2LjE3OTA1IDExLjcwNTcgNi4xNzg4NCAxMS40MTM1IDYuNDcwNTRMNy43NSAxMC4xMjc1VjEuNzVDNy43NSAxLjMzNTc5IDcuNDE0MjEgMSA3IDFWMUM2LjU4NTc5IDEgNi4yNSAxLjMzNTc5IDYuMjUgMS43NVYxMC4xMjc1TDIuNTk3MjYgNi40NjgyMkMyLjMwMzM4IDYuMTczODEgMS44MjY0MSA2LjE3MzU5IDEuNTMyMjYgNi40Njc3NFY2LjQ2Nzc0QzEuMjM4MyA2Ljc2MTcgMS4yMzgzIDcuMjM4MyAxLjUzMjI2IDcuNTMyMjZMNi4yOTI4OSAxMi4yOTI5QzYuNjgzNDIgMTIuNjgzNCA3LjMxNjU4IDEyLjY4MzQgNy43MDcxMSAxMi4yOTI5TDEyLjQ3MSA3LjUyODk5WiIgZmlsbD0iIzYxNjE2MSIvPgo8L3N2Zz4K);
--jp-icon-move-up: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggY2xhc3M9ImpwLWljb24zIiBkPSJNMS41Mjg5OSA2LjQ3MTAxQzEuMjM2ODQgNi43NjMxNiAxLjIzNjg0IDcuMjM2ODQgMS41Mjg5OSA3LjUyODk5VjcuNTI4OTlDMS44MjA5NSA3LjgyMDk1IDIuMjk0MjYgNy44MjExNiAyLjU4NjQ5IDcuNTI5NDZMNi4yNSAzLjg3MjVWMTIuMjVDNi4yNSAxMi42NjQyIDYuNTg1NzkgMTMgNyAxM1YxM0M3LjQxNDIxIDEzIDcuNzUgMTIuNjY0MiA3Ljc1IDEyLjI1VjMuODcyNUwxMS40MDI3IDcuNTMxNzhDMTEuNjk2NiA3LjgyNjE5IDEyLjE3MzYgNy44MjY0MSAxMi40Njc3IDcuNTMyMjZWNy41MzIyNkMxMi43NjE3IDcuMjM4MyAxMi43NjE3IDYuNzYxNyAxMi40Njc3IDYuNDY3NzRMNy43MDcxMSAxLjcwNzExQzcuMzE2NTggMS4zMTY1OCA2LjY4MzQyIDEuMzE2NTggNi4yOTI4OSAxLjcwNzExTDEuNTI4OTkgNi40NzEwMVoiIGZpbGw9IiM2MTYxNjEiLz4KPC9zdmc+Cg==);
--jp-icon-new-folder: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTIwIDZoLThsLTItMkg0Yy0xLjExIDAtMS45OS44OS0xLjk5IDJMMiAxOGMwIDEuMTEuODkgMiAyIDJoMTZjMS4xMSAwIDItLjg5IDItMlY4YzAtMS4xMS0uODktMi0yLTJ6bS0xIDhoLTN2M2gtMnYtM2gtM3YtMmgzVjloMnYzaDN2MnoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-not-trusted: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI1IDI1Ij4KICAgIDxwYXRoIGNsYXNzPSJqcC1pY29uMiIgc3Ryb2tlPSIjMzMzMzMzIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMgMykiIGQ9Ik0xLjg2MDk0IDExLjQ0MDlDMC44MjY0NDggOC43NzAyNyAwLjg2Mzc3OSA2LjA1NzY0IDEuMjQ5MDcgNC4xOTkzMkMyLjQ4MjA2IDMuOTMzNDcgNC4wODA2OCAzLjQwMzQ3IDUuNjAxMDIgMi44NDQ5QzcuMjM1NDkgMi4yNDQ0IDguODU2NjYgMS41ODE1IDkuOTg3NiAxLjA5NTM5QzExLjA1OTcgMS41ODM0MSAxMi42MDk0IDIuMjQ0NCAxNC4yMTggMi44NDMzOUMxNS43NTAzIDMuNDEzOTQgMTcuMzk5NSAzLjk1MjU4IDE4Ljc1MzkgNC4yMTM4NUMxOS4xMzY0IDYuMDcxNzcgMTkuMTcwOSA4Ljc3NzIyIDE4LjEzOSAxMS40NDA5QzE3LjAzMDMgMTQuMzAzMiAxNC42NjY4IDE3LjE4NDQgOS45OTk5OSAxOC45MzU0QzUuMzMzMTkgMTcuMTg0NCAyLjk2OTY4IDE0LjMwMzIgMS44NjA5NCAxMS40NDA5WiIvPgogICAgPHBhdGggY2xhc3M9ImpwLWljb24yIiBzdHJva2U9IiMzMzMzMzMiIHN0cm9rZS13aWR0aD0iMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOS4zMTU5MiA5LjMyMDMxKSIgZD0iTTcuMzY4NDIgMEwwIDcuMzY0NzkiLz4KICAgIDxwYXRoIGNsYXNzPSJqcC1pY29uMiIgc3Ryb2tlPSIjMzMzMzMzIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDkuMzE1OTIgMTYuNjgzNikgc2NhbGUoMSAtMSkiIGQ9Ik03LjM2ODQyIDBMMCA3LjM2NDc5Ii8+Cjwvc3ZnPgo=);
--jp-icon-notebook: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8ZyBjbGFzcz0ianAtbm90ZWJvb2staWNvbi1jb2xvciBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiNFRjZDMDAiPgogICAgPHBhdGggZD0iTTE4LjcgMy4zdjE1LjRIMy4zVjMuM2gxNS40bTEuNS0xLjVIMS44djE4LjNoMTguM2wuMS0xOC4zeiIvPgogICAgPHBhdGggZD0iTTE2LjUgMTYuNWwtNS40LTQuMy01LjYgNC4zdi0xMWgxMXoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-numbering: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyOCAyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CgkJPHBhdGggZD0iTTQgMTlINlYxOS41SDVWMjAuNUg2VjIxSDRWMjJIN1YxOEg0VjE5Wk01IDEwSDZWNkg0VjdINVYxMFpNNCAxM0g1LjhMNCAxNS4xVjE2SDdWMTVINS4yTDcgMTIuOVYxMkg0VjEzWk05IDdWOUgyM1Y3SDlaTTkgMjFIMjNWMTlIOVYyMVpNOSAxNUgyM1YxM0g5VjE1WiIvPgoJPC9nPgo8L3N2Zz4K);
--jp-icon-offline-bolt: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTEyIDIuMDJjLTUuNTEgMC05Ljk4IDQuNDctOS45OCA5Ljk4czQuNDcgOS45OCA5Ljk4IDkuOTggOS45OC00LjQ3IDkuOTgtOS45OFMxNy41MSAyLjAyIDEyIDIuMDJ6TTExLjQ4IDIwdi02LjI2SDhMMTMgNHY2LjI2aDMuMzVMMTEuNDggMjB6Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-palette: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTE4IDEzVjIwSDRWNkg5LjAyQzkuMDcgNS4yOSA5LjI0IDQuNjIgOS41IDRINEMyLjkgNCAyIDQuOSAyIDZWMjBDMiAyMS4xIDIuOSAyMiA0IDIySDE4QzE5LjEgMjIgMjAgMjEuMSAyMCAyMFYxNUwxOCAxM1pNMTkuMyA4Ljg5QzE5Ljc0IDguMTkgMjAgNy4zOCAyMCA2LjVDMjAgNC4wMSAxNy45OSAyIDE1LjUgMkMxMy4wMSAyIDExIDQuMDEgMTEgNi41QzExIDguOTkgMTMuMDEgMTEgMTUuNDkgMTFDMTYuMzcgMTEgMTcuMTkgMTAuNzQgMTcuODggMTAuM0wyMSAxMy40MkwyMi40MiAxMkwxOS4zIDguODlaTTE1LjUgOUMxNC4xMiA5IDEzIDcuODggMTMgNi41QzEzIDUuMTIgMTQuMTIgNCAxNS41IDRDMTYuODggNCAxOCA1LjEyIDE4IDYuNUMxOCA3Ljg4IDE2Ljg4IDkgMTUuNSA5WiIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00IDZIOS4wMTg5NEM5LjAwNjM5IDYuMTY1MDIgOSA2LjMzMTc2IDkgNi41QzkgOC44MTU3NyAxMC4yMTEgMTAuODQ4NyAxMi4wMzQzIDEySDlWMTRIMTZWMTIuOTgxMUMxNi41NzAzIDEyLjkzNzcgMTcuMTIgMTIuODIwNyAxNy42Mzk2IDEyLjYzOTZMMTggMTNWMjBINFY2Wk04IDhINlYxMEg4VjhaTTYgMTJIOFYxNEg2VjEyWk04IDE2SDZWMThIOFYxNlpNOSAxNkgxNlYxOEg5VjE2WiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-paste: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTE5IDJoLTQuMThDMTQuNC44NCAxMy4zIDAgMTIgMGMtMS4zIDAtMi40Ljg0LTIuODIgMkg1Yy0xLjEgMC0yIC45LTIgMnYxNmMwIDEuMS45IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjRjMC0xLjEtLjktMi0yLTJ6bS03IDBjLjU1IDAgMSAuNDUgMSAxcy0uNDUgMS0xIDEtMS0uNDUtMS0xIC40NS0xIDEtMXptNyAxOEg1VjRoMnYzaDEwVjRoMnYxNnoiLz4KICAgIDwvZz4KPC9zdmc+Cg==);
--jp-icon-pdf: url(data:image/svg+xml;base64,PHN2ZwogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMiAyMiIgd2lkdGg9IjE2Ij4KICAgIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKDQ1KSIgY2xhc3M9ImpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iI0ZGMkEyQSIKICAgICAgIGQ9Im0gMjIuMzQ0MzY5LC0zLjAxNjM2NDIgaCA1LjYzODYwNCB2IDEuNTc5MjQzMyBoIC0zLjU0OTIyNyB2IDEuNTA4NjkyOTkgaCAzLjMzNzU3NiBWIDEuNjUwODE1NCBoIC0zLjMzNzU3NiB2IDMuNDM1MjYxMyBoIC0yLjA4OTM3NyB6IG0gLTcuMTM2NDQ0LDEuNTc5MjQzMyB2IDQuOTQzOTU0MyBoIDAuNzQ4OTIgcSAxLjI4MDc2MSwwIDEuOTUzNzAzLC0wLjYzNDk1MzUgMC42NzgzNjksLTAuNjM0OTUzNSAwLjY3ODM2OSwtMS44NDUxNjQxIDAsLTEuMjA0NzgzNTUgLTAuNjcyOTQyLC0xLjgzNDMxMDExIC0wLjY3Mjk0MiwtMC42Mjk1MjY1OSAtMS45NTkxMywtMC42Mjk1MjY1OSB6IG0gLTIuMDg5Mzc3LC0xLjU3OTI0MzMgaCAyLjIwMzM0MyBxIDEuODQ1MTY0LDAgMi43NDYwMzksMC4yNjU5MjA3IDAuOTA2MzAxLDAuMjYwNDkzNyAxLjU1MjEwOCwwLjg5MDAyMDMgMC41Njk4MywwLjU0ODEyMjMgMC44NDY2MDUsMS4yNjQ0ODAwNiAwLjI3Njc3NCwwLjcxNjM1NzgxIDAuMjc2Nzc0LDEuNjIyNjU4OTQgMCwwLjkxNzE1NTEgLTAuMjc2Nzc0LDEuNjM4OTM5OSAtMC4yNzY3NzUsMC43MTYzNTc4IC0wLjg0NjYwNSwxLjI2NDQ4IC0wLjY1MTIzNCwwLjYyOTUyNjYgLTEuNTYyOTYyLDAuODk1NDQ3MyAtMC45MTE3MjgsMC4yNjA0OTM3IC0yLjczNTE4NSwwLjI2MDQ5MzcgaCAtMi4yMDMzNDMgeiBtIC04LjE0NTg1NjUsMCBoIDMuNDY3ODIzIHEgMS41NDY2ODE2LDAgMi4zNzE1Nzg1LDAuNjg5MjIzIDAuODMwMzI0LDAuNjgzNzk2MSAwLjgzMDMyNCwxLjk1MzcwMzE0IDAsMS4yNzUzMzM5NyAtMC44MzAzMjQsMS45NjQ1NTcwNiBRIDkuOTg3MTk2MSwyLjI3NDkxNSA4LjQ0MDUxNDUsMi4yNzQ5MTUgSCA3LjA2MjA2ODQgViA1LjA4NjA3NjcgSCA0Ljk3MjY5MTUgWiBtIDIuMDg5Mzc2OSwxLjUxNDExOTkgdiAyLjI2MzAzOTQzIGggMS4xNTU5NDEgcSAwLjYwNzgxODgsMCAwLjkzODg2MjksLTAuMjkzMDU1NDcgMC4zMzEwNDQxLC0wLjI5ODQ4MjQxIDAuMzMxMDQ0MSwtMC44NDExNzc3MiAwLC0wLjU0MjY5NTMxIC0wLjMzMTA0NDEsLTAuODM1NzUwNzQgLTAuMzMxMDQ0MSwtMC4yOTMwNTU1IC0wLjkzODg2MjksLTAuMjkzMDU1NSB6IgovPgo8L3N2Zz4K);
--jp-icon-python: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iLTEwIC0xMCAxMzEuMTYxMzYxNjk0MzM1OTQgMTMyLjM4ODk5OTkzODk2NDg0Ij4KICA8cGF0aCBjbGFzcz0ianAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjMzA2OTk4IiBkPSJNIDU0LjkxODc4NSw5LjE5Mjc0MjFlLTQgQyA1MC4zMzUxMzIsMC4wMjIyMTcyNyA0NS45NTc4NDYsMC40MTMxMzY5NyA0Mi4xMDYyODUsMS4wOTQ2NjkzIDMwLjc2MDA2OSwzLjA5OTE3MzEgMjguNzAwMDM2LDcuMjk0NzcxNCAyOC43MDAwMzUsMTUuMDMyMTY5IHYgMTAuMjE4NzUgaCAyNi44MTI1IHYgMy40MDYyNSBoIC0yNi44MTI1IC0xMC4wNjI1IGMgLTcuNzkyNDU5LDAgLTE0LjYxNTc1ODgsNC42ODM3MTcgLTE2Ljc0OTk5OTgsMTMuNTkzNzUgLTIuNDYxODE5OTgsMTAuMjEyOTY2IC0yLjU3MTAxNTA4LDE2LjU4NjAyMyAwLDI3LjI1IDEuOTA1OTI4Myw3LjkzNzg1MiA2LjQ1NzU0MzIsMTMuNTkzNzQ4IDE0LjI0OTk5OTgsMTMuNTkzNzUgaCA5LjIxODc1IHYgLTEyLjI1IGMgMCwtOC44NDk5MDIgNy42NTcxNDQsLTE2LjY1NjI0OCAxNi43NSwtMTYuNjU2MjUgaCAyNi43ODEyNSBjIDcuNDU0OTUxLDAgMTMuNDA2MjUzLC02LjEzODE2NCAxMy40MDYyNSwtMTMuNjI1IHYgLTI1LjUzMTI1IGMgMCwtNy4yNjYzMzg2IC02LjEyOTk4LC0xMi43MjQ3NzcxIC0xMy40MDYyNSwtMTMuOTM3NDk5NyBDIDY0LjI4MTU0OCwwLjMyNzk0Mzk3IDU5LjUwMjQzOCwtMC4wMjAzNzkwMyA1NC45MTg3ODUsOS4xOTI3NDIxZS00IFogbSAtMTQuNSw4LjIxODc1MDEyNTc5IGMgMi43Njk1NDcsMCA1LjAzMTI1LDIuMjk4NjQ1NiA1LjAzMTI1LDUuMTI0OTk5NiAtMmUtNiwyLjgxNjMzNiAtMi4yNjE3MDMsNS4wOTM3NSAtNS4wMzEyNSw1LjA5Mzc1IC0yLjc3OTQ3NiwtMWUtNiAtNS4wMzEyNSwtMi4yNzc0MTUgLTUuMDMxMjUsLTUuMDkzNzUgLTEwZS03LC0yLjgyNjM1MyAyLjI1MTc3NCwtNS4xMjQ5OTk2IDUuMDMxMjUsLTUuMTI0OTk5NiB6Ii8+CiAgPHBhdGggY2xhc3M9ImpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iI2ZmZDQzYiIgZD0ibSA4NS42Mzc1MzUsMjguNjU3MTY5IHYgMTEuOTA2MjUgYyAwLDkuMjMwNzU1IC03LjgyNTg5NSwxNi45OTk5OTkgLTE2Ljc1LDE3IGggLTI2Ljc4MTI1IGMgLTcuMzM1ODMzLDAgLTEzLjQwNjI0OSw2LjI3ODQ4MyAtMTMuNDA2MjUsMTMuNjI1IHYgMjUuNTMxMjQ3IGMgMCw3LjI2NjM0NCA2LjMxODU4OCwxMS41NDAzMjQgMTMuNDA2MjUsMTMuNjI1MDA0IDguNDg3MzMxLDIuNDk1NjEgMTYuNjI2MjM3LDIuOTQ2NjMgMjYuNzgxMjUsMCA2Ljc1MDE1NSwtMS45NTQzOSAxMy40MDYyNTMsLTUuODg3NjEgMTMuNDA2MjUsLTEzLjYyNTAwNCBWIDg2LjUwMDkxOSBoIC0yNi43ODEyNSB2IC0zLjQwNjI1IGggMjYuNzgxMjUgMTMuNDA2MjU0IGMgNy43OTI0NjEsMCAxMC42OTYyNTEsLTUuNDM1NDA4IDEzLjQwNjI0MSwtMTMuNTkzNzUgMi43OTkzMywtOC4zOTg4ODYgMi42ODAyMiwtMTYuNDc1Nzc2IDAsLTI3LjI1IC0xLjkyNTc4LC03Ljc1NzQ0MSAtNS42MDM4NywtMTMuNTkzNzUgLTEzLjQwNjI0MSwtMTMuNTkzNzUgeiBtIC0xNS4wNjI1LDY0LjY1NjI1IGMgMi43Nzk0NzgsM2UtNiA1LjAzMTI1LDIuMjc3NDE3IDUuMDMxMjUsNS4wOTM3NDcgLTJlLTYsMi44MjYzNTQgLTIuMjUxNzc1LDUuMTI1MDA0IC01LjAzMTI1LDUuMTI1MDA0IC0yLjc2OTU1LDAgLTUuMDMxMjUsLTIuMjk4NjUgLTUuMDMxMjUsLTUuMTI1MDA0IDJlLTYsLTIuODE2MzMgMi4yNjE2OTcsLTUuMDkzNzQ3IDUuMDMxMjUsLTUuMDkzNzQ3IHoiLz4KPC9zdmc+Cg==);
--jp-icon-r-kernel: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8cGF0aCBjbGFzcz0ianAtaWNvbi1jb250cmFzdDMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjMjE5NkYzIiBkPSJNNC40IDIuNWMxLjItLjEgMi45LS4zIDQuOS0uMyAyLjUgMCA0LjEuNCA1LjIgMS4zIDEgLjcgMS41IDEuOSAxLjUgMy41IDAgMi0xLjQgMy41LTIuOSA0LjEgMS4yLjQgMS43IDEuNiAyLjIgMyAuNiAxLjkgMSAzLjkgMS4zIDQuNmgtMy44Yy0uMy0uNC0uOC0xLjctMS4yLTMuN3MtMS4yLTIuNi0yLjYtMi42aC0uOXY2LjRINC40VjIuNXptMy43IDYuOWgxLjRjMS45IDAgMi45LS45IDIuOS0yLjNzLTEtMi4zLTIuOC0yLjNjLS43IDAtMS4zIDAtMS42LjJ2NC41aC4xdi0uMXoiLz4KPC9zdmc+Cg==);
--jp-icon-react: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMTUwIDE1MCA1NDEuOSAyOTUuMyI+CiAgPGcgY2xhc3M9ImpwLWljb24tYnJhbmQyIGpwLWljb24tc2VsZWN0YWJsZSIgZmlsbD0iIzYxREFGQiI+CiAgICA8cGF0aCBkPSJNNjY2LjMgMjk2LjVjMC0zMi41LTQwLjctNjMuMy0xMDMuMS04Mi40IDE0LjQtNjMuNiA4LTExNC4yLTIwLjItMTMwLjQtNi41LTMuOC0xNC4xLTUuNi0yMi40LTUuNnYyMi4zYzQuNiAwIDguMy45IDExLjQgMi42IDEzLjYgNy44IDE5LjUgMzcuNSAxNC45IDc1LjctMS4xIDkuNC0yLjkgMTkuMy01LjEgMjkuNC0xOS42LTQuOC00MS04LjUtNjMuNS0xMC45LTEzLjUtMTguNS0yNy41LTM1LjMtNDEuNi01MCAzMi42LTMwLjMgNjMuMi00Ni45IDg0LTQ2LjlWNzhjLTI3LjUgMC02My41IDE5LjYtOTkuOSA1My42LTM2LjQtMzMuOC03Mi40LTUzLjItOTkuOS01My4ydjIyLjNjMjAuNyAwIDUxLjQgMTYuNSA4NCA0Ni42LTE0IDE0LjctMjggMzEuNC00MS4zIDQ5LjktMjIuNiAyLjQtNDQgNi4xLTYzLjYgMTEtMi4zLTEwLTQtMTkuNy01LjItMjktNC43LTM4LjIgMS4xLTY3LjkgMTQuNi03NS44IDMtMS44IDYuOS0yLjYgMTEuNS0yLjZWNzguNWMtOC40IDAtMTYgMS44LTIyLjYgNS42LTI4LjEgMTYuMi0zNC40IDY2LjctMTkuOSAxMzAuMS02Mi4yIDE5LjItMTAyLjcgNDkuOS0xMDIuNyA4Mi4zIDAgMzIuNSA0MC43IDYzLjMgMTAzLjEgODIuNC0xNC40IDYzLjYtOCAxMTQuMiAyMC4yIDEzMC40IDYuNSAzLjggMTQuMSA1LjYgMjIuNSA1LjYgMjcuNSAwIDYzLjUtMTkuNiA5OS45LTUzLjYgMzYuNCAzMy44IDcyLjQgNTMuMiA5OS45IDUzLjIgOC40IDAgMTYtMS44IDIyLjYtNS42IDI4LjEtMTYuMiAzNC40LTY2LjcgMTkuOS0xMzAuMSA2Mi0xOS4xIDEwMi41LTQ5LjkgMTAyLjUtODIuM3ptLTEzMC4yLTY2LjdjLTMuNyAxMi45LTguMyAyNi4yLTEzLjUgMzkuNS00LjEtOC04LjQtMTYtMTMuMS0yNC00LjYtOC05LjUtMTUuOC0xNC40LTIzLjQgMTQuMiAyLjEgMjcuOSA0LjcgNDEgNy45em0tNDUuOCAxMDYuNWMtNy44IDEzLjUtMTUuOCAyNi4zLTI0LjEgMzguMi0xNC45IDEuMy0zMCAyLTQ1LjIgMi0xNS4xIDAtMzAuMi0uNy00NS0xLjktOC4zLTExLjktMTYuNC0yNC42LTI0LjItMzgtNy42LTEzLjEtMTQuNS0yNi40LTIwLjgtMzkuOCA2LjItMTMuNCAxMy4yLTI2LjggMjAuNy0zOS45IDcuOC0xMy41IDE1LjgtMjYuMyAyNC4xLTM4LjIgMTQuOS0xLjMgMzAtMiA0NS4yLTIgMTUuMSAwIDMwLjIuNyA0NSAxLjkgOC4zIDExLjkgMTYuNCAyNC42IDI0LjIgMzggNy42IDEzLjEgMTQuNSAyNi40IDIwLjggMzkuOC02LjMgMTMuNC0xMy4yIDI2LjgtMjAuNyAzOS45em0zMi4zLTEzYzUuNCAxMy40IDEwIDI2LjggMTMuOCAzOS44LTEzLjEgMy4yLTI2LjkgNS45LTQxLjIgOCA0LjktNy43IDkuOC0xNS42IDE0LjQtMjMuNyA0LjYtOCA4LjktMTYuMSAxMy0yNC4xek00MjEuMiA0MzBjLTkuMy05LjYtMTguNi0yMC4zLTI3LjgtMzIgOSAuNCAxOC4yLjcgMjcuNS43IDkuNCAwIDE4LjctLjIgMjcuOC0uNy05IDExLjctMTguMyAyMi40LTI3LjUgMzJ6bS03NC40LTU4LjljLTE0LjItMi4xLTI3LjktNC43LTQxLTcuOSAzLjctMTIuOSA4LjMtMjYuMiAxMy41LTM5LjUgNC4xIDggOC40IDE2IDEzLjEgMjQgNC43IDggOS41IDE1LjggMTQuNCAyMy40ek00MjAuNyAxNjNjOS4zIDkuNiAxOC42IDIwLjMgMjcuOCAzMi05LS40LTE4LjItLjctMjcuNS0uNy05LjQgMC0xOC43LjItMjcuOC43IDktMTEuNyAxOC4zLTIyLjQgMjcuNS0zMnptLTc0IDU4LjljLTQuOSA3LjctOS44IDE1LjYtMTQuNCAyMy43LTQuNiA4LTguOSAxNi0xMyAyNC01LjQtMTMuNC0xMC0yNi44LTEzLjgtMzkuOCAxMy4xLTMuMSAyNi45LTUuOCA0MS4yLTcuOXptLTkwLjUgMTI1LjJjLTM1LjQtMTUuMS01OC4zLTM0LjktNTguMy01MC42IDAtMTUuNyAyMi45LTM1LjYgNTguMy01MC42IDguNi0zLjcgMTgtNyAyNy43LTEwLjEgNS43IDE5LjYgMTMuMiA0MCAyMi41IDYwLjktOS4yIDIwLjgtMTYuNiA0MS4xLTIyLjIgNjAuNi05LjktMy4xLTE5LjMtNi41LTI4LTEwLjJ6TTMxMCA0OTBjLTEzLjYtNy44LTE5LjUtMzcuNS0xNC45LTc1LjcgMS4xLTkuNCAyLjktMTkuMyA1LjEtMjkuNCAxOS42IDQuOCA0MSA4LjUgNjMuNSAxMC45IDEzLjUgMTguNSAyNy41IDM1LjMgNDEuNiA1MC0zMi42IDMwLjMtNjMuMiA0Ni45LTg0IDQ2LjktNC41LS4xLTguMy0xLTExLjMtMi43em0yMzcuMi03Ni4yYzQuNyAzOC4yLTEuMSA2Ny45LTE0LjYgNzUuOC0zIDEuOC02LjkgMi42LTExLjUgMi42LTIwLjcgMC01MS40LTE2LjUtODQtNDYuNiAxNC0xNC43IDI4LTMxLjQgNDEuMy00OS45IDIyLjYtMi40IDQ0LTYuMSA2My42LTExIDIuMyAxMC4xIDQuMSAxOS44IDUuMiAyOS4xem0zOC41LTY2LjdjLTguNiAzLjctMTggNy0yNy43IDEwLjEtNS43LTE5LjYtMTMuMi00MC0yMi41LTYwLjkgOS4yLTIwLjggMTYuNi00MS4xIDIyLjItNjAuNiA5LjkgMy4xIDE5LjMgNi41IDI4LjEgMTAuMiAzNS40IDE1LjEgNTguMyAzNC45IDU4LjMgNTAuNi0uMSAxNS43LTIzIDM1LjYtNTguNCA1MC42ek0zMjAuOCA3OC40eiIvPgogICAgPGNpcmNsZSBjeD0iNDIwLjkiIGN5PSIyOTYuNSIgcj0iNDUuNyIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-redo: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjE2Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgICA8cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE4LjQgMTAuNkMxNi41NSA4Ljk5IDE0LjE1IDggMTEuNSA4Yy00LjY1IDAtOC41OCAzLjAzLTkuOTYgNy4yMkwzLjkgMTZjMS4wNS0zLjE5IDQuMDUtNS41IDcuNi01LjUgMS45NSAwIDMuNzMuNzIgNS4xMiAxLjg4TDEzIDE2aDlWN2wtMy42IDMuNnoiLz4KICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-refresh: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDE4IDE4Ij4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTkgMTMuNWMtMi40OSAwLTQuNS0yLjAxLTQuNS00LjVTNi41MSA0LjUgOSA0LjVjMS4yNCAwIDIuMzYuNTIgMy4xNyAxLjMzTDEwIDhoNVYzbC0xLjc2IDEuNzZDMTIuMTUgMy42OCAxMC42NiAzIDkgMyA1LjY5IDMgMy4wMSA1LjY5IDMuMDEgOVM1LjY5IDE1IDkgMTVjMi45NyAwIDUuNDMtMi4xNiA1LjktNWgtMS41MmMtLjQ2IDItMi4yNCAzLjUtNC4zOCAzLjV6Ii8+CiAgICA8L2c+Cjwvc3ZnPgo=);
--jp-icon-regex: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIwIDIwIj4KICA8ZyBjbGFzcz0ianAtaWNvbjIiIGZpbGw9IiM0MTQxNDEiPgogICAgPHJlY3QgeD0iMiIgeT0iMiIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+CiAgPC9nPgoKICA8ZyBjbGFzcz0ianAtaWNvbi1hY2NlbnQyIiBmaWxsPSIjRkZGIj4KICAgIDxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjUuNSIgY3k9IjE0LjUiIHI9IjEuNSIvPgogICAgPHJlY3QgeD0iMTIiIHk9IjQiIGNsYXNzPSJzdDIiIHdpZHRoPSIxIiBoZWlnaHQ9IjgiLz4KICAgIDxyZWN0IHg9IjguNSIgeT0iNy41IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjg2NiAtMC41IDAuNSAwLjg2NiAtMi4zMjU1IDcuMzIxOSkiIGNsYXNzPSJzdDIiIHdpZHRoPSI4IiBoZWlnaHQ9IjEiLz4KICAgIDxyZWN0IHg9IjEyIiB5PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgwLjUgLTAuODY2IDAuODY2IDAuNSAtMC42Nzc5IDE0LjgyNTIpIiBjbGFzcz0ic3QyIiB3aWR0aD0iMSIgaGVpZ2h0PSI4Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-run: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTggNXYxNGwxMS03eiIvPgogICAgPC9nPgo8L3N2Zz4K);
--jp-icon-running: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDUxMiA1MTIiPgogIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICA8cGF0aCBkPSJNMjU2IDhDMTE5IDggOCAxMTkgOCAyNTZzMTExIDI0OCAyNDggMjQ4IDI0OC0xMTEgMjQ4LTI0OFMzOTMgOCAyNTYgOHptOTYgMzI4YzAgOC44LTcuMiAxNi0xNiAxNkgxNzZjLTguOCAwLTE2LTcuMi0xNi0xNlYxNzZjMC04LjggNy4yLTE2IDE2LTE2aDE2MGM4LjggMCAxNiA3LjIgMTYgMTZ2MTYweiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-save: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTE3IDNINWMtMS4xMSAwLTIgLjktMiAydjE0YzAgMS4xLjg5IDIgMiAyaDE0YzEuMSAwIDItLjkgMi0yVjdsLTQtNHptLTUgMTZjLTEuNjYgMC0zLTEuMzQtMy0zczEuMzQtMyAzLTMgMyAxLjM0IDMgMy0xLjM0IDMtMyAzem0zLTEwSDVWNWgxMHY0eiIvPgogICAgPC9nPgo8L3N2Zz4K);
--jp-icon-search: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTggMTgiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTEyLjEsMTAuOWgtMC43bC0wLjItMC4yYzAuOC0wLjksMS4zLTIuMiwxLjMtMy41YzAtMy0yLjQtNS40LTUuNC01LjRTMS44LDQuMiwxLjgsNy4xczIuNCw1LjQsNS40LDUuNCBjMS4zLDAsMi41LTAuNSwzLjUtMS4zbDAuMiwwLjJ2MC43bDQuMSw0LjFsMS4yLTEuMkwxMi4xLDEwLjl6IE03LjEsMTAuOWMtMi4xLDAtMy43LTEuNy0zLjctMy43czEuNy0zLjcsMy43LTMuN3MzLjcsMS43LDMuNywzLjcgUzkuMiwxMC45LDcuMSwxMC45eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-settings: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIiBkPSJNMTkuNDMgMTIuOThjLjA0LS4zMi4wNy0uNjQuMDctLjk4cy0uMDMtLjY2LS4wNy0uOThsMi4xMS0xLjY1Yy4xOS0uMTUuMjQtLjQyLjEyLS42NGwtMi0zLjQ2Yy0uMTItLjIyLS4zOS0uMy0uNjEtLjIybC0yLjQ5IDFjLS41Mi0uNC0xLjA4LS43My0xLjY5LS45OGwtLjM4LTIuNjVBLjQ4OC40ODggMCAwMDE0IDJoLTRjLS4yNSAwLS40Ni4xOC0uNDkuNDJsLS4zOCAyLjY1Yy0uNjEuMjUtMS4xNy41OS0xLjY5Ljk4bC0yLjQ5LTFjLS4yMy0uMDktLjQ5IDAtLjYxLjIybC0yIDMuNDZjLS4xMy4yMi0uMDcuNDkuMTIuNjRsMi4xMSAxLjY1Yy0uMDQuMzItLjA3LjY1LS4wNy45OHMuMDMuNjYuMDcuOThsLTIuMTEgMS42NWMtLjE5LjE1LS4yNC40Mi0uMTIuNjRsMiAzLjQ2Yy4xMi4yMi4zOS4zLjYxLjIybDIuNDktMWMuNTIuNCAxLjA4LjczIDEuNjkuOThsLjM4IDIuNjVjLjAzLjI0LjI0LjQyLjQ5LjQyaDRjLjI1IDAgLjQ2LS4xOC40OS0uNDJsLjM4LTIuNjVjLjYxLS4yNSAxLjE3LS41OSAxLjY5LS45OGwyLjQ5IDFjLjIzLjA5LjQ5IDAgLjYxLS4yMmwyLTMuNDZjLjEyLS4yMi4wNy0uNDktLjEyLS42NGwtMi4xMS0xLjY1ek0xMiAxNS41Yy0xLjkzIDAtMy41LTEuNTctMy41LTMuNXMxLjU3LTMuNSAzLjUtMy41IDMuNSAxLjU3IDMuNSAzLjUtMS41NyAzLjUtMy41IDMuNXoiLz4KPC9zdmc+Cg==);
--jp-icon-share: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTSAxOCAyIEMgMTYuMzU0OTkgMiAxNSAzLjM1NDk5MDQgMTUgNSBDIDE1IDUuMTkwOTUyOSAxNS4wMjE3OTEgNS4zNzcxMjI0IDE1LjA1NjY0MSA1LjU1ODU5MzggTCA3LjkyMTg3NSA5LjcyMDcwMzEgQyA3LjM5ODUzOTkgOS4yNzc4NTM5IDYuNzMyMDc3MSA5IDYgOSBDIDQuMzU0OTkwNCA5IDMgMTAuMzU0OTkgMyAxMiBDIDMgMTMuNjQ1MDEgNC4zNTQ5OTA0IDE1IDYgMTUgQyA2LjczMjA3NzEgMTUgNy4zOTg1Mzk5IDE0LjcyMjE0NiA3LjkyMTg3NSAxNC4yNzkyOTcgTCAxNS4wNTY2NDEgMTguNDM5NDUzIEMgMTUuMDIxNTU1IDE4LjYyMTUxNCAxNSAxOC44MDgzODYgMTUgMTkgQyAxNSAyMC42NDUwMSAxNi4zNTQ5OSAyMiAxOCAyMiBDIDE5LjY0NTAxIDIyIDIxIDIwLjY0NTAxIDIxIDE5IEMgMjEgMTcuMzU0OTkgMTkuNjQ1MDEgMTYgMTggMTYgQyAxNy4yNjc0OCAxNiAxNi42MDE1OTMgMTYuMjc5MzI4IDE2LjA3ODEyNSAxNi43MjI2NTYgTCA4Ljk0MzM1OTQgMTIuNTU4NTk0IEMgOC45NzgyMDk1IDEyLjM3NzEyMiA5IDEyLjE5MDk1MyA5IDEyIEMgOSAxMS44MDkwNDcgOC45NzgyMDk1IDExLjYyMjg3OCA4Ljk0MzM1OTQgMTEuNDQxNDA2IEwgMTYuMDc4MTI1IDcuMjc5Mjk2OSBDIDE2LjYwMTQ2IDcuNzIyMTQ2MSAxNy4yNjc5MjMgOCAxOCA4IEMgMTkuNjQ1MDEgOCAyMSA2LjY0NTAwOTYgMjEgNSBDIDIxIDMuMzU0OTkwNCAxOS42NDUwMSAyIDE4IDIgeiBNIDE4IDQgQyAxOC41NjQxMjkgNCAxOSA0LjQzNTg3MDYgMTkgNSBDIDE5IDUuNTY0MTI5NCAxOC41NjQxMjkgNiAxOCA2IEMgMTcuNDM1ODcxIDYgMTcgNS41NjQxMjk0IDE3IDUgQyAxNyA0LjQzNTg3MDYgMTcuNDM1ODcxIDQgMTggNCB6IE0gNiAxMSBDIDYuNTY0MTI5NCAxMSA3IDExLjQzNTg3MSA3IDEyIEMgNyAxMi41NjQxMjkgNi41NjQxMjk0IDEzIDYgMTMgQyA1LjQzNTg3MDYgMTMgNSAxMi41NjQxMjkgNSAxMiBDIDUgMTEuNDM1ODcxIDUuNDM1ODcwNiAxMSA2IDExIHogTSAxOCAxOCBDIDE4LjU2NDEyOSAxOCAxOSAxOC40MzU4NzEgMTkgMTkgQyAxOSAxOS41NjQxMjkgMTguNTY0MTI5IDIwIDE4IDIwIEMgMTcuNDM1ODcxIDIwIDE3IDE5LjU2NDEyOSAxNyAxOSBDIDE3IDE4LjQzNTg3MSAxNy40MzU4NzEgMTggMTggMTggeiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-spreadsheet: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8cGF0aCBjbGFzcz0ianAtaWNvbi1jb250cmFzdDEganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNENBRjUwIiBkPSJNMi4yIDIuMnYxNy42aDE3LjZWMi4ySDIuMnptMTUuNCA3LjdoLTUuNVY0LjRoNS41djUuNXpNOS45IDQuNHY1LjVINC40VjQuNGg1LjV6bS01LjUgNy43aDUuNXY1LjVINC40di01LjV6bTcuNyA1LjV2LTUuNWg1LjV2NS41aC01LjV6Ii8+Cjwvc3ZnPgo=);
--jp-icon-stop: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgogICAgICAgIDxwYXRoIGQ9Ik02IDZoMTJ2MTJINnoiLz4KICAgIDwvZz4KPC9zdmc+Cg==);
--jp-icon-tab: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTIxIDNIM2MtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxOGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDNWNWgxMHY0aDh2MTB6Ii8+CiAgPC9nPgo8L3N2Zz4K);
--jp-icon-table-rows: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgogICAgICAgIDxwYXRoIGQ9Ik0yMSw4SDNWNGgxOFY4eiBNMjEsMTBIM3Y0aDE4VjEweiBNMjEsMTZIM3Y0aDE4VjE2eiIvPgogICAgPC9nPgo8L3N2Zz4=);
--jp-icon-tag: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCA0MyAyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CgkJPHBhdGggZD0iTTI4LjgzMzIgMTIuMzM0TDMyLjk5OTggMTYuNTAwN0wzNy4xNjY1IDEyLjMzNEgyOC44MzMyWiIvPgoJCTxwYXRoIGQ9Ik0xNi4yMDk1IDIxLjYxMDRDMTUuNjg3MyAyMi4xMjk5IDE0Ljg0NDMgMjIuMTI5OSAxNC4zMjQ4IDIxLjYxMDRMNi45ODI5IDE0LjcyNDVDNi41NzI0IDE0LjMzOTQgNi4wODMxMyAxMy42MDk4IDYuMDQ3ODYgMTMuMDQ4MkM1Ljk1MzQ3IDExLjUyODggNi4wMjAwMiA4LjYxOTQ0IDYuMDY2MjEgNy4wNzY5NUM2LjA4MjgxIDYuNTE0NzcgNi41NTU0OCA2LjA0MzQ3IDcuMTE4MDQgNi4wMzA1NUM5LjA4ODYzIDUuOTg0NzMgMTMuMjYzOCA1LjkzNTc5IDEzLjY1MTggNi4zMjQyNUwyMS43MzY5IDEzLjYzOUMyMi4yNTYgMTQuMTU4NSAyMS43ODUxIDE1LjQ3MjQgMjEuMjYyIDE1Ljk5NDZMMTYuMjA5NSAyMS42MTA0Wk05Ljc3NTg1IDguMjY1QzkuMzM1NTEgNy44MjU2NiA4LjYyMzUxIDcuODI1NjYgOC4xODI4IDguMjY1QzcuNzQzNDYgOC43MDU3MSA3Ljc0MzQ2IDkuNDE3MzMgOC4xODI4IDkuODU2NjdDOC42MjM4MiAxMC4yOTY0IDkuMzM1ODIgMTAuMjk2NCA5Ljc3NTg1IDkuODU2NjdDMTAuMjE1NiA5LjQxNzMzIDEwLjIxNTYgOC43MDUzMyA5Ljc3NTg1IDguMjY1WiIvPgoJPC9nPgo8L3N2Zz4K);
--jp-icon-terminal: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiA+CiAgICA8cmVjdCBjbGFzcz0ianAtdGVybWluYWwtaWNvbi1iYWNrZ3JvdW5kLWNvbG9yIGpwLWljb24tc2VsZWN0YWJsZSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyIDIpIiBmaWxsPSIjMzMzMzMzIi8+CiAgICA8cGF0aCBjbGFzcz0ianAtdGVybWluYWwtaWNvbi1jb2xvciBqcC1pY29uLXNlbGVjdGFibGUtaW52ZXJzZSIgZD0iTTUuMDU2NjQgOC43NjE3MkM1LjA1NjY0IDguNTk3NjYgNS4wMzEyNSA4LjQ1MzEyIDQuOTgwNDcgOC4zMjgxMkM0LjkzMzU5IDguMTk5MjIgNC44NTU0NyA4LjA4MjAzIDQuNzQ2MDkgNy45NzY1NkM0LjY0MDYyIDcuODcxMDkgNC41IDcuNzc1MzkgNC4zMjQyMiA3LjY4OTQ1QzQuMTUyMzQgNy41OTk2MSAzLjk0MzM2IDcuNTExNzIgMy42OTcyNyA3LjQyNTc4QzMuMzAyNzMgNy4yODUxNiAyLjk0MzM2IDcuMTM2NzIgMi42MTkxNCA2Ljk4MDQ3QzIuMjk0OTIgNi44MjQyMiAyLjAxNzU4IDYuNjQyNTggMS43ODcxMSA2LjQzNTU1QzEuNTYwNTUgNi4yMjg1MiAxLjM4NDc3IDUuOTg4MjggMS4yNTk3NyA1LjcxNDg0QzEuMTM0NzcgNS40Mzc1IDEuMDcyMjcgNS4xMDkzOCAxLjA3MjI3IDQuNzMwNDdDMS4wNzIyNyA0LjM5ODQ0IDEuMTI4OTEgNC4wOTU3IDEuMjQyMTkgMy44MjIyN0MxLjM1NTQ3IDMuNTQ0OTIgMS41MTU2MiAzLjMwNDY5IDEuNzIyNjYgMy4xMDE1NkMxLjkyOTY5IDIuODk4NDQgMi4xNzk2OSAyLjczNDM3IDIuNDcyNjYgMi42MDkzOEMyLjc2NTYyIDIuNDg0MzggMy4wOTE4IDIuNDA0MyAzLjQ1MTE3IDIuMzY5MTRWMS4xMDkzOEg0LjM4ODY3VjIuMzgwODZDNC43NDAyMyAyLjQyNzczIDUuMDU2NjQgMi41MjM0NCA1LjMzNzg5IDIuNjY3OTdDNS42MTkxNCAyLjgxMjUgNS44NTc0MiAzLjAwMTk1IDYuMDUyNzMgMy4yMzYzM0M2LjI1MTk1IDMuNDY2OCA2LjQwNDMgMy43NDAyMyA2LjUwOTc3IDQuMDU2NjRDNi42MTkxNCA0LjM2OTE0IDYuNjczODMgNC43MjA3IDYuNjczODMgNS4xMTEzM0g1LjA0NDkyQzUuMDQ0OTIgNC42Mzg2NyA0LjkzNzUgNC4yODEyNSA0LjcyMjY2IDQuMDM5MDZDNC41MDc4MSAzLjc5Mjk3IDQuMjE2OCAzLjY2OTkyIDMuODQ5NjEgMy42Njk5MkMzLjY1MDM5IDMuNjY5OTIgMy40NzY1NiAzLjY5NzI3IDMuMzI4MTIgMy43NTE5NUMzLjE4MzU5IDMuODAyNzMgMy4wNjQ0NSAzLjg3Njk1IDIuOTcwNyAzLjk3NDYxQzIuODc2OTUgNC4wNjgzNiAyLjgwNjY0IDQuMTc5NjkgMi43NTk3NyA0LjMwODU5QzIuNzE2OCA0LjQzNzUgMi42OTUzMSA0LjU3ODEyIDIuNjk1MzEgNC43MzA0N0MyLjY5NTMxIDQuODgyODEgMi43MTY4IDUuMDE5NTMgMi43NTk3NyA1LjE0MDYyQzIuODA2NjQgNS4yNTc4MSAyLjg4MjgxIDUuMzY3MTkgMi45ODgyOCA1LjQ2ODc1QzMuMDk3NjYgNS41NzAzMSAzLjI0MDIzIDUuNjY3OTcgMy40MTYwMiA1Ljc2MTcyQzMuNTkxOCA1Ljg1MTU2IDMuODEwNTUgNS45NDMzNiA0LjA3MjI3IDYuMDM3MTFDNC40NjY4IDYuMTg1NTUgNC44MjQyMiA2LjMzOTg0IDUuMTQ0NTMgNi41QzUuNDY0ODQgNi42NTYyNSA1LjczODI4IDYuODM5ODQgNS45NjQ4NCA3LjA1MDc4QzYuMTk1MzEgNy4yNTc4MSA2LjM3MTA5IDcuNSA2LjQ5MjE5IDcuNzc3MzRDNi42MTcxOSA4LjA1MDc4IDYuNjc5NjkgOC4zNzUgNi42Nzk2OSA4Ljc1QzYuNjc5NjkgOS4wOTM3NSA2LjYyMzA1IDkuNDA0MyA2LjUwOTc3IDkuNjgxNjRDNi4zOTY0OCA5Ljk1NTA4IDYuMjM0MzggMTAuMTkxNCA2LjAyMzQ0IDEwLjM5MDZDNS44MTI1IDEwLjU4OTggNS41NTg1OSAxMC43NSA1LjI2MTcyIDEwLjg3MTFDNC45NjQ4NCAxMC45ODgzIDQuNjMyODEgMTEuMDY0NSA0LjI2NTYyIDExLjA5OTZWMTIuMjQ4SDMuMzMzOThWMTEuMDk5NkMzLjAwMTk1IDExLjA2ODQgMi42Nzk2OSAxMC45OTYxIDIuMzY3MTkgMTAuODgyOEMyLjA1NDY5IDEwLjc2NTYgMS43NzczNCAxMC41OTc3IDEuNTM1MTYgMTAuMzc4OUMxLjI5Njg4IDEwLjE2MDIgMS4xMDU0NyA5Ljg4NDc3IDAuOTYwOTM4IDkuNTUyNzNDMC44MTY0MDYgOS4yMTY4IDAuNzQ0MTQxIDguODE0NDUgMC43NDQxNDEgOC4zNDU3SDIuMzc4OTFDMi4zNzg5MSA4LjYyNjk1IDIuNDE5OTIgOC44NjMyOCAyLjUwMTk1IDkuMDU0NjlDMi41ODM5OCA5LjI0MjE5IDIuNjg5NDUgOS4zOTI1OCAyLjgxODM2IDkuNTA1ODZDMi45NTExNyA5LjYxNTIzIDMuMTAxNTYgOS42OTMzNiAzLjI2OTUzIDkuNzQwMjNDMy40Mzc1IDkuNzg3MTEgMy42MDkzOCA5LjgxMDU1IDMuNzg1MTYgOS44MTA1NUM0LjIwMzEyIDkuODEwNTUgNC41MTk1MyA5LjcxMjg5IDQuNzM0MzggOS41MTc1OEM0Ljk0OTIyIDkuMzIyMjcgNS4wNTY2NCA5LjA3MDMxIDUuMDU2NjQgOC43NjE3MlpNMTMuNDE4IDEyLjI3MTVIOC4wNzQyMlYxMUgxMy40MThWMTIuMjcxNVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMuOTUyNjQgNikiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=);
--jp-icon-text-editor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8cGF0aCBjbGFzcz0ianAtdGV4dC1lZGl0b3ItaWNvbi1jb2xvciBqcC1pY29uLXNlbGVjdGFibGUiIGZpbGw9IiM2MTYxNjEiIGQ9Ik0xNSAxNUgzdjJoMTJ2LTJ6bTAtOEgzdjJoMTJWN3pNMyAxM2gxOHYtMkgzdjJ6bTAgOGgxOHYtMkgzdjJ6TTMgM3YyaDE4VjNIM3oiLz4KPC9zdmc+Cg==);
--jp-icon-toc: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZyBjbGFzcz0ianAtaWNvbjMganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjNjE2MTYxIj4KICAgIDxwYXRoIGQ9Ik03LDVIMjFWN0g3VjVNNywxM1YxMUgyMVYxM0g3TTQsNC41QTEuNSwxLjUgMCAwLDEgNS41LDZBMS41LDEuNSAwIDAsMSA0LDcuNUExLjUsMS41IDAgMCwxIDIuNSw2QTEuNSwxLjUgMCAwLDEgNCw0LjVNNCwxMC41QTEuNSwxLjUgMCAwLDEgNS41LDEyQTEuNSwxLjUgMCAwLDEgNCwxMy41QTEuNSwxLjUgMCAwLDEgMi41LDEyQTEuNSwxLjUgMCAwLDEgNCwxMC41TTcsMTlWMTdIMjFWMTlIN000LDE2LjVBMS41LDEuNSAwIDAsMSA1LjUsMThBMS41LDEuNSAwIDAsMSA0LDE5LjVBMS41LDEuNSAwIDAsMSAyLjUsMThBMS41LDEuNSAwIDAsMSA0LDE2LjVaIiAvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-tree-view: url(data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxnIGNsYXNzPSJqcC1pY29uMyIgZmlsbD0iIzYxNjE2MSI+CiAgICAgICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPgogICAgICAgIDxwYXRoIGQ9Ik0yMiAxMVYzaC03djNIOVYzSDJ2OGg3VjhoMnYxMGg0djNoN3YtOGgtN3YzaC0yVjhoMnYzeiIvPgogICAgPC9nPgo8L3N2Zz4=);
--jp-icon-trusted: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDI0IDI1Ij4KICAgIDxwYXRoIGNsYXNzPSJqcC1pY29uMiIgc3Ryb2tlPSIjMzMzMzMzIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIgMykiIGQ9Ik0xLjg2MDk0IDExLjQ0MDlDMC44MjY0NDggOC43NzAyNyAwLjg2Mzc3OSA2LjA1NzY0IDEuMjQ5MDcgNC4xOTkzMkMyLjQ4MjA2IDMuOTMzNDcgNC4wODA2OCAzLjQwMzQ3IDUuNjAxMDIgMi44NDQ5QzcuMjM1NDkgMi4yNDQ0IDguODU2NjYgMS41ODE1IDkuOTg3NiAxLjA5NTM5QzExLjA1OTcgMS41ODM0MSAxMi42MDk0IDIuMjQ0NCAxNC4yMTggMi44NDMzOUMxNS43NTAzIDMuNDEzOTQgMTcuMzk5NSAzLjk1MjU4IDE4Ljc1MzkgNC4yMTM4NUMxOS4xMzY0IDYuMDcxNzcgMTkuMTcwOSA4Ljc3NzIyIDE4LjEzOSAxMS40NDA5QzE3LjAzMDMgMTQuMzAzMiAxNC42NjY4IDE3LjE4NDQgOS45OTk5OSAxOC45MzU0QzUuMzMzMiAxNy4xODQ0IDIuOTY5NjggMTQuMzAzMiAxLjg2MDk0IDExLjQ0MDlaIi8+CiAgICA8cGF0aCBjbGFzcz0ianAtaWNvbjIiIGZpbGw9IiMzMzMzMzMiIHN0cm9rZT0iIzMzMzMzMyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOCA5Ljg2NzE5KSIgZD0iTTIuODYwMTUgNC44NjUzNUwwLjcyNjU0OSAyLjk5OTU5TDAgMy42MzA0NUwyLjg2MDE1IDYuMTMxNTdMOCAwLjYzMDg3Mkw3LjI3ODU3IDBMMi44NjAxNSA0Ljg2NTM1WiIvPgo8L3N2Zz4K);
--jp-icon-undo: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTEyLjUgOGMtMi42NSAwLTUuMDUuOTktNi45IDIuNkwyIDd2OWg5bC0zLjYyLTMuNjJjMS4zOS0xLjE2IDMuMTYtMS44OCA1LjEyLTEuODggMy41NCAwIDYuNTUgMi4zMSA3LjYgNS41bDIuMzctLjc4QzIxLjA4IDExLjAzIDE3LjE1IDggMTIuNSA4eiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-user: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGFzcz0ianAtaWNvbjMiIGZpbGw9IiM2MTYxNjEiPgogICAgPHBhdGggZD0iTTE2IDdhNCA0IDAgMTEtOCAwIDQgNCAwIDAxOCAwek0xMiAxNGE3IDcgMCAwMC03IDdoMTRhNyA3IDAgMDAtNy03eiIvPgogIDwvZz4KPC9zdmc+);
--jp-icon-users: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDM2IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgY2xhc3M9ImpwLWljb24zIiB0cmFuc2Zvcm09Im1hdHJpeCgxLjczMjcgMCAwIDEuNzMyNyAtMy42MjgyIC4wOTk1NzcpIiBmaWxsPSIjNjE2MTYxIj4KICA8cGF0aCB0cmFuc2Zvcm09Im1hdHJpeCgxLjUsMCwwLDEuNSwwLC02KSIgZD0ibTEyLjE4NiA3LjUwOThjLTEuMDUzNSAwLTEuOTc1NyAwLjU2NjUtMi40Nzg1IDEuNDEwMiAwLjc1MDYxIDAuMzEyNzcgMS4zOTc0IDAuODI2NDggMS44NzMgMS40NzI3aDMuNDg2M2MwLTEuNTkyLTEuMjg4OS0yLjg4MjgtMi44ODA5LTIuODgyOHoiLz4KICA8cGF0aCBkPSJtMjAuNDY1IDIuMzg5NWEyLjE4ODUgMi4xODg1IDAgMCAxLTIuMTg4NCAyLjE4ODUgMi4xODg1IDIuMTg4NSAwIDAgMS0yLjE4ODUtMi4xODg1IDIuMTg4NSAyLjE4ODUgMCAwIDEgMi4xODg1LTIuMTg4NSAyLjE4ODUgMi4xODg1IDAgMCAxIDIuMTg4NCAyLjE4ODV6Ii8+CiAgPHBhdGggdHJhbnNmb3JtPSJtYXRyaXgoMS41LDAsMCwxLjUsMCwtNikiIGQ9Im0zLjU4OTggOC40MjE5Yy0xLjExMjYgMC0yLjAxMzcgMC45MDExMS0yLjAxMzcgMi4wMTM3aDIuODE0NWMwLjI2Nzk3LTAuMzczMDkgMC41OTA3LTAuNzA0MzUgMC45NTg5OC0wLjk3ODUyLTAuMzQ0MzMtMC42MTY4OC0xLjAwMzEtMS4wMzUyLTEuNzU5OC0xLjAzNTJ6Ii8+CiAgPHBhdGggZD0ibTYuOTE1NCA0LjYyM2ExLjUyOTQgMS41Mjk0IDAgMCAxLTEuNTI5NCAxLjUyOTQgMS41Mjk0IDEuNTI5NCAwIDAgMS0xLjUyOTQtMS41Mjk0IDEuNTI5NCAxLjUyOTQgMCAwIDEgMS41Mjk0LTEuNTI5NCAxLjUyOTQgMS41Mjk0IDAgMCAxIDEuNTI5NCAxLjUyOTR6Ii8+CiAgPHBhdGggZD0ibTYuMTM1IDEzLjUzNWMwLTMuMjM5MiAyLjYyNTktNS44NjUgNS44NjUtNS44NjUgMy4yMzkyIDAgNS44NjUgMi42MjU5IDUuODY1IDUuODY1eiIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMy43Njg1IiByPSIyLjk2ODUiLz4KIDwvZz4KPC9zdmc+Cg==);
--jp-icon-vega: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8ZyBjbGFzcz0ianAtaWNvbjEganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjMjEyMTIxIj4KICAgIDxwYXRoIGQ9Ik0xMC42IDUuNGwyLjItMy4ySDIuMnY3LjNsNC02LjZ6Ii8+CiAgICA8cGF0aCBkPSJNMTUuOCAyLjJsLTQuNCA2LjZMNyA2LjNsLTQuOCA4djUuNWgxNy42VjIuMmgtNHptLTcgMTUuNEg1LjV2LTQuNGgzLjN2NC40em00LjQgMEg5LjhWOS44aDMuNHY3Ljh6bTQuNCAwaC0zLjRWNi41aDMuNHYxMS4xeiIvPgogIDwvZz4KPC9zdmc+Cg==);
--jp-icon-yaml: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgdmlld0JveD0iMCAwIDIyIDIyIj4KICA8ZyBjbGFzcz0ianAtaWNvbi1jb250cmFzdDIganAtaWNvbi1zZWxlY3RhYmxlIiBmaWxsPSIjRDgxQjYwIj4KICAgIDxwYXRoIGQ9Ik03LjIgMTguNnYtNS40TDMgNS42aDMuM2wxLjQgMy4xYy4zLjkuNiAxLjYgMSAyLjUuMy0uOC42LTEuNiAxLTIuNWwxLjQtMy4xaDMuNGwtNC40IDcuNnY1LjVsLTIuOS0uMXoiLz4KICAgIDxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjE3LjYiIGN5PSIxNi41IiByPSIyLjEiLz4KICAgIDxjaXJjbGUgY2xhc3M9InN0MCIgY3g9IjE3LjYiIGN5PSIxMSIgcj0iMi4xIi8+CiAgPC9nPgo8L3N2Zz4K);
}
/* Icon <span class="caps">CSS</span> class declarations */
.jp-AddAboveIcon {
background-image: var(--jp-icon-add-above);
}
.jp-AddBelowIcon {
background-image: var(--jp-icon-add-below);
}
.jp-AddIcon {
background-image: var(--jp-icon-add);
}
.jp-BellIcon {
background-image: var(--jp-icon-bell);
}
.jp-BugDotIcon {
background-image: var(--jp-icon-bug-dot);
}
.jp-BugIcon {
background-image: var(--jp-icon-bug);
}
.jp-BuildIcon {
background-image: var(--jp-icon-build);
}
.jp-CaretDownEmptyIcon {
background-image: var(--jp-icon-caret-down-empty);
}
.jp-CaretDownEmptyThinIcon {
background-image: var(--jp-icon-caret-down-empty-thin);
}
.jp-CaretDownIcon {
background-image: var(--jp-icon-caret-down);
}
.jp-CaretLeftIcon {
background-image: var(--jp-icon-caret-left);
}
.jp-CaretRightIcon {
background-image: var(--jp-icon-caret-right);
}
.jp-CaretUpEmptyThinIcon {
background-image: var(--jp-icon-caret-up-empty-thin);
}
.jp-CaretUpIcon {
background-image: var(--jp-icon-caret-up);
}
.jp-CaseSensitiveIcon {
background-image: var(--jp-icon-case-sensitive);
}
.jp-CheckIcon {
background-image: var(--jp-icon-check);
}
.jp-CircleEmptyIcon {
background-image: var(--jp-icon-circle-empty);
}
.jp-CircleIcon {
background-image: var(--jp-icon-circle);
}
.jp-ClearIcon {
background-image: var(--jp-icon-clear);
}
.jp-CloseIcon {
background-image: var(--jp-icon-close);
}
.jp-CodeIcon {
background-image: var(--jp-icon-code);
}
.jp-ConsoleIcon {
background-image: var(--jp-icon-console);
}
.jp-CopyIcon {
background-image: var(--jp-icon-copy);
}
.jp-CopyrightIcon {
background-image: var(--jp-icon-copyright);
}
.jp-CutIcon {
background-image: var(--jp-icon-cut);
}
.jp-DeleteIcon {
background-image: var(--jp-icon-delete);
}
.jp-DownloadIcon {
background-image: var(--jp-icon-download);
}
.jp-DuplicateIcon {
background-image: var(--jp-icon-duplicate);
}
.jp-EditIcon {
background-image: var(--jp-icon-edit);
}
.jp-EllipsesIcon {
background-image: var(--jp-icon-ellipses);
}
.jp-ExtensionIcon {
background-image: var(--jp-icon-extension);
}
.jp-FastForwardIcon {
background-image: var(--jp-icon-fast-forward);
}
.jp-FileIcon {
background-image: var(--jp-icon-file);
}
.jp-FileUploadIcon {
background-image: var(--jp-icon-file-upload);
}
.jp-FilterListIcon {
background-image: var(--jp-icon-filter-list);
}
.jp-FolderFavoriteIcon {
background-image: var(--jp-icon-folder-favorite);
}
.jp-FolderIcon {
background-image: var(--jp-icon-folder);
}
.jp-HomeIcon {
background-image: var(--jp-icon-home);
}
.jp-Html5Icon {
background-image: var(--jp-icon-html5);
}
.jp-ImageIcon {
background-image: var(--jp-icon-image);
}
.jp-InspectorIcon {
background-image: var(--jp-icon-inspector);
}
.jp-JsonIcon {
background-image: var(--jp-icon-json);
}
.jp-JuliaIcon {
background-image: var(--jp-icon-julia);
}
.jp-JupyterFaviconIcon {
background-image: var(--jp-icon-jupyter-favicon);
}
.jp-JupyterIcon {
background-image: var(--jp-icon-jupyter);
}
.jp-JupyterlabWordmarkIcon {
background-image: var(--jp-icon-jupyterlab-wordmark);
}
.jp-KernelIcon {
background-image: var(--jp-icon-kernel);
}
.jp-KeyboardIcon {
background-image: var(--jp-icon-keyboard);
}
.jp-LaunchIcon {
background-image: var(--jp-icon-launch);
}
.jp-LauncherIcon {
background-image: var(--jp-icon-launcher);
}
.jp-LineFormIcon {
background-image: var(--jp-icon-line-form);
}
.jp-LinkIcon {
background-image: var(--jp-icon-link);
}
.jp-ListIcon {
background-image: var(--jp-icon-list);
}
.jp-ListingsInfoIcon {
background-image: var(--jp-icon-listings-info);
}
.jp-MarkdownIcon {
background-image: var(--jp-icon-markdown);
}
.jp-MoveDownIcon {
background-image: var(--jp-icon-move-down);
}
.jp-MoveUpIcon {
background-image: var(--jp-icon-move-up);
}
.jp-NewFolderIcon {
background-image: var(--jp-icon-new-folder);
}
.jp-NotTrustedIcon {
background-image: var(--jp-icon-not-trusted);
}
.jp-NotebookIcon {
background-image: var(--jp-icon-notebook);
}
.jp-NumberingIcon {
background-image: var(--jp-icon-numbering);
}
.jp-OfflineBoltIcon {
background-image: var(--jp-icon-offline-bolt);
}
.jp-PaletteIcon {
background-image: var(--jp-icon-palette);
}
.jp-PasteIcon {
background-image: var(--jp-icon-paste);
}
.jp-PdfIcon {
background-image: var(--jp-icon-pdf);
}
.jp-PythonIcon {
background-image: var(--jp-icon-python);
}
.jp-RKernelIcon {
background-image: var(--jp-icon-r-kernel);
}
.jp-ReactIcon {
background-image: var(--jp-icon-react);
}
.jp-RedoIcon {
background-image: var(--jp-icon-redo);
}
.jp-RefreshIcon {
background-image: var(--jp-icon-refresh);
}
.jp-RegexIcon {
background-image: var(--jp-icon-regex);
}
.jp-RunIcon {
background-image: var(--jp-icon-run);
}
.jp-RunningIcon {
background-image: var(--jp-icon-running);
}
.jp-SaveIcon {
background-image: var(--jp-icon-save);
}
.jp-SearchIcon {
background-image: var(--jp-icon-search);
}
.jp-SettingsIcon {
background-image: var(--jp-icon-settings);
}
.jp-ShareIcon {
background-image: var(--jp-icon-share);
}
.jp-SpreadsheetIcon {
background-image: var(--jp-icon-spreadsheet);
}
.jp-StopIcon {
background-image: var(--jp-icon-stop);
}
.jp-TabIcon {
background-image: var(--jp-icon-tab);
}
.jp-TableRowsIcon {
background-image: var(--jp-icon-table-rows);
}
.jp-TagIcon {
background-image: var(--jp-icon-tag);
}
.jp-TerminalIcon {
background-image: var(--jp-icon-terminal);
}
.jp-TextEditorIcon {
background-image: var(--jp-icon-text-editor);
}
.jp-TocIcon {
background-image: var(--jp-icon-toc);
}
.jp-TreeViewIcon {
background-image: var(--jp-icon-tree-view);
}
.jp-TrustedIcon {
background-image: var(--jp-icon-trusted);
}
.jp-UndoIcon {
background-image: var(--jp-icon-undo);
}
.jp-UserIcon {
background-image: var(--jp-icon-user);
}
.jp-UsersIcon {
background-image: var(--jp-icon-users);
}
.jp-VegaIcon {
background-image: var(--jp-icon-vega);
}
.jp-YamlIcon {
background-image: var(--jp-icon-yaml);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/**
* (<span class="caps">DEPRECATED</span>) Support for consuming icons as <span class="caps">CSS</span> background images
*/
.jp-Icon,
.jp-MaterialIcon {
background-position: center;
background-repeat: no-repeat;
background-size: 16px;
min-width: 16px;
min-height: 16px;
}
.jp-Icon-cover {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
/**
* (<span class="caps">DEPRECATED</span>) Support for specific <span class="caps">CSS</span> icon sizes
*/
.jp-Icon-16 {
background-size: 16px;
min-width: 16px;
min-height: 16px;
}
.jp-Icon-18 {
background-size: 18px;
min-width: 18px;
min-height: 18px;
}
.jp-Icon-20 {
background-size: 20px;
min-width: 20px;
min-height: 20px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.lm-TabBar .lm-TabBar-addButton {
align-items: center;
display: flex;
padding: 4px;
padding-bottom: 5px;
margin-right: 1px;
background-color: var(--jp-layout-color2);
}
.lm-TabBar .lm-TabBar-addButton:hover {
background-color: var(--jp-layout-color1);
}
.lm-DockPanel-tabBar .lm-TabBar-tab {
width: var(--jp-private-horizontal-tab-width);
}
.lm-DockPanel-tabBar .lm-TabBar-content {
flex: unset;
}
.lm-DockPanel-tabBar[data-orientation='horizontal'] {
flex: 1 1 auto;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/**
* Support for icons as inline <span class="caps">SVG</span> HTMLElements
*/
/* recolor the primary elements of an icon */
.jp-icon0[fill] {
fill: var(--jp-inverse-layout-color0);
}
.jp-icon1[fill] {
fill: var(--jp-inverse-layout-color1);
}
.jp-icon2[fill] {
fill: var(--jp-inverse-layout-color2);
}
.jp-icon3[fill] {
fill: var(--jp-inverse-layout-color3);
}
.jp-icon4[fill] {
fill: var(--jp-inverse-layout-color4);
}
.jp-icon0[stroke] {
stroke: var(--jp-inverse-layout-color0);
}
.jp-icon1[stroke] {
stroke: var(--jp-inverse-layout-color1);
}
.jp-icon2[stroke] {
stroke: var(--jp-inverse-layout-color2);
}
.jp-icon3[stroke] {
stroke: var(--jp-inverse-layout-color3);
}
.jp-icon4[stroke] {
stroke: var(--jp-inverse-layout-color4);
}
/* recolor the accent elements of an icon */
.jp-icon-accent0[fill] {
fill: var(--jp-layout-color0);
}
.jp-icon-accent1[fill] {
fill: var(--jp-layout-color1);
}
.jp-icon-accent2[fill] {
fill: var(--jp-layout-color2);
}
.jp-icon-accent3[fill] {
fill: var(--jp-layout-color3);
}
.jp-icon-accent4[fill] {
fill: var(--jp-layout-color4);
}
.jp-icon-accent0[stroke] {
stroke: var(--jp-layout-color0);
}
.jp-icon-accent1[stroke] {
stroke: var(--jp-layout-color1);
}
.jp-icon-accent2[stroke] {
stroke: var(--jp-layout-color2);
}
.jp-icon-accent3[stroke] {
stroke: var(--jp-layout-color3);
}
.jp-icon-accent4[stroke] {
stroke: var(--jp-layout-color4);
}
/* set the color of an icon to transparent */
.jp-icon-none[fill] {
fill: none;
}
.jp-icon-none[stroke] {
stroke: none;
}
/* brand icon colors. Same for light and dark */
.jp-icon-brand0[fill] {
fill: var(--jp-brand-color0);
}
.jp-icon-brand1[fill] {
fill: var(--jp-brand-color1);
}
.jp-icon-brand2[fill] {
fill: var(--jp-brand-color2);
}
.jp-icon-brand3[fill] {
fill: var(--jp-brand-color3);
}
.jp-icon-brand4[fill] {
fill: var(--jp-brand-color4);
}
.jp-icon-brand0[stroke] {
stroke: var(--jp-brand-color0);
}
.jp-icon-brand1[stroke] {
stroke: var(--jp-brand-color1);
}
.jp-icon-brand2[stroke] {
stroke: var(--jp-brand-color2);
}
.jp-icon-brand3[stroke] {
stroke: var(--jp-brand-color3);
}
.jp-icon-brand4[stroke] {
stroke: var(--jp-brand-color4);
}
/* warn icon colors. Same for light and dark */
.jp-icon-warn0[fill] {
fill: var(--jp-warn-color0);
}
.jp-icon-warn1[fill] {
fill: var(--jp-warn-color1);
}
.jp-icon-warn2[fill] {
fill: var(--jp-warn-color2);
}
.jp-icon-warn3[fill] {
fill: var(--jp-warn-color3);
}
.jp-icon-warn0[stroke] {
stroke: var(--jp-warn-color0);
}
.jp-icon-warn1[stroke] {
stroke: var(--jp-warn-color1);
}
.jp-icon-warn2[stroke] {
stroke: var(--jp-warn-color2);
}
.jp-icon-warn3[stroke] {
stroke: var(--jp-warn-color3);
}
/* icon colors that contrast well with each other and most backgrounds */
.jp-icon-contrast0[fill] {
fill: var(--jp-icon-contrast-color0);
}
.jp-icon-contrast1[fill] {
fill: var(--jp-icon-contrast-color1);
}
.jp-icon-contrast2[fill] {
fill: var(--jp-icon-contrast-color2);
}
.jp-icon-contrast3[fill] {
fill: var(--jp-icon-contrast-color3);
}
.jp-icon-contrast0[stroke] {
stroke: var(--jp-icon-contrast-color0);
}
.jp-icon-contrast1[stroke] {
stroke: var(--jp-icon-contrast-color1);
}
.jp-icon-contrast2[stroke] {
stroke: var(--jp-icon-contrast-color2);
}
.jp-icon-contrast3[stroke] {
stroke: var(--jp-icon-contrast-color3);
}
.jp-jupyter-icon-color[fill] {
fill: var(--jp-jupyter-icon-color, var(--jp-warn-color0));
}
.jp-notebook-icon-color[fill] {
fill: var(--jp-notebook-icon-color, var(--jp-warn-color0));
}
.jp-json-icon-color[fill] {
fill: var(--jp-json-icon-color, var(--jp-warn-color1));
}
.jp-console-icon-color[fill] {
fill: var(--jp-console-icon-color, white);
}
.jp-console-icon-background-color[fill] {
fill: var(--jp-console-icon-background-color, var(--jp-brand-color1));
}
.jp-terminal-icon-color[fill] {
fill: var(--jp-terminal-icon-color, var(--jp-layout-color2));
}
.jp-terminal-icon-background-color[fill] {
fill: var(--jp-terminal-icon-background-color, var(--jp-inverse-layout2));
}
.jp-text-editor-icon-color[fill] {
fill: var(--jp-text-editor-icon-color, var(--jp-inverse-layout3));
}
.jp-inspector-icon-color[fill] {
fill: var(--jp-inspector-icon-color, var(--jp-inverse-layout3));
}
/* <span class="caps">CSS</span> for icons in selected filebrowser listing items */
.jp-DirListing-item.jp-mod-selected .jp-icon-selectable[fill] {
fill: #fff;
}
.jp-DirListing-item.jp-mod-selected .jp-icon-selectable-inverse[fill] {
fill: var(--jp-brand-color1);
}
/* <span class="caps">CSS</span> for icons in selected tabs in the sidebar tab manager */
#tab-manager .lm-TabBar-tab.jp-mod-active .jp-icon-selectable[fill] {
fill: #fff;
}
#tab-manager .lm-TabBar-tab.jp-mod-active .jp-icon-selectable-inverse[fill] {
fill: var(--jp-brand-color1);
}
#tab-manager
.lm-TabBar-tab.jp-mod-active
.jp-icon-hover
:hover
.jp-icon-selectable[fill] {
fill: var(--jp-brand-color1);
}
#tab-manager
.lm-TabBar-tab.jp-mod-active
.jp-icon-hover
:hover
.jp-icon-selectable-inverse[fill] {
fill: #fff;
}
/**
* <span class="caps">TODO</span>: come up with non css-hack solution for showing the busy icon on top
* of the close icon
* <span class="caps">CSS</span> for complex behavior of close icon of tabs in the sidebar tab manager
*/
#tab-manager
.lm-TabBar-tab.jp-mod-dirty
> .lm-TabBar-tabCloseIcon
> :not(:hover)
> .jp-icon3[fill] {
fill: none;
}
#tab-manager
.lm-TabBar-tab.jp-mod-dirty
> .lm-TabBar-tabCloseIcon
> :not(:hover)
> .jp-icon-busy[fill] {
fill: var(--jp-inverse-layout-color3);
}
#tab-manager
.lm-TabBar-tab.jp-mod-dirty.jp-mod-active
> .lm-TabBar-tabCloseIcon
> :not(:hover)
> .jp-icon-busy[fill] {
fill: #fff;
}
/**
* <span class="caps">TODO</span>: come up with non css-hack solution for showing the busy icon on top
* of the close icon
* <span class="caps">CSS</span> for complex behavior of close icon of tabs in the main area tabbar
*/
.lm-DockPanel-tabBar
.lm-TabBar-tab.lm-mod-closable.jp-mod-dirty
> .lm-TabBar-tabCloseIcon
> :not(:hover)
> .jp-icon3[fill] {
fill: none;
}
.lm-DockPanel-tabBar
.lm-TabBar-tab.lm-mod-closable.jp-mod-dirty
> .lm-TabBar-tabCloseIcon
> :not(:hover)
> .jp-icon-busy[fill] {
fill: var(--jp-inverse-layout-color3);
}
/* <span class="caps">CSS</span> for icons in status bar */
#jp-main-statusbar .jp-mod-selected .jp-icon-selectable[fill] {
fill: #fff;
}
#jp-main-statusbar .jp-mod-selected .jp-icon-selectable-inverse[fill] {
fill: var(--jp-brand-color1);
}
/* special handling for splash icon <span class="caps">CSS</span>. While the theme <span class="caps">CSS</span> reloads during
splash, the splash icon can loose theming. To prevent that, we set a
default for its color variable */
:root {
--jp-warn-color0: var(--md-orange-700);
}
/* not sure what to do with this one, used in filebrowser listing */
.jp-DragIcon {
margin-right: 4px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/**
* Support for alt colors for icons as inline <span class="caps">SVG</span> HTMLElements
*/
/* alt recolor the primary elements of an icon */
.jp-icon-alt .jp-icon0[fill] {
fill: var(--jp-layout-color0);
}
.jp-icon-alt .jp-icon1[fill] {
fill: var(--jp-layout-color1);
}
.jp-icon-alt .jp-icon2[fill] {
fill: var(--jp-layout-color2);
}
.jp-icon-alt .jp-icon3[fill] {
fill: var(--jp-layout-color3);
}
.jp-icon-alt .jp-icon4[fill] {
fill: var(--jp-layout-color4);
}
.jp-icon-alt .jp-icon0[stroke] {
stroke: var(--jp-layout-color0);
}
.jp-icon-alt .jp-icon1[stroke] {
stroke: var(--jp-layout-color1);
}
.jp-icon-alt .jp-icon2[stroke] {
stroke: var(--jp-layout-color2);
}
.jp-icon-alt .jp-icon3[stroke] {
stroke: var(--jp-layout-color3);
}
.jp-icon-alt .jp-icon4[stroke] {
stroke: var(--jp-layout-color4);
}
/* alt recolor the accent elements of an icon */
.jp-icon-alt .jp-icon-accent0[fill] {
fill: var(--jp-inverse-layout-color0);
}
.jp-icon-alt .jp-icon-accent1[fill] {
fill: var(--jp-inverse-layout-color1);
}
.jp-icon-alt .jp-icon-accent2[fill] {
fill: var(--jp-inverse-layout-color2);
}
.jp-icon-alt .jp-icon-accent3[fill] {
fill: var(--jp-inverse-layout-color3);
}
.jp-icon-alt .jp-icon-accent4[fill] {
fill: var(--jp-inverse-layout-color4);
}
.jp-icon-alt .jp-icon-accent0[stroke] {
stroke: var(--jp-inverse-layout-color0);
}
.jp-icon-alt .jp-icon-accent1[stroke] {
stroke: var(--jp-inverse-layout-color1);
}
.jp-icon-alt .jp-icon-accent2[stroke] {
stroke: var(--jp-inverse-layout-color2);
}
.jp-icon-alt .jp-icon-accent3[stroke] {
stroke: var(--jp-inverse-layout-color3);
}
.jp-icon-alt .jp-icon-accent4[stroke] {
stroke: var(--jp-inverse-layout-color4);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-icon-hoverShow:not(:hover) .jp-icon-hoverShow-content {
display: none !important;
}
/**
* Support for hover colors for icons as inline <span class="caps">SVG</span> HTMLElements
*/
/**
* regular colors
*/
/* recolor the primary elements of an icon */
.jp-icon-hover :hover .jp-icon0-hover[fill] {
fill: var(--jp-inverse-layout-color0);
}
.jp-icon-hover :hover .jp-icon1-hover[fill] {
fill: var(--jp-inverse-layout-color1);
}
.jp-icon-hover :hover .jp-icon2-hover[fill] {
fill: var(--jp-inverse-layout-color2);
}
.jp-icon-hover :hover .jp-icon3-hover[fill] {
fill: var(--jp-inverse-layout-color3);
}
.jp-icon-hover :hover .jp-icon4-hover[fill] {
fill: var(--jp-inverse-layout-color4);
}
.jp-icon-hover :hover .jp-icon0-hover[stroke] {
stroke: var(--jp-inverse-layout-color0);
}
.jp-icon-hover :hover .jp-icon1-hover[stroke] {
stroke: var(--jp-inverse-layout-color1);
}
.jp-icon-hover :hover .jp-icon2-hover[stroke] {
stroke: var(--jp-inverse-layout-color2);
}
.jp-icon-hover :hover .jp-icon3-hover[stroke] {
stroke: var(--jp-inverse-layout-color3);
}
.jp-icon-hover :hover .jp-icon4-hover[stroke] {
stroke: var(--jp-inverse-layout-color4);
}
/* recolor the accent elements of an icon */
.jp-icon-hover :hover .jp-icon-accent0-hover[fill] {
fill: var(--jp-layout-color0);
}
.jp-icon-hover :hover .jp-icon-accent1-hover[fill] {
fill: var(--jp-layout-color1);
}
.jp-icon-hover :hover .jp-icon-accent2-hover[fill] {
fill: var(--jp-layout-color2);
}
.jp-icon-hover :hover .jp-icon-accent3-hover[fill] {
fill: var(--jp-layout-color3);
}
.jp-icon-hover :hover .jp-icon-accent4-hover[fill] {
fill: var(--jp-layout-color4);
}
.jp-icon-hover :hover .jp-icon-accent0-hover[stroke] {
stroke: var(--jp-layout-color0);
}
.jp-icon-hover :hover .jp-icon-accent1-hover[stroke] {
stroke: var(--jp-layout-color1);
}
.jp-icon-hover :hover .jp-icon-accent2-hover[stroke] {
stroke: var(--jp-layout-color2);
}
.jp-icon-hover :hover .jp-icon-accent3-hover[stroke] {
stroke: var(--jp-layout-color3);
}
.jp-icon-hover :hover .jp-icon-accent4-hover[stroke] {
stroke: var(--jp-layout-color4);
}
/* set the color of an icon to transparent */
.jp-icon-hover :hover .jp-icon-none-hover[fill] {
fill: none;
}
.jp-icon-hover :hover .jp-icon-none-hover[stroke] {
stroke: none;
}
/**
* inverse colors
*/
/* inverse recolor the primary elements of an icon */
.jp-icon-hover.jp-icon-alt :hover .jp-icon0-hover[fill] {
fill: var(--jp-layout-color0);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon1-hover[fill] {
fill: var(--jp-layout-color1);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon2-hover[fill] {
fill: var(--jp-layout-color2);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon3-hover[fill] {
fill: var(--jp-layout-color3);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon4-hover[fill] {
fill: var(--jp-layout-color4);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon0-hover[stroke] {
stroke: var(--jp-layout-color0);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon1-hover[stroke] {
stroke: var(--jp-layout-color1);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon2-hover[stroke] {
stroke: var(--jp-layout-color2);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon3-hover[stroke] {
stroke: var(--jp-layout-color3);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon4-hover[stroke] {
stroke: var(--jp-layout-color4);
}
/* inverse recolor the accent elements of an icon */
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent0-hover[fill] {
fill: var(--jp-inverse-layout-color0);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent1-hover[fill] {
fill: var(--jp-inverse-layout-color1);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent2-hover[fill] {
fill: var(--jp-inverse-layout-color2);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent3-hover[fill] {
fill: var(--jp-inverse-layout-color3);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent4-hover[fill] {
fill: var(--jp-inverse-layout-color4);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent0-hover[stroke] {
stroke: var(--jp-inverse-layout-color0);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent1-hover[stroke] {
stroke: var(--jp-inverse-layout-color1);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent2-hover[stroke] {
stroke: var(--jp-inverse-layout-color2);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent3-hover[stroke] {
stroke: var(--jp-inverse-layout-color3);
}
.jp-icon-hover.jp-icon-alt :hover .jp-icon-accent4-hover[stroke] {
stroke: var(--jp-inverse-layout-color4);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-switch {
display: flex;
align-items: center;
padding-left: 4px;
padding-right: 4px;
font-size: var(--jp-ui-font-size1);
background-color: transparent;
color: var(--jp-ui-font-color1);
border: none;
height: 20px;
}
.jp-switch:hover {
background-color: var(--jp-layout-color2);
}
.jp-switch-label {
margin-right: 5px;
}
.jp-switch-track {
cursor: pointer;
background-color: var(--jp-switch-color, var(--jp-border-color1));
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 34px;
height: 16px;
width: 35px;
position: relative;
}
.jp-switch-track::before {
content: '';
position: absolute;
height: 10px;
width: 10px;
margin: 3px;
left: 0px;
background-color: var(--jp-ui-inverse-font-color1);
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 50%;
}
.jp-switch[aria-checked='true'] .jp-switch-track {
background-color: var(--jp-switch-true-position-color, var(--jp-warn-color0));
}
.jp-switch[aria-checked='true'] .jp-switch-track::before {
/* track width (35) - margins (3 + 3) - thumb width (10) */
left: 19px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/* Sibling imports */
/* Override Blueprint's _reset.scss styles */
html {
box-sizing: unset;
}
*,
*::before,
*::after {
box-sizing: unset;
}
body {
color: unset;
font-family: var(--jp-ui-font-family);
}
p {
margin-top: unset;
margin-bottom: unset;
}
small {
font-size: unset;
}
strong {
font-weight: unset;
}
/* Override Blueprint's _typography.scss styles */
a {
text-decoration: unset;
color: unset;
}
a:hover {
text-decoration: unset;
color: unset;
}
/* Override Blueprint's _accessibility.scss styles */
:focus {
outline: unset;
outline-offset: unset;
-moz-outline-radius: unset;
}
/* Styles for ui-components */
.jp-Button {
border-radius: var(--jp-border-radius);
padding: 0px 12px;
font-size: var(--jp-ui-font-size1);
}
/* Use our own theme for hover styles */
button.jp-Button.bp3-button.bp3-minimal:hover {
background-color: var(--jp-layout-color2);
}
.jp-Button.minimal {
color: unset !important;
}
.jp-Button.jp-ToolbarButtonComponent {
text-transform: none;
}
.jp-InputGroup input {
box-sizing: border-box;
border-radius: 0;
background-color: transparent;
color: var(--jp-ui-font-color0);
box-shadow: inset 0 0 0 var(--jp-border-width) var(--jp-input-border-color);
}
.jp-InputGroup input:focus {
box-shadow: inset 0 0 0 var(--jp-border-width)
var(--jp-input-active-box-shadow-color),
inset 0 0 0 3px var(--jp-input-active-box-shadow-color);
}
.jp-InputGroup input::placeholder,
input::placeholder {
color: var(--jp-ui-font-color3);
}
.jp-BPIcon {
display: inline-block;
vertical-align: middle;
margin: auto;
}
/* Stop blueprint futzing with our icon fills */
.bp3-icon.jp-BPIcon > svg:not([fill]) {
fill: var(--jp-inverse-layout-color3);
}
.jp-InputGroupAction {
padding: 6px;
}
.jp-HTMLSelect.jp-DefaultStyle select {
background-color: initial;
border: none;
border-radius: 0;
box-shadow: none;
color: var(--jp-ui-font-color0);
display: block;
font-size: var(--jp-ui-font-size1);
height: 24px;
line-height: 14px;
padding: 0 25px 0 10px;
text-align: left;
-moz-appearance: none;
-webkit-appearance: none;
}
/* Use our own theme for hover and option styles */
.jp-HTMLSelect.jp-DefaultStyle select:hover,
.jp-HTMLSelect.jp-DefaultStyle select > option {
background-color: var(--jp-layout-color2);
color: var(--jp-ui-font-color0);
}
select {
box-sizing: border-box;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-Collapse {
display: flex;
flex-direction: column;
align-items: stretch;
border-top: 1px solid var(--jp-border-color2);
border-bottom: 1px solid var(--jp-border-color2);
}
.jp-Collapse-header {
padding: 1px 12px;
color: var(--jp-ui-font-color1);
background-color: var(--jp-layout-color1);
font-size: var(--jp-ui-font-size2);
}
.jp-Collapse-header:hover {
background-color: var(--jp-layout-color2);
}
.jp-Collapse-contents {
padding: 0px 12px 0px 12px;
background-color: var(--jp-layout-color1);
color: var(--jp-ui-font-color1);
overflow: auto;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Variables
|----------------------------------------------------------------------------*/
:root {
--jp-private-commandpalette-search-height: 28px;
}
/*-----------------------------------------------------------------------------
| Overall styles
|----------------------------------------------------------------------------*/
.lm-CommandPalette {
padding-bottom: 0px;
color: var(--jp-ui-font-color1);
background: var(--jp-layout-color1);
/* This is needed so that all font sizing of children done in ems is
* relative to this base size */
font-size: var(--jp-ui-font-size1);
}
/*-----------------------------------------------------------------------------
| Modal variant
|----------------------------------------------------------------------------*/
.jp-ModalCommandPalette {
position: absolute;
z-index: 10000;
top: 38px;
left: 30%;
margin: 0;
padding: 4px;
width: 40%;
box-shadow: var(--jp-elevation-z4);
border-radius: 4px;
background: var(--jp-layout-color0);
}
.jp-ModalCommandPalette .lm-CommandPalette {
max-height: 40vh;
}
.jp-ModalCommandPalette .lm-CommandPalette .lm-close-icon::after {
display: none;
}
.jp-ModalCommandPalette .lm-CommandPalette .lm-CommandPalette-header {
display: none;
}
.jp-ModalCommandPalette .lm-CommandPalette .lm-CommandPalette-item {
margin-left: 4px;
margin-right: 4px;
}
.jp-ModalCommandPalette
.lm-CommandPalette
.lm-CommandPalette-item.lm-mod-disabled {
display: none;
}
/*-----------------------------------------------------------------------------
| Search
|----------------------------------------------------------------------------*/
.lm-CommandPalette-search {
padding: 4px;
background-color: var(--jp-layout-color1);
z-index: 2;
}
.lm-CommandPalette-wrapper {
overflow: overlay;
padding: 0px 9px;
background-color: var(--jp-input-active-background);
height: 30px;
box-shadow: inset 0 0 0 var(--jp-border-width) var(--jp-input-border-color);
}
.lm-CommandPalette.lm-mod-focused .lm-CommandPalette-wrapper {
box-shadow: inset 0 0 0 1px var(--jp-input-active-box-shadow-color),
inset 0 0 0 3px var(--jp-input-active-box-shadow-color);
}
.jp-SearchIconGroup {
color: white;
background-color: var(--jp-brand-color1);
position: absolute;
top: 4px;
right: 4px;
padding: 5px 5px 1px 5px;
}
.jp-SearchIconGroup svg {
height: 20px;
width: 20px;
}
.jp-SearchIconGroup .jp-icon3[fill] {
fill: var(--jp-layout-color0);
}
.lm-CommandPalette-input {
background: transparent;
width: calc(100% - 18px);
float: left;
border: none;
outline: none;
font-size: var(--jp-ui-font-size1);
color: var(--jp-ui-font-color0);
line-height: var(--jp-private-commandpalette-search-height);
}
.lm-CommandPalette-input::-webkit-input-placeholder,
.lm-CommandPalette-input::-moz-placeholder,
.lm-CommandPalette-input:-ms-input-placeholder {
color: var(--jp-ui-font-color2);
font-size: var(--jp-ui-font-size1);
}
/*-----------------------------------------------------------------------------
| Results
|----------------------------------------------------------------------------*/
.lm-CommandPalette-header:first-child {
margin-top: 0px;
}
.lm-CommandPalette-header {
border-bottom: solid var(--jp-border-width) var(--jp-border-color2);
color: var(--jp-ui-font-color1);
cursor: pointer;
display: flex;
font-size: var(--jp-ui-font-size0);
font-weight: 600;
letter-spacing: 1px;
margin-top: 8px;
padding: 8px 0 8px 12px;
text-transform: uppercase;
}
.lm-CommandPalette-header.lm-mod-active {
background: var(--jp-layout-color2);
}
.lm-CommandPalette-header > mark {
background-color: transparent;
font-weight: bold;
color: var(--jp-ui-font-color1);
}
.lm-CommandPalette-item {
padding: 4px 12px 4px 4px;
color: var(--jp-ui-font-color1);
font-size: var(--jp-ui-font-size1);
font-weight: 400;
display: flex;
}
.lm-CommandPalette-item.lm-mod-disabled {
color: var(--jp-ui-font-color2);
}
.lm-CommandPalette-item.lm-mod-active {
color: var(--jp-ui-inverse-font-color1);
background: var(--jp-brand-color1);
}
.lm-CommandPalette-item.lm-mod-active .lm-CommandPalette-itemLabel > mark {
color: var(--jp-ui-inverse-font-color0);
}
.lm-CommandPalette-item.lm-mod-active .jp-icon-selectable[fill] {
fill: var(--jp-layout-color0);
}
.lm-CommandPalette-item.lm-mod-active .lm-CommandPalette-itemLabel > mark {
color: var(--jp-ui-inverse-font-color0);
}
.lm-CommandPalette-item.lm-mod-active:hover:not(.lm-mod-disabled) {
color: var(--jp-ui-inverse-font-color1);
background: var(--jp-brand-color1);
}
.lm-CommandPalette-item:hover:not(.lm-mod-active):not(.lm-mod-disabled) {
background: var(--jp-layout-color2);
}
.lm-CommandPalette-itemContent {
overflow: hidden;
}
.lm-CommandPalette-itemLabel > mark {
color: var(--jp-ui-font-color0);
background-color: transparent;
font-weight: bold;
}
.lm-CommandPalette-item.lm-mod-disabled mark {
color: var(--jp-ui-font-color2);
}
.lm-CommandPalette-item .lm-CommandPalette-itemIcon {
margin: 0 4px 0 0;
position: relative;
width: 16px;
top: 2px;
flex: 0 0 auto;
}
.lm-CommandPalette-item.lm-mod-disabled .lm-CommandPalette-itemIcon {
opacity: 0.6;
}
.lm-CommandPalette-item .lm-CommandPalette-itemShortcut {
flex: 0 0 auto;
}
.lm-CommandPalette-itemCaption {
display: none;
}
.lm-CommandPalette-content {
background-color: var(--jp-layout-color1);
}
.lm-CommandPalette-content:empty:after {
content: 'No results';
margin: auto;
margin-top: 20px;
width: 100px;
display: block;
font-size: var(--jp-ui-font-size2);
font-family: var(--jp-ui-font-family);
font-weight: lighter;
}
.lm-CommandPalette-emptyMessage {
text-align: center;
margin-top: 24px;
line-height: 1.32;
padding: 0px 8px;
color: var(--jp-content-font-color3);
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-Dialog {
position: absolute;
z-index: 10000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
top: 0px;
left: 0px;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: var(--jp-dialog-background);
}
.jp-Dialog-content {
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: auto;
background: var(--jp-layout-color1);
padding: 24px 24px 12px 24px;
min-width: 300px;
min-height: 150px;
max-width: 1000px;
max-height: 500px;
box-sizing: border-box;
box-shadow: var(--jp-elevation-z20);
word-wrap: break-word;
border-radius: var(--jp-border-radius);
/* This is needed so that all font sizing of children done in ems is
* relative to this base size */
font-size: var(--jp-ui-font-size1);
color: var(--jp-ui-font-color1);
resize: both;
}
.jp-Dialog-content.jp-Dialog-content-small {
max-width: 500px;
}
.jp-Dialog-button {
overflow: visible;
}
button.jp-Dialog-button:focus {
outline: 1px solid var(--jp-brand-color1);
outline-offset: 4px;
-moz-outline-radius: 0px;
}
button.jp-Dialog-button:focus::-moz-focus-inner {
border: 0;
}
button.jp-Dialog-button.jp-mod-styled.jp-mod-accept:focus,
button.jp-Dialog-button.jp-mod-styled.jp-mod-warn:focus,
button.jp-Dialog-button.jp-mod-styled.jp-mod-reject:focus {
outline-offset: 4px;
-moz-outline-radius: 0px;
}
button.jp-Dialog-button.jp-mod-styled.jp-mod-accept:focus {
outline: 1px solid var(--md-blue-700);
}
button.jp-Dialog-button.jp-mod-styled.jp-mod-warn:focus {
outline: 1px solid var(--md-red-600);
}
button.jp-Dialog-button.jp-mod-styled.jp-mod-reject:focus {
outline: 1px solid var(--md-grey-700);
}
button.jp-Dialog-close-button {
padding: 0;
height: 100%;
min-width: unset;
min-height: unset;
}
.jp-Dialog-header {
display: flex;
justify-content: space-between;
flex: 0 0 auto;
padding-bottom: 12px;
font-size: var(--jp-ui-font-size3);
font-weight: 400;
color: var(--jp-ui-font-color0);
}
.jp-Dialog-body {
display: flex;
flex-direction: column;
flex: 1 1 auto;
font-size: var(--jp-ui-font-size1);
background: var(--jp-layout-color1);
overflow: auto;
}
.jp-Dialog-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
flex: 0 0 auto;
margin-left: -12px;
margin-right: -12px;
padding: 12px;
}
.jp-Dialog-checkbox {
padding-right: 5px;
}
.jp-Dialog-checkbox > input:focus-visible {
outline: 1px solid var(--jp-input-active-border-color);
outline-offset: 1px;
}
.jp-Dialog-spacer {
flex: 1 1 auto;
}
.jp-Dialog-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.jp-Dialog-body > .jp-select-wrapper {
width: 100%;
}
.jp-Dialog-body > button {
padding: 0px 16px;
}
.jp-Dialog-body > label {
line-height: 1.4;
color: var(--jp-ui-font-color0);
}
.jp-Dialog-button.jp-mod-styled:not(:last-child) {
margin-right: 12px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2016, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-HoverBox {
position: fixed;
}
.jp-HoverBox.jp-mod-outofview {
display: none;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-IFrame {
width: 100%;
height: 100%;
}
.jp-IFrame > iframe {
border: none;
}
/*
When drag events occur, `p-mod-override-cursor` is added to the body.
Because iframes steal all cursor events, the following two rules are necessary
to suppress pointer events while resize drags are occurring. There may be a
better solution to this problem.
*/
body.lm-mod-override-cursor .jp-IFrame {
position: relative;
}
body.lm-mod-override-cursor .jp-IFrame:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
.jp-Input-Boolean-Dialog {
flex-direction: row-reverse;
align-items: end;
width: 100%;
}
.jp-Input-Boolean-Dialog > label {
flex: 1 1 auto;
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2016, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-MainAreaWidget > :focus {
outline: none;
}
.jp-MainAreaWidget .jp-MainAreaWidget-error {
padding: 6px;
}
.jp-MainAreaWidget .jp-MainAreaWidget-error > pre {
width: auto;
padding: 10px;
background: var(--jp-error-color3);
border: var(--jp-border-width) solid var(--jp-error-color1);
border-radius: var(--jp-border-radius);
color: var(--jp-ui-font-color1);
font-size: var(--jp-ui-font-size1);
white-space: pre-wrap;
word-wrap: break-word;
}
.jp-MainAreaWidget {
contain: strict;
}
/**
* google-material-color v1.2.6
* https://github.com/danlevan/google-material-color
*/
:root {
--md-red-50: #ffebee;
--md-red-100: #ffcdd2;
--md-red-200: #ef9a9a;
--md-red-300: #e57373;
--md-red-400: #ef5350;
--md-red-500: #f44336;
--md-red-600: #e53935;
--md-red-700: #d32f2f;
--md-red-800: #c62828;
--md-red-900: #b71c1c;
--md-red-A100: #ff8a80;
--md-red-A200: #ff5252;
--md-red-A400: #ff1744;
--md-red-A700: #d50000;
--md-pink-50: #fce4ec;
--md-pink-100: #f8bbd0;
--md-pink-200: #f48fb1;
--md-pink-300: #f06292;
--md-pink-400: #ec407a;
--md-pink-500: #e91e63;
--md-pink-600: #d81b60;
--md-pink-700: #c2185b;
--md-pink-800: #ad1457;
--md-pink-900: #880e4f;
--md-pink-A100: #ff80ab;
--md-pink-A200: #ff4081;
--md-pink-A400: #f50057;
--md-pink-A700: #c51162;
--md-purple-50: #f3e5f5;
--md-purple-100: #e1bee7;
--md-purple-200: #ce93d8;
--md-purple-300: #ba68c8;
--md-purple-400: #ab47bc;
--md-purple-500: #9c27b0;
--md-purple-600: #8e24aa;
--md-purple-700: #7b1fa2;
--md-purple-800: #6a1b9a;
--md-purple-900: #4a148c;
--md-purple-A100: #ea80fc;
--md-purple-A200: #e040fb;
--md-purple-A400: #d500f9;
--md-purple-A700: #aa00ff;
--md-deep-purple-50: #ede7f6;
--md-deep-purple-100: #d1c4e9;
--md-deep-purple-200: #b39ddb;
--md-deep-purple-300: #9575cd;
--md-deep-purple-400: #7e57c2;
--md-deep-purple-500: #673ab7;
--md-deep-purple-600: #5e35b1;
--md-deep-purple-700: #512da8;
--md-deep-purple-800: #4527a0;
--md-deep-purple-900: #311b92;
--md-deep-purple-A100: #b388ff;
--md-deep-purple-A200: #7c4dff;
--md-deep-purple-A400: #651fff;
--md-deep-purple-A700: #6200ea;
--md-indigo-50: #e8eaf6;
--md-indigo-100: #c5cae9;
--md-indigo-200: #9fa8da;
--md-indigo-300: #7986cb;
--md-indigo-400: #5c6bc0;
--md-indigo-500: #3f51b5;
--md-indigo-600: #3949ab;
--md-indigo-700: #303f9f;
--md-indigo-800: #283593;
--md-indigo-900: #1a237e;
--md-indigo-A100: #8c9eff;
--md-indigo-A200: #536dfe;
--md-indigo-A400: #3d5afe;
--md-indigo-A700: #304ffe;
--md-blue-50: #e3f2fd;
--md-blue-100: #bbdefb;
--md-blue-200: #90caf9;
--md-blue-300: #64b5f6;
--md-blue-400: #42a5f5;
--md-blue-500: #2196f3;
--md-blue-600: #1e88e5;
--md-blue-700: #1976d2;
--md-blue-800: #1565c0;
--md-blue-900: #0d47a1;
--md-blue-A100: #82b1ff;
--md-blue-A200: #448aff;
--md-blue-A400: #2979ff;
--md-blue-A700: #2962ff;
--md-light-blue-50: #e1f5fe;
--md-light-blue-100: #b3e5fc;
--md-light-blue-200: #81d4fa;
--md-light-blue-300: #4fc3f7;
--md-light-blue-400: #29b6f6;
--md-light-blue-500: #03a9f4;
--md-light-blue-600: #039be5;
--md-light-blue-700: #0288d1;
--md-light-blue-800: #0277bd;
--md-light-blue-900: #01579b;
--md-light-blue-A100: #80d8ff;
--md-light-blue-A200: #40c4ff;
--md-light-blue-A400: #00b0ff;
--md-light-blue-A700: #0091ea;
--md-cyan-50: #e0f7fa;
--md-cyan-100: #b2ebf2;
--md-cyan-200: #80deea;
--md-cyan-300: #4dd0e1;
--md-cyan-400: #26c6da;
--md-cyan-500: #00bcd4;
--md-cyan-600: #00acc1;
--md-cyan-700: #0097a7;
--md-cyan-800: #00838f;
--md-cyan-900: #006064;
--md-cyan-A100: #84ffff;
--md-cyan-A200: #18ffff;
--md-cyan-A400: #00e5ff;
--md-cyan-A700: #00b8d4;
--md-teal-50: #e0f2f1;
--md-teal-100: #b2dfdb;
--md-teal-200: #80cbc4;
--md-teal-300: #4db6ac;
--md-teal-400: #26a69a;
--md-teal-500: #009688;
--md-teal-600: #00897b;
--md-teal-700: #00796b;
--md-teal-800: #00695c;
--md-teal-900: #004d40;
--md-teal-A100: #a7ffeb;
--md-teal-A200: #64ffda;
--md-teal-A400: #1de9b6;
--md-teal-A700: #00bfa5;
--md-green-50: #e8f5e9;
--md-green-100: #c8e6c9;
--md-green-200: #a5d6a7;
--md-green-300: #81c784;
--md-green-400: #66bb6a;
--md-green-500: #4caf50;
--md-green-600: #43a047;
--md-green-700: #388e3c;
--md-green-800: #2e7d32;
--md-green-900: #1b5e20;
--md-green-A100: #b9f6ca;
--md-green-A200: #69f0ae;
--md-green-A400: #00e676;
--md-green-A700: #00c853;
--md-light-green-50: #f1f8e9;
--md-light-green-100: #dcedc8;
--md-light-green-200: #c5e1a5;
--md-light-green-300: #aed581;
--md-light-green-400: #9ccc65;
--md-light-green-500: #8bc34a;
--md-light-green-600: #7cb342;
--md-light-green-700: #689f38;
--md-light-green-800: #558b2f;
--md-light-green-900: #33691e;
--md-light-green-A100: #ccff90;
--md-light-green-A200: #b2ff59;
--md-light-green-A400: #76ff03;
--md-light-green-A700: #64dd17;
--md-lime-50: #f9fbe7;
--md-lime-100: #f0f4c3;
--md-lime-200: #e6ee9c;
--md-lime-300: #dce775;
--md-lime-400: #d4e157;
--md-lime-500: #cddc39;
--md-lime-600: #c0ca33;
--md-lime-700: #afb42b;
--md-lime-800: #9e9d24;
--md-lime-900: #827717;
--md-lime-A100: #f4ff81;
--md-lime-A200: #eeff41;
--md-lime-A400: #c6ff00;
--md-lime-A700: #aeea00;
--md-yellow-50: #fffde7;
--md-yellow-100: #fff9c4;
--md-yellow-200: #fff59d;
--md-yellow-300: #fff176;
--md-yellow-400: #ffee58;
--md-yellow-500: #ffeb3b;
--md-yellow-600: #fdd835;
--md-yellow-700: #fbc02d;
--md-yellow-800: #f9a825;
--md-yellow-900: #f57f17;
--md-yellow-A100: #ffff8d;
--md-yellow-A200: #ffff00;
--md-yellow-A400: #ffea00;
--md-yellow-A700: #ffd600;
--md-amber-50: #fff8e1;
--md-amber-100: #ffecb3;
--md-amber-200: #ffe082;
--md-amber-300: #ffd54f;
--md-amber-400: #ffca28;
--md-amber-500: #ffc107;
--md-amber-600: #ffb300;
--md-amber-700: #ffa000;
--md-amber-800: #ff8f00;
--md-amber-900: #ff6f00;
--md-amber-A100: #ffe57f;
--md-amber-A200: #ffd740;
--md-amber-A400: #ffc400;
--md-amber-A700: #ffab00;
--md-orange-50: #fff3e0;
--md-orange-100: #ffe0b2;
--md-orange-200: #ffcc80;
--md-orange-300: #ffb74d;
--md-orange-400: #ffa726;
--md-orange-500: #ff9800;
--md-orange-600: #fb8c00;
--md-orange-700: #f57c00;
--md-orange-800: #ef6c00;
--md-orange-900: #e65100;
--md-orange-A100: #ffd180;
--md-orange-A200: #ffab40;
--md-orange-A400: #ff9100;
--md-orange-A700: #ff6d00;
--md-deep-orange-50: #fbe9e7;
--md-deep-orange-100: #ffccbc;
--md-deep-orange-200: #ffab91;
--md-deep-orange-300: #ff8a65;
--md-deep-orange-400: #ff7043;
--md-deep-orange-500: #ff5722;
--md-deep-orange-600: #f4511e;
--md-deep-orange-700: #e64a19;
--md-deep-orange-800: #d84315;
--md-deep-orange-900: #bf360c;
--md-deep-orange-A100: #ff9e80;
--md-deep-orange-A200: #ff6e40;
--md-deep-orange-A400: #ff3d00;
--md-deep-orange-A700: #dd2c00;
--md-brown-50: #efebe9;
--md-brown-100: #d7ccc8;
--md-brown-200: #bcaaa4;
--md-brown-300: #a1887f;
--md-brown-400: #8d6e63;
--md-brown-500: #795548;
--md-brown-600: #6d4c41;
--md-brown-700: #5d4037;
--md-brown-800: #4e342e;
--md-brown-900: #3e2723;
--md-grey-50: #fafafa;
--md-grey-100: #f5f5f5;
--md-grey-200: #eeeeee;
--md-grey-300: #e0e0e0;
--md-grey-400: #bdbdbd;
--md-grey-500: #9e9e9e;
--md-grey-600: #757575;
--md-grey-700: #616161;
--md-grey-800: #424242;
--md-grey-900: #212121;
--md-blue-grey-50: #eceff1;
--md-blue-grey-100: #cfd8dc;
--md-blue-grey-200: #b0bec5;
--md-blue-grey-300: #90a4ae;
--md-blue-grey-400: #78909c;
--md-blue-grey-500: #607d8b;
--md-blue-grey-600: #546e7a;
--md-blue-grey-700: #455a64;
--md-blue-grey-800: #37474f;
--md-blue-grey-900: #263238;
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2017, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-Spinner {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: var(--jp-layout-color0);
outline: none;
}
.jp-SpinnerContent {
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 3em;
height: 3em;
border-radius: 50%;
background: var(--jp-brand-color3);
background: linear-gradient(
to right,
#f37626 10%,
rgba(255, 255, 255, 0) 42%
);
position: relative;
animation: load3 1s infinite linear, fadeIn 1s;
}
.jp-SpinnerContent:before {
width: 50%;
height: 50%;
background: #f37626;
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
.jp-SpinnerContent:after {
background: var(--jp-layout-color0);
width: 75%;
height: 75%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes load3 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
button.jp-mod-styled {
font-size: var(--jp-ui-font-size1);
color: var(--jp-ui-font-color0);
border: none;
box-sizing: border-box;
text-align: center;
line-height: 32px;
height: 32px;
padding: 0px 12px;
letter-spacing: 0.8px;
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
input.jp-mod-styled {
background: var(--jp-input-background);
height: 28px;
box-sizing: border-box;
border: var(--jp-border-width) solid var(--jp-border-color1);
padding-left: 7px;
padding-right: 7px;
font-size: var(--jp-ui-font-size2);
color: var(--jp-ui-font-color0);
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
input[type='checkbox'].jp-mod-styled {
appearance: checkbox;
-webkit-appearance: checkbox;
-moz-appearance: checkbox;
height: auto;
}
input.jp-mod-styled:focus {
border: var(--jp-border-width) solid var(--md-blue-500);
box-shadow: inset 0 0 4px var(--md-blue-300);
}
.jp-FileDialog-Checkbox {
margin-top: 35px;
display: flex;
flex-direction: row;
align-items: end;
width: 100%;
}
.jp-FileDialog-Checkbox > label {
flex: 1 1 auto;
}
.jp-select-wrapper {
display: flex;
position: relative;
flex-direction: column;
padding: 1px;
background-color: var(--jp-layout-color1);
height: 28px;
box-sizing: border-box;
margin-bottom: 12px;
}
.jp-select-wrapper.jp-mod-focused select.jp-mod-styled {
border: var(--jp-border-width) solid var(--jp-input-active-border-color);
box-shadow: var(--jp-input-box-shadow);
background-color: var(--jp-input-active-background);
}
select.jp-mod-styled:hover {
background-color: var(--jp-layout-color1);
cursor: pointer;
color: var(--jp-ui-font-color0);
background-color: var(--jp-input-hover-background);
box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.5);
}
select.jp-mod-styled {
flex: 1 1 auto;
height: 32px;
width: 100%;
font-size: var(--jp-ui-font-size2);
background: var(--jp-input-background);
color: var(--jp-ui-font-color0);
padding: 0 25px 0 8px;
border: var(--jp-border-width) solid var(--jp-input-border-color);
border-radius: 0px;
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2016, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
:root {
--jp-private-toolbar-height: calc(
28px + var(--jp-border-width)
); /* leave 28px for content */
}
.jp-Toolbar {
color: var(--jp-ui-font-color1);
flex: 0 0 auto;
display: flex;
flex-direction: row;
border-bottom: var(--jp-border-width) solid var(--jp-toolbar-border-color);
box-shadow: var(--jp-toolbar-box-shadow);
background: var(--jp-toolbar-background);
min-height: var(--jp-toolbar-micro-height);
padding: 2px;
z-index: 8;
overflow-x: hidden;
}
/* Toolbar items */
.jp-Toolbar > .jp-Toolbar-item.jp-Toolbar-spacer {
flex-grow: 1;
flex-shrink: 1;
}
.jp-Toolbar-item.jp-Toolbar-kernelStatus {
display: inline-block;
width: 32px;
background-repeat: no-repeat;
background-position: center;
background-size: 16px;
}
.jp-Toolbar > .jp-Toolbar-item {
flex: 0 0 auto;
display: flex;
padding-left: 1px;
padding-right: 1px;
font-size: var(--jp-ui-font-size1);
line-height: var(--jp-private-toolbar-height);
height: 100%;
}
/* Toolbar buttons */
/* This is the div we use to wrap the react component into a Widget */
div.jp-ToolbarButton {
color: transparent;
border: none;
box-sizing: border-box;
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding: 0px;
margin: 0px;
}
button.jp-ToolbarButtonComponent {
background: var(--jp-layout-color1);
border: none;
box-sizing: border-box;
outline: none;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
padding: 0px 6px;
margin: 0px;
height: 24px;
border-radius: var(--jp-border-radius);
display: flex;
align-items: center;
text-align: center;
font-size: 14px;
min-width: unset;
min-height: unset;
}
button.jp-ToolbarButtonComponent:disabled {
opacity: 0.4;
}
button.jp-ToolbarButtonComponent span {
padding: 0px;
flex: 0 0 auto;
}
button.jp-ToolbarButtonComponent .jp-ToolbarButtonComponent-label {
font-size: var(--jp-ui-font-size1);
line-height: 100%;
padding-left: 2px;
color: var(--jp-ui-font-color1);
}
#jp-main-dock-panel[data-mode='single-document']
.jp-MainAreaWidget
> .jp-Toolbar.jp-Toolbar-micro {
padding: 0;
min-height: 0;
}
#jp-main-dock-panel[data-mode='single-document']
.jp-MainAreaWidget
> .jp-Toolbar {
border: none;
box-shadow: none;
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the <span class="caps">BSD</span> 3-Clause License.
|
| The full license is in the file <span class="caps">LICENSE</span>, distributed with this software.
|----------------------------------------------------------------------------*/
/* <DEPRECATED> */
body.p-mod-override-cursor *, /* </DEPRECATED> */
body.lm-mod-override-cursor * {
cursor: inherit !important;
}
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2016, Jupyter Development Team.
|
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-JSONEditor {
display: flex;
flex-direction: column;
width: 100%;
}
.jp-JSONEditor-host {
flex: 1 1 auto;
border: var(--jp-border-width) solid var(--jp-input-border-color);
border-radius: 0px;
background: var(--jp-layout-color0);
min-height: 50px;
padding: 1px;
}
.jp-JSONEditor.jp-mod-error .jp-JSONEditor-host {
border-color: red;
outline-color: red;
}
.jp-JSONEditor-header {
display: flex;
flex: 1 0 auto;
padding: 0 0 0 12px;
}
.jp-JSONEditor-header label {
flex: 0 0 auto;
}
.jp-JSONEditor-commitButton {
height: 16px;
width: 16px;
background-size: 18px;
background-repeat: no-repeat;
background-position: center;
}
.jp-JSONEditor-host.jp-mod-focused {
background-color: var(--jp-input-active-background);
border: 1px solid var(--jp-input-active-border-color);
box-shadow: var(--jp-input-box-shadow);
}
.jp-Editor.jp-mod-dropTarget {
border: var(--jp-border-width) solid var(--jp-input-active-border-color);
box-shadow: var(--jp-input-box-shadow);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Variables
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
/*-----------------------------------------------------------------------------
| Styles
|----------------------------------------------------------------------------*/
.jp-Statusbar-ProgressCircle svg {
display: block;
margin: 0 auto;
width: 16px;
height: 24px;
align-self: normal;
}
.jp-Statusbar-ProgressCircle path {
fill: var(--jp-inverse-layout-color3);
}
.jp-Statusbar-ProgressBar-progress-bar {
height: 10px;
width: 100px;
border: solid 0.25px var(--jp-brand-color2);
border-radius: 3px;
overflow: hidden;
align-self: center;
}
.jp-Statusbar-ProgressBar-progress-bar > div {
background-color: var(--jp-brand-color2);
background-image: linear-gradient(
-45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
background-size: 40px 40px;
float: left;
width: 0%;
height: 100%;
font-size: 12px;
line-height: 14px;
color: #ffffff;
text-align: center;
animation: jp-Statusbar-ExecutionTime-progress-bar 2s linear infinite;
}
.jp-Statusbar-ProgressBar-progress-bar p {
color: var(--jp-ui-font-color1);
font-family: var(--jp-ui-font-family);
font-size: var(--jp-ui-font-size1);
line-height: 10px;
width: 100px;
}
@keyframes jp-Statusbar-ExecutionTime-progress-bar {
0% {
background-position: 0 0;
}
100% {
background-position: 40px 40px;
}
}
/* <span class="caps">BASICS</span> */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* <span class="caps">PADDING</span> */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* <span class="caps">GUTTER</span> */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* <span class="caps">CURSOR</span> */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: 0;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* <span class="caps">DEFAULT</span> <span class="caps">THEME</span> */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* <span class="caps">STOP</span> */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 50px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -50px; margin-right: -50px;
padding-bottom: 50px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 50px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
outline: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -50px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }
.CodeMirror-dialog {
position: absolute;
left: 0; right: 0;
background: inherit;
z-index: 15;
padding: .1em .8em;
overflow: hidden;
color: inherit;
}
.CodeMirror-dialog-top {
border-bottom: 1px solid #eee;
top: 0;
}
.CodeMirror-dialog-bottom {
border-top: 1px solid #eee;
bottom: 0;
}
.CodeMirror-dialog input {
border: none;
outline: none;
background: transparent;
width: 20em;
color: inherit;
font-family: monospace;
}
.CodeMirror-dialog button {
font-size: 70%;
}
.CodeMirror-foldmarker {
color: blue;
text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
font-family: arial;
line-height: .3;
cursor: pointer;
}
.CodeMirror-foldgutter {
width: .7em;
}
.CodeMirror-foldgutter-open,
.CodeMirror-foldgutter-folded {
cursor: pointer;
}
.CodeMirror-foldgutter-open:after {
content: "\<span class="caps">25BE</span>";
}
.CodeMirror-foldgutter-folded:after {
content: "\25B8";
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.CodeMirror {
line-height: var(--jp-code-line-height);
font-size: var(--jp-code-font-size);
font-family: var(--jp-code-font-family);
border: 0;
border-radius: 0;
height: auto;
/* Changed to auto to autogrow */
}
.CodeMirror pre {
padding: 0 var(--jp-code-padding);
}
.CodeMirror.cm-fat-cursor .cm-overlay.cm-searching {
opacity: 0.5;
}
.jp-CodeMirrorEditor[data-type='inline'] .CodeMirror-dialog {
background-color: var(--jp-layout-color0);
color: var(--jp-content-font-color1);
}
/* This causes https://github.com/jupyter/jupyterlab/issues/522 */
/* May not cause it not because we changed it! */
.CodeMirror-lines {
padding: var(--jp-code-padding) 0;
}
.CodeMirror-linenumber {
padding: 0 8px;
}
.jp-CodeMirrorEditor {
cursor: text;
}
.jp-CodeMirrorEditor[data-type='inline'] .CodeMirror-cursor {
border-left: var(--jp-code-cursor-width0) solid var(--jp-editor-cursor-color);
}
/* When zoomed out 67% and 33% on a screen of 1440 width x 900 height */
@media screen and (min-width: 2138px) and (max-width: 4319px) {
.jp-CodeMirrorEditor[data-type='inline'] .CodeMirror-cursor {
border-left: var(--jp-code-cursor-width1) solid
var(--jp-editor-cursor-color);
}
}
/* When zoomed out less than 33% */
@media screen and (min-width: 4320px) {
.jp-CodeMirrorEditor[data-type='inline'] .CodeMirror-cursor {
border-left: var(--jp-code-cursor-width2) solid
var(--jp-editor-cursor-color);
}
}
.CodeMirror.jp-mod-readOnly .CodeMirror-cursor {
display: none;
}
.CodeMirror-gutters {
border-right: 1px solid var(--jp-border-color2);
background-color: var(--jp-layout-color0);
}
.jp-CollaboratorCursor {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: none;
border-bottom: 3px solid;
background-clip: content-box;
margin-left: -5px;
margin-right: -5px;
}
.CodeMirror-selectedtext.cm-searching {
background-color: var(--jp-search-selected-match-background-color) !important;
color: var(--jp-search-selected-match-color) !important;
}
.cm-searching {
background-color: var(
--jp-search-unselected-match-background-color
) !important;
color: var(--jp-search-unselected-match-color) !important;
}
.cm-trailingspace {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAsElEQVQIHQGlAFr/<span class="caps">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7</span>+r3zKmT0/+pk9P/7+r3zAAAAAAAAAAABAAAAAAAAAAA6OPzM+/q9wAAAAAA6OPzMwAAAAAAAAAAAgAAAAAAAAAAGR8NiRQaCgAZIA0AGR8NiQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQyoYJ/<span class="caps">SY80UAAAAASUVORK5CYII</span>=);
background-position: center left;
background-repeat: repeat-x;
}
.CodeMirror-focused .CodeMirror-selected {
background-color: var(--jp-editor-selected-focused-background);
}
.CodeMirror-selected {
background-color: var(--jp-editor-selected-background);
}
.jp-CollaboratorCursor-hover {
position: absolute;
z-index: 1;
transform: translateX(-50%);
color: white;
border-radius: 3px;
padding-left: 4px;
padding-right: 4px;
padding-top: 1px;
padding-bottom: 1px;
text-align: center;
font-size: var(--jp-ui-font-size1);
white-space: nowrap;
}
.jp-CodeMirror-ruler {
border-left: 1px dashed var(--jp-border-color2);
}
/**
* Here is our jupyter theme for CodeMirror syntax highlighting
* This is used in our marked.js syntax highlighting and CodeMirror itself
* The string "jupyter" is set in ../codemirror/widget.DEFAULT_CODEMIRROR_THEME
* This came from the classic notebook, which came form highlight.js/GitHub
*/
/**
* CodeMirror themes are handling the background/color in this way. This works
* fine for CodeMirror editors outside the notebook, but the notebook styles
* these things differently.
*/
.CodeMirror.cm-s-jupyter {
background: var(--jp-layout-color0);
color: var(--jp-content-font-color1);
}
/* In the notebook, we want this styling to be handled by its container */
.jp-CodeConsole .CodeMirror.cm-s-jupyter,
.jp-Notebook .CodeMirror.cm-s-jupyter {
background: transparent;
}
.cm-s-jupyter .CodeMirror-cursor {
border-left: var(--jp-code-cursor-width0) solid var(--jp-editor-cursor-color);
}
.cm-s-jupyter span.cm-keyword {
color: var(--jp-mirror-editor-keyword-color);
font-weight: bold;
}
.cm-s-jupyter span.cm-atom {
color: var(--jp-mirror-editor-atom-color);
}
.cm-s-jupyter span.cm-number {
color: var(--jp-mirror-editor-number-color);
}
.cm-s-jupyter span.cm-def {
color: var(--jp-mirror-editor-def-color);
}
.cm-s-jupyter span.cm-variable {
color: var(--jp-mirror-editor-variable-color);
}
.cm-s-jupyter span.cm-variable-2 {
color: var(--jp-mirror-editor-variable-2-color);
}
.cm-s-jupyter span.cm-variable-3 {
color: var(--jp-mirror-editor-variable-3-color);
}
.cm-s-jupyter span.cm-punctuation {
color: var(--jp-mirror-editor-punctuation-color);
}
.cm-s-jupyter span.cm-property {
color: var(--jp-mirror-editor-property-color);
}
.cm-s-jupyter span.cm-operator {
color: var(--jp-mirror-editor-operator-color);
font-weight: bold;
}
.cm-s-jupyter span.cm-comment {
color: var(--jp-mirror-editor-comment-color);
font-style: italic;
}
.cm-s-jupyter span.cm-string {
color: var(--jp-mirror-editor-string-color);
}
.cm-s-jupyter span.cm-string-2 {
color: var(--jp-mirror-editor-string-2-color);
}
.cm-s-jupyter span.cm-meta {
color: var(--jp-mirror-editor-meta-color);
}
.cm-s-jupyter span.cm-qualifier {
color: var(--jp-mirror-editor-qualifier-color);
}
.cm-s-jupyter span.cm-builtin {
color: var(--jp-mirror-editor-builtin-color);
}
.cm-s-jupyter span.cm-bracket {
color: var(--jp-mirror-editor-bracket-color);
}
.cm-s-jupyter span.cm-tag {
color: var(--jp-mirror-editor-tag-color);
}
.cm-s-jupyter span.cm-attribute {
color: var(--jp-mirror-editor-attribute-color);
}
.cm-s-jupyter span.cm-header {
color: var(--jp-mirror-editor-header-color);
}
.cm-s-jupyter span.cm-quote {
color: var(--jp-mirror-editor-quote-color);
}
.cm-s-jupyter span.cm-link {
color: var(--jp-mirror-editor-link-color);
}
.cm-s-jupyter span.cm-error {
color: var(--jp-mirror-editor-error-color);
}
.cm-s-jupyter span.cm-hr {
color: #999;
}
.cm-s-jupyter span.cm-tab {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);
background-position: right;
background-repeat: no-repeat;
}
.cm-s-jupyter .CodeMirror-activeline-background,
.cm-s-jupyter .CodeMirror-gutter {
background-color: var(--jp-layout-color2);
}
/* Styles for shared cursors (remote cursor locations and selected ranges) */
.jp-CodeMirrorEditor .remote-caret {
position: relative;
border-left: 2px solid black;
margin-left: -1px;
margin-right: -1px;
box-sizing: border-box;
}
.jp-CodeMirrorEditor .remote-caret > div {
white-space: nowrap;
position: absolute;
top: -1.15em;
padding-bottom: 0.05em;
left: -2px;
font-size: 0.95em;
background-color: rgb(250, 129, 0);
font-family: var(--jp-ui-font-family);
font-weight: bold;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
z-index: 3;
transition: opacity 0.3s ease-in-out;
}
.jp-CodeMirrorEditor .remote-caret.hide-name > div {
transition-delay: 0.7s;
opacity: 0;
}
/* Use `div[style]` as more specific selector on 3.4.x to reduce the impact of
* Chromium style invalidation strategy on performance when many divs are present.
*/
.jp-CodeMirrorEditor .remote-caret:hover > div[style] {
opacity: 1;
transition-delay: 0s;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| RenderedText
|----------------------------------------------------------------------------*/
:root {
/* This is the padding value to fill the gaps between lines containing spans with background color. */
--jp-private-code-span-padding: calc(
(var(--jp-code-line-height) - 1) * var(--jp-code-font-size) / 2
);
}
.jp-RenderedText {
text-align: left;
padding-left: var(--jp-code-padding);
line-height: var(--jp-code-line-height);
font-family: var(--jp-code-font-family);
}
.jp-RenderedText pre,
.jp-RenderedJavaScript pre,
.jp-RenderedHTMLCommon pre {
color: var(--jp-content-font-color1);
font-size: var(--jp-code-font-size);
border: none;
margin: 0px;
padding: 0px;
}
.jp-RenderedText pre a:link {
text-decoration: none;
color: var(--jp-content-link-color);
}
.jp-RenderedText pre a:hover {
text-decoration: underline;
color: var(--jp-content-link-color);
}
.jp-RenderedText pre a:visited {
text-decoration: none;
color: var(--jp-content-link-color);
}
/* console foregrounds and backgrounds */
.jp-RenderedText pre .ansi-black-fg {
color: #3e424d;
}
.jp-RenderedText pre .ansi-red-fg {
color: #e75c58;
}
.jp-RenderedText pre .ansi-green-fg {
color: #00a250;
}
.jp-RenderedText pre .ansi-yellow-fg {
color: #ddb62b;
}
.jp-RenderedText pre .ansi-blue-fg {
color: #208ffb;
}
.jp-RenderedText pre .ansi-magenta-fg {
color: #d160c4;
}
.jp-RenderedText pre .ansi-cyan-fg {
color: #60c6c8;
}
.jp-RenderedText pre .ansi-white-fg {
color: #c5c1b4;
}
.jp-RenderedText pre .ansi-black-bg {
background-color: #3e424d;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-red-bg {
background-color: #e75c58;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-green-bg {
background-color: #00a250;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-yellow-bg {
background-color: #ddb62b;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-blue-bg {
background-color: #208ffb;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-magenta-bg {
background-color: #d160c4;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-cyan-bg {
background-color: #60c6c8;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-white-bg {
background-color: #c5c1b4;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-black-intense-fg {
color: #282c36;
}
.jp-RenderedText pre .ansi-red-intense-fg {
color: #b22b31;
}
.jp-RenderedText pre .ansi-green-intense-fg {
color: #007427;
}
.jp-RenderedText pre .ansi-yellow-intense-fg {
color: #b27d12;
}
.jp-RenderedText pre .ansi-blue-intense-fg {
color: #0065ca;
}
.jp-RenderedText pre .ansi-magenta-intense-fg {
color: #a03196;
}
.jp-RenderedText pre .ansi-cyan-intense-fg {
color: #258f8f;
}
.jp-RenderedText pre .ansi-white-intense-fg {
color: #a1a6b2;
}
.jp-RenderedText pre .ansi-black-intense-bg {
background-color: #282c36;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-red-intense-bg {
background-color: #b22b31;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-green-intense-bg {
background-color: #007427;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-yellow-intense-bg {
background-color: #b27d12;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-blue-intense-bg {
background-color: #0065ca;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-magenta-intense-bg {
background-color: #a03196;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-cyan-intense-bg {
background-color: #258f8f;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-white-intense-bg {
background-color: #a1a6b2;
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-default-inverse-fg {
color: var(--jp-ui-inverse-font-color0);
}
.jp-RenderedText pre .ansi-default-inverse-bg {
background-color: var(--jp-inverse-layout-color0);
padding: var(--jp-private-code-span-padding) 0;
}
.jp-RenderedText pre .ansi-bold {
font-weight: bold;
}
.jp-RenderedText pre .ansi-underline {
text-decoration: underline;
}
.jp-RenderedText[data-mime-type='application/vnd.jupyter.stderr'] {
background: var(--jp-rendermime-error-background);
padding-top: var(--jp-code-padding);
}
/*-----------------------------------------------------------------------------
| RenderedLatex
|----------------------------------------------------------------------------*/
.jp-RenderedLatex {
color: var(--jp-content-font-color1);
font-size: var(--jp-content-font-size1);
line-height: var(--jp-content-line-height);
}
/* Left-justify outputs.*/
.jp-OutputArea-output.jp-RenderedLatex {
padding: var(--jp-code-padding);
text-align: left;
}
/*-----------------------------------------------------------------------------
| RenderedHTML
|----------------------------------------------------------------------------*/
.jp-RenderedHTMLCommon {
color: var(--jp-content-font-color1);
font-family: var(--jp-content-font-family);
font-size: var(--jp-content-font-size1);
line-height: var(--jp-content-line-height);
/* Give a bit more R padding on Markdown text to keep line lengths reasonable */
padding-right: 20px;
}
.jp-RenderedHTMLCommon em {
font-style: italic;
}
.jp-RenderedHTMLCommon strong {
font-weight: bold;
}
.jp-RenderedHTMLCommon u {
text-decoration: underline;
}
.jp-RenderedHTMLCommon a:link {
text-decoration: none;
color: var(--jp-content-link-color);
}
.jp-RenderedHTMLCommon a:hover {
text-decoration: underline;
color: var(--jp-content-link-color);
}
.jp-RenderedHTMLCommon a:visited {
text-decoration: none;
color: var(--jp-content-link-color);
}
/* Headings */
.jp-RenderedHTMLCommon h1,
.jp-RenderedHTMLCommon h2,
.jp-RenderedHTMLCommon h3,
.jp-RenderedHTMLCommon h4,
.jp-RenderedHTMLCommon h5,
.jp-RenderedHTMLCommon h6 {
line-height: var(--jp-content-heading-line-height);
font-weight: var(--jp-content-heading-font-weight);
font-style: normal;
margin: var(--jp-content-heading-margin-top) 0
var(--jp-content-heading-margin-bottom) 0;
}
.jp-RenderedHTMLCommon h1:first-child,
.jp-RenderedHTMLCommon h2:first-child,
.jp-RenderedHTMLCommon h3:first-child,
.jp-RenderedHTMLCommon h4:first-child,
.jp-RenderedHTMLCommon h5:first-child,
.jp-RenderedHTMLCommon h6:first-child {
margin-top: calc(0.5 * var(--jp-content-heading-margin-top));
}
.jp-RenderedHTMLCommon h1:last-child,
.jp-RenderedHTMLCommon h2:last-child,
.jp-RenderedHTMLCommon h3:last-child,
.jp-RenderedHTMLCommon h4:last-child,
.jp-RenderedHTMLCommon h5:last-child,
.jp-RenderedHTMLCommon h6:last-child {
margin-bottom: calc(0.5 * var(--jp-content-heading-margin-bottom));
}
.jp-RenderedHTMLCommon h1 {
font-size: var(--jp-content-font-size5);
}
.jp-RenderedHTMLCommon h2 {
font-size: var(--jp-content-font-size4);
}
.jp-RenderedHTMLCommon h3 {
font-size: var(--jp-content-font-size3);
}
.jp-RenderedHTMLCommon h4 {
font-size: var(--jp-content-font-size2);
}
.jp-RenderedHTMLCommon h5 {
font-size: var(--jp-content-font-size1);
}
.jp-RenderedHTMLCommon h6 {
font-size: var(--jp-content-font-size0);
}
/* Lists */
.jp-RenderedHTMLCommon ul:not(.list-inline),
.jp-RenderedHTMLCommon ol:not(.list-inline) {
padding-left: 2em;
}
.jp-RenderedHTMLCommon ul {
list-style: disc;
}
.jp-RenderedHTMLCommon ul ul {
list-style: square;
}
.jp-RenderedHTMLCommon ul ul ul {
list-style: circle;
}
.jp-RenderedHTMLCommon ol {
list-style: decimal;
}
.jp-RenderedHTMLCommon ol ol {
list-style: upper-alpha;
}
.jp-RenderedHTMLCommon ol ol ol {
list-style: lower-alpha;
}
.jp-RenderedHTMLCommon ol ol ol ol {
list-style: lower-roman;
}
.jp-RenderedHTMLCommon ol ol ol ol ol {
list-style: decimal;
}
.jp-RenderedHTMLCommon ol,
.jp-RenderedHTMLCommon ul {
margin-bottom: 1em;
}
.jp-RenderedHTMLCommon ul ul,
.jp-RenderedHTMLCommon ul ol,
.jp-RenderedHTMLCommon ol ul,
.jp-RenderedHTMLCommon ol ol {
margin-bottom: 0em;
}
.jp-RenderedHTMLCommon hr {
color: var(--jp-border-color2);
background-color: var(--jp-border-color1);
margin-top: 1em;
margin-bottom: 1em;
}
.jp-RenderedHTMLCommon > pre {
margin: 1.5em 2em;
}
.jp-RenderedHTMLCommon pre,
.jp-RenderedHTMLCommon code {
border: 0;
background-color: var(--jp-layout-color0);
color: var(--jp-content-font-color1);
font-family: var(--jp-code-font-family);
font-size: inherit;
line-height: var(--jp-code-line-height);
padding: 0;
white-space: pre-wrap;
}
.jp-RenderedHTMLCommon :not(pre) > code {
background-color: var(--jp-layout-color2);
padding: 1px 5px;
}
/* Tables */
.jp-RenderedHTMLCommon table {
border-collapse: collapse;
border-spacing: 0;
border: none;
color: var(--jp-ui-font-color1);
font-size: var(--jp-ui-font-size1);
table-layout: fixed;
margin-left: auto;
margin-right: auto;
}
.jp-RenderedHTMLCommon thead {
border-bottom: var(--jp-border-width) solid var(--jp-border-color1);
vertical-align: bottom;
}
.jp-RenderedHTMLCommon td,
.jp-RenderedHTMLCommon th,
.jp-RenderedHTMLCommon tr {
vertical-align: middle;
padding: 0.5em 0.5em;
line-height: normal;
white-space: normal;
max-width: none;
border: none;
}
.jp-RenderedMarkdown.jp-RenderedHTMLCommon td,
.jp-RenderedMarkdown.jp-RenderedHTMLCommon th {
max-width: none;
}
:not(.jp-RenderedMarkdown).jp-RenderedHTMLCommon td,
:not(.jp-RenderedMarkdown).jp-RenderedHTMLCommon th,
:not(.jp-RenderedMarkdown).jp-RenderedHTMLCommon tr {
text-align: right;
}
.jp-RenderedHTMLCommon th {
font-weight: bold;
}
.jp-RenderedHTMLCommon tbody tr:nth-child(odd) {
background: var(--jp-layout-color0);
}
.jp-RenderedHTMLCommon tbody tr:nth-child(even) {
background: var(--jp-rendermime-table-row-background);
}
.jp-RenderedHTMLCommon tbody tr:hover {
background: var(--jp-rendermime-table-row-hover-background);
}
.jp-RenderedHTMLCommon table {
margin-bottom: 1em;
}
.jp-RenderedHTMLCommon p {
text-align: left;
margin: 0px;
}
.jp-RenderedHTMLCommon p {
margin-bottom: 1em;
}
.jp-RenderedHTMLCommon img {
-moz-force-broken-image-icon: 1;
}
/* Restrict to direct children as other images could be nested in other content. */
.jp-RenderedHTMLCommon > img {
display: block;
margin-left: 0;
margin-right: 0;
margin-bottom: 1em;
}
/* Change color behind transparent images if they need it... */
[data-jp-theme-light='false'] .jp-RenderedImage img.jp-needs-light-background {
background-color: var(--jp-inverse-layout-color1);
}
[data-jp-theme-light='true'] .jp-RenderedImage img.jp-needs-dark-background {
background-color: var(--jp-inverse-layout-color1);
}
/* ...or leave it untouched if they don't */
[data-jp-theme-light='false'] .jp-RenderedImage img.jp-needs-dark-background {
}
[data-jp-theme-light='true'] .jp-RenderedImage img.jp-needs-light-background {
}
.jp-RenderedHTMLCommon img,
.jp-RenderedImage img,
.jp-RenderedHTMLCommon svg,
.jp-RenderedSVG svg {
max-width: 100%;
height: auto;
}
.jp-RenderedHTMLCommon img.jp-mod-unconfined,
.jp-RenderedImage img.jp-mod-unconfined,
.jp-RenderedHTMLCommon svg.jp-mod-unconfined,
.jp-RenderedSVG svg.jp-mod-unconfined {
max-width: none;
}
.jp-RenderedHTMLCommon .alert {
padding: var(--jp-notebook-padding);
border: var(--jp-border-width) solid transparent;
border-radius: var(--jp-border-radius);
margin-bottom: 1em;
}
.jp-RenderedHTMLCommon .alert-info {
color: var(--jp-info-color0);
background-color: var(--jp-info-color3);
border-color: var(--jp-info-color2);
}
.jp-RenderedHTMLCommon .alert-info hr {
border-color: var(--jp-info-color3);
}
.jp-RenderedHTMLCommon .alert-info > p:last-child,
.jp-RenderedHTMLCommon .alert-info > ul:last-child {
margin-bottom: 0;
}
.jp-RenderedHTMLCommon .alert-warning {
color: var(--jp-warn-color0);
background-color: var(--jp-warn-color3);
border-color: var(--jp-warn-color2);
}
.jp-RenderedHTMLCommon .alert-warning hr {
border-color: var(--jp-warn-color3);
}
.jp-RenderedHTMLCommon .alert-warning > p:last-child,
.jp-RenderedHTMLCommon .alert-warning > ul:last-child {
margin-bottom: 0;
}
.jp-RenderedHTMLCommon .alert-success {
color: var(--jp-success-color0);
background-color: var(--jp-success-color3);
border-color: var(--jp-success-color2);
}
.jp-RenderedHTMLCommon .alert-success hr {
border-color: var(--jp-success-color3);
}
.jp-RenderedHTMLCommon .alert-success > p:last-child,
.jp-RenderedHTMLCommon .alert-success > ul:last-child {
margin-bottom: 0;
}
.jp-RenderedHTMLCommon .alert-danger {
color: var(--jp-error-color0);
background-color: var(--jp-error-color3);
border-color: var(--jp-error-color2);
}
.jp-RenderedHTMLCommon .alert-danger hr {
border-color: var(--jp-error-color3);
}
.jp-RenderedHTMLCommon .alert-danger > p:last-child,
.jp-RenderedHTMLCommon .alert-danger > ul:last-child {
margin-bottom: 0;
}
.jp-RenderedHTMLCommon blockquote {
margin: 1em 2em;
padding: 0 1em;
border-left: 5px solid var(--jp-border-color2);
}
a.jp-InternalAnchorLink {
visibility: hidden;
margin-left: 8px;
color: var(--md-blue-800);
}
h1:hover .jp-InternalAnchorLink,
h2:hover .jp-InternalAnchorLink,
h3:hover .jp-InternalAnchorLink,
h4:hover .jp-InternalAnchorLink,
h5:hover .jp-InternalAnchorLink,
h6:hover .jp-InternalAnchorLink {
visibility: visible;
}
.jp-RenderedHTMLCommon kbd {
background-color: var(--jp-rendermime-table-row-background);
border: 1px solid var(--jp-border-color0);
border-bottom-color: var(--jp-border-color2);
border-radius: 3px;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
display: inline-block;
font-size: var(--jp-ui-font-size0);
line-height: 1em;
padding: 0.2em 0.5em;
}
/* Most direct children of .jp-RenderedHTMLCommon have a margin-bottom of 1.0.
* At the bottom of cells this is a bit too much as there is also spacing
* between cells. Going all the way to 0 gets too tight between markdown and
* code cells.
*/
.jp-RenderedHTMLCommon > *:last-child {
margin-bottom: 0.5em;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-MimeDocument {
outline: none;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Variables
|----------------------------------------------------------------------------*/
:root {
--jp-private-filebrowser-button-height: 28px;
--jp-private-filebrowser-button-width: 48px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-FileBrowser {
display: flex;
flex-direction: column;
color: var(--jp-ui-font-color1);
background: var(--jp-layout-color1);
/* This is needed so that all font sizing of children done in ems is
* relative to this base size */
font-size: var(--jp-ui-font-size1);
}
.jp-FileBrowser-toolbar.jp-Toolbar {
border-bottom: none;
height: auto;
margin: 8px 12px 0px 12px;
padding: 0px;
box-shadow: none;
justify-content: flex-start;
}
.jp-BreadCrumbs {
flex: 0 0 auto;
margin: 8px 12px 8px 12px;
}
.jp-BreadCrumbs-item {
margin: 0px 2px;
padding: 0px 2px;
border-radius: var(--jp-border-radius);
cursor: pointer;
}
.jp-BreadCrumbs-item:hover {
background-color: var(--jp-layout-color2);
}
.jp-BreadCrumbs-item:first-child {
margin-left: 0px;
}
.jp-BreadCrumbs-item.jp-mod-dropTarget {
background-color: var(--jp-brand-color2);
opacity: 0.7;
}
/*-----------------------------------------------------------------------------
| Buttons
|----------------------------------------------------------------------------*/
.jp-FileBrowser-toolbar > .jp-Toolbar-item {
flex: 0 0 auto;
padding-left: 0px;
padding-right: 2px;
}
.jp-FileBrowser-toolbar > .jp-Toolbar-item .jp-ToolbarButtonComponent {
width: 40px;
}
.jp-FileBrowser-toolbar
.jp-ToolbarButtonComponent[data-command='filebrowser:create-main-launcher'] {
width: 72px;
background: var(--jp-brand-color1);
}
.jp-FileBrowser-toolbar
.jp-ToolbarButtonComponent[data-command='filebrowser:create-main-launcher']:hover,
.jp-FileBrowser-toolbar
.jp-ToolbarButtonComponent[data-command='filebrowser:create-main-launcher']:focus-visible {
background-color: var(--jp-brand-color0) !important;
}
.jp-FileBrowser-toolbar
.jp-ToolbarButtonComponent[data-command='filebrowser:create-main-launcher']
.jp-icon3 {
fill: var(--jp-layout-color1);
}
/*-----------------------------------------------------------------------------
| Other styles
|----------------------------------------------------------------------------*/
.jp-FileDialog.jp-mod-conflict input {
color: var(--jp-error-color1);
}
.jp-FileDialog .jp-new-name-title {
margin-top: 12px;
}
.jp-LastModified-hidden {
display: none;
}
.jp-FileBrowser-filterBox {
padding: 0px;
flex: 0 0 auto;
margin: 8px 12px 0px 12px;
}
/*-----------------------------------------------------------------------------
| DirListing
|----------------------------------------------------------------------------*/
.jp-DirListing {
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: 0;
}
.jp-DirListing:focus-visible {
outline: 1px solid var(--jp-brand-color1);
outline-offset: -2px;
}
.jp-DirListing-header {
flex: 0 0 auto;
display: flex;
flex-direction: row;
overflow: hidden;
border-top: var(--jp-border-width) solid var(--jp-border-color2);
border-bottom: var(--jp-border-width) solid var(--jp-border-color1);
box-shadow: var(--jp-toolbar-box-shadow);
z-index: 2;
}
.jp-DirListing-headerItem {
padding: 4px 12px 2px 12px;
font-weight: 500;
}
.jp-DirListing-headerItem:hover {
background: var(--jp-layout-color2);
}
.jp-DirListing-headerItem.jp-id-name {
flex: 1 0 84px;
}
.jp-DirListing-headerItem.jp-id-modified {
flex: 0 0 112px;
border-left: var(--jp-border-width) solid var(--jp-border-color2);
text-align: right;
}
.jp-id-narrow {
display: none;
flex: 0 0 5px;
padding: 4px 4px;
border-left: var(--jp-border-width) solid var(--jp-border-color2);
text-align: right;
color: var(--jp-border-color2);
}
.jp-DirListing-narrow .jp-id-narrow {
display: block;
}
.jp-DirListing-narrow .jp-id-modified,
.jp-DirListing-narrow .jp-DirListing-itemModified {
display: none;
}
.jp-DirListing-headerItem.jp-mod-selected {
font-weight: 600;
}
/* increase specificity to override bundled default */
.jp-DirListing-content {
flex: 1 1 auto;
margin: 0;
padding: 0;
list-style-type: none;
overflow: auto;
background-color: var(--jp-layout-color1);
}
.jp-DirListing-content mark {
color: var(--jp-ui-font-color0);
background-color: transparent;
font-weight: bold;
}
.jp-DirListing-content .jp-DirListing-item.jp-mod-selected mark {
color: var(--jp-ui-inverse-font-color0);
}
/* Style the directory listing content when a user drops a file to upload */
.jp-DirListing.jp-mod-native-drop .jp-DirListing-content {
outline: 5px dashed rgba(128, 128, 128, 0.5);
outline-offset: -10px;
cursor: copy;
}
.jp-DirListing-item {
display: flex;
flex-direction: row;
padding: 4px 12px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.jp-DirListing-item[data-is-dot] {
opacity: 75%;
}
.jp-DirListing-item.jp-mod-selected {
color: var(--jp-ui-inverse-font-color1);
background: var(--jp-brand-color1);
}
.jp-DirListing-item.jp-mod-dropTarget {
background: var(--jp-brand-color3);
}
.jp-DirListing-item:hover:not(.jp-mod-selected) {
background: var(--jp-layout-color2);
}
.jp-DirListing-itemIcon {
flex: 0 0 20px;
margin-right: 4px;
}
.jp-DirListing-itemText {
flex: 1 0 64px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
.jp-DirListing-itemModified {
flex: 0 0 125px;
text-align: right;
}
.jp-DirListing-editor {
flex: 1 0 64px;
outline: none;
border: none;
color: var(--jp-ui-font-color1);
background-color: var(--jp-layout-color1);
}
.jp-DirListing-item.jp-mod-running .jp-DirListing-itemIcon:before {
color: var(--jp-success-color1);
content: '\<span class="caps">25CF</span>';
font-size: 8px;
position: absolute;
left: -8px;
}
.jp-DirListing-item.jp-mod-running.jp-mod-selected
.jp-DirListing-itemIcon:before {
color: var(--jp-ui-inverse-font-color1);
}
.jp-DirListing-item.lm-mod-drag-image,
.jp-DirListing-item.jp-mod-selected.lm-mod-drag-image {
font-size: var(--jp-ui-font-size1);
padding-left: 4px;
margin-left: 4px;
width: 160px;
background-color: var(--jp-ui-inverse-font-color2);
box-shadow: var(--jp-elevation-z2);
border-radius: 0px;
color: var(--jp-ui-font-color1);
transform: translateX(-40%) translateY(-58%);
}
.jp-Document {
min-width: 120px;
min-height: 120px;
outline: none;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Private <span class="caps">CSS</span> variables
|----------------------------------------------------------------------------*/
:root {
}
/*-----------------------------------------------------------------------------
| Main OutputArea
| OutputArea has a list of Outputs
|----------------------------------------------------------------------------*/
.jp-OutputArea {
overflow-y: auto;
}
.jp-OutputArea-child {
display: flex;
flex-direction: row;
}
body[data-format='mobile'] .jp-OutputArea-child {
flex-direction: column;
}
.jp-OutputPrompt {
flex: 0 0 var(--jp-cell-prompt-width);
color: var(--jp-cell-outprompt-font-color);
font-family: var(--jp-cell-prompt-font-family);
padding: var(--jp-code-padding);
letter-spacing: var(--jp-cell-prompt-letter-spacing);
line-height: var(--jp-code-line-height);
font-size: var(--jp-code-font-size);
border: var(--jp-border-width) solid transparent;
opacity: var(--jp-cell-prompt-opacity);
/* Right align prompt text, don't wrap to handle large prompt numbers */
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* Disable text selection */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
body[data-format='mobile'] .jp-OutputPrompt {
flex: 0 0 auto;
text-align: left;
}
.jp-OutputArea-output {
height: auto;
overflow: auto;
user-select: text;
-moz-user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
}
.jp-OutputArea-child .jp-OutputArea-output {
flex-grow: 1;
flex-shrink: 1;
}
body[data-format='mobile'] .jp-OutputArea-child .jp-OutputArea-output {
margin-left: var(--jp-notebook-padding);
}
/**
* Isolated output.
*/
.jp-OutputArea-output.jp-mod-isolated {
width: 100%;
display: block;
}
/*
When drag events occur, `p-mod-override-cursor` is added to the body.
Because iframes steal all cursor events, the following two rules are necessary
to suppress pointer events while resize drags are occurring. There may be a
better solution to this problem.
*/
body.lm-mod-override-cursor .jp-OutputArea-output.jp-mod-isolated {
position: relative;
}
body.lm-mod-override-cursor .jp-OutputArea-output.jp-mod-isolated:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
/* pre */
.jp-OutputArea-output pre {
border: none;
margin: 0px;
padding: 0px;
overflow-x: auto;
overflow-y: auto;
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
}
/* tables */
.jp-OutputArea-output.jp-RenderedHTMLCommon table {
margin-left: 0;
margin-right: 0;
}
/* description lists */
.jp-OutputArea-output dl,
.jp-OutputArea-output dt,
.jp-OutputArea-output dd {
display: block;
}
.jp-OutputArea-output dl {
width: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
.jp-OutputArea-output dt {
font-weight: bold;
float: left;
width: 20%;
padding: 0;
margin: 0;
}
.jp-OutputArea-output dd {
float: left;
width: 80%;
padding: 0;
margin: 0;
}
.jp-TrimmedOutputs a {
margin: 10px;
text-decoration: none;
cursor: pointer;
}
/* Hide the gutter in case of
* - nested output areas (e.g. in the case of output widgets)
* - mirrored output areas
*/
.jp-OutputArea .jp-OutputArea .jp-OutputArea-prompt {
display: none;
}
/* Hide empty lines in the output area, for instance due to cleared widgets */
.jp-OutputArea-prompt:empty {
padding: 0;
border: 0;
}
/*-----------------------------------------------------------------------------
| executeResult is added to any Output-result for the display of the object
| returned by a cell
|----------------------------------------------------------------------------*/
.jp-OutputArea-output.jp-OutputArea-executeResult {
margin-left: 0px;
flex: 1 1 auto;
}
/* Text output with the Out[] prompt needs a top padding to match the
* alignment of the Out[] prompt itself.
*/
.jp-OutputArea-executeResult .jp-RenderedText.jp-OutputArea-output {
padding-top: var(--jp-code-padding);
border-top: var(--jp-border-width) solid transparent;
}
/*-----------------------------------------------------------------------------
| The Stdin output
|----------------------------------------------------------------------------*/
.jp-Stdin-prompt {
color: var(--jp-content-font-color0);
padding-right: var(--jp-code-padding);
vertical-align: baseline;
flex: 0 0 auto;
}
.jp-Stdin-input {
font-family: var(--jp-code-font-family);
font-size: inherit;
color: inherit;
background-color: inherit;
width: 42%;
min-width: 200px;
/* make sure input baseline aligns with prompt */
vertical-align: baseline;
/* padding + margin = 0.5em between prompt and cursor */
padding: 0em 0.25em;
margin: 0em 0.25em;
flex: 0 0 70%;
}
.jp-Stdin-input::placeholder {
opacity: 0;
}
.jp-Stdin-input:focus {
box-shadow: none;
}
.jp-Stdin-input:focus::placeholder {
opacity: 1;
}
/*-----------------------------------------------------------------------------
| Output Area View
|----------------------------------------------------------------------------*/
.jp-LinkedOutputView .jp-OutputArea {
height: 100%;
display: block;
}
.jp-LinkedOutputView .jp-OutputArea-output:only-child {
height: 100%;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
.jp-Collapser {
flex: 0 0 var(--jp-cell-collapser-width);
padding: 0px;
margin: 0px;
border: none;
outline: none;
background: transparent;
border-radius: var(--jp-border-radius);
opacity: 1;
}
.jp-Collapser-child {
display: block;
width: 100%;
box-sizing: border-box;
/* height: 100% doesn't work because the height of its parent is computed from content */
position: absolute;
top: 0px;
bottom: 0px;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Header/Footer
|----------------------------------------------------------------------------*/
/* Hidden by zero height by default */
.jp-CellHeader,
.jp-CellFooter {
height: 0px;
width: 100%;
padding: 0px;
margin: 0px;
border: none;
outline: none;
background: transparent;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Input
|----------------------------------------------------------------------------*/
/* All input areas */
.jp-InputArea {
display: flex;
flex-direction: row;
overflow: hidden;
}
body[data-format='mobile'] .jp-InputArea {
flex-direction: column;
}
.jp-InputArea-editor {
flex: 1 1 auto;
overflow: hidden;
}
.jp-InputArea-editor {
/* This is the non-active, default styling */
border: var(--jp-border-width) solid var(--jp-cell-editor-border-color);
border-radius: 0px;
background: var(--jp-cell-editor-background);
}
body[data-format='mobile'] .jp-InputArea-editor {
margin-left: var(--jp-notebook-padding);
}
.jp-InputPrompt {
flex: 0 0 var(--jp-cell-prompt-width);
color: var(--jp-cell-inprompt-font-color);
font-family: var(--jp-cell-prompt-font-family);
padding: var(--jp-code-padding);
letter-spacing: var(--jp-cell-prompt-letter-spacing);
opacity: var(--jp-cell-prompt-opacity);
line-height: var(--jp-code-line-height);
font-size: var(--jp-code-font-size);
border: var(--jp-border-width) solid transparent;
opacity: var(--jp-cell-prompt-opacity);
/* Right align prompt text, don't wrap to handle large prompt numbers */
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* Disable text selection */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
body[data-format='mobile'] .jp-InputPrompt {
flex: 0 0 auto;
text-align: left;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Placeholder
|----------------------------------------------------------------------------*/
.jp-Placeholder {
display: flex;
flex-direction: row;
flex: 1 1 auto;
}
.jp-Placeholder-prompt {
box-sizing: border-box;
}
.jp-Placeholder-content {
flex: 1 1 auto;
border: none;
background: transparent;
height: 20px;
box-sizing: border-box;
}
.jp-Placeholder-content .jp-MoreHorizIcon {
width: 32px;
height: 16px;
border: 1px solid transparent;
border-radius: var(--jp-border-radius);
}
.jp-Placeholder-content .jp-MoreHorizIcon:hover {
border: 1px solid var(--jp-border-color1);
box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.25);
background-color: var(--jp-layout-color0);
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Private <span class="caps">CSS</span> variables
|----------------------------------------------------------------------------*/
:root {
--jp-private-cell-scrolling-output-offset: 5px;
}
/*-----------------------------------------------------------------------------
| Cell
|----------------------------------------------------------------------------*/
.jp-Cell {
padding: var(--jp-cell-padding);
margin: 0px;
border: none;
outline: none;
background: transparent;
}
/*-----------------------------------------------------------------------------
| Common input/output
|----------------------------------------------------------------------------*/
.jp-Cell-inputWrapper,
.jp-Cell-outputWrapper {
display: flex;
flex-direction: row;
padding: 0px;
margin: 0px;
/* Added to reveal the box-shadow on the input and output collapsers. */
overflow: visible;
}
/* Only input/output areas inside cells */
.jp-Cell-inputArea,
.jp-Cell-outputArea {
flex: 1 1 auto;
}
/*-----------------------------------------------------------------------------
| Collapser
|----------------------------------------------------------------------------*/
/* Make the output collapser disappear when there is not output, but do so
* in a manner that leaves it in the layout and preserves its width.
*/
.jp-Cell.jp-mod-noOutputs .jp-Cell-outputCollapser {
border: none !important;
background: transparent !important;
}
.jp-Cell:not(.jp-mod-noOutputs) .jp-Cell-outputCollapser {
min-height: var(--jp-cell-collapser-min-height);
}
/*-----------------------------------------------------------------------------
| Output
|----------------------------------------------------------------------------*/
/* Put a space between input and output when there <span class="caps">IS</span> output */
.jp-Cell:not(.jp-mod-noOutputs) .jp-Cell-outputWrapper {
margin-top: 5px;
}
.jp-CodeCell.jp-mod-outputsScrolled .jp-Cell-outputArea {
overflow-y: auto;
max-height: 24em;
margin-left: var(--jp-private-cell-scrolling-output-offset);
}
.jp-CodeCell.jp-mod-outputsScrolled .jp-Cell-outputArea::after {
content: ' ';
box-shadow: inset 0 0 6px 2px rgb(0 0 0 / 30%);
width: 100%;
height: 100%;
position: sticky;
bottom: 0;
top: 0;
margin-top: -50%;
float: left;
display: block;
pointer-events: none;
}
.jp-CodeCell.jp-mod-outputsScrolled .jp-OutputArea-child {
padding-top: 6px;
}
.jp-CodeCell.jp-mod-outputsScrolled .jp-OutputArea-prompt {
flex: 0 0
calc(
var(--jp-cell-prompt-width) -
var(--jp-private-cell-scrolling-output-offset)
);
}
/*-----------------------------------------------------------------------------
| CodeCell
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| MarkdownCell
|----------------------------------------------------------------------------*/
.jp-MarkdownOutput {
flex: 1 1 auto;
margin-top: 0;
margin-bottom: 0;
padding-left: var(--jp-code-padding);
}
.jp-MarkdownOutput.jp-RenderedHTMLCommon {
overflow: auto;
}
/* collapseHeadingButton (show always if hiddenCellsButton is _not_ shown) */
.jp-collapseHeadingButton {
display: none;
min-height: var(--jp-cell-collapser-min-height);
font-size: var(--jp-code-font-size);
position: absolute;
right: 0;
top: 0;
bottom: 0;
background-color: transparent;
background-size: 25px;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: top;
background-image: var(--jp-icon-caret-down);
border: none;
cursor: pointer;
}
.jp-collapseHeadingButton:hover {
background-color: var(--jp-layout-color2);
}
.jp-collapseHeadingButton.jp-mod-collapsed {
background-image: var(--jp-icon-caret-right);
}
:is(.jp-MarkdownCell:hover, .jp-mod-active) .jp-collapseHeadingButton {
display: flex;
}
/*
set the container font size to match that of content
so that the nested collapse buttons have the right size
*/
.jp-MarkdownCell .jp-InputPrompt {
font-size: var(--jp-content-font-size1);
}
/*
Align collapseHeadingButton with cell top header
The font sizes are identical to the ones in packages/rendermime/style/base.css
*/
.jp-mod-rendered .jp-collapseHeadingButton[data-heading-level='1'] {
font-size: var(--jp-content-font-size5);
background-position-y: calc(0.3 * var(--jp-content-font-size5));
}
.jp-mod-rendered .jp-collapseHeadingButton[data-heading-level='2'] {
font-size: var(--jp-content-font-size4);
background-position-y: calc(0.3 * var(--jp-content-font-size4));
}
.jp-mod-rendered .jp-collapseHeadingButton[data-heading-level='3'] {
font-size: var(--jp-content-font-size3);
background-position-y: calc(0.3 * var(--jp-content-font-size3));
}
.jp-mod-rendered .jp-collapseHeadingButton[data-heading-level='4'] {
font-size: var(--jp-content-font-size2);
background-position-y: calc(0.3 * var(--jp-content-font-size2));
}
.jp-mod-rendered .jp-collapseHeadingButton[data-heading-level='5'] {
font-size: var(--jp-content-font-size1);
background-position-y: top;
}
.jp-mod-rendered .jp-collapseHeadingButton[data-heading-level='6'] {
font-size: var(--jp-content-font-size0);
background-position-y: top;
}
.jp-showHiddenCellsButton {
margin-left: calc(var(--jp-cell-prompt-width) + 2 * var(--jp-code-padding));
margin-top: var(--jp-code-padding);
border: 1px solid var(--jp-border-color2);
background-color: var(--jp-border-color3) !important;
color: var(--jp-content-font-color0) !important;
}
.jp-showHiddenCellsButton:hover {
background-color: var(--jp-border-color2) !important;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Variables
|----------------------------------------------------------------------------*/
:root {
--jp-notebook-toolbar-padding: 2px 5px 2px 2px;
}
/*-----------------------------------------------------------------------------
/*-----------------------------------------------------------------------------
| Styles
|----------------------------------------------------------------------------*/
.jp-NotebookPanel-toolbar {
padding: var(--jp-notebook-toolbar-padding);
}
.jp-Toolbar-item.jp-Notebook-toolbarCellType .jp-select-wrapper.jp-mod-focused {
border: none;
box-shadow: none;
}
.jp-Notebook-toolbarCellTypeDropdown select {
height: 24px;
font-size: var(--jp-ui-font-size1);
line-height: 14px;
border-radius: 0;
display: block;
}
.jp-Notebook-toolbarCellTypeDropdown span {
top: 5px !important;
}
.jp-Toolbar-responsive-popup {
position: absolute;
height: fit-content;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
border-bottom: var(--jp-border-width) solid var(--jp-toolbar-border-color);
box-shadow: var(--jp-toolbar-box-shadow);
background: var(--jp-toolbar-background);
min-height: var(--jp-toolbar-micro-height);
padding: var(--jp-notebook-toolbar-padding);
z-index: 1;
right: 0px;
top: 0px;
}
.jp-Toolbar > .jp-Toolbar-responsive-opener {
margin-left: auto;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Variables
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
/*-----------------------------------------------------------------------------
| Styles
|----------------------------------------------------------------------------*/
.jp-Notebook-ExecutionIndicator {
position: relative;
display: inline-block;
height: 100%;
z-index: 9997;
}
.jp-Notebook-ExecutionIndicator-tooltip {
visibility: hidden;
height: auto;
width: max-content;
width: -moz-max-content;
background-color: var(--jp-layout-color2);
color: var(--jp-ui-font-color1);
text-align: justify;
border-radius: 6px;
padding: 0 5px;
position: fixed;
display: table;
}
.jp-Notebook-ExecutionIndicator-tooltip.up {
transform: translateX(-50%) translateY(-100%) translateY(-32px);
}
.jp-Notebook-ExecutionIndicator-tooltip.down {
transform: translateX(calc(-100% + 16px)) translateY(5px);
}
.jp-Notebook-ExecutionIndicator-tooltip.hidden {
display: none;
}
.jp-Notebook-ExecutionIndicator:hover .jp-Notebook-ExecutionIndicator-tooltip {
visibility: visible;
}
.jp-Notebook-ExecutionIndicator span {
font-size: var(--jp-ui-font-size1);
font-family: var(--jp-ui-font-family);
color: var(--jp-ui-font-color1);
line-height: 24px;
display: block;
}
.jp-Notebook-ExecutionIndicator-progress-bar {
display: flex;
justify-content: center;
height: 100%;
}
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Private <span class="caps">CSS</span> variables
|----------------------------------------------------------------------------*/
:root {
--jp-private-notebook-dragImage-width: 304px;
--jp-private-notebook-dragImage-height: 36px;
--jp-private-notebook-selected-color: var(--md-blue-400);
--jp-private-notebook-active-color: var(--md-green-400);
}
/*-----------------------------------------------------------------------------
| Imports
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| Notebook
|----------------------------------------------------------------------------*/
.jp-NotebookPanel {
display: block;
height: 100%;
}
.jp-NotebookPanel.jp-Document {
min-width: 240px;
min-height: 120px;
}
.jp-Notebook {
padding: var(--jp-notebook-padding);
outline: none;
overflow: auto;
background: var(--jp-layout-color0);
}
.jp-Notebook.jp-mod-scrollPastEnd::after {
display: block;
content: '';
min-height: var(--jp-notebook-scroll-padding);
}
.jp-MainAreaWidget-ContainStrict .jp-Notebook * {
contain: strict;
}
.jp-Notebook .jp-Cell {
overflow: visible;
}
.jp-Notebook .jp-Cell .jp-InputPrompt {
cursor: move;
float: left;
}
/*-----------------------------------------------------------------------------
| Notebook state related styling
|
| The notebook and cells each have states, here are the possibilities:
|
| - Notebook
| - Command
| - Edit
| - Cell
| - None
| - Active (only one can be active)
| - Selected (the cells actions are applied to)
| - Multiselected (when multiple selected, the cursor)
| - No outputs
|----------------------------------------------------------------------------*/
/* Command or edit modes */
.jp-Notebook .jp-Cell:not(.jp-mod-active) .jp-InputPrompt {
opacity: var(--jp-cell-prompt-not-active-opacity);
color: var(--jp-cell-prompt-not-active-font-color);
}
.jp-Notebook .jp-Cell:not(.jp-mod-active) .jp-OutputPrompt {
opacity: var(--jp-cell-prompt-not-active-opacity);
color: var(--jp-cell-prompt-not-active-font-color);
}
/* cell is active */
.jp-Notebook .jp-Cell.jp-mod-active .jp-Collapser {
background: var(--jp-brand-color1);
}
/* cell is dirty */
.jp-Notebook .jp-Cell.jp-mod-dirty .jp-InputPrompt {
color: var(--jp-warn-color1);
}
.jp-Notebook .jp-Cell.jp-mod-dirty .jp-InputPrompt:before {
color: var(--jp-warn-color1);
content: '•';
}
.jp-Notebook .jp-Cell.jp-mod-active.jp-mod-dirty .jp-Collapser {
background: var(--jp-warn-color1);
}
/* collapser is hovered */
.jp-Notebook .jp-Cell .jp-Collapser:hover {
box-shadow: var(--jp-elevation-z2);
background: var(--jp-brand-color1);
opacity: var(--jp-cell-collapser-not-active-hover-opacity);
}
/* cell is active and collapser is hovered */
.jp-Notebook .jp-Cell.jp-mod-active .jp-Collapser:hover {
background: var(--jp-brand-color0);
opacity: 1;
}
/* Command mode */
.jp-Notebook.jp-mod-commandMode .jp-Cell.jp-mod-selected {
background: var(--jp-notebook-multiselected-color);
}
.jp-Notebook.jp-mod-commandMode
.jp-Cell.jp-mod-active.jp-mod-selected:not(.jp-mod-multiSelected) {
background: transparent;
}
/* Edit mode */
.jp-Notebook.jp-mod-editMode .jp-Cell.jp-mod-active .jp-InputArea-editor {
border: var(--jp-border-width) solid var(--jp-cell-editor-active-border-color);
box-shadow: var(--jp-input-box-shadow);
background-color: var(--jp-cell-editor-active-background);
}
/*-----------------------------------------------------------------------------
| Notebook drag and drop
|----------------------------------------------------------------------------*/
.jp-Notebook-cell.jp-mod-dropSource {
opacity: 0.5;
}
.jp-Notebook-cell.jp-mod-dropTarget,
.jp-Notebook.jp-mod-commandMode
.jp-Notebook-cell.jp-mod-active.jp-mod-selected.jp-mod-dropTarget {
border-top-color: var(--jp-private-notebook-selected-color);
border-top-style: solid;
border-top-width: 2px;
}
.jp-dragImage {
display: block;
flex-direction: row;
width: var(--jp-private-notebook-dragImage-width);
height: var(--jp-private-notebook-dragImage-height);
border: var(--jp-border-width) solid var(--jp-cell-editor-border-color);
background: var(--jp-cell-editor-background);
overflow: visible;
}
.jp-dragImage-singlePrompt {
box-shadow: 2px 2px 4px 0px rgba(0, 0, 0, 0.12);
}
.jp-dragImage .jp-dragImage-content {
flex: 1 1 auto;
z-index: 2;
font-size: var(--jp-code-font-size);
font-family: var(--jp-code-font-family);
line-height: var(--jp-code-line-height);
padding: var(--jp-code-padding);
border: var(--jp-border-width) solid var(--jp-cell-editor-border-color);
background: var(--jp-cell-editor-background-color);
color: var(--jp-content-font-color3);
text-align: left;
margin: 4px 4px 4px 0px;
}
.jp-dragImage .jp-dragImage-prompt {
flex: 0 0 auto;
min-width: 36px;
color: var(--jp-cell-inprompt-font-color);
padding: var(--jp-code-padding);
padding-left: 12px;
font-family: var(--jp-cell-prompt-font-family);
letter-spacing: var(--jp-cell-prompt-letter-spacing);
line-height: 1.9;
font-size: var(--jp-code-font-size);
border: var(--jp-border-width) solid transparent;
}
.jp-dragImage-multipleBack {
z-index: -1;
position: absolute;
height: 32px;
width: 300px;
top: 8px;
left: 8px;
background: var(--jp-layout-color2);
border: var(--jp-border-width) solid var(--jp-input-border-color);
box-shadow: 2px 2px 4px 0px rgba(0, 0, 0, 0.12);
}
/*-----------------------------------------------------------------------------
| Cell toolbar
|----------------------------------------------------------------------------*/
.jp-NotebookTools {
display: block;
min-width: var(--jp-sidebar-min-width);
color: var(--jp-ui-font-color1);
background: var(--jp-layout-color1);
/* This is needed so that all font sizing of children done in ems is
* relative to this base size */
font-size: var(--jp-ui-font-size1);
overflow: auto;
}
.jp-NotebookTools-tool {
padding: 0px 12px 0 12px;
}
.jp-ActiveCellTool {
padding: 12px;
background-color: var(--jp-layout-color1);
border-top: none !important;
}
.jp-ActiveCellTool .jp-InputArea-prompt {
flex: 0 0 auto;
padding-left: 0px;
}
.jp-ActiveCellTool .jp-InputArea-editor {
flex: 1 1 auto;
background: var(--jp-cell-editor-background);
border-color: var(--jp-cell-editor-border-color);
}
.jp-ActiveCellTool .jp-InputArea-editor .CodeMirror {
background: transparent;
}
.jp-MetadataEditorTool {
flex-direction: column;
padding: 12px 0px 12px 0px;
}
.jp-RankedPanel > :not(:first-child) {
margin-top: 12px;
}
.jp-KeySelector select.jp-mod-styled {
font-size: var(--jp-ui-font-size1);
color: var(--jp-ui-font-color0);
border: var(--jp-border-width) solid var(--jp-border-color1);
}
.jp-KeySelector label,
.jp-MetadataEditorTool label {
line-height: 1.4;
}
.jp-NotebookTools .jp-select-wrapper {
margin-top: 4px;
margin-bottom: 0px;
}
.jp-NotebookTools .jp-Collapse {
margin-top: 16px;
}
/*-----------------------------------------------------------------------------
| Presentation Mode (.jp-mod-presentationMode)
|----------------------------------------------------------------------------*/
.jp-mod-presentationMode .jp-Notebook {
--jp-content-font-size1: var(--jp-content-presentation-font-size1);
--jp-code-font-size: var(--jp-code-presentation-font-size);
}
.jp-mod-presentationMode .jp-Notebook .jp-Cell .jp-InputPrompt,
.jp-mod-presentationMode .jp-Notebook .jp-Cell .jp-OutputPrompt {
flex: 0 0 110px;
}
/*-----------------------------------------------------------------------------
| Side-by-side Mode (.jp-mod-sideBySide)
|----------------------------------------------------------------------------*/
:root {
--jp-side-by-side-output-size: 1fr;
--jp-side-by-side-resized-cell: var(--jp-side-by-side-output-size);
}
.jp-mod-sideBySide.jp-Notebook .jp-Notebook-cell {
margin-top: 3em;
margin-bottom: 3em;
margin-left: 5%;
margin-right: 5%;
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell {
display: grid;
grid-template-columns: minmax(0, 1fr) min-content minmax(
0,
var(--jp-side-by-side-output-size)
);
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-areas:
'header header header'
'input handle output'
'footer footer footer';
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell.jp-mod-resizedCell {
grid-template-columns: minmax(0, 1fr) min-content minmax(
0,
var(--jp-side-by-side-resized-cell)
);
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell .jp-CellHeader {
grid-area: header;
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell .jp-Cell-inputWrapper {
grid-area: input;
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell .jp-Cell-outputWrapper {
/* overwrite the default margin (no vertical separation needed in side by side move */
margin-top: 0;
grid-area: output;
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell .jp-CellFooter {
grid-area: footer;
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell .jp-CellResizeHandle {
grid-area: handle;
user-select: none;
display: block;
height: 100%;
cursor: ew-resize;
padding: 0 var(--jp-cell-padding);
}
.jp-mod-sideBySide.jp-Notebook .jp-CodeCell .jp-CellResizeHandle::after {
content: '';
display: block;
background: var(--jp-border-color2);
height: 100%;
width: 5px;
}
.jp-mod-sideBySide.jp-Notebook
.jp-CodeCell.jp-mod-resizedCell
.jp-CellResizeHandle::after {
background: var(--jp-border-color0);
}
.jp-CellResizeHandle {
display: none;
}
/*-----------------------------------------------------------------------------
| Placeholder
|----------------------------------------------------------------------------*/
.jp-Cell-Placeholder {
padding-left: 55px;
}
.jp-Cell-Placeholder-wrapper {
background: #fff;
border: 1px solid;
border-color: #e5e6e9 #dfe0e4 #d0d1d5;
border-radius: 4px;
-webkit-border-radius: 4px;
margin: 10px 15px;
}
.jp-Cell-Placeholder-wrapper-inner {
padding: 15px;
position: relative;
}
.jp-Cell-Placeholder-wrapper-body {
background-repeat: repeat;
background-size: 50% auto;
}
.jp-Cell-Placeholder-wrapper-body div {
background: #f6f7f8;
background-image: -webkit-linear-gradient(
left,
#f6f7f8 0%,
#edeef1 20%,
#f6f7f8 40%,
#f6f7f8 100%
);
background-repeat: no-repeat;
background-size: 800px 104px;
height: 104px;
position: relative;
}
.jp-Cell-Placeholder-wrapper-body div {
position: absolute;
right: 15px;
left: 15px;
top: 15px;
}
div.jp-Cell-Placeholder-h1 {
top: 20px;
height: 20px;
left: 15px;
width: 150px;
}
div.jp-Cell-Placeholder-h2 {
left: 15px;
top: 50px;
height: 10px;
width: 100px;
}
div.jp-Cell-Placeholder-content-1,
div.jp-Cell-Placeholder-content-2,
div.jp-Cell-Placeholder-content-3 {
left: 15px;
right: 15px;
height: 10px;
}
div.jp-Cell-Placeholder-content-1 {
top: 100px;
}
div.jp-Cell-Placeholder-content-2 {
top: 120px;
}
div.jp-Cell-Placeholder-content-3 {
top: 140px;
}
</style>
<style type="text/css">
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified <span class="caps">BSD</span> License.
|----------------------------------------------------------------------------*/
/*
The following <span class="caps">CSS</span> variables define the main, public <span class="caps">API</span> for styling JupyterLab.
These variables should be used by all plugins wherever possible. In other
words, plugins should not define custom colors, sizes, etc unless absolutely
necessary. This enables users to change the visual theme of JupyterLab
by changing these variables.
Many variables appear in an ordered sequence (0,1,2,3). These sequences
are designed to work well together, so for example, `--jp-border-color1` should
be used with `--jp-layout-color1`. The numbers have the following meanings:
* 0: super-primary, reserved for special emphasis
* 1: primary, most important under normal situations
* 2: secondary, next most important under normal situations
* 3: tertiary, next most important under normal situations
Throughout JupyterLab, we are mostly following principles from Google's
Material Design when selecting colors. We are not, however, following
all of <span class="caps">MD</span> as it is not optimized for dense, information rich UIs.
*/
:root {
/* Elevation
*
* We style box-shadows using Material Design's idea of elevation. These particular numbers are taken from here:
*
* https://github.com/material-components/material-components-web
* https://material-components-web.appspot.com/elevation.html
*/
--jp-shadow-base-lightness: 0;
--jp-shadow-umbra-color: rgba(
var(--jp-shadow-base-lightness),
var(--jp-shadow-base-lightness),
var(--jp-shadow-base-lightness),
0.2
);
--jp-shadow-penumbra-color: rgba(
var(--jp-shadow-base-lightness),
var(--jp-shadow-base-lightness),
var(--jp-shadow-base-lightness),
0.14
);
--jp-shadow-ambient-color: rgba(
var(--jp-shadow-base-lightness),
var(--jp-shadow-base-lightness),
var(--jp-shadow-base-lightness),
0.12
);
--jp-elevation-z0: none;
--jp-elevation-z1: 0px 2px 1px -1px var(--jp-shadow-umbra-color),
0px 1px 1px 0px var(--jp-shadow-penumbra-color),
0px 1px 3px 0px var(--jp-shadow-ambient-color);
--jp-elevation-z2: 0px 3px 1px -2px var(--jp-shadow-umbra-color),
0px 2px 2px 0px var(--jp-shadow-penumbra-color),
0px 1px 5px 0px var(--jp-shadow-ambient-color);
--jp-elevation-z4: 0px 2px 4px -1px var(--jp-shadow-umbra-color),
0px 4px 5px 0px var(--jp-shadow-penumbra-color),
0px 1px 10px 0px var(--jp-shadow-ambient-color);
--jp-elevation-z6: 0px 3px 5px -1px var(--jp-shadow-umbra-color),
0px 6px 10px 0px var(--jp-shadow-penumbra-color),
0px 1px 18px 0px var(--jp-shadow-ambient-color);
--jp-elevation-z8: 0px 5px 5px -3px var(--jp-shadow-umbra-color),
0px 8px 10px 1px var(--jp-shadow-penumbra-color),
0px 3px 14px 2px var(--jp-shadow-ambient-color);
--jp-elevation-z12: 0px 7px 8px -4px var(--jp-shadow-umbra-color),
0px 12px 17px 2px var(--jp-shadow-penumbra-color),
0px 5px 22px 4px var(--jp-shadow-ambient-color);
--jp-elevation-z16: 0px 8px 10px -5px var(--jp-shadow-umbra-color),
0px 16px 24px 2px var(--jp-shadow-penumbra-color),
0px 6px 30px 5px var(--jp-shadow-ambient-color);
--jp-elevation-z20: 0px 10px 13px -6px var(--jp-shadow-umbra-color),
0px 20px 31px 3px var(--jp-shadow-penumbra-color),
0px 8px 38px 7px var(--jp-shadow-ambient-color);
--jp-elevation-z24: 0px 11px 15px -7px var(--jp-shadow-umbra-color),
0px 24px 38px 3px var(--jp-shadow-penumbra-color),
0px 9px 46px 8px var(--jp-shadow-ambient-color);
/* Borders
*
* The following variables, specify the visual styling of borders in JupyterLab.
*/
--jp-border-width: 1px;
--jp-border-color0: var(--md-grey-400);
--jp-border-color1: var(--md-grey-400);
--jp-border-color2: var(--md-grey-300);
--jp-border-color3: var(--md-grey-200);
--jp-inverse-border-color: var(--md-grey-600);
--jp-border-radius: 2px;
/* <span class="caps">UI</span> Fonts
*
* The <span class="caps">UI</span> font <span class="caps">CSS</span> variables are used for the typography all of the JupyterLab
* user interface elements that are not directly user generated content.
*
* The font sizing here is done assuming that the body font size of --jp-ui-font-size1
* is applied to a parent element. When children elements, such as headings, are sized
* in em all things will be computed relative to that body size.
*/
--jp-ui-font-scale-factor: 1.2;
--jp-ui-font-size0: 0.83333em;
--jp-ui-font-size1: 13px; /* Base font size */
--jp-ui-font-size2: 1.2em;
--jp-ui-font-size3: 1.44em;
--jp-ui-font-family: -apple-system, BlinkMacSystemFont, 'Segoe <span class="caps">UI</span>', Helvetica,
Arial, sans-serif, 'Apple Color Emoji', 'Segoe <span class="caps">UI</span> Emoji', 'Segoe <span class="caps">UI</span> Symbol';
/*
* Use these font colors against the corresponding main layout colors.
* In a light theme, these go from dark to light.
*/
/* Defaults use Material Design specification */
--jp-ui-font-color0: rgba(0, 0, 0, 1);
--jp-ui-font-color1: rgba(0, 0, 0, 0.87);
--jp-ui-font-color2: rgba(0, 0, 0, 0.54);
--jp-ui-font-color3: rgba(0, 0, 0, 0.38);
/*
* Use these against the brand/accent/warn/error colors.
* These will typically go from light to darker, in both a dark and light theme.
*/
--jp-ui-inverse-font-color0: rgba(255, 255, 255, 1);
--jp-ui-inverse-font-color1: rgba(255, 255, 255, 1);
--jp-ui-inverse-font-color2: rgba(255, 255, 255, 0.7);
--jp-ui-inverse-font-color3: rgba(255, 255, 255, 0.5);
/* Content Fonts
*
* Content font variables are used for typography of user generated content.
*
* The font sizing here is done assuming that the body font size of --jp-content-font-size1
* is applied to a parent element. When children elements, such as headings, are sized
* in em all things will be computed relative to that body size.
*/
--jp-content-line-height: 1.6;
--jp-content-font-scale-factor: 1.2;
--jp-content-font-size0: 0.83333em;
--jp-content-font-size1: 14px; /* Base font size */
--jp-content-font-size2: 1.2em;
--jp-content-font-size3: 1.44em;
--jp-content-font-size4: 1.728em;
--jp-content-font-size5: 2.0736em;
/* This gives a magnification of about 125% in presentation mode over normal. */
--jp-content-presentation-font-size1: 17px;
--jp-content-heading-line-height: 1;
--jp-content-heading-margin-top: 1.2em;
--jp-content-heading-margin-bottom: 0.8em;
--jp-content-heading-font-weight: 500;
/* Defaults use Material Design specification */
--jp-content-font-color0: rgba(0, 0, 0, 1);
--jp-content-font-color1: rgba(0, 0, 0, 0.87);
--jp-content-font-color2: rgba(0, 0, 0, 0.54);
--jp-content-font-color3: rgba(0, 0, 0, 0.38);
--jp-content-link-color: var(--md-blue-700);
--jp-content-font-family: -apple-system, BlinkMacSystemFont, 'Segoe <span class="caps">UI</span>',
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe <span class="caps">UI</span> Emoji',
'Segoe <span class="caps">UI</span> Symbol';
/*
* Code Fonts
*
* Code font variables are used for typography of code and other monospaces content.
*/
--jp-code-font-size: 13px;
--jp-code-line-height: 1.3077; /* 17px for 13px base */
--jp-code-padding: 5px; /* 5px for 13px base, codemirror highlighting needs integer px value */
--jp-code-font-family-default: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
--jp-code-font-family: var(--jp-code-font-family-default);
/* This gives a magnification of about 125% in presentation mode over normal. */
--jp-code-presentation-font-size: 16px;
/* may need to tweak cursor width if you change font size */
--jp-code-cursor-width0: 1.4px;
--jp-code-cursor-width1: 2px;
--jp-code-cursor-width2: 4px;
/* Layout
*
* The following are the main layout colors use in JupyterLab. In a light
* theme these would go from light to dark.
*/
--jp-layout-color0: white;
--jp-layout-color1: white;
--jp-layout-color2: var(--md-grey-200);
--jp-layout-color3: var(--md-grey-400);
--jp-layout-color4: var(--md-grey-600);
/* Inverse Layout
*
* The following are the inverse layout colors use in JupyterLab. In a light
* theme these would go from dark to light.
*/
--jp-inverse-layout-color0: #111111;
--jp-inverse-layout-color1: var(--md-grey-900);
--jp-inverse-layout-color2: var(--md-grey-800);
--jp-inverse-layout-color3: var(--md-grey-700);
--jp-inverse-layout-color4: var(--md-grey-600);
/* Brand/accent */
--jp-brand-color0: var(--md-blue-900);
--jp-brand-color1: var(--md-blue-700);
--jp-brand-color2: var(--md-blue-300);
--jp-brand-color3: var(--md-blue-100);
--jp-brand-color4: var(--md-blue-50);
--jp-accent-color0: var(--md-green-900);
--jp-accent-color1: var(--md-green-700);
--jp-accent-color2: var(--md-green-300);
--jp-accent-color3: var(--md-green-100);
/* State colors (warn, error, success, info) */
--jp-warn-color0: var(--md-orange-900);
--jp-warn-color1: var(--md-orange-700);
--jp-warn-color2: var(--md-orange-300);
--jp-warn-color3: var(--md-orange-100);
--jp-error-color0: var(--md-red-900);
--jp-error-color1: var(--md-red-700);
--jp-error-color2: var(--md-red-300);
--jp-error-color3: var(--md-red-100);
--jp-success-color0: var(--md-green-900);
--jp-success-color1: var(--md-green-700);
--jp-success-color2: var(--md-green-300);
--jp-success-color3: var(--md-green-100);
--jp-info-color0: var(--md-cyan-900);
--jp-info-color1: var(--md-cyan-700);
--jp-info-color2: var(--md-cyan-300);
--jp-info-color3: var(--md-cyan-100);
/* Cell specific styles */
--jp-cell-padding: 5px;
--jp-cell-collapser-width: 8px;
--jp-cell-collapser-min-height: 20px;
--jp-cell-collapser-not-active-hover-opacity: 0.6;
--jp-cell-editor-background: var(--md-grey-100);
--jp-cell-editor-border-color: var(--md-grey-300);
--jp-cell-editor-box-shadow: inset 0 0 2px var(--md-blue-300);
--jp-cell-editor-active-background: var(--jp-layout-color0);
--jp-cell-editor-active-border-color: var(--jp-brand-color1);
--jp-cell-prompt-width: 64px;
--jp-cell-prompt-font-family: var(--jp-code-font-family-default);
--jp-cell-prompt-letter-spacing: 0px;
--jp-cell-prompt-opacity: 1;
--jp-cell-prompt-not-active-opacity: 0.5;
--jp-cell-prompt-not-active-font-color: var(--md-grey-700);
/* A custom blend of <span class="caps">MD</span> grey and blue 600
* See https://meyerweb.com/eric/tools/color-blend/#<span class="caps">546E7A</span>:<span class="caps">1E88E5</span>:5:hex */
--jp-cell-inprompt-font-color: #307fc1;
/* A custom blend of <span class="caps">MD</span> grey and orange 600
* https://meyerweb.com/eric/tools/color-blend/#<span class="caps">546E7A</span>:<span class="caps">F4511E</span>:5:hex */
--jp-cell-outprompt-font-color: #bf5b3d;
/* Notebook specific styles */
--jp-notebook-padding: 10px;
--jp-notebook-select-background: var(--jp-layout-color1);
--jp-notebook-multiselected-color: var(--md-blue-50);
/* The scroll padding is calculated to fill enough space at the bottom of the
notebook to show one single-line cell (with appropriate padding) at the top
when the notebook is scrolled all the way to the bottom. We also subtract one
pixel so that no scrollbar appears if we have just one single-line cell in the
notebook. This padding is to enable a 'scroll past end' feature in a notebook.
*/
--jp-notebook-scroll-padding: calc(
100% - var(--jp-code-font-size) * var(--jp-code-line-height) -
var(--jp-code-padding) - var(--jp-cell-padding) - 1px
);
/* Rendermime styles */
--jp-rendermime-error-background: #fdd;
--jp-rendermime-table-row-background: var(--md-grey-100);
--jp-rendermime-table-row-hover-background: var(--md-light-blue-50);
/* Dialog specific styles */
--jp-dialog-background: rgba(0, 0, 0, 0.25);
/* Console specific styles */
--jp-console-padding: 10px;
/* Toolbar specific styles */
--jp-toolbar-border-color: var(--jp-border-color1);
--jp-toolbar-micro-height: 8px;
--jp-toolbar-background: var(--jp-layout-color1);
--jp-toolbar-box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.24);
--jp-toolbar-header-margin: 4px 4px 0px 4px;
--jp-toolbar-active-background: var(--md-grey-300);
/* Statusbar specific styles */
--jp-statusbar-height: 24px;
/* Input field styles */
--jp-input-box-shadow: inset 0 0 2px var(--md-blue-300);
--jp-input-active-background: var(--jp-layout-color1);
--jp-input-hover-background: var(--jp-layout-color1);
--jp-input-background: var(--md-grey-100);
--jp-input-border-color: var(--jp-inverse-border-color);
--jp-input-active-border-color: var(--jp-brand-color1);
--jp-input-active-box-shadow-color: rgba(19, 124, 189, 0.3);
/* General editor styles */
--jp-editor-selected-background: #d9d9d9;
--jp-editor-selected-focused-background: #d7d4f0;
--jp-editor-cursor-color: var(--jp-ui-font-color0);
/* Code mirror specific styles */
--jp-mirror-editor-keyword-color: #008000;
--jp-mirror-editor-atom-color: #88f;
--jp-mirror-editor-number-color: #080;
--jp-mirror-editor-def-color: #00f;
--jp-mirror-editor-variable-color: var(--md-grey-900);
--jp-mirror-editor-variable-2-color: #05a;
--jp-mirror-editor-variable-3-color: #085;
--jp-mirror-editor-punctuation-color: #05a;
--jp-mirror-editor-property-color: #05a;
--jp-mirror-editor-operator-color: #aa22ff;
--jp-mirror-editor-comment-color: #408080;
--jp-mirror-editor-string-color: #ba2121;
--jp-mirror-editor-string-2-color: #708;
--jp-mirror-editor-meta-color: #aa22ff;
--jp-mirror-editor-qualifier-color: #555;
--jp-mirror-editor-builtin-color: #008000;
--jp-mirror-editor-bracket-color: #997;
--jp-mirror-editor-tag-color: #170;
--jp-mirror-editor-attribute-color: #00c;
--jp-mirror-editor-header-color: blue;
--jp-mirror-editor-quote-color: #090;
--jp-mirror-editor-link-color: #00c;
--jp-mirror-editor-error-color: #f00;
--jp-mirror-editor-hr-color: #999;
/*
<span class="caps">RTC</span> user specific colors.
These colors are used for the cursor, username in the editor,
and the icon of the user.
*/
--jp-collaborator-color1: #ffad8e;
--jp-collaborator-color2: #dac83d;
--jp-collaborator-color3: #72dd76;
--jp-collaborator-color4: #00e4d0;
--jp-collaborator-color5: #45d4ff;
--jp-collaborator-color6: #e2b1ff;
--jp-collaborator-color7: #ff9de6;
/* Vega extension styles */
--jp-vega-background: white;
/* Sidebar-related styles */
--jp-sidebar-min-width: 250px;
/* Search-related styles */
--jp-search-toggle-off-opacity: 0.5;
--jp-search-toggle-hover-opacity: 0.8;
--jp-search-toggle-on-opacity: 1;
--jp-search-selected-match-background-color: rgb(245, 200, 0);
--jp-search-selected-match-color: black;
--jp-search-unselected-match-background-color: var(
--jp-inverse-layout-color0
);
--jp-search-unselected-match-color: var(--jp-ui-inverse-font-color0);
/* Icon colors that work well with light or dark backgrounds */
--jp-icon-contrast-color0: var(--md-purple-600);
--jp-icon-contrast-color1: var(--md-green-600);
--jp-icon-contrast-color2: var(--md-pink-600);
--jp-icon-contrast-color3: var(--md-blue-600);
/* File or activity icons and switch semantic variables */
--jp-jupyter-icon-color: #f37626;
--jp-notebook-icon-color: #f37626;
--jp-json-icon-color: var(--md-orange-700);
--jp-console-icon-background-color: var(--md-blue-700);
--jp-console-icon-color: white;
--jp-terminal-icon-background-color: var(--md-grey-800);
--jp-terminal-icon-color: var(--md-grey-200);
--jp-text-editor-icon-color: var(--md-grey-700);
--jp-inspector-icon-color: var(--md-grey-700);
--jp-switch-color: var(--md-grey-400);
--jp-switch-true-position-color: var(--md-orange-900);
}
</style>
<style type="text/css">
/* Force rendering true colors when outputing to pdf */
* {
-webkit-print-color-adjust: exact;
}
/* Misc */
a.anchor-link {
display: none;
}
/* Input area styling */
.jp-InputArea {
overflow: hidden;
}
.jp-InputArea-editor {
overflow: hidden;
}
.CodeMirror.cm-s-jupyter .highlight pre {
/* weird, but --jp-code-padding defined to be 5px but 4px horizontal padding is hardcoded for pre.CodeMirror-line */
padding: var(--jp-code-padding) 4px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
}
.jp-OutputArea-output pre {
line-height: inherit;
font-family: inherit;
}
.jp-RenderedText pre {
color: var(--jp-content-font-color1);
font-size: var(--jp-code-font-size);
}
/* Using table instead of flexbox so that we can use break-inside property */
/* <span class="caps">CSS</span> rules under this comment should not be required anymore after we move to the JupyterLab 4.0 <span class="caps">CSS</span> */
.jp-CodeCell.jp-mod-outputsScrolled .jp-OutputArea-prompt {
min-width: calc(
var(--jp-cell-prompt-width) - var(--jp-private-cell-scrolling-output-offset)
);
}
.jp-OutputArea-child {
display: table;
width: 100%;
}
.jp-OutputPrompt {
display: table-cell;
vertical-align: top;
min-width: var(--jp-cell-prompt-width);
}
body[data-format='mobile'] .jp-OutputPrompt {
display: table-row;
}
.jp-OutputArea-output {
display: table-cell;
width: 100%;
}
body[data-format='mobile'] .jp-OutputArea-child .jp-OutputArea-output {
display: table-row;
}
.jp-OutputArea-output.jp-OutputArea-executeResult {
width: 100%;
}
/* Hiding the collapser by default */
.jp-Collapser {
display: none;
}
@page {
margin: 0.5in; /* Margin for each printed piece of paper */
}
@media print {
.jp-Cell-inputWrapper,
.jp-Cell-outputWrapper {
display: block;
}
.jp-OutputArea-child {
break-inside: avoid-page;
}
}
</style>
<!-- Load mathjax -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?config=TeX-AMS_CHTML-full,Safe"> </script>
<!-- MathJax configuration -->
<script type="text/x-mathjax-config">
init_mathjax = function() {
if (window.MathJax) {
// MathJax loaded
MathJax.Hub.Config({
TeX: {
equationNumbers: {
autoNumber: "AMS",
useLabelIds: true
}
},
tex2jax: {
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
processEscapes: true,
processEnvironments: true
},
displayAlign: 'center',
CommonHTML: {
linebreaks: {
automatic: true
}
}
});
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
}
}
init_mathjax();
</script>
<!-- End of mathjax configuration --></head>
<body class="jp-Notebook" data-jp-theme-light="true" data-jp-theme-name="JupyterLab Light">
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h1 id="Introduction">Introduction<a class="anchor-link" href="#Introduction">¶</a></h1><p>Recently, I started using the <a href="http://pandas.pydata.org/">pandas</a> python library to improve the quality
(and quantity) of statistics in my applications. One pandas method that I use frequently and is really powerful is <code>pivot_table</code>. This is a rather complex method that has
<a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.pivot_table.html">very poor documentation</a>.
Beyond this, this command is explained a little more in an article about
<a href="http://pandas.pydata.org/pandas-docs/stable/reshaping.html">data reshaping</a>, however, even this leaves
much to be desired (when I first tried reading it I was overwhelmed by the amount of information there).</p>
<p>A great introduction to pandas is the <a href="http://www.gregreda.com/2013/10/26/intro-to-pandas-data-structures/">three part series</a> by Greg Reda - it touches <code>pivot_table</code> however I was only able to understand it properly <em>after</em> I played a lot with it. I don’t know, maybe playing with <code>pivot_table</code> yourself (or being really experienced in such concepts) is the only way to properly comprehend it! To help with this journey however I’m going to try to explain various basic pandas concepts that will lead us to the <code>pivot_table</code> command (and some of its friends). Notice that I’m using pandas 0.18.1.</p>
<p>I have to mention that I am no expert in statistics or
numeric analysis so this post won’t have any advanced
information and may even point out some obvious things. However
keep in mind things that may seem obvious to some
experts are really difficult to grasp for a non-expert.</p>
<p>Before continuing, please notice that this article has been written as a <a href="http://jupyter.org/">jupyter notebook</a> and was integrated with pelican using the <a href="https://github.com/danielfrg/pelican-ipynb">pelican-ipynb plugin</a>. I had to do some modifications to my theme to better integrate the notebook styling, however some stuff may not look as nice as the other articles. I have to mention that this integration is really great and I totally recommend it!</p>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h1 id="The-DataFrame">The DataFrame<a class="anchor-link" href="#The-DataFrame">¶</a></h1><p>The most important data structure that pandas uses is the <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html">DataFrame</a>. This can be thought as a two dimensional array, something like an Excel spreadsheet. In the pandas nomenclature, the rows of that two-dimensional array are called <em>indexes</em> (while the columns are still called <em>columns</em>) — I’ll either use rows or indexes for the rows of the DataFrame. The rows are called indexes because they can be used to … index data (think of each column as a dictionary). However please notice that pandas has a different data structure named <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Index.html">Index</a> that is used to store the names of the headers (axis) of the rows and columns.</p>
<p>If we horizontally/vertically pick the values of a single row(index)/column we’ll be left with a different data structure called <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html">Series</a> - this is more or less a single dimensional array (or a dictionary with the names of the columns/indexes as keys). There’s also a <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Panel.html">Panel</a> data structure which is 3-dimensional, more like a complete Excel workbook (the third dimension being the individual sheets of the workbook) but I won’t cover that here.</p>
<p>More info on the above can be found on the corresponding <a href="http://pandas.pydata.org/pandas-docs/stable/dsintro.html">article about data structures</a>.</p>
<p>There are various ways to read the data for a Series or DataFrame: Initializing through <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.from_records.html">arrays or dicts</a>, <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.from_csv.html">reading from csv</a>, <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_excel.html">xls</a>, <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_sql_query.html">database</a>, combinining series to create an array and various others. I won’t go into any details about this but will include some examples on how to create Series and DataFrames. If you are familiar with python you can just convert everything to a dict and read that instead of researching individual methods.</p>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h2 id="Using-Series">Using Series<a class="anchor-link" href="#Using-Series">¶</a></h2><p>The <code>Series</code> data structure is more or less used to store a single dimensional array of data. This array-like structure could either have numbers as indexes (so will be more similar to a normal array) or have textual indexes (so will be more similar to a dictionary). Let’s see some examples:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [203]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
<span class="k">def</span> <span class="nf">t</span><span class="p">(</span><span class="n">o</span><span class="p">):</span>
<span class="c1"># Return the class name of the object</span>
<span class="k">return</span> <span class="n">o</span><span class="o">.</span><span class="vm">__class__</span><span class="o">.</span><span class="vm">__name__</span>
<span class="c1"># Use an array to create a Series</span>
<span class="n">series1</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">Series</span><span class="p">([</span><span class="mi">10</span><span class="p">,</span><span class="mi">20</span><span class="p">,</span><span class="mi">30</span><span class="p">])</span>
<span class="nb">print</span> <span class="s2">"series1 ("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">series1</span><span class="p">),</span> <span class="s1">')</span><span class="se">\n</span><span class="s1">'</span><span class="p">,</span> <span class="n">series1</span>
<span class="c1"># Notice that the index names were automatically generated as 0,1,2</span>
<span class="c1"># Use a dict to create a Series</span>
<span class="c1"># notice that the keys of the dict will be the index names</span>
<span class="n">series2</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">Series</span><span class="p">({</span><span class="s1">'row1'</span><span class="p">:</span><span class="mi">11</span><span class="p">,</span><span class="s1">'row2'</span><span class="p">:</span><span class="mi">22</span><span class="p">,</span><span class="s1">'row3'</span><span class="p">:</span><span class="mi">33</span><span class="p">})</span>
<span class="nb">print</span> <span class="s2">"series2 ("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">series2</span><span class="p">),</span> <span class="s1">')</span><span class="se">\n</span><span class="s1">'</span><span class="p">,</span> <span class="n">series2</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>series1 ( Series )
0 10
1 20
2 30
dtype: int64
series2 ( Series )
row1 11
row2 22
row3 33
dtype: int64
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The are various ways to select values from the Series. You can use textual or numeric indexes or you can filter the elements using an intuitive syntax:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [204]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Get values from series using named indexes</span>
<span class="n">series2</span><span class="p">[</span><span class="s1">'row1'</span><span class="p">]</span>
<span class="c1"># Can also use slicing and interesting operations </span>
<span class="c1"># like array in array [[]] to select specific indexes</span>
<span class="nb">print</span> <span class="n">series1</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
<span class="nb">print</span> <span class="n">series1</span><span class="p">[[</span><span class="mi">0</span><span class="p">,</span><span class="mi">2</span><span class="p">]]</span>
<span class="nb">print</span> <span class="n">series2</span><span class="p">[</span><span class="s1">'row2'</span><span class="p">:]</span>
<span class="nb">print</span> <span class="n">series2</span><span class="p">[[</span><span class="s1">'row1'</span><span class="p">,</span> <span class="s1">'row3'</span><span class="p">]]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>1 20
2 30
dtype: int64
0 10
2 30
dtype: int64
row2 22
row3 33
dtype: int64
row1 11
row3 33
dtype: int64
</pre>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [205]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Filtering series</span>
<span class="c1"># You can use comparison operators with a Series to </span>
<span class="c1"># get an array of booleans with the result of each element</span>
<span class="nb">print</span> <span class="s2">"Boolean result</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">series2</span><span class="o">></span><span class="mi">15</span>
<span class="c1"># This boolean array can then be used to filter the Series</span>
<span class="c1"># by returning only the elements that are "True"</span>
<span class="nb">print</span> <span class="s2">"Filtered result</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">series2</span><span class="p">[</span><span class="n">series2</span><span class="o">></span><span class="mi">15</span><span class="p">]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Boolean result
row1 False
row2 True
row3 True
dtype: bool
Filtered result
row2 22
row3 33
dtype: int64
</pre>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [206]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># The above means that we'll only get the second and third (index: 0,2)</span>
<span class="c1"># So we can create a function that returns Boolean, apply it to </span>
<span class="c1"># all elements of series with map and use the result for indexing!</span>
<span class="k">def</span> <span class="nf">is_22</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="k">return</span> <span class="n">x</span><span class="o">==</span><span class="mi">22</span>
<span class="nb">print</span> <span class="s2">"Map filtering</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">series2</span><span class="p">[</span><span class="n">series2</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">is_22</span><span class="p">)]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Map filtering
row2 22
dtype: int64
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The <code>map</code> method above gets a callback function and applies it to all elements of the Series, returning a new Series with the results. It is similar to the <code>map(function, sequence) -> list</code> global python funtion. Using map filtering is the most general way to filter elements of a series.</p>
<h2 id="Using-DataFrames">Using DataFrames<a class="anchor-link" href="#Using-DataFrames">¶</a></h2><p>Let’s start by a quick introduction to see some basic operations on DataFrames:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [207]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Create a DataFrame using a two-dimensional array</span>
<span class="c1"># Notice that the indexes and column names were automatically generated</span>
<span class="n">df1</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([[</span><span class="mi">10</span><span class="p">,</span><span class="mi">20</span><span class="p">,</span><span class="mi">30</span><span class="p">],</span> <span class="p">[</span><span class="mi">40</span><span class="p">,</span><span class="mi">50</span><span class="p">,</span><span class="mi">60</span><span class="p">]])</span>
<span class="nb">print</span> <span class="s2">"Dataframe from array: df1("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">df1</span><span class="p">),</span> <span class="s1">')'</span>
<span class="nb">print</span> <span class="n">df1</span>
<span class="c1"># Use a dict to give names to columns</span>
<span class="n">df2</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([{</span><span class="s1">'col1'</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span><span class="s1">'col2'</span><span class="p">:</span><span class="mi">20</span><span class="p">,</span><span class="s1">'col3'</span><span class="p">:</span><span class="mi">30</span><span class="p">},</span> <span class="p">{</span><span class="s1">'col1'</span><span class="p">:</span><span class="mi">40</span><span class="p">,</span><span class="s1">'col2'</span><span class="p">:</span><span class="mi">50</span><span class="p">,</span><span class="s1">'col3'</span><span class="p">:</span><span class="mi">60</span><span class="p">}])</span>
<span class="nb">print</span> <span class="s2">"Dataframe from dict: df2("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">df2</span><span class="p">),</span> <span class="s1">')'</span>
<span class="nb">print</span> <span class="n">df2</span>
<span class="c1"># Give names to indexes</span>
<span class="n">df3</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([</span>
<span class="p">{</span><span class="s1">'col1'</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span><span class="s1">'col2'</span><span class="p">:</span><span class="mi">20</span><span class="p">,</span><span class="s1">'col3'</span><span class="p">:</span><span class="mi">30</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'col1'</span><span class="p">:</span><span class="mi">40</span><span class="p">,</span><span class="s1">'col2'</span><span class="p">:</span><span class="mi">50</span><span class="p">,</span><span class="s1">'col3'</span><span class="p">:</span><span class="mi">60</span><span class="p">}</span>
<span class="p">],</span> <span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'idx1'</span><span class="p">,</span> <span class="s1">'idx2'</span><span class="p">])</span>
<span class="nb">print</span> <span class="s2">"Dataframe from dict, named indexes: df3("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">df3</span><span class="p">),</span> <span class="s1">')'</span>
<span class="nb">print</span> <span class="n">df3</span>
<span class="c1"># What happens when columns are missing</span>
<span class="n">df4</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([{</span><span class="s1">'col1'</span><span class="p">:</span><span class="mi">10</span><span class="p">,</span><span class="s1">'col2'</span><span class="p">:</span><span class="mi">20</span><span class="p">,</span><span class="s1">'col3'</span><span class="p">:</span><span class="mi">30</span><span class="p">},</span> <span class="p">{</span><span class="s1">'col2'</span><span class="p">:</span><span class="mi">40</span><span class="p">,</span><span class="s1">'col3'</span><span class="p">:</span><span class="mi">50</span><span class="p">,</span><span class="s1">'col4'</span><span class="p">:</span><span class="mi">60</span><span class="p">}])</span>
<span class="nb">print</span> <span class="s2">"Dataframe from dict, missing columns: df4("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">df4</span><span class="p">),</span> <span class="s1">')'</span>
<span class="nb">print</span> <span class="n">df4</span>
<span class="c1"># Create a DataFrame by combining series</span>
<span class="n">df5</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([</span><span class="n">pd</span><span class="o">.</span><span class="n">Series</span><span class="p">([</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">]),</span> <span class="n">pd</span><span class="o">.</span><span class="n">Series</span><span class="p">([</span><span class="mi">3</span><span class="p">,</span><span class="mi">4</span><span class="p">])],</span> <span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'a'</span><span class="p">,</span> <span class="s1">'b'</span><span class="p">],</span> <span class="p">)</span>
<span class="nb">print</span> <span class="s2">"Dataframe from series: df5("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">df5</span><span class="p">),</span> <span class="s1">')'</span>
<span class="nb">print</span> <span class="n">df5</span>
<span class="c1"># Output a dataframe as html</span>
<span class="nb">print</span> <span class="n">df5</span><span class="o">.</span><span class="n">to_html</span><span class="p">()</span>
<span class="c1"># Notice that there are many more interesting DataFrame output methods, like</span>
<span class="c1"># to_csv, to_dict, to_excel, to_json, to_latex, to_msgpack, to_string,</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Dataframe from array: df1( DataFrame )
0 1 2
0 10 20 30
1 40 50 60
Dataframe from dict: df2( DataFrame )
col1 col2 col3
0 10 20 30
1 40 50 60
Dataframe from dict, named indexes: df3( DataFrame )
col1 col2 col3
idx1 10 20 30
idx2 40 50 60
Dataframe from dict, missing columns: df4( DataFrame )
col1 col2 col3 col4
0 10.0 20 30 NaN
1 NaN 40 50 60.0
Dataframe from series: df5( DataFrame )
0 1
a 1 2
b 3 4
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>0</th>
<th>1</th>
</tr>
</thead>
<tbody>
<tr>
<th>a</th>
<td>1</td>
<td>2</td>
</tr>
<tr>
<th>b</th>
<td>3</td>
<td>4</td>
</tr>
</tbody>
</table>
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Reading a DataFrame from an array of python dicts is (at least for me) the easiest way to put my data in a DataFrame. Use any normal python method to generate that array of dicts and then just initialize the DataFrme with that. Also, the <code>to_html</code> method is really useful to quickly output a DataFrame to your web application - don’t forget to add some styling to the <code>.dataframe</code> class!</p>
<p>Selecting values from the Dataframe is very easy if you know how to do it. You index (<code>[]</code>) directly to select columns:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [208]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="nb">print</span> <span class="s2">"df3("</span><span class="p">,</span> <span class="n">t</span><span class="p">(</span><span class="n">df3</span><span class="p">),</span> <span class="s2">")</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span>
<span class="c1"># We can get a column as a Series</span>
<span class="nb">print</span> <span class="s2">"Get column as series</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="p">[</span><span class="s1">'col3'</span><span class="p">]</span>
<span class="c1"># Or multiple columns as a DataFrame</span>
<span class="nb">print</span> <span class="s2">"Get multiple columns</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="p">[[</span><span class="s1">'col3'</span><span class="p">,</span> <span class="s1">'col2'</span><span class="p">]]</span>
<span class="c1"># We can also get the column by its idx </span>
<span class="nb">print</span> <span class="s2">"Get column by index</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="p">[</span><span class="n">df3</span><span class="o">.</span><span class="n">columns</span><span class="p">[</span><span class="mi">1</span><span class="p">]]</span>
<span class="c1"># Pick values from a dataframe using array indexing</span>
<span class="c1"># df3['col2'] returns a Series so using the ['idx2'] </span>
<span class="c1"># index to it will return the actual value</span>
<span class="nb">print</span> <span class="s2">"Get value</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="p">[</span><span class="s1">'col2'</span><span class="p">][</span><span class="s1">'idx2'</span><span class="p">]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>df3( DataFrame )
col1 col2 col3
idx1 10 20 30
idx2 40 50 60
Get column as series
idx1 30
idx2 60
Name: col3, dtype: int64
Get multiple columns
col3 col2
idx1 30 20
idx2 60 50
Get column by index
idx1 20
idx2 50
Name: col2, dtype: int64
Get value
50
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Also you use the <code>loc/iloc</code> properties of the <code>DataFrame</code> to select rows/indexes (either by number or by text). The <code>loc/iloc</code> actually behave as a two dimensional array - they can get two parameters, the first one being the row/rows and the second one being the column/columns:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [209]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Pick an index (select a horizontal line) as a series</span>
<span class="nb">print</span> <span class="s2">"Get index as a series</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'idx1'</span><span class="p">]</span>
<span class="c1"># Also can pick by index number</span>
<span class="nb">print</span> <span class="s2">"Get index as a series by index</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="o">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="c1"># iloc can be used to numerically index both rows and columns by passing two indexes:</span>
<span class="nb">print</span> <span class="s2">"Two dimensional - get by index</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">df3</span><span class="o">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="p">:]</span> <span class="c1"># This is the same as the previous</span>
<span class="c1"># so to select the first column we'll use</span>
<span class="nb">print</span> <span class="s2">"Two dimensional - get by column</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="o">.</span><span class="n">iloc</span><span class="p">[:,</span> <span class="mi">0</span><span class="p">]</span>
<span class="c1"># We could do more interesting things, for example select a square</span>
<span class="nb">print</span> <span class="s2">"Two dimensional - get by index and column</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="o">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">:</span><span class="mi">3</span><span class="p">]</span>
<span class="c1"># Loc which is for label based indexing can also be used as a two dimensional index</span>
<span class="nb">print</span> <span class="s2">"Two dimensional - use label based indexing</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[[</span><span class="s1">'idx1'</span><span class="p">,</span><span class="s1">'idx2'</span><span class="p">],</span> <span class="s1">'col1'</span><span class="p">]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Get index as a series
col1 10
col2 20
col3 30
Name: idx1, dtype: int64
Get index as a series by index
col1 10
col2 20
col3 30
Name: idx1, dtype: int64
Two dimensional - get by index
col1 10
col2 20
col3 30
Name: idx1, dtype: int64
Two dimensional - get by column
idx1 10
idx2 40
Name: col1, dtype: int64
Two dimensional - get by index and column
col2 col3
idx1 20 30
idx2 50 60
Two dimensional - use label based indexing
idx1 10
idx2 40
Name: col1, dtype: int64
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Of course, boolean indexing and filtering can also be used just like in Series:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [210]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="nb">print</span> <span class="s2">"Boolean dataframe</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="n">df3</span><span class="o">></span><span class="mi">30</span>
<span class="nb">print</span> <span class="s2">"Boolean indexing</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">df3</span><span class="p">[</span><span class="n">df3</span><span class="o">></span><span class="mi">30</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">is_20_or_50</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="k">return</span> <span class="n">x</span><span class="o">==</span><span class="mi">20</span> <span class="ow">or</span> <span class="n">x</span><span class="o">==</span><span class="mi">50</span>
<span class="c1"># We need to use applymap (instead of map we used in Series)</span>
<span class="nb">print</span> <span class="s2">"Boolean indexing</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">df3</span><span class="p">[</span><span class="n">df3</span><span class="o">.</span><span class="n">applymap</span><span class="p">(</span><span class="n">is_20_or_50</span><span class="p">)]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Boolean dataframe
col1 col2 col3
idx1 False False False
idx2 True True True
Boolean indexing
col1 col2 col3
idx1 NaN NaN NaN
idx2 40.0 50.0 60.0
Boolean indexing
col1 col2 col3
idx1 NaN 20 NaN
idx2 NaN 50 NaN
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Notice that for the DataFrame we use the <code>applymap</code> method which applies the callback function to all individual elements of the DataFrame and returns the result as a new DataFrame (with the same dimensions of course). The boolean indexing is nice but it does not actually drop not needed things, we see that we just get a NaN in the positions that are filtered. Could we do something better? The answer is yes, but we’ll need to do index/column boolean indexing - i.e select only specific columns or indexes and then pass these to filter the dataframe:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [211]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Let's see the indexes that have *10* in their col1 column</span>
<span class="nb">print</span> <span class="n">df3</span><span class="p">[</span><span class="s1">'col1'</span><span class="p">]</span><span class="o">==</span><span class="mi">10</span>
<span class="c1"># And then select *only* these indexes (i.e idx1)</span>
<span class="nb">print</span> <span class="n">df3</span><span class="p">[</span><span class="n">df3</span><span class="p">[</span><span class="s1">'col1'</span><span class="p">]</span><span class="o">==</span><span class="mi">10</span><span class="p">]</span>
<span class="c1"># Now we can do exactly the opposite (see columns that have 10 in their idx1 index)</span>
<span class="nb">print</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'idx1'</span><span class="p">]</span><span class="o">==</span><span class="mi">10</span>
<span class="c1"># And then select *only* these columns (i.e col1)</span>
<span class="nb">print</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[:,</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'idx1'</span><span class="p">]</span><span class="o">==</span><span class="mi">10</span><span class="p">]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>idx1 True
idx2 False
Name: col1, dtype: bool
col1 col2 col3
idx1 10 20 30
col1 True
col2 False
col3 False
Name: idx1, dtype: bool
col1
idx1 10
idx2 40
</pre>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [212]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Let's finally see a general solution to boolean selecting with loc:</span>
<span class="c1"># Select specific columns </span>
<span class="nb">print</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[:,</span> <span class="p">[</span><span class="kc">True</span><span class="p">,</span> <span class="kc">False</span><span class="p">,</span> <span class="kc">True</span><span class="p">]</span> <span class="p">]</span>
<span class="c1"># Select specific rows</span>
<span class="nb">print</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[[</span><span class="kc">False</span><span class="p">,</span> <span class="kc">True</span><span class="p">],</span> <span class="p">:</span> <span class="p">]</span>
<span class="c1"># Select specific rows and cols</span>
<span class="nb">print</span> <span class="n">df3</span><span class="o">.</span><span class="n">loc</span><span class="p">[[</span><span class="kc">False</span><span class="p">,</span> <span class="kc">True</span><span class="p">],</span> <span class="p">[</span><span class="kc">True</span><span class="p">,</span> <span class="kc">False</span><span class="p">,</span><span class="kc">True</span><span class="p">]</span> <span class="p">,</span> <span class="p">]</span>
<span class="c1"># So we can pass two boolean arrays to loc, the first for selecting indexes and </span>
<span class="c1"># the second for selecting columns</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> col1 col3
idx1 10 30
idx2 40 60
col1 col2 col3
idx2 40 50 60
col1 col3
idx2 40 60
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h2 id="Modifying-DataFrames">Modifying DataFrames<a class="anchor-link" href="#Modifying-DataFrames">¶</a></h2><p>It’s easy to modify the DataFrame by changing its values, adding more indexes / columns, dropping rows and columns, renaming columns and indexes. Notice that some operations are performed in place (so they modify the original DataFrame), while others return a copy of the original array.</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [213]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Let's copy because some of the following operators change the dataframes</span>
<span class="n">df</span> <span class="o">=</span> <span class="n">df3</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
<span class="nb">print</span> <span class="n">df</span>
<span class="nb">print</span> <span class="s2">"Change values of a column"</span>
<span class="n">df</span><span class="p">[</span><span class="s1">'col1'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span><span class="mi">41</span><span class="p">]</span>
<span class="nb">print</span> <span class="n">df</span>
<span class="nb">print</span> <span class="s2">"Change values of an index"</span>
<span class="n">df</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'idx1'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span><span class="mi">21</span><span class="p">,</span> <span class="mi">31</span><span class="p">]</span>
<span class="nb">print</span> <span class="n">df</span>
<span class="nb">print</span> <span class="s2">"We can change more specific values (a 2x2 array here)"</span>
<span class="n">df</span><span class="o">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">:</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">[[</span><span class="mi">4</span><span class="p">,</span><span class="mi">3</span><span class="p">],</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span><span class="mi">1</span><span class="p">]]</span>
<span class="nb">print</span> <span class="n">df</span>
<span class="nb">print</span> <span class="s2">"Add another column to an existing dataframe (changes DataFrame)"</span>
<span class="n">df</span><span class="p">[</span><span class="s1">'col4'</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">]</span>
<span class="nb">print</span> <span class="n">df</span>
<span class="nb">print</span> <span class="s2">"Add another row (index) to an existing dataframe (changes DataFrame)"</span>
<span class="n">df</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'idx3'</span><span class="p">]</span><span class="o">=</span><span class="p">[</span><span class="mi">100</span><span class="p">,</span><span class="mi">200</span><span class="p">,</span><span class="mi">300</span><span class="p">,</span><span class="mi">400</span><span class="p">]</span>
<span class="nb">print</span> <span class="n">df</span>
<span class="nb">print</span> <span class="s2">"Drop a row (returns new object)"</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="s1">'idx1'</span><span class="p">)</span>
<span class="nb">print</span> <span class="s2">"Drop a column (returns new object)"</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="s1">'col1'</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="nb">print</span> <span class="s2">"Rename index (returns new object)"</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">rename</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">{</span><span class="s1">'idx1'</span><span class="p">:</span> <span class="s1">'new-idx-1'</span><span class="p">})</span>
<span class="nb">print</span> <span class="s2">"Rename column (returns new object)"</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">rename</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">{</span><span class="s1">'col1'</span><span class="p">:</span> <span class="s1">'new-col-1'</span><span class="p">})</span>
<span class="nb">print</span> <span class="s2">"Transpose array- change columns to rows and vice versa"</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">T</span>
<span class="nb">print</span> <span class="s2">"Double transpose - returns the initial DataFrame"</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">T</span><span class="o">.</span><span class="n">T</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> col1 col2 col3
idx1 10 20 30
idx2 40 50 60
Change values of a column
col1 col2 col3
idx1 11 20 30
idx2 41 50 60
Change values of an index
col1 col2 col3
idx1 11 21 31
idx2 41 50 60
We can change more specific values (a 2x2 array here)
col1 col2 col3
idx1 4 3 31
idx2 2 1 60
Add another column to an existing dataframe (changes DataFrame)
col1 col2 col3 col4
idx1 4 3 31 1
idx2 2 1 60 2
Add another row (index) to an existing dataframe (changes DataFrame)
col1 col2 col3 col4
idx1 4 3 31 1
idx2 2 1 60 2
idx3 100 200 300 400
Drop a row (returns new object)
col1 col2 col3 col4
idx2 2 1 60 2
idx3 100 200 300 400
Drop a column (returns new object)
col2 col3 col4
idx1 3 31 1
idx2 1 60 2
idx3 200 300 400
Rename index (returns new object)
col1 col2 col3 col4
new-idx-1 4 3 31 1
idx2 2 1 60 2
idx3 100 200 300 400
Rename column (returns new object)
new-col-1 col2 col3 col4
idx1 4 3 31 1
idx2 2 1 60 2
idx3 100 200 300 400
Transpose array- change columns to rows and vice versa
idx1 idx2 idx3
col1 4 2 100
col2 3 1 200
col3 31 60 300
col4 1 2 400
Double transpose - returns the initial DataFrame
col1 col2 col3 col4
idx1 4 3 31 1
idx2 2 1 60 2
idx3 100 200 300 400
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h2 id="More-advanced-operations">More advanced operations<a class="anchor-link" href="#More-advanced-operations">¶</a></h2><p>Beyond the previous, more or less basic operations, pandas allows you to do some advanced operations like <span class="caps">SQL</span>-like joins of more than one dataset or, applying a function to each of the rows / columns or even individual cells of the DataFrame:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [214]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">authors_df</span><span class="o">=</span><span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">},</span> <span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">}],</span> <span class="p">)</span>
<span class="n">books_df</span><span class="o">=</span><span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([</span>
<span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s1">'author_id'</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'It'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="s1">'author_id'</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Stand'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="s1">'author_id'</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Airframe'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'id'</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="s1">'author_id'</span><span class="p">:</span><span class="mi">2</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Jurassic Park'</span><span class="p">}</span>
<span class="p">])</span>
<span class="nb">print</span> <span class="n">authors_df</span>
<span class="nb">print</span> <span class="n">books_df</span>
<span class="nb">print</span> <span class="n">books_df</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="n">authors_df</span><span class="p">,</span> <span class="n">left_on</span><span class="o">=</span><span class="s1">'author_id'</span><span class="p">,</span> <span class="n">right_on</span><span class="o">=</span><span class="s1">'id'</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> id name
0 1 Stephen King
1 2 Michael Crichton
author_id id name
0 1 1 It
1 1 2 The Stand
2 2 3 Airframe
3 2 4 Jurassic Park
author_id id_x name_x id_y name_y
0 1 1 It 1 Stephen King
1 1 2 The Stand 1 Stephen King
2 2 3 Airframe 2 Michael Crichton
3 2 4 Jurassic Park 2 Michael Crichton
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>As can be seen above, the <code>merge</code> method of DataFrame can be used to do an sql-like join with another DataFrame, using specific columns as join-keys for each of the two dataframes (<code>left_on</code> and <code>right_on</code>). There are a lot of options for doing various join types (left, right, inner, outer etc) and concatenating DataFrames with other ways - most are discussed in the <a href="http://pandas.pydata.org/pandas-docs/stable/merging.html">corresponding post</a>.</p>
<p>Let’s see another method of doing the above join that is more controlled, using the <code>apply</code> method of DataFrame that <em>applies</em> a function to each row/column of the DataFrame and returns the result as a series:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [215]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Let's do the join using a different method</span>
<span class="k">def</span> <span class="nf">f</span><span class="p">(</span><span class="n">r</span><span class="p">):</span>
<span class="n">author_df_partial</span> <span class="o">=</span> <span class="n">authors_df</span><span class="p">[</span><span class="n">authors_df</span><span class="p">[</span><span class="s1">'id'</span><span class="p">]</span><span class="o">==</span><span class="n">r</span><span class="p">[</span><span class="s1">'author_id'</span><span class="p">]]</span>
<span class="k">return</span> <span class="n">author_df_partial</span><span class="o">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s1">'name'</span><span class="p">]</span>
<span class="n">books_df</span><span class="p">[</span><span class="s1">'author name'</span><span class="p">]</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="nb">print</span> <span class="n">books_df</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> author_id id name author name
0 1 1 It Stephen King
1 1 2 The Stand Stephen King
2 2 3 Airframe Michael Crichton
3 2 4 Jurassic Park Michael Crichton
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>How does this work? We pass the <code>axis=1</code> parameter to <code>apply</code> so that the callback function will be called for each row of the DataFrame (by default <code>axis=0</code> which means it will be called for each column). So, <code>f</code> will be called getting each row as an input. From this <code>book_df</code> row, we get the <code>author_id</code> it contains and filter <code>authors_df</code> by it. Notice that <code>author_df_partial</code> is actually a DataFrame containing only one row, so we need to filter it by getting its only line, using <code>iloc[0]</code> which will return a Series and finally, we return the author name using the corresponding index <code>name</code>.</p>
<p>When calling the <code>apply</code> method, by defautl the <code>axis</code> parameter is 0 (i.e the function will be called for each column). When I first encountered this I found it very strange because I thought that most users would usually want to apply a function to each of the rows. However, there’s a reason for applying the function to all columns, here’s an example:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [216]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">values</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([</span>
<span class="p">{</span><span class="s1">'temperature'</span><span class="p">:</span> <span class="mi">31</span><span class="p">,</span> <span class="s1">'moisture'</span><span class="p">:</span> <span class="mi">68</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'temperature'</span><span class="p">:</span> <span class="mi">33</span><span class="p">,</span> <span class="s1">'moisture'</span><span class="p">:</span> <span class="mi">72</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'temperature'</span><span class="p">:</span> <span class="mf">31.5</span><span class="p">,</span> <span class="s1">'moisture'</span><span class="p">:</span> <span class="mi">58</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'temperature'</span><span class="p">:</span> <span class="mf">28.5</span><span class="p">,</span> <span class="s1">'moisture'</span><span class="p">:</span> <span class="mi">42</span><span class="p">},</span>
<span class="p">])</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
<span class="c1"># We can easily create statistics for our data using apply -- that's why</span>
<span class="c1"># axis=0 is the default parameter to apply (to operate vertically to each column)</span>
<span class="n">values</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'avg'</span><span class="p">]</span><span class="o">=</span><span class="n">values</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">average</span> <span class="p">)</span>
<span class="n">values</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'len'</span><span class="p">]</span><span class="o">=</span><span class="n">values</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="nb">len</span> <span class="p">)</span>
<span class="n">values</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'sum'</span><span class="p">]</span><span class="o">=</span><span class="n">values</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="nb">sum</span><span class="p">)</span>
<span class="nb">print</span> <span class="n">values</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> moisture temperature
0 68.0 31.0
1 72.0 33.0
2 58.0 31.5
3 42.0 28.5
avg 60.0 31.0
len 5.0 5.0
sum 305.0 160.0
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h1 id="Comprehending-pivot_table">Comprehending <code>pivot_table</code><a class="anchor-link" href="#Comprehending-pivot_table">¶</a></h1><p>After this (rather long) introduction to using and manipulating DataFrames, the time has come to see <code>pivot_table</code>. The <code>pivot_table</code> method is applied to a DataFrame and its purpose is to “reshape” and “aggregate” the values of a DataFrame . More on reshaping can be found <a href="http://pandas.pydata.org/pandas-docs/stable/reshaping.html">here</a> and it means changing the indexes/columns of the DataFrame to create a new DataFrame that fits our needs. Aggregate on the other hand means that for each of the cells of the new DataFrame we’ll create a summary of the data that should have appeared there.</p>
<p>We’ll start by explaining how to create a nice set of data for the <code>pivot_table</code> operations!</p>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The recommended type of input (at least by me) to the <code>pivot_table</code> is a simple DataFrame like the one I have already created: Your index will be the <code>id</code> of your database (or you could even have an auto-generated index like in the example) and the columns will be the values you want to aggregate and reshape. This is very easy to create either by reading a file (xls/csv) or by a simple <span class="caps">SQL</span> query (substituting all foreign keys with a representative value). In the above example, we actually have the following columns: <em>author, genre, name, pages, year, decade, size</em> - this is a pool of data that will be very useful to remember for later and it is important to also keep it in your mind for your data. So, use a unique id as the index and remember the names of your columns.</p>
<p>As we can see in the documentation, the <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.pivot_table.html">pivot_table method</a> uses four basic parameters:</p>
<ul>
<li><code>index</code>: An array of the data that will be used as indexes to the resulting (i.e the reshaped and aggregated) DataFrame</li>
<li><code>columns</code>: An array of the data that will be used as a columns to the resulting DataFrame</li>
<li><code>values</code>: An array of the data whose values we want to aggregate in each cell</li>
<li><code>aggfunc</code>: Which is the function (or functions) that will be used for aggregating the values</li>
</ul>
<p>So, how it actually works? You select a number of the headers from your pool of data and assign them to either <code>index</code> or <code>columns</code>, depending if you want to put them horizontally or vertically. Notice that both <code>index</code> and <code>columns</code>:</p>
<ul>
<li>take either a string (to denote a single column) or an array to denote multiple columns</li>
<li>are optional (but you must define one of them) — if you skip either columns or index you’ll get a <code>Series</code> instead of a <code>DataFrame</code></li>
<li>are interchangable (you can put any header from your pool to either index or columns, depending on how you want to display your data)</li>
<li>are mutually exclusive (you can’t put the same header in both index and columns)</li>
</ul>
<p>Multiple data headers means that you’ll have <a href="http://pandas.pydata.org/pandas-docs/stable/advanced.html">hierachical indexes / columns</a> in your pivot (or <code>MultiIndex</code> as it’s called - remember that <code>Index</code> is used to store the axis of the DataFrame), ie the rows/columns would be grouped by a hierarchy. Let’s see an example of multiple indexes:</p>
<p>If we used <code>'decade'</code> as an index, then the pivot_table index would be like</p>
<ul>
<li><code>70s</code> value1 value2 …</li>
<li><code>80s</code> value1 value2 …</li>
<li><code>90s</code> value1 value2 …</li>
</ul>
<p>while, if we used <code>['decade', 'year']</code> we’d hove something like</p>
<ul>
<li><code>70s</code><ul>
<li><code>1975</code> value1 value2 …</li>
<li><code>1978</code> value1 value2 …</li>
</ul>
</li>
<li><code>80s</code><ul>
<li><code>1980</code> value1 value2 …</li>
<li><code>1982</code> value1 value2 …</li>
<li>…</li>
</ul>
</li>
<li><code>90s</code><ul>
<li><code>1990</code> value1 value2 …</li>
<li>…</li>
</ul>
</li>
</ul>
<p>So, each year would automatically be grouped to its corresponing decade. The same would be true if we used <code>['decade', 'year']</code> in columns (but we’ll now have a vertical grouping from top to bottom). Notice that pandas doesn’t know if the values have any parent / child relationship but just goes from left to right (or top to bottom). For example, if we had used <code>['year', 'decade']</code>, we’d get something like:</p>
<ul>
<li><code>1975 70s'</code> value1 value2 …</li>
<li><code>1978 70s'</code> value1 value2 …</li>
<li><code>1980 80s'</code> value1 value2 …</li>
<li><code>1982 80s'</code> value1 value2 …</li>
</ul>
<p>Also, pandas doesn’t care if the values of the hierarchical index are actually related. We could for example had selected a multi index of <code>['decade', 'size', 'genre']</code> that would</p>
<ul>
<li>display the list of decades at the left (or at the top if we used it as a column)</li>
<li>for each decade will display the sizes of the book of that decade at the center (header column or row) and finally</li>
<li>at the right header (or bottom correspondingly) will display the available genres for each size.</li>
</ul>
<p>So, since we have 3 values for each decade, 3 values for each size and 4 values for each genre in our dataset, each decade will appear 1 time (at the left), each size will appear 3 times (one for each decade) in the middle and each genre will appear 3x3 = 9(one for each combination of decade and size) times in the right. The total number of lines that our MultiIndex will contain is 3x3x4 = 36 (one line for each combination of decade/size/genre).</p>
<p>I hope the above clarifies how index and columns are used to create the headers for rows and index of <code>pivot_table</code>. I will show some examples of various index and columns combinations but first, I’d like to talk about contents of the pivot table (since we’ve only talked about the headers of rows/columns until now).</p>
<p>The values that the pivot_table will contain are defined through the other two parameters, <code>values</code> and <code>aggfunc</code>: We select one or more columns of the initial DataFrame through the <code>values</code> parameter and these are aggregated in the corresponding cell of the resulting dataframe using the aggfunc fuction, so for each cell as defined by index and column, pandas will pick the values that correspond to that cell and pass them to a function that will return the result (by combining these values). As can be understood, the values must be different than index and columns (so all three sets of values, index and columns must not intersect). By default, the <code>values</code> and <code>aggfunc</code> parameters may be ommited - this will result in using average as the function and selecting all numerical columns (that are not in indexes or columns of course) in the values.</p>
<p>I know that this is difficult to understand so I’ll give a simple example right away. Let’s first create a nice set of data for our samples:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [218]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">=</span><span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">([</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'It'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">1138</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1986</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Horror'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Stand'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">823</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1978</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Horror'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'Salem</span><span class="se">\'</span><span class="s1">s Lot'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">439</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1975</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Horror'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'Misery'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">320</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1987</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Thriller'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'Pet Sematary'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">374</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1983</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Horror'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'Bag of bones'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">529</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1998</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Horror'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'Different Seasons'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">527</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1982</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Thriller'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'The Dark Tower: The Gunslinger'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">224</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1982</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'The Dark Tower II: The Drawing of the Three'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">400</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1987</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'The Dark Tower III: The Waste Lands'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">512</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1991</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Stephen King'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span> <span class="s1">'The Dark Tower IV: Wizard and Glass'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">787</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1998</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Airframe'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">352</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span> <span class="mi">1996</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Jurassic Park'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">448</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1990</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Congo'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">348</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1980</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Sphere'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">385</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1987</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Rising Sun'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">385</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1992</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Disclosure '</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">597</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1994</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'Michael Crichton'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Lost World '</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">430</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1995</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'A Time to Kill'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">515</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1989</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Firm'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">432</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1991</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Pelican Brief'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">387</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1992</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Chamber'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">496</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1994</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Rainmaker'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">434</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1995</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Runaway Jury'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">414</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1996</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'John Grisham'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Street Lawyer'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">347</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1998</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'George Pelecanos'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'Nick</span><span class="se">\'</span><span class="s1">s Trip '</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">276</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1993</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'George Pelecanos'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'A Firing Offense'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">216</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1992</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'George Pelecanos'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'The Big Blowdown'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">313</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1996</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Crime'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'George R.R Martin'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'A Clash of Kings'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">768</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1998</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">{</span><span class="s1">'author'</span><span class="p">:</span><span class="s1">'George R.R Martin'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">:</span><span class="s1">'A Game of Thrones'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="mi">694</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">:</span><span class="mi">1996</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span> <span class="s1">'Fantasy'</span><span class="p">,},</span>
<span class="p">])</span>
<span class="c1"># Add a decade column to the books DataFrame</span>
<span class="k">def</span> <span class="nf">add_decade</span><span class="p">(</span><span class="n">y</span><span class="p">):</span>
<span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="s1">'year'</span><span class="p">])[</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="s1">'0</span><span class="se">\'</span><span class="s1">s'</span>
<span class="n">books_df</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">]</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">add_decade</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># Add a size column to the books DataFrame</span>
<span class="k">def</span> <span class="nf">add_size</span><span class="p">(</span><span class="n">y</span><span class="p">):</span>
<span class="k">if</span> <span class="n">y</span><span class="p">[</span><span class="s1">'pages'</span><span class="p">]</span> <span class="o">></span> <span class="mi">600</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">'big'</span>
<span class="k">elif</span> <span class="n">y</span><span class="p">[</span><span class="s1">'pages'</span><span class="p">]</span> <span class="o"><</span> <span class="mi">300</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">'small'</span>
<span class="k">return</span> <span class="s1">'medium'</span>
<span class="n">books_df</span><span class="p">[</span><span class="s1">'size'</span><span class="p">]</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">add_size</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># Let's display it sorted here</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">sort_values</span><span class="p">([</span><span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">])</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[218]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>author</th>
<th>genre</th>
<th>name</th>
<th>pages</th>
<th>year</th>
<th>decade</th>
<th>size</th>
</tr>
</thead>
<tbody>
<tr>
<th>2</th>
<td>Stephen King</td>
<td>Horror</td>
<td>Salem’s Lot</td>
<td>439</td>
<td>1975</td>
<td>70’s</td>
<td>medium</td>
</tr>
<tr>
<th>1</th>
<td>Stephen King</td>
<td>Horror</td>
<td>The Stand</td>
<td>823</td>
<td>1978</td>
<td>70’s</td>
<td>big</td>
</tr>
<tr>
<th>18</th>
<td>John Grisham</td>
<td>Crime</td>
<td>A Time to Kill</td>
<td>515</td>
<td>1989</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>13</th>
<td>Michael Crichton</td>
<td>Fantasy</td>
<td>Congo</td>
<td>348</td>
<td>1980</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>7</th>
<td>Stephen King</td>
<td>Fantasy</td>
<td>The Dark Tower: The Gunslinger</td>
<td>224</td>
<td>1982</td>
<td>80’s</td>
<td>small</td>
</tr>
<tr>
<th>8</th>
<td>Stephen King</td>
<td>Fantasy</td>
<td>The Dark Tower <span class="caps">II</span>: The Drawing of the Three</td>
<td>400</td>
<td>1987</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>14</th>
<td>Michael Crichton</td>
<td>Fantasy</td>
<td>Sphere</td>
<td>385</td>
<td>1987</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>4</th>
<td>Stephen King</td>
<td>Horror</td>
<td>Pet Sematary</td>
<td>374</td>
<td>1983</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>0</th>
<td>Stephen King</td>
<td>Horror</td>
<td>It</td>
<td>1138</td>
<td>1986</td>
<td>80’s</td>
<td>big</td>
</tr>
<tr>
<th>6</th>
<td>Stephen King</td>
<td>Thriller</td>
<td>Different Seasons</td>
<td>527</td>
<td>1982</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>3</th>
<td>Stephen King</td>
<td>Thriller</td>
<td>Misery</td>
<td>320</td>
<td>1987</td>
<td>80’s</td>
<td>medium</td>
</tr>
<tr>
<th>19</th>
<td>John Grisham</td>
<td>Crime</td>
<td>The Firm</td>
<td>432</td>
<td>1991</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>15</th>
<td>Michael Crichton</td>
<td>Crime</td>
<td>Rising Sun</td>
<td>385</td>
<td>1992</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>20</th>
<td>John Grisham</td>
<td>Crime</td>
<td>The Pelican Brief</td>
<td>387</td>
<td>1992</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>26</th>
<td>George Pelecanos</td>
<td>Crime</td>
<td>A Firing Offense</td>
<td>216</td>
<td>1992</td>
<td>90’s</td>
<td>small</td>
</tr>
<tr>
<th>25</th>
<td>George Pelecanos</td>
<td>Crime</td>
<td>Nick’s Trip</td>
<td>276</td>
<td>1993</td>
<td>90’s</td>
<td>small</td>
</tr>
<tr>
<th>16</th>
<td>Michael Crichton</td>
<td>Crime</td>
<td>Disclosure</td>
<td>597</td>
<td>1994</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>21</th>
<td>John Grisham</td>
<td>Crime</td>
<td>The Chamber</td>
<td>496</td>
<td>1994</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>22</th>
<td>John Grisham</td>
<td>Crime</td>
<td>The Rainmaker</td>
<td>434</td>
<td>1995</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>11</th>
<td>Michael Crichton</td>
<td>Crime</td>
<td>Airframe</td>
<td>352</td>
<td>1996</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>23</th>
<td>John Grisham</td>
<td>Crime</td>
<td>The Runaway Jury</td>
<td>414</td>
<td>1996</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>27</th>
<td>George Pelecanos</td>
<td>Crime</td>
<td>The Big Blowdown</td>
<td>313</td>
<td>1996</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>24</th>
<td>John Grisham</td>
<td>Crime</td>
<td>The Street Lawyer</td>
<td>347</td>
<td>1998</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>12</th>
<td>Michael Crichton</td>
<td>Fantasy</td>
<td>Jurassic Park</td>
<td>448</td>
<td>1990</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>9</th>
<td>Stephen King</td>
<td>Fantasy</td>
<td>The Dark Tower <span class="caps">III</span>: The Waste Lands</td>
<td>512</td>
<td>1991</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>17</th>
<td>Michael Crichton</td>
<td>Fantasy</td>
<td>The Lost World</td>
<td>430</td>
<td>1995</td>
<td>90’s</td>
<td>medium</td>
</tr>
<tr>
<th>29</th>
<td>George R.R Martin</td>
<td>Fantasy</td>
<td>A Game of Thrones</td>
<td>694</td>
<td>1996</td>
<td>90’s</td>
<td>big</td>
</tr>
<tr>
<th>10</th>
<td>Stephen King</td>
<td>Fantasy</td>
<td>The Dark Tower <span class="caps">IV</span>: Wizard and Glass</td>
<td>787</td>
<td>1998</td>
<td>90’s</td>
<td>big</td>
</tr>
<tr>
<th>28</th>
<td>George R.R Martin</td>
<td>Fantasy</td>
<td>A Clash of Kings</td>
<td>768</td>
<td>1998</td>
<td>90’s</td>
<td>big</td>
</tr>
<tr>
<th>5</th>
<td>Stephen King</td>
<td>Horror</td>
<td>Bag of bones</td>
<td>529</td>
<td>1998</td>
<td>90’s</td>
<td>medium</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [219]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Here's the first example</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span> <span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[219]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="4" halign="left">pages</th>
<th colspan="4" halign="left">year</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>631.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>1976.5</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>515.000000</td>
<td>339.25</td>
<td>756.0</td>
<td>423.5</td>
<td>1989.000000</td>
<td>1984.000000</td>
<td>1984.5</td>
<td>1984.5</td>
</tr>
<tr>
<th>90’s</th>
<td>387.416667</td>
<td>606.50</td>
<td>529.0</td>
<td>NaN</td>
<td>1994.083333</td>
<td>1994.666667</td>
<td>1998.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>In the above, we aggregated dour books by their decade and genre.</p>
<p>As we can see we just passed <code>decade</code> as an index and <code>genre</code> as a column. We ommited <code>values</code> and <code>aggfunc</code> so the default values were used. What happened? Pandas created a new DataFrame that had the values of decade as its index and the values of genre as its columns. Now, for each of the values (remember that since we ommited values, pandas just gets all numerical data, i.e pages and year) it found the corresponding entries for each cell, got their average and put it in that cell. For example, since there are no Crime genre books in the 70’s we got a <code>NaN</code> to both the pages and year values. However, there are two Horror books, with 823 and 439 pages so their average is 631. Notice that for each value a separate top-level multi-column containing all indexes and columns was created - we can display only pages or year by indexing with <code>['pages']</code> or <code>['year']</code>. We can think of each of the values columns as a seperate pivot table, so in the above example we have a pivot table for pages and a pivot table for year.</p>
<p>The above year column will also use the default average aggregate, something that doesn’t actually makes sense. So we can use <code>values</code> to explicitly define which values to aggregate — here’s how we can display only the pages:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [220]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">)</span>
<span class="c1">#The above is more or less the same as with books_df.pivot_table(index=['decade', ], columns=['genre'], )['pages']</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[220]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>631.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>515.000000</td>
<td>339.25</td>
<td>756.0</td>
<td>423.5</td>
</tr>
<tr>
<th>90’s</th>
<td>387.416667</td>
<td>606.50</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [221]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># In the above, we could pass ['pages'] instead of 'pages' as the values.</span>
<span class="c1"># This will result in creating a multi-column index with 'pages' as the top level column</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'pages'</span><span class="p">])</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[221]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="4" halign="left">pages</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>631.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>515.000000</td>
<td>339.25</td>
<td>756.0</td>
<td>423.5</td>
</tr>
<tr>
<th>90’s</th>
<td>387.416667</td>
<td>606.50</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [222]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Also, please notice that you can skip index or columns (but not both) to get a series</span>
<span class="nb">print</span> <span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">)</span>
<span class="nb">print</span> <span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>decade
70's 631.000000
80's 470.111111
90's 464.052632
Name: pages, dtype: float64
decade
70's 631.000000
80's 470.111111
90's 464.052632
Name: pages, dtype: float64
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Notice that above we have exactly the same result since for both cases we got a <code>Series</code> (it doesn’t matter that we used index in the first and columns in the second). Also, since we use <em>less</em> columns from our data pool (we used only decade while previously we used both decade and genre), the aggregation is more coarse: We got the averages of book pages in each decade. Of course, we could have the same values as before but use a multi-column index:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [223]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">s1</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">)</span>
<span class="n">s2</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">)</span>
<span class="nb">print</span> <span class="s2">"s1 equals s2: "</span><span class="p">,</span> <span class="n">s1</span><span class="o">.</span><span class="n">equals</span><span class="p">(</span><span class="n">s2</span><span class="p">)</span>
<span class="nb">print</span> <span class="n">s1</span><span class="o">.</span><span class="n">index</span>
<span class="n">s1</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>s1 equals s2: True
MultiIndex(levels=[[u'70's', u'80's', u'90's'], [u'Crime', u'Fantasy', u'Horror', u'Thriller']],
labels=[[0, 1, 1, 1, 1, 2, 2, 2], [2, 0, 1, 2, 3, 0, 1, 2]],
names=[u'decade', u'genre'])
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[223]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre>decade genre
70's Horror 631.000000
80's Crime 515.000000
Fantasy 339.250000
Horror 756.000000
Thriller 423.500000
90's Crime 387.416667
Fantasy 606.500000
Horror 529.000000
Name: pages, dtype: float64</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The above return a Series with a multi column index (they are both the same). Notice that the data is exactly the same as when we passed decade and genre in in index and column. The only difference is that some NaN rows have been dropped from the Series while in the DataFrame are there, for example Crime/70’s (the DataFrame will by default drop a row or index if all its values are NaN). Finally, take a look at how the multi index is represented (each easy to decypher it).</p>
<p>Let’s now say that we actually wanted to have a meaningful value for the year, for example the first year we have a book for that genre/decade:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [224]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># I'll intentionally skip values again to see what happens</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span> <span class="n">aggfunc</span><span class="o">=</span><span class="nb">min</span> <span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[224]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="4" halign="left">author</th>
<th colspan="4" halign="left">name</th>
<th colspan="4" halign="left">pages</th>
<th colspan="4" halign="left">year</th>
<th colspan="4" halign="left">size</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>None</td>
<td>None</td>
<td>Stephen King</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>Salem’s Lot</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>439</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>1975</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>big</td>
<td>None</td>
</tr>
<tr>
<th>80’s</th>
<td>John Grisham</td>
<td>Michael Crichton</td>
<td>Stephen King</td>
<td>Stephen King</td>
<td>A Time to Kill</td>
<td>Congo</td>
<td>It</td>
<td>Different Seasons</td>
<td>515</td>
<td>224</td>
<td>374</td>
<td>320</td>
<td>1989</td>
<td>1980</td>
<td>1983</td>
<td>1982</td>
<td>medium</td>
<td>medium</td>
<td>big</td>
<td>medium</td>
</tr>
<tr>
<th>90’s</th>
<td>George Pelecanos</td>
<td>George R.R Martin</td>
<td>Stephen King</td>
<td>None</td>
<td>A Firing Offense</td>
<td>A Clash of Kings</td>
<td>Bag of bones</td>
<td>None</td>
<td>216</td>
<td>430</td>
<td>529</td>
<td>None</td>
<td>1991</td>
<td>1990</td>
<td>1998</td>
<td>None</td>
<td>medium</td>
<td>big</td>
<td>medium</td>
<td>None</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>This is more interesting. It seems that since we didn’t use the default aggfunc value but instead we passed our own (<code>min</code>), pandas did not use only the numerical values but used instead <em>all remaining columns</em> as values: Remember that our pool of data was <em>author, genre, name, pages, year, decade, size</em>, the genre and decade were used as an index/column so the remaining headers were used as values: author, name, pages, year, size! For the pages and year we can understand what happens: For example, for the Horror novels of the 80’s, the one with the minimal pages is Pet Sematery with 374 pages. The same has also the minimal year (1983). However, the one with the minimal name is <code>It</code> (since <code>I</code> is before <code>P</code> it just compares strings). The author is the same for both(<code>Stephen King</code>) and the minimum size is medium (since <code>small (s) > medium (m)</code>). Of course we could pass the <code>values</code> parameter to actually define which values we wanted to see.</p>
<p>Another really interesting thing is to take a peek at which are the values that are passed to the aggregation function. For this, we can just use <code>tuple</code>:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [225]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Please notice that for reasons unknown to me, if I used aggfunc=tuple it would throw an exception</span>
<span class="n">books_df_tuples</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span> <span class="n">aggfunc</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
<span class="n">books_df_tuples</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[225]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="4" halign="left">author</th>
<th colspan="4" halign="left">name</th>
<th colspan="4" halign="left">pages</th>
<th colspan="4" halign="left">year</th>
<th colspan="4" halign="left">size</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>None</td>
<td>None</td>
<td>(Stephen King, Stephen King)</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>(The Stand, Salem’s Lot)</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>(823, 439)</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>(1978, 1975)</td>
<td>None</td>
<td>None</td>
<td>None</td>
<td>(big, medium)</td>
<td>None</td>
</tr>
<tr>
<th>80’s</th>
<td>(John Grisham,)</td>
<td>(Stephen King, Stephen King, Michael Crichton,…</td>
<td>(Stephen King, Stephen King)</td>
<td>(Stephen King, Stephen King)</td>
<td>(A Time to Kill,)</td>
<td>(The Dark Tower: The Gunslinger, The Dark Towe…</td>
<td>(It, Pet Sematary)</td>
<td>(Misery, Different Seasons)</td>
<td>(515,)</td>
<td>(224, 400, 348, 385)</td>
<td>(1138, 374)</td>
<td>(320, 527)</td>
<td>(1989,)</td>
<td>(1982, 1987, 1980, 1987)</td>
<td>(1986, 1983)</td>
<td>(1987, 1982)</td>
<td>(medium,)</td>
<td>(small, medium, medium, medium)</td>
<td>(big, medium)</td>
<td>(medium, medium)</td>
</tr>
<tr>
<th>90’s</th>
<td>(Michael Crichton, Michael Crichton, Michael C…</td>
<td>(Stephen King, Stephen King, Michael Crichton,…</td>
<td>(Stephen King,)</td>
<td>None</td>
<td>(Airframe, Rising Sun, Disclosure , The Firm, …</td>
<td>(The Dark Tower <span class="caps">III</span>: The Waste Lands, The Dark…</td>
<td>(Bag of bones,)</td>
<td>None</td>
<td>(352, 385, 597, 432, 387, 496, 434, 414, 347, …</td>
<td>(512, 787, 448, 430, 768, 694)</td>
<td>(529,)</td>
<td>None</td>
<td>(1996, 1992, 1994, 1991, 1992, 1994, 1995, 199…</td>
<td>(1991, 1998, 1990, 1995, 1998, 1996)</td>
<td>(1998,)</td>
<td>None</td>
<td>(medium, medium, medium, medium, medium, mediu…</td>
<td>(medium, big, medium, medium, big, big)</td>
<td>(medium,)</td>
<td>None</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [226]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Dont worry about the ellipsis, the values are all there in each cell, for example</span>
<span class="n">books_df_tuples</span><span class="p">[</span><span class="s1">'author'</span><span class="p">][</span><span class="s1">'Crime'</span><span class="p">][</span><span class="s1">'90</span><span class="se">\'</span><span class="s1">s'</span><span class="p">]</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[226]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre>('Michael Crichton',
'Michael Crichton',
'Michael Crichton',
'John Grisham',
'John Grisham',
'John Grisham',
'John Grisham',
'John Grisham',
'John Grisham',
'George Pelecanos',
'George Pelecanos',
'George Pelecanos')</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Notice that for the columns we havea MultiIndex of both value_type (author, name etc) and genre (Crime, Fantasy etc) while for the index we have the decade. So by <code>books_df_tuples['author']</code> we’ll get the author values DataFrame, by <code>books_df_tuples['author']['Crime']</code> we’ll get the Crime column of that DataFrame as a series and finally with <code>books_df_tuples['author']['Crime']['90\'s']</code> we’ll get the actuall value which is all author names that have written Crime books in the 90’s — authors that have written multiple books will be displayed multiple times.</p>
<p>What if we wanted to only display the different authors for each genre and decade and remove duplicates:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [227]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'author'</span><span class="p">,</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="s1">', '</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[227]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>None</td>
<td>None</td>
<td>Stephen King</td>
<td>None</td>
</tr>
<tr>
<th>80’s</th>
<td>John Grisham</td>
<td>Stephen King, Michael Crichton</td>
<td>Stephen King</td>
<td>Stephen King</td>
</tr>
<tr>
<th>90’s</th>
<td>John Grisham, Michael Crichton, George Pelecanos</td>
<td>Stephen King, George R.R Martin, Michael Crichton</td>
<td>Stephen King</td>
<td>None</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>What happens above is that we use the <code>lambda x: ', '.join(set(x))</code> function to aggregate. This function will create a set (i.e remove duplicates) from the input (which is the corresponding values for each cell) and then join the set members using <code>','</code>.</p>
<p>Notice that the inpout parameter that is passed to our aggfunc is actually a <code>Series</code> so don’t be alarmed if some list operations are not working:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [228]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'author'</span><span class="p">,</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">type</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[228]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>None</td>
<td>None</td>
<td><class ‘pandas.core.series.Series’></td>
<td>None</td>
</tr>
<tr>
<th>80’s</th>
<td><class ‘pandas.core.series.Series’></td>
<td><class ‘pandas.core.series.Series’></td>
<td><class ‘pandas.core.series.Series’></td>
<td><class ‘pandas.core.series.Series’></td>
</tr>
<tr>
<th>90’s</th>
<td><class ‘pandas.core.series.Series’></td>
<td><class ‘pandas.core.series.Series’></td>
<td><class ‘pandas.core.series.Series’></td>
<td>None</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Before continuing, I’d like to present another two parameters that could be passed to the pivot_table: <code>fill_value</code> to define a value to display when no values are found to be aggregated for a cell and <code>margins</code> to enable or disable margin rows/columns to the left/bottom that will aggregate all values of that column, for example:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [229]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span> <span class="o">=</span><span class="s1">'author'</span><span class="p">,</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="s1">', '</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">x</span><span class="p">)),</span>
<span class="n">margins</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">fill_value</span><span class="o">=</span><span class="s1">'-'</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[229]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>All</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>-</td>
<td>-</td>
<td>Stephen King</td>
<td>-</td>
<td>Stephen King</td>
</tr>
<tr>
<th>80’s</th>
<td>John Grisham</td>
<td>Stephen King, Michael Crichton</td>
<td>Stephen King</td>
<td>Stephen King</td>
<td>Stephen King, John Grisham, Michael Crichton</td>
</tr>
<tr>
<th>90’s</th>
<td>John Grisham, Michael Crichton, George Pelecanos</td>
<td>Stephen King, George R.R Martin, Michael Crichton</td>
<td>Stephen King</td>
<td>-</td>
<td>Stephen King, George R.R Martin, John Grisham,…</td>
</tr>
<tr>
<th>All</th>
<td>John Grisham, Michael Crichton, George Pelecanos</td>
<td>Stephen King, George R.R Martin, Michael Crichton</td>
<td>Stephen King</td>
<td>Stephen King</td>
<td>Stephen King, George R.R Martin, John Grisham,…</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The “All” column above will aggregate all values for each row/column (and the All/All down right will aggregate all values).</p>
<p>Using our previous knowledge of multi column indexes, let’s display the average number of pages each author writes for each decade and genre:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [230]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'author'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[230]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th>author</th>
<th>George Pelecanos</th>
<th>George R.R Martin</th>
<th>John Grisham</th>
<th colspan="2" halign="left">Michael Crichton</th>
<th colspan="3" halign="left">Stephen King</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Crime</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>631.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>NaN</td>
<td>515.000000</td>
<td>NaN</td>
<td>366.5</td>
<td>312.0</td>
<td>756.0</td>
<td>423.5</td>
</tr>
<tr>
<th>90’s</th>
<td>268.333333</td>
<td>731.0</td>
<td>418.333333</td>
<td>444.666667</td>
<td>439.0</td>
<td>649.5</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [231]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># One interesting thing is that if we changed the order of the multi-columns we'd get the same data</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'author'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[231]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th>genre</th>
<th colspan="3" halign="left">Crime</th>
<th colspan="3" halign="left">Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>author</th>
<th>George Pelecanos</th>
<th>John Grisham</th>
<th>Michael Crichton</th>
<th>George R.R Martin</th>
<th>Michael Crichton</th>
<th>Stephen King</th>
<th>Stephen King</th>
<th>Stephen King</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>631.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>515.000000</td>
<td>NaN</td>
<td>NaN</td>
<td>366.5</td>
<td>312.0</td>
<td>756.0</td>
<td>423.5</td>
</tr>
<tr>
<th>90’s</th>
<td>268.333333</td>
<td>418.333333</td>
<td>444.666667</td>
<td>731.0</td>
<td>439.0</td>
<td>649.5</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [232]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Or we can interchange index with columns to get the same data in a horizontal format</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'author'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[232]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>decade</th>
<th>70’s</th>
<th>80’s</th>
<th>90’s</th>
</tr>
<tr>
<th>author</th>
<th>genre</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>George Pelecanos</th>
<th>Crime</th>
<td>NaN</td>
<td>NaN</td>
<td>268.333333</td>
</tr>
<tr>
<th>George R.R Martin</th>
<th>Fantasy</th>
<td>NaN</td>
<td>NaN</td>
<td>731.000000</td>
</tr>
<tr>
<th>John Grisham</th>
<th>Crime</th>
<td>NaN</td>
<td>515.0</td>
<td>418.333333</td>
</tr>
<tr>
<th rowspan="2" valign="top">Michael Crichton</th>
<th>Crime</th>
<td>NaN</td>
<td>NaN</td>
<td>444.666667</td>
</tr>
<tr>
<th>Fantasy</th>
<td>NaN</td>
<td>366.5</td>
<td>439.000000</td>
</tr>
<tr>
<th rowspan="3" valign="top">Stephen King</th>
<th>Fantasy</th>
<td>NaN</td>
<td>312.0</td>
<td>649.500000</td>
</tr>
<tr>
<th>Horror</th>
<td>631.0</td>
<td>756.0</td>
<td>529.000000</td>
</tr>
<tr>
<th>Thriller</th>
<td>NaN</td>
<td>423.5</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>So, <code>Michael Crichton</code> was writing 445 pages for Crime novels and 439 pages for Fantasy novels on average at the 90’s (of course this would be true if we had included all works of Michael Crichton).
In the previous table we can see that, for example for <code>George Pelecanos</code> only the <code>Crime</code> genre is displayed (since he’s only Crime genre books in our database). Pandas automatically drops columns / lines where everything is empty (NaN)— if we for some reason wanted to display it, could use the <code>dropna=False</code> parameter:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [233]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'author'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'pages'</span><span class="p">],</span>
<span class="n">dropna</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[233]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="20" halign="left">pages</th>
</tr>
<tr>
<th>author</th>
<th colspan="4" halign="left">George Pelecanos</th>
<th colspan="4" halign="left">George R.R Martin</th>
<th colspan="4" halign="left">John Grisham</th>
<th colspan="4" halign="left">Michael Crichton</th>
<th colspan="4" halign="left">Stephen King</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>631.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>515.000000</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>366.5</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>312.0</td>
<td>756.0</td>
<td>423.5</td>
</tr>
<tr>
<th>90’s</th>
<td>268.333333</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>731.0</td>
<td>NaN</td>
<td>NaN</td>
<td>418.333333</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>444.666667</td>
<td>439.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>649.5</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [234]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># We can create any combination we want with our multi-index colums, for example let's see where each book belongs</span>
<span class="c1"># be decade / year / author and genre / size</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">,</span> <span class="s1">'author'</span><span class="p">,</span> <span class="s1">'name'</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'size'</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">,</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="s1">'v'</span><span class="p">,</span>
<span class="n">fill_value</span><span class="o">=</span><span class="s1">''</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[234]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th>size</th>
<th colspan="2" halign="left">big</th>
<th colspan="4" halign="left">medium</th>
<th colspan="2" halign="left">small</th>
</tr>
<tr>
<th></th>
<th></th>
<th></th>
<th>genre</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
</tr>
<tr>
<th>decade</th>
<th>year</th>
<th>author</th>
<th>name</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="2" valign="top">70’s</th>
<th>1975</th>
<th>Stephen King</th>
<th>Salem’s Lot</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>1978</th>
<th>Stephen King</th>
<th>The Stand</th>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="9" valign="top">80’s</th>
<th>1980</th>
<th>Michael Crichton</th>
<th>Congo</th>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="2" valign="top">1982</th>
<th rowspan="2" valign="top">Stephen King</th>
<th>Different Seasons</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
</tr>
<tr>
<th>The Dark Tower: The Gunslinger</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
</tr>
<tr>
<th>1983</th>
<th>Stephen King</th>
<th>Pet Sematary</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>1986</th>
<th>Stephen King</th>
<th>It</th>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="3" valign="top">1987</th>
<th>Michael Crichton</th>
<th>Sphere</th>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="2" valign="top">Stephen King</th>
<th>Misery</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
</tr>
<tr>
<th>The Dark Tower <span class="caps">II</span>: The Drawing of the Three</th>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>1989</th>
<th>John Grisham</th>
<th>A Time to Kill</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="19" valign="top">90’s</th>
<th>1990</th>
<th>Michael Crichton</th>
<th>Jurassic Park</th>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="2" valign="top">1991</th>
<th>John Grisham</th>
<th>The Firm</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>Stephen King</th>
<th>The Dark Tower <span class="caps">III</span>: The Waste Lands</th>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="3" valign="top">1992</th>
<th>George Pelecanos</th>
<th>A Firing Offense</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
</tr>
<tr>
<th>John Grisham</th>
<th>The Pelican Brief</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>Michael Crichton</th>
<th>Rising Sun</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>1993</th>
<th>George Pelecanos</th>
<th>Nick’s Trip</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
</tr>
<tr>
<th rowspan="2" valign="top">1994</th>
<th>John Grisham</th>
<th>The Chamber</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>Michael Crichton</th>
<th>Disclosure</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="2" valign="top">1995</th>
<th>John Grisham</th>
<th>The Rainmaker</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>Michael Crichton</th>
<th>The Lost World</th>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="4" valign="top">1996</th>
<th>George Pelecanos</th>
<th>The Big Blowdown</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>George R.R Martin</th>
<th>A Game of Thrones</th>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>John Grisham</th>
<th>The Runaway Jury</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>Michael Crichton</th>
<th>Airframe</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="4" valign="top">1998</th>
<th>George R.R Martin</th>
<th>A Clash of Kings</th>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>John Grisham</th>
<th>The Street Lawyer</th>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th rowspan="2" valign="top">Stephen King</th>
<th>Bag of bones</th>
<td></td>
<td></td>
<td></td>
<td></td>
<td>v</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<th>The Dark Tower <span class="caps">IV</span>: Wizard and Glass</th>
<td>v</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>One more advanced thing I’d like to cover here is that we could define multiple aggregate functions for each one of our values by passing a dictionary of <code>value:function</code> to the <code>aggfunc</code> parameter. For example, if we wanted to display</p>
<ul>
<li>the sum of the pages that have been written</li>
<li>the range of years for which we have books</li>
<li>the names of the authors</li>
<li>the name of one book we have</li>
</ul>
<p>for each genre each decade, we could do something like this</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [235]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="k">def</span> <span class="nf">get_range</span><span class="p">(</span><span class="n">years</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">'</span><span class="si">{0}</span><span class="s1"> - </span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">years</span><span class="p">),</span> <span class="nb">max</span><span class="p">(</span><span class="n">years</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_names</span><span class="p">(</span><span class="n">authors</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">', '</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">authors</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">get_book</span><span class="p">(</span><span class="n">books</span><span class="p">):</span>
<span class="c1"># Don't forget the the passed parameter is a Series so we use iloc to index it</span>
<span class="k">return</span> <span class="n">books</span><span class="o">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'author'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">,</span> <span class="s1">'name'</span><span class="p">],</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span>
<span class="s1">'author'</span><span class="p">:</span> <span class="n">get_names</span><span class="p">,</span>
<span class="s1">'pages'</span><span class="p">:</span> <span class="nb">sum</span><span class="p">,</span>
<span class="s1">'year'</span><span class="p">:</span> <span class="n">get_range</span><span class="p">,</span>
<span class="s1">'name'</span><span class="p">:</span> <span class="n">get_book</span><span class="p">,</span>
<span class="p">},</span>
<span class="n">fill_value</span><span class="o">=</span><span class="s1">'-'</span>
<span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[235]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="4" halign="left">year</th>
<th colspan="4" halign="left">pages</th>
<th colspan="4" halign="left">name</th>
<th colspan="4" halign="left">author</th>
</tr>
<tr>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>-</td>
<td>-</td>
<td>1975 - 1978</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>1262</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>The Stand</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>Stephen King</td>
<td>-</td>
</tr>
<tr>
<th>80’s</th>
<td>1989 - 1989</td>
<td>1980 - 1987</td>
<td>1983 - 1986</td>
<td>1982 - 1987</td>
<td>515</td>
<td>1357</td>
<td>1512</td>
<td>847</td>
<td>A Time to Kill</td>
<td>The Dark Tower: The Gunslinger</td>
<td>It</td>
<td>Misery</td>
<td>John Grisham</td>
<td>Stephen King, Michael Crichton</td>
<td>Stephen King</td>
<td>Stephen King</td>
</tr>
<tr>
<th>90’s</th>
<td>1991 - 1998</td>
<td>1990 - 1998</td>
<td>1998 - 1998</td>
<td>-</td>
<td>4649</td>
<td>3639</td>
<td>529</td>
<td>-</td>
<td>Airframe</td>
<td>The Dark Tower <span class="caps">III</span>: The Waste Lands</td>
<td>Bag of bones</td>
<td>-</td>
<td>John Grisham, Michael Crichton, George Pelecanos</td>
<td>Stephen King, George R.R Martin, Michael Crichton</td>
<td>Stephen King</td>
<td>-</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>As we’ve already mentioned, the above is more or less like four different pivot tables — for example we could get a pivot table with only pages if we passed <code>'pages'</code> as the <code>values</code> and <code>sum</code> as the <code>aggfunc</code> in the above method call.</p>
<h1 id="Friends-of-pivot_table">Friends of pivot_table<a class="anchor-link" href="#Friends-of-pivot_table">¶</a></h1>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The <code>pivot_table</code> method has some friends — these are functions that operate on DataFrame and can do reshaping but they are not as powerful as pivot_table. Let’s introduce some of them:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [236]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># First, I'll create a DataFrame as an example:</span>
<span class="n">df</span><span class="o">=</span><span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="p">],</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'author'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">,</span> <span class="n">aggfunc</span><span class="o">=</span><span class="nb">sum</span><span class="p">,</span> <span class="p">)</span>
<span class="c1"># This df has a Multi-index in columns - first level is the genres, second level is the authors</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">columns</span>
<span class="nb">print</span> <span class="n">df</span><span class="o">.</span><span class="n">index</span>
<span class="n">df</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>MultiIndex(levels=[[u'Crime', u'Fantasy', u'Horror', u'Thriller'], [u'George Pelecanos', u'George R.R Martin', u'John Grisham', u'Michael Crichton', u'Stephen King']],
labels=[[0, 0, 0, 1, 1, 1, 2, 3], [0, 2, 3, 1, 3, 4, 4, 4]],
names=[u'genre', u'author'])
Index([u'70's', u'80's', u'90's'], dtype='object', name=u'decade')
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[236]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th>genre</th>
<th colspan="3" halign="left">Crime</th>
<th colspan="3" halign="left">Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>author</th>
<th>George Pelecanos</th>
<th>John Grisham</th>
<th>Michael Crichton</th>
<th>George R.R Martin</th>
<th>Michael Crichton</th>
<th>Stephen King</th>
<th>Stephen King</th>
<th>Stephen King</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>1262.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>515.0</td>
<td>NaN</td>
<td>NaN</td>
<td>733.0</td>
<td>624.0</td>
<td>1512.0</td>
<td>847.0</td>
</tr>
<tr>
<th>90’s</th>
<td>805.0</td>
<td>2510.0</td>
<td>1334.0</td>
<td>1462.0</td>
<td>878.0</td>
<td>1299.0</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Notice above the <code>MultiIndex</code> and <code>Index</code> structs that are used to hold the axis for columns and index.</p>
<h2 id="Stack-/-unstack">Stack / unstack<a class="anchor-link" href="#Stack-/-unstack">¶</a></h2><p>These two operations move columns to indexes and vice-versa. Let’s see what the <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.stack.html">manual says</a>:</p>
<ul>
<li>stack: Pivot a level of the (possibly hierarchical) column labels, returning a DataFrame (or Series in the case of an object with a single level of column labels) having a hierarchical index with a new inner-most level of row labels.</li>
<li>unstack: Pivot a level of the (necessarily hierarchical) index labels, returning a DataFrame having a new level of column labels whose inner-most level consists of the pivoted index labels. If the index is not a MultiIndex, the output will be a Series (the analogue of stack when the columns are not a MultiIndex). The level involved will automatically get sorted.</li>
</ul>
<p>I must confess that I was not able to comprehend the above! A more easy explanation is that:</p>
<ul>
<li>stack will re-arrange the values of the DataFrame so that the most inner column (the one at the bottom) will be converted to the most inner index (to the right)</li>
<li>unstack will do the exactly opposite: Re-arrange the values of the DataFrame so that the most inner index (the one at the right) will be converted to the most inner column (to the bottom)</li>
</ul>
<p>Also, stack and unstack do not really make sense. It would be much easier (at least to me) if stack was named <code>col_to_idx</code> (or <code>col_to_row</code>) and unstack was named <code>idx_to_col</code> (<code>row_to_col</code>).</p>
<p>Before looking at examples of stack and unstack let’s take a look at the index and columns of our dataframe. Notice again the <code>Index</code> and <code>MultiIndex</code> data structs:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [237]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="nb">print</span> <span class="s2">"Index</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">df</span><span class="o">.</span><span class="n">index</span>
<span class="nb">print</span> <span class="s2">"Column</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">df</span><span class="o">.</span><span class="n">columns</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Index
Index([u'70's', u'80's', u'90's'], dtype='object', name=u'decade')
Column
MultiIndex(levels=[[u'Crime', u'Fantasy', u'Horror', u'Thriller'], [u'George Pelecanos', u'George R.R Martin', u'John Grisham', u'Michael Crichton', u'Stephen King']],
labels=[[0, 0, 0, 1, 1, 1, 2, 3], [0, 2, 3, 1, 3, 4, 4, 4]],
names=[u'genre', u'author'])
</pre>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [238]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">stacked</span> <span class="o">=</span> <span class="n">df</span><span class="o">.</span><span class="n">stack</span><span class="p">()</span>
<span class="nb">print</span> <span class="s2">"Index</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">stacked</span><span class="o">.</span><span class="n">index</span>
<span class="nb">print</span> <span class="s2">"Column</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span><span class="n">stacked</span><span class="o">.</span><span class="n">columns</span>
<span class="n">stacked</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>Index
MultiIndex(levels=[[u'70's', u'80's', u'90's'], [u'George Pelecanos', u'George R.R Martin', u'John Grisham', u'Michael Crichton', u'Stephen King']],
labels=[[0, 1, 1, 1, 2, 2, 2, 2, 2], [4, 2, 3, 4, 0, 1, 2, 3, 4]],
names=[u'decade', u'author'])
Column
Index([u'Crime', u'Fantasy', u'Horror', u'Thriller'], dtype='object', name=u'genre')
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[238]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>decade</th>
<th>author</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<th>Stephen King</th>
<td>NaN</td>
<td>NaN</td>
<td>1262.0</td>
<td>NaN</td>
</tr>
<tr>
<th rowspan="3" valign="top">80’s</th>
<th>John Grisham</th>
<td>515.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>NaN</td>
<td>733.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Stephen King</th>
<td>NaN</td>
<td>624.0</td>
<td>1512.0</td>
<td>847.0</td>
</tr>
<tr>
<th rowspan="5" valign="top">90’s</th>
<th>George Pelecanos</th>
<td>805.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>George R.R Martin</th>
<td>NaN</td>
<td>1462.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>John Grisham</th>
<td>2510.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>1334.0</td>
<td>878.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Stephen King</th>
<td>NaN</td>
<td>1299.0</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>We see that the <code>author</code> column (which was the most inner column) was moved to the right of the indexes. The rows (index) was converted to a multi-index while the columns is a simple index now.</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [239]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># We can of course stack again -- this time we'll get a series (with a three level index) since there are no more columns</span>
<span class="n">stacked2</span> <span class="o">=</span> <span class="n">stacked</span><span class="o">.</span><span class="n">stack</span><span class="p">()</span>
<span class="nb">print</span> <span class="n">stacked2</span><span class="o">.</span><span class="n">index</span>
<span class="n">stacked2</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>MultiIndex(levels=[[u'70's', u'80's', u'90's'], [u'George Pelecanos', u'George R.R Martin', u'John Grisham', u'Michael Crichton', u'Stephen King'], [u'Crime', u'Fantasy', u'Horror', u'Thriller']],
labels=[[0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2], [4, 2, 3, 4, 4, 4, 0, 1, 2, 3, 3, 4, 4], [2, 0, 1, 1, 2, 3, 0, 1, 0, 0, 1, 1, 2]],
names=[u'decade', u'author', u'genre'])
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[239]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre>decade author genre
70's Stephen King Horror 1262.0
80's John Grisham Crime 515.0
Michael Crichton Fantasy 733.0
Stephen King Fantasy 624.0
Horror 1512.0
Thriller 847.0
90's George Pelecanos Crime 805.0
George R.R Martin Fantasy 1462.0
John Grisham Crime 2510.0
Michael Crichton Crime 1334.0
Fantasy 878.0
Stephen King Fantasy 1299.0
Horror 529.0
dtype: float64</pre>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [240]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># unstack does the opposite operation</span>
<span class="n">unstacked</span> <span class="o">=</span> <span class="n">df</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span>
<span class="nb">print</span> <span class="n">unstacked</span><span class="o">.</span><span class="n">index</span>
<span class="n">unstacked</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>MultiIndex(levels=[[u'Crime', u'Fantasy', u'Horror', u'Thriller'], [u'George Pelecanos', u'George R.R Martin', u'John Grisham', u'Michael Crichton', u'Stephen King'], [u'70's', u'80's', u'90's']],
labels=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3], [0, 0, 0, 2, 2, 2, 3, 3, 3, 1, 1, 1, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4], [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2]],
names=[u'genre', u'author', u'decade'])
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[240]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre>genre author decade
Crime George Pelecanos 70's NaN
80's NaN
90's 805.0
John Grisham 70's NaN
80's 515.0
90's 2510.0
Michael Crichton 70's NaN
80's NaN
90's 1334.0
Fantasy George R.R Martin 70's NaN
80's NaN
90's 1462.0
Michael Crichton 70's NaN
80's 733.0
90's 878.0
Stephen King 70's NaN
80's 624.0
90's 1299.0
Horror Stephen King 70's 1262.0
80's 1512.0
90's 529.0
Thriller Stephen King 70's NaN
80's 847.0
90's NaN
dtype: float64</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>We now see that the that the <code>decade</code> column (which was the only index) was moved as the most inner to the columns — however this also converts this DataFrame to a Series!</p>
<p>One interesting thing to notice is that a Series can only be <code>unstack()</code>ed since it has no columns (so <code>stack</code> won’t work, remember stack = col_to_idx)</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [241]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># unstack - move the rightmost idx (decade) to columns</span>
<span class="n">unstacked</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[241]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>decade</th>
<th>70’s</th>
<th>80’s</th>
<th>90’s</th>
</tr>
<tr>
<th>genre</th>
<th>author</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="3" valign="top">Crime</th>
<th>George Pelecanos</th>
<td>NaN</td>
<td>NaN</td>
<td>805.0</td>
</tr>
<tr>
<th>John Grisham</th>
<td>NaN</td>
<td>515.0</td>
<td>2510.0</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>NaN</td>
<td>NaN</td>
<td>1334.0</td>
</tr>
<tr>
<th rowspan="3" valign="top">Fantasy</th>
<th>George R.R Martin</th>
<td>NaN</td>
<td>NaN</td>
<td>1462.0</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>NaN</td>
<td>733.0</td>
<td>878.0</td>
</tr>
<tr>
<th>Stephen King</th>
<td>NaN</td>
<td>624.0</td>
<td>1299.0</td>
</tr>
<tr>
<th>Horror</th>
<th>Stephen King</th>
<td>1262.0</td>
<td>1512.0</td>
<td>529.0</td>
</tr>
<tr>
<th>Thriller</th>
<th>Stephen King</th>
<td>NaN</td>
<td>847.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [243]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1">#Also, because `unstack` works on series we can use it for ever to cycle through different representations</span>
<span class="n">df</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span>
<span class="n">df</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span>
<span class="n">df</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span><span class="o">.</span><span class="n">unstack</span><span class="p">()</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[243]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th>decade</th>
<th colspan="5" halign="left">70’s</th>
<th colspan="5" halign="left">80’s</th>
<th colspan="5" halign="left">90’s</th>
</tr>
<tr>
<th>author</th>
<th>George Pelecanos</th>
<th>George R.R Martin</th>
<th>John Grisham</th>
<th>Michael Crichton</th>
<th>Stephen King</th>
<th>George Pelecanos</th>
<th>George R.R Martin</th>
<th>John Grisham</th>
<th>Michael Crichton</th>
<th>Stephen King</th>
<th>George Pelecanos</th>
<th>George R.R Martin</th>
<th>John Grisham</th>
<th>Michael Crichton</th>
<th>Stephen King</th>
</tr>
<tr>
<th>genre</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>Crime</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>515.0</td>
<td>NaN</td>
<td>NaN</td>
<td>805.0</td>
<td>NaN</td>
<td>2510.0</td>
<td>1334.0</td>
<td>NaN</td>
</tr>
<tr>
<th>Fantasy</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>733.0</td>
<td>624.0</td>
<td>NaN</td>
<td>1462.0</td>
<td>NaN</td>
<td>878.0</td>
<td>1299.0</td>
</tr>
<tr>
<th>Horror</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>1262.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>1512.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>529.0</td>
</tr>
<tr>
<th>Thriller</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>847.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [244]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># One final comment is that stack and unstack can get a level parameter to inticate which</span>
<span class="c1"># index/column level we want to pivot</span>
<span class="c1"># For example the following will unstack - idx_to_col the leftmost index (genre)</span>
<span class="n">unstacked</span><span class="o">.</span><span class="n">unstack</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[244]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>author</th>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="3" valign="top">George Pelecanos</th>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>90’s</th>
<td>805.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th rowspan="3" valign="top">George R.R Martin</th>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>90’s</th>
<td>NaN</td>
<td>1462.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th rowspan="3" valign="top">John Grisham</th>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>515.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>90’s</th>
<td>2510.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th rowspan="3" valign="top">Michael Crichton</th>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>733.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>90’s</th>
<td>1334.0</td>
<td>878.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th rowspan="3" valign="top">Stephen King</th>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>1262.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>624.0</td>
<td>1512.0</td>
<td>847.0</td>
</tr>
<tr>
<th>90’s</th>
<td>NaN</td>
<td>1299.0</td>
<td>529.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h2 id="pivot">pivot<a class="anchor-link" href="#pivot">¶</a></h2><p>The <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.pivot.html"><code>pivot</code></a> command will convert a column values to an index. This is similar like the <code>pivot_table</code> but does not aggregate the values and does not create multi-hierarchy indexes so you must be careful that each cell will contain only one value.</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [245]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># We'll use the initial books_df DataFrame</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s1">'name'</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s1">'genre'</span><span class="p">,</span> <span class="n">values</span><span class="o">=</span><span class="s1">'year'</span><span class="p">)</span>
<span class="c1"># Notice that we used 'name' as an index (to be sure that each cell will contain a single value)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[245]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>genre</th>
<th>Crime</th>
<th>Fantasy</th>
<th>Horror</th>
<th>Thriller</th>
</tr>
<tr>
<th>name</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>A Clash of Kings</th>
<td>NaN</td>
<td>1998.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>A Firing Offense</th>
<td>1992.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>A Game of Thrones</th>
<td>NaN</td>
<td>1996.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>A Time to Kill</th>
<td>1989.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Airframe</th>
<td>1996.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Bag of bones</th>
<td>NaN</td>
<td>NaN</td>
<td>1998.0</td>
<td>NaN</td>
</tr>
<tr>
<th>Congo</th>
<td>NaN</td>
<td>1980.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Different Seasons</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>1982.0</td>
</tr>
<tr>
<th>Disclosure</th>
<td>1994.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>It</th>
<td>NaN</td>
<td>NaN</td>
<td>1986.0</td>
<td>NaN</td>
</tr>
<tr>
<th>Jurassic Park</th>
<td>NaN</td>
<td>1990.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Misery</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>1987.0</td>
</tr>
<tr>
<th>Nick’s Trip</th>
<td>1993.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Pet Sematary</th>
<td>NaN</td>
<td>NaN</td>
<td>1983.0</td>
<td>NaN</td>
</tr>
<tr>
<th>Rising Sun</th>
<td>1992.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>Salem’s Lot</th>
<td>NaN</td>
<td>NaN</td>
<td>1975.0</td>
<td>NaN</td>
</tr>
<tr>
<th>Sphere</th>
<td>NaN</td>
<td>1987.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Big Blowdown</th>
<td>1996.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Chamber</th>
<td>1994.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Dark Tower <span class="caps">II</span>: The Drawing of the Three</th>
<td>NaN</td>
<td>1987.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Dark Tower <span class="caps">III</span>: The Waste Lands</th>
<td>NaN</td>
<td>1991.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Dark Tower <span class="caps">IV</span>: Wizard and Glass</th>
<td>NaN</td>
<td>1998.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Dark Tower: The Gunslinger</th>
<td>NaN</td>
<td>1982.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Firm</th>
<td>1991.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Lost World</th>
<td>NaN</td>
<td>1995.0</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Pelican Brief</th>
<td>1992.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Rainmaker</th>
<td>1995.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Runaway Jury</th>
<td>1996.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>The Stand</th>
<td>NaN</td>
<td>NaN</td>
<td>1978.0</td>
<td>NaN</td>
</tr>
<tr>
<th>The Street Lawyer</th>
<td>1998.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [246]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># We could pivot by using name as a column</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s1">'decade'</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s1">'name'</span><span class="p">,</span> <span class="n">values</span><span class="o">=</span><span class="s1">'pages'</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[246]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>name</th>
<th>A Clash of Kings</th>
<th>A Firing Offense</th>
<th>A Game of Thrones</th>
<th>A Time to Kill</th>
<th>Airframe</th>
<th>Bag of bones</th>
<th>Congo</th>
<th>Different Seasons</th>
<th>Disclosure</th>
<th>It</th>
<th>…</th>
<th>The Dark Tower <span class="caps">III</span>: The Waste Lands</th>
<th>The Dark Tower <span class="caps">IV</span>: Wizard and Glass</th>
<th>The Dark Tower: The Gunslinger</th>
<th>The Firm</th>
<th>The Lost World</th>
<th>The Pelican Brief</th>
<th>The Rainmaker</th>
<th>The Runaway Jury</th>
<th>The Stand</th>
<th>The Street Lawyer</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>…</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>823.0</td>
<td>NaN</td>
</tr>
<tr>
<th>80’s</th>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>515.0</td>
<td>NaN</td>
<td>NaN</td>
<td>348.0</td>
<td>527.0</td>
<td>NaN</td>
<td>1138.0</td>
<td>…</td>
<td>NaN</td>
<td>NaN</td>
<td>224.0</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
<td>NaN</td>
</tr>
<tr>
<th>90’s</th>
<td>768.0</td>
<td>216.0</td>
<td>694.0</td>
<td>NaN</td>
<td>352.0</td>
<td>529.0</td>
<td>NaN</td>
<td>NaN</td>
<td>597.0</td>
<td>NaN</td>
<td>…</td>
<td>512.0</td>
<td>787.0</td>
<td>NaN</td>
<td>432.0</td>
<td>430.0</td>
<td>387.0</td>
<td>434.0</td>
<td>414.0</td>
<td>NaN</td>
<td>347.0</td>
</tr>
</tbody>
</table>
<p>3 rows × 30 columns</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>What happens is that we got the values of one column and coverted these to a column/index and use another column’s values as the values of the new DataFrame. So, in the first example the values of the genre column were converted to columns and inside each cell we put the page number. In the second example instead we converted the decade value to index and put the page number inside each cell. In both cases we used the name of the book to be sure that we would each cell will contain one value (remember that pivot cannot aggregate).</p>
<p>The <code>pivot</code> command is not very useful (at least to me) since it does not actually modify (by aggregating) data but just changes its representation - you won’t get any new information from <code>pivot</code> but you’ll only display it differently. Also keep in mind that each cell after the pivoting must contain <em>one</em> value, for example in the above wouldn’t work if we used <code>author</code> as columns (instead of book name).</p>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h2 id="groupby">groupby<a class="anchor-link" href="#groupby">¶</a></h2><p>The final method we’ll talk about and is related to <code>pivot_table</code> is <a href="http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.groupby.html"><code>groupby</code></a>. This of course is related to the <span class="caps">SQL</span> group by method and should be easy to comprehend. The <code>groupby</code> gets a parameter that defines how to group the entries and returns a GroupBy object that contains the groups. The GroupBy object can be enumerated to get the groups and their data. It’s interesting to take a look at the structure of each such object:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [247]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">groupby_object</span> <span class="o">=</span> <span class="n">books_df</span><span class="o">.</span><span class="n">groupby</span><span class="p">([</span><span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'author'</span><span class="p">])</span>
<span class="nb">print</span> <span class="nb">type</span><span class="p">(</span><span class="n">groupby_object</span><span class="p">)</span>
<span class="c1"># Let's see what groupby-object contains</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">groupby_object</span><span class="p">:</span>
<span class="nb">print</span> <span class="s2">"type: "</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="s2">"len: "</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="c1">#len(x), type(x[0]), type(x[1]), x[0]</span>
<span class="nb">print</span> <span class="s2">"first element of tuple"</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="n">x</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="nb">print</span> <span class="s2">"second element of tuple"</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="n">x</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre><class 'pandas.core.groupby.DataFrameGroupBy'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("70's", 'Stephen King')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("80's", 'John Grisham')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("80's", 'Michael Crichton')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("80's", 'Stephen King')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("90's", 'George Pelecanos')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("90's", 'George R.R Martin')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("90's", 'John Grisham')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("90's", 'Michael Crichton')
second element of tuple <class 'pandas.core.frame.DataFrame'>
type: <type 'tuple'> len: 2
first element of tuple <type 'tuple'> ("90's", 'Stephen King')
second element of tuple <class 'pandas.core.frame.DataFrame'>
</pre>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>So, from the above we can see that the GroupBy object contains a number of 2-element tuples. Each tuple contains (another tuple with) the columns that were used for groupping and the actual data of that group (as a DataFrame). Now, we could either use the enumeration I shown above to operate on each group or, better, to use some of the methods that the GroupBy object contains:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [248]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># get some statistics</span>
<span class="nb">print</span> <span class="n">groupby_object</span><span class="o">.</span><span class="n">mean</span><span class="p">()</span>
<span class="nb">print</span> <span class="n">groupby_object</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span>
<span class="c1"># We can use the aggregate method to do anything we want</span>
<span class="c1"># Each aggregate function will get a Series with the values (similar to pivot_table)</span>
<span class="k">def</span> <span class="nf">year_aggr</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">'</span><span class="si">{0}</span><span class="s1">-</span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="nb">max</span><span class="p">(</span><span class="n">x</span><span class="p">),</span> <span class="nb">min</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">genre_aggr</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">', '</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
<span class="n">groupby_object</span><span class="o">.</span><span class="n">aggregate</span><span class="p">({</span><span class="s1">'year'</span><span class="p">:</span><span class="n">year_aggr</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">:</span> <span class="nb">sum</span><span class="p">,</span> <span class="s1">'genre'</span><span class="p">:</span><span class="n">genre_aggr</span><span class="p">})</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> pages year
decade author
70's Stephen King 631.000000 1976.500000
80's John Grisham 515.000000 1989.000000
Michael Crichton 366.500000 1983.500000
Stephen King 497.166667 1984.500000
90's George Pelecanos 268.333333 1993.666667
George R.R Martin 731.000000 1997.000000
John Grisham 418.333333 1994.333333
Michael Crichton 442.400000 1993.400000
Stephen King 609.333333 1995.666667
pages year
decade author
70's Stephen King 1262 3953
80's John Grisham 515 1989
Michael Crichton 733 3967
Stephen King 2983 11907
90's George Pelecanos 805 5981
George R.R Martin 1462 3994
John Grisham 2510 11966
Michael Crichton 2212 9967
Stephen King 1828 5987
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[248]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th></th>
<th>genre</th>
<th>pages</th>
<th>year</th>
</tr>
<tr>
<th>decade</th>
<th>author</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<th>Stephen King</th>
<td>Horror</td>
<td>1262</td>
<td>1978-1975</td>
</tr>
<tr>
<th rowspan="3" valign="top">80’s</th>
<th>John Grisham</th>
<td>Crime</td>
<td>515</td>
<td>1989-1989</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>Fantasy</td>
<td>733</td>
<td>1987-1980</td>
</tr>
<tr>
<th>Stephen King</th>
<td>Fantasy, Horror, Thriller</td>
<td>2983</td>
<td>1987-1982</td>
</tr>
<tr>
<th rowspan="5" valign="top">90’s</th>
<th>George Pelecanos</th>
<td>Crime</td>
<td>805</td>
<td>1996-1992</td>
</tr>
<tr>
<th>George R.R Martin</th>
<td>Fantasy</td>
<td>1462</td>
<td>1998-1996</td>
</tr>
<tr>
<th>John Grisham</th>
<td>Crime</td>
<td>2510</td>
<td>1998-1991</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>Fantasy, Crime</td>
<td>2212</td>
<td>1996-1990</td>
</tr>
<tr>
<th>Stephen King</th>
<td>Fantasy, Horror</td>
<td>1828</td>
<td>1998-1991</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [249]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># It's interesting to notice that the previous is exactly</span>
<span class="c1"># the same that we can do with this pivot_table command</span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'author'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">],</span> <span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span>
<span class="s1">'genre'</span><span class="p">:</span> <span class="n">genre_aggr</span><span class="p">,</span>
<span class="s1">'year'</span><span class="p">:</span> <span class="n">year_aggr</span><span class="p">,</span>
<span class="s1">'pages'</span><span class="p">:</span> <span class="nb">sum</span><span class="p">,</span>
<span class="p">})</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[249]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th></th>
<th>genre</th>
<th>pages</th>
<th>year</th>
</tr>
<tr>
<th>decade</th>
<th>author</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>70’s</th>
<th>Stephen King</th>
<td>Horror</td>
<td>1262</td>
<td>1978-1975</td>
</tr>
<tr>
<th rowspan="3" valign="top">80’s</th>
<th>John Grisham</th>
<td>Crime</td>
<td>515</td>
<td>1989-1989</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>Fantasy</td>
<td>733</td>
<td>1987-1980</td>
</tr>
<tr>
<th>Stephen King</th>
<td>Fantasy, Horror, Thriller</td>
<td>2983</td>
<td>1987-1982</td>
</tr>
<tr>
<th rowspan="5" valign="top">90’s</th>
<th>George Pelecanos</th>
<td>Crime</td>
<td>805</td>
<td>1996-1992</td>
</tr>
<tr>
<th>George R.R Martin</th>
<td>Fantasy</td>
<td>1462</td>
<td>1998-1996</td>
</tr>
<tr>
<th>John Grisham</th>
<td>Crime</td>
<td>2510</td>
<td>1998-1991</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>Fantasy, Crime</td>
<td>2212</td>
<td>1996-1990</td>
</tr>
<tr>
<th>Stephen King</th>
<td>Fantasy, Horror</td>
<td>1828</td>
<td>1998-1991</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [250]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># The added value of pivot_table over group of course is that we could instead do </span>
<span class="n">books_df</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">],</span> <span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'author'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'genre'</span><span class="p">,</span> <span class="s1">'pages'</span><span class="p">,</span> <span class="s1">'year'</span><span class="p">],</span> <span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span>
<span class="s1">'genre'</span><span class="p">:</span> <span class="n">genre_aggr</span><span class="p">,</span>
<span class="s1">'year'</span><span class="p">:</span> <span class="n">year_aggr</span><span class="p">,</span>
<span class="s1">'pages'</span><span class="p">:</span> <span class="nb">sum</span><span class="p">,</span>
<span class="p">})</span>
<span class="c1"># or any other combination of columns - index we wanted</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[250]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="3" halign="left">genre</th>
<th colspan="3" halign="left">pages</th>
<th colspan="3" halign="left">year</th>
</tr>
<tr>
<th>decade</th>
<th>70’s</th>
<th>80’s</th>
<th>90’s</th>
<th>70’s</th>
<th>80’s</th>
<th>90’s</th>
<th>70’s</th>
<th>80’s</th>
<th>90’s</th>
</tr>
<tr>
<th>author</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>George Pelecanos</th>
<td>None</td>
<td>None</td>
<td>Crime</td>
<td>None</td>
<td>None</td>
<td>805</td>
<td>None</td>
<td>None</td>
<td>1996-1992</td>
</tr>
<tr>
<th>George R.R Martin</th>
<td>None</td>
<td>None</td>
<td>Fantasy</td>
<td>None</td>
<td>None</td>
<td>1462</td>
<td>None</td>
<td>None</td>
<td>1998-1996</td>
</tr>
<tr>
<th>John Grisham</th>
<td>None</td>
<td>Crime</td>
<td>Crime</td>
<td>None</td>
<td>515</td>
<td>2510</td>
<td>None</td>
<td>1989-1989</td>
<td>1998-1991</td>
</tr>
<tr>
<th>Michael Crichton</th>
<td>None</td>
<td>Fantasy</td>
<td>Fantasy, Crime</td>
<td>None</td>
<td>733</td>
<td>2212</td>
<td>None</td>
<td>1987-1980</td>
<td>1996-1990</td>
</tr>
<tr>
<th>Stephen King</th>
<td>Horror</td>
<td>Fantasy, Horror, Thriller</td>
<td>Fantasy, Horror</td>
<td>1262</td>
<td>2983</td>
<td>1828</td>
<td>1978-1975</td>
<td>1987-1982</td>
<td>1998-1991</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h1 id="A-real-world-example">A real-world example<a class="anchor-link" href="#A-real-world-example">¶</a></h1><p>To continue with a real-world example, I will use the <a href="http://grouplens.org/datasets/movielens/">MovieLens 100k</a> to represent some <code>pivot_table</code> (and friends) operations. To load the data I’ve used the code already provided by the <a href="http://www.gregreda.com/2013/10/26/working-with-pandas-dataframes/">three part series I already mentioned</a>. Notice that this won’t load the genre of the movie (left as an excersize to the reader).</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [251]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Useful to display graphs inline</span>
<span class="o">%</span><span class="k">matplotlib</span> inline
</pre></div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [252]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
<span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="nn">plt</span>
<span class="n">path</span> <span class="o">=</span> <span class="s1">'C:/Users/serafeim/Downloads/ml-100k'</span> <span class="c1"># Change this to your own directory</span>
<span class="n">u_cols</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'user_id'</span><span class="p">,</span> <span class="s1">'age'</span><span class="p">,</span> <span class="s1">'sex'</span><span class="p">,</span> <span class="s1">'occupation'</span><span class="p">,</span> <span class="s1">'zip_code'</span><span class="p">]</span>
<span class="n">users</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s1">'u.user'</span><span class="p">),</span> <span class="n">sep</span><span class="o">=</span><span class="s1">'|'</span><span class="p">,</span> <span class="n">names</span><span class="o">=</span><span class="n">u_cols</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">'latin-1'</span><span class="p">)</span>
<span class="n">r_cols</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'user_id'</span><span class="p">,</span> <span class="s1">'movie_id'</span><span class="p">,</span> <span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'unix_timestamp'</span><span class="p">]</span>
<span class="n">ratings</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s1">'u.data'</span><span class="p">),</span> <span class="n">sep</span><span class="o">=</span><span class="s1">'</span><span class="se">\t</span><span class="s1">'</span><span class="p">,</span> <span class="n">names</span><span class="o">=</span><span class="n">r_cols</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">'latin-1'</span><span class="p">)</span>
<span class="n">m_cols</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'movie_id'</span><span class="p">,</span> <span class="s1">'title'</span><span class="p">,</span> <span class="s1">'release_date'</span><span class="p">,</span> <span class="s1">'video_release_date'</span><span class="p">,</span> <span class="s1">'imdb_url'</span><span class="p">]</span>
<span class="n">movies</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s1">'u.item'</span><span class="p">),</span> <span class="n">sep</span><span class="o">=</span><span class="s1">'|'</span><span class="p">,</span> <span class="n">names</span><span class="o">=</span><span class="n">m_cols</span><span class="p">,</span> <span class="n">usecols</span><span class="o">=</span><span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">),</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">'latin-1'</span><span class="p">)</span>
<span class="n">movie_ratings</span> <span class="o">=</span> <span class="n">movies</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="n">ratings</span><span class="p">)</span>
<span class="n">lens</span> <span class="o">=</span> <span class="n">movie_ratings</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="n">users</span><span class="p">)</span>
<span class="n">lens</span><span class="o">.</span><span class="n">head</span><span class="p">()</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[252]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>movie_id</th>
<th>title</th>
<th>release_date</th>
<th>video_release_date</th>
<th>imdb_url</th>
<th>user_id</th>
<th>rating</th>
<th>unix_timestamp</th>
<th>age</th>
<th>sex</th>
<th>occupation</th>
<th>zip_code</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>1</td>
<td>Toy Story (1995)</td>
<td>01-Jan-1995</td>
<td>NaN</td>
<td>http://us.imdb.com/M/title-exact?Toy%20Story%2…</td>
<td>308</td>
<td>4</td>
<td>887736532</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td>95076</td>
</tr>
<tr>
<th>1</th>
<td>4</td>
<td>Get Shorty (1995)</td>
<td>01-Jan-1995</td>
<td>NaN</td>
<td>http://us.imdb.com/M/title-exact?Get%20Shorty%…</td>
<td>308</td>
<td>5</td>
<td>887737890</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td>95076</td>
</tr>
<tr>
<th>2</th>
<td>5</td>
<td>Copycat (1995)</td>
<td>01-Jan-1995</td>
<td>NaN</td>
<td>http://us.imdb.com/M/title-exact?Copycat%20(1995)</td>
<td>308</td>
<td>4</td>
<td>887739608</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td>95076</td>
</tr>
<tr>
<th>3</th>
<td>7</td>
<td>Twelve Monkeys (1995)</td>
<td>01-Jan-1995</td>
<td>NaN</td>
<td>http://us.imdb.com/M/title-exact?Twelve%20Monk…</td>
<td>308</td>
<td>4</td>
<td>887738847</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td>95076</td>
</tr>
<tr>
<th>4</th>
<td>8</td>
<td>Babe (1995)</td>
<td>01-Jan-1995</td>
<td>NaN</td>
<td>http://us.imdb.com/M/title-exact?Babe%20(1995)</td>
<td>308</td>
<td>5</td>
<td>887736696</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td>95076</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>As we can see, we are using the <code>merge</code> method of DataFrame to do an <span class="caps">SQL</span>-style join between <code>movies</code> and <code>ratings</code> and then between <code>movie_ratings</code> and <code>users</code> - this will result in a fat DataFrame with all the info of the movie and user for each review. The <code>head</code> method displays the 5 first rows of the DataFrame.</p>
<p>We can see that there’s a <code>zip_code</code> - I wanted to convert it to the specific <span class="caps">US</span>-state. There’s a service from <code>ziptasticapi.com</code> that can be used for that but you need to do the queries one-by-one! I’ve executed the queries once and created a zip-state dict to be used instead:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [253]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># The following can be used to find out the state by zip_code for each row</span>
<span class="n">API</span><span class="o">=</span><span class="s2">"http://ziptasticapi.com/</span><span class="si">{0}</span><span class="s2">"</span>
<span class="n">states</span> <span class="o">=</span> <span class="p">{}</span>
<span class="kn">import</span> <span class="nn">urllib2</span><span class="o">,</span> <span class="nn">json</span>
<span class="k">def</span> <span class="nf">get_state</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
<span class="k">global</span> <span class="n">states</span>
<span class="k">if</span> <span class="n">states</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
<span class="k">return</span> <span class="n">states</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span> <span class="s1">'User-Agent'</span> <span class="p">:</span> <span class="s1">'Mozilla/5.0'</span> <span class="p">}</span>
<span class="n">req</span> <span class="o">=</span> <span class="n">urllib2</span><span class="o">.</span><span class="n">Request</span><span class="p">(</span><span class="n">API</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="kc">None</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span>
<span class="n">state</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">urllib2</span><span class="o">.</span><span class="n">urlopen</span><span class="p">(</span><span class="n">req</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">())</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'state'</span><span class="p">)</span>
<span class="n">states</span><span class="p">[</span><span class="n">s</span><span class="p">]</span> <span class="o">=</span> <span class="n">state</span>
<span class="k">return</span> <span class="n">state</span>
<span class="c1">#using this command we can add the state column</span>
<span class="c1">#lens['state']=lens['zip_code'].apply(get_state)</span>
</pre></div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell jp-mod-noOutputs ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [254]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># However, since we shouldn't call the zipstatic API so many times, I'll provide here the</span>
<span class="c1"># dict of zip:state (that I actually got through the previous command)</span>
<span class="n">states2</span><span class="o">=</span><span class="p">{</span><span class="sa">u</span><span class="s1">'73013'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77042'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61455'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55345'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19711'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DE'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'19716'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DE'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55343'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'15203'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48446'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92093'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'92653'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61073'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55346'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'32303'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'32301'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'32712'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06437'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01581'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85719'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12065'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'10960'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'32789'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01375'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60135'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98501'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'95521'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'49512'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02215'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80209'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97330'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'98006'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'52302'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60187'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'46005'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'46260'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'63021'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'17036'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'99206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10707'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'21208'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75204'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60007'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60005'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22902'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'21201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22906'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'45680'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94025'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'53144'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'05001'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97208'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'54494'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90008'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'45660'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53214'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53210'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53211'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37411'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'37412'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63119'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10025'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10022'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10021'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'44648'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60641'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78213'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78212'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22973'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'96819'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'HI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'42647'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'62901'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'62903'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90095'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'04102'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ME'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14627'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20006'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'70808'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'LA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20003'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'20001'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'70802'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'LA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'05452'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20009'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20008'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'08610'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33775'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30329'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'76013'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84408'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'11758'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95014'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'08052'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37777'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37771'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'76309'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'23509'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50311'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33884'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30803'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'42459'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95064'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02859'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'RI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'68504'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NE'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'40243'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'68503'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NE'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02918'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'RI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'34656'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'L1V3W'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22003'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55113'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55117'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55116'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'23112'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'91206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06927'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55337'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02136'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11577'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'47130'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02139'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02138'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'N2L5N'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'15217'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'15213'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50670'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'04988'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ME'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19382'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'87501'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NM'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55454'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19149'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19146'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55021'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'V1G4L'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'06405'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'73071'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77459'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92037'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60089'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'64118'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21114'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98101'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98103'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98102'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'02341'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94306'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94305'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85233'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11753'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'90814'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14534'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98072'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'16803'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43512'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'10309'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95468'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60402'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60152'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75218'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'98199'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12603'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90254'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84116'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'16801'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'41850'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97214'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97215'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97212'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10019'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'10018'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'49705'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10011'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10010'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10016'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'13210'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78209'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60659'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01754'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60657'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'70124'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'LA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12345'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95161'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20015'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94708'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'58202'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ND'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29379'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94703'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94702'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'68767'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NE'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'24060'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33763'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33765'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'54248'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80303'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'03062'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'03060'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'18301'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'08403'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94551'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'48043'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'28450'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78264'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63304'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06333'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'08105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'07102'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'18015'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11231'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27606'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'38115'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95076'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77845'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77841'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14476'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'08360'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02903'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'RI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01945'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'40256'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91919'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'89801'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NV'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48825'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48823'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'07204'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92154'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55106'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55107'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55104'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55108'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55109'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61755'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91351'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'Y1A6B'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91606'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'28734'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55320'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78205'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01824'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'47024'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43212'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43215'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02125'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'08816'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'15222'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'M7A1A'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97520'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'76234'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55420'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55423'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55422'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55038'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55428'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94560'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'T8H1N'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'16125'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02154'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'R3T5K'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'35802'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'97006'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02159'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'32250'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50613'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92020'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60804'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'21044'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98117'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'E2A4H'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90804'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'74101'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'22903'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22904'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'52245'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'52246'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'52241'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'17331'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20723'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63044'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'17110'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10314'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'32605'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60067'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90247'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61820'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84103'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'84105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84107'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60090'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'99835'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98281'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'05201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10003'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20090'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90064'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01040'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'21250'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20657'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97203'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60466'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'42141'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'44134'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78390'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44133'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'83686'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ID'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14085'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'45810'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75006'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63146'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91335'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'39762'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MS'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'80302'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44224'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37076'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33755'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'54901'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'03052'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30220'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94403'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91040'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29464'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'49931'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'49938'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'71457'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'LA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'03755'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78739'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'77048'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30040'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11101'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'83702'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ID'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'31211'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'83709'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ID'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'34105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'76201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91903'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01913'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'31404'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27705'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27708'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92121'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29631'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'28480'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94583'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'V0R2H'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91344'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'28806'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'95821'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95823'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43202'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11211'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11217'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'43204'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77504'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01960'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'82435'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19047'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'15235'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'15237'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'15232'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92629'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55436'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'50112'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55439'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92626'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22030'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98682'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'65203'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19341'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77005'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77009'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77008'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'02146'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02143'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'56567'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93055'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27249'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'06492'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93117'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20064'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'64131'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'17604'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94086'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'01915'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02320'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01810'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02324'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06260'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'32067'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78155'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43537'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94131'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90405'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'85210'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'17325'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53188'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98225'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53066'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'95403'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'32114'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01602'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02176'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85281'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'85282'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22911'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53115'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20817'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97405'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'90291'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97403'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'54467'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97408'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'68106'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NE'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'25652'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WV'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60476'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75240'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12205'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14853'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'V0R2M'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14850'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'05464'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20910'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85710'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'85711'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44124'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44691'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48118'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29210'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'49428'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95628'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75013'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94612'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94619'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'94618'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84302'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97232'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'59801'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'36106'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'15610'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33556'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14211'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'V5A2B'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'14216'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'99709'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37212'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'40206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'40205'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78741'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'78628'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78746'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50266'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95050'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'66315'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'77904'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30350'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30606'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'83716'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ID'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'79508'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'67401'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KS'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27713'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'23092'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55128'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80227'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55122'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92507'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55125'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97124'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'94591'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'66221'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KS'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30078'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'66046'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KS'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'28814'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'23322'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77081'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'89503'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NV'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77840'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'07310'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'01970'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55013'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'08832'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22306'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55409'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55408'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55406'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'73162'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'73439'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06472'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'93101'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93063'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93109'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55303'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'96349'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AP'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55305'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'07733'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98038'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98034'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'17345'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'43017'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'31820'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06371'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85202'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93402'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'51157'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'38866'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MS'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20879'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95453'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53171'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'98257'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20707'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19807'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DE'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93711'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61801'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'98133'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97365'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'05779'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'46538'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01701'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'22932'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75094'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20770'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'70116'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'LA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60440'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'94117'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94115'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80538'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48322'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'45243'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'80027'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90210'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85016'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90034'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48103'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'20902'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'59717'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'29205'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63129'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60614'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60615'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60613'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'20685'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94608'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48197'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95110'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'36117'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'08534'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37901'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48076'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95129'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'29440'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20057'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'DC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95123'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01080'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33319'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'07030'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'71701'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'07039'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78750'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78756'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'17870'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27502'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02140'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'99603'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97229'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'76059'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'V3N4P'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01331'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30011'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'N4T1A'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'40515'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'48911'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50325'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92103'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50322'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30067'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'30068'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06811'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77801'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'L9G2B'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22202'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'22207'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'22206'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11238'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'K7L5J'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'89104'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NV'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'87544'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NM'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43221'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01940'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'23237'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19102'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'19104'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95938'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55412'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55413'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55414'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'55417'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'16509'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'16506'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60302'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55369'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'90036'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'73034'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27105'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61401'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'19422'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'39042'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MS'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98121'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98027'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21012'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21010'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'93612'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'96754'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'HI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06365'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93550'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'93555'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'93003'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60115'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'45439'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80228'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'64153'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'91505'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'81648'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'49036'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12180'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11701'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'53715'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53711'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53713'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90630'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94040'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'94043'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'05146'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80521'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80526'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80525'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'44265'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'09645'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AE'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33716'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60626'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63132'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'63130'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01002'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98801'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'83814'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ID'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90703'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'42101'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44212'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'54302'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44405'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84010'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'80913'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80919'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37235'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29301'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44106'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'84604'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'84601'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'UT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33308'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'37725'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'18053'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'03261'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'07029'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78602'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95032'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'99687'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AK'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'28018'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27514'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27510'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'27511'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'17961'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'08034'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30002'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12866'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92688'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'15017'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'40504'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'40503'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92110'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92113'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92115'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'29646'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30093'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'73132'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33484'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06779'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'23226'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'23227'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'VA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'10522'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06906'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'61462'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'74078'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'74075'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OK'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06518'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77073'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06513'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'06512'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'43085'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'51250'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92064'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98620'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'02110'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'02113'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92660'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92705'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60201'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'60202'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'32707'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'56321'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06355'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11787'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'90840'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'06059'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CT'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97007'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'11727'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'63645'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'49508'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21911'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60515'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80236'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'26241'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WV'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'85258'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'98405'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97302'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85251'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'97301'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OR'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'46032'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'62522'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'45218'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53706'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53705'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'76111'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53703'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21218'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60035'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'57197'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SD'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'63033'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20755'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'01720'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'85032'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'AZ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'75230'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'38401'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20854'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'03869'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20850'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'80127'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'80123'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'53202'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WI'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90016'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'58644'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'ND'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'90019'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'63108'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MO'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'12550'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94143'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33066'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'FL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'68147'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NE'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'60630'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'20784'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'70403'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'LA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'33205'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94720'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'95662'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95660'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'18505'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'PA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'44074'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'M4J2K'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'44092'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'OH'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94533'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'50233'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'79070'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'94920'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'08043'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'47401'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30033'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'78704'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'30030'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'66215'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'KS'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'21227'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MD'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'95316'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'29678'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'SC'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'60008'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IL'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'47905'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'91711'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'47906'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'IN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'55443'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'MN'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'77380'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'TX'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'13820'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NY'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'26506'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'WV'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'31909'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'GA'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'08904'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'NJ'</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'92374'</span><span class="p">:</span> <span class="sa">u</span><span class="s1">'CA'</span><span class="p">,</span>
<span class="sa">u</span><span class="s1">'00000'</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="sa">u</span><span class="s1">'E2E3R'</span><span class="p">:</span> <span class="kc">None</span><span class="p">}</span>
<span class="c1"># And use the dict to initialize lens['state']:</span>
<span class="n">lens</span><span class="p">[</span><span class="s1">'state'</span><span class="p">]</span><span class="o">=</span><span class="n">lens</span><span class="p">[</span><span class="s1">'zip_code'</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">states2</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">x</span><span class="p">))</span>
</pre></div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Beyond the state, I’d like to add some other columns for describing data and drop a bunch of non-needed columns:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [255]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="kn">import</span> <span class="nn">datetime</span>
<span class="c1"># Let's also initialize it by the release_year, decade and review day</span>
<span class="n">lens</span><span class="p">[</span><span class="s1">'release_year'</span><span class="p">]</span><span class="o">=</span><span class="n">lens</span><span class="p">[</span><span class="s1">'release_date'</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">'-'</span><span class="p">)[</span><span class="mi">2</span><span class="p">]</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">'-'</span><span class="p">))</span><span class="o">></span><span class="mi">2</span> <span class="k">else</span> <span class="mi">0</span><span class="p">)</span>
<span class="n">lens</span><span class="p">[</span><span class="s1">'decade'</span><span class="p">]</span><span class="o">=</span><span class="n">lens</span><span class="p">[</span><span class="s1">'release_year'</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">x</span><span class="p">)[</span><span class="mi">2</span><span class="p">:</span><span class="mi">3</span><span class="p">]</span><span class="o">+</span><span class="s2">"0's"</span> <span class="k">if</span> <span class="n">x</span> <span class="k">else</span> <span class="s2">""</span><span class="p">)</span>
<span class="n">lens</span><span class="p">[</span><span class="s1">'review_day'</span><span class="p">]</span><span class="o">=</span><span class="n">lens</span><span class="p">[</span><span class="s1">'unix_timestamp'</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%A'</span><span class="p">))</span>
<span class="c1"># And remove some non-needed stuff</span>
<span class="n">final_lens</span> <span class="o">=</span> <span class="n">lens</span><span class="o">.</span><span class="n">drop</span><span class="p">([</span><span class="s1">'release_date'</span><span class="p">,</span><span class="s1">'zip_code'</span><span class="p">,</span> <span class="s1">'unix_timestamp'</span><span class="p">,</span> <span class="s1">'video_release_date'</span><span class="p">,</span> <span class="s1">'imdb_url'</span><span class="p">,</span> <span class="s1">'movie_id'</span><span class="p">,</span> <span class="s1">'user_id'</span><span class="p">],</span> <span class="mi">1</span><span class="p">)</span>
<span class="c1"># Also add an idx column</span>
<span class="n">final_lens</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">]</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">index</span>
<span class="n">final_lens</span><span class="o">.</span><span class="n">head</span><span class="p">()</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[255]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>title</th>
<th>rating</th>
<th>age</th>
<th>sex</th>
<th>occupation</th>
<th>state</th>
<th>release_year</th>
<th>decade</th>
<th>review_day</th>
<th>idx</th>
</tr>
</thead>
<tbody>
<tr>
<th>0</th>
<td>Toy Story (1995)</td>
<td>4</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td><span class="caps">CA</span></td>
<td>1995</td>
<td>90’s</td>
<td>Tuesday</td>
<td>0</td>
</tr>
<tr>
<th>1</th>
<td>Get Shorty (1995)</td>
<td>5</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td><span class="caps">CA</span></td>
<td>1995</td>
<td>90’s</td>
<td>Tuesday</td>
<td>1</td>
</tr>
<tr>
<th>2</th>
<td>Copycat (1995)</td>
<td>4</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td><span class="caps">CA</span></td>
<td>1995</td>
<td>90’s</td>
<td>Tuesday</td>
<td>2</td>
</tr>
<tr>
<th>3</th>
<td>Twelve Monkeys (1995)</td>
<td>4</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td><span class="caps">CA</span></td>
<td>1995</td>
<td>90’s</td>
<td>Tuesday</td>
<td>3</td>
</tr>
<tr>
<th>4</th>
<td>Babe (1995)</td>
<td>5</td>
<td>60</td>
<td>M</td>
<td>retired</td>
<td><span class="caps">CA</span></td>
<td>1995</td>
<td>90’s</td>
<td>Tuesday</td>
<td>4</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>So, after the previous (I hope easy) modifications we have a DataFrame that contains useful info about reviews of movies. Each line of the dataframe contains the following 9 columns of data:</p>
<ul>
<li>Title of movie</li>
<li>Rating it got from this review</li>
<li>Age of the reviewer</li>
<li>Sex of the reviewer</li>
<li>Occupation of the reviewer</li>
<li>State (<span class="caps">US</span>) of the reviewer</li>
<li>Release year of the movie</li>
<li>Decade the movie was released</li>
<li>Day of the review</li>
</ul>
<p>Let’s take a peek at the movies of which decade were prefered by reviewers, by sex. First of all, we’ll do a pivot table to aggregate the rating and idx columns values by sex and decade. For the rating we’ll take the average of the reviews for each decade/sex while, for idx we’ll get the len (just to count the number of reviews):</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [256]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">prefered_decade_by_sex</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'sex'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span> <span class="s1">'decade'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">],</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span><span class="s1">'rating'</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">average</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">:</span> <span class="nb">len</span><span class="p">},</span>
<span class="n">fill_value</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="p">)</span>
<span class="nb">print</span> <span class="n">prefered_decade_by_sex</span>
<span class="c1"># We see that there are a bunch of movies without decade, we'll drop them using the drop method</span>
<span class="c1"># Notice that I pass '' to drop to remove the column with empty index</span>
<span class="n">prefered_decade_by_sex</span> <span class="o">=</span> <span class="n">prefered_decade_by_sex</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="s1">''</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre> rating idx
sex F M F M
decade
3.500000 3.428571 2 7
20's 2.857143 3.632653 7 49
30's 3.961340 3.912455 388 1108
40's 3.952641 4.029412 549 1700
50's 3.835006 3.972403 897 2609
60's 3.852321 3.891015 948 2927
70's 3.754007 3.899522 1435 4807
80's 3.700214 3.764700 2802 9320
90's 3.437366 3.384938 18712 51733
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[256]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="2" halign="left">rating</th>
<th colspan="2" halign="left">idx</th>
</tr>
<tr>
<th>sex</th>
<th>F</th>
<th>M</th>
<th>F</th>
<th>M</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>20’s</th>
<td>2.857143</td>
<td>3.632653</td>
<td>7</td>
<td>49</td>
</tr>
<tr>
<th>30’s</th>
<td>3.961340</td>
<td>3.912455</td>
<td>388</td>
<td>1108</td>
</tr>
<tr>
<th>40’s</th>
<td>3.952641</td>
<td>4.029412</td>
<td>549</td>
<td>1700</td>
</tr>
<tr>
<th>50’s</th>
<td>3.835006</td>
<td>3.972403</td>
<td>897</td>
<td>2609</td>
</tr>
<tr>
<th>60’s</th>
<td>3.852321</td>
<td>3.891015</td>
<td>948</td>
<td>2927</td>
</tr>
<tr>
<th>70’s</th>
<td>3.754007</td>
<td>3.899522</td>
<td>1435</td>
<td>4807</td>
</tr>
<tr>
<th>80’s</th>
<td>3.700214</td>
<td>3.764700</td>
<td>2802</td>
<td>9320</td>
</tr>
<tr>
<th>90’s</th>
<td>3.437366</td>
<td>3.384938</td>
<td>18712</td>
<td>51733</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Now, we see that we have the rating and review count for both men and women. However, I’d also like to get their combined (average rating and total review) values. There are two ways that this can be done: First, we can create <em>another</em> dataframe that does not seperate sex:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [257]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">prefered_decade</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span> <span class="s1">'decade'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">],</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span><span class="s1">'rating'</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">average</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">:</span> <span class="nb">len</span><span class="p">},</span>
<span class="n">fill_value</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1"># Drop non needed index</span>
<span class="n">prefered_decade</span> <span class="o">=</span> <span class="n">prefered_decade</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="s1">''</span><span class="p">)</span>
<span class="n">prefered_decade</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[257]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th>idx</th>
<th>rating</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>20’s</th>
<td>56</td>
<td>3.535714</td>
</tr>
<tr>
<th>30’s</th>
<td>1496</td>
<td>3.925134</td>
</tr>
<tr>
<th>40’s</th>
<td>2249</td>
<td>4.010671</td>
</tr>
<tr>
<th>50’s</th>
<td>3506</td>
<td>3.937250</td>
</tr>
<tr>
<th>60’s</th>
<td>3875</td>
<td>3.881548</td>
</tr>
<tr>
<th>70’s</th>
<td>6242</td>
<td>3.866069</td>
</tr>
<tr>
<th>80’s</th>
<td>12122</td>
<td>3.749794</td>
</tr>
<tr>
<th>90’s</th>
<td>70445</td>
<td>3.398864</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Now, these two DataFrames can be easily combined because they have the same index. I’ll put the values from the total DataFrame as a second level index to the seperated (by sex) DataFrame - notice how the multi index is used for indexing:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [258]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">prefered_decade_by_sex</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'total'</span><span class="p">]</span> <span class="o">=</span> <span class="n">prefered_decade</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">]</span>
<span class="n">prefered_decade_by_sex</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">,</span> <span class="s1">'total'</span><span class="p">]</span> <span class="o">=</span> <span class="n">prefered_decade</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">]</span>
<span class="n">prefered_decade_by_sex</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[258]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="2" halign="left">rating</th>
<th colspan="2" halign="left">idx</th>
<th>rating</th>
<th>idx</th>
</tr>
<tr>
<th>sex</th>
<th>F</th>
<th>M</th>
<th>F</th>
<th>M</th>
<th>total</th>
<th>total</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>20’s</th>
<td>2.857143</td>
<td>3.632653</td>
<td>7</td>
<td>49</td>
<td>3.535714</td>
<td>56</td>
</tr>
<tr>
<th>30’s</th>
<td>3.961340</td>
<td>3.912455</td>
<td>388</td>
<td>1108</td>
<td>3.925134</td>
<td>1496</td>
</tr>
<tr>
<th>40’s</th>
<td>3.952641</td>
<td>4.029412</td>
<td>549</td>
<td>1700</td>
<td>4.010671</td>
<td>2249</td>
</tr>
<tr>
<th>50’s</th>
<td>3.835006</td>
<td>3.972403</td>
<td>897</td>
<td>2609</td>
<td>3.937250</td>
<td>3506</td>
</tr>
<tr>
<th>60’s</th>
<td>3.852321</td>
<td>3.891015</td>
<td>948</td>
<td>2927</td>
<td>3.881548</td>
<td>3875</td>
</tr>
<tr>
<th>70’s</th>
<td>3.754007</td>
<td>3.899522</td>
<td>1435</td>
<td>4807</td>
<td>3.866069</td>
<td>6242</td>
</tr>
<tr>
<th>80’s</th>
<td>3.700214</td>
<td>3.764700</td>
<td>2802</td>
<td>9320</td>
<td>3.749794</td>
<td>12122</td>
</tr>
<tr>
<th>90’s</th>
<td>3.437366</td>
<td>3.384938</td>
<td>18712</td>
<td>51733</td>
<td>3.398864</td>
<td>70445</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Now, the previous would be almost perfect but I would really prefer the rating and idx first-level columns to be all together. This can be done by using the <code>sort_index</code> — the <code>axis=1</code> parameter sorts the columns(or else the index will be sorted):</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [259]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">prefered_decade_by_sex</span> <span class="o">=</span> <span class="n">prefered_decade_by_sex</span><span class="o">.</span><span class="n">sort_index</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[259]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="3" halign="left">idx</th>
<th colspan="3" halign="left">rating</th>
</tr>
<tr>
<th>sex</th>
<th>F</th>
<th>M</th>
<th>total</th>
<th>F</th>
<th>M</th>
<th>total</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>20’s</th>
<td>7</td>
<td>49</td>
<td>56</td>
<td>2.857143</td>
<td>3.632653</td>
<td>3.535714</td>
</tr>
<tr>
<th>30’s</th>
<td>388</td>
<td>1108</td>
<td>1496</td>
<td>3.961340</td>
<td>3.912455</td>
<td>3.925134</td>
</tr>
<tr>
<th>40’s</th>
<td>549</td>
<td>1700</td>
<td>2249</td>
<td>3.952641</td>
<td>4.029412</td>
<td>4.010671</td>
</tr>
<tr>
<th>50’s</th>
<td>897</td>
<td>2609</td>
<td>3506</td>
<td>3.835006</td>
<td>3.972403</td>
<td>3.937250</td>
</tr>
<tr>
<th>60’s</th>
<td>948</td>
<td>2927</td>
<td>3875</td>
<td>3.852321</td>
<td>3.891015</td>
<td>3.881548</td>
</tr>
<tr>
<th>70’s</th>
<td>1435</td>
<td>4807</td>
<td>6242</td>
<td>3.754007</td>
<td>3.899522</td>
<td>3.866069</td>
</tr>
<tr>
<th>80’s</th>
<td>2802</td>
<td>9320</td>
<td>12122</td>
<td>3.700214</td>
<td>3.764700</td>
<td>3.749794</td>
</tr>
<tr>
<th>90’s</th>
<td>18712</td>
<td>51733</td>
<td>70445</td>
<td>3.437366</td>
<td>3.384938</td>
<td>3.398864</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>The other method to create the sex and total reviews DataFrame is to aggregate the values from <code>prefered_decade_by_sex</code> directly (without creating another pivot table). Take a look at the <code>get_average</code> function below. For each row it will take the total number of reviews for men and women and multiply that number with the corresponding average. It will then divide the sum of averages with the total number of reviws to get the average for each row. We also use the <code>sort_index</code> method to display the columns correctly:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [260]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">prefered_decade_by_sex</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'sex'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span> <span class="s1">'decade'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">],</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span><span class="s1">'rating'</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">average</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">:</span> <span class="nb">len</span><span class="p">},</span>
<span class="n">fill_value</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">prefered_decade_by_sex</span> <span class="o">=</span> <span class="n">prefered_decade_by_sex</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="s1">''</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_average</span><span class="p">(</span><span class="n">row</span><span class="p">):</span>
<span class="n">total_f</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">][</span><span class="s1">'F'</span><span class="p">]</span>
<span class="n">total_m</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">][</span><span class="s1">'M'</span><span class="p">]</span>
<span class="n">total_rating_f</span> <span class="o">=</span> <span class="n">total_f</span><span class="o">*</span><span class="n">row</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">][</span><span class="s1">'F'</span><span class="p">]</span>
<span class="n">total_rating_m</span> <span class="o">=</span> <span class="n">total_m</span><span class="o">*</span><span class="n">row</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">][</span><span class="s1">'M'</span><span class="p">]</span>
<span class="k">return</span> <span class="p">(</span><span class="n">total_rating_f</span><span class="o">+</span><span class="n">total_rating_m</span><span class="p">)</span><span class="o">/</span><span class="p">(</span><span class="n">total_f</span><span class="o">+</span><span class="n">total_m</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'total'</span><span class="p">]</span> <span class="o">=</span> <span class="n">prefered_decade_by_sex</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">get_average</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">,</span> <span class="s1">'total'</span><span class="p">]</span> <span class="o">=</span> <span class="n">prefered_decade_by_sex</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">][</span><span class="s1">'F'</span><span class="p">]</span><span class="o">+</span><span class="n">x</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">][</span><span class="s1">'M'</span><span class="p">],</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span> <span class="o">=</span> <span class="n">prefered_decade_by_sex</span><span class="o">.</span><span class="n">sort_index</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[260]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="3" halign="left">idx</th>
<th colspan="3" halign="left">rating</th>
</tr>
<tr>
<th>sex</th>
<th>F</th>
<th>M</th>
<th>total</th>
<th>F</th>
<th>M</th>
<th>total</th>
</tr>
<tr>
<th>decade</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>20’s</th>
<td>7</td>
<td>49</td>
<td>56.0</td>
<td>2.857143</td>
<td>3.632653</td>
<td>3.535714</td>
</tr>
<tr>
<th>30’s</th>
<td>388</td>
<td>1108</td>
<td>1496.0</td>
<td>3.961340</td>
<td>3.912455</td>
<td>3.925134</td>
</tr>
<tr>
<th>40’s</th>
<td>549</td>
<td>1700</td>
<td>2249.0</td>
<td>3.952641</td>
<td>4.029412</td>
<td>4.010671</td>
</tr>
<tr>
<th>50’s</th>
<td>897</td>
<td>2609</td>
<td>3506.0</td>
<td>3.835006</td>
<td>3.972403</td>
<td>3.937250</td>
</tr>
<tr>
<th>60’s</th>
<td>948</td>
<td>2927</td>
<td>3875.0</td>
<td>3.852321</td>
<td>3.891015</td>
<td>3.881548</td>
</tr>
<tr>
<th>70’s</th>
<td>1435</td>
<td>4807</td>
<td>6242.0</td>
<td>3.754007</td>
<td>3.899522</td>
<td>3.866069</td>
</tr>
<tr>
<th>80’s</th>
<td>2802</td>
<td>9320</td>
<td>12122.0</td>
<td>3.700214</td>
<td>3.764700</td>
<td>3.749794</td>
</tr>
<tr>
<th>90’s</th>
<td>18712</td>
<td>51733</td>
<td>70445.0</td>
<td>3.437366</td>
<td>3.384938</td>
<td>3.398864</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Let’s try to plot this DataFrame to see if we can extract some useful conclusions:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [261]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">f</span><span class="p">,</span> <span class="n">a</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">]</span><span class="o">.</span><span class="n">plot</span><span class="p">(</span><span class="n">ax</span><span class="o">=</span><span class="n">a</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span><span class="mi">5</span><span class="p">),</span> <span class="n">logy</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">prefered_decade_by_sex</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">]</span><span class="o">.</span><span class="n">plot</span><span class="p">(</span><span class="n">ax</span><span class="o">=</span><span class="n">a</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span><span class="mi">5</span><span class="p">))</span>
<span class="c1"># It seems that women don't like movies from the 20's (but if you take at the number </span>
<span class="c1"># of votes there are too few women that have voted for 20's movies. Also, the best</span>
<span class="c1"># reviews seem to be for movies of 30's and 40's and (as expected) the most revies</span>
<span class="c1"># are for newest movies</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[261]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x20f13e90></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
ZpZgZouBH4CPj5bQhfQApvsRh4ZeiogULkWDDiA/1qyBceNg5cqgIxEREQHnXBbQwMxKAxPNrI5z
bkXO88ysK9AIOGrqlX3YamJiIolhrHqSmgp/+1ve4xYRkYKRkpISkelnOcX08MuOHaFFC3jggQgF
JSIivihswy8BzGwgsNc592yO45cCzwGtnHM/HeW1x9xGZmTAqafCV1/BaacdUxEiIsdMwy9zp+GX
R5GaCkuXQp8+QUciIiICZnaqmZ0UelwSuBRYleOcBsB/gKuPltDl15IlULGiEjoRkcIkJodfZmV5
G40/+SSUKBF0NCIiIgCcAbxmZgl4N03HO+emmdkgYKFzbgrwNHA88I6ZGfCNc65jJIPQfDoRkcIn
JpO6N96AYsW8BVJERESigXNuGdAwl+NJ2R5f5nccqanQtavfVxERkWgSc8Mv9+2Dhx+GZ58FK1Sz
M0RERP5YZqa3xU+rVkFHIiLiv3379tG+fXsaNGhAvXr1eOedd1i0aBGJiYlccMEFtGvXji1btpCZ
mUmTJk2YNWsWAAMGDOAf//hHwNFHVsz11P3rX9C0qbdAioiIiPwuPR3KlYPy5YOORETEfx9++CEV
K1ZkypQpAOzevZt27doxadIkTjnlFN5++20eeughRo4cyZgxY7j++ut57rnnmDFjBmlpaQFHH1mB
JnXJyclhLdP8ww9eUhdnvwMRkbjl19LNkjvNpxORwqRu3brcf//9DBgwgCuvvJIyZcqwfPlyLrvs
MpxzZGVlccYZZwBQp04dunbtylVXXUVaWhpFi8Zc39YfiqktDe68E044AYYO9SkoERHxRWHc0iA/
jnVLg2uugU6doHNnH4ISEcmDgt7SYOfOnUybNo1XX32V1q1b89FHHzF37txcz73ppptISUlhzJgx
tG3btsBiBG1p8Jvly+H99+GRR4KOREREJPpkZXnz6dRTJyKFxffff0/JkiW56aab6NevH2lpaWzd
upXPPvsMgIyMDFasWAHAhAkT2L59O7NmzaJ3797s3r07yNAjLmb6He+/31sgpUyZoCMRERGJPl9+
6bWRFSoEHYmISMFYtmwZ999/PwkJCRx33HG8/PLLFC1alN69e7Nr1y4yMzO5++67KVeuHA899BAz
Z86kQoUK9O7dm759+zJ69OigqxAxMTH8csYM6NXLa7COO87nwEREJOI0/DI8xzL88vnnYelSGDHC
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
"
class="
"
>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Let’s try another quick <code>pivot_table</code>. Can we see if there’s a specific day-of-week at which the reviewers prefer to vote?</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [262]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'review_day'</span><span class="p">],</span>
<span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s1">'sex'</span><span class="p">],</span>
<span class="n">values</span><span class="o">=</span><span class="s1">'idx'</span><span class="p">,</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="nb">len</span>
<span class="p">)</span><span class="o">.</span><span class="n">plot</span><span class="p">()</span>
<span class="c1"># Probably not</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[262]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x20f542d0></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
"
class="
"
>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Continuing our exploration of the movie-reviews data set, we’d like to get the total number of reviews and average rating for each movie. To make a better display for the release we’ll categorise the movies by their decade and release year (using multi index rows):</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [263]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">rating_count</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span>
<span class="n">index</span><span class="o">=</span><span class="p">[</span> <span class="s1">'decade'</span><span class="p">,</span> <span class="s1">'release_year'</span><span class="p">,</span> <span class="s1">'title'</span><span class="p">,],</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">,</span> <span class="p">],</span>
<span class="n">aggfunc</span><span class="o">=</span><span class="p">{</span>
<span class="s1">'rating'</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">average</span><span class="p">,</span>
<span class="s1">'idx'</span><span class="p">:</span> <span class="nb">len</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">)</span>
<span class="c1"># Drop movies without decade</span>
<span class="n">rating_count</span> <span class="o">=</span> <span class="n">rating_count</span><span class="o">.</span><span class="n">drop</span><span class="p">(</span><span class="s1">''</span><span class="p">)</span>
<span class="n">rating_count</span><span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="c1"># Notice the nice hierarchical index on decade and release_year</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[263]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th></th>
<th></th>
<th>idx</th>
<th>rating</th>
</tr>
<tr>
<th>decade</th>
<th>release_year</th>
<th>title</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th rowspan="2" valign="top">20’s</th>
<th>1922</th>
<th>Nosferatu (Nosferatu, eine Symphonie des Grauens) (1922)</th>
<td>54</td>
<td>3.555556</td>
</tr>
<tr>
<th>1926</th>
<th>Scarlet Letter, The (1926)</th>
<td>2</td>
<td>3.000000</td>
</tr>
<tr>
<th rowspan="8" valign="top">30’s</th>
<th>1930</th>
<th>Blue Angel, The (Blaue Engel, Der) (1930)</th>
<td>18</td>
<td>3.777778</td>
</tr>
<tr>
<th>1931</th>
<th>M (1931)</th>
<td>44</td>
<td>4.000000</td>
</tr>
<tr>
<th>1932</th>
<th>Farewell to Arms, A (1932)</th>
<td>12</td>
<td>3.833333</td>
</tr>
<tr>
<th rowspan="2" valign="top">1933</th>
<th>Duck Soup (1933)</th>
<td>93</td>
<td>4.000000</td>
</tr>
<tr>
<th>Liebelei (1933)</th>
<td>1</td>
<td>1.000000</td>
</tr>
<tr>
<th rowspan="3" valign="top">1934</th>
<th>Gay Divorcee, The (1934)</th>
<td>15</td>
<td>3.866667</td>
</tr>
<tr>
<th>It Happened One Night (1934)</th>
<td>81</td>
<td>4.012346</td>
</tr>
<tr>
<th>Of Human Bondage (1934)</th>
<td>5</td>
<td>3.200000</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>As we can see above, there are movies with very few revies. I don’t really want to count them since they’ll probably won’t have correct ratings. Also, instead of displaying all the movies I’d like to create a small list with only the best movies:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [264]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># So, let's find the best movies (rating more than 4) with more than 150 reviews</span>
<span class="n">best_movies</span> <span class="o">=</span> <span class="n">rating_count</span><span class="p">[(</span><span class="n">rating_count</span><span class="o">.</span><span class="n">idx</span><span class="o">></span><span class="mi">150</span><span class="p">)</span> <span class="o">&</span> <span class="p">(</span><span class="n">rating_count</span><span class="o">.</span><span class="n">rating</span><span class="o">></span><span class="mi">4</span><span class="p">)]</span>
<span class="n">best_movies</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[264]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th></th>
<th></th>
<th></th>
<th>idx</th>
<th>rating</th>
</tr>
<tr>
<th>decade</th>
<th>release_year</th>
<th>title</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>30’s</th>
<th>1939</th>
<th>Wizard of Oz, The (1939)</th>
<td>246</td>
<td>4.077236</td>
</tr>
<tr>
<th rowspan="3" valign="top">40’s</th>
<th>1941</th>
<th>Citizen Kane (1941)</th>
<td>198</td>
<td>4.292929</td>
</tr>
<tr>
<th>1942</th>
<th>Casablanca (1942)</th>
<td>243</td>
<td>4.456790</td>
</tr>
<tr>
<th>1946</th>
<th>It’s a Wonderful Life (1946)</th>
<td>231</td>
<td>4.121212</td>
</tr>
<tr>
<th rowspan="5" valign="top">50’s</th>
<th>1951</th>
<th>African Queen, The (1951)</th>
<td>152</td>
<td>4.184211</td>
</tr>
<tr>
<th>1954</th>
<th>Rear Window (1954)</th>
<td>209</td>
<td>4.387560</td>
</tr>
<tr>
<th>1957</th>
<th>Bridge on the River Kwai, The (1957)</th>
<td>165</td>
<td>4.175758</td>
</tr>
<tr>
<th>1958</th>
<th>Vertigo (1958)</th>
<td>179</td>
<td>4.251397</td>
</tr>
<tr>
<th>1959</th>
<th>North by Northwest (1959)</th>
<td>179</td>
<td>4.284916</td>
</tr>
<tr>
<th rowspan="5" valign="top">60’s</th>
<th>1960</th>
<th>Psycho (1960)</th>
<td>239</td>
<td>4.100418</td>
</tr>
<tr>
<th rowspan="2" valign="top">1962</th>
<th>Lawrence of Arabia (1962)</th>
<td>173</td>
<td>4.231214</td>
</tr>
<tr>
<th>To Kill a Mockingbird (1962)</th>
<td>219</td>
<td>4.292237</td>
</tr>
<tr>
<th>1963</th>
<th>Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb (1963)</th>
<td>194</td>
<td>4.252577</td>
</tr>
<tr>
<th>1967</th>
<th>Graduate, The (1967)</th>
<td>239</td>
<td>4.104603</td>
</tr>
<tr>
<th rowspan="8" valign="top">70’s</th>
<th>1972</th>
<th>Godfather, The (1972)</th>
<td>413</td>
<td>4.283293</td>
</tr>
<tr>
<th>1973</th>
<th>Sting, The (1973)</th>
<td>241</td>
<td>4.058091</td>
</tr>
<tr>
<th rowspan="2" valign="top">1974</th>
<th>Godfather: Part <span class="caps">II</span>, The (1974)</th>
<td>209</td>
<td>4.186603</td>
</tr>
<tr>
<th>Monty Python and the Holy Grail (1974)</th>
<td>316</td>
<td>4.066456</td>
</tr>
<tr>
<th>1975</th>
<th>One Flew Over the Cuckoo’s Nest (1975)</th>
<td>264</td>
<td>4.291667</td>
</tr>
<tr>
<th>1977</th>
<th>Star Wars (1977)</th>
<td>583</td>
<td>4.358491</td>
</tr>
<tr>
<th rowspan="2" valign="top">1979</th>
<th>Alien (1979)</th>
<td>291</td>
<td>4.034364</td>
</tr>
<tr>
<th>Apocalypse Now (1979)</th>
<td>221</td>
<td>4.045249</td>
</tr>
<tr>
<th rowspan="7" valign="top">80’s</th>
<th>1980</th>
<th>Empire Strikes Back, The (1980)</th>
<td>367</td>
<td>4.204360</td>
</tr>
<tr>
<th>1981</th>
<th>Raiders of the Lost Ark (1981)</th>
<td>420</td>
<td>4.252381</td>
</tr>
<tr>
<th rowspan="2" valign="top">1982</th>
<th>Blade Runner (1982)</th>
<td>275</td>
<td>4.138182</td>
</tr>
<tr>
<th>Gandhi (1982)</th>
<td>195</td>
<td>4.020513</td>
</tr>
<tr>
<th>1984</th>
<th>Amadeus (1984)</th>
<td>276</td>
<td>4.163043</td>
</tr>
<tr>
<th>1987</th>
<th>Princess Bride, The (1987)</th>
<td>324</td>
<td>4.172840</td>
</tr>
<tr>
<th>1989</th>
<th>Glory (1989)</th>
<td>171</td>
<td>4.076023</td>
</tr>
<tr>
<th rowspan="20" valign="top">90’s</th>
<th rowspan="2" valign="top">1991</th>
<th>Silence of the Lambs, The (1991)</th>
<td>390</td>
<td>4.289744</td>
</tr>
<tr>
<th>Terminator 2: Judgment Day (1991)</th>
<td>295</td>
<td>4.006780</td>
</tr>
<tr>
<th rowspan="3" valign="top">1993</th>
<th>Fugitive, The (1993)</th>
<td>336</td>
<td>4.044643</td>
</tr>
<tr>
<th>Much Ado About Nothing (1993)</th>
<td>176</td>
<td>4.062500</td>
</tr>
<tr>
<th>Schindler’s List (1993)</th>
<td>298</td>
<td>4.466443</td>
</tr>
<tr>
<th rowspan="2" valign="top">1994</th>
<th>Pulp Fiction (1994)</th>
<td>394</td>
<td>4.060914</td>
</tr>
<tr>
<th>Shawshank Redemption, The (1994)</th>
<td>283</td>
<td>4.445230</td>
</tr>
<tr>
<th rowspan="2" valign="top">1995</th>
<th>Sense and Sensibility (1995)</th>
<td>268</td>
<td>4.011194</td>
</tr>
<tr>
<th>Usual Suspects, The (1995)</th>
<td>267</td>
<td>4.385768</td>
</tr>
<tr>
<th rowspan="4" valign="top">1996</th>
<th>Braveheart (1995)</th>
<td>297</td>
<td>4.151515</td>
</tr>
<tr>
<th>Lone Star (1996)</th>
<td>187</td>
<td>4.053476</td>
</tr>
<tr>
<th>Secrets <span class="amp">&</span> Lies (1996)</th>
<td>162</td>
<td>4.265432</td>
</tr>
<tr>
<th>Taxi Driver (1976)</th>
<td>182</td>
<td>4.120879</td>
</tr>
<tr>
<th rowspan="6" valign="top">1997</th>
<th>Boot, Das (1981)</th>
<td>201</td>
<td>4.203980</td>
</tr>
<tr>
<th>Fargo (1996)</th>
<td>508</td>
<td>4.155512</td>
</tr>
<tr>
<th>Good Will Hunting (1997)</th>
<td>198</td>
<td>4.262626</td>
</tr>
<tr>
<th><span class="caps">L.A.</span> Confidential (1997)</th>
<td>297</td>
<td>4.161616</td>
</tr>
<tr>
<th>Return of the Jedi (1983)</th>
<td>507</td>
<td>4.007890</td>
</tr>
<tr>
<th>Titanic (1997)</th>
<td>350</td>
<td>4.245714</td>
</tr>
<tr>
<th>1998</th>
<th>Apt Pupil (1998)</th>
<td>160</td>
<td>4.100000</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [265]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Which are the most popular movies (number of votes) ?</span>
<span class="n">best_movies</span><span class="o">.</span><span class="n">sort_values</span><span class="p">(</span><span class="n">by</span><span class="o">=</span><span class="s1">'idx'</span><span class="p">,</span> <span class="n">ascending</span><span class="o">=</span><span class="kc">False</span><span class="p">)[</span><span class="s1">'idx'</span><span class="p">][:</span><span class="mi">10</span><span class="p">]</span><span class="o">.</span><span class="n">plot</span><span class="p">(</span><span class="n">kind</span><span class="o">=</span><span class="s1">'bar'</span><span class="p">)</span>
<span class="c1"># Fargo at the 2nd and Fugitive at the 10nth place of popularity seem a little strange to mee</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[265]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x20f92bd0></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
aq1mFjruAAAAAElFTkSuQmCC
"
class="
"
>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [266]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Which are the best movies (vote average) ?</span>
<span class="n">best_movies</span><span class="o">.</span><span class="n">sort_values</span><span class="p">(</span><span class="n">by</span><span class="o">=</span><span class="s1">'rating'</span><span class="p">,</span> <span class="n">ascending</span><span class="o">=</span><span class="kc">False</span><span class="p">)[</span><span class="s1">'rating'</span><span class="p">][:</span><span class="mi">10</span><span class="p">]</span><span class="o">.</span><span class="n">plot</span><span class="p">(</span><span class="n">kind</span><span class="o">=</span><span class="s1">'bar'</span><span class="p">,</span> <span class="n">ylim</span><span class="o">=</span><span class="p">(</span><span class="mf">4.2</span><span class="p">,</span><span class="mf">4.5</span><span class="p">))</span>
<span class="c1"># I tend to agree with most of them, however I feel that the Godfather is missing...</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[266]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x21210030></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
"
class="
"
>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Let’s try to see how many people of each age are voting, however instead of displaying the votes for people of each specific age, we’ll seperate the ages into groups (0-10, 10,20 etc) and dispaly the counts for them:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [267]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">review_idx_by_age</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="p">[</span><span class="s1">'age'</span><span class="p">],</span> <span class="n">values</span><span class="o">=</span><span class="s1">'idx'</span><span class="p">,</span> <span class="n">aggfunc</span><span class="o">=</span><span class="nb">len</span><span class="p">)</span>
<span class="nb">print</span> <span class="n">review_idx_by_age</span><span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
<span class="c1"># Let's group by age group</span>
<span class="k">def</span> <span class="nf">by_age</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="k">return</span> <span class="s1">'</span><span class="si">{0}</span><span class="s1">-</span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">((</span><span class="n">x</span><span class="o">/</span><span class="mi">10</span><span class="p">)</span><span class="o">*</span><span class="mi">10</span><span class="p">,</span> <span class="p">(</span><span class="n">x</span><span class="o">/</span><span class="mi">10</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span><span class="o">*</span><span class="mi">10</span><span class="p">)</span>
<span class="n">grouped_review_idx</span> <span class="o">=</span> <span class="n">review_idx_by_age</span><span class="o">.</span><span class="n">groupby</span><span class="p">(</span><span class="n">by_age</span><span class="p">)</span><span class="o">.</span><span class="n">aggregate</span><span class="p">(</span><span class="nb">sum</span><span class="p">)</span>
<span class="n">grouped_review_idx</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>age
7 43
10 31
11 27
13 497
14 264
15 397
16 335
17 897
18 2219
19 3514
Name: idx, dtype: int64
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[267]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre>0-10 43
10-20 8181
20-30 39535
30-40 25696
40-50 15021
50-60 8704
60-70 2623
70-80 197
Name: idx, dtype: int64</pre>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [268]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Let's plot our number of votes - we can see that most people voting are 20-30</span>
<span class="n">grouped_review_idx</span><span class="o">.</span><span class="n">plot</span><span class="p">(</span><span class="n">kind</span><span class="o">=</span><span class="s1">'pie'</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">),</span> <span class="n">legend</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autopct</span><span class="o">=</span><span class="s1">'</span><span class="si">%1.1f%%</span><span class="s1">'</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">10</span> <span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[268]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x21579610></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
AVJo9YhKtkGBAAAAAElFTkSuQmCC
"
class="
"
>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [269]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># We can also see the same (more or less) info directly from the</span>
<span class="c1"># initial dataset, using a histogram</span>
<span class="n">final_lens</span><span class="p">[</span><span class="s1">'age'</span><span class="p">]</span><span class="o">.</span><span class="n">hist</span><span class="p">(</span><span class="n">bins</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[269]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x21554050></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
TkSuQmCC
"
class="
"
>
</div>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [270]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="c1"># Or just by plotting the num of reviews / age (not the grouped one)</span>
<span class="n">review_idx_by_age</span><span class="o">.</span><span class="n">plot</span><span class="p">()</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[270]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre><matplotlib.axes._subplots.AxesSubplot at 0x2172d250></pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
rkJggg==
"
class="
"
>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Let’s see how people of each occupation and sex are voting. We’ll get the averages for age, and rating and total number of reviews:</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [271]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">aggrs</span> <span class="o">=</span> <span class="p">{</span><span class="s1">'age'</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">average</span><span class="p">,</span> <span class="s1">'idx'</span><span class="p">:</span> <span class="nb">len</span><span class="p">,</span> <span class="s1">'rating'</span> <span class="p">:</span><span class="n">np</span><span class="o">.</span><span class="n">average</span><span class="p">}</span>
<span class="c1"># This creeates a dataframe for men and women</span>
<span class="n">d1</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s1">'occupation'</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s1">'sex'</span><span class="p">,</span> <span class="n">aggfunc</span><span class="o">=</span><span class="n">aggrs</span><span class="p">)</span>
<span class="c1"># This creates a dataframe for both</span>
<span class="n">d2</span> <span class="o">=</span> <span class="n">final_lens</span><span class="o">.</span><span class="n">pivot_table</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="s1">'occupation'</span><span class="p">,</span> <span class="n">aggfunc</span><span class="o">=</span><span class="n">aggrs</span><span class="p">)</span>
<span class="c1"># Let's put the values from the "both" dataframe to the men/women dataframe</span>
<span class="n">d1</span><span class="p">[</span> <span class="s1">'idx'</span><span class="p">,</span><span class="s1">'Total'</span> <span class="p">]</span> <span class="o">=</span> <span class="n">d2</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">]</span>
<span class="n">d1</span><span class="p">[</span> <span class="s1">'age'</span><span class="p">,</span><span class="s1">'Total'</span><span class="p">]</span> <span class="o">=</span> <span class="n">d2</span><span class="p">[</span><span class="s1">'age'</span><span class="p">]</span>
<span class="n">d1</span><span class="p">[</span> <span class="s1">'rating'</span><span class="p">,</span><span class="s1">'Total'</span><span class="p">]</span> <span class="o">=</span> <span class="n">d2</span><span class="p">[</span><span class="s1">'rating'</span><span class="p">]</span>
<span class="c1"># And now let's sort the row index so that it will have the correct multiindex</span>
<span class="n">occupations</span> <span class="o">=</span> <span class="n">d1</span><span class="o">.</span><span class="n">sort_index</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># Finally, let's sort the DataFrame by the total number of votes for each occupation</span>
<span class="n">occupations</span> <span class="o">=</span> <span class="n">occupations</span><span class="o">.</span><span class="n">sort_values</span><span class="p">(</span> <span class="p">(</span><span class="s1">'idx'</span><span class="p">,</span> <span class="s1">'Total'</span><span class="p">),</span> <span class="n">ascending</span><span class="o">=</span><span class="kc">False</span> <span class="p">)</span>
<span class="n">occupations</span>
<span class="c1"># Students are no1 - not a surpise!</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[271]:</div>
<div class="jp-RenderedHTMLCommon jp-RenderedHTML jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/html">
<div>
<table border="1" class="dataframe">
<thead>
<tr>
<th></th>
<th colspan="3" halign="left">age</th>
<th colspan="3" halign="left">idx</th>
<th colspan="3" halign="left">rating</th>
</tr>
<tr>
<th>sex</th>
<th>F</th>
<th>M</th>
<th>Total</th>
<th>F</th>
<th>M</th>
<th>Total</th>
<th>F</th>
<th>M</th>
<th>Total</th>
</tr>
<tr>
<th>occupation</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<th>student</th>
<td>21.092697</td>
<td>22.510731</td>
<td>22.142870</td>
<td>5696.0</td>
<td>16261.0</td>
<td>21957</td>
<td>3.602879</td>
<td>3.484411</td>
<td>3.515143</td>
</tr>
<tr>
<th>other</th>
<td>31.813370</td>
<td>32.964704</td>
<td>32.568977</td>
<td>3665.0</td>
<td>6998.0</td>
<td>10663</td>
<td>3.531241</td>
<td>3.563447</td>
<td>3.552377</td>
</tr>
<tr>
<th>educator</th>
<td>37.942058</td>
<td>44.570167</td>
<td>42.789240</td>
<td>2537.0</td>
<td>6905.0</td>
<td>9442</td>
<td>3.698857</td>
<td>3.660246</td>
<td>3.670621</td>
</tr>
<tr>
<th>engineer</th>
<td>33.489655</td>
<td>34.371731</td>
<td>34.356086</td>
<td>145.0</td>
<td>8030.0</td>
<td>8175</td>
<td>3.751724</td>
<td>3.537609</td>
<td>3.541407</td>
</tr>
<tr>
<th>programmer</th>
<td>32.463007</td>
<td>32.502167</td>
<td>32.500064</td>
<td>419.0</td>
<td>7382.0</td>
<td>7801</td>
<td>3.577566</td>
<td>3.567732</td>
<td>3.568260</td>
</tr>
<tr>
<th>administrator</th>
<td>38.096081</td>
<td>39.688083</td>
<td>39.123145</td>
<td>2654.0</td>
<td>4825.0</td>
<td>7479</td>
<td>3.781839</td>
<td>3.555233</td>
<td>3.635646</td>
</tr>
<tr>
<th>writer</th>
<td>37.429848</td>
<td>32.219527</td>
<td>34.325867</td>
<td>2238.0</td>
<td>3298.0</td>
<td>5536</td>
<td>3.663986</td>
<td>3.180109</td>
<td>3.375723</td>
</tr>
<tr>
<th>librarian</th>
<td>36.707343</td>
<td>38.129300</td>
<td>37.358050</td>
<td>2860.0</td>
<td>2413.0</td>
<td>5273</td>
<td>3.580070</td>
<td>3.537920</td>
<td>3.560781</td>
</tr>
<tr>
<th>technician</th>
<td>38.000000</td>
<td>31.512655</td>
<td>31.712493</td>
<td>108.0</td>
<td>3398.0</td>
<td>3506</td>
<td>3.268519</td>
<td>3.540612</td>
<td>3.532230</td>
</tr>
<tr>
<th>executive</th>
<td>42.529412</td>
<td>36.203331</td>
<td>36.614164</td>
<td>221.0</td>
<td>3182.0</td>
<td>3403</td>
<td>3.773756</td>
<td>3.319610</td>
<td>3.349104</td>
</tr>
<tr>
<th>healthcare</th>
<td>37.644993</td>
<td>44.641851</td>
<td>38.885164</td>
<td>2307.0</td>
<td>497.0</td>
<td>2804</td>
<td>2.736021</td>
<td>3.639839</td>
<td>2.896220</td>
</tr>
<tr>
<th>artist</th>
<td>27.155510</td>
<td>33.088257</td>
<td>30.592288</td>
<td>971.0</td>
<td>1337.0</td>
<td>2308</td>
<td>3.347065</td>
<td>3.875841</td>
<td>3.653380</td>
</tr>
<tr>
<th>entertainment</th>
<td>27.546667</td>
<td>28.912834</td>
<td>28.766110</td>
<td>225.0</td>
<td>1870.0</td>
<td>2095</td>
<td>3.448889</td>
<td>3.440107</td>
<td>3.441050</td>
</tr>
<tr>
<th>scientist</th>
<td>28.273381</td>
<td>35.855133</td>
<td>35.343052</td>
<td>139.0</td>
<td>1919.0</td>
<td>2058</td>
<td>3.251799</td>
<td>3.637311</td>
<td>3.611273</td>
</tr>
<tr>
<th>marketing</th>
<td>32.106335</td>
<td>37.759947</td>
<td>36.478462</td>
<td>442.0</td>
<td>1508.0</td>
<td>1950</td>
<td>3.522624</td>
<td>3.474801</td>
<td>3.485641</td>
</tr>
<tr>
<th>retired</th>
<td>70.000000</td>
<td>61.375163</td>
<td>61.755749</td>
<td>71.0</td>
<td>1538.0</td>
<td>1609</td>
<td>3.239437</td>
<td>3.477243</td>
<td>3.466750</td>
</tr>
<tr>
<th>lawyer</th>
<td>36.000000</td>
<td>34.478056</td>
<td>34.556134</td>
<td>69.0</td>
<td>1276.0</td>
<td>1345</td>
<td>3.623188</td>
<td>3.741379</td>
<td>3.735316</td>
</tr>
<tr>
<th>none</th>
<td>33.887671</td>
<td>18.960821</td>
<td>25.007769</td>
<td>365.0</td>
<td>536.0</td>
<td>901</td>
<td>3.632877</td>
<td>3.878731</td>
<td>3.779134</td>
</tr>
<tr>
<th>salesman</th>
<td>31.318584</td>
<td>34.882012</td>
<td>33.470794</td>
<td>339.0</td>
<td>517.0</td>
<td>856</td>
<td>3.870206</td>
<td>3.394584</td>
<td>3.582944</td>
</tr>
<tr>
<th>doctor</th>
<td>NaN</td>
<td>35.592593</td>
<td>35.592593</td>
<td>NaN</td>
<td>540.0</td>
<td>540</td>
<td>NaN</td>
<td>3.688889</td>
<td>3.688889</td>
</tr>
<tr>
<th>homemaker</th>
<td>33.416357</td>
<td>23.000000</td>
<td>32.371237</td>
<td>269.0</td>
<td>30.0</td>
<td>299</td>
<td>3.278810</td>
<td>3.500000</td>
<td>3.301003</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<p>Let’s manipulate the previous DataFrame to take a look at only the occupations that have voted the most number of times. Actually, we’ll get only occupations that have voted more than 6000 times, all other occupations we’ll just add them to the “other” occupation. For this, we’ll get only the <code>'idx'</code> column and filter it by the rows which have a <code>Total<6000</code>. We’ll then take the sum for this DataFrame so that we’ll get the total votes for each male/female and total.</p>
<p>Next, we’ll add this to the “other” row of the dataframe and remove the less than 6000 rows from it. Finally, we’ll plot the resulting DataFrame for all male, female and both.</p>
</div>
</div>
</div>
</div><div class="jp-Cell jp-CodeCell jp-Notebook-cell ">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputPrompt jp-InputArea-prompt">In [272]:</div>
<div class="jp-CodeMirrorEditor jp-Editor jp-InputArea-editor" data-type="inline">
<div class="CodeMirror cm-s-jupyter">
<div class=" highlight hl-ipython2"><pre><span></span><span class="n">occupations_num</span> <span class="o">=</span> <span class="n">occupations</span><span class="p">[</span><span class="s1">'idx'</span><span class="p">]</span>
<span class="c1"># Let's see which are the total numbers we need to add to "other"</span>
<span class="n">add_to_other</span> <span class="o">=</span> <span class="n">occupations_num</span><span class="p">[</span><span class="n">occupations_num</span><span class="p">[</span><span class="s1">'Total'</span><span class="p">]</span><span class="o"><</span><span class="mi">6000</span><span class="p">]</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span>
<span class="nb">print</span> <span class="n">add_to_other</span>
<span class="n">occupations_num</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'other'</span><span class="p">]</span><span class="o">+=</span><span class="n">add_to_other</span>
<span class="nb">print</span> <span class="n">occupations_num</span><span class="o">.</span><span class="n">loc</span><span class="p">[</span><span class="s1">'other'</span><span class="p">]</span>
<span class="c1"># Now let's get the rows that have a total of > 6000</span>
<span class="n">most_voters_with_other</span> <span class="o">=</span> <span class="n">occupations_num</span><span class="p">[</span><span class="n">occupations_num</span><span class="p">[</span><span class="s1">'Total'</span><span class="p">]</span><span class="o">></span><span class="mi">6000</span><span class="p">]</span>
<span class="nb">print</span> <span class="n">most_voters_with_other</span>
<span class="n">most_voters_with_other</span><span class="o">.</span><span class="n">plot</span><span class="p">(</span><span class="n">kind</span><span class="o">=</span><span class="s1">'pie'</span><span class="p">,</span> <span class="n">subplots</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">18</span><span class="p">,</span><span class="mi">5</span> <span class="p">),</span> <span class="n">legend</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autopct</span><span class="o">=</span><span class="s1">'</span><span class="si">%1.1f%%</span><span class="s1">'</span><span class="p">,</span> <span class="n">fontsize</span><span class="o">=</span><span class="mi">12</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
</div>
<div class="jp-Cell-outputWrapper">
<div class="jp-Collapser jp-OutputCollapser jp-Cell-outputCollapser">
</div>
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedText jp-OutputArea-output" data-mime-type="text/plain">
<pre>sex
F 10624.0
M 23859.0
Total 34483.0
dtype: float64
sex
F 14289.0
M 30857.0
Total 45146.0
Name: other, dtype: float64
sex F M Total
occupation
student 5696.0 16261.0 21957.0
other 14289.0 30857.0 45146.0
educator 2537.0 6905.0 9442.0
engineer 145.0 8030.0 8175.0
programmer 419.0 7382.0 7801.0
administrator 2654.0 4825.0 7479.0
</pre>
</div>
</div>
<div class="jp-OutputArea-child jp-OutputArea-executeResult">
<div class="jp-OutputPrompt jp-OutputArea-prompt">Out[272]:</div>
<div class="jp-RenderedText jp-OutputArea-output jp-OutputArea-executeResult" data-mime-type="text/plain">
<pre>array([<matplotlib.axes._subplots.AxesSubplot object at 0x2179D090>,
<matplotlib.axes._subplots.AxesSubplot object at 0x18CFAC10>,
<matplotlib.axes._subplots.AxesSubplot object at 0x18D0E0D0>], dtype=object)</pre>
</div>
</div>
<div class="jp-OutputArea-child">
<div class="jp-OutputPrompt jp-OutputArea-prompt"></div>
<div class="jp-RenderedImage jp-OutputArea-output ">
<img src="data:image/png;base64,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
YL/NHLMSuI9QEvewu+fM7EHeyz7Z3PW6Mp47vx1v5G3XNd5D3H0sYbwPkYJRKY2IiIiUGs1eINK3
dXUNjQO+Z2Y7m9mewDc67DsLOMfMEmb2OWBU3rZbgQvNbLQFQ+K4QJVxWRuDIicSxhZqtxr4oJnl
D7R5N3C1mQ00s4HADwileyLSBykwIiIiIiVFsxeI9G1buIauJZSwLCaMI3FHh92vIAxuvJ4w7tCD
ecd9DrgQuBHYAEwFhrp7E3A5cG+8ns8GHs7bbz7wF2BRnA1nMPBj4HlCuc3s+PP13fU7EJHSYmHc
MhEREZG+J85e8Ft3v73YbREREZHSpIwRERER6TM0e4GIiIhsKw2+KiIiIn2JZi8QERGRbaJSGhER
ERERERHpt1RKIyIiIiIiIiL9lgIjIiIiIiIiItJvKTAiIiIiIiIiIv2WAiMiIiIiIiIi0m8pMCIi
IiIiIiIi/ZYCIyIiIiIiIiLSbykwIiIiIiIiIiL9lgIjIiIiIiIiItJvKTAiIiIiIiIiIv2WAiMi
IiIiIiIi0m8pMCIiIiIiIiIi/ZYCIyIiIiIiIiLSbykwIiIiIiIiIiL9lgIjIiIiIiIiItJvKTAi
IiIiIiIiIv2WAiMiIiIiIiIi0m8pMCIiIiIiIiIi/ZYCIyIiIiIiIiLSb/0/t/XlCX2AthkAAAAA
SUVORK5CYII=
"
class="
"
>
</div>
</div>
</div>
</div>
</div>
<div class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea"><div class="jp-InputPrompt jp-InputArea-prompt">
</div><div class="jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput " data-mime-type="text/markdown">
<h1 id="Conclusion">Conclusion<a class="anchor-link" href="#Conclusion">¶</a></h1><p>In the previous, I have tried to present a comprehensive introduction of the <code>pivot_table</code> command along with a small introduction to the pandas data structures (<code>Series</code> and <code>DataFrame</code>) and a bunch of other methods that will help in using <code>pivot_table</code> presenting both toy and real world examples for all of them. If you feel that something is missing or you want me to add another example pivot_table operation for either the books or the movie lens dataset feel free to tell me in the comments.</p>
<p>I hope that after reading (and understanding) the above you’ll be able to use pandas and <code>pivot_table</code> without problems!</p>
</div>
</div>
</div>
</div>
</body>
</html>Splitting a query into individual fields in Django2016-09-12T16:20:00+03:002016-09-12T16:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2016-09-12:/2016/09/12/django-split-query/<p class="first last">How to split a query containing spaces into individual model fields</p>
<p>As you should have already seen in <a class="reference external" href="https://spapas.github.io/2015/10/05/django-dynamic-tables-similar-models/">previous articles</a>, I really like using <a class="reference external" href="https://github.com/alex/django-filter">django-filter</a> since it covers (nearly) all my queryset filtering needs.
With django-filter, you define a bunch of fields and it will automatically create inputs for each one of these fields so that you can filter by each one of these fields individually or a
combination of them.</p>
<p>However, one thing that django-filter (and django in generally) lacks is the ability to <em>filter multiple fields using a single input</em>. This functionality may be familiar to some readers from
the <a class="reference external" href="https://datatables.net/">datatable</a> jquery plugin. If you take a look at the example in the datatable homepage, you’ll see a single “Search” field. What is really great is that you can enter multiple values (seperated
by spaces) into that field and it will filter the individual table values by each one of them. For example, if you enter “2011 Engineer” you’ll see all engineering positions that started
on 2011. If you append “Singapore” (so you’ll have “2011 Engineer Singapore”) you’ll also get only the corresponding results!</p>
<p>This functionality is really useful and is very important to have if you use single-input fields to query your data. One such example is if you use autocompletes, for example with
django-autocomplete-light: You’ll have a single input however you may need to filter on more than one field to find out your selection.</p>
<p>In the following ost I’ll show you how to implement this functionality using Django and django-filters (actually django-filters will be used to provide the form) - to see it
in action you may use the <a class="reference external" href="https://github.com/spapas/django_table_filtering">https://github.com/spapas/django_table_filtering</a> repository (check out the <cite>/filter_ex/</cite> view).</p>
<p>I won’t go into detail on how the code is structured (it’s really simple) and I’ll go directly to the filter I am using. Instead of using a filter you can of course directly query on
your view. What you actually need is:</p>
<blockquote>
<ul class="simple">
<li>a queryset with the instances you want to search</li>
<li>a text value with the query (that may contain spaces)</li>
<li>a list of the names of the fields you want to search</li>
</ul>
</blockquote>
<p>In my case, I am using a <cite>Book</cite> model that has the following fields: <cite>id, title, author, category</cite>. I have created a filter with a single field named <cite>ex</cite> that will filter on
all these fields. So you should be able to enter “King It” and find “It by Stephen King”. Let’s see how the filter is implemented:</p>
<pre class="code literal-block">
import itertools
class BookFilterEx(django_filters.FilterSet):
ex = django_filters.MethodFilter()
search_fields = ['title', 'author', 'category', 'id', ]
def filter_ex(self, qs, value):
if value:
q_parts = value.split()
# Permutation code copied from http://stackoverflow.com/a/12935562/119071
list1=self.search_fields
list2=q_parts
perms = [zip(x,list2) for x in itertools.permutations(list1,len(list2))]
q_totals = Q()
for perm in perms:
q_part = Q()
for p in perm:
q_part = q_part & Q(**{p[0]+'__icontains': p[1]})
q_totals = q_totals | q_part
qs = qs.filter(q_totals)
return qs
class Meta:
model = books.models.Book
fields = ['ex']
</pre>
<p>The meat of this code is in the <tt class="docutils literal">filter_ex</tt> method, let’s analyze it line by line:
First of all, we split the value to its corresponding parts using the whitespace to sperate into individual tokens. For example if the user has entered
<tt class="docutils literal">King It</tt>, <tt class="docutils literal">q_parts</tt> be equal to <tt class="docutils literal">['King', 'It']</tt>. As you can see the <cite>search_fields</cite> attribute contains the names of the
fields we want to search. The first thing I like to do is to generate all possible combinations between <tt class="docutils literal">q_parts</tt>
and <tt class="docutils literal">search_fields</tt>, I’ve copied the list combination code from <a class="reference external" href="http://stackoverflow.com/a/12935562/119071">http://stackoverflow.com/a/12935562/119071</a> and it is the line
<tt class="docutils literal">perms = [zip(x,list2) for x in <span class="pre">itertools.permutations(list1,len(list2))]</span></tt>.</p>
<p>The <tt class="docutils literal">itertools.permutations(list1,len(list2))</tt> will generate all permutations of list1
that have length equal to the length of list2. I.e if list2 is <tt class="docutils literal">['King', 'It']</tt> (len=2) then it will generate all combinations
of search_fields with length=2, i.e it will generate the following list of tuples:</p>
<pre class="code literal-block">
[
('title', 'author'), ('title', 'category'), ('title', 'id'), ('author', 'title'),
('author', 'category'), ('author', 'id'), ('category', 'title'), ('category', 'author'),
('category', 'id'), ('id', 'title'), ('id', 'author'), ('id', 'category')
]
</pre>
<p>Now, the <tt class="docutils literal">zip</tt> will combine the elements of each one of these tuples with the elements of <tt class="docutils literal">list2</tt>, so, in our example
(<tt class="docutils literal"><span class="pre">list2=['King',</span> 'It']</tt>) <tt class="docutils literal">perms</tt> will be the following array:</p>
<pre class="code literal-block">
[
[('title', 'King'), ('author', 'It')],
[('title', 'King'), ('category', 'It')],
[('title', 'King'), ('id', 'It')],
[('author', 'King'), ('title', 'It')],
[('author', 'King'), ('category', 'It')],
[('author', 'King'), ('id', 'It')],
[('category', 'King'), ('title', 'It')],
[('category', 'King'), ('author', 'It')],
[('category', 'King'), ('id', 'It')],
[('id', 'King'), ('title', 'It')],
[('id', 'King'), ('author', 'It')],
[('id', 'King'), ('category', 'It')]
]
</pre>
<p>Notice that <tt class="docutils literal">itertools.permutations(list1,len(list2))</tt> will return an empty list if <tt class="docutils literal">len(list2) > len(list1)</tt> - this is actually what
we want since that means that the user entered more query parts than the available fields, i.e we can’t match each one of the
possible values after we split the input with a search field so we should return nothing.</p>
<p>Now, what I want is to create a single query that will combine the tuples in each of these combinations by <span class="caps">AND</span> (i.e
<tt class="docutils literal"><span class="pre">title==King</span> <span class="caps">AND</span> <span class="pre">author==It</span></tt> ) and then combine all these subqueries using <span class="caps">OR</span> (i.e
“ (title==King <span class="caps">AND</span> author==It) <span class="caps">OR</span> (title==King <span class="caps">AND</span> category==It) <span class="caps">OR</span> (title==King <span class="caps">AND</span> id==It) <span class="caps">OR</span> …“.</p>
<p>This could of course be implemented with a raw sql query however we could use some interesting django tricks for this.
I’ve already done something similar to <a class="reference external" href="https://spapas.github.io/2015/10/05/django-dynamic-tables-similar-models/">a previous article</a> so I won’t go
into much detail explaining the code that creates the <tt class="docutils literal">q_totals</tt> <tt class="docutils literal">Q</tt> object. What it does is that it create a big django <tt class="docutils literal">Q</tt> object that combines using <span class="caps">AND</span> (<tt class="docutils literal">&</tt>)
all individual <tt class="docutils literal">q_part</tt> objects. Each <tt class="docutils literal">q_part</tt> object combines using <span class="caps">OR</span> (<tt class="docutils literal">|</tt>) the individual combinations of field name
and value — I’ve used <cite>__icontains`</cite> to create the query. So the result will be something like this:</p>
<pre class="code literal-block">
q_totals =
Q(title__icontains='King') & Q(author__icontains='It')
|
Q(title__icontains='King') & Q(category__icontains='It')
|
Q(title__icontains='King') & Q(id__icontains='It')
|
Q(author__icontains='King') & Q(title__icontains='It')
...
</pre>
<p>Filtering by this <tt class="docutils literal">q_totals</tt> will return the correct values!</p>
<p>One extra complication we should be aware of is what happens if the user needs to also search for books with multiple words
in their titles. For example, if the user enters “Under the Dome King” or “It Stephen King” or even “The Stand Stephen King”
we won’t get any results :(</p>
<p>To fix this, we need to get all possible combinations of sequential substrings, i.e for “Under the Dome King”, after we
split it to [‘Under’, ‘the’, ‘Dome’, ‘King’] we’ll need the following combinations:</p>
<pre class="code literal-block">
[
['Under', 'the', 'Dome', 'King'],
['Under', 'the', 'Dome King'],
['Under', 'the Dome', 'King'],
['Under', 'the Dome King'],
['Under the', 'Dome', 'King'],
['Under the', 'Dome King'],
['Under the Dome', 'King'],
['Under the Dome King']
]
</pre>
<p>A possible solution for that problem can be found on this <span class="caps">SO</span> answer: <a class="reference external" href="http://stackoverflow.com/a/27263616/119071">http://stackoverflow.com/a/27263616/119071</a>.</p>
<p>Now, to extend our solution to include this, we’d need to actually search for each one of the above possiblities
and combine again the results with <span class="caps">OR</span>, something like this:</p>
<pre class="code literal-block">
def filter_ex(self, qs, value):
if value:
q_parts = value.split()
# Use a global q_totals
q_totals = Q()
# This part will get us all possible segmantiation of the query parts and put it in the possibilities list
combinatorics = itertools.product([True, False], repeat=len(q_parts) - 1)
possibilities = []
for combination in combinatorics:
i = 0
one_such_combination = [q_parts[i]]
for slab in combination:
i += 1
if not slab: # there is a join
one_such_combination[-1] += ' ' + q_parts[i]
else:
one_such_combination += [q_parts[i]]
possibilities.append(one_such_combination)
# Now, for all possiblities we'll append all the Q objects using OR
for p in possibilities:
list1=self.search_fields
list2=p
perms = [zip(x,list2) for x in itertools.permutations(list1,len(list2))]
for perm in perms:
q_part = Q()
for p in perm:
q_part = q_part & Q(**{p[0]+'__icontains': p[1]})
q_totals = q_totals | q_part
qs = qs.filter(q_totals)
return qs
</pre>
<p>The previous filtering code works fine with querise like “The Stand” or “Under the Dome Stephen King”!</p>
<p>One thing that you must be careful is that this code will create <em>very complicated and big</em> queries. For example,
searching for “Under the Dome Stephen King” will result to <cite>q_totals</cite> getting this monster value:</p>
<pre class="code literal-block">
(OR:
(AND: ),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the'), ('category__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the'), ('id__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the'), ('author__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the'), ('id__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the'), ('author__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the'), ('category__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the'), ('category__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the'), ('id__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the'), ('title__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the'), ('id__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the'), ('title__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the'), ('category__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the'), ('author__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the'), ('id__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the'), ('title__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the'), ('id__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the'), ('title__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the'), ('author__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the'), ('author__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the'), ('category__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the'), ('title__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the'), ('category__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the'), ('title__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the'), ('author__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the'), ('category__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the'), ('id__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the'), ('author__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the'), ('id__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the'), ('author__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the'), ('category__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the'), ('category__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the'), ('id__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the'), ('title__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the'), ('id__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the'), 'title__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the'), ('category__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the'), ('author__icontains', u'Dome Stephen'),('id__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the'), ('id__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the'), ('title__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the'), ('id__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the'), ('title__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the'), ('author__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the'), ('author__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the'), ('category__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the'), ('title__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the'), ('category__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the'), ('title__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the'), ('author__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome'), ('category__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome'), ('id__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome'), ('author__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome'), ('id__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome'), ('author__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome'), ('category__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome'), ('category__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome'), ('id__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome'), ('title__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome'), ('id__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome'), ('title__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome'), ('category__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome'), ('author__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome'), ('id__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome'), ('title__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome'), ('id__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome'), ('title__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome'), ('author__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome'), ('author__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome'), ('category__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome'), ('title__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome'), ('category__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome'), ('title__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome'), ('author__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under'), ('author__icontains', u'the Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('category__icontains', u'the Dome Stephen King')),
(AND: ('title__icontains', u'Under'), ('id__icontains', u'the Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('title__icontains', u'the Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('category__icontains', u'the Dome Stephen King')),
(AND: ('author__icontains', u'Under'), ('id__icontains', u'the Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('title__icontains', u'the Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('author__icontains', u'the Dome Stephen King')),
(AND: ('category__icontains', u'Under'), ('id__icontains', u'the Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('title__icontains', u'the Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('author__icontains', u'the Dome Stephen King')),
(AND: ('id__icontains', u'Under'), ('category__icontains', u'the Dome Stephen King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome'), ('category__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome'), ('id__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome'), ('author__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome'), ('id__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome'), ('author__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome'), ('category__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome'), ('category__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome'), ('id__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome'), ('title__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome'), ('id__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome'), ('title__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome'), ('category__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome'), ('author__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome'), ('id__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome'), ('title__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome'), ('id__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome'), ('title__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome'), ('author__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome'), ('author__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome'), ('category__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome'), ('title__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome'), ('category__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome'), ('title__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome'), ('author__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome'), ('category__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('author__icontains', u'Under the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('category__icontains', u'Under the'), ('id__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under the'), ('title__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under the'), ('author__icontains', u'Dome Stephen King')),
(AND: ('id__icontains', u'Under the'), ('category__icontains', u'Dome Stephen King')),
(AND: ('title__icontains', u'Under the Dome'), ('author__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome'), ('author__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome'), ('category__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome'), ('category__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome'), ('id__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome'), ('id__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome'), ('title__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome'), ('title__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome'), ('category__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome'), ('category__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome'), ('id__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome'), ('id__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome'), ('title__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome'), ('title__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome'), ('author__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome'), ('author__icontains', u'Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome'), ('id__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome'), ('id__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome'), ('title__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome'), ('title__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome'), ('author__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome'), ('author__icontains', u'Stephen'), ('category__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome'), ('category__icontains', u'Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome'), ('category__icontains', u'Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('author__icontains', u'Under the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('category__icontains', u'Under the Dome'), ('id__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the Dome'), ('title__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the Dome'), ('author__icontains', u'Stephen King')),
(AND: ('id__icontains', u'Under the Dome'), ('category__icontains', u'Stephen King')),
(AND: ('title__icontains', u'Under the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('title__icontains', u'Under the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome Stephen'), ('category__icontains', u'King')),
(AND: ('author__icontains', u'Under the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('category__icontains', u'Under the Dome Stephen'), ('id__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome Stephen'), ('title__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome Stephen'), ('author__icontains', u'King')),
(AND: ('id__icontains', u'Under the Dome Stephen'), ('category__icontains', u'King')),
('title__icontains', u'Under the Dome Stephen King'),
('author__icontains', u'Under the Dome Stephen King'),
('category__icontains', u'Under the Dome Stephen King'),
('id__icontains', u'Under the Dome Stephen King')
)
</pre>
<p>This query has around 200 different <span class="caps">OR</span> parts!!! So please be careful on the amount of search fields you’ll enable to works
with this method or your database will really struggle!</p>
How to download all images of an imgur album2016-06-27T14:20:00+03:002016-06-27T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2016-06-27:/2016/06/27/download-imgur-album-images/<p class="first last">How to download all images of an imgur album</p>
<p>Recently I stubmled upon a great <a class="reference external" href="http://imgur.com/">imgur</a> album that contained
<a class="reference external" href="http://imgur.com/a/aoi3T">379 movie stills that could be used for desktop background</a>. I
really liked the idea and wanted to download all the images in order
to put them in a folder and use them as a slideshow for my Windows
desktop background.</p>
<p>Downloading them one by one would be considered penal
labour so I tried to find out an automatic way to get them all. With some
research in google, I found out an old post with the hint that by appending <tt class="docutils literal">/zip</tt>
to the <span class="caps">URL</span> you could get a zip with all the images — this didn’t work for me. I
also tried various browser tools for scrapping or downloading all images from a page but they
didn’t work also (they could only download a small number of the images and not all).</p>
<p>This seemed strange to me until I understood how imgur loads its images by “inspecting”
an image and taking a look at the page’s <span class="caps">DOM</span> structure through the console:</p>
<img alt="How imgur loads images" src="/images/imgur.gif" style="width: 700px;" />
<p>As we can see, the imgur client-side code has a component with a <tt class="docutils literal"><span class="pre">post-images</span></tt> class
that contains the visible images (and thoese that are above/below the visible images). When
the user scrolls up/down the contents of <tt class="docutils literal"><span class="pre">post-images</span></tt> will be changed accordingly
(notice how the component with <tt class="docutils literal">id=EKMGEPc</tt> moves down when I scroll up).
What this means is that each time there are 3-4 images (this actually depends on your window size)
under <tt class="docutils literal"><span class="pre">post-images</span></tt> that are changed when you scroll — that’s why downloaders / scrappers are not working (since these
tools just inspect the <span class="caps">DOM</span> they only see these 3-4 images to download).</p>
<p>Another interesting observation is that if you take a look at the network tab when you
scroll app down you won’t see any ajax calls (the only network calls are the images that
are downloaded when they are appended to the <span class="caps">DOM</span>). So this means that somewhere there’s
an array that is loaded when the page is loaded and contains all the images of the album.
If we can access this array then we’d be able to get all the URLs of the images…</p>
<p>From a quick look at the <span class="caps">DOM</span> structure we can understand that this is a React application
(components have a <tt class="docutils literal"><span class="pre">data-reactid</span></tt> attribute). So I tried the <a class="reference external" href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi">React Developer Tools</a>
extension to see if I could find anything insteresting. Here’s the output:</p>
<img alt="Imgur - react dev tools" src="/images/imgur-react.png" style="width: 1000px;" />
<p>As you can see, there seem to be 4 top-level react elements — the interesting one is <tt class="docutils literal">GalleryPost</tt>.
If you take a look at its props (in the right hand side of the react-devtools) you’ll see that it has
an <tt class="docutils literal">album_image_store</tt> property which also seems interesting (it should be the image store for this
album). After searching a bit its attributes
you’ll see that it has a <tt class="docutils literal">_</tt> child attribute, which has a <tt class="docutils literal">posts</tt> child attribute which has an <tt class="docutils literal">aoi3T</tt>
attribute (notice that this is similar to the <span class="caps">URL</span> id of the album) and, finally this has an <tt class="docutils literal">images</tt>
attribute with objects describing all the images of that album \o/!</p>
<p>Now we need to get our hands on that <tt class="docutils literal">images</tt> array contents. Unfortunately, right clicking doesn’t seem
to do anything from react-dev-tools and there doesn’t seem a way to copy data from that panel… However, in
the upper right position of that window you’ll see the hint <tt class="docutils literal">($r in the console)</tt> which means that
the selected react component is available as $r in the normal javascript console - so by entering</p>
<pre class="code literal-block">
copy($r.props.album_image_store._.posts.aoi3T.images)
</pre>
<p>I was able to copy the images of the album to my clipboard (please notice that <tt class="docutils literal">$r</tt> will have the
value of the selected react component so, before trying it you must select the <tt class="docutils literal">GalleryPost</tt>
component in the react-dev-tools tab)!</p>
<p>I dumped this to a file to take a look at it - it is really easy to interpret it:</p>
<pre class="code literal-block">
[
{
"hash": "MQplfkV",
"title": "2001: A Space Odyssey",
"description": "Cinematographer: Geoffrey Unsworth\n\nsource:\nhttp://www.filmcaptures.com/2001-a-space-odyssey/",
"width": 1920,
"height": 864,
"size": 2262862,
"ext": ".png",
"animated": false,
"prefer_video": false,
"looping": false,
"datetime": "2014-10-25 04:02:58",
"thumbsize": "g",
"minHeight": 306,
"shown": true,
"containerHeight": 501
},
..
</pre>
<p>The imgur images have a <span class="caps">URL</span> of http//i.imgur.com/{hash}{ext} so, we
can use the following small python 2 program to download all images from that album:</p>
<pre class="code literal-block">
import requests
import json
from slugify import slugify
# Modified from http://stackoverflow.com/a/16696317/119071
def download_file(url, local_filename):
r = requests.get(url, stream=True)
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
#f.flush() commented by recommendation from J.F.Sebastian
return local_filename
if __name__ == '__main__':
for i, jo in enumerate(json.loads(open("album.txt").read())):
filename = '{0}-{1}{2}'.format(slugify(jo['title']), i+1, jo['ext'])
url = 'http://i.imgur.com/{0}{1}'.format(jo['hash'].strip(), jo['ext'])
print filename, url
download_file(url, filename)
</pre>
<p>Notice that the above uses the <tt class="docutils literal">requests</tt> library to retrieve the files and the
<tt class="docutils literal"><span class="pre">python-slugify</span></tt> library to generate a filename using the image title so these
libraries must be installed by using <tt class="docutils literal">pip install requests <span class="pre">python-slugify</span></tt>. This
will read a file named <tt class="docutils literal">album.txt</tt> that should contain the copied imgur album images
in the same directory and download all the images.</p>
<p><strong>Disclaimer</strong> The above methodology works today (27-06-2016) - probably it will
stop working sometime in the future, when imgur changes its image loading algorithm
or its image object representation.
Also, I haven’t been able to find a way to quickly access the <tt class="docutils literal">GalleryPost</tt> react
component from the javascript console - you need to install the react dev tools and
select that component from there so that you’ll have the <tt class="docutils literal">$r</tt> reference to it in
the javascript console. Finally, don’t forget to change the
<tt class="docutils literal"><span class="pre">copy($r.props.album_image_store._.posts.aoi3T.images)</span></tt>
depending on your album id (also if the id is not a valid identifier, for example it
starts with number, use <tt class="docutils literal"><span class="pre">copy($r.props.album_image_store._.posts['aoi3T'].images</span></tt>).</p>
Using Werkzeug debugger with Django2016-06-07T10:40:00+03:002016-06-07T10:40:00+03:00Serafeim Papastefanostag:spapas.github.io,2016-06-07:/2016/06/07/django-werkzeug-debugger/<p class="first last">How to use the Werkzeug interactive debugger with Django to improve your development workflow</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p><a class="reference external" href="http://werkzeug.pocoo.org/">Werkzeug</a> is a <span class="caps">WSGI</span> utility library for Python. Beyond others, it includes an interactive
debugger - what this means is that when your python application throws an exception,
Werkzeug will display the exception stacktrace in the browser (that’s not a big deal)
and allow you to write python commands interactively wherever you want in that stacktrace
(that’s the important stuff).</p>
<p>Now, the even more important stuff is that you can <em>abuse</em> the above feature by adding
code that will throw an exception in various parts of your application and, as a result
get an interactive python prompt at <em>specific</em> parts of your application (for example,
before validating your form, or when a method in your model is executed). All this,
without the need to use a specific <span class="caps">IDE</span> to add breakpoints!</p>
<p>This is an old trick however some people don’t use it and make their work more
difficult. Actually, this one of the first things I learned when starting with django
and use it all the time since then - I am writing this post mainly to emphasize
its usefulness and to urge more people to use it. If you don’t already use it please try
it (and thank me later).</p>
</div>
<div class="section" id="configuration">
<h2>Configuration</h2>
<p>There are two components you need to install in your django project to use the
above technique:</p>
<ul class="simple">
<li><a class="reference external" href="https://github.com/django-extensions/django-extensions">django-extensions</a>: a swiss army knife toolset for django - beyond other useful tools it includes a management command (<tt class="docutils literal">runserver_plus</tt>) to start the Werkzeug interactive debugger with your project</li>
<li>werkzeug: the werkzeug utility library</li>
</ul>
<p>Both of these can just be installed with pip (even on windows). After installing them, add <tt class="docutils literal">django_extensions</tt> to your <tt class="docutils literal">INSTALLED_APPS</tt> setting to enable
the management command.</p>
<p>After that, you can just run <tt class="docutils literal">python manage.py runserver_plus</tt> - if everything was installed successfully you should see something like this (in windows at least):</p>
<pre class="code literal-block">
(venv) C:\progr\py\werkzeug\testdebug>python manage.py runserver_plus
* Restarting with stat
Performing system checks...
System check identified no issues (0 silenced).
Django version 1.9.7, using settings 'testdebug.settings'
Development server is running at http://127.0.0.1:8000/
Using the Werkzeug debugger (http://werkzeug.pocoo.org/)
Quit the server with CTRL-BREAK.
* Debugger is active!
* Debugger pin code: 143-738-172
* Debugger is active!
* Debugger pin code: 174-740-467
* Running on http://127.0.0.1:8000/ (Press CTRL+C to quit)
</pre>
<p>Now, the “debugger pin” you see is a way to protect your interactive debugger (i.e it asks for the pin
before allowing you to enter the interactive prompt). Since this feature should <em>only</em> be used in your
local development system I recommend to just disable it by setting the <tt class="docutils literal">WERKZEUG_DEBUG_PIN</tt> environment
variable to <tt class="docutils literal">off</tt> (i.e <tt class="docutils literal">set WERKZEUG_DEBUG_PIN=off</tt> in windows). After that you should see the message
“ * Debugger pin disabled. <span class="caps">DEBUGGER</span> <span class="caps">UNSECURED</span>!“. Please be careful with the interactive debugger
and <em>never, ever use it in a production deployment</em> even with the debug pin enabled. I also recommend to
use it <em>only</em> on a local development server (i.e the server must be run on 127.0.0.1/local <span class="caps">IP</span> and not
allow remote connections).</p>
</div>
<div class="section" id="usage">
<h2>Usage</h2>
<p>Now its time for the magic: Let’s add a django view that throws an exception, like this:</p>
<pre class="code literal-block">
def test(request):
a+=1
</pre>
<p>to your urls.py ( <tt class="docutils literal"><span class="pre">url(r'^test/',</span> test )</tt> ) and after you visit <cite>test</cite> you should see something like this:</p>
<img alt="Werkzeug debugger" src="/images/werkzeug.png" style="width: 800px;" />
<p>Since the <tt class="docutils literal">a</tt> variable was not defined you’ll get an exception when you try to increaseit.
Now, notice the console icon in the lower right corner - when you click it you’ll get the interactive debugger!
Now you can enter python commands exactly where the <tt class="docutils literal"><span class="pre">a+=1</span></tt> code was. For example, you can see what are the
attributes of the <tt class="docutils literal">request</tt> object you receive (for example, just enter <tt class="docutils literal">request.<span class="caps">GET</span></tt> to output the <tt class="docutils literal"><span class="caps">GET</span></tt>
dictionary to the interactive console).</p>
<p>Notice that you can get interactive consoles wherever you want in the stacktrace, i.e I could get a console at line 147
of <cite>django.core.handlers.base</cite> module on the <tt class="docutils literal">get_response</tt> method — this is needed sometimes especially when you
want to see how your code is called by other modules.</p>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>As you can see, using the presented technique you can really quickly start an interactive console wherever you
want and start entering commands. I use it whenever I need to write anything non trivial (or even trivial stuff -
I sometimes prefer opening and interactive debugger to find out by trial and error how should I write a django
<span class="caps">ORM</span> query than open models.py) and really miss it on other environments (Java).</p>
<p>The above technique should also work with few modifications with other python web frameworks so it’s not django-only.</p>
<p>Finally, please notice that both Werkzeug and django-extensions offer many more tools beyond the interactive debugger presented here -
I encourage you to research them since - if you follow my advice - you’ll integrate these to <em>all</em> your django projects!</p>
</div>
Understanding nested list comprehension syntax in Python2016-04-27T11:20:00+03:002016-04-27T11:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2016-04-27:/2016/04/27/python-nested-list-comprehensions/<p class="first last">Write nested list comprehensions with ease</p>
<p>List comprehensions are one of the really nice and powerful features of Python. It
is actually a smart way to introduce new users to functional programming concepts
(after all a list comprehension is just a combination of map and filter) and compact statements.</p>
<p>However, one thing that always troubled me when using list comprehensions is their
non intuitive syntax when nesting was needed. For example, let’s say that we just
want to flatten a list of lists using a nested list comprehension:</p>
<div class="highlight"><pre><span></span><span class="n">non_flat</span> <span class="o">=</span> <span class="p">[</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">],</span> <span class="p">[</span><span class="mi">4</span><span class="p">,</span><span class="mi">5</span><span class="p">,</span><span class="mi">6</span><span class="p">],</span> <span class="p">[</span><span class="mi">7</span><span class="p">,</span><span class="mi">8</span><span class="p">]</span> <span class="p">]</span>
</pre></div>
<p>To write that, somebody would think: For a simple list
comprehension I need to
write <tt class="docutils literal">[ x for x in non_flat ]</tt> to get all its items - however I want to retrieve each element of the <tt class="docutils literal">x</tt> list so I’ll write something like this:</p>
<div class="highlight"><pre><span></span><span class="o">>>></span> <span class="p">[</span><span class="n">y</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">x</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">non_flat</span><span class="p">]</span>
<span class="p">[</span><span class="mi">7</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">]</span>
</pre></div>
<p>Well duh! At this time I’d need research google for a working list comprehension syntax and adjust it to my needs (or give up and write it as a double for loop).</p>
<p>Here’s the correct nested list comprehension people wondering:</p>
<div class="highlight"><pre><span></span><span class="o">>>></span> <span class="p">[</span><span class="n">y</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">non_flat</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">x</span><span class="p">]</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">]</span>
</pre></div>
<p>What if I wanted to add a third level of nesting or an if? Well I’d just bite the bullet and use for loops!</p>
<p>However, if you take a look at the document describing list comprehensions in python (<cite><span class="caps">PEP</span> 202</cite>) you’ll see
the following phrase:</p>
<blockquote>
It is proposed to allow conditional construction of list literals
using for and if clauses. <strong>They would nest in the same way for
loops and if statements nest now.</strong></blockquote>
<p>This statement explains everything! <em>Just think in for-loops syntax</em>. So, If I used for loops for the previous flattening, I’d do something like:</p>
<div class="highlight"><pre><span></span><span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">non_flat</span><span class="p">:</span>
<span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">x</span><span class="p">:</span>
<span class="n">y</span>
</pre></div>
<p>which, if <cite>y</cite> is moved to the front and joined in one line would be the correct nested list comprehension!</p>
<p>So that’s the way… What If I wanted to include only lists with more than 2 elements in the flattening
(so <cite>[7,8]</cite> should not be included)? I’ll write it with for loops first:</p>
<div class="highlight"><pre><span></span><span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">non_flat</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">></span> <span class="mi">2</span>
<span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">x</span><span class="p">:</span>
<span class="n">y</span>
</pre></div>
<p>so by convering this to list comprehension we get:</p>
<div class="highlight"><pre><span></span><span class="o">>>></span> <span class="p">[</span> <span class="n">y</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">non_flat</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">></span> <span class="mi">2</span> <span class="k">for</span> <span class="n">y</span> <span class="ow">in</span> <span class="n">x</span> <span class="p">]</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">]</span>
</pre></div>
<p>Success!</p>
<p>One final, more complex example: Let’s say that we have a list
of lists of words and we want to get a list of all the letters of these words
along with the index of the list they belong to
but only for words with more than two characters. Using the same
for-loop syntax for the nested list comprehensions we’ll get:</p>
<div class="highlight"><pre><span></span><span class="o">>>></span> <span class="n">strings</span> <span class="o">=</span> <span class="p">[</span> <span class="p">[</span><span class="s1">'foo'</span><span class="p">,</span> <span class="s1">'bar'</span><span class="p">],</span> <span class="p">[</span><span class="s1">'baz'</span><span class="p">,</span> <span class="s1">'taz'</span><span class="p">],</span> <span class="p">[</span><span class="s1">'w'</span><span class="p">,</span> <span class="s1">'koko'</span><span class="p">]</span> <span class="p">]</span>
<span class="o">>>></span> <span class="p">[</span> <span class="p">(</span><span class="n">letter</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span> <span class="k">for</span> <span class="n">idx</span><span class="p">,</span> <span class="n">lst</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">strings</span><span class="p">)</span> <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">lst</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span><span class="o">></span><span class="mi">2</span> <span class="k">for</span> <span class="n">letter</span> <span class="ow">in</span> <span class="n">word</span><span class="p">]</span>
<span class="p">[(</span><span class="s1">'f'</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="s1">'o'</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="s1">'o'</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="s1">'b'</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="s1">'a'</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="s1">'r'</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="s1">'b'</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="s1">'a'</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="s1">'z'</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="s1">'t'</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="s1">'a'</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="s1">'z'</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span> <span class="p">(</span><span class="s1">'k'</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="s1">'o'</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="s1">'k'</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="s1">'o'</span><span class="p">,</span> <span class="mi">2</span><span class="p">)]</span>
</pre></div>
Configuring Spring Boot2016-03-31T19:20:00+03:002016-03-31T19:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2016-03-31:/2016/03/31/spring-boot-settings/<p class="first last">Configuring your Spring Boot applications using application properties, profiles, locan settings and command line arguments and deploying them using init.d!</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#properties-vs-yml-files" id="toc-entry-2">properties vs yml files</a></li>
<li><a class="reference internal" href="#structuring-your-configuration-files" id="toc-entry-3">Structuring your configuration files</a><ul>
<li><a class="reference internal" href="#main-application-settings" id="toc-entry-4">Main application settings</a></li>
<li><a class="reference internal" href="#profiles" id="toc-entry-5">Profiles</a><ul>
<li><a class="reference internal" href="#some-more-advanced-profile-usage" id="toc-entry-6">Some more advanced profile usage</a></li>
</ul>
</li>
<li><a class="reference internal" href="#overriding-settings" id="toc-entry-7">Overriding settings</a><ul>
<li><a class="reference internal" href="#using-a-config-application-properties" id="toc-entry-8">Using a config/application.properties</a></li>
<li><a class="reference internal" href="#passing-command-line-arguments" id="toc-entry-9">Passing command line arguments</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#deploying-spring-boot-applications" id="toc-entry-10">Deploying Spring Boot applications</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-11">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>The <a class="reference external" href="http://projects.spring.io/spring-boot/">Spring Boot</a> project is great way of building Java applications using
Spring. Instead of trying to integrate everything by hand (and usually
end up with a configuration hell) you use spring-boot to help you to
bootstrap your application: Just include its
dependencies in your pom.xml and Spring Boot will try its
best to auto-configure all these components!</p>
<p>Of course, no matter how hard Spring Boot tries to auto-configure everything,
you’ll still need to pass some configuration to configure your databases,
caches, email sending, security etc. Thankfully, Spring Boot
can be configured without <em>any</em> xml (actually, its a bad practice to
use xml-based configuration with it), using plain Java .properties
files or (if you prefer the more compact syntax) <a class="reference external" href="https://en.wikipedia.org/wiki/YAML"><span class="caps">YAML</span></a> .yml files!</p>
<p>In this guide, along with a simple introduction to the way Spring Boot configuration
works, we’ll talk about a specific way of stucturing your settings configuration files in
order to have:</p>
<ul class="simple">
<li>A global configuration file that will contain all your settings</li>
<li>Different settings for each of your environments (development, <span class="caps">UAT</span>, staging, production and test)</li>
<li>A way to configure your passwords and other sensitive data (that you don’t want to put to your <span class="caps">VCS</span>)</li>
<li>Being able to override any setting in any environment</li>
<li>Deploying your Spring Boot app in Linux using <tt class="docutils literal">init.d</tt></li>
</ul>
<p>To quickly test the proposed settings configuration I’ve created a simple
Spring Boot project @ <a class="reference external" href="https://github.com/spapas/spring-boot-config">https://github.com/spapas/spring-boot-config</a>. Just clone
it, optionally change the packaged settings (more on this later), package it (<tt class="docutils literal">mvn package</tt>), optionally change
the <tt class="docutils literal">config</tt> settings (more on this also later) and run it
(using something like <tt class="docutils literal">java <span class="pre">-jar</span> <span class="pre">spring-boot-config-0.0.1-<span class="caps">SNAPSHOT</span>.jar</span></tt>) optionally passing it command line settings (more on this
also later). You’ll then be able to visit <tt class="docutils literal"><span class="pre">http://127.0.0.1:8080</span></tt> and check the current settings!</p>
</div>
<div class="section" id="properties-vs-yml-files">
<h2><a class="toc-backref" href="#toc-entry-2">properties vs yml files</a></h2>
<p>You can use two kinds of files to configure your settings: Normal Java .properties files
or <a class="reference external" href="https://en.wikipedia.org/wiki/YAML"><span class="caps">YAML</span></a> .yml files. The .properties files have the form:</p>
<pre class="code literal-block">
config.value.a=1
config.value.b=2
config.value.c=3
</pre>
<p>while the .yml files are like:</p>
<pre class="code literal-block">
config:
value:
a: 1
b: 2
c: 3
</pre>
<p>You may use whatever you wish - in the examples I’ll use normal Java .properties
files because they are more compact (you don’t need to use multiple lines to represent
a single setting like in <span class="caps">YAML</span>).</p>
</div>
<div class="section" id="structuring-your-configuration-files">
<h2><a class="toc-backref" href="#toc-entry-3">Structuring your configuration files</a></h2>
<p>Spring Boot reads its configuration from <a class="reference external" href="https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html">various places</a>, however in this article we’ll talk
about four of them which should be enough for most cases. Starting from the most global to the most
specific ones (i.e the latter ones will override the previous ones) these are:</p>
<ul class="simple">
<li>Main (global) application settings</li>
<li>Profile settings</li>
<li>Local (/config) settings</li>
<li>Command line arguments</li>
</ul>
<p>The first two are setting files that will be contained inside the artifact (jar or war) that will be
created and should be commited to your version control system. I’ll call them jar-packaged
settings. The other two won’t be commited to the version control but will be created directly
on the server to-deploy. Let’s see a little more about them:</p>
<div class="section" id="main-application-settings">
<h3><a class="toc-backref" href="#toc-entry-4">Main application settings</a></h3>
<p>These are kept in a file named <tt class="docutils literal">application.properties</tt> (or <tt class="docutils literal">yml</tt> — from now on I’ll just use
<cite>.properties`</cite> but keep in mind that you may use <tt class="docutils literal">.yml</tt>): This file should reside
inside the <tt class="docutils literal">src\main\resources</tt> folder
of your project and ideally contain all the settings your spring-boot application users. Some
of these settings will be overriden by settings kept in the next source so they may have a
default value or even be empty if they will be always overriden (or contain sensitive data
like passwords), however I still prefer to list
them all in this file even as placeholders to have a central source of all the settings that
your Spring Boot application uses.</p>
</div>
<div class="section" id="profiles">
<h3><a class="toc-backref" href="#toc-entry-5">Profiles</a></h3>
<p>A profile is a set of settings that can be configured to override settings from <tt class="docutils literal">application.properties</tt>.
Each profile is contained in a file named <tt class="docutils literal"><span class="pre">application-profilename.properties</span></tt> where <tt class="docutils literal">profilename</tt> is
the name of the profile. Now, a profile could configure anything you want, however
for most projects I propose to
have the following profiles:</p>
<ul class="simple">
<li><tt class="docutils literal">dev</tt> for your local development settings</li>
<li><tt class="docutils literal">uat</tt> for your <span class="caps">UAT</span> server settings</li>
<li><tt class="docutils literal">staging</tt> for your staging server settings</li>
<li><tt class="docutils literal">prod</tt> for your production settings</li>
<li><tt class="docutils literal">test</tt> for running your tests</li>
</ul>
<p>(depending of course on what are your requirements, some projects may not
need <tt class="docutils literal">uat</tt> or <tt class="docutils literal">staging</tt> but all projects should have a <tt class="docutils literal">dev</tt>, a <tt class="docutils literal">prod</tt> and a <tt class="docutils literal">test</tt> profile).
The configuration for these environemnts needs to be different for obvious reasons.
For example when developing you may want
to use a local database, when running tests an ephemeral in memory database
and your production database when deploying to production.
These profile configuration files will be stored inside your <tt class="docutils literal">src\main\resources</tt> folder,
right next to the <tt class="docutils literal">application.properties</tt>, i.e you’ll have
<tt class="docutils literal"><span class="pre">application-dev.properties</span></tt>, <tt class="docutils literal"><span class="pre">application-prod.properties</span></tt>,
<tt class="docutils literal"><span class="pre">application-test.properties</span></tt> etc - and all these files will be kept
in your <span class="caps">VCS</span> (and will also be jar-packaged since they will be
contained in the resulting artifact).</p>
<p>How do you select which profile is active each time (i.e pick it
when running the Spring Boot application under
its corresponding environment)?</p>
<p>For tests, since they can be run by a different <tt class="docutils literal">Main</tt> than
the normal application, you should use the <tt class="docutils literal">@ActiveProfiles</tt> annotation
(for example <tt class="docutils literal"><span class="pre">@ActiveProfiles("test")</span></tt>) to make sure that the tests
will run with the correct settings. So if the contents of your <tt class="docutils literal"><span class="pre">application-test.properties</span></tt>
are <tt class="docutils literal">config.value=Hello test!</tt> running this test should produce no errors:</p>
<div class="highlight"><pre><span></span><span class="nd">@RunWith</span><span class="p">(</span><span class="n">SpringJUnit4ClassRunner</span><span class="p">.</span><span class="na">class</span><span class="p">)</span>
<span class="nd">@SpringApplicationConfiguration</span><span class="p">(</span><span class="n">classes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">SpringBootConfigApplication</span><span class="p">.</span><span class="na">class</span><span class="p">)</span>
<span class="nd">@ActiveProfiles</span><span class="p">(</span><span class="s">"test"</span><span class="p">)</span>
<span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">SpringBootConfigApplicationTests</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nd">@Value</span><span class="p">(</span><span class="s">"${config.value}"</span><span class="p">)</span>
<span class="w"> </span><span class="kd">private</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">value</span><span class="p">;</span>
<span class="w"> </span><span class="nd">@Value</span><span class="p">(</span><span class="s">"${spring.profiles.active}"</span><span class="p">)</span>
<span class="w"> </span><span class="kd">private</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">profile</span><span class="p">;</span>
<span class="w"> </span><span class="nd">@Test</span>
<span class="w"> </span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">contextLoads</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">assertThat</span><span class="p">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">is</span><span class="p">(</span><span class="s">"Hello test!"</span><span class="p">));</span>
<span class="w"> </span><span class="n">assertThat</span><span class="p">(</span><span class="n">profile</span><span class="p">,</span><span class="w"> </span><span class="n">is</span><span class="p">(</span><span class="s">"test"</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>To activate a different profile when running your Spring Boot applications
you’ll need to use the <tt class="docutils literal">spring.profiles.active</tt> setting, so if you set
<tt class="docutils literal">spring.profiles.active=prod</tt> in your <tt class="docutils literal">application.properties</tt> and
create the packaged jar (or war) then you’ll have the production settings
when you run your application (i.e the contents of <tt class="docutils literal"><span class="pre">application-prod.properties</span></tt>
will be used to override your <tt class="docutils literal">application.properties</tt>). Of course, to deploy it
to <span class="caps">UAT</span>, you’ll need to change
<tt class="docutils literal">spring.profiles.active</tt> to <tt class="docutils literal">uat</tt> and re-create the packaged artifact —
see some repetition and penal labour here? Definitely you don’t want to do re-create
your artifacts for each of the environments you may want to deploy —
we’ll see in the next sections how to improve this flow by overriding
jar-packaged settings!</p>
<div class="section" id="some-more-advanced-profile-usage">
<h4><a class="toc-backref" href="#toc-entry-6">Some more advanced profile usage</a></h4>
<p>You may have noticed in the previous section that the name of the
annotation is <tt class="docutils literal">@ActiveProfiles</tt> and the name of the setting
<tt class="docutils literal">spring.profiles.active</tt> - both in plural. This of course is
on purpose: You may have <em>more than one</em> active profiles!</p>
<p>This, along with the fact that you can make <tt class="docutils literal">@Components</tt> or <tt class="docutils literal">@Configuration</tt>
available <em>only</em> on certail profiles is a really powerful tool!</p>
<p>Here are some examples:</p>
<ul class="simple">
<li>Configure two spring-security <tt class="docutils literal">@Configuration</tt> s: Use in memory security for your dev environment, while using <span class="caps">LDAP</span> for your production.</li>
<li>If you want to support more than one database you can configure multiple profiles — and use them along with the dev/uat/prod I mentioned before.</li>
<li>Create verbose and non-verbose logging profiles and quickly change between them</li>
</ul>
</div>
</div>
<div class="section" id="overriding-settings">
<h3><a class="toc-backref" href="#toc-entry-7">Overriding settings</a></h3>
<p>All the above settings we’ve defined should be safely kept inside
your <span class="caps">VCS</span> - however we wouldn’t like storing passwords or other
sensitive data to a <span class="caps">VCS</span>! Sensitive settings should be empty
(or have a default value) when
saved to <span class="caps">VCS</span> and overriden by “local” settings.</p>
<p>Also, all the previous are jar-packaged
and we definitely need a way to override them without messing
with the artifacts (for example, we need to select the
correct profile for running the application by overriding
<tt class="docutils literal">spring.profiles.active</tt>).</p>
<p>There two methods of overriding settings, and these are the last
two methods of the four we discussed above:</p>
<div class="section" id="using-a-config-application-properties">
<h4><a class="toc-backref" href="#toc-entry-8">Using a config/application.properties</a></h4>
<p>You can put files in a directory named <tt class="docutils literal">config</tt> that is at the same level
as the location from which you try to run your jar. These file should be
named either <tt class="docutils literal">application.properties</tt> or <tt class="docutils literal"><span class="pre">application-profilename.properties</span></tt>
and will be used to override your jar-packaged settings.</p>
<p>What happens is that Spring will at first try to load a file named <tt class="docutils literal">config/application.properties</tt> that will
override your jar-packaged <tt class="docutils literal">application.properties</tt> (so here you can set your current profile). Then, it will also try to load
a file named <tt class="docutils literal"><span class="pre">config/application-profilename.properties</span></tt> that will override
your jar-packaged <tt class="docutils literal"><span class="pre">application-profilename.properties</span></tt> (so here you may
override any profile related properties).</p>
<p>The priority of the files from lowest to highest:</p>
<ul class="simple">
<li>jar-packaged <tt class="docutils literal">application.properties</tt></li>
<li>local <tt class="docutils literal">config/application.properties</tt></li>
<li>jar-packaged <tt class="docutils literal"><span class="pre">application-profilename.properties</span></tt></li>
<li>local <tt class="docutils literal"><span class="pre">config/application-profilename.properties</span></tt></li>
</ul>
<p>So (repeating for emphasis) the settings in your jar-packaged <tt class="docutils literal"><span class="pre">application-profilename.properties</span></tt> will <em>only</em>
be overriden by <tt class="docutils literal"><span class="pre">config/application-profilename.properties</span></tt> (and not by the <tt class="docutils literal">config/application.properties</tt>
which will only override settings on the jar-packaged <tt class="docutils literal">application.properties</tt>).</p>
<p>Also, to make everything clear about where the <tt class="docutils literal">config</tt> directory should be kept:</p>
<p>If the current directory from which you’ll run your jar is <tt class="docutils literal">/home/serafeim</tt> and
you want to execute <tt class="docutils literal"><span class="pre">/opt/spring/my-spring-app.jar</span></tt> (so you’ll run something like
<tt class="docutils literal">/home/serafeim$ java <span class="pre">-jar</span> <span class="pre">/opt/spring/my-spring-app.jar</span></tt>) then
the <tt class="docutils literal">config</tt> directory should
be at <tt class="docutils literal">/home/serafeim/config</tt> (i.e at the same directory from where you execute
jar). Normally however and to avoid confusion, the best approach would
be to just put it at <tt class="docutils literal">/opt/spring/config</tt> and <tt class="docutils literal">cd /opt/spring</tt> before running
your jar (so <tt class="docutils literal">config</tt> will be right next to your jar and run the jar from the directory).</p>
<p>Finally, my recommendation is to keep these <tt class="docutils literal"><span class="pre">config/*properties</span></tt> files off version control
(after all they should be different for each of your environments - common settings should
go to the jar-packaged files)
and to put only the profile selection setting and sensitive settings there. That means that
the <tt class="docutils literal">config/application.properties</tt> file should <em>only</em> contain a <tt class="docutils literal">spring.profiles.active=profilename</tt>
setting to set the correct profile for this instance of your app and the <tt class="docutils literal"><span class="pre">config/application-profilename.properties</span></tt>
will contain all sensitive information that you’ll need to run that profile.</p>
<p>For example in your <span class="caps">UAT</span> server you’ll have <tt class="docutils literal">spring.profiles.active=uat</tt> in your <tt class="docutils literal">application.properties</tt>
and your uat server passwords in your <tt class="docutils literal"><span class="pre">application-uat.properties</span></tt></p>
</div>
<div class="section" id="passing-command-line-arguments">
<h4><a class="toc-backref" href="#toc-entry-9">Passing command line arguments</a></h4>
<p>The most specific way of overriding parameters (including the active profile of course) is by
directly passing these parameters as arguments when running your jar. For example,
if you run <tt class="docutils literal">java <span class="pre">-Dconfig.value=foo</span> <span class="pre">-jar</span> <span class="pre">my-spring-app.jar</span></tt> then the <tt class="docutils literal">config.value</tt>
will always have a value of <tt class="docutils literal">foo</tt> no matter what you have in your other config files.</p>
<p>That’s a different way to set your active profile (by passing <tt class="docutils literal"><span class="pre">-Dspring.profiles.active=profilename</span></tt>)
or to quickly set sensitive settings however
I prefer to keep the settings in properties files (and not to put them in scripts where they will definitely
be missed and will be more difficult to be managed)
so I’ll recommend the previous way of using a non-commited to version control local config/application.properties.
Use command line arguments only for quick tests (run something with a specific setting to test how it works).</p>
</div>
</div>
</div>
<div class="section" id="deploying-spring-boot-applications">
<h2><a class="toc-backref" href="#toc-entry-10">Deploying Spring Boot applications</a></h2>
<p>If you check
the deployment documentation of Spring Boot you’ll see that it has various hints on
<a class="reference external" href="http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#deployment">on deploying Spring Boot applications</a>. I won’t go into much detail about these however I’ll
represent my recommendation on deploying Spring Boot apps on Linux as an init.d script:</p>
<p>What is really interesting about Spring boot is that it allows you to make your jar-packaged jars <a class="reference external" href="https://docs.spring.io/spring-boot/docs/current/reference/html/deployment-install.html">executable as an init.d script</a> so that you will
be able to manage it using something like <tt class="docutils literal">service springbootapp start/stop/restart</tt> etc. To do that,
you’ll just need to add the <tt class="docutils literal"><span class="pre"><executable>true</executable></span></tt> <tt class="docutils literal">configuration</tt> for your pom’s
<tt class="docutils literal"><span class="pre">spring-boot-maven-plugin</span></tt>. This will add some things in the start of your resulting jar file
that will make it behave as a unix init.d script. If you take a look at your package artifact
you’ll see something like this:</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="c1">#</span>
<span class="c1"># . ____ _ __ _ _</span>
<span class="c1"># /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \</span>
<span class="c1"># ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \</span>
<span class="c1"># \\/ ___)| |_)| | | | | || (_| | ) ) ) )</span>
<span class="c1"># ' |____| .__|_| |_|_| |_\__, | / / / /</span>
<span class="c1"># =========|_|==============|___/=/_/_/_/</span>
<span class="c1"># :: Spring Boot Startup Script ::</span>
<span class="c1">#</span>
<span class="c1">### BEGIN INIT INFO</span>
<span class="c1"># Provides: spring-boot-config</span>
<span class="c1"># Required-Start: $remote_fs $syslog $network</span>
<span class="c1"># Required-Stop: $remote_fs $syslog $network</span>
<span class="c1"># Default-Start: 2 3 4 5</span>
<span class="c1"># Default-Stop: 0 1 6</span>
<span class="c1"># Short-Description: spring-boot-config</span>
<span class="c1"># Description: Demo project for Spring Boot configuration</span>
<span class="c1"># chkconfig: 2345 99 01</span>
<span class="c1">### END INIT INFO</span>
<span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$DEBUG</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nb">set</span><span class="w"> </span>-x
<span class="c1"># Initialize variables that cannot be provided by a .conf file</span>
<span class="nv">WORKING_DIR</span><span class="o">=</span><span class="s2">"</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">"</span>
<span class="c1"># shellcheck disable=SC2153</span>
<span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$JARFILE</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nv">jarfile</span><span class="o">=</span><span class="s2">"</span><span class="nv">$JARFILE</span><span class="s2">"</span>
<span class="o">[[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$APP_NAME</span><span class="s2">"</span><span class="w"> </span><span class="o">]]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nv">identity</span><span class="o">=</span><span class="s2">"</span><span class="nv">$APP_NAME</span><span class="s2">"</span>
...
</pre></div>
<p>One thing that may seem puzzling at first is that if make this jar executable
and try to run it you’ll see that, instead of offering you the well known
options of the init scripts (Usage … start/stop/restart etc) it will immediatelly
run the application! This is because the embedded script is smart enough to
check that it will be executed as an init script only when it is executed
as a link from <tt class="docutils literal">/etc/init.d</tt> - else it will immediately run the application.</p>
<p>If
you want to quickly test that behavior, you may override the <tt class="docutils literal"><span class="caps">MODE</span></tt> parameter
which forces the mode of operation of the jar. If you want to run it as a
script (without using a links from /etc/ini.d) then just set <tt class="docutils literal"><span class="caps">MODE</span>=service</tt>.
So, try runnin:</p>
<pre class="code literal-block">
> MODE=service ./springapplication.jar
Usage: ./hsk9eea.jar {start|stop|restart|force-reload|status|run}
</pre>
<p>Success! Of course, this is just for testing purposes, to actually deploy
your application then please create a link to it from <tt class="docutils literal">/etc/init.d</tt> as
proposed by the Spring Boot docs.</p>
<p>If you want to <a class="reference external" href="http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#deployment-script-customization">customize the init.d script</a> you can use a file named
<tt class="docutils literal">sprinbootapp.conf</tt> in the same directory as your <tt class="docutils literal">springbootapp.jar</tt>
(i.e it should have the same name as your jar with an extension of .conf). The
options from it will be sourced before running your application — for example
you could set the active profile using <tt class="docutils literal">RUN_ARGS</tt>, however as I already
recommended, explicitly setting it to a file named <tt class="docutils literal">config/applications.properties</tt>
is preferrable.</p>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-11">Conclusion</a></h2>
<p>Using the described file structure you should be able to fully configure Spring Boot and have all the
goodies you’d expect from a modern framework: global settings, profiles, non-version control settings! Also, using the
advanced profiles techniques (multiple profiles, profile enabled
@Components and @Configurations) you’ll be able to implement
some really complex configurations! Finally, you’ll be able to really
quickly deploy the resulting jar as an init.d system service!</p>
</div>
A comprehensive react-redux tutorial2016-03-02T11:20:00+02:002016-03-02T11:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2016-03-02:/2016/03/02/react-redux-tutorial/<p class="first last">A comprehensive tutorial for using react with redux</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#introduction-to-redux" id="toc-entry-2">Introduction to redux</a><ul>
<li><a class="reference internal" href="#a-simple-example" id="toc-entry-3">A simple example</a></li>
<li><a class="reference internal" href="#interlude-so-what-s-a-reducer" id="toc-entry-4">Interlude: So what’s a reducer?</a></li>
<li><a class="reference internal" href="#what-about-react-redux" id="toc-entry-5">What about react-redux?</a></li>
</ul>
</li>
<li><a class="reference internal" href="#our-project" id="toc-entry-6">Our project</a><ul>
<li><a class="reference internal" href="#other-libraries-used" id="toc-entry-7">Other libraries used</a></li>
<li><a class="reference internal" href="#redux-thunk" id="toc-entry-8">redux-thunk?</a></li>
</ul>
</li>
<li><a class="reference internal" href="#explaining-the-application" id="toc-entry-9">Explaining the application</a><ul>
<li><a class="reference internal" href="#main-js" id="toc-entry-10">main.js</a></li>
<li><a class="reference internal" href="#store-js" id="toc-entry-11">store.js</a><ul>
<li><a class="reference internal" href="#interlude-combining-reducers" id="toc-entry-12">Interlude: Combining reducers</a></li>
<li><a class="reference internal" href="#interlude-middlewares" id="toc-entry-13">Interlude: Middlewares</a></li>
</ul>
</li>
<li><a class="reference internal" href="#reducers-js" id="toc-entry-14">reducers.js</a><ul>
<li><a class="reference internal" href="#the-global-state-tree" id="toc-entry-15">The global state tree</a></li>
</ul>
</li>
<li><a class="reference internal" href="#actions-js" id="toc-entry-16">actions.js</a></li>
<li><a class="reference internal" href="#components-app-js" id="toc-entry-17">components/app.js</a><ul>
<li><a class="reference internal" href="#components-notification-js" id="toc-entry-18">components/notification.js</a></li>
<li><a class="reference internal" href="#creating-a-re-usable-notification" id="toc-entry-19">Creating a re-usable notification</a></li>
<li><a class="reference internal" href="#components-loading-js" id="toc-entry-20">components/loading.js</a></li>
<li><a class="reference internal" href="#components-statpanel-js" id="toc-entry-21">components/StatPanel.js</a></li>
</ul>
</li>
<li><a class="reference internal" href="#components-bookpanel-js" id="toc-entry-22">components/BookPanel.js</a><ul>
<li><a class="reference internal" href="#components-booksearchpanel-js" id="toc-entry-23">components/BookSearchPanel.js</a></li>
<li><a class="reference internal" href="#components-table-js" id="toc-entry-24">components/Table.js</a></li>
<li><a class="reference internal" href="#components-pagingpanel-js" id="toc-entry-25">components/PagingPanel.js</a></li>
<li><a class="reference internal" href="#interlude-a-more-functional-component" id="toc-entry-26">Interlude: A more functional component</a></li>
</ul>
</li>
<li><a class="reference internal" href="#components-bookform" id="toc-entry-27">components/BookForm</a><ul>
<li><a class="reference internal" href="#components-input" id="toc-entry-28">components/Input</a></li>
<li><a class="reference internal" href="#components-select" id="toc-entry-29">components/Select</a></li>
<li><a class="reference internal" href="#components-datepicker" id="toc-entry-30">components/DatePicker</a></li>
<li><a class="reference internal" href="#should-i-create-my-own-input-components" id="toc-entry-31">Should I create my own input components?</a></li>
</ul>
</li>
<li><a class="reference internal" href="#components-authorpanel" id="toc-entry-32">components/AuthorPanel</a></li>
<li><a class="reference internal" href="#components-authorform" id="toc-entry-33">components/AuthorForm</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changing-the-ui-when-the-data-is-changed" id="toc-entry-34">Changing the <span class="caps">UI</span> when the data is changed</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-35">Conclusion</a></li>
</ul>
</div>
<p><strong>Update 10/05/2016</strong>: Add a section on how to update the <span class="caps">UI</span> when the data in the database is changed without using the <span class="caps">UI</span>.</p>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>Continuing the series of React-related articles, we’ll try to make a comprehensive
tutorial to the <a class="reference external" href="https://github.com/rackt/redux">redux</a> framework and its integrations with React, using the
<a class="reference external" href="https://github.com/rackt/react-redux">react-redux</a> library. Redux can be used as an alternative to Flux
(which we discussed in <a class="reference external" href="https://spapas.github.io/2015/07/02/comprehensive-react-flux-tutorial-2/">a previous article</a>)
to orchestrate the message passing between ui/components/data.</p>
<p>This tutorial will also produce as a result an opinionated (my opinion) boilerplate
project for creating react-redux Single Page Applications. I have used Django
as the back end however you could use any server-side framework you like,
Django is only used for implementing a bunch of <span class="caps">REST</span> APIs through django-rest-framework.
You may replace it with any other <span class="caps">REST</span> framework you like.</p>
<p>I have also used <tt class="docutils literal">browserify</tt> to pack the client-side code — I prefer it
from webpack as I find it much easier, clear and less-magical, and, since
it fills all my needs I don’t see any reason to take the webpack pill.</p>
<p>All client side code is written in <span class="caps">ES6</span> javascript using the latest trends — I will
try to explain anything I feel that is not very clear.</p>
<p>Before continuing, I have to mention that although I will provide an
(as comprehensive as possible) introduction to redux,
I will concentrate on the correct integration between redux, react-redux and
React in a complex, production level application. I’ll try to explain everything
I talk about, however, before reading the rest of
this article I recommend reading the introduction and basics sections
of <a class="reference external" href="http://rackt.org/redux/index.html">redux documentation</a> and watching the <a class="reference external" href="https://egghead.io/series/getting-started-with-redux">getting started with redux</a> from the
creator of redux — I can’t recommend the videos enough, they are really great!</p>
<p>One final thing: This is a <em>really long</em> article (more than 100 kb). It will
require a lot of time to read and understand it, especially if you are not
familiar with the concepts described here. Please take your time when reading
it and try verifying what I say here through the project that accompanies
this article @ <a class="reference external" href="https://github.com/spapas/react-tutorial/">https://github.com/spapas/react-tutorial/</a>, tag <tt class="docutils literal"><span class="pre">react-redux</span></tt>.</p>
</div>
<div class="section" id="introduction-to-redux">
<h2><a class="toc-backref" href="#toc-entry-2">Introduction to redux</a></h2>
<p>I have to say that I got interested in redux because of all that buzz this
framework generated. However, I got really interested in it when I
understood how close its philosophy was to functional programming -
this, when combined with the usage of react functional components will
enable you to write great code <em>and</em> (most important for me) really
enjoy coding with it (if you like functional programming of course :) )</p>
<p>Redux is simpler, having fewer and less complex concepts than the original Flux
implementation and more opinionated - this is a good thing!</p>
<p>Here are the basic building blocks of redux:</p>
<ul class="simple">
<li>One (and only one) <strong>state</strong>: It is an object that keeps the <em>global</em> state of your application. Everything has to be in that object, both data and ui.</li>
<li>A bunch of <strong>actions</strong>: These are simple objects that are created/dispatched when something happens (ui interaction, server response etc) with a mandatory property (their type) and a number of optional properties that define the data that accompanies each action.</li>
<li>A bunch of <strong>action creators</strong>: These are very simple functions that create action objects. Usually, there are as many action creators as actions (unless you use redux-thunk, we’ll talk about it later).</li>
<li>One (and only one) <strong>reducer</strong>: It is a function that retrieves the current state and an action and creates the resulting state. This is the central component of a redux application - every action along with the current state will be passed to the reducer and the state of the application will be the resulting, new state.</li>
<li>One (and only one) <strong>store</strong>: It is an object that is created by redux and is used as a glue between the state, the reducer and the components</li>
</ul>
<p>The general idea/flow is:</p>
<ul class="simple">
<li>Something (let’s call it event) happens (i.e a user clicks a button, a timeout is fired, an ajax request responds)</li>
<li>An action describing that event is created by its the corresponding action creator and dispatched (i.e passed to the reducer along with the current state) through the store</li>
<li>The dispach calls the reducer with the current state object and the action as parameters</li>
<li>The reducer checks the type of the action and, depending on the action type and any other properties this action has, creates a new state object</li>
<li>The store applies the new state to all components</li>
</ul>
<p>One thing we can see from the above is that redux is not react-only (although its general architecture is a perfect fit to react) but
could be also used with different view frameworks, or even with <em>no view framework</em>!</p>
<div class="section" id="a-simple-example">
<h3><a class="toc-backref" href="#toc-entry-3">A simple example</a></h3>
<p>I’ve implemented a very simple redux example @ jsfiddle that increases and decreases
a number using two buttons to support the above:</p>
<div class="jsfiddle"><iframe width="100%" height="300" src="http://jsfiddle.net/8aba3sp6/embedded/js,resources,html,css,result/light/" allowfullscreen="allowfullscreen" frameborder="0"></iframe></div><p>Its html is:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'state_container'</span><span class="p">></span>0<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">onclick</span><span class="o">=</span><span class="s">'increase()'</span><span class="p">></span>+<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">onclick</span><span class="o">=</span><span class="s">'decrease()'</span><span class="p">></span>-<span class="p"></</span><span class="nt">button</span><span class="p">></span>
</pre></div>
<p>while its javascript (es6) code is:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">reducer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'INCREASE'</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="o">+</span><span class="mf">1</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'DECREASE'</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="o">-</span><span class="mf">1</span>
<span class="w"> </span><span class="k">default</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">store</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Redux</span><span class="p">.</span><span class="nx">createStore</span><span class="p">(</span><span class="nx">reducer</span><span class="p">)</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">unsubscribe</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span>
<span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">'state_container'</span><span class="p">).</span><span class="nx">innerHTML</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">()</span>
<span class="p">)</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">increase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">({</span>
<span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s1">'INCREASE'</span>
<span class="p">})</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">decrease</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">({</span>
<span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s1">'DECREASE'</span>
<span class="p">})</span>
</pre></div>
<p>The <span class="caps">HTML</span> just displays a div which keeps the current number value
and two buttons that call the increase and decrease functions.</p>
<p>Now, for the javascript, we create a reducer function that
gets the previous state value (which initially is the number 0) and the
action that is dispatched. When the reducer is called it will check if the action type is ‘<span class="caps">INCREASE</span>’
or ‘<span class="caps">DECREASE</span>’ and correspondigly increase or decreases the state,
which is just that number. Normally, the state will be a (rather fat) object.</p>
<p>We then create a store which gets the reducer as its only parameter
then and call its subscribe method passing a callback. This callback will be
called whenever the state is changed - in our case, the callback just updates
the div with the current number from the state. Finally, the increase
and decrease methods that are called when the butts are clicked
will just dispatch the corresponding action.</p>
<p>Please notice that in the above example I didn’t use action creators for
simplicity. For completeness, the action creator for increase would be something like</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">increaseCreator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s1">'INCREASE'</span>
<span class="p">}</span>
</pre></div>
<p>i.e it would just return an <tt class="docutils literal"><span class="caps">INCREASE</span></tt> action and <tt class="docutils literal">window.increase</tt>
would be <tt class="docutils literal">window.increase = e => <span class="pre">store.dispatch(increaseCreator())</span></tt>. Notice that
the <tt class="docutils literal">increaseCreator</tt> is called (has <tt class="docutils literal">()</tt>) so that <tt class="docutils literal">dispatch</tt> will receive the
resulting action object as a parameter.</p>
<p>The flow of the data when the increase button is clicked is the following:</p>
<ul class="simple">
<li><tt class="docutils literal">button.onClick</tt></li>
<li><tt class="docutils literal">increase()</tt></li>
<li><tt class="docutils literal">increaseCreator()</tt> (if we used action creators - this a param to <tt class="docutils literal">dispatch</tt> so it will be called first)</li>
<li><tt class="docutils literal"><span class="pre">store.dispatch({type:</span> '<span class="caps">INCREASE</span>' })</tt></li>
<li><tt class="docutils literal">reducer(current_state, {type: <span class="pre">'<span class="caps">INCREASE</span>'})</span></tt></li>
<li>Reducer returns the new state (<tt class="docutils literal">state+1</tt>)</li>
<li><tt class="docutils literal">callback()</tt></li>
<li>value is updated</li>
</ul>
<p>Having one and only one store/state makes the flow of the data crystal and
resolves some of the dillemas I had when using the original Flux architecture!
Some people may argue that although a single reducer function is nice for
the above simple demo, having a huge (spaghetti-like) switch statement in
your reducer is not a very good practice - thankfully redux has a bunch
of tools that will presented later and greatly help on this (seperating the
reducing logic, using different modules for each module etc).</p>
</div>
<div class="section" id="interlude-so-what-s-a-reducer">
<h3><a class="toc-backref" href="#toc-entry-4">Interlude: So what’s a reducer?</a></h3>
<p>I’d like to talk a bit about the “reducer”, mainly for people not familiar with
functional programming (although people writing Javascript <em>should</em> be familiar
with functional programming since Javascript has functional features) to clarify
my statement above that the redux philosophy is close to functional programming.</p>
<p>One basic concept of functional programming is the concept of
“map-reduce”. Mapping means calling a function (let’s call it mapper)
for all elements of a list and creating a new list with the output of each
individual call. So, a mapper gets only one parameter, the current value of
the list. For example the “double” mapper, defined like
<tt class="docutils literal">let double = x => x*2</tt> would “map” the list <tt class="docutils literal">[1,2,3]</tt> to <tt class="docutils literal">[2,4,6]</tt>.</p>
<p>Reducing means calling a function (let’s call it <em>reducer</em>) for all elements
of a list and creating a single value that accumulates the result of each
individual call. This can be done because the reducer gets <em>two</em> parameters,
the accumulated value of the list until this point and the current value of the list.
Also, when doing a reduce we need to define a starting value for the accumulator.
For example, the “sum” reducer, defined like <tt class="docutils literal">let sum = (s=0, x) => s+x</tt>,
(which has an initial value of 0), would “reduce” the list <tt class="docutils literal">[1,2,3]</tt> to <tt class="docutils literal">6</tt> by calling
the <tt class="docutils literal">sum</tt> reducer three times:</p>
<div class="highlight"><pre><span></span><span class="nx">tmp1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sum</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="mf">1</span><span class="p">);</span><span class="w"> </span><span class="c1">// tmp1 = 1</span>
<span class="nx">tmp2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sum</span><span class="p">(</span><span class="nx">tmp1</span><span class="p">,</span><span class="w"> </span><span class="mf">2</span><span class="p">);</span><span class="w"> </span><span class="c1">// tmp2 = 3</span>
<span class="nx">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sum</span><span class="p">(</span><span class="nx">tmp2</span><span class="p">,</span><span class="w"> </span><span class="mf">3</span><span class="p">);</span><span class="w"> </span><span class="c1">// result = 6</span>
</pre></div>
<p>So, a redux reducer is <em>actually</em> a (rather complex) functional reducer, getting the current
state (as the accumulated value) and each individual action as the value and
returning the new state which is the result of applying this action to the state!</p>
<p>Three extra things to make sure about your redux reducers are that:</p>
<ul class="simple">
<li>they should have an initial value (with the initial state of the application) or know how to handle an undefined initial state</li>
<li>they must not not mutate (change) the state object but instead create and return a new one (not allowing object mutations is a general practice in functional programming but in redux also helps to quickly apply the changes to the components)</li>
<li>always return a valid state as a result (or else the application will have invalid state)</li>
</ul>
</div>
<div class="section" id="what-about-react-redux">
<h3><a class="toc-backref" href="#toc-entry-5">What about react-redux?</a></h3>
<p>React-redux is a rather simple framework that offers two helpful utilities for integrating
redux with React:</p>
<ul class="simple">
<li>A <tt class="docutils literal">connect</tt> function that “connects” React components to the redux store. This function (among others) retrieves a callback parameter that defines properties that will be passed to that component and each one will be (magically) mapped to state properties.</li>
<li>A <tt class="docutils literal">Provider</tt> component. This is a parent component that can be used to (magically) pass the store properties to its children components.</li>
</ul>
<p>Please notice that nothing actually magical happens when the store properties are passed to the children
components through <tt class="docutils literal">connect</tt> and <tt class="docutils literal">Provider</tt>! this is accomplished through the <a class="reference external" href="https://facebook.github.io/react/docs/context.html">react context</a> feature
that allows you to “pass data through the component tree without having to pass the props down manually
at every level”. So <tt class="docutils literal"><span class="pre">connect``ed</span> components used context to retrieve the store properties that
have been passed to the context by the ``Provider</tt>.</p>
<p>How react-redux is used be made more clear with another jsfiddle that will convert the previous example to React and react-redux:</p>
<div class="jsfiddle"><iframe width="100%" height="300" src="http://jsfiddle.net/8aba3sp6/2/embedded/js,resources,html,css,result/light/" allowfullscreen="allowfullscreen" frameborder="0"></iframe></div><p>The html is just <tt class="docutils literal"><div <span class="pre">id='container'></div></span></tt> since the components will
be rendered through react, while the es6/jsx code is:</p>
<div class="highlight"><pre><span></span><span class="kd">let</span><span class="w"> </span><span class="nx">reducer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'INCREASE'</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="o">+</span><span class="mf">1</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'DECREASE'</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="o">-</span><span class="mf">1</span>
<span class="w"> </span><span class="k">default</span><span class="o">:</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">store</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Redux</span><span class="p">.</span><span class="nx">createStore</span><span class="p">(</span><span class="nx">reducer</span><span class="p">)</span>
<span class="kd">class</span><span class="w"> </span><span class="nx">RootComponent</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">React</span><span class="p">.</span><span class="nx">Component</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">render</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="nx">number</span><span class="p">,</span><span class="w"> </span><span class="nx">increase</span><span class="p">,</span><span class="w"> </span><span class="nx">decrease</span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span><span class="p">{</span><span class="nx">number</span><span class="p">}</span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="p">=></span><span class="nx">increase</span><span class="p">()}</span><span class="o">>+<</span><span class="err">/button></span>
<span class="w"> </span><span class="o"><</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="p">=></span><span class="nx">decrease</span><span class="p">()}</span><span class="o">></span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="o"><</span><span class="err">/button></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">number</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span>
<span class="p">})</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">dispatch</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">increase</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">dispatch</span><span class="p">({</span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s1">'INCREASE'</span><span class="p">}),</span>
<span class="w"> </span><span class="nx">decrease</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">dispatch</span><span class="p">({</span><span class="nx">type</span><span class="o">:</span><span class="w"> </span><span class="s1">'DECREASE'</span><span class="p">})</span>
<span class="p">})</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">ConnectedRootComponent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">ReactRedux</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span>
<span class="w"> </span><span class="nx">mapStateToProps</span><span class="p">,</span><span class="w"> </span><span class="nx">mapDispatchToProps</span>
<span class="p">)(</span><span class="nx">RootComponent</span><span class="p">)</span>
<span class="nx">ReactDOM</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span>
<span class="w"> </span><span class="o"><</span><span class="nx">ReactRedux</span><span class="p">.</span><span class="nx">Provider</span><span class="w"> </span><span class="nx">store</span><span class="o">=</span><span class="p">{</span><span class="nx">store</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">ConnectedRootComponent</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/ReactRedux.Provider>,</span>
<span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">'container'</span><span class="p">)</span>
<span class="p">)</span>
</pre></div>
<p>As we can see, the reducer and store are the same as the non-react version. What is new
here is that I’ve added a React <tt class="docutils literal">RootComponent</tt> that has two properties, one named <tt class="docutils literal">number</tt>
and one named <tt class="docutils literal">dispatch</tt> that can be used to dispatch an action through the store.</p>
<p>Using react-redux’s <tt class="docutils literal">connect</tt> function we create a new component, <tt class="docutils literal">ConnnectedRootComponent</tt>
which is a new component with the redux-enabled functionality (i.e it will have access to
store). The <tt class="docutils literal">connect()</tt> function takes
a bunch of optional arguments. I won’t go into much detail since its a little complex
(the <a class="reference external" href="https://github.com/rackt/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options">react-redux documentation</a> is clear enough), however in our example we have defined
two objects named <tt class="docutils literal">mapStateToProps</tt> and <tt class="docutils literal">mapDispatchToProps</tt>
which are passed to <tt class="docutils literal">connect</tt> and define how the state and the dispatch are mapped to
the properties the connected component will have.</p>
<p>The <tt class="docutils literal">mapStateToProps</tt> is a function that will be called whenever the store’s state
changes and should return an object whose attributes will be passed to the connected component
as properties. In our example,
we return an object with a number attribute having the current state
(which don’t forget that is just a number) as its value -
that’s why we can extract the <tt class="docutils literal">number</tt> attribute from <tt class="docutils literal">this.props</tt> when rendering.</p>
<p>The <tt class="docutils literal">mapDispatchToProps</tt> as we use it, once again returns an object
whose attributes will be passed to the connected component and will dispatch
actions when called.</p>
<p>Of course, in order for
the <tt class="docutils literal">ConnectedRootComponent</tt> to <em>actually</em> have these properties that we passed through <tt class="docutils literal">connect</tt>,
it must
be enclosed in a <tt class="docutils literal"><Provider></tt> parent component (so that the correct react context will be initialized).
Notice that this is recursive so if we had something</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">Provider</span> <span class="na">store</span><span class="o">=</span><span class="s">{store}</span><span class="p">></span>
<span class="p"><</span><span class="nt">Component1</span><span class="p">></span>
<span class="p"><</span><span class="nt">Component2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ConnectedComponent</span><span class="p">></span>
<span class="p"></</span><span class="nt">ConnectedComponent</span><span class="p">></span>
<span class="p"></</span><span class="nt">Component2</span><span class="p">></span>
<span class="p"></</span><span class="nt">Component1</span><span class="p">></span>
<span class="p"></</span><span class="nt">Provider</span><span class="p">></span>
</pre></div>
<p>the <tt class="docutils literal"><ConnectedComponent></tt> would still get the props (dispatch + state slice) we mentioned above
even if its parent components were not connected.</p>
<p>Although we could have avoided using react-redux by passing the store directly
to <tt class="docutils literal"><RootComponent></tt> and subscibing to the store changes from the <tt class="docutils literal">RootComponent</tt>‘s <tt class="docutils literal">componentWillMount</tt> method,
the added-value of react-redux is that using <tt class="docutils literal">connect</tt> and <tt class="docutils literal">Provider</tt> we could pass dispatch and
state slices deep inside our component hierarchy without the need to explicitly pass the store
to each individual component and also that react-redux will make optimizations so that the
each connected component will be re-rendered only when needed (depending on the state slice it uses)
and not for every state change. Please be warned that this does not mean that you should connect everything
so that everything will have access to the global state and be able to dispatch actions. You should be very
careful to connect only the components that really need to be connected (redux calls them container components)
and pass dispatch and state as
properties to their children (which are called presentational components). Also, each connected component should receive only
the part of the global state it
needs and not everything (so that each particular component will update only when needed and not for
every state update). The above is absolutely necessary if you want to crate re-usable (<span class="caps">DRY</span>) and
easily testable components. I’ll discuss this a little more when
describing the sample project.</p>
<p>Finally, notice how easy it is to create reusable container
components using <tt class="docutils literal">mapStateToProps</tt> and <tt class="docutils literal">mapDispatchToProps</tt>:
Both the way the component gets its state and calls its actions are
defined through these two objects so you can create
as many connected objects as you want by passing different
<tt class="docutils literal">mapStateToProps</tt> and <tt class="docutils literal">mapDispatchToProps</tt> - more on this later.</p>
</div>
</div>
<div class="section" id="our-project">
<h2><a class="toc-backref" href="#toc-entry-6">Our project</a></h2>
<p>After this rather lengthy introduction to redux and react-redux we may move on to our
project. First of all, let’s see an example of what we’ll actually build here:</p>
<img alt="Our project" src="/images/demo3.gif" style="width: 600px;" />
<p>This is a single-page application that supports client side routing and
has four different routes: A books list, an authors list, a book editing/create
form and an author editing/create form.
The books list supports searching (with the filter field),
pagination (with 5 books per page) and per-column sorting when
clicking the column name (ascending/descending). The book form
supports validation (on the book title), cascading drop downs
(changing category limits the subcategory choices) and a
jquery ui datepicker to select the book publish date. Also,
you can delete books or authors from their corresponding
form. Notice that there’s a statistics panel showing the current
number of authors and books. A nice loading spinner
will be displayed when asynchronous ajax actions are executed
and a snackbar notification will be shown when such an action
is executed. Finally, although you won’t be able to see it,
the url of the application is changed according to the choices
the user makes.</p>
<div class="section" id="other-libraries-used">
<h3><a class="toc-backref" href="#toc-entry-7">Other libraries used</a></h3>
<p>React (and redux) have a big ecosystem of great libraries. Some of these have been used
for this project and will also be discussed in this article:</p>
<ul class="simple">
<li><a class="reference external" href="https://github.com/gaearon/redux-thunk">redux-thunk</a>: This is a nice add-on for redux that generalizes action creators.</li>
<li><a class="reference external" href="https://github.com/erikras/redux-form">redux-form</a>: A better way to use forms with react and redux. Always use it if you have non-trivial forms.</li>
<li><a class="reference external" href="https://github.com/rackt/react-router">react-router</a>: A library to create routes for single page applications with React.</li>
<li><a class="reference external" href="https://github.com/rackt/react-router-redux">react-router-redux</a> (ex redux-simple-router): This library will help integrating react-router with redux.</li>
<li><a class="reference external" href="https://github.com/rackt/history">history</a>: This is used bt react-router to crete the page history (so that back forward etc work).</li>
<li><a class="reference external" href="https://github.com/pburtchaell/react-notification">react-notification</a>: A simple react component to display notifications.</li>
</ul>
<p>The triplet react-router, react-router-redux and history needs to be used for projects that
enable client side routing. The redux-form is really useful if you have non-trivial forms
in your projects - you may skip it if you don’t use forms or for example you use a form for
searching/filtering with a single input. react-notification just displays notifications,
you can easily exchange it with other similar components or create your own.</p>
</div>
<div class="section" id="redux-thunk">
<h3><a class="toc-backref" href="#toc-entry-8">redux-thunk?</a></h3>
<p>Now, about redux-thunk. I won’t go into much detail here, you can read more about it in this <a class="reference external" href="http://stackoverflow.com/a/35415559/119071">great <span class="caps">SO</span> answer</a>,
however I’d like to point out here that <strong>everything that can be done with redux-thunk
can also be done without it</strong> so you may safely skip it if you feel that you don’t really
need it in your project.</p>
<p>But what does it do? Well, redux-thunk allows you to create action creators that don’t only return
action objects but can do various other things, like calling other actions or
dispatching actions conditionally. When using redux-thunk, an action returns a function
that can do any of the above mentioned things, something like this:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">thunkAction</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// here you may</span>
<span class="w"> </span><span class="c1">// dispatch other actions (more than one) using the provided dispatch() parameter</span>
<span class="w"> </span><span class="c1">// or</span>
<span class="w"> </span><span class="c1">// check the current state using the getState() parameter and do conditional dispatches</span>
<span class="w"> </span><span class="c1">// or</span>
<span class="w"> </span><span class="c1">// call functions asynchronously so that these will use the provided</span>
<span class="w"> </span><span class="c1">// dispatch function when they return</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Let’s say that we wanted to implement an asynchronous, ajax call.
If we don’t want to use redux thunk,
then we need to create a normal function that gets <tt class="docutils literal">dispatch</tt> as an argument, something
like this:</p>
<div class="highlight"><pre><span></span><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="nx">showLoadingAction</span><span class="p">,</span><span class="w"> </span><span class="nx">hideLoadingAction</span><span class="p">,</span><span class="w"> </span><span class="nx">showDataAction</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./actions'</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">getData</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showLoadingAction</span><span class="p">())</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">data_url</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">hideLoadingAction</span><span class="p">())</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showDataAction</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span>
<span class="w"> </span><span class="p">})</span>
<span class="p">}</span>
</pre></div>
<p>The main problem with this approach is that the getData functions <em>is not</em>
called like a normal
action creator (like <tt class="docutils literal">showLoadingAction</tt>, <tt class="docutils literal">hideLoadingAction</tt> and <tt class="docutils literal">showDataAction</tt>)
since it actually returns nothing (so nothing will be dispatched),
so you’ll need to remember to call it directly
and pass it the <tt class="docutils literal">dispatch</tt> <em>instead of</em> passing its return value to <tt class="docutils literal">dispatch</tt>!</p>
<p>If however we used thunk, then we’d have something like this:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">getDataThunk</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showLoadingAction</span><span class="p">())</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">data_url</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">hideLoadingAction</span><span class="p">())</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showDataAction</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span>
<span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Now, the above can be used like a normal action (i.e it can be called using <tt class="docutils literal"><span class="pre">dispatch(getDataThunk())</span></tt>).
That’s more or less the main advantage of redux-thunk: You are able to create thunk action creators that
can be called like normal actions and can do more complex things than just returning action objects
so you don’t have to remember how to call each function.</p>
<p>I have to repeat
again that everything that you be done with thunk action creators, can also be done with normal functions
that get <tt class="docutils literal">dispatch</tt> as a paremeter - the advantage of thunk action creators is that you don’t need to
remember if an action creator needs to be called through <tt class="docutils literal"><span class="pre">disaptch(actionCreator())</span></tt>
or <tt class="docutils literal">actionCreator(dispatch)</tt>.</p>
<p>In this tutorial you’ll see heavy use of redux-thunk. This is just my personal preference - you may
use it less or not at all (however, if you’ve configured your project to use redux-thunk then I propose
to go all the way and use it all the time for those more complex action creators).</p>
</div>
</div>
<div class="section" id="explaining-the-application">
<h2><a class="toc-backref" href="#toc-entry-9">Explaining the application</a></h2>
<p>In the following paragraphs we’ll see together the structure and source code of
this application. I’ll try to go into as much detail as possible in order to solve
any questions you may have (I know I had many when I tried setting up everything for
the first time). I’ll skip imports and non-interesting components - after all the
complete source code can be found @ <a class="reference external" href="https://github.com/spapas/react-tutorial/">https://github.com/spapas/react-tutorial/</a>,
checkout the tag <tt class="docutils literal"><span class="pre">react-redux</span></tt>.
We’ll use a top down approach, starting from the main component where the
client side routing
is defined and the application is mounted to the <span class="caps">DOM</span>:</p>
<div class="section" id="main-js">
<h3><a class="toc-backref" href="#toc-entry-10">main.js</a></h3>
<p>This module is used as an entry point for browserify (i.e we call browserify with
<tt class="docutils literal">browserify main.js <span class="pre">-o</span> bundle.js</tt> ) and uses components defined elsewhere to
create the basic structure of our application. Let’s take a look at the important
part of it:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">About</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">h2</span><span class="o">></span><span class="nx">About</span><span class="o"><</span><span class="err">/h2></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="s2">"/"</span><span class="o">></span><span class="nx">Home</span><span class="o"><</span><span class="err">/Link></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">((</span>
<span class="w"> </span><span class="o"><</span><span class="nx">Provider</span><span class="w"> </span><span class="nx">store</span><span class="o">=</span><span class="p">{</span><span class="nx">store</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Router</span><span class="w"> </span><span class="nx">history</span><span class="o">=</span><span class="p">{</span><span class="nx">history</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">App</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">IndexRoute</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">BookPanel</span><span class="p">}</span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/book_create/"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">BookForm</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/book_update/:id"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">BookForm</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/authors/"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">AuthorPanel</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/author_create/"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">AuthorForm</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/author_update/:id"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">AuthorForm</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"/about"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">About</span><span class="p">}</span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Route</span><span class="w"> </span><span class="nx">path</span><span class="o">=</span><span class="s2">"*"</span><span class="w"> </span><span class="nx">component</span><span class="o">=</span><span class="p">{</span><span class="nx">NoMatch</span><span class="p">}</span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/Route></span>
<span class="w"> </span><span class="o"><</span><span class="err">/Router></span>
<span class="w"> </span><span class="o"><</span><span class="err">/Provider></span>
<span class="w"> </span><span class="p">),</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">'content'</span><span class="p">)</span>
<span class="p">)</span>
</pre></div>
<p>We can see the well-known <tt class="docutils literal">render</tt> function from ReactDOM that gets a component
and a <span class="caps">DOM</span> element to mount it to. The domponent we provide to render is the <tt class="docutils literal">Provider</tt>
from react-redux we talked about before in order to enable all children components
to use <tt class="docutils literal">connect</tt> to have access to the store properties and dispatch. This is the usual
approact with react-redux: <em>The outer component will always be the “Provider“.</em></p>
<p>The <tt class="docutils literal">Provider</tt> component gets one parameter which is the store that redux will use. We
have initialized our store in a different module which I will present below.</p>
<p>Inside the <tt class="docutils literal">Provider</tt> we are defining a <tt class="docutils literal">Router</tt> from <tt class="docutils literal"><span class="pre">react-router</span></tt>. This should
be the parent component inside which all client-side routes of our appliccation are defined.
The <tt class="docutils literal">Router</tt> gets a <tt class="docutils literal">history</tt> parameter which is initialized elsewhere (stick with me
for now, I will talk about it later).</p>
<p>Now, inside <tt class="docutils literal">Router</tt> we are defining the actual routes of this application. As we
can see,
there’s a parent <tt class="docutils literal">Route</tt> that is connnected to the <tt class="docutils literal">App</tt> component which actually
contains everything else. The parent route contains an <tt class="docutils literal">IndexRoute</tt> whose corresponding
component (<tt class="docutils literal">BookPanel</tt>) is called
when no route is defined and a bunch of normal <tt class="docutils literal">Route</tt> components whose
components are called when the url matches their part. Notice how we pass parameters
to urls (e.g <tt class="docutils literal"><span class="pre">/book_update/:id</span></tt>) and the match-all route
(<tt class="docutils literal"><Route <span class="pre">path="*"</span> <span class="pre">component={NoMatch}/></span></tt>).</p>
<p>Finally as an example of a routed-to component, notice the <tt class="docutils literal">About</tt> component
which is rendered when the route is <tt class="docutils literal">/about</tt>. This is just a normal react component that-
will be rendered <em>inside</em> the <tt class="docutils literal">App</tt> component -
the <tt class="docutils literal">Link</tt> is a <tt class="docutils literal"><span class="pre">react-router</span></tt> component that renders a link to a defined route.</p>
</div>
<div class="section" id="store-js">
<h3><a class="toc-backref" href="#toc-entry-11">store.js</a></h3>
<p>The <tt class="docutils literal">store.js</tt> module contains the definition of the global store of our application
(which is passed to the <tt class="docutils literal">Provider</tt>).
Here, we also define the <tt class="docutils literal">history</tt> object we passed to the parent <tt class="docutils literal">Router</tt>.</p>
<div class="highlight"><pre><span></span><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">reducer</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">formReducer</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'redux-form'</span><span class="p">;</span>
<span class="k">import</span><span class="w"> </span><span class="nx">createHistory</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'history/lib/createHashHistory'</span>
<span class="c1">// Opt-out of persistent state, not recommended.</span>
<span class="c1">// https://github.com/reactjs/history/blob/master/docs/HashHistoryCaveats.md</span>
<span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">history</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">createHistory</span><span class="p">({</span>
<span class="w"> </span><span class="nx">queryKey</span><span class="o">:</span><span class="w"> </span><span class="kc">false</span>
<span class="p">});</span>
</pre></div>
<p>First of all, we see that our <tt class="docutils literal">history</tt> object is of type <tt class="docutils literal">HashHistory</tt>
(<a class="reference external" href="https://github.com/reactjs/react-router/blob/latest/docs/guides/Histories.md#hashhistory">more info about history types</a>) and I’ve also opted out of using
<tt class="docutils literal">queryKey</tt>. If I hadn’t used the <tt class="docutils literal">queryKey: false</tt> configuration
then there’d be a <tt class="docutils literal"><span class="pre">?_k=ckuvup</span></tt> query parameter in the <span class="caps">URL</span>. Now, this
parameter is actually useful (it stores location state <em>not</em> present
in the <span class="caps">URL</span> for example <span class="caps">POST</span> form data) but I don’t need it for this
example (and generally I prefer cleaner <span class="caps">URLS</span>) - but if you don’t like
the behavior of your history without it then go ahead and add it.</p>
<p>Also, notice that I’ve used <tt class="docutils literal">HashHistory</tt> which will append a <tt class="docutils literal">#</tt>
to the <span class="caps">URL</span> and the client-side <span class="caps">URL</span> will come after that, so all
URLs will be under (for example) <tt class="docutils literal">/index.html</tt> like <tt class="docutils literal"><span class="pre">/index.html#/authors</span></tt>.
The react-router
documentation recommends using <tt class="docutils literal">BrowserHistory</tt> which uses normal (clean)
urls — so instead of <tt class="docutils literal"><span class="pre">/index.html#/authors</span></tt> we’d see <tt class="docutils literal">/authors</tt> if we’d
used <tt class="docutils literal">BrowserHistory</tt>.</p>
<p>The problem with <tt class="docutils literal">BrowserHistory</tt> is that you’ll need to configure correctly
your <span class="caps">HTTP</span> server so that it will translate every <span class="caps">URL</span> (/foo) to the same
<span class="caps">URL</span> under <tt class="docutils literal">/index.html</tt> (<tt class="docutils literal"><span class="pre">/index.html#/foo</span></tt>) - as can be understood,
without that configuration the <span class="caps">HTTP</span> server doesn’t know anything about the
client side urls so when the webserver sees <tt class="docutils literal">/authors</tt> it will pass
<tt class="docutils literal">/authors</tt> to your server side framework (which will return a 404 error) - that’s
why this translation is needed.</p>
<p>However, in my case, I don’t think
that configuring your <span class="caps">HTTP</span> server to rewrite urls is worth the trouble and also I do really
prefer using <tt class="docutils literal">#</tt> for client-side urls! This is a common patter, recognised
by everybody and even without the <span class="caps">HTTP</span> server-configuration part I’d still
prefer <tt class="docutils literal">HashHistory</tt> over <tt class="docutils literal">BrowserHistory</tt> - of course this is just my opinion, feel free to use
<tt class="docutils literal">BrowserHistory</tt> if you don’t like the hash <tt class="docutils literal">#</tt>!</p>
<p>The next block of code from the <tt class="docutils literal">store</tt> module creates the
most important thing of a redux application, its reducer
along with the global store:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">reducer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">combineReducers</span><span class="p">(</span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">books</span><span class="p">,</span>
<span class="w"> </span><span class="nx">notification</span><span class="p">,</span>
<span class="w"> </span><span class="nx">ui</span><span class="p">,</span>
<span class="w"> </span><span class="nx">categories</span><span class="p">,</span>
<span class="w"> </span><span class="nx">authors</span><span class="p">,</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">routing</span><span class="o">:</span><span class="w"> </span><span class="nx">routeReducer</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">form</span><span class="o">:</span><span class="w"> </span><span class="nx">formReducer</span>
<span class="w"> </span><span class="p">})</span>
<span class="p">)</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">reduxRouterMiddleware</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">syncHistory</span><span class="p">(</span><span class="nx">history</span><span class="p">)</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">store</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">createStore</span><span class="p">(</span><span class="nx">reducer</span><span class="p">,</span><span class="w"> </span><span class="nx">applyMiddleware</span><span class="p">(</span>
<span class="w"> </span><span class="nx">thunk</span><span class="p">,</span><span class="w"> </span><span class="nx">reduxRouterMiddleware</span>
<span class="p">));</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">store</span>
</pre></div>
<p>Please notice above that the <tt class="docutils literal">Object.assign</tt> method is used - I’ll talk about
it later — however, another common <span class="caps">ES6</span> idiom that I’ve used here
is that when you define
an object you can change <tt class="docutils literal">{ x: x }</tt> to <tt class="docutils literal">{ x }</tt> (so <tt class="docutils literal">{books}</tt>
means <tt class="docutils literal">{'books': books}</tt>).</p>
<p>The <tt class="docutils literal">combineReducers</tt> function is provided
by redux and is a helper function that helps you in … combining reducers!
As you see, I’ve combined the reducers defined in this application
<tt class="docutils literal">(books, notification, ui, categories, authors)</tt> with the reducers
of <tt class="docutils literal"><span class="pre">react-router-redux</span></tt> and <tt class="docutils literal"><span class="pre">redux-form</span></tt>
to create <em>the reducer</em>. We’ll talk a bit in the next
interlude on what does combining reducers is.</p>
<p>The <tt class="docutils literal">routeReducer</tt> and <tt class="docutils literal">formReducer</tt> reducers are provided by
react-router-redux and refux-form to properly handle the routing
and form-related actions.</p>
<p>The remaining of the code generates the <tt class="docutils literal">store</tt>: First of all, a middleware
(please see next-next interlude for more)
is created with <tt class="docutils literal">syncHistory</tt> that allows actions to call history methods
(so that when the <span class="caps">URL</span> is changed through actions they will be reflected to the
history). Then, the <tt class="docutils literal">createStoreWithMiddleware</tt> function is called to generate
the store that will be passed to the <tt class="docutils literal">Provider</tt>. This function takes the
reducer as a parameter along with any store enchancers that we’d like to
apply. A store enchancer is a function that modifies the store. The only
store enchanccer that we use now is the output of the
<tt class="docutils literal">applyMiddleware</tt> function that combines the two middlewares we’ve defined (one is for
redux thunk, the other is for <tt class="docutils literal">syncHistory</tt>). I know, your head hurts but
I’ll try to clear things out below.</p>
<div class="section" id="interlude-combining-reducers">
<h4><a class="toc-backref" href="#toc-entry-12">Interlude: Combining reducers</a></h4>
<p>So, what does the <tt class="docutils literal">combineReducers</tt> function do? As we’ve already seen,
the reducer is a simple function that gets the current state and an
action as parameters and returns the next state (which is the result of applying
the action to the state). The reducer will have a big switch statement that
checks the type of the action and returns the correct new state. Unfortunately,
this switch statement may get way too fat and unmaintainable for large projects.</p>
<p>That’s where combining reducers comes to the rescue: Instead of having one big,
monolithic reducer for all the parts of our application state tree, we can break it to individual
reducers depending only on specific slices of the state object. What this means is
that if we have for example a state tree like this:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="s1">'data'</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="s1">'ui'</span><span class="o">:</span><span class="w"> </span><span class="p">{}</span>
<span class="p">}</span>
</pre></div>
<p>with actions that manipulate either data or ui, we could create two indivdual reducers,
one for the <tt class="docutils literal">data</tt> slice, and one for the <tt class="docutils literal">ui</tt> slice of the state tree. These reducers would get <em>only</em>
the slice of the state that they are interested in, so the <tt class="docutils literal">dataReducer</tt> will get
only the <tt class="docutils literal">data</tt> part of the state tree and the <tt class="docutils literal">uiReducer</tt> will get only the <tt class="docutils literal">ui</tt>
part of the state tree.</p>
<p>To <em>combine</em> these reducers the <tt class="docutils literal">combineReducers</tt> function is used. This function
gets an object with the name of the state part for each sub-reducer as attribute names
and that sub-reducer
as values and returns a reducer function that passes the action along with
the correct state slice to each of the sub-reducers and creates the global state object by
combining the output of each sub-reducer.</p>
<p>This may be clarified more with our own version of a combine reducers function:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">combineReducers2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">o</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="p">{},</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">mapped</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">o</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">k</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="nx">k</span><span class="p">,</span>
<span class="w"> </span><span class="nx">slice</span><span class="o">:</span><span class="w"> </span><span class="nx">o</span><span class="p">[</span><span class="nx">k</span><span class="p">](</span><span class="nx">state</span><span class="p">[</span><span class="nx">k</span><span class="p">],</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="c1">// call k sub-reducer and get result</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">))</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">reduced</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mapped</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">s</span><span class="p">,</span><span class="w"> </span><span class="nx">x</span><span class="p">)=>{</span>
<span class="w"> </span><span class="nx">s</span><span class="p">[</span><span class="nx">x</span><span class="p">[</span><span class="s1">'key'</span><span class="p">]]</span><span class="o">=</span><span class="nx">x</span><span class="p">[</span><span class="s1">'slice'</span><span class="p">]</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">s</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{})</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">reduced</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>The above function gets an object (<tt class="docutils literal">o</tt>) with state slices and sub-reducers
as input and returns a function (that function is the resulting reducer) that:</p>
<ul class="simple">
<li>Creates an array (named <tt class="docutils literal">mapped</tt>) of objects with two attributes: <tt class="docutils literal">key</tt> for each attribute of <tt class="docutils literal">o</tt> and <tt class="docutils literal">slice</tt> after applying the sub-reducer to the corresponding state slice</li>
<li>Reduces and returns the above array (<tt class="docutils literal">reduced</tt>) to a single object that has attributes for each state slice and the resulting state slice as values (this is actually the global state)</li>
</ul>
<p>To show-off the <span class="caps">ES6</span> code (and my most sadistic tendencies),
the above code could be also writen like this:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">combineReducers3</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">o</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="p">{},</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">o</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">k</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="nx">k</span><span class="p">,</span><span class="w"> </span><span class="nx">o</span><span class="p">[</span><span class="nx">k</span><span class="p">](</span><span class="nx">state</span><span class="p">[</span><span class="nx">k</span><span class="p">],</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span>
<span class="p">]).</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">s</span><span class="p">,</span><span class="w"> </span><span class="nx">x</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">s</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">[</span><span class="nx">x</span><span class="p">[</span><span class="mf">0</span><span class="p">]]</span><span class="o">:</span><span class="w"> </span><span class="nx">x</span><span class="p">[</span><span class="mf">1</span><span class="p">]</span>
<span class="p">}),</span><span class="w"> </span><span class="p">{})</span>
</pre></div>
<p>I wouldn’t like to explain this - its more or less a more functional version of <tt class="docutils literal">combineReducers2</tt>.</p>
</div>
<div class="section" id="interlude-middlewares">
<h4><a class="toc-backref" href="#toc-entry-13">Interlude: Middlewares</a></h4>
<p>A redux middleware is <a class="reference external" href="http://redux.js.org/docs/advanced/Middleware.html">rather difficult to explain</a> technically but easier to explain
conceptually: What it does it that it can be used to extend the store’s dispatch by providing
extra functionality. We’ve already seen such functionality, the ability to use
thunk action creators (for action creators that don’t return the next state object).</p>
<p>If you take a look at the <tt class="docutils literal">createStore</tt> function, you’ll see that
its second parameter is called <tt class="docutils literal">enhancer</tt>. When <tt class="docutils literal">enhancer</tt>
is a function (like in our case where it is the
result of <tt class="docutils literal">applyMiddleware</tt>) the return value of <tt class="docutils literal">createStore</tt>
is <tt class="docutils literal"><span class="pre">enhancer(createStore(...))</span></tt> so it will call the result of <tt class="docutils literal">applyMiddleware</tt>
with the store as parameter.</p>
<p>Now, what does <tt class="docutils literal">applyMiddleware</tt>? It gets a variable
number of functions (using the spread <tt class="docutils literal">...</tt> operator) as input arguments (let’s call
them middlewares) and returns
<em>another</em> function (this is the <tt class="docutils literal">enhancer</tt> we mentioned in the previous paragraph)
that gets a store as an input and
returns the same store with its <tt class="docutils literal">dispatch</tt> method modified so that it will
call each middleware and pass the result to the next. So, in our case the
resulting store’s dispatch function would be something like:</p>
<div class="highlight"><pre><span></span><span class="p">(</span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">reduxRouterMiddleware</span><span class="p">(</span><span class="nx">thunk</span><span class="p">(</span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">action</span><span class="p">)))</span>
</pre></div>
<p>Now, a middleware function looks like this:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">middleware</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">store</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">next</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">action</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">//</span>
<span class="p">}</span>
</pre></div>
<p>it returns a function that gets the <tt class="docutils literal">store</tt> as input
and returns another function. This returned function
gets <tt class="docutils literal">next</tt> as an input. What is next? It’s just the
next <tt class="docutils literal">dispatch</tt> function to be called. So the first middleware will have the original
store’s <tt class="docutils literal">dispatch</tt> as its <tt class="docutils literal">next</tt> parameter, the second middleware will have the
result of passing the store’s <tt class="docutils literal">dispatch</tt> from the first middleware, etc. Something like
this: <tt class="docutils literal">middleware2Dispatch(next=middleware1Dispatch(next=storeDispatch))</tt>.</p>
<p>Another
explanation of the above is that a middleware:</p>
<ul class="simple">
<li>is a function (that gets a store to enhance as input) that returns</li>
<li>another function (that gets the next dispatcher to be called as input) that returns</li>
<li>another function (that gets an action as input) which is</li>
<li>the dispatcher modified by this middleware</li>
</ul>
<p>Let’s take a look at the thunk middleware to actually see what it looks like:</p>
<div class="highlight"><pre><span></span><span class="kd">function</span><span class="w"> </span><span class="nx">thunkMiddleware</span><span class="p">({</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">next</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">action</span><span class="w"> </span><span class="p">=></span>
<span class="w"> </span><span class="ow">typeof</span><span class="w"> </span><span class="nx">action</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="s1">'function'</span><span class="w"> </span><span class="o">?</span>
<span class="w"> </span><span class="nx">action</span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="o">:</span>
<span class="w"> </span><span class="nx">next</span><span class="p">(</span><span class="nx">action</span><span class="p">);</span>
<span class="p">}</span>
</pre></div>
<p>So, it gets the store (<tt class="docutils literal">dispatch</tt> and <tt class="docutils literal">getState</tt> are store attributes)
as an input and returns a function that gets <tt class="docutils literal">next</tt> (i.e
the next dispatcher to be called) as input. This function returns <em>another function</em>
(the modified <tt class="docutils literal">dispatch</tt>). Since this function is a dispatcher, it will get
an <tt class="docutils literal">action</tt> as an input and if that action
is a function it calls this function passing it dispatch (remember how we
said if we didn’t want to use thunk then we’d just create normal functions
to which we’d pass the dispatch as a parameter - that’s what it does here!).
If this action is not a function
(so it is a normal object) it just returns <tt class="docutils literal">dispatch(action)</tt> to dispatch it.</p>
<p>Finally, we’ll create a simple middleware that will output the action type and the
state for every dispatch:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">logStateMiddleware</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">next</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">action</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// log the action type</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">())</span>
<span class="w"> </span><span class="c1">// now we must call next(action) to propagate and finally dispatch the action object</span>
<span class="w"> </span><span class="nx">next</span><span class="p">(</span><span class="nx">action</span><span class="p">)</span>
<span class="p">}</span>
</pre></div>
<p>just put it in the applyMiddleware parameter list and observe all state changes!</p>
</div>
</div>
<div class="section" id="reducers-js">
<h3><a class="toc-backref" href="#toc-entry-14">reducers.js</a></h3>
<p>This module contains the definition for our own defined sub-reducers that we combined
in the previous paragraph (<tt class="docutils literal">books, notification, ui, categories, authors</tt>) to create
the global reducer of the application. I’ve put everything in a single file, however
it is more common to create a <tt class="docutils literal">reducers</tt> directory and put every sub-reducer inside it
as a different module. Let’s start reviewing the code of the <tt class="docutils literal">reducers.js</tt> module:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">notification</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="p">{},</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'SHOW_NOTIFICATION'</span><span class="o">:</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">notification_type</span><span class="p">,</span><span class="w"> </span><span class="nx">message</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">action</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">message</span><span class="p">,</span>
<span class="w"> </span><span class="nx">notification_type</span><span class="p">,</span>
<span class="w"> </span><span class="p">})</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'CLEAR_NOTIFICATION'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">ui</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="p">{},</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'IS_LOADING'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">isLoading</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">isLoading</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'IS_SUBMITTING'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">isSubmitting</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">isSubmitting</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">notification</tt> and <cite>ui</cite> are two sub-reducers that control the state of the notification popup and if
the application is loading / is submitting. I won’t go into much detail about
them, they are really simple. However, notice that they both create a new state object
for each of their actions. To achieve this, the <tt class="docutils literal">Object.assign()</tt> method is used.
This method is defined like this:
<tt class="docutils literal">Object.assign(target, <span class="pre">...sources)</span></tt>. Its first parameter is an object (a new, empty object) while the rest
parameters (<tt class="docutils literal">sources</tt>) are other objects whose properties will be assigned <tt class="docutils literal">target</tt>. The rightmost members of
<tt class="docutils literal">sources</tt> overwrite the previous ones if they have the same names. So, for example the code</p>
<div class="highlight"><pre><span></span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">results</span><span class="p">,</span>
<span class="w"> </span><span class="nx">count</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">count</span><span class="p">,</span>
<span class="p">});</span>
</pre></div>
<p>creates a new object which will have all the properties of the current <tt class="docutils literal">state</tt> with the exception of the
<tt class="docutils literal">rows</tt> and <tt class="docutils literal">count</tt> attributes which will get their values from the <tt class="docutils literal">action</tt>. This is a common idiom in
redux and you are going to see it all the time so please make sure that you grok it before continuing. Also,
notice that the new state is a new, empty object in which
all the attributes of the new state are copied - this is because
the old state cannot be mutated.</p>
<p>Now we’ll see the reducer that handles books. Before understanding the actual reducer, I will present
the initial value of the books state slice:</p>
<div class="highlight"><pre><span></span><span class="c1">//http://stackoverflow.com/a/5158301/119071</span>
<span class="kd">function</span><span class="w"> </span><span class="nx">getParameterByName</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">var</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">RegExp</span><span class="p">(</span><span class="s1">'[?&]'</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">name</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s1">'=([^&]*)'</span><span class="p">).</span><span class="nx">exec</span><span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">hash</span><span class="p">);</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">match</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">match</span><span class="p">[</span><span class="mf">1</span><span class="p">].</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/\+/g</span><span class="p">,</span><span class="w"> </span><span class="s1">' '</span><span class="p">));</span>
<span class="p">}</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">BOOKS_INITIAL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span>
<span class="w"> </span><span class="nx">count</span><span class="o">:</span><span class="w"> </span><span class="mf">0</span><span class="p">,</span>
<span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="p">,</span>
<span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="nx">getParameterByName</span><span class="p">(</span><span class="s1">'sorting'</span><span class="p">),</span>
<span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="nx">getParameterByName</span><span class="p">(</span><span class="s1">'search'</span><span class="p">),</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="p">}</span>
</pre></div>
<p>As we see, the <tt class="docutils literal">BOOK_INITIAL</tt>
constant is used to setup an initial state for the books slice of the global state. The <tt class="docutils literal">BOOKS_INITIAL</tt>
attributs are:</p>
<ul class="simple">
<li><tt class="docutils literal">rows</tt>: The rows of the book table</li>
<li><tt class="docutils literal">count</tt>: The number of rows that are displayed</li>
<li><tt class="docutils literal">page</tt>: The current page we are on</li>
<li><tt class="docutils literal">sorting</tt>: User-defined sorting</li>
<li><tt class="docutils literal">search</tt>: User-search / filtering</li>
<li><tt class="docutils literal">book</tt>: The data of the book to be edited/displayed</li>
</ul>
<p>The <tt class="docutils literal">BOOK_INITIAL</tt> constant
gets the <tt class="docutils literal">sorting</tt> and the <tt class="docutils literal">search</tt> initial values from the <span class="caps">URL</span> to allow these parameters
to be initialized from the <span class="caps">URL</span> (so that using a url like <tt class="docutils literal"><span class="pre">#?search=foo</span></tt> will show all books
containing <tt class="docutils literal">foo</tt>). To get the parameters from the <span class="caps">URL</span> I’m using the <tt class="docutils literal">getParameterByName</tt>
function. Now, the actual reducer is:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">books</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="nx">BOOKS_INITIAL</span><span class="p">,</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">0</span><span class="p">;</span>
<span class="w"> </span><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">action</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'SHOW_BOOKS'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">results</span><span class="p">,</span>
<span class="w"> </span><span class="nx">count</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">count</span><span class="p">,</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'SHOW_BOOK'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'CHANGE_PAGE'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">page</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">page</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'TOGGLE_SORTING'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="p">.</span><span class="nx">sorting</span><span class="o">==</span><span class="nx">action</span><span class="p">.</span><span class="nx">sorting</span><span class="p">)</span><span class="o">?</span><span class="p">(</span><span class="s1">'-'</span><span class="o">+</span><span class="nx">action</span><span class="p">.</span><span class="nx">sorting</span><span class="p">)</span><span class="o">:</span><span class="nx">action</span><span class="p">.</span><span class="nx">sorting</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'CHANGE_SEARCH'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">search</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'ADD_BOOK'</span><span class="o">:</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span><span class="p">,</span>
<span class="w"> </span><span class="nx">count</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="o">+</span><span class="mf">1</span><span class="p">,</span>
<span class="w"> </span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">,</span>
<span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span><span class="p">,</span>
<span class="w"> </span><span class="p">]</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'UPDATE_BOOK'</span><span class="o">:</span>
<span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">findIndex</span><span class="p">(</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">r</span><span class="p">.</span><span class="nx">id</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">idx</span><span class="o">==-</span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span><span class="p">,</span>
<span class="w"> </span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">idx</span><span class="p">),</span>
<span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span><span class="p">,</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">idx</span><span class="o">+</span><span class="mf">1</span><span class="p">),</span>
<span class="w"> </span><span class="p">]</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'DELETE_BOOK'</span><span class="o">:</span>
<span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">findIndex</span><span class="p">(</span><span class="w"> </span><span class="nx">r</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">r</span><span class="p">.</span><span class="nx">id</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">idx</span><span class="o">==-</span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="kc">undefined</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span><span class="w"> </span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="kc">undefined</span><span class="p">,</span>
<span class="w"> </span><span class="nx">count</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">count</span><span class="o">-</span><span class="mf">1</span><span class="p">,</span>
<span class="w"> </span><span class="nx">rows</span><span class="o">:</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">idx</span><span class="p">),</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">idx</span><span class="o">+</span><span class="mf">1</span><span class="p">),</span>
<span class="w"> </span><span class="p">]</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">state</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>The books subreducer handles the <tt class="docutils literal">SHOW_BOOKS, SHOW_BOOK, CHANGE_PAGE, TOGGLE_SORTING</tt> and <tt class="docutils literal">CHANGE_SEARCH</tt>
actions by retrieving the paramaters of these actions and returning a new books-state-slice object
with the correct parameters using <tt class="docutils literal">Object.assign</tt>.</p>
<p>The <tt class="docutils literal">ADD_BOOK</tt> action is a little more complicated: This action will be dispached when a new book is added with
the data of that new book as a parameter (<tt class="docutils literal">action.book</tt>). In order to make everything easier, I just append the new
book to the end of the books that are displayed on the
current page and increase the count number (I also set the new book to be the <tt class="docutils literal">book</tt> attribute
of the state). This means that the newly created book will not go to its correct place (based on the ordering) and
that the visible items will be more than the ajax page coun (also notice that if you add another book then the visible
items will also be increased by one more). This is not a problem (for me) since if the user changes page or does a search
everything will fall back to its place. However, if you don’t like it there are two solutions, one easier and one more difficult:</p>
<ul class="simple">
<li>Easier solution: When adding a book just <em>invalidate</em> (make undefined) the <tt class="docutils literal">books</tt> state attribute. This will result in an ajax call to reload the books and everything will be in place. However the user may not see the newly added book if it does not fall to the currently selected page (and there’d be an extra, unnecessary ajax call)</li>
<li>Harder solution: Depending on the sorting you may check if the current books should be displayed or not on the current page and push it to its correct place (and remove the last item of <tt class="docutils literal">rows</tt> so that count is not increased). Once again, the newly book may no be displayed at all if it does not belong to the visible page</li>
</ul>
<p>The <tt class="docutils literal">UPDATE_BOOK</tt> and <tt class="docutils literal">DELETE_BOOK</tt> actions are even more complex. I’ll only explain update, delete is more or less
the same (with the difference that update has the updated book as an action parameter while delete has only its id
as an acton parameter): First of all we check if the updated book is currently displayed (if one of the books of
<tt class="docutils literal">rows</tt> has the same <tt class="docutils literal">id</tt> as the updated book). If the book is not displayed then only the current edited book
is set to the new state. However, if it is displayed then it would need to be updated because the <tt class="docutils literal">rows</tt> array
does not know anything about the updated values of the book!</p>
<p>So, inside the <tt class="docutils literal">else</tt> branch, the <tt class="docutils literal">idx</tt> variable will hold its current index and the
<tt class="docutils literal">rows</tt> attribute of the new state will get the following value:</p>
<div class="highlight"><pre><span></span><span class="p">[</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mf">0</span><span class="p">,</span><span class="w"> </span><span class="nx">idx</span><span class="p">),</span>
<span class="w"> </span><span class="nx">action</span><span class="p">.</span><span class="nx">book</span><span class="p">,</span>
<span class="w"> </span><span class="p">...</span><span class="nx">state</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">idx</span><span class="o">+</span><span class="mf">1</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>The <tt class="docutils literal">...</tt> spread operator expands an array so, for example <tt class="docutils literal">[ <span class="pre">...[1,2,3]</span> ]</tt> would be like <tt class="docutils literal">[1,2,3]</tt>
and the <tt class="docutils literal">slice</tt> method gets two parameters and returns a copy of the array elements between them. Using
this knowledge, we can understand that the above code returns an array (<tt class="docutils literal">[]</tt>) that contains the books of
<tt class="docutils literal">rows</tt> from the first to the updated one (not including the updated one), the updated book
object (which we get
from <tt class="docutils literal">action</tt>) and the rest of the books of <tt class="docutils literal">rows</tt> (after the updated one).</p>
<p>The code for the <tt class="docutils literal">authors</tt> and <tt class="docutils literal">categories</tt> sub-reducers does not have any surprises so I won’t go
into detail about it.</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">AUTHORS_INITIAL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// ...</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">authors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="nx">AUTHORS_INITIAL</span><span class="p">,</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// ...</span>
<span class="p">}</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">CATEGORIES_INITIAL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// ...</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">categories</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="o">=</span><span class="nx">CATEGORIES_INITIAL</span><span class="p">,</span><span class="w"> </span><span class="nx">action</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// ...</span>
<span class="p">}</span>
</pre></div>
<div class="section" id="the-global-state-tree">
<h4><a class="toc-backref" href="#toc-entry-15">The global state tree</a></h4>
<p>Remember that all the above are sub-reducers, each one taking only a slice
of the global state tree. They are all combined, along with the routing and
form reducers to create the global reducer function.</p>
<p>This also means, that the global state object will be something like this:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="nx">books</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">notification</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">ui</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">categories</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">authors</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">routing</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="w"> </span><span class="nx">form</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
<span class="p">}</span>
</pre></div>
<p>We won’t see this object anywhere because each sub-reducer will get its corresponding
slice of that object.</p>
</div>
</div>
<div class="section" id="actions-js">
<h3><a class="toc-backref" href="#toc-entry-16">actions.js</a></h3>
<p>The <tt class="docutils literal">actions.js</tt> module should probably have been named <tt class="docutils literal">action_creators.js</tt> since
it actually contains redux action creators. Also, a common practice is create a folder
named <tt class="docutils literal">actions</tt> and put there individual modules that contain action creators for
the sub-reducers (in our case, for example there would be <tt class="docutils literal">books.js</tt>, <tt class="docutils literal">authors.s</tt> etc).</p>
<p>In any case, for simplicity I chose to just use a module named <tt class="docutils literal">actions.js</tt> and put
everything there. One important thing to keep in mind is that <tt class="docutils literal">actions.js</tt> contains both
normal action creators (i.e functions that return actions and should be “dispatched”)
<em>and</em> thunk action creators (i.e
functions that not necessarily return actions but can be “dispatcher”) - please see the
discussion about redux-thunk on a previous paragraph.</p>
<p>First of all, there’s a bunch of some simple action creators that just return
the corresponding action object with the correct parameters. Notice that
the action creators that end in <tt class="docutils literal">*Result</tt> are called when an
(async) ajax request returns, for example <tt class="docutils literal">showBooksResult</tt> will be
called when the book loading has returned and pass its result data to
the reducer. The other action creators change various parts of the state
object, for example <tt class="docutils literal">loadingChanged</tt> will create an action that when
dispatched it will set <tt class="docutils literal">ui.isLoading</tt> attribute
to the action parameter.</p>
<pre class="code literal-block">
showBooksResult(books) for "SHOW_BOOKS",
showBookResult(book) for "SHOW_BOOK",
addBookResult(book) for "ADD_BOOK",
updateBookResult(book) for "UPDATE_BOOK",
deleteBookResult(id) for "DELETE_BOOK",
showAuthorsResult(authors) for "SHOW_AUTHORS",
showAuthorResult(author) for "SHOW_AUTHOR",
addAuthorResult(author) for "ADD_AUTHOR",
updateAuthorResult(author) for "UPDATE_AUTHOR",
deleteAuthorResult(id) "DELETE_AUTHOR",
showCategoriesResult(categories) for "SHOW_CATEGORIES",
showSubCategoriesResult(subcategories) for "SHOW_SUBCATEGORIES",
loadingChanged(isLoading) for "IS_LOADING",
submittingChanged(isSubmitting) for "IS_SUBMITTING",
toggleSorting(sorting) for "TOGGLE_SORTING",
changePage(page) for "CHANGE_PAGE",
changeSearch(search) for 'CHANGE_SEARCH',
showSuccessNotification(message) for 'SHOW_NOTIFICATION' (type: success),
showErrorNotification(message) for 'SHOW_NOTIFICATION', (type: error)
hideNotification() for 'CLEAR_NOTIFICATION'
</pre>
<p>The following two are thunk action creators that are called when either the
user sorting or the search/filtering parameters of the displayed books are changed:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">changeSearchAndLoadBooks</span><span class="p">(</span><span class="nx">search</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">changeSearch</span><span class="p">(</span><span class="nx">search</span><span class="p">))</span>
<span class="w"> </span><span class="nx">history</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="nx">formatUrl</span><span class="p">(</span><span class="nx">getState</span><span class="p">().</span><span class="nx">books</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadBooks</span><span class="p">())</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">toggleSortingAndLoadBooks</span><span class="p">(</span><span class="nx">sorting</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">toggleSorting</span><span class="p">(</span><span class="nx">sorting</span><span class="p">))</span>
<span class="w"> </span><span class="nx">history</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="nx">formatUrl</span><span class="p">(</span><span class="nx">getState</span><span class="p">().</span><span class="nx">books</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadBooks</span><span class="p">())</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Notice that these are thunk action creators (they return a function) and
the important thing that they do is that they call two other action creators
(<tt class="docutils literal">toggleSorting</tt> or <tt class="docutils literal">changeSearch</tt> and <tt class="docutils literal">loadBooks</tt>) and update the
<span class="caps">URL</span> using <tt class="docutils literal">history.push</tt>. The <tt class="docutils literal">history</tt> object is the one we had created in
the <tt class="docutils literal">store.js</tt> and its <tt class="docutils literal">push</tt> method changes the displayed <span class="caps">URL</span>. This
method <a class="reference external" href="https://github.com/reactjs/history/blob/master/docs/Location.md#location-descriptors">uses a location descriptor</a> that contains
an attribute for the path name and an attribute for the query parameters
- in or case we just want to update the query parameters (i.e <tt class="docutils literal"><span class="pre">#/url/?search=query1&sorting=query2</span></tt>),
so we pass an obect with only the <tt class="docutils literal">search</tt> attribute. The <tt class="docutils literal">formatUrl</tt> function, to
which the books state slice is passsed, is a rather simple function
that checks if either the sorting or the search should exist in th <span class="caps">URL</span> and
returns the full <span class="caps">URL</span>. This function is contained in the <tt class="docutils literal">util/formatters.s</tt> module.</p>
<p>The following thunk action creators are used for asynchronous, ajax queries:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">loadBooks</span><span class="p">(</span><span class="nx">page</span><span class="o">=</span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">getState</span><span class="p">();</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">page</span><span class="p">,</span><span class="w"> </span><span class="nx">sorting</span><span class="p">,</span><span class="w"> </span><span class="nx">search</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">books</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`//127.0.0.1:8000/api/books/?format=json&page=</span><span class="si">${</span><span class="nx">page</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">sorting</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">url</span><span class="o">+=</span><span class="sb">`&ordering=</span><span class="si">${</span><span class="nx">sorting</span><span class="si">}</span><span class="sb">`</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">search</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">url</span><span class="o">+=</span><span class="sb">`&search=</span><span class="si">${</span><span class="nx">search</span><span class="si">}</span><span class="sb">`</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadingChanged</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">setTimeout</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showBooksResult</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadingChanged</span><span class="p">(</span><span class="kc">false</span><span class="p">));</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="mf">1000</span><span class="p">);</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">loadBookAction</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`//127.0.0.1:8000/api/books/</span><span class="si">${</span><span class="nx">id</span><span class="si">}</span><span class="sb">/?format=json`</span><span class="p">;</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadingChanged</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showBookResult</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadingChanged</span><span class="p">(</span><span class="kc">false</span><span class="p">));</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadSubCategories</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">category</span><span class="p">));</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">loadAuthors</span><span class="p">(</span><span class="nx">page</span><span class="o">=</span><span class="mf">1</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// similar to loadBooks</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">loadAuthor</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// similar to loadBook</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">loadCategories</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// similar to loadBooks</span>
<span class="p">}</span>
<span class="k">export</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">loadSubCategories</span><span class="p">(</span><span class="nx">category</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">dispatch</span><span class="p">,</span><span class="w"> </span><span class="nx">getState</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">category</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showSubCategoriesResult</span><span class="p">([]));</span>
<span class="w"> </span><span class="k">return</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`//127.0.0.1:8000/api/subcategories/?format=json&category=</span><span class="si">${</span><span class="nx">category</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showSubCategoriesResult</span><span class="p">(</span><span class="nx">data</span><span class="p">));</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">loadBooks</tt> thunk action creator creates the <span class="caps">URL</span> parameters that should
be passed to the <span class="caps">REST</span> <span class="caps">API</span> using the <tt class="docutils literal">getState()</tt> method that returns the current state.
It then dispatches the <tt class="docutils literal">loadingChanged</tt> action so that the <tt class="docutils literal">ui.isLoading</tt> will be
changed to true. After that it asynchronously calls the load books <span class="caps">REST</span> <span class="caps">API</span> passing
it the created url and returns.
Since this is a thunk action there’s no problem that nothing is returned. When the
ajax call returns it will dispatch the <tt class="docutils literal">showBooksResult</tt>, passing the book data to
change the state with the loaded book data and the <tt class="docutils literal">loadingChanged</tt> to hide the loading
spinner. Also, please notice that I’ve put the return of the ajax call inside a <tt class="docutils literal">setTimeout</tt>
to emulate a 1 second delay and be able to see the loading spinner.</p>
<p>I may have used
setTimeout in some other places to make sure to be able to emulate server-side delays so
<em>please don’t forget to remove these “setTimeout“s from your code!</em></p>
<p>The <tt class="docutils literal">loadBook</tt> is more or less the same - however here only a single book’s data will
be loaded. When this book is loaded the <tt class="docutils literal">loadSubCategories</tt> action will also be dispatched,
passing it the loaded book’s category (so that the correct subcategories based on the category
will be displayed to the form).</p>
<p>I won’t go into any detail about the other thunk action creators, they are simpler than those
we’ve already described, except <tt class="docutils literal">loadSubCategories</tt>: This one, checks if there’s a category
and if not it will just set the displayed subcategories to and empty list (by dispatching
<tt class="docutils literal"><span class="pre">showSubCategoriesResult([])</span></tt>). If the category is not empty, it will retrieve asynchronously the
subcategories of the passed category.</p>
</div>
<div class="section" id="components-app-js">
<h3><a class="toc-backref" href="#toc-entry-17">components/app.js</a></h3>
<p>We’ll now start explaining the actual react components (modified to be used through redux of course).
The parent of all other components is the <tt class="docutils literal">App</tt> which, as we’ve already seen in <tt class="docutils literal">main.js</tt>
is connected with the parent route:</p>
<div class="highlight"><pre><span></span><span class="kd">class</span><span class="w"> </span><span class="nx">App</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">React</span><span class="p">.</span><span class="nx">Component</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">render</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">isLoading</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">ui</span><span class="p">;</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">children</span><span class="p">}</span>
<span class="w"> </span><span class="o"><</span><span class="nx">NotificationContainer</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">LoadingContainer</span><span class="w"> </span><span class="nx">isLoading</span><span class="o">=</span><span class="p">{</span><span class="nx">isLoading</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">br</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">StatPanel</span><span class="w"> </span><span class="nx">bookLength</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">count</span><span class="p">}</span><span class="w"> </span><span class="nx">authorLength</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">authors</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'button'</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="s2">"/"</span><span class="o">></span><span class="nx">Books</span><span class="o"><</span><span class="err">/Link></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'button'</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="s2">"/authors/"</span><span class="o">></span><span class="nx">Authors</span><span class="o"><</span><span class="err">/Link></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">componentDidMount</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loadBooks</span><span class="p">,</span><span class="w"> </span><span class="nx">loadAuthors</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="o">==</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">loadBooks</span><span class="p">();</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">authors</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="o">==</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">loadAuthors</span><span class="p">();</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">books</span><span class="o">:</span><span class="nx">state</span><span class="p">.</span><span class="nx">books</span><span class="p">,</span>
<span class="w"> </span><span class="nx">authors</span><span class="o">:</span><span class="nx">state</span><span class="p">.</span><span class="nx">authors</span><span class="p">,</span>
<span class="w"> </span><span class="nx">ui</span><span class="o">:</span><span class="nx">state</span><span class="p">.</span><span class="nx">ui</span><span class="p">,</span>
<span class="p">})</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">dispatch</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">bindActionCreators</span><span class="p">({</span>
<span class="w"> </span><span class="nx">loadBooks</span><span class="p">,</span><span class="w"> </span><span class="nx">loadAuthors</span>
<span class="p">},</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">)</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">connect</span><span class="p">(</span><span class="nx">mapStateToProps</span><span class="p">,</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="p">)(</span><span class="nx">App</span><span class="p">);</span>
</pre></div>
<p>As we can see, there’s an internal component (named <tt class="docutils literal">App</tt>) but we export the <tt class="docutils literal"><span class="pre">connect``ed</span> component.
One interesting thing to notice is that ``App</tt> is an <span class="caps">ES6</span> class based react component (i.e it extends
<tt class="docutils literal">React.Component</tt> — I’ll talk a bit about these components while taking a look at
the <tt class="docutils literal">BookSearchPanel</tt> which has some more interesting features).</p>
<p>Concerning the exported,
connected component, it
uses <tt class="docutils literal">mapStateToProps</tt> for defining the state attributes that should be passed as properties
to the componnt (<tt class="docutils literal"><span class="pre">state.{books,</span> authors, ui}</tt>) and <tt class="docutils literal">mapDispatchToProps</tt> for defining the <tt class="docutils literal">props</tt> methods that will
dispatch actions. To make <tt class="docutils literal">mapDispatchToProps</tt> more compact I’ve used the <tt class="docutils literal">bindActionCreators</tt> method from redux.
This method gets an object whose values are action creators and the <tt class="docutils literal">dispatch</tt> (from store) and returns an object
whose values are the dispatch-enabled corresponding action creators. So, in our case
the returned object would be something like:</p>
<div class="highlight"><pre><span></span><span class="p">{</span>
<span class="w"> </span><span class="nx">loadBooks</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadBooks</span><span class="p">()),</span>
<span class="w"> </span><span class="nx">loadAuthors</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadAuthors</span><span class="p">()),</span>
<span class="p">}</span>
</pre></div>
<p>This object of course could be created by hand, however <tt class="docutils literal">bindActionCreators</tt> would be really useful if we wanted
to dispatch lots of actions in a component (or if we had seperated our action creators to different modules) —
we could for example do something like this:</p>
<div class="highlight"><pre><span></span><span class="k">import</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="nx">actions</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'../actions'</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">dispatch</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">bindActionCreators</span><span class="p">(</span><span class="nx">actions</span><span class="p">,</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">)</span>
</pre></div>
<p>The <tt class="docutils literal">import *</tt> statemenet will create an object named item that will have all the exported actions and then
<tt class="docutils literal">bindActionCreators</tt> will return an object that dispatches these actions — passing this <tt class="docutils literal">mapDispatchToProps</tt>
to connect will allow your component to call every action and automatically dispatch it.</p>
<p>The internal component returns a <tt class="docutils literal"><div /></tt> containing, among others <tt class="docutils literal">{this.props.children}</tt> - this
will be provided by rendering the child routes. It also renders a <tt class="docutils literal">NotificationContainer</tt> to render the notifications, a
<tt class="docutils literal">LoadingContainer</tt> to display a css “loading” spinner and a <tt class="docutils literal">StatPanel</tt> to display some stats about books and
authors. It also renders two Links one for the books table and one for the authors table.</p>
<p>Beyond these, when the component is mounted it checks if the authors and books have been loaded and if not, it
dispatches the <tt class="docutils literal">loadBooks</tt> and <tt class="docutils literal">loadAuthors</tt> actions (remember, because we used <tt class="docutils literal">mapDispatchToProps</tt> by
calling these methods from <tt class="docutils literal">props</tt> they’ll be automatically dspatched when called).</p>
<p>Let’s take a quick look at the three small components that are contained in <tt class="docutils literal">App</tt></p>
<div class="section" id="components-notification-js">
<h4><a class="toc-backref" href="#toc-entry-18">components/notification.js</a></h4>
<p>This component is responsible for displaying a notification if there’s an active one.
It also defines an internal component and exports a connected version of it, passing it the
<tt class="docutils literal">notification</tt> slice of the state tree and an <tt class="docutils literal">onHide</tt> method that dispatches the
<tt class="docutils literal">hidNotification</tt> action.</p>
<p>When the internal component is rendered, it checks to see if the notification should be
displayed (<tt class="docutils literal">isActive</tt> will be true if there’s an actual message) and select the color
of the background. Finally, it passes this information along with some styling
to the real <tt class="docutils literal">Notification</tt> component from <tt class="docutils literal"><span class="pre">react-notification</span></tt>.</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">NotificationContainer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">props</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">message</span><span class="p">,</span><span class="w"> </span><span class="nx">notification_type</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">props</span><span class="p">.</span><span class="nx">notification</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">onHide</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">props</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">isActive</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">message</span><span class="o">?</span><span class="kc">true</span><span class="o">:</span><span class="kc">false</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">color</span><span class="p">;</span>
<span class="w"> </span><span class="k">switch</span><span class="p">(</span><span class="nx">notification_type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'SUCCESS'</span><span class="o">:</span>
<span class="w"> </span><span class="nx">color</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">colors</span><span class="p">.</span><span class="nx">success</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'ERROR'</span><span class="o">:</span>
<span class="w"> </span><span class="nx">color</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">colors</span><span class="p">.</span><span class="nx">danger</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">'INFO'</span><span class="o">:</span>
<span class="w"> </span><span class="nx">color</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">colors</span><span class="p">.</span><span class="nx">info</span>
<span class="w"> </span><span class="k">break</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Notification</span>
<span class="w"> </span><span class="nx">isActive</span><span class="o">=</span><span class="p">{</span><span class="nx">isActive</span><span class="p">}</span>
<span class="w"> </span><span class="nx">message</span><span class="o">=</span><span class="p">{</span><span class="nx">message</span><span class="o">?</span><span class="nx">message</span><span class="o">:</span><span class="s1">''</span><span class="p">}</span>
<span class="w"> </span><span class="nx">dismissAfter</span><span class="o">=</span><span class="p">{</span><span class="mf">5000</span><span class="p">}</span>
<span class="w"> </span><span class="nx">onDismiss</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="p">()=></span><span class="nx">onHide</span><span class="p">()</span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">action</span><span class="o">=</span><span class="s1">'X'</span>
<span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="p">()=></span><span class="nx">onHide</span><span class="p">()</span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span>
<span class="w"> </span><span class="nx">bar</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">background</span><span class="o">:</span><span class="w"> </span><span class="nx">color</span><span class="p">,</span>
<span class="w"> </span><span class="nx">color</span><span class="o">:</span><span class="w"> </span><span class="s1">'black'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">fontSize</span><span class="o">:</span><span class="w"> </span><span class="s1">'2rem'</span><span class="p">,</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">active</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">left</span><span class="o">:</span><span class="w"> </span><span class="s1">'3rem'</span><span class="p">,</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">action</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">color</span><span class="o">:</span><span class="w"> </span><span class="s1">'#FFCCBC'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">fontSize</span><span class="o">:</span><span class="w"> </span><span class="s1">'3rem'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">border</span><span class="o">:</span><span class="w"> </span><span class="s1">'1 pt solid black'</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}}</span>
<span class="w"> </span><span class="o">/></span>
<span class="p">}</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">notification</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">notification</span>
<span class="p">})</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">dispatch</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">onHide</span><span class="o">:</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">hideNotification</span><span class="p">())</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">})</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">connect</span><span class="p">(</span><span class="nx">mapStateToProps</span><span class="p">,</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="p">)(</span><span class="nx">NotificationContainer</span><span class="p">);</span>
</pre></div>
<p>Notice the <tt class="docutils literal"><span class="pre">style={{</span> .. }}</tt> snippet above: The external <tt class="docutils literal">{}</tt> are the Javascript code
inclusion tags of <span class="caps">JSX</span> while the internal <tt class="docutils literal">{}</tt> are for creating a normal javascript object
that defines the styling of the notification.</p>
</div>
<div class="section" id="creating-a-re-usable-notification">
<h4><a class="toc-backref" href="#toc-entry-19">Creating a re-usable notification</a></h4>
<p>Please notice that although I’ve implemented this as a connected component this is not the only
way to do it! Actually, probably my implementation is less-reusable from the others I will propose…</p>
<p>In any case, instead of implementing <tt class="docutils literal">NotificationContainer</tt> as a connected component we could
have implemented it as a normal, non connected component that would receive two properties
from its parent:
the <tt class="docutils literal">notification</tt> slice of state and an <tt class="docutils literal">onHide</tt> function that would dispatch
<tt class="docutils literal">hideNotification</tt>. Doing this would be very easy, just change
<tt class="docutils literal">App</tt> so that its <tt class="docutils literal">mapDispatchToProps</tt> would also return the <tt class="docutils literal">notification</tt> slice of
the state - and pass this slice as a property to the <tt class="docutils literal">NotificationContainer</tt>. Also, the
<tt class="docutils literal">onHide</tt> method should have been also defined in the <tt class="docutils literal">mapDispatchToProps</tt> of <tt class="docutils literal">App</tt> and
passed as a property to <tt class="docutils literal">NotificationContainer</tt>. Notice that this makes <tt class="docutils literal">NotificationContainer</tt>
a reusable component since we could pass anything we wanted as the <tt class="docutils literal">notification</tt> object and
<tt class="docutils literal">onHide</tt> method.</p>
<p>Also, if we needed to implement <tt class="docutils literal">NotificationContainer</tt> as a connected object but we still
needed it to be reusable we’d then export the non-connected <tt class="docutils literal">NotificationContainer</tt>
and create a bunch of <tt class="docutils literal">ConnectedNotificationContainer</tt> that would
define <tt class="docutils literal">mapStateToProps</tt> and <tt class="docutils literal">mapDispatchToProps</tt>
and export the connected component. This way, each <tt class="docutils literal">ConnecteNotificationContainer</tt> would
receive a different state slice and a different <tt class="docutils literal">onHide</tt> method, for example we may had
different notifications for books and different notifications for authors. Notice that this
approach, i.e create a reusable non-connected component and use it to create connected
components by defining their <tt class="docutils literal">mapStateToProps</tt> and <tt class="docutils literal">mapDispatchToProps</tt> is the
approach proposed by react-redux to create components.</p>
<p>Finally, one last comment on this approach that may clarify more
the purpose of <tt class="docutils literal">mapStateToProps</tt> and
<tt class="docutils literal">mapDispatchToProps</tt> of connected components
is that these two functions are <em>dual</em> (mirror):</p>
<ul class="simple">
<li>Using <tt class="docutils literal">mapStateToProps</tt> we define which parts of the state will actually be passed to the component (= reading the state).</li>
<li>Using <tt class="docutils literal">mapDispatchToProps</tt> we define the actions which will be dispatched by the component (= changing/writing the state)</li>
</ul>
</div>
<div class="section" id="components-loading-js">
<h4><a class="toc-backref" href="#toc-entry-20">components/loading.js</a></h4>
<p>This is a really simple component: If the <tt class="docutils literal">isLoading</tt> parameter is true, display a <tt class="docutils literal">div</tt> with the <tt class="docutils literal">loading</tt> class:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">({</span><span class="nx">isLoading</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">isLoading</span><span class="o">?<</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"loading"</span><span class="o">></span><span class="nx">Loading</span><span class="o">&</span><span class="err">#</span><span class="mf">8230</span><span class="p">;</span><span class="o"><</span><span class="err">/div>:null}</span>
<span class="o"><</span><span class="err">/div></span>
</pre></div>
<p>The important thing here is what the <tt class="docutils literal">loading</tt> class does to display the spinner - I’m leaving it to you to check
it at <tt class="docutils literal">static/cssloader.css</tt> (this is not my css code - I’ve copied it from <a class="reference external" href="http://codepen.io/MattIn4D/pen/LiKFC">http://codepen.io/MattIn4D/pen/LiKFC</a> ).</p>
<p>Also, please notice that in this module we just export a function, taking an object which
has an <tt class="docutils literal">isLoading</tt> attribute as a parameter. That’s a functional react component: A
function that gets a <tt class="docutils literal">params</tt> object as an input and implements the render method,
returning a component.</p>
<p>Using functional components is recommended for reasons that
are far too obvious (easy to test - just call the function, idemponent - no state
to keep track of, less code to write, easier to the eye, <em>functional</em> )
- you should use class based components only when absolutely
necessary (i.e when the component needs to keep some local state or when it needs
to do stuff on <tt class="docutils literal">componentWillMount</tt>).</p>
</div>
<div class="section" id="components-statpanel-js">
<h4><a class="toc-backref" href="#toc-entry-21">components/StatPanel.js</a></h4>
<p>Another very simple functional component - just display the number of books and authors from the passed parameter.</p>
</div>
</div>
<div class="section" id="components-bookpanel-js">
<h3><a class="toc-backref" href="#toc-entry-22">components/BookPanel.js</a></h3>
<p>Continuing our top-down approach on exploring the project, we’ll now talk
about the <tt class="docutils literal">BookPanel</tt> component which is displayed by the <tt class="docutils literal">IndexRoute</tt>.
Before talking about the actual component, I’d like to present
the <tt class="docutils literal">getCols</tt> function that is used to create an array of the columns
that will be displayed by the <tt class="docutils literal">Table</tt> we render in this panel.</p>
<p>As we can see, the <tt class="docutils literal">getCols</tt> gets one parameter which is the sort method —
this method gets a string and uses it to toggle sorting by this string.
We use a function to create the columns instead of a constant because
that <tt class="docutils literal">sort_function</tt> needs to <tt class="docutils literal">dispatch</tt> an action — <tt class="docutils literal">dispatch</tt> is available from
the <tt class="docutils literal">props</tt> that the functional <tt class="docutils literal">BookPanel</tt> component receives so the function
will be called from inside that functional component.</p>
<p>Each column, has up to four parameters:</p>
<ul class="simple">
<li>A <tt class="docutils literal">key</tt> which is the attribute of the <tt class="docutils literal">row</tt> object to display</li>
<li>A <tt class="docutils literal">title</tt> which is the column title</li>
<li>A <tt class="docutils literal">format</tt> (optional) which may be used to display the value of that column and</li>
<li>A <tt class="docutils literal">sorting</tt> (optional) which is a function that will be called when the column title is clicked (so that the sorting is changed ) - this attribute is created using the <tt class="docutils literal">sort_method</tt></li>
</ul>
<p>We’ll see how these attributes are used by the <tt class="docutils literal">Table</tt> in the corresponding section. Five
columns have been defined: <tt class="docutils literal">id</tt> (which, when clicked will update the book
that’s where <tt class="docutils literal">format</tt> is used), <tt class="docutils literal">title</tt>, <tt class="docutils literal">category_name</tt>, <tt class="docutils literal">publish_date</tt>
and <tt class="docutils literal">author_name</tt>:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">getCols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">sort_method</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'id'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'ID'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">format</span><span class="o">:</span><span class="w"> </span><span class="nx">x</span><span class="p">=></span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="p">{</span><span class="sb">`/book_update/</span><span class="si">${</span><span class="nx">x</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="sb">/`</span><span class="p">}</span><span class="o">></span><span class="p">{</span><span class="nx">x</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o"><</span><span class="err">/Link>,</span>
<span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="nx">sort_method</span><span class="p">(</span><span class="s1">'id'</span><span class="p">)</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'title'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'Title'</span><span class="p">,</span><span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="nx">sort_method</span><span class="p">(</span><span class="s1">'title'</span><span class="p">)},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'category_name'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'Category'</span><span class="p">,</span><span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="nx">sort_method</span><span class="p">(</span><span class="s1">'subcategory__name'</span><span class="p">)},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'publish_date'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'Publish date'</span><span class="p">,</span><span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="nx">sort_method</span><span class="p">(</span><span class="s1">'publish_date'</span><span class="p">)},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'author_name'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'Author'</span><span class="p">,</span><span class="w"> </span><span class="nx">sorting</span><span class="o">:</span><span class="w"> </span><span class="nx">sort_method</span><span class="p">(</span><span class="s1">'author__last_name'</span><span class="p">)},</span>
<span class="p">]</span>
</pre></div>
<p>The actual (exported) <tt class="docutils literal">BookPanel</tt> is a connected component - we need to use connect because we can’t
actually pass properties or <tt class="docutils literal">dispatch</tt> to this component since it is
rendered through a route (and not as a child of another component), so it
must be connected to the store through <tt class="docutils literal">connect</tt> to be able to receive
state attributes and <tt class="docutils literal">dispatch</tt>. We pass the <tt class="docutils literal">books</tt> state
slice as a property using <tt class="docutils literal">mapStateToProps</tt> and use the same techique as
before in <tt class="docutils literal">App</tt> with
<tt class="docutils literal">bindActionCreators</tt> to create auto-dispatchable actions.</p>
<p>As we can see, after retrieving the needed properties from the <tt class="docutils literal">books</tt> state slice
and the actions to dispatch, we define an <tt class="docutils literal">onSearchChanged</tt> function that will be
passed to the <tt class="docutils literal">BookSearchPanel</tt> to be called when the search query is changed.</p>
<p>Next, the <tt class="docutils literal">sort_method</tt> is defined. This is
a function that gets a <tt class="docutils literal">key</tt> parameter and returns another function that
dispatches <tt class="docutils literal">toggleSortingAndLoadBooks</tt> passing it that <tt class="docutils literal">key</tt>. This is the
parameter that is passed to <tt class="docutils literal">getCols</tt>. So, for example for the <tt class="docutils literal">id</tt>,
the result of the <tt class="docutils literal">sort_method</tt> would be the following function:
<tt class="docutils literal">() => <span class="pre">toggleSortingAndLoadBooks('id')</span></tt>.</p>
<p>Finally, the <tt class="docutils literal">BookPanel</tt> renders the following:</p>
<ul class="simple">
<li>A <tt class="docutils literal">BookSearchPanel</tt> passing it the <tt class="docutils literal">search</tt> property and the <tt class="docutils literal">onSearchChanged</tt> action</li>
<li>A <tt class="docutils literal">Link</tt> to create a new book</li>
<li>A <tt class="docutils literal">Table</tt> passing it the <tt class="docutils literal">sorting</tt> and <tt class="docutils literal">rows</tt> parameters and the <tt class="docutils literal">cols</tt> constant we just defined</li>
<li>A <tt class="docutils literal">PagingPanel</tt> passing it the total number of books (<tt class="docutils literal">count</tt>), the current page (<tt class="docutils literal">page</tt>) and two methods <tt class="docutils literal">onNextPage</tt> and <tt class="docutils literal">onPreviousPage</tt> that will be called when switch to the next or previous page.</li>
</ul>
<p>As we can see, the <tt class="docutils literal">onNextPage</tt> and <tt class="docutils literal">onPreviousPage</tt> functions dispach the <tt class="docutils literal">changePage</tt> action passing it
the page to change to and reload the books by dispatch <tt class="docutils literal">loadBooks</tt>. Instead of this we could create
a <tt class="docutils literal">changePageAndLoadBooks</tt> thunk action creator that would call these two methods when dispatched
(similarly to how <tt class="docutils literal">changeSearchAndLoadBooks</tt> and <tt class="docutils literal">toggleSortingAndLoadBooks</tt> have been implemented)
- I’m leaving it like this to show all possibilities:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">BookPanel</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">props</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">rows</span><span class="p">,</span><span class="w"> </span><span class="nx">count</span><span class="p">,</span><span class="w"> </span><span class="nx">page</span><span class="p">,</span><span class="w"> </span><span class="nx">sorting</span><span class="p">,</span><span class="w"> </span><span class="nx">search</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">props</span><span class="p">.</span><span class="nx">books</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loadBooks</span><span class="p">,</span><span class="w"> </span><span class="nx">changePage</span><span class="p">,</span><span class="w"> </span><span class="nx">toggleSortingAndLoadBooks</span><span class="p">,</span><span class="w"> </span><span class="nx">changeSearchAndLoadBooks</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">props</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">onSearchChanged</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">query</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">changeSearchAndLoadBooks</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">sort_method</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">key</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">toggleSortingAndLoadBooks</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">cols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">getCols</span><span class="p">(</span><span class="nx">sort_method</span><span class="p">)</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">BookSearchPanel</span><span class="w"> </span><span class="nx">search</span><span class="o">=</span><span class="p">{</span><span class="nx">search</span><span class="p">}</span><span class="w"> </span><span class="nx">onSearchChanged</span><span class="o">=</span><span class="p">{</span><span class="nx">onSearchChanged</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"row"</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"twelve columns"</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">h3</span><span class="o">></span><span class="nx">Book</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'button button-primary'</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="nx">fontSize</span><span class="o">:</span><span class="s1">'1em'</span><span class="p">}}</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="s2">"/book_create/"</span><span class="o">>+<</span><span class="err">/Link></h3></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Table</span><span class="w"> </span><span class="nx">sorting</span><span class="o">=</span><span class="p">{</span><span class="nx">sorting</span><span class="p">}</span><span class="w"> </span><span class="nx">cols</span><span class="o">=</span><span class="p">{</span><span class="nx">cols</span><span class="p">}</span><span class="w"> </span><span class="nx">rows</span><span class="o">=</span><span class="p">{</span><span class="nx">rows</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">PagingPanel</span><span class="w"> </span><span class="nx">count</span><span class="o">=</span><span class="p">{</span><span class="nx">count</span><span class="p">}</span><span class="w"> </span><span class="nx">page</span><span class="o">=</span><span class="p">{</span><span class="nx">page</span><span class="p">}</span><span class="w"> </span><span class="nx">onNextPage</span><span class="o">=</span><span class="p">{()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">changePage</span><span class="p">(</span><span class="nx">page</span><span class="o">+</span><span class="mf">1</span><span class="p">);</span>
<span class="w"> </span><span class="nx">loadBooks</span><span class="p">()</span>
<span class="w"> </span><span class="p">}}</span><span class="w"> </span><span class="nx">onPreviousPage</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">changePage</span><span class="p">(</span><span class="nx">page</span><span class="o">-</span><span class="mf">1</span><span class="p">);</span>
<span class="w"> </span><span class="nx">loadBooks</span><span class="p">()</span>
<span class="w"> </span><span class="p">}}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="p">}</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">books</span><span class="o">:</span><span class="nx">state</span><span class="p">.</span><span class="nx">books</span><span class="p">,</span>
<span class="p">})</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">dispatch</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="nx">bindActionCreators</span><span class="p">({</span>
<span class="w"> </span><span class="nx">loadBooks</span><span class="p">,</span><span class="w"> </span><span class="nx">changePage</span><span class="p">,</span><span class="w"> </span><span class="nx">toggleSortingAndLoadBooks</span><span class="p">,</span><span class="w"> </span><span class="nx">changeSearchAndLoadBooks</span>
<span class="p">},</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">)</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">connect</span><span class="p">(</span><span class="nx">mapStateToProps</span><span class="p">,</span><span class="w"> </span><span class="nx">mapDispatchToProps</span><span class="p">)(</span><span class="nx">BookPanel</span><span class="p">);</span>
</pre></div>
<div class="section" id="components-booksearchpanel-js">
<h4><a class="toc-backref" href="#toc-entry-23">components/BookSearchPanel.js</a></h4>
<p>The <tt class="docutils literal">BookSearchPanel</tt> is a component used for searching books. What
is interesting about this component is that it has internal state (i.e
state that is not reflected to the global search tree). Notice that
<tt class="docutils literal">BookSearchPanel</tt> is an <span class="caps">ES6</span> class component. Here are some of its
characteristics as opposed to non-<span class="caps">ES6</span> react components:</p>
<ul class="simple">
<li>It extends <tt class="docutils literal">React.Component</tt> instead of using <tt class="docutils literal">React.CreateClass</tt></li>
<li>It has a constructor that initializes the local state instead of implementing <tt class="docutils literal">getInitialState</tt></li>
<li>It does not automatically bind the methods to <tt class="docutils literal">this</tt> so we do it in the constructor (or else <tt class="docutils literal">this</tt> would be undefined in <tt class="docutils literal">onSearchChange</tt> and <tt class="docutils literal">onClearSearch</tt>) - <em>be very careful with that, its a common problem</em></li>
</ul>
<p>So, what happens
here? We render an <span class="caps">HTML</span> <tt class="docutils literal">input</tt> element and call <tt class="docutils literal">this.onSearchChange</tt>
method. This method retrieves the current value of the input (using <tt class="docutils literal">this.refs</tt>)
and, if the previous change was more than 400 ms ago calls the provided
(through <tt class="docutils literal">props</tt>) <tt class="docutils literal">onSearchChanged</tt> method that will dispatch the
<tt class="docutils literal">changeSearchAndLoadBooks</tt> action with the current value as a parameter
(notice however that <tt class="docutils literal">this.setState</tt> is <em>always</em> called immediately or else the
user keypresses wouldn’t be reflected to the input).</p>
<p>The whole thing with the <tt class="docutils literal">ths.promise</tt> and <tt class="docutils literal">clearInterval</tt> is to make
sure that the provided <tt class="docutils literal">onSearchChanged</tt> will not be called too often
(i.e it will be called 400 ms after the last keypress):</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nx">SearchPanel</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">React</span><span class="p">.</span><span class="nx">Component</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kr">constructor</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">super</span><span class="p">()</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">onSearchChange</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">onSearchChange</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">onClearSearch</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">onClearSearch</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">render</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"row"</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"one-fourth column"</span><span class="o">></span>
<span class="w"> </span><span class="nx">Filter</span><span class="o">:</span><span class="w"> </span><span class="o">&</span><span class="nx">nbsp</span><span class="p">;</span>
<span class="w"> </span><span class="o"><</span><span class="nx">input</span><span class="w"> </span><span class="nx">ref</span><span class="o">=</span><span class="s1">'search'</span><span class="w"> </span><span class="nx">name</span><span class="o">=</span><span class="s1">'search'</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'text'</span><span class="w"> </span><span class="nx">defaultValue</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">search</span><span class="p">}</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">search</span><span class="p">}</span><span class="w"> </span><span class="nx">onChange</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">onSearchChange</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="p">{(</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">search</span><span class="o">||</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">search</span><span class="p">)</span><span class="o">?<</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">onClearSearch</span><span class="p">}</span><span class="w"> </span><span class="o">></span><span class="nx">x</span><span class="o"><</span><span class="err">/button>:''}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">onSearchChange</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">query</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">ReactDOM</span><span class="p">.</span><span class="nx">findDOMNode</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">search</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">promise</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">clearInterval</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">promise</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="nx">query</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">promise</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">setTimeout</span><span class="p">(()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">onSearchChanged</span><span class="p">(</span><span class="nx">query</span><span class="p">),</span><span class="w"> </span><span class="mf">400</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">onClearSearch</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="s1">''</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">onSearchChanged</span><span class="p">(</span><span class="kc">undefined</span><span class="p">)</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Let’s take a closer look at the <tt class="docutils literal"><input></tt> element:</p>
<div class="highlight"><pre><span></span><span class="o"><</span><span class="nx">input</span><span class="w"> </span><span class="nx">ref</span><span class="o">=</span><span class="s1">'search'</span><span class="w"> </span><span class="nx">name</span><span class="o">=</span><span class="s1">'search'</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'text'</span><span class="w"> </span><span class="nx">defaultValue</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">search</span><span class="p">}</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">search</span><span class="p">}</span><span class="w"> </span><span class="nx">onChange</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">onSearchChange</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
</pre></div>
<p>The <tt class="docutils literal">ref</tt> property is used to reference this element using <tt class="docutils literal">ReactDOM.findDOMNode</tt> - that’s
one possible way to retrieve the value of this object. Another way would be to add an <tt class="docutils literal">event</tt>
parameter to <tt class="docutils literal">onSearchChange</tt> - this parameter would receive the <span class="caps">DOM</span> event of the change so
the value of the element could be retrieved using <tt class="docutils literal">event.target.value</tt>.</p>
<p>The difference between the <tt class="docutils literal">defaultValue</tt> and <tt class="docutils literal">value</tt> parameters is really important: The
<tt class="docutils literal">defaultValue</tt> is just the initial value of this specific input and it will be equal to
<tt class="docutils literal">props.search</tt> (so that if the user enters a <span class="caps">URL</span> which has a search query parameter this will
be pre-filled here). On the other hand, the <tt class="docutils literal">value</tt> parameter is the current value of
the element and will be equal to the <tt class="docutils literal">state.search</tt>. When the user types in the input,
the <tt class="docutils literal">onSearchChange</tt> will be called which will <em>always</em> change the <tt class="docutils literal">state.search</tt>
and the <tt class="docutils literal">value</tt> will get the correct, new value (or
else the change wouldn’t be reflected to the user)!</p>
<p>Finally concerning the clear search query button,
when there’s a search query a <tt class="docutils literal">x</tt> button will be displayed
if there’s something to the input field which, when
clicked the search local state will be cleared
and the provided <tt class="docutils literal">onSearchChanged</tt> will be called with an empty query.</p>
</div>
<div class="section" id="components-table-js">
<h4><a class="toc-backref" href="#toc-entry-24">components/Table.js</a></h4>
<p>The <tt class="docutils literal">Table</tt> is a reusable, functional react component that is used for both books and authors.</p>
<p>First of all, we define a formatHeader function that is used to format the
table header: This function gets an object with key and label as parameters (which
is the column to be formated) and a sorting parameter (which is the current table’s
sorting) and returns the label with a <tt class="docutils literal">'+'</tt> in front of it if the sorting is ascending
by this column or a <tt class="docutils literal"><span class="pre">'-'</span></tt> if the sorting is descending by this column or just the
label if this column is not used for sorting:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">formatHeader</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="p">},</span><span class="w"> </span><span class="nx">sorting</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span><span class="nx">sorting</span><span class="o">==</span><span class="nx">key</span><span class="p">)</span><span class="o">?</span><span class="p">(</span><span class="s1">'+'</span><span class="o">+</span><span class="nx">label</span><span class="p">)</span><span class="o">:</span><span class="p">(</span>
<span class="w"> </span><span class="p">(</span><span class="nx">sorting</span><span class="o">==</span><span class="s1">'-'</span><span class="o">+</span><span class="nx">key</span><span class="p">)</span><span class="o">?</span><span class="p">(</span><span class="s1">'-'</span><span class="o">+</span><span class="nx">label</span><span class="p">)</span><span class="o">:</span><span class="nx">label</span>
<span class="p">)</span>
</pre></div>
<p>The <tt class="docutils literal">Table</tt>
uses the props we mentioned before when talking about
<tt class="docutils literal">BookPanel</tt>. When it is rendered, the headers of the table are constructed by
applying a map method on the items of the <tt class="docutils literal">cols</tt> attribute. Remember that map
will apply a function to all items of a list and return a new list with the results.
So this will create a list of correctly formatted <tt class="docutils literal"><th></tt> elements.</p>
<p>In our case, the mapper
checks if each column has a <tt class="docutils literal">sorting</tt> attribute and if yes it
creates a clickable header that calls <tt class="docutils literal">sorting</tt> when clicked and is
formatted with <tt class="docutils literal">formatHeader</tt> (remember <tt class="docutils literal">sort_method</tt> we talked about
in <tt class="docutils literal">BookPanel</tt>). If there’s no <tt class="docutils literal">sorting</tt> for that column it just
displays the column header.</p>
<p>The rows of the table are created using two maps, one that maps the <tt class="docutils literal">rows</tt>
which, for each row maps <tt class="docutils literal">cols</tt> to get the individual values for this row and column.
So, for the rows a list of <tt class="docutils literal"><tr></tt> elements each one including the correct
<tt class="docutils literal"><td></tt> elements will be created:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">(</span><span class="nx">props</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">headers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">props</span><span class="p">.</span><span class="nx">cols</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">col</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">th</span><span class="w"> </span><span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">col</span><span class="p">.</span><span class="nx">key</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">col</span><span class="p">.</span><span class="nx">sorting</span><span class="o">?<</span><span class="nx">a</span><span class="w"> </span><span class="nx">href</span><span class="o">=</span><span class="s1">'#'</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="w"> </span><span class="nx">col</span><span class="p">.</span><span class="nx">sorting</span><span class="p">()</span>
<span class="w"> </span><span class="p">}}</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">formatHeader</span><span class="p">(</span><span class="nx">col</span><span class="p">,</span><span class="w"> </span><span class="nx">props</span><span class="p">.</span><span class="nx">sorting</span><span class="p">)}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/a>:col.label</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/th>)</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">rows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">props</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">row</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">tr</span><span class="w"> </span><span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">row</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">props</span><span class="p">.</span><span class="nx">cols</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">col</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">td</span><span class="w"> </span><span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">col</span><span class="p">.</span><span class="nx">key</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="p">{(</span><span class="nx">col</span><span class="p">.</span><span class="nx">format</span><span class="o">?</span><span class="nx">col</span><span class="p">.</span><span class="nx">format</span><span class="p">(</span><span class="nx">row</span><span class="p">)</span><span class="o">:</span><span class="nx">row</span><span class="p">[</span><span class="nx">col</span><span class="p">.</span><span class="nx">key</span><span class="p">])}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/td>)</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/tr>)</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">table</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">thead</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">tr</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">headers</span><span class="p">}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/tr></span>
<span class="w"> </span><span class="o"><</span><span class="err">/thead></span>
<span class="w"> </span><span class="o"><</span><span class="nx">tbody</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">rows</span><span class="p">}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/tbody></span>
<span class="w"> </span><span class="o"><</span><span class="err">/table></span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">key</tt> property I am passing to all elements that belong to a list is to
help React identify these child elements - we’d get a <tt class="docutils literal">Warning: Each
child in an array or iterator should have a unique "key" prop.</tt>
error without this property.</p>
<p>Also, please notice that the <tt class="docutils literal">const headers</tt> and <tt class="docutils literal">rows</tt> we’ve defined are there just
for clarity - we could instead put them directly inside the returned <tt class="docutils literal"><table></tt>
and have a cool, totally <em>functional function</em>!</p>
</div>
<div class="section" id="components-pagingpanel-js">
<h4><a class="toc-backref" href="#toc-entry-25">components/PagingPanel.js</a></h4>
<p>Another functional and reusable component - this one has params with the
attributes <tt class="docutils literal">page</tt>, <tt class="docutils literal">page_size</tt>, <tt class="docutils literal">count</tt>, <tt class="docutils literal">onNextPage</tt>,
<tt class="docutils literal">onPreviousPage</tt> and, after finding out the total number of pages
it renders the current page number and the total pages number along
with two buttons that will execute the <tt class="docutils literal">onNextPage</tt> and <tt class="docutils literal">onPreviousPage</tt>
that are passed as properties (these methods will dispatch the changePage and
loadBooks actions as we’ve already seen in <tt class="docutils literal">BookPanel</tt>). One thing to notice
here is that the next and previous page buttons will only be rendered if
we are not in the first or last page (so if there’s only one page you won’t
see any buttons).</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">({</span><span class="nx">page</span><span class="o">=</span><span class="mf">1</span><span class="p">,</span><span class="w"> </span><span class="nx">page_size</span><span class="o">=</span><span class="mf">5</span><span class="p">,</span><span class="w"> </span><span class="nx">count</span><span class="p">,</span><span class="w"> </span><span class="nx">onNextPage</span><span class="p">,</span><span class="w"> </span><span class="nx">onPreviousPage</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">total_pages</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">count</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">page_size</span><span class="p">);</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"row"</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">page</span><span class="o">==</span><span class="mf">1</span><span class="o">?</span><span class="kc">null</span><span class="o">:<</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="w"> </span><span class="nx">onPreviousPage</span><span class="p">();</span>
<span class="w"> </span><span class="p">}}</span><span class="o">>&</span><span class="nx">lt</span><span class="p">;</span><span class="o"><</span><span class="err">/button>}</span>
<span class="w"> </span><span class="o">&</span><span class="nx">nbsp</span><span class="p">;</span><span class="w"> </span><span class="nx">Page</span><span class="w"> </span><span class="p">{</span><span class="nx">page</span><span class="p">}</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">{</span><span class="nx">total_pages</span><span class="p">}</span><span class="w"> </span><span class="o">&</span><span class="nx">nbsp</span><span class="p">;</span>
<span class="w"> </span><span class="p">{</span><span class="nx">page</span><span class="o">==</span><span class="nx">total_pages</span><span class="o">?</span><span class="kc">null</span><span class="o">:<</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="w"> </span><span class="nx">onNextPage</span><span class="p">();</span>
<span class="w"> </span><span class="p">}}</span><span class="o">>&</span><span class="nx">gt</span><span class="p">;</span><span class="o"><</span><span class="err">/button>}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="p">}</span>
</pre></div>
<p>The same paging panel could be used for any table we wanted to have paging: Just pass it
the page number, page size, total number of items and what to do when next or previous page
buttons are clicked.</p>
</div>
<div class="section" id="interlude-a-more-functional-component">
<h4><a class="toc-backref" href="#toc-entry-26">Interlude: A more functional component</a></h4>
<p>How could we make <tt class="docutils literal">PagingPanel</tt> more functional (i.e how could we remove the
<tt class="docutils literal">const total_pages</tt> definition) ? The easy way would be to just substitute it
with its definition inside the returned <tt class="docutils literal"><div></tt> however we’d need to substitute it <em>two</em> times
so we’d loose our precious DRYness! So we’d need to think of another way.</p>
<p>People from the django world will be familiar with the <a class="reference external" href="https://docs.djangoproject.com/es/1.9/ref/templates/builtins/#with">with template tag</a>. This tag
is used in django templates to assign a complex value to a constant and use this value
instead of the complex value. Something like this</p>
<pre class="code literal-block">
{% with simple=a.complex|calculation %}
In here I can just use {{ simple }} instead of {{ a.complex|calculation }}!
{% endwith %}
</pre>
<p>Having such a concept in <span class="caps">ES6</span> would be ideal for our case! I am not sure if something
like <tt class="docutils literal">with</tt> actually exists, however we can really easy emulate it with a function
closure, something like this:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">({</span><span class="nx">page</span><span class="o">=</span><span class="mf">1</span><span class="p">,</span><span class="w"> </span><span class="nx">page_size</span><span class="o">=</span><span class="mf">5</span><span class="p">,</span><span class="w"> </span><span class="nx">count</span><span class="p">,</span><span class="w"> </span><span class="nx">onNextPage</span><span class="p">,</span><span class="w"> </span><span class="nx">onPreviousPage</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">(</span>
<span class="w"> </span><span class="nx">total_pages</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"row"</span><span class="o">></span>
<span class="w"> </span><span class="p">{</span><span class="nx">page</span><span class="o">==</span><span class="mf">1</span><span class="o">?</span><span class="kc">null</span><span class="o">:<</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="cm">/* ... */</span><span class="w"> </span><span class="p">}}</span><span class="o">>&</span><span class="nx">lt</span><span class="p">;</span><span class="o"><</span><span class="err">/button>}</span>
<span class="w"> </span><span class="o">&</span><span class="nx">nbsp</span><span class="p">;</span><span class="w"> </span><span class="nx">Page</span><span class="w"> </span><span class="p">{</span><span class="nx">page</span><span class="p">}</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">{</span><span class="nx">total_pages</span><span class="p">}</span><span class="w"> </span><span class="o">&</span><span class="nx">nbsp</span><span class="p">;</span>
<span class="w"> </span><span class="p">{</span><span class="nx">page</span><span class="o">==</span><span class="nx">total_pages</span><span class="o">?</span><span class="kc">null</span><span class="o">:<</span><span class="nx">button</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">e</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="cm">/* ... */</span><span class="w"> </span><span class="p">}}</span><span class="o">>&</span><span class="nx">gt</span><span class="p">;</span><span class="o"><</span><span class="err">/button>}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="p">)(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">count</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">page_size</span><span class="p">))</span>
</pre></div>
<p>We define a function that gets <tt class="docutils literal">total_pages</tt> as a parameter and returns <em>another function</em> (
this one is the actual render method of the <tt class="docutils literal">PagingPanel</tt>) and <em>call the outer function</em>
passing it the value we want to give to <tt class="docutils literal">total_pages</tt>. This way, the <tt class="docutils literal">total_pages</tt> will
have a value in the inner function! Thank you, function closure!!!</p>
<p>Now <tt class="docutils literal">PagingPanel</tt> is also a completely functional function component!</p>
</div>
</div>
<div class="section" id="components-bookform">
<h3><a class="toc-backref" href="#toc-entry-27">components/BookForm</a></h3>
<p>The last book-related component is <tt class="docutils literal">BookForm</tt>. This component is used to both create and update
a book. It also has a delete button for removing books. To decide if this is a create or an
update form, it relies on the parameter passed from the route - remember how the two routes have
been defined in <tt class="docutils literal">main.js</tt>:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">Route</span> <span class="na">path</span><span class="o">=</span><span class="s">"/book_create/"</span> <span class="na">component</span><span class="o">=</span><span class="s">{BookForm}</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">Route</span> <span class="na">path</span><span class="o">=</span><span class="s">"/book_update/:id"</span> <span class="na">component</span><span class="o">=</span><span class="s">{BookForm}</span> <span class="p">/></span>
</pre></div>
<p>So, although create and update render the same component, their difference
is that update will contain the <tt class="docutils literal">id</tt> of the book to be
updated in the url. This (along with any other url parameters) is passed by react-router as a property
through an object named <tt class="docutils literal">params</tt> to the <tt class="docutils literal">BookForm</tt> component, so, when updating a book
the <tt class="docutils literal">props.params.id</tt> of <tt class="docutils literal">BookForm</tt>
should have a value.</p>
<p>The <tt class="docutils literal">BookForm</tt> is a connected component (because it needs access to the state slice and dispatch and
is rendered through a route), however because it is also a redux-form, a special
method (<tt class="docutils literal">reduxForm</tt>) is used to connect the component and pass the form-related props to the component:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="p">,</span><span class="w"> </span><span class="nx">props</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">initial</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{}</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">book</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">books</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">props</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">book</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">initial</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">book</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">book</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">books</span><span class="p">.</span><span class="nx">book</span><span class="p">,</span>
<span class="w"> </span><span class="nx">categories</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">categories</span><span class="p">,</span>
<span class="w"> </span><span class="nx">authors</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">authors</span><span class="p">,</span>
<span class="w"> </span><span class="nx">ui</span><span class="o">:</span><span class="w"> </span><span class="nx">state</span><span class="p">.</span><span class="nx">ui</span><span class="p">,</span>
<span class="w"> </span><span class="nx">initialValues</span><span class="o">:</span><span class="w"> </span><span class="nx">initial</span><span class="p">,</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">reduxForm</span><span class="p">({</span>
<span class="w"> </span><span class="nx">form</span><span class="o">:</span><span class="w"> </span><span class="s1">'bookForm'</span><span class="p">,</span>
<span class="w"> </span><span class="nx">fields</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">'title'</span><span class="p">,</span><span class="w"> </span><span class="s1">'category'</span><span class="p">,</span><span class="w"> </span><span class="s1">'subcategory'</span><span class="p">,</span><span class="w"> </span><span class="s1">'publish_date'</span><span class="p">,</span><span class="w"> </span><span class="s1">'author'</span><span class="w"> </span><span class="p">],</span>
<span class="w"> </span><span class="nx">validate</span>
<span class="p">},</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="p">)(</span><span class="nx">BookForm</span><span class="p">);</span>
</pre></div>
<p>The <tt class="docutils literal">mapStateToProps</tt> contains a bunch of required things from the state (we need
the current <tt class="docutils literal">book</tt> that is edited, the <tt class="docutils literal">categories</tt> to select from, the <tt class="docutils literal">authors</tt> to also
select from and the <tt class="docutils literal">ui</tt> to find out if submitting has finished and render the buttons
as enabled/disabled). Beyond these, we see
that there’s an <tt class="docutils literal">initialValues</tt> attribute to the object returned from <tt class="docutils literal">mapStateToProps</tt>. This
attribute is used to initialize the form fields. So if our form has
fields named <tt class="docutils literal">title</tt> and <tt class="docutils literal">category</tt>, if the <tt class="docutils literal">initial</tt> object had <tt class="docutils literal">title</tt> and
<tt class="docutils literal">category</tt> attributes the form fields would be initialized by them. In our case, we just
check if the <tt class="docutils literal">props.params.id</tt> method is defined and the to-be-updated book has been loaded
to the state and just assign the to-be-updated <tt class="docutils literal">book</tt> to <tt class="docutils literal">initialValues</tt>.</p>
<p>The <tt class="docutils literal">reduxForm</tt> method is used to <tt class="docutils literal">connect</tt> the form component: Beyond the usual
<tt class="docutils literal">mapStateToProps</tt> and <tt class="docutils literal">mapDispatchToProps</tt> (we don’t actually use <tt class="docutils literal">mapDispatchToProps</tt> here
because I feel that just getting <tt class="docutils literal">dispatch</tt> is more clear),
it needs a required parameter which is the object used to initialize the redux-form form: This
object should have</p>
<ul class="simple">
<li>A <tt class="docutils literal">form</tt> attribute with the name of the form. This must be unique among all forms in your application and will be used as a parameter to the actions that redux-form will dispatch</li>
<li>A <tt class="docutils literal">fields</tt> string array with the names of the form fields. For each one of them a <tt class="docutils literal">field</tt> will be passed to the <tt class="docutils literal">props.fields</tt> array in the form component</li>
<li>A optional <tt class="docutils literal">validate</tt> attribute that is a function that will be called when the form fields are changed</li>
</ul>
<p>The validate function gets an object with the field names with their corresponding values as attributes and
should return another object with the field names that have an error and the error message. In our case,
we want the <tt class="docutils literal">title</tt> to be required and the date to be valid (if exists), so the <tt class="docutils literal">validate</tt> is:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">validate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">values</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">errors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">values</span><span class="p">.</span><span class="nx">title</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Required'</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">values</span><span class="p">.</span><span class="nx">publish_date</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">re</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sr">/^\d{4}-\d{2}-\d{2}$/</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">re</span><span class="p">.</span><span class="nx">exec</span><span class="p">(</span><span class="nx">values</span><span class="p">.</span><span class="nx">publish_date</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">publish_date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Invalid'</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">errors</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>I just used a very simple regular expression to check the validity of
the date (it must be in the format <span class="caps">YYYY</span>-<span class="caps">MM</span>-<span class="caps">DD</span>) - this is just to make
a point, for normal date rendered you should use <a class="reference external" href="http://momentjs.com/">moment.js</a>.</p>
<p>This validate function is called <em>whenever a form field is changed</em> so, depending on the implementation
of course, the error messages will be shown and hidden as the user types in the fields. Please notice
that when the user starts typing in a field in an empty form, this field may be valid but all other
fields will be empty - to avoid displaying an error message for fields that the user has not been yet
been able to modify, we can use the <tt class="docutils literal">touched</tt> property of each field — only display the
field’s error message if this field has been <tt class="docutils literal">touched</tt>. When the form is submitted all fields are
changed to <tt class="docutils literal">touched</tt> so all error messages will be displayed.</p>
<p>The internal <tt class="docutils literal">BookForm</tt> is an <span class="caps">ES6</span> class based component that needs to do some things
when the <tt class="docutils literal">componentDidMount</tt> method is called:</p>
<ul class="simple">
<li>Check if the categories have been loaded - if not dispatch the <tt class="docutils literal">loadCategories</tt> action</li>
<li>Check if the this is an update and if yes, check to see if the to-be-updated book needs to be loaded and, if it needs dispatch <tt class="docutils literal">loadBook</tt> with the book’s id</li>
</ul>
<div class="highlight"><pre><span></span><span class="kd">class</span><span class="w"> </span><span class="nx">BookForm</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">React</span><span class="p">.</span><span class="nx">Component</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">componentDidMount</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">categories</span><span class="p">.</span><span class="nx">categories</span><span class="p">.</span><span class="nx">length</span><span class="o">==</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadCategories</span><span class="p">());</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">book</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">book</span><span class="p">.</span><span class="nx">id</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadBook</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// New book</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">render</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="nx">fields</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">title</span><span class="p">,</span><span class="w"> </span><span class="nx">category</span><span class="p">,</span><span class="w"> </span><span class="nx">subcategory</span><span class="p">,</span><span class="w"> </span><span class="nx">publish_date</span><span class="p">,</span><span class="w"> </span><span class="nx">author</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">handleSubmit</span><span class="p">,</span><span class="w"> </span><span class="nx">dispatch</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">params</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">isSubmitting</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">ui</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">categories</span><span class="p">,</span><span class="w"> </span><span class="nx">subcategories</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">categories</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">authors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">authors</span><span class="p">.</span><span class="nx">rows</span><span class="p">;</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">tsubmit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">submit</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span><span class="nx">id</span><span class="p">);</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">dsubmit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">del</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="kc">undefined</span><span class="p">,</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">);</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">form</span><span class="w"> </span><span class="nx">onSubmit</span><span class="o">=</span><span class="p">{</span><span class="nx">handleSubmit</span><span class="p">(</span><span class="nx">tsubmit</span><span class="p">)}</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'row'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'six columns'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Input</span><span class="w"> </span><span class="nx">label</span><span class="o">=</span><span class="s1">'Title'</span><span class="w"> </span><span class="nx">field</span><span class="o">=</span><span class="p">{</span><span class="nx">title</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'row'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'six columns'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Select</span><span class="w"> </span><span class="nx">label</span><span class="o">=</span><span class="s1">'Category'</span><span class="w"> </span><span class="nx">field</span><span class="o">=</span><span class="p">{</span><span class="nx">category</span><span class="p">}</span><span class="w"> </span><span class="nx">options</span><span class="o">=</span><span class="p">{</span><span class="nx">categories</span><span class="p">}</span><span class="w"> </span><span class="nx">onChange</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="nx">event</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">category</span><span class="p">.</span><span class="nx">onChange</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadSubCategories</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">))</span>
<span class="w"> </span><span class="p">}}</span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'six columns'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Select</span><span class="w"> </span><span class="nx">label</span><span class="o">=</span><span class="s1">'Subcategory'</span><span class="w"> </span><span class="nx">field</span><span class="o">=</span><span class="p">{</span><span class="nx">subcategory</span><span class="p">}</span><span class="w"> </span><span class="nx">options</span><span class="o">=</span><span class="p">{</span><span class="nx">subcategories</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'row'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'six columns'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">DatePicker</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"u-full-width"</span><span class="w"> </span><span class="nx">label</span><span class="o">=</span><span class="s1">'Publish Date'</span><span class="w"> </span><span class="nx">field</span><span class="o">=</span><span class="p">{</span><span class="nx">publish_date</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'six columns'</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Select</span><span class="w"> </span><span class="nx">label</span><span class="o">=</span><span class="s1">'Author'</span><span class="w"> </span><span class="nx">field</span><span class="o">=</span><span class="p">{</span><span class="nx">author</span><span class="p">}</span><span class="w"> </span><span class="nx">options</span><span class="o">=</span><span class="p">{</span>
<span class="w"> </span><span class="nx">authors</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">a</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span><span class="s1">'id'</span><span class="o">:</span><span class="w"> </span><span class="nx">a</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="s1">'name'</span><span class="o">:</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">a</span><span class="p">.</span><span class="nx">first_name</span><span class="si">}</span><span class="sb"> </span><span class="si">${</span><span class="nx">a</span><span class="p">.</span><span class="nx">last_name</span><span class="si">}</span><span class="sb">`</span><span class="p">}))</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="o"><</span><span class="nx">button</span><span class="w"> </span><span class="nx">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">isSubmitting</span><span class="p">}</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'button button-primary'</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">handleSubmit</span><span class="p">(</span><span class="nx">tsubmit</span><span class="p">)}</span><span class="o">></span>
<span class="w"> </span><span class="nx">Save</span>
<span class="w"> </span><span class="o"><</span><span class="err">/button></span>
<span class="w"> </span><span class="p">{</span><span class="nx">id</span><span class="o">?<</span><span class="nx">button</span><span class="w"> </span><span class="nx">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">isSubmitting</span><span class="p">}</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'button'</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'button button-primary'</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="nx">backgroundColor</span><span class="o">:</span><span class="w"> </span><span class="nx">danger</span><span class="p">}}</span><span class="w"> </span><span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">dsubmit</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="nx">Delete</span>
<span class="w"> </span><span class="o"><</span><span class="err">/button>:null}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/form></span>
<span class="w"> </span><span class="p">}</span>
<span class="p">};</span>
</pre></div>
<p>The <tt class="docutils literal">render</tt> method of <tt class="docutils literal">BookForm</tt> defines (for convenience) a bunch of constants which are
attributes of <tt class="docutils literal">props</tt>:</p>
<ul class="simple">
<li>The <tt class="docutils literal">fields</tt> object contains the field attributes of the form which have been defined through the <tt class="docutils literal">reduxForm</tt> function.</li>
<li>The <tt class="docutils literal">handleSubmit</tt> is also provided by <tt class="docutils literal">reduxForm</tt> and is used to submit the form - I’ll explain it a bit later</li>
<li>The <tt class="docutils literal">dispatch</tt> is provided by <tt class="docutils literal">connect</tt> (remember, <tt class="docutils literal">reduxForm</tt> is a special <tt class="docutils literal">connect</tt>). If you don’t use <tt class="docutils literal">mapDispatchToProps</tt> then <tt class="docutils literal">connect</tt> will provide <tt class="docutils literal">dispatch</tt> to <tt class="docutils literal">props</tt> to use it as you like</li>
<li>The <tt class="docutils literal">id</tt> is from the route - it will have value when updating and will be undefined when creating a new book</li>
<li>The <tt class="docutils literal">isSubmitting</tt>, <tt class="docutils literal">categories</tt>, <tt class="docutils literal">subcategories</tt> and <tt class="docutils literal">authors</tt> are provided from the state attributes through <tt class="docutils literal">mapStateToProps</tt></li>
<li>The <tt class="docutils literal">tsubmit</tt> and <tt class="docutils literal">dsubmit</tt> are used when the form is submitted or the Delete button is clicked. As we’ll see the <tt class="docutils literal">tsubmit</tt> is passed as an argument to <tt class="docutils literal">handleSubmit</tt> while the <cite>dsubmit`</cite> is used as it is.</li>
</ul>
<p>Beyond <tt class="docutils literal">fields</tt> and <tt class="docutils literal">handleSubmit</tt> a <tt class="docutils literal">reduxForm</tt> enabled form component has various other
<a class="reference external" href="http://erikras.github.io/redux-form/#/api/props?_k=y5rbd2">properties that you can use</a>, like <tt class="docutils literal">active</tt>, <tt class="docutils literal">dirty</tt>, <tt class="docutils literal">error</tt>, <tt class="docutils literal">pristine</tt> etc.
Each <tt class="docutils literal">field</tt> provided from <tt class="docutils literal">reduxForm</tt> also has a bunch of properties, like
<tt class="docutils literal">active</tt>, <tt class="docutils literal">checked</tt>, <tt class="docutils literal">dirty</tt>, <tt class="docutils literal">error</tt>,
<tt class="docutils literal">onBlur</tt>, <tt class="docutils literal">onChange</tt>, <tt class="docutils literal">onFocus</tt>,
<tt class="docutils literal">pristine</tt>, <tt class="docutils literal">touched</tt>,
<tt class="docutils literal">valid</tt>, <tt class="docutils literal">value</tt>, <tt class="docutils literal">visited</tt>. I won’t use most of these here however please make sure
that you are familiar with these when using redux-form.</p>
<p>After defining the constants, the <tt class="docutils literal">render</tt> method returns the actual component.
Here we are using a bunch of child components we’ve defined to render the input
fields, like <tt class="docutils literal">Input</tt>, <tt class="docutils literal">DatePicker</tt> and <tt class="docutils literal">Select</tt> which will be explained
later. For each one of these components we pass the corresponding <tt class="docutils literal">field</tt>
property along with the label we want to show. There are some interesting things
in the parameters we pass to these input components:</p>
<p>All fields except <tt class="docutils literal">category</tt> use their own <tt class="docutils literal">onChange</tt>. For the <tt class="docutils literal">category</tt>
field we pass a custom <tt class="docutils literal">onChange</tt> function that will override the <tt class="docutils literal">field</tt>
onChange in order to dispatch <tt class="docutils literal">loadSubCategories</tt> when the <tt class="docutils literal">category</tt>
is changed (notice that in the custom <tt class="docutils literal">onChange</tt>
I actually call the <tt class="docutils literal">category.onChange</tt> first and then
use <tt class="docutils literal">event.target.value</tt> to get the current value of the dropdown to pass it
to the dispatched <tt class="docutils literal">loadSubCategories</tt>).</p>
<p>The <tt class="docutils literal">Select</tt> fields get an <tt class="docutils literal">options</tt> parameter which should be an array
with <tt class="docutils literal">id/name</tt> objects. For <tt class="docutils literal">authors</tt> we create that array on the fly
using <tt class="docutils literal">map</tt> (since an author object has a <tt class="docutils literal">first_name</tt> and <tt class="docutils literal">last_name</tt>).</p>
<p>The submit and delete buttons will be enabled or disabled depending on the <tt class="docutils literal">isSubmitting</tt> flag,
and will call <tt class="docutils literal">handleClick(tsubmit)</tt> or <tt class="docutils literal">dsubmit</tt> correspondingly. Also, the Delete button will
be hidden if no <tt class="docutils literal">id</tt> is provided.</p>
<p>The <tt class="docutils literal">handleSubmit</tt> method provided by <tt class="docutils literal">reduxForm</tt> will run the <tt class="docutils literal">validate</tt> function passing it
the values from the form (notice that this is synchronous validation, we could also do asynchronous -
on the server- validation for example to immediately check if a username is already used), and if
the validation does not return any errors, it will submit the form.
Submitting the form means that <tt class="docutils literal">handleSubmit</tt> will
either call <tt class="docutils literal">this.props.onSubmit</tt> <em>or</em> will call the argument of <tt class="docutils literal">handleSubmit</tt> (that’s what
we’ve done here), passing it the data of the form.</p>
<p>In our case, we want to pass the id of the book to be updated (or undefined when the form
is used to create a book) to the submit function, that’s why I am assigning
<tt class="docutils literal">submit.bind(undefined,id)</tt> to <tt class="docutils literal">tsubmit</tt> (which is what is passed to <tt class="docutils literal">handleSubmit</tt>) - this
will return a new function with the <tt class="docutils literal">id</tt> as its first argument. The <tt class="docutils literal">handleSubmit</tt> also
passes the <tt class="docutils literal">values</tt> of the form as an object along with the <tt class="docutils literal">dispatch</tt> function, so
<tt class="docutils literal">submit</tt> is a function with three arguments:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">submit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">values</span><span class="p">,</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'//127.0.0.1:8000/api/books/'</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'POST'</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`//127.0.0.1:8000/api/books/</span><span class="si">${</span><span class="nx">id</span><span class="si">}</span><span class="sb">/`</span>
<span class="w"> </span><span class="nx">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'PUT'</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">submittingChanged</span><span class="p">(</span><span class="kc">true</span><span class="p">))</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">type</span><span class="p">,</span>
<span class="w"> </span><span class="nx">url</span><span class="p">,</span>
<span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="nx">values</span><span class="p">,</span>
<span class="w"> </span><span class="nx">success</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">d</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">submittingChanged</span><span class="p">(</span><span class="kc">false</span><span class="p">))</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showSuccessNotification</span><span class="p">(</span><span class="s1">'Success!'</span><span class="p">))</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">updateBookResult</span><span class="p">(</span><span class="nx">d</span><span class="p">))</span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">addBookResult</span><span class="p">(</span><span class="nx">d</span><span class="p">))</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">routeActions</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">'/'</span><span class="p">));</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">error</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">d</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">submittingChanged</span><span class="p">(</span><span class="kc">false</span><span class="p">))</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">d</span><span class="p">);</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showErrorNotification</span><span class="p">(</span><span class="sb">`Error (</span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">status</span><span class="si">}</span><span class="sb"> - </span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">statusText</span><span class="si">}</span><span class="sb">) while saving: </span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">responseText</span><span class="si">}</span><span class="sb">`</span><span class="w"> </span><span class="p">))</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">};</span>
</pre></div>
<p>As we can see it just checks if the <tt class="docutils literal">id</tt> has a value and creates the
url and the <span class="caps">HTTP</span> method for the update (either a <tt class="docutils literal"><span class="caps">POST</span></tt> when creatign a new book or
a <tt class="docutils literal"><span class="caps">PUT</span></tt> when updating an existing one). It will then <tt class="docutils literal">dispatch</tt> the
<tt class="docutils literal">submittingChanged</tt> action to change the <span class="caps">UI</span> (disable the buttons) and do the ajax call. When
the call returns, if everything was ok it will <tt class="docutils literal">dispatch</tt> the <tt class="docutils literal">submittingChanged</tt>
(with false as a parameter), the <tt class="docutils literal">showSuccessNotification</tt> (with success as parameter),
either <tt class="docutils literal">updateBookResult</tt> or <tt class="docutils literal">addBookResult</tt> with the retrieved data as paramater
(depending if there was an <tt class="docutils literal">id</tt>) and finally it will change the <span class="caps">URL</span> to <tt class="docutils literal">/</tt> to display
the books table. If there was an error it will once again dispatch the
<tt class="docutils literal">submittingChanged</tt> action to turn off the submit flag of the state and
<tt class="docutils literal">showErrorNotification</tt> with information on the error. The url won’t change
so that the user will be able to fix the error and retry submitting.</p>
<p>The <tt class="docutils literal">del</tt> function is a little different. We bind not only with <tt class="docutils literal">id</tt> but also
with <tt class="docutils literal">dispatch</tt> because we don’t call it through <tt class="docutils literal">handleSubmit</tt>
(since when deleting no validation is actually needed)
but directly as the <tt class="docutils literal">onclick</tt> handler of the delete button
(so we must pass dispatch manually):</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">del</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">dispatch</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`//127.0.0.1:8000/api/books/</span><span class="si">${</span><span class="nx">id</span><span class="si">}</span><span class="sb">/`</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'DELETE'</span><span class="p">;</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="w"> </span><span class="nx">type</span><span class="p">,</span>
<span class="w"> </span><span class="nx">url</span><span class="p">,</span>
<span class="w"> </span><span class="nx">success</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">d</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showSuccessNotification</span><span class="p">(</span><span class="s1">'Success!'</span><span class="p">))</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">deleteBookResult</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">routeActions</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s1">'/'</span><span class="p">));</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nx">error</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">d</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showErrorNotification</span><span class="p">(</span><span class="sb">`Error (</span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">status</span><span class="si">}</span><span class="sb"> - </span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">statusText</span><span class="si">}</span><span class="sb">) while saving: </span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">responseText</span><span class="si">}</span><span class="sb">`</span><span class="w"> </span><span class="p">))</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">});</span>
<span class="p">};</span>
</pre></div>
<p>This function sends a <tt class="docutils literal"><span class="caps">DELETE</span></tt> method to the correct url and, if everything
was ok <tt class="docutils literal">dispatches</tt> the <tt class="docutils literal">showSuccessNotification</tt>, <tt class="docutils literal">deleteBookResult</tt> and
<tt class="docutils literal">routeActions.push</tt>, similarly with <tt class="docutils literal">submit</tt>.</p>
<p>One thing that I’d like to discuss here is the nature of the <tt class="docutils literal">submit</tt> and <tt class="docutils literal">del</tt>
functions: These function do dispatch other actions but they are not implemented
as thunks so, in order
to be able to actually dispatch something they need to retrieve <tt class="docutils literal">dispatch</tt>
as a parameter
(please remember the discussion on the redux-thunk section and the difference
between <tt class="docutils literal">dispatch(actionCreator)</tt> and <tt class="docutils literal">actionCreator(dispatch)</tt> ). The
<tt class="docutils literal">submit</tt> function receives <tt class="docutils literal">dispatch</tt> from <tt class="docutils literal">handleSubmit</tt> while we pass
dispatch directly (using <tt class="docutils literal">bind</tt>) to the <tt class="docutils literal">del</tt> function.</p>
<p>I could
have implemented them as thunks (and put them to the <tt class="docutils literal">actions</tt> module) however
I feel that leaving them here
will make the <span class="caps">API</span> of the application more compact (since if these functions
had been put in the actions module they would need to be exported so they’d
be a part of the public <span class="caps">API</span> of this application - however these two are only
called from <tt class="docutils literal">BookForm</tt>) and also their purpose and integration with <tt class="docutils literal">handleSubmit</tt>
is more clear if we leave them as plain functions. This is just my personal
opinion - if for example you wanted to allow deleting a book not only from the <tt class="docutils literal">BookForm</tt>
but also from the <tt class="docutils literal">BookPanel</tt> (by adding a delete button to each book row) then
you’d definitely need to export <tt class="docutils literal">del</tt> as an action creator (preferably as a thunk action
creator to be consistent with the others).</p>
<div class="section" id="components-input">
<h4><a class="toc-backref" href="#toc-entry-28">components/Input</a></h4>
<p>This is a simple functional component that gets a redux-form
text field and a label as properties and renders a text
input with a <tt class="docutils literal"><label></tt> and an optional error message.
The error message will only be rendered if the field
has been touched (i.e the user has changed the field or
the form has been submitted) and there’s an actual error:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">({</span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">label</span><span class="w"> </span><span class="nx">forHtml</span><span class="o">=</span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">></span><span class="p">{</span><span class="nx">label</span><span class="p">}</span><span class="o"><</span><span class="err">/label></span>
<span class="w"> </span><span class="o"><</span><span class="nx">input</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'text'</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"u-full-width"</span><span class="w"> </span><span class="p">{...</span><span class="nx">field</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">touched</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">field</span><span class="p">.</span><span class="nx">error</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="nx">color</span><span class="o">:</span><span class="w"> </span><span class="s1">'white'</span><span class="p">,</span><span class="w"> </span><span class="nx">backgroundColor</span><span class="o">:</span><span class="w"> </span><span class="nx">danger</span><span class="p">}}</span><span class="o">></span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">error</span><span class="p">}</span><span class="o"><</span><span class="err">/div>}</span>
<span class="o"><</span><span class="err">/div></span>
</pre></div>
<p>One thing I’d like to explain is the <tt class="docutils literal"><span class="pre">{...field}</span></tt> snippet
I pass to <tt class="docutils literal"><input></tt>. This is the object spread operator and will
convert each attribute of the <tt class="docutils literal">field</tt> object to a corresponding <tt class="docutils literal">attr=value</tt>
pair, i.e</p>
<div class="highlight"><pre><span></span><span class="o"><</span><span class="nx">input</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="nx">name</span><span class="o">=</span><span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="w"> </span><span class="nx">onChange</span><span class="o">=</span><span class="nx">field</span><span class="p">.</span><span class="nx">onChange</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="nx">field</span><span class="p">.</span><span class="nx">value</span><span class="w"> </span><span class="nx">onBlur</span><span class="o">=</span><span class="nx">field</span><span class="p">.</span><span class="nx">OnBlur</span><span class="w"> </span><span class="nx">etc</span><span class="w"> </span><span class="o">/></span>
</pre></div>
</div>
<div class="section" id="components-select">
<h4><a class="toc-backref" href="#toc-entry-29">components/Select</a></h4>
<p>The <tt class="docutils literal">Select</tt> component renders a dropdown (select) input.
It should receive a redux-form field, a label and an array
of objects with <tt class="docutils literal">id</tt> and <tt class="docutils literal">name</tt> attributes that will be
rendered as the options. The <tt class="docutils literal">label</tt> and <tt class="docutils literal">error` will be
rendered similarly to ``Input</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">({</span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="p">,</span><span class="w"> </span><span class="nx">options</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">label</span><span class="w"> </span><span class="nx">forHtml</span><span class="o">=</span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">></span><span class="p">{</span><span class="nx">label</span><span class="p">}</span><span class="o"><</span><span class="err">/label></span>
<span class="w"> </span><span class="o"><</span><span class="nx">select</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'text'</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"u-full-width"</span><span class="w"> </span><span class="p">{...</span><span class="nx">field</span><span class="p">}</span><span class="w"> </span><span class="p">{...</span><span class="nx">props</span><span class="p">}</span><span class="w"> </span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">option</span><span class="o">><</span><span class="err">/option></span>
<span class="w"> </span><span class="p">{</span><span class="nx">options</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">option</span><span class="w"> </span><span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="w"> </span><span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="w"> </span><span class="o">></span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o"><</span><span class="err">/option>)}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/select></span>
<span class="w"> </span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">touched</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">field</span><span class="p">.</span><span class="nx">error</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="nx">color</span><span class="o">:</span><span class="w"> </span><span class="s1">'white'</span><span class="p">,</span><span class="w"> </span><span class="nx">backgroundColor</span><span class="o">:</span><span class="w"> </span><span class="nx">danger</span><span class="p">}}</span><span class="o">></span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">error</span><span class="p">}</span><span class="o"><</span><span class="err">/div>}</span>
<span class="o"><</span><span class="err">/div></span>
</pre></div>
<p>For the options we include an empty option (as a default value) and the other
options are created with the help of a <tt class="docutils literal">map</tt>. Finally, notice that I have
also used <tt class="docutils literal"><span class="pre">...props</span></tt> here in the the function parameter list to capture all parameters not
captured by <tt class="docutils literal">field</tt>, <tt class="docutils literal">label</tt> and <tt class="docutils literal">options</tt> and then pass
both <tt class="docutils literal"><span class="pre">{...field}</span></tt> and <tt class="docutils literal"><span class="pre">{...props}</span></tt> to the <tt class="docutils literal">select</tt> component. This is
to capture the custom <tt class="docutils literal">onChange</tt> (that I pass for the categories <tt class="docutils literal">Select</tt>)
and use that custom <tt class="docutils literal">onChange</tt> when the select value changes. The custom
<tt class="docutils literal">onChange</tt> will override the <tt class="docutils literal">field.onChange</tt> because the {…props} is
<em>after</em> {…field}, so the resulting select will be something like:</p>
<div class="highlight"><pre><span></span><span class="o"><</span><span class="nx">select</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="nx">onChange</span><span class="o">=</span><span class="nx">field</span><span class="p">.</span><span class="nx">onChange</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="nx">onChange</span><span class="o">=</span><span class="nx">props</span><span class="p">.</span><span class="nx">onChange</span><span class="w"> </span><span class="o">></span>
</pre></div>
<p>This is
a common idiom for overriding properties of objects that are passed
to components - for example I could pass a <tt class="docutils literal">className</tt> property to
<tt class="docutils literal"><Select></tt> to override the default one (<tt class="docutils literal"><span class="pre">className="u-full-width"</span></tt>).</p>
</div>
<div class="section" id="components-datepicker">
<h4><a class="toc-backref" href="#toc-entry-30">components/DatePicker</a></h4>
<p>This component is used to render a jquery-ui datepicker. Similarly
to the other input components it receives a redux-form <tt class="docutils literal">field</tt>
and a <tt class="docutils literal">label</tt>. However, this is a
class based component because it needs to have <tt class="docutils literal">this</tt> for attaching
the <tt class="docutils literal">datepicker</tt> to an input. Beyond the normal rendering, we can
see that we have added a <tt class="docutils literal"><span class="pre">ref='date'</span></tt> to the <tt class="docutils literal">input</tt> to allow
us to refer to it later. This ref is used by <tt class="docutils literal">componentDidMount</tt>
and <tt class="docutils literal">handleChange</tt>:</p>
<div class="highlight"><pre><span></span><span class="kd">class</span><span class="w"> </span><span class="nx">DatePicker</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">React</span><span class="p">.</span><span class="nx">Component</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">render</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span>
<span class="w"> </span><span class="k">return</span><span class="p">(</span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">label</span><span class="w"> </span><span class="nx">forHtml</span><span class="o">=</span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">></span><span class="p">{</span><span class="nx">label</span><span class="p">}</span><span class="o"><</span><span class="err">/label></span>
<span class="w"> </span><span class="o"><</span><span class="nx">input</span><span class="w"> </span><span class="nx">type</span><span class="o">=</span><span class="s1">'text'</span><span class="w"> </span><span class="nx">ref</span><span class="o">=</span><span class="s1">'date'</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"u-full-width"</span><span class="w"> </span><span class="p">{...</span><span class="nx">field</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">touched</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="nx">field</span><span class="p">.</span><span class="nx">error</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="nx">color</span><span class="o">:</span><span class="w"> </span><span class="s1">'white'</span><span class="p">,</span><span class="w"> </span><span class="nx">backgroundColor</span><span class="o">:</span><span class="w"> </span><span class="nx">danger</span><span class="p">}}</span><span class="o">></span><span class="p">{</span><span class="nx">field</span><span class="p">.</span><span class="nx">error</span><span class="p">}</span><span class="o"><</span><span class="err">/div>}</span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="w"> </span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">componentDidMount</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="nx">ReactDOM</span><span class="p">.</span><span class="nx">findDOMNode</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">date</span><span class="p">)).</span><span class="nx">datepicker</span><span class="p">({</span><span class="w"> </span><span class="nx">dateFormat</span><span class="o">:</span><span class="w"> </span><span class="s1">'yy-mm-dd'</span><span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="nx">$</span><span class="p">(</span><span class="nx">ReactDOM</span><span class="p">.</span><span class="nx">findDOMNode</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">date</span><span class="p">)).</span><span class="nx">on</span><span class="p">(</span><span class="s1">'change'</span><span class="p">,</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">handleChange</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">componentWillUnmount</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">handleChange</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">()</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">date</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">ReactDOM</span><span class="p">.</span><span class="nx">findDOMNode</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">date</span><span class="p">).</span><span class="nx">value</span>
<span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">field</span><span class="p">.</span><span class="nx">onChange</span><span class="p">(</span><span class="nx">date</span><span class="p">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">componentDidMount</tt> retrieves the input <span class="caps">DOM</span> element through the <tt class="docutils literal">ref</tt>
and makes it a datepicker. It also sets its <tt class="docutils literal">onchange</tt> method to the
<tt class="docutils literal">handleChange</tt> method (notice the <tt class="docutils literal">bind(this)</tt> part — this is needed
so that <tt class="docutils literal">this</tt> will be defined correctly inside the <tt class="docutils literal">handleChange</tt>). The
<tt class="docutils literal">handleChange</tt> retrieves the current date (once again from the <tt class="docutils literal">ref</tt>)
and just calls the <tt class="docutils literal">onChange</tt> of the provided <tt class="docutils literal">field</tt>, passing it the
date value.</p>
</div>
<div class="section" id="should-i-create-my-own-input-components">
<h4><a class="toc-backref" href="#toc-entry-31">Should I create my own input components?</a></h4>
<p>As you’ve seen, I’ve created my <em>own</em> custom input components. These components
are created with the correct styling for the css framework I use here (<tt class="docutils literal">skeleton.css</tt>) but
of course with small changes could easily be modified to be used with other css frameworks
(I am using <tt class="docutils literal">bootstrap 3</tt> in most of my normal apps and such components work great).
They also have been created for exactly my needs (i.e get the <tt class="docutils literal"><span class="pre">redux-form</span></tt> field as input,
allow overriding onChange etc).</p>
<p>Instead of creating your own components by hand, you could of course use some specific component
libraries like <a class="reference external" href="http://www.material-ui.com/#/">material-ui</a> or <a class="reference external" href="https://react-bootstrap.github.io/">react-bootstrap</a>. These libraries contain components
such as the <tt class="docutils literal">Input</tt> or <tt class="docutils literal">Select</tt> we implemented here with a consistent <span class="caps">API</span> and
styling. Unfortunately, these components are a little more difficult to use than
just creating your own:</p>
<ul class="simple">
<li>You need to learn their <span class="caps">API</span> (the names of the properties they get, their behavior in various conditions etc)</li>
<li>You need to learn their styling <span class="caps">API</span> (most of them make it difficult to customize their appearence)</li>
<li>It is not very easy to integrate them to your existing css framework (if you have one), so you’ll need to go all the way to use their styles</li>
<li>You may need to use custom components anyway because you want to use a different javascript component that is not provided by these libraries</li>
</ul>
<p>It all boils down to how big is your project and if you already have some styling for
your pages. If you want to build a rather small project or your project already has a consistent
styling then its better to create the required input components by hand. If on the other hand
you want to build a big project from scratch then probably it would be better to bite the
bullet and use a component library, but keep in mind that you may actually need to create
your own components.</p>
</div>
</div>
<div class="section" id="components-authorpanel">
<h3><a class="toc-backref" href="#toc-entry-32">components/AuthorPanel</a></h3>
<p>The <tt class="docutils literal">AuthorPanel</tt> uses <tt class="docutils literal">Table</tt> to list the authors. Since I haven’t used
pagination or sorting here the component is very simple:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">cols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'id'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'id'</span><span class="p">,</span><span class="w"> </span><span class="nx">format</span><span class="o">:</span><span class="w"> </span><span class="nx">x</span><span class="p">=></span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="p">{</span><span class="sb">`/author_update/</span><span class="si">${</span><span class="nx">x</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="sb">/`</span><span class="p">}</span><span class="o">></span><span class="p">{</span><span class="nx">x</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o"><</span><span class="err">/Link>},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'last_name'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'Last name'</span><span class="p">,},</span>
<span class="w"> </span><span class="p">{</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">'first_name'</span><span class="p">,</span><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">'First name'</span><span class="p">,},</span>
<span class="p">]</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">AuthorPanel</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">props</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"row"</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">div</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s2">"twelve columns"</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">h3</span><span class="o">></span><span class="nx">Author</span><span class="w"> </span><span class="nx">list</span><span class="w"> </span><span class="o"><</span><span class="nx">Link</span><span class="w"> </span><span class="nx">className</span><span class="o">=</span><span class="s1">'button button-primary'</span><span class="w"> </span><span class="nx">style</span><span class="o">=</span><span class="p">{{</span><span class="nx">fontSize</span><span class="o">:</span><span class="s1">'1em'</span><span class="p">}}</span><span class="w"> </span><span class="nx">to</span><span class="o">=</span><span class="s2">"/author_create/"</span><span class="o">>+<</span><span class="err">/Link></h3></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Table</span><span class="w"> </span><span class="nx">cols</span><span class="o">=</span><span class="p">{</span><span class="nx">cols</span><span class="p">}</span><span class="w"> </span><span class="nx">rows</span><span class="o">=</span><span class="p">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">authors</span><span class="p">.</span><span class="nx">rows</span><span class="p">}</span><span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/div></span>
<span class="o"><</span><span class="err">/div></span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mapStateToProps</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">state</span><span class="p">)</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">({</span>
<span class="w"> </span><span class="nx">authors</span><span class="o">:</span><span class="nx">state</span><span class="p">.</span><span class="nx">authors</span><span class="p">,</span>
<span class="p">})</span>
</pre></div>
</div>
<div class="section" id="components-authorform">
<h3><a class="toc-backref" href="#toc-entry-33">components/AuthorForm</a></h3>
<p>The <tt class="docutils literal">AuthorForm</tt> is similar to <tt class="docutils literal">BookForm</tt> and is used to
create a new author, update or delete an existing one. It uses
the <tt class="docutils literal">Input</tt> component to edit the first and last name
of each author. I won’t go into more detail about this
component since everything must be clear by now.</p>
</div>
</div>
<div class="section" id="changing-the-ui-when-the-data-is-changed">
<h2><a class="toc-backref" href="#toc-entry-34">Changing the <span class="caps">UI</span> when the data is changed</a></h2>
<p>Commenter Tomas Jacobsen asked for a tutorial on <a class="reference external" href="http://spapas.github.io/2015/09/08/more-complex-react-flux-example/#comment-2667922265">how the <span class="caps">UI</span> will be updated when the data is changed in a different window</a>. Well,
requirements like this is actually the reason for using react and redux I decided to add that capability to this application. If you want
to check it out, please <tt class="docutils literal">git checkout</tt> the tag <tt class="docutils literal"><span class="pre">react-redux-poll-update</span></tt> of the same repository (<a class="reference external" href="https://github.com/spapas/react-tutorial/">https://github.com/spapas/react-tutorial/</a>).</p>
<p>This extra functionality simply checks (using polling) every few seconds if the number of authors has changed and if it has, it will reload the authors
and dispaly a notification. Beyond the <span class="caps">ES6</span> code, I’ve added a small <span class="caps">REST</span> <span class="caps">API</span> view that returns the number of authors in the database (it just
returns a number) and a django management command that adds an author. So, to test the new functionality, run
the django application and thhen run <tt class="docutils literal">python manage.py add_author</tt> in a different window - after a few seconds you should see that the
authors have been updated along with a notification.</p>
<p>To implement the <span class="caps">UI</span> update in <span class="caps">ES6</span>, I’ve added a file named <tt class="docutils literal">scheduler.js</tt> with the following contents:</p>
<div class="highlight"><pre><span></span><span class="k">import</span><span class="w"> </span><span class="nx">store</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./store'</span>
<span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="nx">loadAuthors</span><span class="p">,</span><span class="w"> </span><span class="nx">showSuccessNotification</span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./actions'</span><span class="p">;</span>
<span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Starting scheduler"</span><span class="p">)</span>
<span class="w"> </span><span class="nb">window</span><span class="p">.</span><span class="nx">setInterval</span><span class="p">(</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'http://127.0.0.1:8000/api/authors/get_author_number/'</span><span class="p">;</span>
<span class="w"> </span><span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span><span class="w"> </span><span class="nx">realAuthorNumber</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">authorNumber</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">authors</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">authorNumber</span><span class="o">!=</span><span class="nx">realAuthorNumber</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">showSuccessNotification</span><span class="p">(</span><span class="s2">"Authors have changed - reloading ..."</span><span class="p">));</span>
<span class="w"> </span><span class="nx">store</span><span class="p">.</span><span class="nx">dispatch</span><span class="p">(</span><span class="nx">loadAuthors</span><span class="p">());</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="mf">2000</span><span class="p">);</span>
<span class="p">};</span>
</pre></div>
<p>and then just import the default function from <tt class="docutils literal">main.js</tt> and call it:</p>
<div class="highlight"><pre><span></span><span class="k">import</span><span class="w"> </span><span class="nx">schedule</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">'./scheduler'</span><span class="p">;</span>
<span class="nx">schedule</span><span class="p">();</span>
</pre></div>
<p>So, we can see that every 2 seconds we retrieve (using Ajax) the number of authors and,
if their number is different than the current number of authors in the <span class="caps">UI</span> (which we retrieve from
the store using its <tt class="docutils literal">getState</tt> method) display the notification and reload the authors (by
dispatching two actions (which are created through the corresponding actions creators)! That’s
all that is needed to update the <span class="caps">UI</span>!</p>
<p>Notice that, although I’ve used ajax and polling to find out when the <span class="caps">UI</span> needs to be updated
you could instead use whatever different method you like, for example WebSockets.</p>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-35">Conclusion</a></h2>
<p>In the above sections we presented and explained
a more or less complete single page react / redux
application, <em>almost</em> ready to be deployed to production.
I tried to explain every concept I came across that’s why
this article became more fat that I was expecting when I
started writing it! The presented application supports nearly
everything you’ll want to use when creating your own apps:</p>
<ul class="simple">
<li>Complex forms</li>
<li>Custom components</li>
<li>Asynchronous actions / Ajax</li>
<li>Creating / updating / deleting objects</li>
<li>Client side routing</li>
<li>Result lists with pagination, sorting and filtering</li>
</ul>
<p>The above have been implemented using the following technologies / libraries:</p>
<ul class="simple">
<li>Django / django-rest-framework</li>
<li><span class="caps">ES6</span> with babel</li>
<li>browserify / watchify / babelify</li>
<li>React / redux / react-redux</li>
<li>redux-thunk / redux-form / react-router-redux / react-notification</li>
</ul>
<p>What could be missing from the application we presented here:</p>
<ul class="simple">
<li>Tests!</li>
<li>Integrating <a class="reference external" href="https://github.com/gaearon/redux-devtools">redux-devtools</a></li>
<li>Using envify to seperate development/production client side code (this is needeed if you actually integrate redux-devtools)</li>
<li>Intagration with a component library</li>
</ul>
<p>I advice you to research these subjects a bit - I’ll also try to
write another (hopefully thinner) post with more info
about these.</p>
<p>Finally, two thing I’d like to point out and keep in mind are that using
redux/react-redux the flow of the data is <em>crystal</em> and that as you’ve already
seen by now, writing functional components and reducers is <em>pure fun</em>!</p>
</div>
Ajax data with Fixed Data Table for React2015-12-22T15:20:00+02:002015-12-22T15:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2015-12-22:/2015/12/22/ajax-with-react-fixed-data-table/<p class="first last">A simple demonstration on using FixedDataTable with Ajax data</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#our-project" id="toc-entry-2">Our project</a></li>
<li><a class="reference internal" href="#how-fixeddatatable-gets-its-data" id="toc-entry-3">How FixedDataTable gets its data</a></li>
<li><a class="reference internal" href="#ajaxcell-non-working-version-1" id="toc-entry-4">AjaxCell: Non working version 1</a></li>
<li><a class="reference internal" href="#ajaxcell-non-working-version-2" id="toc-entry-5">AjaxCell: Non working version 2</a></li>
<li><a class="reference internal" href="#ajaxcell-non-working-version-3" id="toc-entry-6">AjaxCell: Non working version 3</a></li>
<li><a class="reference internal" href="#ajaxcell-final-version" id="toc-entry-7">AjaxCell: Final version</a></li>
<li><a class="reference internal" href="#the-table-container-component" id="toc-entry-8">The Table container component</a></li>
<li><a class="reference internal" href="#some-enchancements" id="toc-entry-9">Some enchancements</a></li>
<li><a class="reference internal" href="#conslusion" id="toc-entry-10">Conslusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p><a class="reference external" href="https://facebook.github.io/fixed-data-table/">FixedDataTable</a> is a nice React component from Facebook that is used to render tabular data.
Its main characteristic is that it can handle many rows without sacrificing performance, however
probably due to this fact I was not able to find any examples for loading data using Ajax - all
examples I was able to find had the data already loaded (or created on the fly).</p>
<p>So, although FixedDataTable is able to handle many rows, I am against transfering all of them to the user
whenever our page loads since at most one page of data will be shown on screen (30 rows or so ?) -
any other actions (filtering, sorting, aggregate calculations etc) should be done on the server.</p>
<p>In the following I will present a simple, react-only example with a FixedDataTable that can be
used with server-side, asynchronous, paginated data.</p>
</div>
<div class="section" id="our-project">
<h2><a class="toc-backref" href="#toc-entry-2">Our project</a></h2>
<p>Let’s see an example of what we’ll build:</p>
<img alt="Our project" src="/images/ajax_fixed_data_tables.gif" style="width: 600px;" />
<p>As a source of the data I’ve used the <a class="reference external" href="http://swapi.co/">Star Wars <span class="caps">API</span></a> and specifically its <a class="reference external" href="http://swapi.co/documentation#people">People <span class="caps">API</span></a> by issuing
requests to <a class="reference external" href="http://swapi.co/api/people/?format=json">http://swapi.co/api/people/?format=json</a>. This will return an array of people from the
star wars universe in <span class="caps">JSON</span> format - the results are paginated with a page size of 10 (and we can switch
to another page using the extra <tt class="docutils literal">page=</tt> request parameter).</p>
<p>I will use es6 with the object spread operator (as described in <a class="reference external" href="https://spapas.github.io/2015/11/16/using-browserify-es6/">a previous article</a>)
to write the code, using a single main.js as a source which will be transpiled to <tt class="docutils literal">dist/bundle.js</tt>.</p>
<p>The placeholder <span class="caps">HTML</span> for our application is:</p>
<div class="highlight"><pre><span></span><span class="cp"><!DOCTYPE html></span>
<span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">"UTF-8"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">title</span><span class="p">></span>Hello Ajax Fixed Data Table!<span class="p"></</span><span class="nt">title</span><span class="p">></span>
<span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"stylesheet"</span> <span class="na">href</span><span class="o">=</span><span class="s">'https://cdnjs.cloudflare.com/ajax/libs/fixed-data-table/0.6.0/fixed-data-table.min.css'</span><span class="p">></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">"main"</span><span class="p">></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">"text/javascript"</span> <span class="na">src</span><span class="o">=</span><span class="s">'dist/bundle.js'</span> <span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>The versions that are used are react 14.3 and fixed-data-table 0.6.0. You can find this project in github @
<a class="reference external" href="https://github.com/spapas/react-tables">https://github.com/spapas/react-tables</a> — tag name <tt class="docutils literal"><span class="pre">fixed-data-table-ajax</span></tt>.</p>
</div>
<div class="section" id="how-fixeddatatable-gets-its-data">
<h2><a class="toc-backref" href="#toc-entry-3">How FixedDataTable gets its data</a></h2>
<p>The data of a <tt class="docutils literal">FixedDataTable</tt> component is defined through a number of <tt class="docutils literal">Column</tt> components
and specifically, the <tt class="docutils literal">cell</tt> attribute of that component which sould either return be a static string
to be displayed in that column or a React component (usually a <tt class="docutils literal">Cell</tt>) that will be displayed in
that column or even a function that returns a string or a react component. The function (or component)
passed to <tt class="docutils literal">cell</tt> will have an object with a <tt class="docutils literal">rowIndex</tt> attribute (among others) as a parameter,
for example the following</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">Column</span>
<span class="na">header</span><span class="o">=</span><span class="s">{<Cell</span><span class="p">></span>Url<span class="p"></</span><span class="nt">Cell</span><span class="p">></span>}
cell={props => props.rowIndex}
width={200}
/>
</pre></div>
<p>will just display the rowIndex for each cell.</p>
<p>So we can see that for each cell that FixedDataTable wants to display it will use its corresponding
<tt class="docutils literal">cell</tt> attribute.
So, a general though if we wanted to support asynchronous, paginated data
is to check the rowIndex and retrieve the correct page asynchronously.
This would lead to a difficulty: What should the <tt class="docutils literal">cell</tt> function return? We’ll try to resolve this
using an <tt class="docutils literal">AjaxCell</tt> function that should return a react componet to put in the cell:</p>
</div>
<div class="section" id="ajaxcell-non-working-version-1">
<h2><a class="toc-backref" href="#toc-entry-4">AjaxCell: Non working version 1</a></h2>
<p>A first sketch of an <tt class="docutils literal">`AjaxCell</tt> component could be something like this:</p>
<div class="highlight"><pre><span></span><span class="c1">// Careful - not working</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">AjaxCell1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="nx">rowIndex</span><span class="p">,</span><span class="w"> </span><span class="nx">col</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">rowIndex</span><span class="o">>=</span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://swapi.co/api/people/?format=json&page='</span><span class="o">+</span><span class="nx">page</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c1">// Here we have the result ! But where will it go ?</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="c1">/// what ?</span>
<span class="p">}</span>
</pre></div>
<p>The col attribute will later be used to select the attribute we want to display in this cell.
The <tt class="docutils literal">page</tt> and <tt class="docutils literal">idx</tt> will have the correct page number and idx inside that page (for example,
if <tt class="docutils literal">rowIndex</tt> is 33, <tt class="docutils literal">page</tt> will be 4 and idx will be 3. The problem with the above is that
the fetch function will be called <em>asynchronously</em> so when the AjaxCell returns it will <em>not</em> have
the results (yet)! So we won’t be able to render anything :(</p>
</div>
<div class="section" id="ajaxcell-non-working-version-2">
<h2><a class="toc-backref" href="#toc-entry-5">AjaxCell: Non working version 2</a></h2>
<p>To be able to render <em>something</em>, we could put the results of fetching a page to a <tt class="docutils literal">cache</tt> dictionary — and only
fetch that page if it is not inside the <tt class="docutils literal">cache</tt> - if it is inside the cache then we’ll just return
the correct value:</p>
<div class="highlight"><pre><span></span><span class="c1">// Careful - not working correctly</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">cache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{}</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">AjaxCell2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="nx">rowIndex</span><span class="p">,</span><span class="w"> </span><span class="nx">col</span><span class="p">,</span><span class="w"> </span><span class="nx">forceUpdate</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">rowIndex</span><span class="o">>=</span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">])</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span><span class="p">{</span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">][</span><span class="nx">idx</span><span class="p">][</span><span class="nx">col</span><span class="p">]}</span><span class="o"><</span><span class="err">/Cell></span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Loading page "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">page</span><span class="p">);</span>
<span class="w"> </span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://swapi.co/api/people/?format=json&page='</span><span class="o">+</span><span class="nx">page</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">j</span><span class="p">[</span><span class="s1">'results'</span><span class="p">];</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Cell</span><span class="o">>-<</span><span class="err">/Cell>;</span>
<span class="p">}</span>
</pre></div>
<p>The above will work, since it will return an empty (<tt class="docutils literal"><span class="pre"><Cell>-</Cell></span></tt>) initially but when the
<tt class="docutils literal">fetch</tt> returns it will set the <tt class="docutils literal">cache</tt> for that page and return the correct value (<tt class="docutils literal"><span class="pre"><Cell>{cache[page][idx][col]}</Cell></span></tt>).</p>
<p>However, as can be understood, when the page first loads it will call <tt class="docutils literal">AjaxCell2</tt> for all visible cells —
because fetch is asynchronous and takes time until it returns (and sets the cache for that page), so the
<tt class="docutils literal">fetch</tt> will be called for <em>all</em> cells!</p>
</div>
<div class="section" id="ajaxcell-non-working-version-3">
<h2><a class="toc-backref" href="#toc-entry-6">AjaxCell: Non working version 3</a></h2>
<p><tt class="docutils literal">fetch</tt> ing each page multiple times is of course not acceptable, so we’ll add a <tt class="docutils literal">loading</tt> flag
and <tt class="docutils literal">fetch</tt> will be called only when this flag is <tt class="docutils literal">false</tt>, like this:</p>
<div class="highlight"><pre><span></span><span class="c1">// Not ready yet</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">cache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">AjaxCell2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="nx">rowIndex</span><span class="p">,</span><span class="w"> </span><span class="nx">col</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">rowIndex</span><span class="o">>=</span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">])</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span><span class="p">{</span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">][</span><span class="nx">idx</span><span class="p">][</span><span class="nx">col</span><span class="p">]}</span><span class="o"><</span><span class="err">/Cell></span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">loading</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Loading page "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">page</span><span class="p">);</span>
<span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="w"> </span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://swapi.co/api/people/?format=json&page='</span><span class="o">+</span><span class="nx">page</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">j</span><span class="p">[</span><span class="s1">'results'</span><span class="p">];</span>
<span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Cell</span><span class="o">>-<</span><span class="err">/Cell>;</span>
<span class="p">}</span>
</pre></div>
<p>This works much better - the cells are rendered correctly and each page is loaded only once. However, if for example
I tried to move to the end of the table quickly, I would see some cells that are always loading (they never get their correct value). This is because
there is no way to know that the fetch function has actually completed in order to update with the latest (correct) value of that
cell and will contain the stale placeholder (<tt class="docutils literal"><span class="pre"><Cell>-</Cell></span></tt>) value.</p>
</div>
<div class="section" id="ajaxcell-final-version">
<h2><a class="toc-backref" href="#toc-entry-7">AjaxCell: Final version</a></h2>
<p>To clear the stale data we need to do an update to the table data
when each fetch is finished — this should be done by a callback that will be passed to the <tt class="docutils literal">AjaxCell</tt>, like this:</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">cache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{};</span>
<span class="kd">let</span><span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">AjaxCell</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">({</span><span class="nx">rowIndex</span><span class="p">,</span><span class="w"> </span><span class="nx">col</span><span class="p">,</span><span class="w"> </span><span class="nx">forceUpdate</span><span class="p">,</span><span class="w"> </span><span class="p">...</span><span class="nx">props</span><span class="p">})</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="p">;</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="nx">rowIndex</span><span class="o">>=</span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">page</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">1</span><span class="p">;</span>
<span class="w"> </span><span class="nx">idx</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rowIndex</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nx">pageSize</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">])</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span><span class="p">{</span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">][</span><span class="nx">idx</span><span class="p">][</span><span class="nx">col</span><span class="p">]}</span><span class="o"><</span><span class="err">/Cell></span>
<span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nx">loading</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Loading page "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nx">page</span><span class="p">);</span>
<span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">true</span><span class="p">;</span>
<span class="w"> </span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://swapi.co/api/people/?format=json&page='</span><span class="o">+</span><span class="nx">page</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="w"> </span><span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">j</span><span class="p">[</span><span class="s1">'results'</span><span class="p">];</span>
<span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="w"> </span><span class="nx">forceUpdate</span><span class="p">();</span>
<span class="w"> </span><span class="p">});</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">loadingCell</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>So we pass a forceUpdate callback as a property which is called when a fetch is finished. This may
result to some not needed updates to the table (since we would do a fetch + forceUpdate for non-displayed
data) but we can now be positive that when the data is loaded the table will be updated to dispaly it.</p>
</div>
<div class="section" id="the-table-container-component">
<h2><a class="toc-backref" href="#toc-entry-8">The Table container component</a></h2>
<p>Finally, the component that contains the table is the following:</p>
<div class="highlight"><pre><span></span><span class="kd">class</span><span class="w"> </span><span class="nx">TableContainer</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="nx">React</span><span class="p">.</span><span class="nx">Component</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">render</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="o"><</span><span class="nx">Table</span>
<span class="w"> </span><span class="nx">rowHeight</span><span class="o">=</span><span class="p">{</span><span class="mf">30</span><span class="p">}</span><span class="w"> </span><span class="nx">rowsCount</span><span class="o">=</span><span class="p">{</span><span class="mf">87</span><span class="p">}</span><span class="w"> </span><span class="nx">width</span><span class="o">=</span><span class="p">{</span><span class="mf">600</span><span class="p">}</span><span class="w"> </span><span class="nx">height</span><span class="o">=</span><span class="p">{</span><span class="mf">200</span><span class="p">}</span><span class="w"> </span><span class="nx">headerHeight</span><span class="o">=</span><span class="p">{</span><span class="mf">30</span><span class="p">}</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Column</span>
<span class="w"> </span><span class="nx">header</span><span class="o">=</span><span class="p">{</span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span><span class="nx">Name</span><span class="o"><</span><span class="err">/Cell>}</span>
<span class="w"> </span><span class="nx">cell</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="o"><</span><span class="nx">AjaxCell</span><span class="w"> </span><span class="nx">col</span><span class="o">=</span><span class="s1">'name'</span><span class="w"> </span><span class="nx">forceUpdate</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">forceUpdate</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)}</span><span class="w"> </span><span class="o">/></span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">width</span><span class="o">=</span><span class="p">{</span><span class="mf">200</span><span class="p">}</span>
<span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Column</span>
<span class="w"> </span><span class="nx">header</span><span class="o">=</span><span class="p">{</span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span><span class="nx">Birth</span><span class="w"> </span><span class="nx">Year</span><span class="o"><</span><span class="err">/Cell>}</span>
<span class="w"> </span><span class="nx">cell</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="o"><</span><span class="nx">AjaxCell</span><span class="w"> </span><span class="nx">col</span><span class="o">=</span><span class="s1">'birth_year'</span><span class="w"> </span><span class="nx">forceUpdate</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">forceUpdate</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)}</span><span class="w"> </span><span class="o">/></span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">width</span><span class="o">=</span><span class="p">{</span><span class="mf">200</span><span class="p">}</span>
<span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="nx">Column</span>
<span class="w"> </span><span class="nx">header</span><span class="o">=</span><span class="p">{</span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span><span class="nx">Url</span><span class="o"><</span><span class="err">/Cell>}</span>
<span class="w"> </span><span class="nx">cell</span><span class="o">=</span><span class="p">{</span><span class="w"> </span><span class="o"><</span><span class="nx">AjaxCell</span><span class="w"> </span><span class="nx">col</span><span class="o">=</span><span class="s1">'url'</span><span class="w"> </span><span class="nx">forceUpdate</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">forceUpdate</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">)}</span><span class="w"> </span><span class="o">/></span><span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nx">width</span><span class="o">=</span><span class="p">{</span><span class="mf">200</span><span class="p">}</span>
<span class="w"> </span><span class="o">/></span>
<span class="w"> </span><span class="o"><</span><span class="err">/Table></span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>I’ve made it a component in order to be able to bind the <tt class="docutils literal">forceUpdate</tt> method of the component to and
<tt class="docutils literal">this</tt> and pass it to the <tt class="docutils literal">forceUpdate</tt> parameter to the <tt class="docutils literal">AjaxCell</tt> component. I’ve hard-coded
the rowsCount value — instead we should have done an initial fetch to the first page of the <span class="caps">API</span> to get
the total number of rows and only after that fetch had returned display the <tt class="docutils literal"><Table></tt> component (left
as an exercise to the reader).</p>
</div>
<div class="section" id="some-enchancements">
<h2><a class="toc-backref" href="#toc-entry-9">Some enchancements</a></h2>
<p>Instead of displaying <tt class="docutils literal"><span class="pre"><Cell>-</Cell></span></tt> (or <tt class="docutils literal"></Cell></tt>) when the page loads, I propose to define a
cell with an embedded spinner, like</p>
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">loadingCell</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o"><</span><span class="nx">Cell</span><span class="o">></span>
<span class="w"> </span><span class="o"><</span><span class="nx">img</span><span class="w"> </span><span class="nx">width</span><span class="o">=</span><span class="s2">"16"</span><span class="w"> </span><span class="nx">height</span><span class="o">=</span><span class="s2">"16"</span><span class="w"> </span><span class="nx">alt</span><span class="o">=</span><span class="s2">"star"</span><span class="w"> </span><span class="nx">src</span><span class="o">=</span><span class="s2">"data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA=="</span><span class="w"> </span><span class="o">/></span>
<span class="o"><</span><span class="err">/Cell></span>
</pre></div>
<p>and return this instead.</p>
<p>Also, if your <span class="caps">REST</span> <span class="caps">API</span> returns too fast and you’d like to see what would happen if the server request took too long to return, you could
change fetch like this</p>
<div class="highlight"><pre><span></span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://swapi.co/api/people/?format=json&page='</span><span class="o">+</span><span class="nx">page</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">setTimeout</span><span class="p">(</span><span class="w"> </span><span class="p">()</span><span class="w"> </span><span class="p">=></span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nx">cache</span><span class="p">[</span><span class="nx">page</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">j</span><span class="p">[</span><span class="s1">'results'</span><span class="p">];</span>
<span class="w"> </span><span class="nx">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">false</span><span class="p">;</span>
<span class="w"> </span><span class="nx">forceUpdate</span><span class="p">();</span>
<span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="mf">1000</span><span class="p">);</span>
<span class="p">});</span>
</pre></div>
<p>to add a 1 second delay.</p>
</div>
<div class="section" id="conslusion">
<h2><a class="toc-backref" href="#toc-entry-10">Conslusion</a></h2>
<p>The above is a just a proof of concept of using FixedDataTable with asynchronously loaded server-side data.
This of course could be used for small projects (I am already using it for an internal project) but I recommend
using the <a class="reference external" href="https://spapas.github.io/2015/07/02/comprehensive-react-flux-tutorial-2/">flux architecture</a> for more complex projects. What this more or
less means is that a store component
should be developed that will actually keep the data for each row, and a <tt class="docutils literal">fetchCompleted</tt> action should be
dispatched when the <tt class="docutils literal">fetch</tt> is finished instead of calling <tt class="docutils literal">forceUpdate</tt> directly.</p>
</div>
PDFs in Django: The essential guide2015-11-27T10:20:00+02:002015-11-27T10:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2015-11-27:/2015/11/27/pdf-in-django/<p class="first last">An essential guide to creating, editing and serving <span class="caps">PDF</span> files in django</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#the-players" id="toc-entry-2">The players</a><ul>
<li><a class="reference internal" href="#reportlab" id="toc-entry-3">ReportLab</a></li>
<li><a class="reference internal" href="#xhtml2pdf" id="toc-entry-4">xhtml2pdf</a></li>
<li><a class="reference internal" href="#pypdf2" id="toc-entry-5">PyPDF2</a></li>
</ul>
</li>
<li><a class="reference internal" href="#django-integration" id="toc-entry-6">Django integration</a><ul>
<li><a class="reference internal" href="#using-a-plain-old-view" id="toc-entry-7">Using a plain old view</a></li>
<li><a class="reference internal" href="#using-a-cbv" id="toc-entry-8">Using a <span class="caps">CBV</span></a></li>
<li><a class="reference internal" href="#how-does-django-xhtml2pdf-loads-resources" id="toc-entry-9">How does django-xhtml2pdf loads resources</a></li>
<li><a class="reference internal" href="#using-a-common-style-for-your-pdfs" id="toc-entry-10">Using a common style for your PDFs</a></li>
<li><a class="reference internal" href="#changing-the-font-to-a-unicode-enabled-one" id="toc-entry-11">Changing the font (to a Unicode enabled one)</a></li>
<li><a class="reference internal" href="#configure-django-for-debugging-pdf-creation" id="toc-entry-12">Configure Django for debugging <span class="caps">PDF</span> creation</a></li>
<li><a class="reference internal" href="#concatenating-pdfs-in-django" id="toc-entry-13">Concatenating PDFs in Django</a></li>
</ul>
</li>
<li><a class="reference internal" href="#more-advanced-xhtml2pdf-features" id="toc-entry-14">More advanced xhtml2pdf features</a><ul>
<li><a class="reference internal" href="#laying-out" id="toc-entry-15">Laying out</a></li>
<li><a class="reference internal" href="#extra-stuff" id="toc-entry-16">Extra stuff</a></li>
</ul>
</li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-17">Conclusion</a></li>
</ul>
</div>
<p><strong>Upgrade 23/05/2018</strong> I’ve upgraded the sample project (<a class="reference external" href="https://github.com/spapas/django-pdf-guide">https://github.com/spapas/django-pdf-guide</a>) for usage with Django 2.0 and Python 3.x.
There were very few changes between the old, Django 1.8 compatible project and the new one. To see the differences, you can just clone it
and do a <tt class="docutils literal">git diff <span class="pre">django-1.8</span> <span class="pre">django-2.0</span></tt>: Most are related to changes between django 1.8 and django 2.0 (in the <span class="caps">URL</span> configuration use <tt class="docutils literal">path</tt> instead of <tt class="docutils literal">url</tt>,
use new style middleware etc) and between python 2.7 and 3.x (small changes in import syntax, <tt class="docutils literal">StringIO</tt> has been moved to the <tt class="docutils literal">io</tt> package). There’s also
a couple of requirement upgrades, the most important are the upgrade to <tt class="docutils literal">xhtml2pdf 0.2.2</tt> and using my own fork of <tt class="docutils literal">django_xhtml2pdf</tt> since django_xhtml2pdf
does not seem to be maintained anymore.</p>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>I’ve noticed that although it is easy to create PDFs with
Python, there’s no complete guide on how to
integrate these tools with Django and resolve the problems
that you’ll encounter when trying to actually create PDFs
from your Django web application.</p>
<p>In this article I will present the solution I use for
creating PDFs with Django, along with various tips on how to
solve most of your common requirements. Specifically, here
are some things that we’ll cover:</p>
<ul class="simple">
<li>Learn how to create PDFs “by hand”</li>
<li>Create PDFs with Django using a normal Djang Template (similar to an <span class="caps">HTML</span> page)</li>
<li>Change the fonts of your PDFs</li>
<li>Use styling in your output</li>
<li>Create layouts</li>
<li>Embed images to your PDFs</li>
<li>Add page numbers</li>
<li>Merge (concatenate) PDFs</li>
</ul>
</div>
<div class="section" id="the-players">
<h2><a class="toc-backref" href="#toc-entry-2">The players</a></h2>
<p>We are going to use the following main tools:</p>
<ul class="simple">
<li><a class="reference external" href="https://bitbucket.org/rptlab/reportlab">ReportLab</a> is an open source python library for creating PDFs. It uses a low-level <span class="caps">API</span> that allows “drawing” strings on specific coordinates on the <span class="caps">PDF</span> - for people familiar with creating PDFs in Java it is more or less <a class="reference external" href="http://itextpdf.com/">iText</a> for python.</li>
<li><a class="reference external" href="https://github.com/chrisglass/xhtml2pdf">xhtml2pdf</a> (formerly named <em>pisa</em>) is an open source library that can convert <span class="caps">HTML</span>/<span class="caps">CSS</span> pages to <span class="caps">PDF</span> using ReportLab.</li>
<li><a class="reference external" href="https://github.com/chrisglass/django-xhtml2pdf">django-xhtml2pdf</a> is a wrapper around xhtml2pdf that makes integration with Django easier.</li>
<li><a class="reference external" href="https://github.com/mstamy2/PyPDF2">PyPDF2</a> is an open source tool of that can split, merge and transform pages of <span class="caps">PDF</span> files.</li>
</ul>
<p>I’ve created a <a class="reference external" href="https://github.com/spapas/django-pdf-guide">django project</a> <a class="reference external" href="https://github.com/spapas/django-pdf-guide">https://github.com/spapas/django-pdf-guide</a> with everything covered here. Please clone it,
install its requirements and play with it to see how everything works !</p>
<p>Before integrating the above tools to a Django project, I’d like to describe them individually a bit more. Any files
I mention below will be included in this project.</p>
<div class="section" id="reportlab">
<h3><a class="toc-backref" href="#toc-entry-3">ReportLab</a></h3>
<p>ReportLab offers a really low <span class="caps">API</span> for creating PDFs. It is something like having a <tt class="docutils literal">canvas.drawString()</tt> method (for
people familiar with drawing APIs) for your <span class="caps">PDF</span> page. Let’s take a look at an example, creating a <span class="caps">PDF</span> with a simple string:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">reportlab.pdfgen</span> <span class="kn">import</span> <span class="n">canvas</span>
<span class="kn">import</span> <span class="nn">reportlab.rl_config</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">reportlab</span><span class="o">.</span><span class="n">rl_config</span><span class="o">.</span><span class="n">warnOnMissingFontGlyphs</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">canvas</span><span class="o">.</span><span class="n">Canvas</span><span class="p">(</span><span class="s2">"./hello1.pdf"</span><span class="p">,)</span>
<span class="n">c</span><span class="o">.</span><span class="n">drawString</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="s2">"Hello World"</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">showPage</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
</pre></div>
<p>Save the above in a file named testreportlab1.py. If you run python testreportlab1.py (in an environment that has
reportlab of cours) you should see no errors and a pdf named <tt class="docutils literal">hello1.pdf</tt> created. If you open it in your <span class="caps">PDF</span>
reader you’ll see a blank page with “Hello World” written in its lower right corner.</p>
<p>If you try to add a unicode text, for example “Καλημέρα ελλάδα”, you should see something like the following:</p>
<img alt="Hello PDF" src="/images/hellopdf2.png" style="width: 480px;" />
<p>It seems that the default font that ReportLab uses does not have a good support for accented greek characters
since they are missing (and probably for various other characters).</p>
<p>To resolve this, we could try changing the font to one that contains the missing symbols. You can find free
fonts on the internet (for example the <cite>DejaVu</cite> font), or even grab one from your system fonts (in windows,
check out <tt class="docutils literal"><span class="pre">c:\windows\fonts\</span></tt>). In any case, just copy the ttf file of your font inside the folder of
your project and crate a file named testreportlab2.py with the following (I am using the DejaVuSans font):</p>
<div class="highlight"><pre><span></span><span class="c1"># -*- coding: utf-8 -*-</span>
<span class="kn">import</span> <span class="nn">reportlab.rl_config</span>
<span class="kn">from</span> <span class="nn">reportlab.pdfbase</span> <span class="kn">import</span> <span class="n">pdfmetrics</span>
<span class="kn">from</span> <span class="nn">reportlab.pdfbase.ttfonts</span> <span class="kn">import</span> <span class="n">TTFont</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">c</span> <span class="o">=</span> <span class="n">canvas</span><span class="o">.</span><span class="n">Canvas</span><span class="p">(</span><span class="s2">"./hello2.pdf"</span><span class="p">,)</span>
<span class="n">reportlab</span><span class="o">.</span><span class="n">rl_config</span><span class="o">.</span><span class="n">warnOnMissingFontGlyphs</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">pdfmetrics</span><span class="o">.</span><span class="n">registerFont</span><span class="p">(</span><span class="n">TTFont</span><span class="p">(</span><span class="s1">'DejaVuSans'</span><span class="p">,</span> <span class="s1">'DejaVuSans.ttf'</span><span class="p">))</span>
<span class="n">c</span><span class="o">.</span><span class="n">setFont</span><span class="p">(</span><span class="s1">'DejaVuSans'</span><span class="p">,</span> <span class="mi">22</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">drawString</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="sa">u</span><span class="s2">"Καλημέρα ελλάδα."</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">showPage</span><span class="p">()</span>
<span class="n">c</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
</pre></div>
<p>The above was just a scratch on the surface of ReportLab, mainly to be confident that
everything <em>will</em> work fine for non-english speaking people! To find out more, you should check the <a class="reference external" href="http://www.reportlab.com/docs/reportlab-userguide.pdf">ReportLab open-source User Guide</a>.</p>
<p>I also have to mention that
<a class="reference external" href="http://reportlab.com/">the company behind ReportLab</a> offers some great commercial solutions based on ReportLab for creating PDFs (similar to <a class="reference external" href="http://community.jaspersoft.com/project/jasperreports-library">JasperReports</a>) - check it out
if you need support or advanced capabilities.</p>
</div>
<div class="section" id="xhtml2pdf">
<h3><a class="toc-backref" href="#toc-entry-4">xhtml2pdf</a></h3>
<p>The xhtml2pdf is a really great library that allows you to use html files as a template
to a <span class="caps">PDF</span>. Of course, an html cannot always be converted to a <span class="caps">PDF</span> since,
unfortunately, PDFs <em>do</em> have pages.</p>
<p>xhtml2pdf has a nice executable script that can be used to test its capabilities. After
you install it (either globally or to a virtual environment) you should be able to find
out the executable <tt class="docutils literal">$<span class="caps">PYTHON</span>/scripts/xhtml2pdf</tt> (or <tt class="docutils literal">xhtml2pdf.exe</tt> if you are in
Windows) and a corresponding python script @ <tt class="docutils literal"><span class="pre">$<span class="caps">PYTHON</span>/scripts/xhtml2pdf-script.py</span></tt>.</p>
<p>Let’s try to use xhtml2pdf to explore some of its capabilities. Create a file named
testxhtml2pdf.html with the following contents and run <tt class="docutils literal">xhtml2pdf testxhtml2pdf.html</tt>:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">"Content-Type"</span> <span class="na">content</span><span class="o">=</span><span class="s">"text/html; charset=utf-8"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Testing xhtml2pdf <span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">><</span><span class="nt">b</span><span class="p">></span>Hello, world!<span class="p"></</span><span class="nt">b</span><span class="p">></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">><</span><span class="nt">i</span><span class="p">></span>Hello, italics<span class="p"></</span><span class="nt">i</span><span class="p">></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>Καλημέρα Ελλάδα!<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus nulla erat, porttitor ut venenatis eget,
tempor et purus. Nullam nec erat vel enim euismod auctor et at nisl. Integer posuere bibendum condimentum. Ut
euismod velit ut porttitor condimentum. In ullamcorper nulla at lectus fermentum aliquam. Nunc elementum commodo
dui, id pulvinar ex viverra id. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos.<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed aliquam vitae lectus sit amet accumsan. Morbi
nibh urna, condimentum nec volutpat at, lobortis sit amet odio. Etiam quis neque interdum sapien cursus ornare. Cras
commodo lacinia sapien nec porta. Suspendisse potenti. Nulla hendrerit dolor et rutrum consectetur.<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">width</span><span class="o">=</span><span class="s">"26"</span> <span class="na">height</span><span class="o">=</span><span class="s">"20"</span> <span class="na">src</span><span class="o">=</span><span class="s">"data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/</span>
<span class="s"> rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0C</span>
<span class="s"> cguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7"</span> <span class="p">></span>
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">table</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>header0<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>header1<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>header2<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>header3<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>header4<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>header5<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>Hello World!!!<span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="p"></</span><span class="nt">table</span><span class="p">></span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>Please notice the <tt class="docutils literal"><meta <span class="pre">http-equiv="Content-Type"</span> <span class="pre">content="text/html;</span> <span class="pre">charset=utf-8"</span> /></tt> in the above <span class="caps">HTML</span> — also it is saved as
Unicode (Encoding - Covert to <span class="caps">UTF</span>-8 in Notepad++). The result (<tt class="docutils literal">testxhtml2pdf.pdf</tt>) should have:</p>
<ul class="simple">
<li>A nice header (h1)</li>
<li>Paragraphs</li>
<li>Horizontal lines</li>
<li>No support for greek characters (same problem as with reportlab)</li>
<li>Images (I am inlining it as a base 64 image)</li>
<li>A list</li>
<li>A table</li>
</ul>
<p>Before moving on, I’d like to fix the problem with the greek characters. You should
set the font to one supporting greek characters, just like you did with ReportLab before.
This can be done with the help of the <tt class="docutils literal"><span class="pre">@font-face</span></tt> <a class="reference external" href="https://github.com/xhtml2pdf/xhtml2pdf/blob/master/doc/usage.rst#fonts">css directive</a>. So, let’s create
a file named <tt class="docutils literal">testxhtml2pdf2.html</tt> with the following contents:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">"Content-Type"</span> <span class="na">content</span><span class="o">=</span><span class="s">"text/html; charset=utf-8"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">style</span><span class="p">></span>
<span class="w"> </span><span class="p">@</span><span class="k">font-face</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">font-family</span><span class="o">:</span><span class="w"> </span><span class="nt">DejaVuSans</span><span class="o">;</span>
<span class="w"> </span><span class="nt">src</span><span class="o">:</span><span class="w"> </span><span class="nt">url</span><span class="o">(</span><span class="s2">"c:/progr/py/django-pdf-guide/django_pdf_guide/DejaVuSans.ttf"</span><span class="o">);</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="nt">body</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">font-family</span><span class="p">:</span><span class="w"> </span><span class="n">DejaVuSans</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p"></</span><span class="nt">style</span><span class="p">></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Δοκιμή του xhtml2pdf <span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>Καλημέρα Ελλάδα!<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>Before running <tt class="docutils literal">xhtml2pdf testxhtml2pdf2.html</tt>, please make
sure to change the url of the font file above to the absolute path of that font in your
local system . As a result, after running xhhtml2pdf
you
should see the unicode characters without problems.</p>
<p>I have to mention here that I wasn’t able to use the font from a relative path, that’s
why I used the absolute one. In case something is not right, try
running it with the <tt class="docutils literal"><span class="pre">-d</span></tt> option to output debugging information (something like
<tt class="docutils literal">xhtml2pdf <span class="pre">-d</span> testxhtml2pdf2.html</tt>). You must see a line like this one:</p>
<pre class="code literal-block">
DEBUG [xhtml2pdf] C:\progr\py\django-pdf-guide\venv\lib\site-packages\xhtml2pdf\context.py line 857: Load font 'c:\\progr\\py\\django-pdf-guide\\django_pdf_guide\\DejaVuSans.ttf'
</pre>
<p>to make sure that the font is actually loaded!</p>
</div>
<div class="section" id="pypdf2">
<h3><a class="toc-backref" href="#toc-entry-5">PyPDF2</a></h3>
<p>The PyPDF2 library can be used to extract pages from a <span class="caps">PDF</span> to a new one
or combine pages from different PDFs to a a new one. A common requirement is
to have the first and page of a report as static PDFs, create the contents
of this report through your app as a <span class="caps">PDF</span> and combine all three PDFs (front page,
content and back page) to the resulting <span class="caps">PDF</span>.</p>
<p>Let’s see a quick example of combining two PDFs:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">from</span> <span class="nn">PyPDF2</span> <span class="kn">import</span> <span class="n">PdfFileMerger</span>
<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span>
<span class="n">pdfs</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">pdfs</span> <span class="ow">or</span> <span class="nb">len</span><span class="p">(</span><span class="n">pdfs</span><span class="p">)</span> <span class="o"><</span> <span class="mi">2</span><span class="p">:</span>
<span class="n">exit</span><span class="p">(</span><span class="s2">"Please enter at least two pdfs for merging!"</span><span class="p">)</span>
<span class="n">merger</span> <span class="o">=</span> <span class="n">PdfFileMerger</span><span class="p">()</span>
<span class="k">for</span> <span class="n">pdf</span> <span class="ow">in</span> <span class="n">pdfs</span><span class="p">:</span>
<span class="n">merger</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">fileobj</span><span class="o">=</span><span class="nb">open</span><span class="p">(</span><span class="n">pdf</span><span class="p">,</span> <span class="s2">"rb"</span><span class="p">))</span>
<span class="n">output</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"output.pdf"</span><span class="p">,</span> <span class="s2">"wb"</span><span class="p">)</span>
<span class="n">merger</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
</pre></div>
<p>The above will try to open all input parameters (as files) and append them to a the output.pdf.</p>
</div>
</div>
<div class="section" id="django-integration">
<h2><a class="toc-backref" href="#toc-entry-6">Django integration</a></h2>
<p>To integrate the <span class="caps">PDF</span> creation process with django we’ll use a simple app with only one model about books. We are
going to use the django-xhtml2pdf library — I recommend installing the latest version (from github
using something like <tt class="docutils literal">pip install <span class="pre">-e</span> <span class="pre">git+https://github.com/chrisglass/django-xhtml2pdf.git#egg=django-xhtml2pdf</span></tt>
) since the pip package has not been updated in a long time!</p>
<div class="section" id="using-a-plain-old-view">
<h3><a class="toc-backref" href="#toc-entry-7">Using a plain old view</a></h3>
<p>The simplest case is to just create plain old view to display the <span class="caps">PDF</span>. We’ll use django-xhtml2pdf along with the
followig django template:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">"Content-Type"</span> <span class="na">content</span><span class="o">=</span><span class="s">"text/html; charset=utf-8"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Books<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">table</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>ID<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>Title<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
{% for book in books %}
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span>{{ book.id }}<span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span>{{ book.title }}<span class="p"></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">table</span><span class="p">></span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>Name it as <tt class="docutils literal">books_plain_old_view.html</tt> and put it on <tt class="docutils literal">books/templates</tt> directory. The view that
returns the above template as <span class="caps">PDF</span> is the following:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponse</span>
<span class="kn">from</span> <span class="nn">django_xhtml2pdf.utils</span> <span class="kn">import</span> <span class="n">generate_pdf</span>
<span class="k">def</span> <span class="nf">books_plain_old_view</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
<span class="n">context</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'books'</span><span class="p">:</span> <span class="n">Book</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">generate_pdf</span><span class="p">(</span><span class="s1">'books_plain_old_view.html'</span><span class="p">,</span> <span class="n">file_object</span><span class="o">=</span><span class="n">resp</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="n">context</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
</pre></div>
<p>We just use the <tt class="docutils literal">generate_pdf</tt> method of django-xhtml2pdf to help us generate the <span class="caps">PDF</span>, passing it
our response object and a context dictionary (containing all books).</p>
<p>Instead of the simple <span class="caps">HTTP</span> response above, we could add a ‘Content Disposition’ <span class="caps">HTTP</span> header to
our response
(or use the django-xhtml2pdf method <tt class="docutils literal">render_to_pdf_response</tt> instead of <tt class="docutils literal">generate_pdf</tt>)
to suggest a default filename for the file to be saved by adding the line</p>
<pre class="code literal-block">
resp['Content-Disposition'] = 'attachment; filename="output.pdf"'
</pre>
<p>after the definition of <tt class="docutils literal">resp</tt>.</p>
<p>This will have the extra effect, at least in Chrome and Firefox to show the “Save File” dialog
when clicking on the link instead of retrieving the <span class="caps">PDF</span> and displaying it inside* the browser window.</p>
</div>
<div class="section" id="using-a-cbv">
<h3><a class="toc-backref" href="#toc-entry-8">Using a <span class="caps">CBV</span></a></h3>
<p>I don’t really recommend using plain old Django views - instead I propose to always use Class Based Views
for their DRYness. The best approach is to create a mixin that would allow any kind of <span class="caps">CBV</span> (at least any
kind of <span class="caps">CBV</span> that uses a template) to be rendered in <span class="caps">PDF</span>. Here’s how we could implement a <tt class="docutils literal">PdfResponseMixin</tt>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">PdfResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">context</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">()</span>
<span class="n">template</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">generate_pdf</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">file_object</span><span class="o">=</span><span class="n">resp</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="n">context</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
</pre></div>
<p>Now, we could use this mixin to create <span class="caps">PDF</span> outputting views from any other view! For example, here’s how
we could create a book list in pdf:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">BookPdfListView</span><span class="p">(</span><span class="n">PdfResponseMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'books'</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
</pre></div>
<p>To display it, you could use the same template as <tt class="docutils literal">books_plain_old_view.html</tt> (so either add a <tt class="docutils literal"><span class="pre">template_name='books_plain_old_view.html'</span></tt>
property to the class or copy <tt class="docutils literal">books_plain_old_view.html</tt> to <tt class="docutils literal">books/book_list.html</tt>).</p>
<p>Also, as another example, here’s a <tt class="docutils literal">BookPdfDetailView</tt> that outputs <span class="caps">PDF</span>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">BookPdfDetailView</span><span class="p">(</span><span class="n">PdfResponseMixin</span><span class="p">,</span> <span class="n">DetailView</span><span class="p">):</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'book'</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
</pre></div>
<p>and a corresponding template (name it <tt class="docutils literal">books/book_detail.html</tt>):</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">"Content-Type"</span> <span class="na">content</span><span class="o">=</span><span class="s">"text/html; charset=utf-8"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Book Detail<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">b</span><span class="p">></span>ID<span class="p"></</span><span class="nt">b</span><span class="p">></span>: <span class="cp">{{</span> <span class="nv">book.id</span> <span class="cp">}}</span> <span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">b</span><span class="p">></span>Title<span class="p"></</span><span class="nt">b</span><span class="p">></span>: <span class="cp">{{</span> <span class="nv">book.title</span> <span class="cp">}}</span> <span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>To add the content-disposition header and a name for your <span class="caps">PDF</span>, you can use the following mixin:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">PdfResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="n">pdf_name</span> <span class="o">=</span> <span class="s2">"output"</span>
<span class="k">def</span> <span class="nf">get_pdf_name</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">pdf_name</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">context</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">()</span>
<span class="n">template</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
<span class="n">resp</span><span class="p">[</span><span class="s1">'Content-Disposition'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'attachment; filename="</span><span class="si">{0}</span><span class="s1">.pdf"'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_pdf_name</span><span class="p">())</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">generate_pdf</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">file_object</span><span class="o">=</span><span class="n">resp</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="n">context</span><span class="p">)</span>
<span class="k">return</span> <span class="n">result</span>
</pre></div>
<p>You see that, in order to havea configurable output name for our <span class="caps">PDF</span> and be consistent with the other
django CBVs, a <tt class="docutils literal">pdf_name</tt> class attribute and a <tt class="docutils literal">get_pdf_name</tt> instance method are added. When
using the above mixin in your classes you can either provide a value for <tt class="docutils literal">pdf_name</tt> (to use the same for all
your instances), or override <tt class="docutils literal">get_pdf_name</tt> to have a dynamic value!</p>
</div>
<div class="section" id="how-does-django-xhtml2pdf-loads-resources">
<h3><a class="toc-backref" href="#toc-entry-9">How does django-xhtml2pdf loads resources</a></h3>
<p>Before doing more advanced things, we need to understand how <tt class="docutils literal"><span class="pre">django-xhtml2pdf</span></tt> works and specifically
how we can refer to things like css, images, fonts etc from our <span class="caps">PDF</span> templates.
If you check the <a class="reference external" href="https://github.com/chrisglass/django-xhtml2pdf/blob/master/django_xhtml2pdf/utils.py">utils.py of django-xhtml2pdf</a> you’ll see that it uses a function named <tt class="docutils literal">fetch_resources</tt>
for loading these resources. This function checks to see if the resource starts with <tt class="docutils literal">/MEDIA_URL</tt> or
<tt class="docutils literal">/STATIC_URL</tt> and converts it to a local (filesystem) path. For example, if you refer to a font like
<tt class="docutils literal">/static/font1.ttf</tt> in your <span class="caps">PDF</span> template, <tt class="docutils literal">xhtml2pdf</tt> will try to load the file <tt class="docutils literal">STATIC_ROOT + /font1.ttf</tt>
(and if it does not find the file you want to refer to there it will check all <tt class="docutils literal">STATICFILES_DIRS</tt> entries).</p>
<p>Thus, you can just put your resources into your <tt class="docutils literal">STATIC_ROOT</tt> directory and use the <tt class="docutils literal">{% static %}</tt>
template tag to create <span class="caps">URL</span> paths for them — django-xhtml2pdf will convert these to local paths and
everything will work fine.</p>
<p><strong>Please notice that you *need* to have configured “STATIC_ROOT“ for this to work</strong> — if <tt class="docutils literal">STATIC_ROOT</tt> is
empty (and, for example you use <tt class="docutils literal">static</tt> directories in your apps) then the described substitution
mechanism will <em>not</em> work. Also, notice that the <tt class="docutils literal">/static</tt> directory inside your apps <em>cannot be used</em>
for fetch resources like this
(due to how <tt class="docutils literal">fetch_resources</tt> is implemented it only checks if the static resource is contained inside
the <tt class="docutils literal">STATIC_ROOT</tt> or in one of the the <tt class="docutils literal">STATICFILES_DIRS</tt>) so be careful to either put the static files
you need to load from PDFs (fonts, styles and possibly images) to either the <tt class="docutils literal">STATIC_ROOT</tt> or the
one of the <tt class="docutils literal">STATICFILES_DIRS</tt>.</p>
</div>
<div class="section" id="using-a-common-style-for-your-pdfs">
<h3><a class="toc-backref" href="#toc-entry-10">Using a common style for your PDFs</a></h3>
<p>If you need to create a lot of similar PDFs then you’ll probably want to
use a bunch of common styles for them (same fonts, headers etc). This could be done using
the <tt class="docutils literal">{% static %}</tt> trick we saw on the previous section. However, if we include the
styling css as a static file then we won’t be able to use the static-file-uri-to-local-path
mechanism described above (since the <tt class="docutils literal">{% static %}</tt> template tag won’t work in static files).</p>
<p>Thankfully, not everything is lost — Django comes to the rescue!!! We can create a single <span class="caps">CSS</span> file
that would be used by all our <span class="caps">PDF</span> templates and <em>include</em> it in the templates using the <tt class="docutils literal">{% include %}</tt> Django
template tag! Django will think that this will be a normal template and paste its contents where we wanted and
also execute the templates tags!</p>
<p>We’ll see an example of all this in the next section.</p>
</div>
<div class="section" id="changing-the-font-to-a-unicode-enabled-one">
<h3><a class="toc-backref" href="#toc-entry-11">Changing the font (to a Unicode enabled one)</a></h3>
<p>The time has finally arrived to change the font! It’s easy if you know exactly what to do. First of all
configure your <tt class="docutils literal">STATIC_ROOT</tt> and <tt class="docutils literal">STATIC_URL</tt> setting, for example <tt class="docutils literal">STATIC_ROOT = <span class="pre">os.path.join(BASE_DIR,'static')</span></tt>
and <tt class="docutils literal">STATIC_URL = '/static/'</tt>.</p>
<p>Then, add a template-css file for your fonts in one of your templates directories. I am naming the
file <tt class="docutils literal">pdfstylefonts.css</tt> and I’ve put it to <tt class="docutils literal">books/templates</tt>:</p>
<div class="highlight"><pre><span></span><span class="p">{</span><span class="err">%</span><span class="w"> </span><span class="err">load</span><span class="w"> </span><span class="err">static</span><span class="w"> </span><span class="err">%</span><span class="p">}</span>
<span class="p">@</span><span class="k">font-face</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">font-family</span><span class="o">:</span><span class="w"> </span><span class="s2">"Calibri"</span><span class="o">;</span>
<span class="w"> </span><span class="nt">src</span><span class="o">:</span><span class="w"> </span><span class="nt">url</span><span class="o">(</span><span class="p">{</span><span class="err">%</span><span class="w"> </span><span class="err">static</span><span class="w"> </span><span class="err">"fonts/calibri.ttf"</span><span class="w"> </span><span class="err">%</span><span class="p">}</span><span class="o">);</span>
<span class="p">}</span>
<span class="p">@</span><span class="k">font-face</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">font-family</span><span class="o">:</span><span class="w"> </span><span class="s2">"Calibri"</span><span class="o">;</span>
<span class="w"> </span><span class="nt">src</span><span class="o">:</span><span class="w"> </span><span class="nt">url</span><span class="o">(</span><span class="p">{</span><span class="err">%</span><span class="w"> </span><span class="err">static</span><span class="w"> </span><span class="err">"fonts/calibrib.ttf"</span><span class="w"> </span><span class="err">%</span><span class="p">}</span><span class="o">);</span>
<span class="w"> </span><span class="nt">font-weight</span><span class="o">:</span><span class="w"> </span><span class="nt">bold</span><span class="o">;</span>
<span class="p">}</span>
<span class="p">@</span><span class="k">font-face</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">font-family</span><span class="o">:</span><span class="w"> </span><span class="s2">"Calibri"</span><span class="o">;</span>
<span class="w"> </span><span class="nt">src</span><span class="o">:</span><span class="w"> </span><span class="nt">url</span><span class="o">(</span><span class="p">{</span><span class="err">%</span><span class="w"> </span><span class="err">static</span><span class="w"> </span><span class="err">"fonts/calibrii.ttf"</span><span class="w"> </span><span class="err">%</span><span class="p">}</span><span class="o">);</span>
<span class="w"> </span><span class="nt">font-style</span><span class="o">:</span><span class="w"> </span><span class="nt">italic</span><span class="o">,</span><span class="w"> </span><span class="nt">oblique</span><span class="o">;</span>
<span class="p">}</span>
<span class="p">@</span><span class="k">font-face</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">font-family</span><span class="o">:</span><span class="w"> </span><span class="s2">"Calibri"</span><span class="o">;</span>
<span class="w"> </span><span class="nt">src</span><span class="o">:</span><span class="w"> </span><span class="nt">url</span><span class="o">(</span><span class="p">{</span><span class="err">%</span><span class="w"> </span><span class="err">static</span><span class="w"> </span><span class="err">"fonts/calibriz.ttf"</span><span class="w"> </span><span class="err">%</span><span class="p">}</span><span class="o">);</span>
<span class="w"> </span><span class="nt">font-weight</span><span class="o">:</span><span class="w"> </span><span class="nt">bold</span><span class="o">;</span>
<span class="w"> </span><span class="nt">font-style</span><span class="o">:</span><span class="w"> </span><span class="nt">italic</span><span class="o">,</span><span class="w"> </span><span class="nt">oblique</span><span class="o">;</span>
<span class="p">}</span>
</pre></div>
<p>I am using Calibri family of fonts (copied from <tt class="docutils literal"><span class="pre">c:\windows\fonts</span></tt>) for this — I’ve also configured
all styles (bold, italic, bold-italic) of this font family to use the correct ttf files. All the
ttf files have been copied to the directory <tt class="docutils literal">static/fonts/</tt>.</p>
<p>Now, add another css file that will be your global <span class="caps">PDF</span> styles. This should be put to the <tt class="docutils literal">static</tt> directory
and could be named <tt class="docutils literal">pdfstyle.css</tt>:</p>
<div class="highlight"><pre><span></span><span class="nt">h1</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="kc">blue</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">*,</span><span class="w"> </span><span class="nt">html</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">font-family</span><span class="p">:</span><span class="w"> </span><span class="s2">"Calibri"</span><span class="p">;</span>
<span class="w"> </span><span class="k">font-size</span><span class="p">:</span><span class="mi">11</span><span class="kt">pt</span><span class="p">;</span>
<span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="kc">red</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>
<p>Next, here’s a template that lists all books (and contain some greek characters — the title of the books also contain
greek characters) — I’ve named it <tt class="docutils literal">book_list_ex.html</tt>:</p>
<div class="highlight"><pre><span></span><span class="cp">{%</span> <span class="k">load</span> <span class="nv">static</span> <span class="cp">%}</span>
<span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">"Content-Type"</span> <span class="na">content</span><span class="o">=</span><span class="s">"text/html; charset=utf-8"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">style</span><span class="p">></span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">include</span> <span class="s2">"pdfstylefonts.css"</span> <span class="cp">%}</span>
<span class="w"> </span><span class="p"></</span><span class="nt">style</span><span class="p">></span>
<span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">'stylesheet'</span> <span class="na">href</span><span class="o">=</span><span class="s">'</span><span class="cp">{%</span> <span class="k">static</span> <span class="s2">"pdfstyle.css"</span> <span class="cp">%}</span><span class="s">'</span><span class="p">/></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Λίστα βιβλίων<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">'</span><span class="cp">{%</span> <span class="k">static</span> <span class="s2">"pony.png"</span> <span class="cp">%}</span><span class="s">'</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">table</span><span class="p">></span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">th</span><span class="p">></span>ID<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>Title<span class="p"></</span><span class="nt">th</span><span class="p">><</span><span class="nt">th</span><span class="p">></span>Cover<span class="p"></</span><span class="nt">th</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="cp">{%</span> <span class="k">for</span> <span class="nv">book</span> <span class="k">in</span> <span class="nv">books</span> <span class="cp">%}</span>
<span class="p"><</span><span class="nt">tr</span><span class="p">></span>
<span class="p"><</span><span class="nt">td</span><span class="p">></span><span class="cp">{{</span> <span class="nv">book.id</span> <span class="cp">}}</span><span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">></span><span class="cp">{{</span> <span class="nv">book.title</span> <span class="cp">}}</span><span class="p"></</span><span class="nt">td</span><span class="p">><</span><span class="nt">td</span><span class="p">><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">'</span><span class="cp">{{</span> <span class="nv">book.cover.url</span> <span class="cp">}}</span><span class="s">'</span> <span class="p">/></</span><span class="nt">td</span><span class="p">></span>
<span class="p"></</span><span class="nt">tr</span><span class="p">></span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="p"></</span><span class="nt">table</span><span class="p">></span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>You’ll see that the <tt class="docutils literal">pdfstylefonts.css</tt> is included as a Django template (so that <tt class="docutils literal">{% static %}</tt> will
work in that file) while <tt class="docutils literal">pdfstyle.css</tt> is included using <tt class="docutils literal">{% static %}</tt>.
Als, notice that I’ve also added a static image (using the <tt class="docutils literal">{% static %}</tt> tag) and a dynamic (media)
file to show off how great the url-to-local-path mechanism works. Please notice that for the
media files to work fine in your development environment you need to configure the
<tt class="docutils literal">MEDIA_URL</tt> and <tt class="docutils literal">MEDIA_ROOT</tt> settigns (similar to <tt class="docutils literal">STATIC_URL</tt> and <tt class="docutils literal">STATIC_ROOT</tt>) and follow the
<a class="reference external" href="https://docs.djangoproject.com/en/1.8/howto/static-files/#serving-files-uploaded-by-a-user-during-development">serve files uploaded by a user during development</a> tutorial on Django docs.</p>
<p>Finally, if you configure a PdfResponseMixin ListView like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">BookExPdfListView</span><span class="p">(</span><span class="n">PdfResponseMixin</span><span class="p">,</span> <span class="n">ListView</span><span class="p">):</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'books'</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'books/book_list_ex.html'</span>
</pre></div>
<p>you should see be able to see the correct (calibri) font (defined in <tt class="docutils literal">pdfstylefonts.css</tt>), with unicode characters without problems
including both the static and user uploaded images and with the styles defined in the pdf stylesheet (<tt class="docutils literal">pdfstyle.css</tt>).</p>
</div>
<div class="section" id="configure-django-for-debugging-pdf-creation">
<h3><a class="toc-backref" href="#toc-entry-12">Configure Django for debugging <span class="caps">PDF</span> creation</a></h3>
<p>If you experience any problems, you can configure xhtml2pdf to output <span class="caps">DEBUG</span> information. To do this,
you may change your django logging configuration like this:</p>
<div class="highlight"><pre><span></span><span class="n">LOGGING</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'version'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="s1">'disable_existing_loggers'</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'console'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'class'</span><span class="p">:</span> <span class="s1">'logging.StreamHandler'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'loggers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'xhtml2pdf'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'console'</span><span class="p">],</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>This configuration will keep existing loggers (<tt class="docutils literal">'disable_existing_loggers': False,</tt>) and will configure
<tt class="docutils literal">xhtml2pdf</tt> to log its output to the console, helping us find out why some things won’t be working.</p>
</div>
<div class="section" id="concatenating-pdfs-in-django">
<h3><a class="toc-backref" href="#toc-entry-13">Concatenating PDFs in Django</a></h3>
<p>The final section of the <span class="caps">PDF</span>-Django-integration is to explain how we can concatenate PDFs in django using PyPDF2. There may be
some other requirements like extracting pages from PDFs however the most common one as explained before is to just append
the pages of one <span class="caps">PDF</span> after the other — after all using PyPDF2 is really easy after you get the hang of it.</p>
<p>To be more <span class="caps">DRY</span>, I will create a <tt class="docutils literal">CoverPdfResponseMixin</tt> that will output a <span class="caps">PDF</span> <em>with</em> a cover. To be <em>even more</em> <span class="caps">DRY</span>,
I will refactor <tt class="docutils literal">PdfResponseMixin</tt> to put some common code in an extra method so that <tt class="docutils literal">CoverPdfResponseMixin</tt> could inherit from it:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">PdfResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">write_pdf</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">file_object</span><span class="p">,</span> <span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">()</span>
<span class="n">template</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">generate_pdf</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">file_object</span><span class="o">=</span><span class="n">file_object</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="n">context</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">write_pdf</span><span class="p">(</span><span class="n">resp</span><span class="p">)</span>
<span class="k">return</span> <span class="n">resp</span>
<span class="k">class</span> <span class="nc">CoverPdfResponseMixin</span><span class="p">(</span><span class="n">PdfResponseMixin</span><span class="p">,</span> <span class="p">):</span>
<span class="n">cover_pdf</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">merger</span> <span class="o">=</span> <span class="n">PdfFileMerger</span><span class="p">()</span>
<span class="n">merger</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">cover_pdf</span><span class="p">,</span> <span class="s2">"rb"</span><span class="p">))</span>
<span class="n">pdf_fo</span> <span class="o">=</span> <span class="n">StringIO</span><span class="o">.</span><span class="n">StringIO</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">write_pdf</span><span class="p">(</span><span class="n">pdf_fo</span><span class="p">)</span>
<span class="n">merger</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">pdf_fo</span><span class="p">)</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
<span class="n">merger</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">resp</span><span class="p">)</span>
<span class="k">return</span> <span class="n">resp</span>
</pre></div>
<p>So, <tt class="docutils literal">PdfResponseMixin</tt> now has a <tt class="docutils literal">write_pdf</tt> method that gets a file-like object and outputs the <span class="caps">PDF</span> there.
The new mixin, <tt class="docutils literal">CoverPdfResponseMixin</tt> has a <tt class="docutils literal">cover_pdf</tt> attribute that should be configured with the filesystem
path of the cover file. The <tt class="docutils literal">render_to_response</tt> method now will create a <tt class="docutils literal">PdfFileMerger</tt> (which is empty
initially) to which it appends the contents <tt class="docutils literal">cover_pdf</tt>. After that, it creates a file-stream (using StreamIO)
and uses <tt class="docutils literal">write_pdf</tt> to create the <span class="caps">PDF</span> there and appends that file-stream to the merger. Finally, it writes
the merger contents to the <tt class="docutils literal">HttpResponse</tt>.</p>
<p>One thing that I’ve seen is that if you want to concatenate many PDFs with many pages sometimes you’ll get
a strange an error when using <tt class="docutils literal">PdfFileMerger</tt>. I was able to overcome this by reading and appending the pages of each
<span class="caps">PDF</span> to-be-appended one by one using the <tt class="docutils literal">PdfFileReader</tt> and <tt class="docutils literal">PdfFileWriter</tt> objects. Here’s a small snippet of how
this could be done:</p>
<div class="highlight"><pre><span></span><span class="n">pdfs</span> <span class="o">=</span> <span class="p">[]</span> <span class="c1"># List of pdfs to be concatenated</span>
<span class="n">writer</span> <span class="o">=</span> <span class="n">PdfFileWriter</span><span class="p">()</span>
<span class="k">for</span> <span class="n">pdf</span> <span class="ow">in</span> <span class="n">pdfs</span><span class="p">:</span>
<span class="n">reader</span> <span class="o">=</span> <span class="n">PdfFileReader</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">pdf</span><span class="p">,</span> <span class="s2">"rb"</span><span class="p">))</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">reader</span><span class="o">.</span><span class="n">getNumPages</span><span class="p">()):</span>
<span class="n">writer</span><span class="o">.</span><span class="n">addPage</span><span class="p">(</span><span class="n">reader</span><span class="o">.</span><span class="n">getPage</span><span class="p">(</span><span class="n">i</span><span class="p">))</span>
<span class="n">resp</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/pdf'</span><span class="p">)</span>
<span class="n">writer</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">resp</span><span class="p">)</span>
<span class="k">return</span> <span class="n">resp</span>
</pre></div>
</div>
</div>
<div class="section" id="more-advanced-xhtml2pdf-features">
<h2><a class="toc-backref" href="#toc-entry-14">More advanced xhtml2pdf features</a></h2>
<p>In this section I will present some information on how to use various xhtml2pdf features
to create the most common required printed document features, for example adding page
numbers, adding headers and footers etc.</p>
<div class="section" id="laying-out">
<h3><a class="toc-backref" href="#toc-entry-15">Laying out</a></h3>
<p>To find out how you can create your pages you should read the
<a class="reference external" href="https://github.com/xhtml2pdf/xhtml2pdf/blob/master/doc/usage.rst#defining-page-layouts">defining page layouts</a> section of the xhtml2pdf manual. There
you’ll see that the basic components of laying out in xhtml2pdf is
@page to create pages and @frames to create rectangular components
inside these pages. So each page will have a number of frames
inside it. These frames are seperated to static and dynamic. Staticsome
should be used for things like headers
and footers (so they’ll be the same across all pages) and dynamic will
contain the main content of the report.</p>
<p>For pages you can use any page size you want, not just the specified ones. For example, in one of my
projects I wanted to create a <span class="caps">PDF</span> print on a normal plastic card, so I’d used the
following <tt class="docutils literal">size: 8.56cm 5.398cm;</tt>. Also, page templates can be named
and you can use different ones in the same <span class="caps">PDF</span> (so you could create
a cover page with a different page template, use it first and then continue
with the normal pages). To name a tempalte you just use @page template_name {}
and to change the template use the combination of the following two xhtml2pdf tags:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">pdf:nexttemplate</span> <span class="na">name</span><span class="o">=</span><span class="s">"back_page"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">pdf:nextpage</span> <span class="p">/></span>
</pre></div>
<p>Now, one thing I’ve noticed is that you are not able to use a named template for the first
page of your <span class="caps">PDF</span>. So, what I’ve done is that I create an anonymous (default) page for the
first page of the report. If I want to <em>reuse</em> it in another page, I copy it and name it
accordingly. I will give an example shortly.</p>
<p>Now, for frames, I recommend using the <tt class="docutils literal"><span class="pre">-pdf-frame-border:</span> 1;</tt> command for debugging where
they are actually printed. Also, I recommend using a normal ruler and measuring completely
where you want them to be. For example, for the following frame:</p>
<div class="highlight"><pre><span></span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">photo_frame</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">-pdf-frame-border</span><span class="o">:</span><span class="w"> </span><span class="nt">1</span><span class="o">;</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">left</span><span class="o">:</span><span class="w"> </span><span class="nt">6</span><span class="p">.</span><span class="nc">2cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">1</span><span class="p">.</span><span class="nc">9cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">2cm</span><span class="o">;</span>
<span class="p">}</span>
</pre></div>
<p>I’d used a ruler to find out that I want it to start 2.4 cm from the top of my page (actually a credit card)
and 6.2 cm from the left and have a width and height of 1.9 and 2.2 cm.</p>
<p>I recommend naming all frames to be able to distinguish them however I don’t think that their name plays
any other role. However, for static frames you must define the id of the content they will contain using <tt class="docutils literal"><span class="pre">-pdf-frame-content</span></tt>
in the css and a div with the corresponding id in the <span class="caps">PDF</span> template. For example, you could define a frame header like this</p>
<div class="highlight"><pre><span></span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">header</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">-pdf-frame-content</span><span class="o">:</span><span class="w"> </span><span class="nt">headerContent</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">8in</span><span class="o">;</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">5cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">margin-left</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">5cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">margin-right</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">5cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">2cm</span><span class="o">;</span>
<span class="p">}</span>
</pre></div>
<p>and its content like this:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'headerContent'</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span> <span class="p">></span>
Header !
<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>Please notice that if for some reason the stuff you want to put in your static frames does not fit there the frame will
be totally empty. This means that if you have size for three lines but you want to output five lines in a static frame
then you’ll see no lines!</p>
<p>Now, for dynamic content you can expect the opposite behavior:
You <em>cannot</em> select to which dynamic frame your content goes, instead the content just just flows to the first dynamic frame it
fits! If it does not fit in any dynamic frames in the current page then a new page will be created.</p>
<p>I’d like to present a full example here on the already mentioned project of printing a plastic card for the books. I had two page layouts,
one for the front page having a frame with the owner’s data and another frame with his photo and one for the back page having
a barcode. This is a Django template that is used to print not only but a <span class="caps">PDF</span> with a group of these cards:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">"Content-Type"</span> <span class="na">content</span><span class="o">=</span><span class="s">"text/html; charset=utf-8"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">style</span><span class="p">></span>
<span class="w"> </span><span class="cp">{%</span> <span class="k">include</span> <span class="s2">"pdfstylefonts.css"</span> <span class="cp">%}</span>
<span class="p"></</span><span class="nt">style</span><span class="p">></span>
<span class="p"><</span><span class="nt">style</span> <span class="na">type</span><span class="o">=</span><span class="s">'text/css'</span><span class="p">></span>
<span class="w"> </span><span class="p">@</span><span class="k">page</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">size</span><span class="o">:</span><span class="w"> </span><span class="nt">8</span><span class="p">.</span><span class="nc">56cm</span><span class="w"> </span><span class="nt">5</span><span class="p">.</span><span class="nc">398cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">margin</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">padding</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="o">;</span>
<span class="w"> </span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">table_frame</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c">/* -pdf-frame-border: 1; */</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">left</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">5</span><span class="p">.</span><span class="nc">5cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">3cm</span><span class="o">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">photo_frame</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c">/* -pdf-frame-border: 1; */</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">left</span><span class="o">:</span><span class="w"> </span><span class="nt">6</span><span class="p">.</span><span class="nc">2cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">1</span><span class="p">.</span><span class="nc">9cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">2cm</span><span class="o">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">@</span><span class="k">page</span><span class="w"> </span><span class="nt">front_page</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">size</span><span class="o">:</span><span class="w"> </span><span class="nt">8</span><span class="p">.</span><span class="nc">56cm</span><span class="w"> </span><span class="nt">5</span><span class="p">.</span><span class="nc">398cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">margin</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">padding</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="o">;</span>
<span class="w"> </span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">table_frame</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c">/* -pdf-frame-border: 1; */</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">left</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">5</span><span class="p">.</span><span class="nc">5cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">3cm</span><span class="o">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">photo_frame</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c">/* -pdf-frame-border: 1; */</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">4cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">left</span><span class="o">:</span><span class="w"> </span><span class="nt">6</span><span class="p">.</span><span class="nc">2cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">1</span><span class="p">.</span><span class="nc">9cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">2</span><span class="p">.</span><span class="nc">2cm</span><span class="o">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">@</span><span class="k">page</span><span class="w"> </span><span class="nt">back_page</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">size</span><span class="o">:</span><span class="w"> </span><span class="nt">8</span><span class="p">.</span><span class="nc">56cm</span><span class="w"> </span><span class="nt">5</span><span class="p">.</span><span class="nc">398cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">margin</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="w"> </span><span class="nt">0</span><span class="p">.</span><span class="nc">0cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">padding</span><span class="o">:</span><span class="w"> </span><span class="nt">0</span><span class="o">;</span>
<span class="w"> </span><span class="p">@</span><span class="k">frame</span><span class="w"> </span><span class="nt">barcode_frame</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="c">/* -pdf-frame-border: 1; */</span>
<span class="w"> </span><span class="nt">top</span><span class="o">:</span><span class="w"> </span><span class="nt">3</span><span class="p">.</span><span class="nc">9cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">left</span><span class="o">:</span><span class="w"> </span><span class="nt">1</span><span class="p">.</span><span class="nc">8cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">width</span><span class="o">:</span><span class="w"> </span><span class="nt">4</span><span class="p">.</span><span class="nc">9cm</span><span class="o">;</span>
<span class="w"> </span><span class="nt">height</span><span class="o">:</span><span class="w"> </span><span class="nt">1</span><span class="p">.</span><span class="nc">1cm</span><span class="o">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="o">*,</span><span class="w"> </span><span class="nt">html</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">font-family</span><span class="p">:</span><span class="w"> </span><span class="s2">"Calibri"</span><span class="p">;</span>
<span class="w"> </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="kt">pt</span><span class="p">;</span>
<span class="w"> </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">80</span><span class="kt">%</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="p">.</span><span class="nc">bigger</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="k">font-size</span><span class="p">:</span><span class="mi">9</span><span class="kt">pt</span><span class="p">;</span>
<span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="kc">bold</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p"></</span><span class="nt">style</span><span class="p">></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="cp">{%</span> <span class="k">for</span> <span class="nv">book</span> <span class="k">in</span> <span class="nv">books</span> <span class="cp">%}</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="cp"><! -- Here I print the card data. It fits exactly in the table_frame so ... -></span>
<span class="cp">{{</span> <span class="nv">book.title</span> <span class="cp">}}</span><span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 2<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 3<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 4<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 5<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 6<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 7<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 8<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 9<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
Data line 10<span class="p"><</span><span class="nt">br</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="cp"><! -- This photo here will be outputted to the photo_frame --></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">'</span><span class="cp">{{</span> <span class="nv">book.cover.url</span> <span class="cp">}}</span><span class="s">'</span> <span class="na">style</span><span class="o">=</span><span class="s">'width:1.9cm ; height: 2.2cm ; '</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="cp"><! -- Now the template is chaned to print the back of the card --></span>
<span class="p"><</span><span class="nt">pdf:nexttemplate</span> <span class="na">name</span><span class="o">=</span><span class="s">"back_page"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">pdf:nextpage</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="p">></span>
<span class="p"><</span><span class="nt">center</span><span class="p">></span>
<span class="cp"><! -- Print the barcode to the barcode_frame --></span>
<span class="p"><</span><span class="nt">pdf:barcode</span> <span class="na">value</span><span class="o">=</span><span class="s">"</span><span class="cp">{{</span> <span class="nv">book.id</span> <span class="cp">}}</span><span class="s">"</span> <span class="na">type</span><span class="o">=</span><span class="s">"code128"</span> <span class="na">humanreadable</span><span class="o">=</span><span class="s">"1"</span> <span class="na">barwidth</span><span class="o">=</span><span class="s">"0.43mm"</span> <span class="na">barheight</span><span class="o">=</span><span class="s">"0.6cm"</span> <span class="na">align</span><span class="o">=</span><span class="s">"middle"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">center</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="cm"><!-- Use the front_page template again for the next card --></span>
<span class="p"><</span><span class="nt">pdf:nexttemplate</span> <span class="na">name</span><span class="o">=</span><span class="s">"front_page"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">pdf:nextpage</span> <span class="p">/></span>
<span class="cp">{%</span> <span class="k">endfor</span> <span class="cp">%}</span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<p>Notice that I want to print exactly 10 lines in the <tt class="docutils literal">table_frame</tt> (that’s why I use <br /> to go to next line) — if I had printed
3-4 lines (for example) then the photo <em>would fit</em> in the <tt class="docutils literal">table_frame</tt> and would be printed there and <em>not</em> in the <tt class="docutils literal">photo_frame</tt>! Also,
another interesting thing is that if I had
outputed 8-9 lines then the photo wouldn’t fit in that small space and would also be printed to the <tt class="docutils literal">photo_frame</tt>.</p>
</div>
<div class="section" id="extra-stuff">
<h3><a class="toc-backref" href="#toc-entry-16">Extra stuff</a></h3>
<p>Some extra things I want to mention concerning xhtml2pdf:</p>
<ul class="simple">
<li><tt class="docutils literal"><pdf:pagenumber></tt> to output the current page number (please end the line after this tag)</li>
<li><tt class="docutils literal"><pdf:pagecount></tt> to output the total page number (please end the line after this tag)</li>
</ul>
<p>So if you want to print a footer with the pages, I recommend something like this:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">'footerContent'</span><span class="p">></span>
Page <span class="p"><</span><span class="nt">pdf:pagenumber</span><span class="p">></span>
from <span class="p"><</span><span class="nt">pdf:pagecount</span><span class="p">></span>
total
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>Notice how the lines end after the tags.</p>
<ul class="simple">
<li><tt class="docutils literal"><pdf:barcode></tt>: Output a barcode in your <span class="caps">PDF</span>. Here are the possible barcode types: I2of5, Standard39, Extended39, Standard93, Extended93, <span class="caps">MSI</span>, Codabar, Code11, <span class="caps">FIM</span>, <span class="caps">POSTNET</span>, USPS_4State, Code128, <span class="caps">EAN13</span>, <span class="caps">EAN8</span>, <span class="caps">QR</span>.</li>
<li>You may add a page background image using the <tt class="docutils literal"><span class="pre">background-image</span></tt> property of <tt class="docutils literal">@page</tt>.</li>
</ul>
<p>For example, if you want the templates you create from your development/<span class="caps">UAT</span> systems to be different
than the production ones you could do something like this:</p>
<pre class="code literal-block">
@page {
{% if DEBUG %}
background-image: url({% static "debug.png" %});
{% endif %}
<!-- Other page properties -->
}
</pre>
</div>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-17">Conclusion</a></h2>
<p>I hope that using the techniques described in this essential guide you’ll
be able to create great looking <span class="caps">PDF</span> documents from your Django application
and overcome any difficulties that may arise. If you feel that there’s something
I’ve not covered properly (and is not covered by the documentation) please comment
out and I’ll be happy to research it a bit and update the article with info.</p>
<p>I am using all the above in various production applications for a long time and everything
is smooth so don’t afraid to create PDFs from Django!</p>
</div>
Improve your client-side-javascript workflow more by using ES62015-11-16T14:20:00+02:002015-11-16T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2015-11-16:/2015/11/16/using-browserify-es6/<p class="first last">Browserify can be used to integrate next generation javascript (es6) to your client side scripts using babel.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#npm-save-save-dev-and-avoiding-global-deps" id="toc-entry-2"><span class="caps">NPM</span>, —save, —save-dev and avoiding global deps</a></li>
<li><a class="reference internal" href="#using-babel" id="toc-entry-3">Using babel</a></li>
<li><a class="reference internal" href="#integrate-babel-with-browserify" id="toc-entry-4">Integrate babel with browserify</a></li>
<li><a class="reference internal" href="#the-object-spread-operator" id="toc-entry-5">The object-spread operator</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-6">Conclusion</a></li>
</ul>
</div>
<p><strong>Update 22/12/2015:</strong> Add section for the object-spread operator.</p>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>In a <a class="reference external" href="https://spapas.github.io/2015/05/27/using-browserify-watchify/">previous article</a>
we presented a simple workflow to improve your client-side (javascript) workflow
by including a bunch of tools from the node-js world i.e <a class="reference external" href="http://browserify.org/">browserify</a> and
its friends, <a class="reference external" href="https://github.com/substack/watchify">watchify</a> and <a class="reference external" href="https://www.npmjs.com/package/uglify-js">uglify-js</a>.</p>
<p>Using these tools we were able to properly manage our client side dependencies
(no more script tags in our html templates) and modularize our code
(so we could avoid monolithic javascript files that contained all our code).
Another great improvement to our workflow would be to include <span class="caps">ES6</span> to the mix!</p>
<p><span class="caps">ES6</span> (or EcmaScript 2015) is more or less “Javascript: <span class="caps">TNG</span>”. It has many features
that greatly improve the readability and writability of javascript code - for instance,
thick arrows, better classes, better modules, iterators/generators, template strings,
default function parameters and many others!
More info on these features and how they could be used can be found on the <a class="reference external" href="https://github.com/lukehoban/es6features">es6features</a> repository.</p>
<p>Unfortunately, these features are either not supported at all, or they are partially supported
on current (November 2015) browsers. However all is <em>not</em> lost yet: Since we already use browserify
to our client side workflow, we can easily enable it to read source files with <span class="caps">ES6</span> syntax
and transform them to normal javascript using a transformation through the <a class="reference external" href="https://babeljs.io/">babel</a> tool.</p>
<p>In the following, we’ll talk a bit more about the node-js package manager (npm) features,
talk a little about babel and finally
modify our workflow so that it will be able to use <span class="caps">ES6</span>! Please notice that to properly follow this article
you need to first read the <a class="reference external" href="https://spapas.github.io/2015/05/27/using-browserify-watchify/">previous one</a>.</p>
</div>
<div class="section" id="npm-save-save-dev-and-avoiding-global-deps">
<h2><a class="toc-backref" href="#toc-entry-2"><span class="caps">NPM</span>, —save, —save-dev and avoiding global deps</a></h2>
<p>In the previous article, I had recommended to install the needed tools
(needed to create the output bundle browserify, watchify, uglify) globally
using npm install -g package (of course the normal dependencies like moment.js
would be installed locally).
This has one advantage and two disadvantages: It
puts these tools to the path so they can be called immediately (i.e browserify)
but you will need root access to install a package globally and nothing is
saved on <tt class="docutils literal">package.json</tt> so you don’t know which packages must be installed
in order to start developing the project!</p>
<p>This could be ok for the introductionary article, however for this one I
will propose another alternative: Install all the tools <em>locally</em> using just
<tt class="docutils literal">npm install package <span class="pre">--save</span></tt>. These tools will be put to the <tt class="docutils literal">node_modules</tt> folder. They
<em>will not</em> be put to the path, but if you want to execute a binary by yourself
to debug the output, you can find it in <tt class="docutils literal">node_modules/bin</tt>, for example,
for browserify you can run <tt class="docutils literal">node_modules/bin/browserify</tt>. Another intersting
thing is that if you create executable scripts in your <tt class="docutils literal">package.json</tt> you
don’t actually need to include the full path but the binaries will be found.</p>
<p>Another thing I’d like to discuss here is the difference between <tt class="docutils literal"><span class="pre">--save</span></tt>
and <tt class="docutils literal"><span class="pre">--save-dev</span></tt> options that can be passed to npm install. If you take
a look at other guides you’ll see that people are using <tt class="docutils literal"><span class="pre">--save-dev</span></tt> for
development dependencies (i.e testing) and <tt class="docutils literal"><span class="pre">--save</span></tt> for normal dependencies.
The difference is that these dependencies are saved in different places in
<tt class="docutils literal">package.json</tt> and if you run <tt class="docutils literal">npm install <span class="pre">--production</span></tt> you’ll get only
the normal dependencies (while, if you run <tt class="docutils literal">npm install</tt> all dependencies
will be installed). In these articles, I chose to just use <tt class="docutils literal"><span class="pre">--save</span></tt> everywhere,
after all the only thing that would be needed for production would be the
<tt class="docutils literal">bundle.js</tt> output file.</p>
</div>
<div class="section" id="using-babel">
<h2><a class="toc-backref" href="#toc-entry-3">Using babel</a></h2>
<p>The <a class="reference external" href="https://babeljs.io/">babel</a> library “is a JavaScript compiler”. It gets input files in a variant
of javascript (for example, <span class="caps">ES6</span>) and produces normal javascript files — something
like what browserify transforms do. However, what babel does (and I think its
the only tool that does this) is that it allows you to use <span class="caps">ES6</span> features <em>now</em> by
transpiling them to normal (<span class="caps">ES5</span>) javascript. Also, babel has <a class="reference external" href="https://babeljs.io/docs/plugins/">various other transforms</a>,
including a react transform
(so you can use this instead of the reactify browserify-transform)!</p>
<p>In any case, to be able to use <span class="caps">ES6</span>, we’ll need to install babel and its es6 presets
(don’t forget that you need to have a <tt class="docutils literal">package.json</tt> for the dependencies to be
saved so either do an <tt class="docutils literal">npm init</tt> or create a <tt class="docutils literal">package.json</tt> file containing only
<tt class="docutils literal">{}</tt>):</p>
<pre class="code literal-block">
npm install babel babel-preset-es2015 --save
</pre>
<p>If we wanted to also use babel for react we’d need to install babel-preset-react.</p>
<p>To configure babel we can either add a <tt class="docutils literal">babel</tt>
section in our <tt class="docutils literal">package.json</tt> or create a new file named .babelrc and put the configuration there.</p>
<p>I prefer the first one since we are already using the <tt class="docutils literal">package.json</tt>. So add the following attribute
to your <tt class="docutils literal">package.json</tt>:</p>
<pre class="code literal-block">
"babel": {
"presets": [
"es2015"
]
}
</pre>
<p>If you wanted to configure it through <tt class="docutils literal">.babelrc</tt> then you’d just copy to it the contents of <tt class="docutils literal">"babel"</tt>.</p>
<p>To do some tests with babel, you can install its cli (it’s not included in the babel package) through
<tt class="docutils literal">npm install <span class="pre">babel-cli</span></tt>. Now, you can run <tt class="docutils literal"><span class="pre">node_modules/.bin/babel</span></tt>. For example, create a
file named <tt class="docutils literal">testbabel.js</tt> with the following contents (thick arrow):</p>
<pre class="code literal-block">
[1,2,3].forEach(x => console.log(x) );
</pre>
<p>when you pass it to babel you’ll see the following output:</p>
<pre class="code literal-block">
>node_modules\.bin\babel testbabel.js
"use strict";
[1, 2, 3].forEach(function (x) {
return console.log(x);
});
</pre>
</div>
<div class="section" id="integrate-babel-with-browserify">
<h2><a class="toc-backref" href="#toc-entry-4">Integrate babel with browserify</a></h2>
<p>To call babel from browserify we’re going to use the <a class="reference external" href="https://github.com/babel/babelify">babelify</a> browserify transform which
actually uses babel to transpile the browserify input. After installing it with</p>
<pre class="code literal-block">
npm install babelify --save
</pre>
<p>you need to tell browserify to use it. To do this, you’ll just pass a -t babelify parameter to
browserify. So if you run it with the <tt class="docutils literal">testbabel.js</tt> file as input you’ll see the following output:</p>
<pre class="code literal-block">
>node_modules\.bin\browserify -t babelify testbabel.js
[...] browserify gibberish
"use strict";
[1, 2, 3].forEach(function (x) {
return console.log(x);
});
[...] more browserify gibberish
</pre>
<p>yey — the code is transpiled to <span class="caps">ES5</span>!</p>
<p>To create a complete project, let’s add a normal requirement (moment.js):</p>
<pre class="code literal-block">
npm install moment --save
</pre>
<p>and a file named <tt class="docutils literal">src\main.js</tt> that uses it with <span class="caps">ES6</span> syntax:</p>
<pre class="code literal-block">
import moment from 'moment';
const arr = [1,2,3,4,5];
arr.forEach(x => setTimeout(() => console.log(`Now: ${moment().format("HH:mm:ss")}, Later: ${moment().add(x, "days").format("L")}...`), x*1000));
</pre>
<p>To create the output javascript file, we’ll use the browserify and watchify commands with the
addition of the -t babelify switch. Here’s the complete <tt class="docutils literal">package.json</tt> for this project:</p>
<pre class="code literal-block">
{
"dependencies": {
"babel": "^6.1.18",
"babel-preset-es2015": "^6.1.18",
"babelify": "^7.2.0",
"browserify": "^12.0.1",
"moment": "^2.10.6",
"uglify-js": "^2.6.0",
"watchify": "^3.6.1"
},
"scripts": {
"watch": "watchify src/main.js -o dist/bundle.js -v -t babelify",
"build": "browserify src/main.js -t babelify | uglifyjs -mc warnings=false > dist/bundle.js"
},
"babel": {
"presets": [
"es2015"
]
}
}
</pre>
<p>Running <tt class="docutils literal">npm run build</tt> should create a <tt class="docutils literal">dist/bundle.js</tt> file. If you include this in an html,
you should see something like this in the console:</p>
<pre class="code literal-block">
Now: 13:52:09, Later: 11/17/2015...
Now: 13:52:10, Later: 11/18/2015...
</pre>
</div>
<div class="section" id="the-object-spread-operator">
<h2><a class="toc-backref" href="#toc-entry-5">The object-spread operator</a></h2>
<p>Many examples in the internet use the <a class="reference external" href="https://facebook.github.io/react/docs/jsx-spread.html#spread-attributes">object spread operator</a> which is <a class="reference external" href="http://stackoverflow.com/questions/31115276/ecmascript-6-spread-operator-in-object-deconstruction-support-in-typescript-and">not part of es6</a> so our
proposed babel configuration does not support it!
To be able to use this syntax, we’ll need to install the corresponding babel plugin by using
<tt class="docutils literal">npm install <span class="pre">babel-plugin-transform-object-rest-spread</span> <span class="pre">--save</span></tt> and add it to our babel configuration
in the plugins section, something like this:</p>
<pre class="code literal-block">
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
</pre>
<p>If everything is ok this should be transpiled without errors using <tt class="docutils literal"><span class="pre">node_modules\.bin\browserify</span> testbabe.js <span class="pre">-t</span> babelify</tt></p>
<pre class="code literal-block">
let x = {a:1 , b:2 };
let y = {...x, c: 3};
</pre>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-6">Conclusion</a></h2>
<p>Using the combination of babel and javascript we can easily write <span class="caps">ES6</span> code in our modules! This,
along with the modularization of our code and the management of client-side dependencies should
make client side development a breeze!</p>
<p>Please notice that to keep the presented workflow simple and easy to
replicate and configure, we have not used any external
task runners (like gulp or grunt) — all configuration is kept in a single file (package.json) and
the whole environment can be replicated just by doing a <tt class="docutils literal">npm install</tt>. Of course, the capabilities of
browserify are not unlimited, so if you wanted to do something more complicated
(for instance, lint your code before passing it to browserify) you’d need to use the mentioned
task runners (or webpack which is the current trend in javascript bundlers and actually replaces
the task runners).</p>
</div>
Django dynamic tables and filters for similar models2015-10-05T14:20:00+03:002015-10-05T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-10-05:/2015/10/05/django-dynamic-tables-similar-models/<p class="first last">Creating <span class="caps">DRY</span> and dynamic tables and forms for similar models in django</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#the-problem-we-ll-solve" id="toc-entry-2">The problem we’ll solve</a></li>
<li><a class="reference internal" href="#adding-the-dynamic-table" id="toc-entry-3">Adding the dynamic table</a></li>
<li><a class="reference internal" href="#creating-a-dynamic-form-for-filtering" id="toc-entry-4">Creating a dynamic form for filtering</a></li>
<li><a class="reference internal" href="#creating-the-dynamic-cbv" id="toc-entry-5">Creating the dynamic <span class="caps">CBV</span></a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-6">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>One of my favorite django apps is <a class="reference external" href="https://github.com/bradleyayers/django-tables2">django-tables2</a>: It allows you to
easily create pagination and sorting enabled <span class="caps">HTML</span> tables to represent
your model data using the usual djangonic technique (similar to how
you <a class="reference external" href="https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#modelform">create ModelForms</a>). I use it to almost all my projects to
represent the data, along with <a class="reference external" href="https://github.com/alex/django-filter">django-filter</a> to create forms
to filter my model data. I’ve written <a class="reference external" href="http://stackoverflow.com/questions/13611741/django-tables-column-filtering/15129259#15129259">a nice <span class="caps">SO</span> answer</a> with
instructions on how to use django-filter along with django-tables2.
This article will describe a technique that allows you to generate
tables and filters for your models in the most <span class="caps">DRY</span> way!</p>
</div>
<div class="section" id="the-problem-we-ll-solve">
<h2><a class="toc-backref" href="#toc-entry-2">The problem we’ll solve</a></h2>
<p>The main tool django-tables2 offers is a template tag called <tt class="docutils literal">render_table</tt>
that gets an instance of a subclass of <tt class="docutils literal">django_tables2.Table</tt>
which contains a description of the table (columns, style etc) along
with a queryset with the table’s data and outputs it to the page. A nice
extra feature of render_table is that you could pass it just a simple
django queryset and it will output it without the need to create the
custom Table class. So you can do something like {% render_table User.objects.all() %}
and get a quick output of your Users table.</p>
<p>In one of my projects I had three models that
were used for keeping a different type of business log in the database (for auditing reasons)
and was using the above feature to display these logs to the administrators, just by
creating a very simple <tt class="docutils literal">ListView</tt> (a different one for each Log type) and using the
<tt class="docutils literal">render_table</tt> to display the data. So I’d created three views limilar to this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AuditLogListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">AuditLog</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'logs'</span>
</pre></div>
<p>which all used a single template that contained a line <tt class="docutils literal">{% render_table logs %}</tt> to display
the table (the <tt class="docutils literal">logs</tt> context variable contains the list/queryset of <tt class="docutils literal">AuditLog</tt> s)
so <tt class="docutils literal">render_table</tt> will just output that list in a page. Each view of course had a different
<tt class="docutils literal">model</tt> attribute.</p>
<p>This was a nice <span class="caps">DRY</span> (but quick and dirty solution) that soon was not enough to fulfill the
needs of the administrators since they neeeded to have default sorting, filtering, hide
the primary key-id column etc. The obvious solution for that would be to just create three different
<tt class="docutils literal">Table</tt> subclasses that
would more or less have the same options with only their <tt class="docutils literal">model</tt> attributte different. I didn’t
like this solution that much since it seemed non-<span class="caps">DRY</span> to me and I would instead prefer to
create a generic <tt class="docutils literal">Table</tt> (that wouldn’t have a <tt class="docutils literal">model</tt> attributte) and would just output
its data with the correct options — so instead of three <tt class="docutils literal">Table</tt> classes I’d like to create
just a single one with common options that would display its data (what <tt class="docutils literal">render_table</tt> does).</p>
<p>Unfortunately, I couldn’t find a solution to this, since when I ommited the <tt class="docutils literal">model</tt> attribute
from the <tt class="docutils literal">Table</tt> subclass nothing was outputed (there is no way to define fields to
display in a <tt class="docutils literal">Table</tt> without also defining the model). An obvious (and <span class="caps">DRY</span>) resolution would be to create
a base <tt class="docutils literal">Table</tt> subclass that would define the needed options and create three subclasses
that would inherit from this class and override only the <tt class="docutils literal">model</tt> attribute. This unfortunately was not
possible becase <a class="reference external" href="http://stackoverflow.com/questions/16696066/django-tables2-dynamically-adding-columns-to-table-not-adding-attrs-to-table/16741665#16741665">inheritance does not work well with django-tables2</a>!</p>
<p>Furthermore, there’s the extra hurdle of adding filtering to the above tables so that
the admin’s would be able to quickly find a log message - if we wanted to use django-filter
we’d need again to create three different subclasses (one for each log model type)
of <tt class="docutils literal">django_filter.FilterSet</tt> since
django-filter requires you to define the model for which the filter will be created!</p>
<p>One cool way to resolve such problems is to create your classes dynamically when
you need ‘em. I’ve already described a way to <a class="reference external" href="https://spapas.github.io/2013/12/24/django-dynamic-forms/">dynamically create forms in django
in a previous post</a>. Below, I’ll describe
a similar technique which can be used to create both dynamic tables and filters. Using
this methodology, you’ll be able to add a new <span class="caps">CBV</span> that displays a table with
pagination, order and filtering for your model by just inheriting from a base class!</p>
</div>
<div class="section" id="adding-the-dynamic-table">
<h2><a class="toc-backref" href="#toc-entry-3">Adding the dynamic table</a></h2>
<p>To create the <span class="caps">DRY</span> <span class="caps">CBV</span> we’ll use the <a class="reference external" href="http://django-tables2.readthedocs.org/en/latest/pages/generic-mixins.html?highlight=singletableview">SingleTableView</a> (<tt class="docutils literal">django_tables2.SingleTableView</tt>)
as a base and override its <tt class="docutils literal">get_table_class</tt> method to dynamically create our table class
using <tt class="docutils literal">type</tt>.
Here’s how it could be done using a mixin (notice that this mixin should be used in a
<tt class="docutils literal">SingleTableView</tt> to override its <tt class="docutils literal">get_table_class</tt> method):</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AddTableMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="n">table_pagination</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"per_page"</span><span class="p">:</span> <span class="mi">25</span><span class="p">}</span>
<span class="k">def</span> <span class="nf">get_table_class</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_table_column</span><span class="p">(</span><span class="n">field</span><span class="p">):</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">field</span><span class="p">,</span> <span class="n">django</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">):</span>
<span class="k">return</span> <span class="n">tables</span><span class="o">.</span><span class="n">DateColumn</span><span class="p">(</span><span class="s2">"d/m/Y H:i"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="n">tables</span><span class="o">.</span><span class="n">Column</span><span class="p">()</span>
<span class="n">attrs</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
<span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">get_table_column</span><span class="p">(</span><span class="n">f</span><span class="p">))</span> <span class="k">for</span>
<span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">model</span><span class="o">.</span><span class="n">_meta</span><span class="o">.</span><span class="n">fields</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">f</span><span class="o">.</span><span class="n">name</span> <span class="o">==</span> <span class="s1">'id'</span>
<span class="p">)</span>
<span class="n">attrs</span><span class="p">[</span><span class="s1">'Meta'</span><span class="p">]</span> <span class="o">=</span> <span class="nb">type</span><span class="p">(</span><span class="s1">'Meta'</span><span class="p">,</span> <span class="p">(),</span> <span class="p">{</span><span class="s1">'attrs'</span><span class="p">:{</span><span class="s2">"class"</span><span class="p">:</span><span class="s2">"table"</span><span class="p">},</span> <span class="s2">"order_by"</span><span class="p">:</span> <span class="p">(</span><span class="s2">"-created_on"</span><span class="p">,</span> <span class="p">)</span> <span class="p">}</span> <span class="p">)</span>
<span class="n">klass</span> <span class="o">=</span> <span class="nb">type</span><span class="p">(</span><span class="s1">'DTable'</span><span class="p">,</span> <span class="p">(</span><span class="n">tables</span><span class="o">.</span><span class="n">Table</span><span class="p">,</span> <span class="p">),</span> <span class="n">attrs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">klass</span>
</pre></div>
<p>Let’s try to explain the <tt class="docutils literal">get_table_class</tt> method: First of all, we’ve defined a local <tt class="docutils literal">get_table_column</tt>
function that will return a django-tables2 column depending on the field of the model. For example, in
our case I wanted to use a <tt class="docutils literal">django_tables2.DateColumn</tt> with a specific format when a <tt class="docutils literal">DateTimeField</tt> is
encountered and for all other model fields just use the stock <tt class="docutils literal">Column</tt>. You may add other overrides here,
for example add a <tt class="docutils literal">TemplateColumn</tt> to properly render some data.</p>
<p>After that, we create a dictionary with all the attributes of the dynamic table model.
The <tt class="docutils literal">self.model</tt> field will contain
the model Class of this <tt class="docutils literal">SingleTableView</tt>, so using its <tt class="docutils literal">_meta.fields</tt> will return
the defined fields of that model. As we can see, I just use
a generator expression to create a tuple with the name of the field and its column type (using <tt class="docutils literal">get_table_column</tt>)
excluding the ‘id’ column. So, attrs will be a dictionary of field names and column types. Here you may
also exclude other columns you don’t want to display.</p>
<p>The <tt class="docutils literal">Meta</tt> class of this table is crated using <tt class="docutils literal">type</tt> which creates a parentless class by
defining all its attributes in a dictionary
and set it as the <tt class="docutils literal">Meta</tt> key in the previously defined <tt class="docutils literal">attrs</tt> dict.
Finally, we create the actual <tt class="docutils literal">django_tables2.Table</tt> subclass by
inheriting from it and passing the <tt class="docutils literal">attrs</tt> dict. We’ll see an example of what <tt class="docutils literal">get_table_class</tt> returns later.</p>
</div>
<div class="section" id="creating-a-dynamic-form-for-filtering">
<h2><a class="toc-backref" href="#toc-entry-4">Creating a dynamic form for filtering</a></h2>
<p>Let’s create another mixin that could be used to create a dynamic <tt class="docutils literal">django.Form</tt> subclass to the <span class="caps">CBV</span>:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AddFormMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,</span> <span class="p">):</span>
<span class="k">def</span> <span class="nf">define_form</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">get_form_field_type</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
<span class="k">return</span> <span class="n">forms</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">attrs</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span>
<span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">get_form_field_type</span><span class="p">(</span><span class="n">f</span><span class="p">))</span> <span class="k">for</span>
<span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_form_fields</span><span class="p">()</span> <span class="p">)</span>
<span class="n">klass</span> <span class="o">=</span> <span class="nb">type</span><span class="p">(</span><span class="s1">'DForm'</span><span class="p">,</span> <span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">,</span> <span class="p">),</span> <span class="n">attrs</span><span class="p">)</span>
<span class="k">return</span> <span class="n">klass</span>
<span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">define_form</span><span class="p">()</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">form</span> <span class="o">=</span> <span class="n">form_class</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">form</span> <span class="o">=</span> <span class="n">form_class</span><span class="p">()</span>
<span class="n">qs</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">AddFormMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_queryset</span><span class="p">()</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">form</span><span class="o">.</span><span class="n">data</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">form</span><span class="o">.</span><span class="n">is_valid</span><span class="p">():</span>
<span class="n">q_objects</span> <span class="o">=</span> <span class="n">Q</span><span class="p">()</span>
<span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_form_fields</span><span class="p">():</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">f</span><span class="p">):</span>
<span class="n">q_objects</span> <span class="o">&=</span> <span class="n">Q</span><span class="p">(</span><span class="o">**</span><span class="p">{</span><span class="n">f</span><span class="o">+</span><span class="s1">'__icontains'</span><span class="p">:</span><span class="bp">self</span><span class="o">.</span><span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="n">f</span><span class="p">]})</span>
<span class="n">qs</span> <span class="o">=</span> <span class="n">qs</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">q_objects</span><span class="p">)</span>
<span class="k">return</span> <span class="n">qs</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">AddFormMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'form'</span><span class="p">]</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">form</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
<p>The first method that will be called is the <tt class="docutils literal">get_queryset</tt> method that will generate the dynamic form
using <tt class="docutils literal">define_form</tt>. This method has a <tt class="docutils literal">get_form_field_type</tt> local function (similar to get_table_fields)
that can be used to override the types of the fields (or just fallback to a normal <tt class="docutils literal">CharField</tt>) and
then create the <tt class="docutils literal">attrs</tt> dictionary and <tt class="docutils literal">forms.Form</tt> subclass in a similar way as the <tt class="docutils literal">Table</tt> subclass. Here,
we don’t want to create a filter form from all fields of the model as we did on table, so instead
we’ll use a <tt class="docutils literal">get_form_fields</tt> (don’t confuse it with the local <tt class="docutils literal">get_form_field_type</tt>)
method that returns the name of the fields that we want to
use in the filtering form and needs to be defined in each <span class="caps">CBV</span> — the <tt class="docutils literal">get_form_fields</tt> must
be defined in classes that use this mixin.</p>
<p>After defining the form, we need to check if there’s anything to the <tt class="docutils literal"><span class="caps">GET</span></tt> dict — since we are just filtering the
queryset we’d need to submit the form with a <tt class="docutils literal"><span class="caps">GET</span></tt> (and not a <tt class="docutils literal"><span class="caps">POST</span></tt>). We see that if we have
data to our <tt class="docutils literal">request.<span class="caps">GET</span></tt> dictionary we’ll instantiate the form using this data (or else we’ll just
create an empty form). To do the actual filtering, we check if the form is valid and create a <tt class="docutils literal">django.models.db.Q</tt> object
that is used to combine (by <span class="caps">AND</span>) the conditions. Each of the individual Q objects that will be combined
(<tt class="docutils literal">&=</tt>)
to create the complete one will be created using the line <tt class="docutils literal"><span class="pre">Q(**{f+'__icontains':self.form.cleaned_data.get(f,</span> <span class="pre">'')})</span></tt>
(where f will be the name of the field) which is a nice trick: It will create a
dictionary of the form <tt class="docutils literal">{'action__icontains': 'test text'}</tt> and then pass this as a keyword
argument to the Q (using the <tt class="docutils literal">**</tt> mechanism), so <tt class="docutils literal">Q</tt> will be called like
<tt class="docutils literal"><span class="pre">Q(action__icontains='test</span> text')</tt> - this (using the <tt class="docutils literal"><span class="pre">**{key:val}</span></tt> trick) is
the only way to pass dynamic kwargs to a function!</p>
<p>Finally, the queryset will be filtered using the combined <tt class="docutils literal">Q</tt> object we just described.</p>
</div>
<div class="section" id="creating-the-dynamic-cbv">
<h2><a class="toc-backref" href="#toc-entry-5">Creating the dynamic <span class="caps">CBV</span></a></h2>
<p>Using the above mixins, we can easily create a dynamic <span class="caps">CBV</span> with a table and a filter form only by inheriting
from the mixins
and <tt class="docutils literal">SingleTableView</tt> and defining the <tt class="docutils literal">get_form_fields</tt> method:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AuditLogListView</span><span class="p">(</span><span class="n">AddTableMixin</span><span class="p">,</span> <span class="n">AddFormMixin</span><span class="p">,</span> <span class="n">SingleTableView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">AuditLog</span>
<span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">'logs'</span>
<span class="k">def</span> <span class="nf">get_form_fields</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="s1">'action'</span><span class="p">,</span><span class="s1">'user__username'</span><span class="p">,</span> <span class="p">)</span>
</pre></div>
<p>Let’s suppose that the <tt class="docutils literal">AuditLog</tt> is defined as following:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AuditLog</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span> <span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">editable</span><span class="o">=</span><span class="kc">False</span><span class="p">,)</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span> <span class="n">editable</span><span class="o">=</span><span class="kc">False</span><span class="p">,)</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">256</span><span class="p">,</span> <span class="n">editable</span><span class="o">=</span><span class="kc">False</span><span class="p">,)</span>
</pre></div>
<p>Using the above <tt class="docutils literal">AuditLogListView</tt>, a dynamic table and a dynamic form will be automaticall created whenever the view is visited.
The <tt class="docutils literal">Table</tt> class will be like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DTable</span><span class="p">(</span><span class="n">tables</span><span class="o">.</span><span class="n">Table</span><span class="p">):</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">tables</span><span class="o">.</span><span class="n">DateColumn</span><span class="p">(</span><span class="s2">"d/m/Y H:i"</span><span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">tables</span><span class="o">.</span><span class="n">Column</span><span class="p">()</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">tables</span><span class="o">.</span><span class="n">Column</span><span class="p">()</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">attrs</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"class"</span><span class="p">:</span><span class="s2">"table"</span><span class="p">}</span>
<span class="n">order_by</span> <span class="o">=</span> <span class="p">(</span><span class="s2">"-created_on"</span><span class="p">,</span> <span class="p">)</span>
</pre></div>
<p>and the <tt class="docutils literal">Form</tt> class like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">user__username</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">CharField</span><span class="p">()</span>
<span class="n">action</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">CharField</span><span class="p">()</span>
</pre></div>
<p>An interesting thing to notice is that we can drill down to foreign keys (f.e. <tt class="docutils literal">user__username</tt>) to create more interesting filters.
Also we could add some more
methods to be overriden by the implementing class beyond
the <tt class="docutils literal">get_form_fields</tt>. For example, instead of using <tt class="docutils literal">self.model._meta.fields</tt>
to generate the fields of the table, we
could instead use a <tt class="docutils literal">get_table_fields</tt> (similar to the <tt class="docutils literal">get_form_fields</tt>)
method that would be overriden in the implementing
classes (and even drill down on foreign keys to display more data on the table using <a class="reference external" href="http://django-tables2.readthedocs.org/en/latest/pages/accessors.html">accessors</a>).</p>
<p>Or, we could also define the form field types and lookups (instead of always using <tt class="docutils literal">CharField</tt> and
<tt class="docutils literal">icontains</tt> ) in the <tt class="docutils literal">get_form_fields</tt> — similar to django-filter.</p>
<p>Please notice that instead of creating a django form instance for filtering, we could instead create a django-filter instance with
a similar methodology. However, I preferred to just use a normal django form because it makes the whole process more clear and removes
a level of abstraction (we just create a <tt class="docutils literal">django.Form</tt> subclass while, if we used django-filter we’d need to create
a django-filter subclass which would create a <tt class="docutils literal">django.Form</tt> subclass)!</p>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-6">Conclusion</a></h2>
<p>Using the above technique we can quickly create a table and filter for a number of Models that all share
the same properties in the most <span class="caps">DRY</span>. This technique of course is useful only for quick CBVs that
are more or less the same and require little customization. Another interesting thing is that instead of
creating different <tt class="docutils literal">SingleTableView</tt> s we could instead create a single <span class="caps">CBV</span> that will get the content type
of the Model to be viewed as a parameter and retrieve the model (and queryset) from the content type - so
we could have a single <span class="caps">CBV</span> for all our table/filtering views !</p>
</div>
A (little more) complex react and flux example2015-09-08T12:55:00+03:002015-09-08T12:55:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-09-08:/2015/09/08/more-complex-react-flux-example/<p class="first last">A (little more) complex React and Flux example, continuing from the previous posts.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#more-about-flux-components" id="toc-entry-2">More about Flux components</a></li>
<li><a class="reference internal" href="#explaining-the-application-components" id="toc-entry-3">Explaining the application components</a></li>
<li><a class="reference internal" href="#reusable-application-components" id="toc-entry-4">Reusable application components</a><ul>
<li><a class="reference internal" href="#dropdown-react-js" id="toc-entry-5">DropDown.react.js</a></li>
<li><a class="reference internal" href="#datepicker-react-js" id="toc-entry-6">DatePicker.react.js</a></li>
<li><a class="reference internal" href="#pagingpanel-react-js" id="toc-entry-7">PagingPanel.react.js</a></li>
<li><a class="reference internal" href="#searchpanel-react-js" id="toc-entry-8">SearchPanel.react.js</a></li>
<li><a class="reference internal" href="#messagepanel" id="toc-entry-9">MessagePanel</a><ul>
<li><a class="reference internal" href="#messagepanel-react-js" id="toc-entry-10">MessagePanel.react.js</a></li>
<li><a class="reference internal" href="#messagestore-js" id="toc-entry-11">MessageStore.js</a></li>
<li><a class="reference internal" href="#messageactions" id="toc-entry-12">MessageActions</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#non-reusable-application-components" id="toc-entry-13">Non-reusable application components</a><ul>
<li><a class="reference internal" href="#booktable-react-js" id="toc-entry-14">BookTable.react.js</a></li>
<li><a class="reference internal" href="#authordialog-react-js" id="toc-entry-15">AuthorDialog.react.js</a></li>
<li><a class="reference internal" href="#authorpanel-react-js" id="toc-entry-16">AuthorPanel.react.js</a></li>
<li><a class="reference internal" href="#statpanel-react-js" id="toc-entry-17">StatPanel.react.js</a></li>
<li><a class="reference internal" href="#bookform-react-js" id="toc-entry-18">BookForm.react.js</a></li>
<li><a class="reference internal" href="#bookpanel-react-js" id="toc-entry-19">BookPanel.react.js</a></li>
</ul>
</li>
<li><a class="reference internal" href="#the-actions" id="toc-entry-20">The Actions</a></li>
<li><a class="reference internal" href="#the-stores" id="toc-entry-21">The Stores</a><ul>
<li><a class="reference internal" href="#messagestore-js-1" id="toc-entry-22">MessageStore.js</a></li>
<li><a class="reference internal" href="#authorstore-js" id="toc-entry-23">AuthorStore.js</a></li>
<li><a class="reference internal" href="#categorystore-js" id="toc-entry-24">CategoryStore.js</a></li>
<li><a class="reference internal" href="#bookstore-js" id="toc-entry-25">BookStore.js</a></li>
</ul>
</li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-26">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>In <a class="reference external" href="https://spapas.github.io/2015/06/05/comprehensive-react-flux-tutorial/">previous</a>
<a class="reference external" href="https://spapas.github.io/2015/07/02/comprehensive-react-flux-tutorial-2/">two</a> parts of this series, we have seen
how we could implement a
not-so-simple single page application with full <span class="caps">CRUD</span> capabilities
using react only (in the <a class="reference external" href="https://spapas.github.io/2015/06/05/comprehensive-react-flux-tutorial/">first part</a> ) and then how to
change it to use the flux architecture (in the <a class="reference external" href="https://spapas.github.io/2015/07/02/comprehensive-react-flux-tutorial-2/">second part</a>).</p>
<p>In this, third, part, we will
add some more capabilities to the previous app
to prove how easy it is to create complex apps
and continue the discussion on
the Flux architecture. The source code of this project
can
be found at <a class="reference external" href="https://github.com/spapas/react-tutorial">https://github.com/spapas/react-tutorial</a> (tag name react-flux-complex).</p>
<p>Here’s a demo of the final application:</p>
<img alt="Our project" src="/images/demo2.gif" style="width: 780px;" />
<p>We can see that, when compared to the previous version this one has:</p>
<ul class="simple">
<li>Sorting by table columns</li>
<li>Pagination</li>
<li>Message for loading</li>
<li>Client side url updating (with hashes)</li>
<li>Cascading drop downs (selecting category will filter out subcategories)</li>
<li>Integration with jquery-ui datepicker</li>
<li>Add a child object (author)</li>
<li>Add authors with a modal dialog (popup)</li>
<li>Delete authors (using the “-” button)</li>
<li>Colored ajax result messages (green when all is ok, red with error)</li>
<li>A statistics panel (number of books / authors)</li>
</ul>
<p>In the paragraphs below, beyond the whole architecture, we’ll see
a bunch of techniques such as:</p>
<ul class="simple">
<li>Creating reusable flux components</li>
<li>Integrating react with jquery</li>
<li>Creating cascading dropdowns</li>
<li>Updating URLs with hashes</li>
<li>Adding pagination/sorting/querying to a table of results</li>
<li>Displaying pop ups</li>
</ul>
</div>
<div class="section" id="more-about-flux-components">
<h2><a class="toc-backref" href="#toc-entry-2">More about Flux components</a></h2>
<p>Before delving into the source code of the above application, I’d like to
make crystal what is the proposed way to call methods in the flux chain
and which types of components can include other types of components. Let’s
take a look at the following diagram:</p>
<img alt="flux dependencies" src="/images/react_flux_deps1.png" style="width: 480px;" />
<p>The arrows display the dependencies of the Flux architecture - an arrow
from X to Y means that a component of type Y could depend on an Component
of type X (of example, a store could depend on an action or another store
but not on a panel).</p>
<p>In more detail, we can see that in the top of the hierarchy,
having no dependencies are the
Dispatcher and the Constants. The Dispatcher is just a single component
(which actually is a singleton - only one dispatcher exists in each
single page react-flux application)
that inherits from the Facebook’s dispatcher and is imported by the
action components (since action methods defined in action components
call the <tt class="docutils literal">dispatch</tt> method of the dispatcher passing the correct
parameters) and the stores which do the real actions when an action
is dispatched, depending on the action type. The Constant components
define constant values for the action types and are used by both
the actions (to set the action types to the dispatch calls) and by
the stores to be used on the switch statement when an action is
dispatched. As an example, for BookActions we have the following</p>
<pre class="code literal-block">
var BookConstants = require('../constants/BookConstants')
var AppDispatcher = require('../dispatcher/AppDispatcher').AppDispatcher;
var BookActions = {
change_book: function(book) {
AppDispatcher.dispatch({
actionType: BookConstants.BOOK_CHANGE,
book: book
});
},
// Other book actions [...]
</pre>
<p>and for BookStore</p>
<pre class="code literal-block">
var $ = require('jquery');
var AppDispatcher = require('../dispatcher/AppDispatcher').AppDispatcher;
var BookConstants = require('../constants/BookConstants')
// [...]
BookStore.dispatchToken = AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.BOOK_EDIT:
_editBook(action.book);
break;
// [...] other switch branches
</pre>
<p>An interesting thing to see is that the actions depend only on the Dispatcher
and on the Constants, while the stores also depend on actions (not only
from the same action types as the store type, for example AuthorStore depends
on both AuthorActions and MessageActions) and on <em>other</em> stores. This means
that the actions should be a “clean” component: Just declare your action methods
whose only purpose is to pass the action type along with the required parameters
to the dispatcher.</p>
<p>On the other hand, the stores are depending on both actions and other stores.
The action dependency is because sometimes when something is done in a store
we need to notify another store to do something as a response to that. For
example, in our case, when a book is updated we need to notify the message
store to show the “Book updated ok” message. This preferrably should not be done by
directly calling the corresponding method on the message store but instead
by calling the corresponding action of MessageAction and passing the correct
parameters (actually, the method that updates the message in MessageStore
should be a private method that is called <em>only</em> through the action).</p>
<p>One thing to notice here is that you <em>cannot</em> call (and dispatch) an action in the
same call stack (meaning it is directly called) as of another dispatch or you’ll get a
<tt class="docutils literal">Invariant Violation: <span class="pre">Dispatch.dispatch(...):</span> Cannot dispatch in the middle of a dispatch.</tt>
error in your javascript console. Let’s see an example of what this means and how
to avoid it because its a common error. Let’s say that we have the following
code to a store:</p>
<pre class="code literal-block">
AppDispatcher.register(function(action) {
switch(action.actionType) {
case Constants.TEST_ERR:
TestActions.action1();
break;
case Constants.TEST_OK1:
setTimeout(function() {
TestActions.action1();
}, 0);
break;
case Constants.TEST_OK2:
$.get('/get/', function() {
TestActions.action1();
});
break;
}
return true;
});
</pre>
<p>Here, the <tt class="docutils literal">TEST_ERR</tt> branch will throw an error because the <tt class="docutils literal">TestAction.action1</tt> is <em>in the same call stack</em>
as this dispatch! On the other hand, both <tt class="docutils literal">TEST_OK1</tt> and <tt class="docutils literal">TEST_OK2</tt> will work: <tt class="docutils literal">TEST_OK2</tt> is the most
usual, since most of the times we want to call an action as a result of an ajax call - however, sometimes we
want to call an action without any ajax — in this case we use the setTimeout (with a timeout of 0 ms) in
order to move the call to <tt class="docutils literal">TestActions.action1()</tt> to a different call stack.</p>
<p>Now, as I mentioned before, there’s also a <em>different-store</em> dependency on stores.
This dependency is needed because some stores may offer public methods for other stores to use
(methods that don’t actually need to be dispatched through the dispatcher) and
for the <tt class="docutils literal">waitFor</tt> method , in case there are two different
stores that respond to a single action and want to have one store executing
its dispatch method before the other.
(the waitFor method takes the dispatchToken, which is the result of <tt class="docutils literal">Dispatcher.register</tt> of
a different store as a parameter in order to wait for that action to finish).</p>
<p>Finally, the Panels depend on both actions (to initiate an action as a response
to user input) and stores (to read the state of the each required store when it
is changed). Of course, not all panels actually depend on stores, since as we
already know the state of a React app should be kept as high in the hierarchy
as possible, so a central component will get the state of a store and pass the
required state attributes to its children through parameters. On the other
hand, every component that responds to user input will have to import
am action object and call the corresponding method — we should never pass
callbacks from a parent to a child component as a parameter anymore <em>unless</em>
we want to make the component reusable (more on this later).</p>
<p>Finally, as expected, because of their hierarchy the compoents depend on their
child components (a BookTable depends on a BookTableRow etc).</p>
</div>
<div class="section" id="explaining-the-application-components">
<h2><a class="toc-backref" href="#toc-entry-3">Explaining the application components</a></h2>
<p>There are a lot of components that are needed for
the above application. A bird’s eye view of the components
and their hierarchies can be seen on the following figure:</p>
<img alt="Our components" src="/images/complex_react_components.png" style="width: 780px;" />
<p>We will first explain a bit the components that could be easily reused
by other applications and after that the components that are specifically
implemented for our book/author single page application.</p>
</div>
<div class="section" id="reusable-application-components">
<h2><a class="toc-backref" href="#toc-entry-4">Reusable application components</a></h2>
<p>We can see that beyond the specific components (<tt class="docutils literal">BookPanel</tt>, <tt class="docutils literal">BookTable</tt> etc)
there’s a bunch of more general components (<tt class="docutils literal">DropDown</tt>, <tt class="docutils literal">DatePicker</tt>, <tt class="docutils literal">PagingPanel</tt>, <tt class="docutils literal">SearchPanel</tt>,
<tt class="docutils literal">MessagePanel</tt>) that have names like they could be used by other applications (for example every application
that wants to implement a DropDown could use our component) and not only by the
Book-Author application. Let’s take a quick look at these components and if they
are actually reusable:</p>
<div class="section" id="dropdown-react-js">
<h3><a class="toc-backref" href="#toc-entry-5">DropDown.react.js</a></h3>
<pre class="code literal-block">
var React = require('react');
var DropDown = React.createClass({
render: function() {
var options = [];
options.push(<option key='-1' value='' >---</option>);
if(this.props.options) {
this.props.options.forEach(function(option) {
options.push(<option key={option.id} value={option.id}>{option.name}</option>);
});
}
return(
<select ref='dropdown' value={this.props.value?this.props.value:''} onChange={this.onFormChange} >
{options}
</select>
);
},
onFormChange: function() {
var val = React.findDOMNode(this.refs.dropdown).value
this.props.dropDownValueChanged(val);
}
});
module.exports.DropDown = DropDown;
</pre>
<p>What we see here is that this component does not actually have a local state and will need three parameters:</p>
<ul class="simple">
<li>the list of options (<tt class="docutils literal">props.options</tt>)</li>
<li>the curently selected option (<tt class="docutils literal">props.value</tt>)</li>
<li>a callback to be called when the dropdown is changed (<tt class="docutils literal">props.dropDownValueChanged</tt>)</li>
</ul>
<p>By passing the above three parameters this dropdown component can be reused whenever a dropdown is needed!
As we can see here we need to pass a callback function to this dropdown and call the corresponding action
directly. This is important to have a reusable component: The component that includes each dropdown decides
which should be the action on the dropdown value change. Another option if we wanted to avoid callbacks
would be to pass an identifier as a property for each dropdown and the dropdown would call a generic
dropdown action and passing this identifier — however I think that actually passing the callback is
easier and makes the code much more readable.</p>
</div>
<div class="section" id="datepicker-react-js">
<h3><a class="toc-backref" href="#toc-entry-6">DatePicker.react.js</a></h3>
<p>The datepicker component has more or less the same structure as with the dropdown: It needs one parameter
for its current value (<tt class="docutils literal">props.value</tt>) and another as the callback to the function when the date is
changed (once again this is required to have a reusable component):</p>
<pre class="code literal-block">
var React = require('react');
var DatePicker = React.createClass({
render: function() {
return(
<input type='text' ref='date' value={this.props.value} onChange={this.handleChange} />
);
},
componentDidMount: function() {
$(React.findDOMNode(this)).datepicker({ dateFormat: 'yy-mm-dd' });
$(React.findDOMNode(this)).on('change', this.handleChange);
},
componentWillUnmount: function() {
},
handleChange: function() {
var date = React.findDOMNode(this.refs.date).value
this.props.onChange(date);
}
});
module.exports.DatePicker = DatePicker ;
</pre>
<p>Another thing we can see here is how can integrate jquery components with react: When
the component is mounted, we get its <span class="caps">DOM</span> component using <tt class="docutils literal">React.findDomNode(this)</tt> and
convert it to a datepicker. We also set its change function to be the passed callback.</p>
</div>
<div class="section" id="pagingpanel-react-js">
<h3><a class="toc-backref" href="#toc-entry-7">PagingPanel.react.js</a></h3>
<p>The paging panel is not actually a reusable component because it requires BookActions
(to handle next and previous page clicks) - so
it can’t be used by Authors (if authors was a table that is). However, we can easily
change it to be reusable if we passed callbacks for next and previous page so that
the component including PagingPanel would call the correct action on each click. Having
a reusable PagingPangel is not needed for our application since only books have a table.</p>
<pre class="code literal-block">
var React = require('react');
var BookActions = require('../actions/BookActions').BookActions;
var PagingPanel = React.createClass({
render: function() {
return(
<div className="row">
{this.props.page==1?'':<button onClick={this.onPreviousPageClick}>&lt;</button>}
&nbsp; Page {this.props.page} of {this.getTotalPages()} &nbsp;
{this.props.page==this.getTotalPages()?'':<button onClick={this.onNextPageClick} >&gt;</button>}
</div>
);
},
onNextPageClick: function(e) {
e.preventDefault();
BookActions.change_page(this.props.page+1)
},
onPreviousPageClick: function(e) {
e.preventDefault();
BookActions.change_page(this.props.page-1)
},
getTotalPages: function() {
return Math.ceil(this.props.total / this.props.page_size);
}
})
module.exports.PagingPanel = PagingPanel;
</pre>
<p>The component is very simple, it needs three parameters:</p>
<ul class="simple">
<li>the current page (<tt class="docutils literal">props.page</tt>)</li>
<li>the page size (<tt class="docutils literal">props.page_size</tt>)</li>
<li>the total pages number (<tt class="docutils literal">props.total</tt>)</li>
</ul>
<p>and just displayd the current page along with buttons to go to the next or previous page (if these buttons should be visible of course).</p>
</div>
<div class="section" id="searchpanel-react-js">
<h3><a class="toc-backref" href="#toc-entry-8">SearchPanel.react.js</a></h3>
<p>The SearchPanel is another panel that could be reusable if we’d passed a callbeck instead of calling the <tt class="docutils literal">BookActions.search</tt>
action directly. The promise behavior has been explained in the previous posts and is needed to buffer the queries to the
server when a user types his search query.</p>
<pre class="code literal-block">
var React = require('react');
var BookActions = require('../actions/BookActions').BookActions;
var SearchPanel = React.createClass({
getInitialState: function() {
return {
search: this.props.query,
}
},
componentWillReceiveProps: function(nextProps) {
this.setState({
search: nextProps.query
});
},
render: function() {
return (
<div className="row">
<div className="one-fourth column">
Filter: &nbsp;
<input ref='search' name='search' type='text' value={this.state.search} onChange={this.onSearchChange} />
{this.state.search?<button onClick={this.onClearSearch} >x</button>:''}
</div>
</div>
)
},
onSearchChange: function() {
var query = React.findDOMNode(this.refs.search).value;
if (this.promise) {
clearInterval(this.promise)
}
this.setState({
search: query
});
this.promise = setTimeout(function () {
BookActions.search(query);
}.bind(this), 400);
},
onClearSearch: function() {
this.setState({
search: ''
});
BookActions.search('');
}
});
module.exports.SearchPanel = SearchPanel;
</pre>
<p>As we can see, this panel is a little different than the previous ones because it actually handles its
own local state: When the component should get properties from its parent compoent, its state will
be updated to the <tt class="docutils literal">query</tt> attribute - so the current value of the search query gets updated through
properties. However, when the value of the search input is changed, we see that the local state is
changed immediately but the <tt class="docutils literal">BookActions.search</tt> (or the corresponding callback) gets called <em>only</em>
when the timeout has passed!</p>
<p>The above means that we can type whatever we want on the search input, but it at first it will be used
only locally to immediately update the value of the input and, only after the timeout has fired the
search action will be called. If we hadn’t used the local state it would be much more difficult to have
this consistent behavior (we’d need to add two actions, one to handle the search query value change
and another to handle the timeout firing — making everythimg much more complicated).</p>
</div>
<div class="section" id="messagepanel">
<h3><a class="toc-backref" href="#toc-entry-9">MessagePanel</a></h3>
<p>The MessagePanel is really interesting because it is a reusable component that actually has its own action and store module! This
component can be reused in <em>different</em> applications that need to display message but <em>not</em> on the same application (because a single state
is kept for all messages). If we wanted to use a different MessagePanel for Books or Authors then we’d need to keep both in the
state <em>and</em> also it to the action to differentiate between messages for author and for book. Instead, by keeping a single Messages
state for both Books and Authors we have a much more simple version.</p>
<div class="section" id="messagepanel-react-js">
<h4><a class="toc-backref" href="#toc-entry-10">MessagePanel.react.js</a></h4>
<p>The MessagePanel component has a local state which responds to changes on MessageStore. When the
state of MessageStore is changed the MessagePanel will be re-rendered with the new message.</p>
<pre class="code literal-block">
var React = require('react');
var MessageStore = require('../stores/MessageStore').MessageStore;
var MessagePanel = React.createClass({
getInitialState: function() {
return {
};
},
render: function() {
return(
<div className="row">
{this.state.message?<div className={this.state.message.color}>{this.state.message.text}</div>:""}
</div>
);
},
_onChange: function() {
this.setState(MessageStore.getState());
},
componentWillUnmount: function() {
MessageStore.removeChangeListener(this._onChange);
},
componentDidMount: function() {
MessageStore.addChangeListener(this._onChange);
}
})
module.exports.MessagePanel = MessagePanel;
</pre>
</div>
<div class="section" id="messagestore-js">
<h4><a class="toc-backref" href="#toc-entry-11">MessageStore.js</a></h4>
<p>The MessageStore has a (private) state containing a <tt class="docutils literal">message</tt> that gets updated
only when the ccorresponding action is dispached. The store has a single state
for all messages - it doesn’t care if the messages are for books or authors.</p>
<pre class="code literal-block">
var $ = require('jquery');
var EventEmitter = require('events').EventEmitter;
var AppDispatcher = require('../dispatcher/AppDispatcher').AppDispatcher;
var BookConstants = require('../constants/BookConstants')
var _state = {
message: {}
};
var MessageStore = $.extend({}, EventEmitter.prototype, {
getState: function() {
return _state;
},
emitChange: function() {
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
},
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
MessageStore.dispatchToken = AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.MESSAGE_ADD:
_state.message = action.message;
MessageStore.emitChange();
break;
}
return true;
});
module.exports.MessageStore = MessageStore;
</pre>
</div>
<div class="section" id="messageactions">
<h4><a class="toc-backref" href="#toc-entry-12">MessageActions</a></h4>
<p>Finally, there are two actions that are defined for the MessageStore: One for adding
an ok message and one for adding an error message - both of which have the same
message type (but pass a different color parameter).</p>
<pre class="code literal-block">
var AppDispatcher = require('../dispatcher/AppDispatcher').AppDispatcher;
var BookConstants = require('../constants/BookConstants')
var MessageActions = {
add_message_ok: function(msg) {
AppDispatcher.dispatch({
actionType: BookConstants.MESSAGE_ADD,
message: {
color: 'green',
text: msg
}
});
},
add_message_error: function(msg) {
AppDispatcher.dispatch({
actionType: BookConstants.MESSAGE_ADD,
message: {
color: 'green',
text: msg
}
});
}
};
module.exports.MessageActions = MessageActions;
</pre>
</div>
</div>
</div>
<div class="section" id="non-reusable-application-components">
<h2><a class="toc-backref" href="#toc-entry-13">Non-reusable application components</a></h2>
<p>I don’t want to discuss the source code for all the non-reusable components since some of them
are more or less the same with the previous version and are easy to understand just by
checking the source code (BookTableRow and ButtonPanel). However, I’ll
discuss the other, more complex components starting from the inside of the react-onion:</p>
<div class="section" id="booktable-react-js">
<h3><a class="toc-backref" href="#toc-entry-14">BookTable.react.js</a></h3>
<p>I want to display this component to discuss how sorting is implemented: Each column has a key which,
when passed to django-rest-framework will sort the results based on that key (the <tt class="docutils literal">__</tt> does a
join so by <tt class="docutils literal">author__last_name</tt> we mean that we want to sort by the last_name field of the author
of each book. Also, you can pass the key as it is to sort ascending or with a minus (-) in front
(for example <tt class="docutils literal"><span class="pre">-author__last_name</span></tt>).</p>
<pre class="code literal-block">
var React = require('react');
var BookTableRow = require('./BookTableRow.react').BookTableRow;
var BookActions = require('../actions/BookActions').BookActions;
var BookTable = React.createClass({
render: function() {
var rows = [];
this.props.books.forEach(function(book) {
rows.push(<BookTableRow key={book.id} book={book} />);
});
return (
<table>
<thead>
<tr>
<th><a href='#' onClick={this.onClick.bind(this, 'id')}>{this.showOrdering('id')} Id</a></th>
<th><a href='#' onClick={this.onClick.bind(this, 'title')}>{this.showOrdering('title')} Title</a></th>
<th><a href='#' onClick={this.onClick.bind(this, 'subcategory__name')}>{this.showOrdering('subcategory__name')} Category</a></th>
<th><a href='#' onClick={this.onClick.bind(this, 'publish_date')}>{this.showOrdering('publish_date')} Publish date</a></th>
<th><a href='#' onClick={this.onClick.bind(this, 'author__last_name')}>{this.showOrdering('author__last_name')} Author</a></th>
<th>Edit</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
},
onClick: function(v, e) {
e.preventDefault();
BookActions.sort_books(v);
},
showOrdering: function(v) {
if (v==this.props.ordering) {
return '+'
} else if ('-'+v==this.props.ordering) {
return '-'
}
}
});
module.exports.BookTable = BookTable ;
</pre>
<p>The only thing that needs explaining in this module is the line of the form</p>
<pre class="code literal-block">
<th><a href='#' onClick={this.onClick.bind(this, 'id')}>{this.showOrdering('id')} Id</a></th>
</pre>
<p>that creates the title of each column and triggers ascending or descending sorting on this column by
clicking on it. So, we can see that we’ve create an onClick function that actually expects a value - the key
to that column. To allow passing that value, we use the bind method of the function object which will
create new a function that has this key as its first parameter. If we didn’t want to use bind, we’d need to
creatre 5 different function (onIdClick, onTitleClick etc)! The most common usage of <tt class="docutils literal">bind</tt> is to actually <em>bind</em>
a function to an object (that’s what the first parameter to this function does)
so that calling this inside that function will refer to that object - here we leave the binding
of the function to the same object and only do the parameter passing.</p>
<p>Also, the showOrdering checks if the current ordering is the same as that column’s key and displays
either a + (for ascending) or - (for descending) in front of the column title.</p>
</div>
<div class="section" id="authordialog-react-js">
<h3><a class="toc-backref" href="#toc-entry-15">AuthorDialog.react.js</a></h3>
<p>This is a handmade pop-up dialog that gets displayed when a new author is added (the + button is clicked)
using only css to center it on the screen when it is displayed.
We can see that it is either visible on invisible based on the <tt class="docutils literal">showDialog</tt> input property which actually
is the only input this component requires. When it is visible and the ok or cancel button are pressed
the corresponding action will be dispatched (which will actually close this popup by setting the <tt class="docutils literal">showDialog</tt>
to false):</p>
<pre class="code literal-block">
var React = require('react');
var AuthorActions = require('../actions/AuthorActions').AuthorActions;
var AuthorDialog = React.createClass({
render: function() {
if (!this.props.showDialog) {
return (
<div />
)
} else {
return(
<div className='modal-dialog' id="dialog-form" >
<label htmlFor="first_name">First name:</label> <input type='text' ref='first_name' name='first_name' /> <br />
<label htmlFor="last_name">Last name:</label> <input type='text' ref='last_name' name='last_name' /> <br />
<button onClick={this.onOk}>Ok</button>
<button onClick={this.onCancel} >Cancel</button>
</div>
);
}
},
onCancel: function(e) {
e.preventDefault();
AuthorActions.hide_add_author();
},
onOk: function(e) {
e.preventDefault();
first_name = React.findDOMNode(this.refs.first_name).value;
last_name = React.findDOMNode(this.refs.last_name).value;
AuthorActions.add_author_ok({
first_name: first_name,
last_name: last_name
});
}
});
module.exports.AuthorDialog = AuthorDialog ;
</pre>
</div>
<div class="section" id="authorpanel-react-js">
<h3><a class="toc-backref" href="#toc-entry-16">AuthorPanel.react.js</a></h3>
<p>The AuthorPanel displays the author select DropDown along with the + (add author) and
- (delete author) buttons. It also contains the AuthorDialog which will be displayed
or not depending on the value of the <tt class="docutils literal">showDialog</tt> property.</p>
<pre class="code literal-block">
var React = require('react');
var DropDown = require('./DropDown.react').DropDown;
var AuthorDialog = require('./AuthorDialog.react').AuthorDialog;
var AuthorActions = require('../actions/AuthorActions').AuthorActions;
var AuthorPanel = React.createClass({
getInitialState: function() {
return {};
},
render: function() {
var authorExists = false ;
if(this.props.authors) {
var ids = this.props.authors.map(function(x) {
return x.id*1;
});
if(ids.indexOf(1*this.props.author)>=0 ) {
authorExists = true;
}
}
return(
<div className='one-half column'>
<AuthorDialog showDialog={this.props.showDialog} />
<label forHtml='date'>Author</label>
<DropDown options={this.props.authors} dropDownValueChanged={this.props.onAuthorChanged} value={authorExists?this.props.author:''} />
<button onClick={this.addAuthor} >+</button>
{authorExists?<button onClick={this.deleteAuthor}>-</button>:""}
</div>
);
},
addAuthor: function(e) {
e.preventDefault();
console.log("ADD AUTHOR");
AuthorActions.show_add_author();
},
deleteAuthor: function(e) {
e.preventDefault();
AuthorActions.delete_author(this.props.author);
console.log("DELETE AUTHOR");
console.log(this.props.author);
},
});
module.exports.AuthorPanel = AuthorPanel;
</pre>
<p>As we can see, there are three properties that are passed to this component:</p>
<ul class="simple">
<li><tt class="docutils literal">props.author</tt>: The currently selected author</li>
<li><tt class="docutils literal">props.authors</tt>: The list of all authors</li>
<li><tt class="docutils literal">props.onAuthorChanged</tt>: A callback that is called when the author is changed. Here, we could have used an action (just like for add/delete author) instead of a callback, however its not actually required. When the author is changed, it means that the currently edited book’s author is changed. So we could propagate the change to the parent (form) component that handles the book change along with the other changes (i.e title, publish date etc).</li>
</ul>
</div>
<div class="section" id="statpanel-react-js">
<h3><a class="toc-backref" href="#toc-entry-17">StatPanel.react.js</a></h3>
<p>The StatPanel is an interesting, read-only component that displays the number of
authors and books. This component requests updates from both the <tt class="docutils literal">BookStore</tt> and
<tt class="docutils literal">AuthorStore</tt> - when their state is updated the component will be re-rendered
with the number of books and authors:</p>
<pre class="code literal-block">
var React = require('react');
var BookStore = require('../stores/BookStore').BookStore;
var AuthorStore = require('../stores/AuthorStore').AuthorStore;
var StatPanel = React.createClass({
getInitialState: function() {
return {};
},
render: function() {
var book_len = '-';
var author_len = '-';
if(this.state.books) {
book_len = this.state.books.length
}
if(this.state.authors) {
author_len = this.state.authors.length
}
return(
<div className="row">
<div className="one-half column">
Books number: {book_len}
</div>
<div className="one-half column">
Authors number: {author_len}
</div>
<br />
</div>
);
},
_onBookChange: function() {
this.setState({
books:BookStore.getState().books
});
},
_onAuthorChange: function() {
this.setState({
authors: AuthorStore.getState().authors
});
},
componentWillUnmount: function() {
AuthorStore.removeChangeListener(this._onAuthorChange);
BookStore.removeChangeListener(this._onBookChange);
},
componentDidMount: function() {
AuthorStore.addChangeListener(this._onAuthorChange);
BookStore.addChangeListener(this._onBookChange);
}
});
module.exports.StatPanel = StatPanel ;
</pre>
<p>We’ve added different change listeners in case we wanted to do
some more computations for book or author change (instead of just
getting their books / authors property). Of course the same behavior
could be achieved with just a single change listener that would
get both the books and authors.</p>
</div>
<div class="section" id="bookform-react-js">
<h3><a class="toc-backref" href="#toc-entry-18">BookForm.react.js</a></h3>
<p>The BookForm is one of the most complex panels of this application (along with BookPanel) because
it actually contains a bunch of other panels and has some callbacks for them to use.</p>
<p>We can see that, as explained before, when the current book form values are changed (through callbacks) the change_book action will be called.</p>
<pre class="code literal-block">
var React = require('react');
var BookActions = require('../actions/BookActions').BookActions;
var DropDown = require('./DropDown.react.js').DropDown;
var StatPanel = require('./StatPanel.react.js').StatPanel;
var MessagePanel = require('./MessagePanel.react.js').MessagePanel;
var DatePicker = require('./DatePicker.react.js').DatePicker;
var ButtonPanel = require('./ButtonPanel.react.js').ButtonPanel;
var AuthorPanel = require('./AuthorPanel.react.js').AuthorPanel;
var CategoryStore = require('../stores/CategoryStore').CategoryStore;
var AuthorStore = require('../stores/AuthorStore').AuthorStore;
var loadCategories = require('../stores/CategoryStore').loadCategories;
var loadAuthors = require('../stores/AuthorStore').loadAuthors;
var BookForm = React.createClass({
getInitialState: function() {
return {};
},
render: function() {
return(
<form onSubmit={this.onSubmit}>
<div className='row'>
<div className='one-half column'>
<label forHtml='title'>Title</label>
<input ref='title' name='title' type='text' value={this.props.book.title} onChange={this.onTitleChange} />
</div>
<div className='one-half column'>
<label forHtml='date'>Publish date</label>
<DatePicker ref='date' onChange={this.onDateChange} value={this.props.book.publish_date} />
</div>
</div>
<div className='row'>
<div className='one-half column'>
<label forHtml='category'>Category</label>
<DropDown options={this.state.categories} dropDownValueChanged={this.onCategoryChanged} value={this.props.book.category} />
<DropDown options={this.state.subcategories} dropDownValueChanged={this.onSubCategoryChanged} value={this.props.book.subcategory} />
</div>
<AuthorPanel authors={this.state.authors} author={this.props.book.author} onAuthorChanged={this.onAuthorChanged} showDialog={this.state.showDialog} />
</div>
<ButtonPanel book={this.props.book} />
<MessagePanel />
<StatPanel />
</form>
);
},
onSubmit: function(e) {
e.preventDefault();
BookActions.save(this.props.book)
},
onTitleChange: function() {
this.props.book.title = React.findDOMNode(this.refs.title).value;
BookActions.change_book(this.props.book);
},
onDateChange: function(date) {
this.props.book.publish_date = date;
BookActions.change_book(this.props.book);
},
onCategoryChanged: function(cat) {
this.props.book.category = cat;
this.props.book.subcategory = '';
BookActions.change_book(this.props.book);
},
onSubCategoryChanged: function(cat) {
this.props.book.subcategory = cat;
BookActions.change_book(this.props.book);
},
onAuthorChanged: function(author) {
this.props.book.author = author;
BookActions.change_book(this.props.book);
},
_onChangeCategories: function() {
this.setState(CategoryStore.getState());
},
_onChangeAuthors: function() {
this.setState(AuthorStore.getState());
},
componentWillUnmount: function() {
CategoryStore.removeChangeListener(this._onChangeCategories);
AuthorStore.removeChangeListener(this._onChangeAuthors);
},
componentDidMount: function() {
CategoryStore.addChangeListener(this._onChangeCategories);
AuthorStore.addChangeListener(this._onChangeAuthors);
loadCategories();
loadAuthors();
}
});
module.exports.BookForm = BookForm;
</pre>
<p>The above component listens for updates on both Category and Author store to update
when the authors (when an author is added or deleted) and the categories are changed (for example to implement the cascading dropdown
functionality), so the list of authors and the list of categories and subcategoreis are all stored in
the local state. The book that is edited is just passed as a property - actually, this is the only
property that this component needs to work.</p>
</div>
<div class="section" id="bookpanel-react-js">
<h3><a class="toc-backref" href="#toc-entry-19">BookPanel.react.js</a></h3>
<p>Finally, BookPanel is the last component we’ll talk about. This is the central component
of the application - however we’ll see that it is not very complex (since most user interaction
is performed in other components). This component just listens on changes in the BookStore state
and depending on the parameters either displays the “Loading” message or the table of books
(depending on the state of ajax calls that load the books). The other parameters like the
list of books, the ordering of the books etc are passed to the child components.</p>
<pre class="code literal-block">
var React = require('react');
var BookStore = require('../stores/BookStore').BookStore;
var BookActions = require('../actions/BookActions').BookActions;
var SearchPanel = require('./SearchPanel.react').SearchPanel;
var BookTable = require('./BookTable.react').BookTable;
var PagingPanel = require('./PagingPanel.react').PagingPanel;
var BookForm = require('./BookForm.react').BookForm;
var reloadBooks = require('../stores/BookStore').reloadBooks;
var BookPanel = React.createClass({
getInitialState: function() {
return BookStore.getState();
},
render: function() {
return(
<div className="row">
<div className="one-half column">
{
this.state.loading?
<div class='loading' >Loading...</div>:
<div>
<SearchPanel query={this.state.query} ></SearchPanel>
<BookTable books={this.state.books} ordering={this.state.ordering} />
<PagingPanel page_size='5' total={this.state.total} page={this.state.page} />
</div>
}
</div>
<div className="one-half column">
<BookForm
book={this.state.editingBook}
/>
</div>
<br />
</div>
);
},
_onChange: function() {
this.setState( BookStore.getState() );
},
componentWillUnmount: function() {
BookStore.removeChangeListener(this._onChange);
},
componentDidMount: function() {
BookStore.addChangeListener(this._onChange);
reloadBooks();
}
});
module.exports.BookPanel = BookPanel ;
</pre>
</div>
</div>
<div class="section" id="the-actions">
<h2><a class="toc-backref" href="#toc-entry-20">The Actions</a></h2>
<p>All actions are rather simple components without other dependencies as we’ve already discussed. They
just define “actions” (which are simple functions) that create the correct parameter object type and pass it
to the dispatcher. The only attribute that is required for this object is the <tt class="docutils literal">actionType</tt> that
should get a value from the constants. I won’t go into any more detail about the actions — please check
the source code and all your questions will be resolved.</p>
</div>
<div class="section" id="the-stores">
<h2><a class="toc-backref" href="#toc-entry-21">The Stores</a></h2>
<p>First of all, all stores are defined through the following code that is already discussed in the previous part:</p>
<pre class="code literal-block">
var MessageStore = $.extend({}, EventEmitter.prototype, {
getState: function() {
return _state;
},
emitChange: function() {
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
},
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
</pre>
<p>When the state of a store is changed its <tt class="docutils literal">emitChange</tt> function will be called (I mean
called manually from the code that actually changes the state and knows that it has
actually been changed - nothing will be called automatically). When <tt class="docutils literal">emitChange</tt> is called,
all the components that listen for changes for this component (that have called <tt class="docutils literal">addChangeListener</tt>
of the store with a callback)
will be notified (their callback will be called) and will use
<tt class="docutils literal">getState</tt> of the store to get its current state - after that, these components will set their own state
to re-render and display the changes to the store.</p>
<p>Let’s now discuss the four stores defined — I will include only the parts of each file that are
actually interesting, for everything else <em>use the source Luke</em>!</p>
<div class="section" id="messagestore-js-1">
<h3><a class="toc-backref" href="#toc-entry-22">MessageStore.js</a></h3>
<p>A very simple store that goes together with MessagePanel and MessageActions. It just keeps a state with
the current message object and just changes this message when the MESSAGE_ADD message
type is dispatched. After changing the message, the listeners (only one in this case) will be notified to
update the displayed message:</p>
<pre class="code literal-block">
var _state = {
message: {}
};
MessageStore.dispatchToken = AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.MESSAGE_ADD:
_state.message = action.message;
MessageStore.emitChange();
break;
}
return true;
});
</pre>
</div>
<div class="section" id="authorstore-js">
<h3><a class="toc-backref" href="#toc-entry-23">AuthorStore.js</a></h3>
<p>Here we see that the local state has an array of the authors and a <tt class="docutils literal">showDialog</tt> flag that
controls the state of the add author popup. For the <tt class="docutils literal">AUTHOR_ADD</tt> and <tt class="docutils literal">HIDE_ADD_AUTHOR</tt>
cases of the dispatch we just change the state of this flag and emit the change; the <tt class="docutils literal">BookForm</tt>
listens for changes to the <tt class="docutils literal">AuthorStore</tt> and will pass the <tt class="docutils literal">showDialog</tt> to the <tt class="docutils literal">AuthorPanel</tt>
as a property which in turn will pass it to <tt class="docutils literal">AuthorDialog</tt> and it will display the panel (or not)
depending on the value of that flag. This flag will also take a false value when the add author
ajax call returns.</p>
<p>The <tt class="docutils literal">showDialog</tt> flag is not related to the actual data but is <span class="caps">UI</span>-related. This
is something that we should keep in mind when creating stores: Stores don’t only contain the actual
data (like models in an <span class="caps">MVC</span> application) but they should also contain <span class="caps">UI</span> (controller/view in an <span class="caps">MVC</span>
architecture) related information since that is <em>also</em> part of the state!</p>
<p>We can see that the ajax calls just issue the corresponding <span class="caps">HTTP</span> method to the <tt class="docutils literal">authors_url</tt> and
when they return the <tt class="docutils literal">add_message_ok</tt> or <tt class="docutils literal">add_message_error</tt> methods of
<tt class="docutils literal">MessageActions</tt> will be called. These calls are in a different call stack so everything will work fine
(please remember the discussion about dispatches in different call stacks before).</p>
<p>Finally, on the success of <tt class="docutils literal">_load_authors</tt> the map array method is called to transform the returned data
as we want it:</p>
<pre class="code literal-block">
var $ = require('jquery');
var _state = {
authors: [],
showDialog: false
}
var _load_authors = function() {
$.ajax({
url: _props.authors_url,
dataType: 'json',
cache: false,
success: function(data) {
_state.authors = data.map(function(a){
return {
id: a.id,
name: a.last_name+' '+a.first_name
}
});
AuthorStore.emitChange();
},
error: function(xhr, status, err) {
MessageActions.add_message_error(err.toString());
}
});
};
var _deleteAuthor = function(authorId) {
$.ajax({
url: _props.authors_url+authorId,
method: 'DELETE',
cache: false,
success: function(data) {
_load_authors();
MessageActions.add_message_ok("Author delete ok");
AuthorActions.delete_author_ok();
},
error: function(xhr, status, err) {
MessageActions.add_message_error(err.toString());
}
});
};
var _addAuthor = function(author) {
$.ajax({
url: _props.authors_url,
dataType: 'json',
method: 'POST',
data:author,
cache: false,
success: function(data) {
MessageActions.add_message_ok("Author add ok");
_state.showDialog = false;
_load_authors();
},
error: function(xhr, status, err) {
MessageActions.add_message_error(err.toString());
}
});
};
AuthorStore.dispatchToken = AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.SHOW_ADD_AUTHOR:
_state.showDialog = true;
AuthorStore.emitChange();
break;
case BookConstants.HIDE_ADD_AUTHOR:
_state.showDialog = false;
AuthorStore.emitChange();
break;
case BookConstants.AUTHOR_ADD:
_addAuthor(action.author);
break;
case BookConstants.AUTHOR_DELETE:
_deleteAuthor(action.authorId);
break;
}
return true;
});
</pre>
</div>
<div class="section" id="categorystore-js">
<h3><a class="toc-backref" href="#toc-entry-24">CategoryStore.js</a></h3>
<p>The CategoryStore has an interesting functionality concerning the load_subcategory
function. This function is called whenever a book is changed (so its category form field
may be changed and the subcategories may be reloaded based on this category) or is edited
(so the category is that of the new book and once again the subcategories may need to because
rerendered). It is important that we <em>actually</em> pass the current category to the book to the
action. If for example we wanted to retrieve that from the state of the BookStore then we’d
need to use the <tt class="docutils literal">waitFor</tt> functionality of the dispatcher so that the category of the
current book would be changed first then the load_category (that would read that value to
read the subcategoreis) would be called after that.</p>
<p>Also, another thing to notice here is that there’s a simple subcat_cache that for each category
contains the subcategories of that category so that we won’t do repeated ajax calls to reload
the subcategories each time the category is changed.</p>
<pre class="code literal-block">
var _state = {
categories: [],
subcategories: []
}
var _current_cat = ''
var _subcat_cache = []
var _load_categories = function() {
$.ajax({
url: _props.categories_url,
dataType: 'json',
cache: false,
success: function(data) {
_state.categories = data;
CategoryStore.emitChange();
},
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}
});
};
var _load_subcategories = function(cat) {
if(!cat) {
_state.subcategories = [];
CategoryStore.emitChange();
return ;
}
if(_subcat_cache[cat]) {
_state.subcategories = _subcat_cache[cat] ;
CategoryStore.emitChange();
}
$.ajax({
url: _props.subcategories_url+'?category='+cat,
dataType: 'json',
cache: false,
success: function(data) {
_state.subcategories = data;
_subcat_cache[cat] = data;
CategoryStore.emitChange();
},
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}
});
};
CategoryStore.dispatchToken = AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.BOOK_EDIT:
case BookConstants.BOOK_CHANGE:
_load_subcategories(action.book.category);
break;
case BookConstants.BOOK_EDIT_CANCEL:
_state.subcategories = [];
CategoryStore.emitChange();
break;
}
return true;
});
</pre>
</div>
<div class="section" id="bookstore-js">
<h3><a class="toc-backref" href="#toc-entry-25">BookStore.js</a></h3>
<p>Here, beyond the book-related functionality we have also implemented the
<span class="caps">URL</span> updating. The getUrlParameter that returns the value of a <span class="caps">URL</span> parameter has been taken from
<a class="reference external" href="http://stackoverflow.com/questions/19491336/get-url-parameter-jquery">http://stackoverflow.com/questions/19491336/get-url-parameter-jquery</a>. Depending on the url parameters, we set some initial properties of
the local state and, on the other hand, when the search query, ordering or page are changed,
the <tt class="docutils literal">_update_href</tt> function is called to update the url parameters. This is not really related
to the flux architecture beyond the initialization of state.</p>
<p>Another thing to notice is that the when the <tt class="docutils literal">_search</tt> is executed whenever there’s
a change in the list of books (query is updated, sorting is changed, page is changed or
when an author is deleted since the books that have that author should now display an
empty field). The setTimeout in the _search ajax return is to simulate a 400ms delay (in order for the “Loading” text to
be visible).</p>
<pre class="code literal-block">
function getUrlParameter(sParam) {
var sPageURL = $(location).attr('hash');
sPageURL = sPageURL.substr(1)
var sURLVariables = sPageURL.split('&');
for (var i = 0; i < sURLVariables.length; i++) {
var sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] == sParam) {
return sParameterName[1];
}
}
}
var _page_init = 1*getUrlParameter('page');
if(!_page_init) _page_init = 1 ;
var _ordering_init = getUrlParameter('ordering');
if(!_ordering_init) _ordering_init = '' ;
var _query_init = getUrlParameter('query');
if(!_query_init) _query_init = ''
var _state = {
loading: false,
books: [],
message:{},
page: _page_init,
total: 0,
editingBook: {},
query: _query_init,
ordering: _ordering_init
}
var _search = function() {
_state.loading = true;
BookStore.emitChange();
$.ajax({
url: _props.url+'?search='+_state.query+"&ordering="+_state.ordering+"&page="+_state.page,
dataType: 'json',
cache: false,
success: function(data) {
// Simulate a small delay in server response
setTimeout(function() {
_state.books = data.results;
_state.total = data.count;
_state.loading = false;
BookStore.emitChange();
}, 400);
},
error: function(xhr, status, err) {
_state.loading = false;
MessageActions.add_message_error(err.toString());
BookStore.emitChange();
}
});
};
var _reloadBooks = function() {
_search('');
};
var _clearEditingBook = function() {
_state.editingBook = {};
};
var _editBook = function(book) {
_state.editingBook = book;
BookStore.emitChange();
};
var _cancelEditBook = function() {
_clearEditingBook();
BookStore.emitChange();
};
var _update_href = function() {
var hash = 'page='+_state.page;
hash += '&ordering='+_state.ordering;
hash += '&query='+_state.query;
$(location).attr('hash', hash);
}
BookStore.dispatchToken = AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.BOOK_EDIT:
_editBook(action.book);
break;
case BookConstants.BOOK_EDIT_CANCEL:
_cancelEditBook();
break;
case BookConstants.BOOK_SAVE:
_saveBook(action.book);
break;
case BookConstants.BOOK_SEARCH:
_state.query = action.query
_state.page = 1;
_update_href();
_search();
break;
case BookConstants.BOOK_DELETE:
_deleteBook(action.bookId);
break;
case BookConstants.BOOK_CHANGE:
_state.editingBook = action.book;
BookStore.emitChange();
break;
case BookConstants.BOOK_PAGE:
_state.page = action.page;
_update_href();
_search();
break;
case BookConstants.AUTHOR_DELETE_OK:
_search();
break;
case BookConstants.BOOK_SORT:
_state.page = 1;
if(_state.ordering == action.field) {
_state.ordering = '-'+_state.ordering
} else {
_state.ordering = action.field;
}
_update_href();
_search();
break;
}
return true;
});
</pre>
</div>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-26">Conclusion</a></h2>
<p>The application presented here has a number of techniques that will help
you when you actually try to create a more complex react flux application.
I hope that in the whole three part series I’ve thoroughly explained the
flux architecture and how each
part of (actions, stores, components) it works. Also, I tried to cover
almost anything that somebody creating react/flux application
will need to use — if you feel that something is not covered and
could be integrated to the authors/book application I’d be happy
to research and implement it!</p>
</div>
django-rq redux: advanced techniques and tools2015-09-01T14:20:00+03:002015-09-01T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-09-01:/2015/09/01/django-rq-redux/<p class="first last">Another article about django-rq with some more advanced techniques and tools!</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#displaying-your-task-progress" id="toc-entry-2">Displaying your task progress</a></li>
<li><a class="reference internal" href="#displaying-your-queue-statistics" id="toc-entry-3">Displaying your queue statistics</a></li>
<li><a class="reference internal" href="#making-sure-that-workers-for-your-queue-are-actually-running" id="toc-entry-4">Making sure that workers for your queue are actually running</a></li>
<li><a class="reference internal" href="#checking-how-many-jobs-are-in-the-queue" id="toc-entry-5">Checking how many jobs are in the queue</a></li>
<li><a class="reference internal" href="#better-exception-handling" id="toc-entry-6">Better exception handling</a></li>
<li><a class="reference internal" href="#multiple-django-apps-single-redis-db" id="toc-entry-7">Multiple django-apps, single redis db</a></li>
<li><a class="reference internal" href="#low-level-debugging" id="toc-entry-8">Low level debugging</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-9">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>In the <a class="reference external" href="https://spapas.github.io/2015/01/27/async-tasks-with-django-rq/">previous django-rq article</a>
we presented a quick introduction to asynchronous job queues and created a
small (but complete) project that used rq and django-rq to implement asynchronous
job queues in a django project.</p>
<p>In this article, we will present some more advanced techniques and tools
for improving the capabilities of our asynchronous tasks and
integrate them to the <a class="reference external" href="https://github.com/spapas/django-test-rq">https://github.com/spapas/django-test-rq</a> project (please
checkout tag django-rq-redux
<tt class="docutils literal">git checkout <span class="pre">django-rq-redux</span></tt>)</p>
</div>
<div class="section" id="displaying-your-task-progress">
<h2><a class="toc-backref" href="#toc-entry-2">Displaying your task progress</a></h2>
<p>Sometimes, especially for long-running tasks it is useful to let
the user (task initiator) know what is the status of each task he’s started. For this,
I recommend creating a task-description model that will hold the required information for this
task with more or less the following fields (please also check <tt class="docutils literal">LongTask</tt> model of django-test-rq):</p>
<pre class="code literal-block">
class LongTask(models.Model):
created_on = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=128, help_text='Enter a unique name for the task',)
progress = models.PositiveIntegerField(default=0)
result = models.CharField(max_length=128, blank=True, null=True)
</pre>
<p>Now, when the view that starts the task is <tt class="docutils literal"><span class="caps">POST</span></tt> ed, you’ll first create
the <tt class="docutils literal">LongTask</tt> model instance with a result of <tt class="docutils literal">'<span class="caps">QUEUED</span>'</tt> and a progress
of 0 (and a name that identifies your task) and then you’ll start the real task
asynchronously by passing the LongTask instance, something like this (also check
<tt class="docutils literal">LongTaskCreateView</tt>):</p>
<pre class="code literal-block">
long_task = LongTask.objects.create(...)
long_runnig_task.delay(long_task)
</pre>
<p>In your asynchronous job, the first thing you’ll need to do is to set its result
to ‘<span class="caps">STARTED</span>’ and save it so that the user will immediately see when he’s job is
actually started. Also, if you can estimate its progress, you can update its
progress value with the current value so that the user will know how close he
is to finishing. Finally, when the job finished (or if it throws an expectable
exception) you’ll update its status accordingly. Here’s an example of my
long_running_task that just waits for the specifid amount of seconds:</p>
<pre class="code literal-block">
@job('django-test-rq-low')
def long_runnig_task(task):
job = get_current_job()
task.job_id = job.get_id()
task.result = 'STARTED'
duration_in_second_persentages = task.duration*1.0 / 100
for i in range(100):
task.progress = i
task.save()
print task.progress
time.sleep(duration_in_second_persentages)
task.result = 'FINISHED'
task.save()
return task.result
</pre>
<p>To have proper feedback I propose to have your task-description model instance
created by the view that starts the asynchronous task and <em>not</em> by the
actual task! This is important since the worker may be full so the asynchronous
task will need a lot of time until is actually started (or maybe there are no
running workers - more on this later) and the user will not be able to see
his task instance anywhere (unless of course you provide him access to the actual task
queue but I don’t recommend this).</p>
</div>
<div class="section" id="displaying-your-queue-statistics">
<h2><a class="toc-backref" href="#toc-entry-3">Displaying your queue statistics</a></h2>
<p>django-rq has a really nice dashboard with a lot of queue statistics (
instructions here
<a class="reference external" href="https://github.com/ui/django-rq#queue-statistics">https://github.com/ui/django-rq#queue-statistics</a> and also on django-test-rq
project) which I recommend to always enable.</p>
<p>Also, there’s the individual use <a class="reference external" href="https://github.com/brutasse/django-rq-dashboard">django-rq-dashboard</a> project that could
be installed to display some more statistics, however the only extra
statistic that you can see throuh django-rq-dashboard is the status of
your scheduled jobs so I don’t recommend installing it if you don’t
use scheduling.</p>
</div>
<div class="section" id="making-sure-that-workers-for-your-queue-are-actually-running">
<h2><a class="toc-backref" href="#toc-entry-4">Making sure that workers for your queue are actually running</a></h2>
<p>Using the django-rq dashboard you can make sure that all queues
have at least one worker. However, sometimes workers fail, or
maybe you’ve forgotten to start your workers or not configured
your application correctly (this happens to me all the time for
test/uat projects). So, for tasks that you want to display feedback
to the user, you can easily add a check to make sure that there are
active workers using the following code:</p>
<pre class="code literal-block">
from rq import Worker
import django_rq
redis_conn = django_rq.get_connection('default')
if len([
x for x in Worker.all(connection=redis_conn)
if 'django-test-rq-low' in x.queue_names()
]) == 0:
# Error -- no workers
</pre>
<p>With <tt class="docutils literal">Worker.all()</tt> you get all workers for a connection and the <tt class="docutils literal">queue_names()</tt>
method returns the names that each worker serves. So we check that we have at least one
worker for that queue.</p>
<p>This check can be added when the job is started and display a feedback error
to the user (check example in django-test-rq).</p>
<p>For quick tasks (for example sending emails etc) you should not display anything
to the user even if no workers are running (since the task <em>will</em> be queued and
will be executed eventually when the workers are started) but instead send an email to the administrators
so that they will start the workers.</p>
</div>
<div class="section" id="checking-how-many-jobs-are-in-the-queue">
<h2><a class="toc-backref" href="#toc-entry-5">Checking how many jobs are in the queue</a></h2>
<p>To find out programatically how many jobs are actually in the queue (and display a message
if the queue has too many jobs etc) you’ll need to use the <tt class="docutils literal">Queue</tt> class, something like this:</p>
<pre class="code literal-block">
from rq import Queue
redis_conn = django_rq.get_connection('default')
queue = Queue('django-test-rq-default', connection=redis_conn)
print queue.name
print len(queue.jobs)
</pre>
</div>
<div class="section" id="better-exception-handling">
<h2><a class="toc-backref" href="#toc-entry-6">Better exception handling</a></h2>
<p>When a job fails, rq will put it in a failed jobs queue and finish with it. You (as administrator)
won’t get any feedback and the user (unless he has access to that failed jobs queue) won’t be
able to do anything aboutt this job.</p>
<p>In almost all cases you can’t rely only on this behavior but instead you have to
<a class="reference external" href="http://python-rq.org/docs/exceptions/">install a custom exception handler</a>. Using the custom exception handler you can
do whatever you want for each failed job. For instance, you can create a new instance
of a <tt class="docutils literal">FailedTask</tt> model which will have information about the failure and the
original task allow the user (or administrator) to restart the failed task after
he’s fixed the error conditions.</p>
<p>Or, if you want to be informed when a job is failed, you can just send an email
to <tt class="docutils literal"><span class="caps">ADMINS</span></tt> and fall back to the default behavior to enqueue the failed task the
failed jobs queue (since job exception handlers can be chained).</p>
<p>A simple management command that starts a worker for a specific queue and installs
a custom exception handler follows:</p>
<pre class="code literal-block">
from django.conf import settings
from django.core.management.base import BaseCommand
import django_rq
from rq import Queue, Worker
def my_handler(job, *exc_info):
print "FAILURE"
print job
print exc_info
class Command(BaseCommand):
def handle(self, *args, **options):
redis_conn = django_rq.get_connection('default')
q = Queue(settings.DJANGO_TEST_RQ_LOW_QUEUE, connection=redis_conn)
worker = Worker([q], exc_handler=my_handler, connection=redis_conn)
worker.work()
</pre>
<p>This handler is for demonstration purposes since it just prints a message to the console
(so please do not use it)!</p>
</div>
<div class="section" id="multiple-django-apps-single-redis-db">
<h2><a class="toc-backref" href="#toc-entry-7">Multiple django-apps, single redis db</a></h2>
<p>One thing to keep in mind is that the only thing that seperates the queues are
their name. If you have many django applications that define a “default” (or “low”, “hight” etc)
and they all use the <em>same</em> redis database to store their queue, the workers
of each application won’t know which jobs belong to them and they’ll end up
dequeuing the wrong job types. This will lead to an exception or, if you
are really unlucky to a very nasty bug!</p>
<p>To avoid this, you can either use a different redis database (not database server)
for each of your apps or add a prefix with the name of your app to your queue names:</p>
<p>Each redis database server can host a number of databases that are identified
by a number (that’s what the /0 you see in <tt class="docutils literal"><span class="pre">redis://127.0.0.1:6379/0</span></tt> means)
and each one of them has a totally different keyspace. So, if you use /0 in an
application and /1 in another application, you’ll have no problems. This solution
has the disadvantage that you need to be really careful to use different database
numbers for your projects and also the number of possible databases that redis
can use is limited by a configuration file (so if you reach the maximum you’ll
need to also increase that number)!</p>
<p>Instead of this, you can avoid using the ‘default’ queue, and use queues that
contain your application name in their name, for example, for the sample project
you could create something like ‘django-test-rq-default’, ‘django-test-rq-low’,
‘django-test-rq-high’ etc. You need to configure the extra queues by adding them
to the <tt class="docutils literal">RQ_QUEUES</tt> dictionary (check settings.py of django-test-rq) and then
put the jobs to these queues using for example the job decorator
(<tt class="docutils literal"><span class="pre">@job('django-test-rq-default')</span></tt>)
and run your workers so that they will retrieve jobs from these queues
(<tt class="docutils literal">python manage.py rqworker <span class="pre">django-test-rq-default</span></tt>) and not the
default one (which may contain jobs of other applications).</p>
<p>If you use the default queue, and because you’ll need to use its name to
many places, I recommend to add a (f.i) <tt class="docutils literal">QUEUE_NAME = <span class="pre">'django-test-rq-default'</span></tt>
setting and use this instead of just a string to be totally <span class="caps">DRY</span>.</p>
<p><strong>Update 13/09/2015</strong>: Please notice that using a <em>single</em> redis database server
(either with multiple numeric databases or in the same database using a keyword
in keys to differentiate the apps) <a class="reference external" href="https://redislabs.com/blog/benchmark-shared-vs-dedicated-redis-instances#.VfUl0xHtmko">is not recommended</a> as commenter
Itamar Haber pointed out to me!</p>
<p>This is because for speed reasons redis uses a single thread to handle all requests
(regardless if they are in the same or different numerical databases), so all
resources may be used by a single, redis hungry, application and leave all others to starve!</p>
<p>Therefore, the recommended solution is to have a <em>different redis</em> server for each different
application. This does not mean that you need to have different servers, just to run
different instances of redis binding to different <span class="caps">IP</span> ports. Redis uses very little
resourecs when it is idle (<a class="reference external" href="http://redis.io/topics/faq">empty instance uses ~ 1 <span class="caps">MB</span> <span class="caps">RAM</span></a>) so you can run a lot
of instances in a single server.</p>
<p>Long story short, my proposal is to have a redis.conf <em>inside</em> your application root tree
(next to manage.py and requirements.txt) which has the redis options for each
application. The options in redis.conf that need to be changed per application
is the port that this redis instance will bind (this port also needs to be passed to
django settings.py) and the pid filename if you daemonize redis — I recommend using
a tool like <a class="reference external" href="http://supervisord.org/">supervisord</a> instead so that you won’t need any daemonizing and pid files for
each per-app-redis-instance!</p>
</div>
<div class="section" id="low-level-debugging">
<h2><a class="toc-backref" href="#toc-entry-8">Low level debugging</a></h2>
<p>In this section I’ll present some commands that you can issue to your redis
server using a simple telnet connection to get various info about your queues. You
probably will never need to issue these commands to actually debug, but they
will answer some of your (scientific) questions! In the following, <tt class="docutils literal">></tt> is
things I type, <tt class="docutils literal">#</tt> are comments, <tt class="docutils literal"><span class="pre">[...]</span></tt> is more output and everything else is the output I get:</p>
<pre class="code literal-block">
> telnet 127.0.0.1 6379
# You won't see anything at first but you'll be connected and you can try typing things
> INFO
$1020
redis_version:2.4.10
redis_git_sha1:00000000
# [...]
db0:keys=83,expires=2
db1:keys=26,expires=1 # My redis server has two databases
# Now you'll see what you type!
> SELECT 1
+ OK # Now queries will be issued to database 1
> SELECT 0
+ OK # Now queries will be issued to database 0
KEYS rq* # List all rq related queues
*25
$43
rq:job:1d7afa32-3f90-4502-912f-d58eaa049fb1
$43
rq:queue:django-test-rq-low
$43
[...]
> SMEMBERS rq:workers # See workers
*1
$26
rq:worker:SERAFEIM-PC.6892
> LRANGE rq:queue:django-test-rq-low 0 100 # Check queued jobs
*2
$36
def896f4-84cb-4833-be6a-54d917f05271
$36
53cb1367-2fb5-46b3-99b2-7680397203b9
> HGETALL rq:job:def896f4-84cb-4833-be6a-54d917f05271 # Get info about this job
*16
$6
status
$6
queued
$11
description
$57
tasks.tasks.long_runnig_task(<LongTask: LongTask object>)
$10
created_at
$20
2015-09-01T09:04:38Z
$7
timeout
$3
180
$6
origin
$18
django-test-rq-low
$11
enqueued_at
$20
2015-09-01T09:04:38Z
$4
data
$409
[...] # data is the pickled parameters passed to the job !
> HGET rq:job:def896f4-84cb-4833-be6a-54d917f05271 status # Get only status
$6
queued
</pre>
<p>For more info on querying redis you can check the <a class="reference external" href="http://redis.io/documentation">redis documentation</a> and especially
<a class="reference external" href="http://redis.io/topics/data-types">http://redis.io/topics/data-types</a> and <a class="reference external" href="http://redis.io/commands">http://redis.io/commands</a>.</p>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-9">Conclusion</a></h2>
<p>Using some of the above techniques will help you in your asynchronous
task adventures with rq. I’ll try to keep this article updated with
any new techniques or tools I find in the future!</p>
</div>
A comprehensive React and Flux tutorial part 2: Flux2015-07-02T14:20:00+03:002015-07-02T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-07-02:/2015/07/02/comprehensive-react-flux-tutorial-2/<p class="first last">A React and Flux tutorial that tries to be as comprehensive as possible! Part 2 is about Flux.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#flux-components" id="toc-entry-2">Flux components</a></li>
<li><a class="reference internal" href="#the-react-flux-version" id="toc-entry-3">The react-flux version</a><ul>
<li><a class="reference internal" href="#main-js" id="toc-entry-4">main.js</a></li>
<li><a class="reference internal" href="#constants-js" id="toc-entry-5">constants.js</a></li>
<li><a class="reference internal" href="#actions-js" id="toc-entry-6">actions.js</a></li>
<li><a class="reference internal" href="#stores-js" id="toc-entry-7">stores.js</a></li>
<li><a class="reference internal" href="#components-js" id="toc-entry-8">components.js</a></li>
</ul>
</li>
<li><a class="reference internal" href="#explaining-the-data-flow" id="toc-entry-9">Explaining the data flow</a></li>
<li><a class="reference internal" href="#a-better-code-organization" id="toc-entry-10">A better code organization</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-11">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>In the <a class="reference external" href="https://spapas.github.io/2015/06/05/comprehensive-react-flux-tutorial/">first part of this series</a> we implemented a
not-so-simple one page application with full <span class="caps">CRUD</span> capabilities. In this part, we will
modify that application to make it use the Flux architecture. The full source code can
be found at <a class="reference external" href="https://github.com/spapas/react-tutorial">https://github.com/spapas/react-tutorial</a> (tag name react-flux). In the
<a class="reference external" href="https://spapas.github.io/2015/09/08/more-complex-react-flux-example/">next part,</a> we will create an even more complex application
using react/flux!</p>
<p>I recommend reading Facebook’s <a class="reference external" href="https://facebook.github.io/flux/docs/overview.html">Flux overview</a> before reading this article — please
read it even if you find some concepts difficult to grasp (I know I found it difficult
the first time I read it), I will try to explain everything here. Also,
because a rather large number of extra components will need to be created, we are
going to split our javascript code to different files using <a class="reference external" href="http://browserify.org/">browserify</a> - you can
learn <a class="reference external" href="https://spapas.github.io/2015/05/27/using-browserify-watchify/">how to use browserify here</a>.</p>
</div>
<div class="section" id="flux-components">
<h2><a class="toc-backref" href="#toc-entry-2">Flux components</a></h2>
<p>To implement the Flux architecture, an application needs to have at least a store and a dispatcher.</p>
<p>The store is the central place of truth for the application and the dispacher is the central
place of communications. The store should hold the
state (and any models/DAOs) of the application and notify the react components when this state is changed. Also,
the store will be notified by the dispatcher when an action happens (for example a button is clicked)
so that it will change the state. As a flow, we can think of something like this:</p>
<pre class="code literal-block">
a ui action on a component (click, change, etc) ->
^ dispatcher is notified ->
| store is notified (by the dispacher)->
| store state is changed ->
└─ component is notified (by the store) and updated to reflect the change
</pre>
<p>One thing to keep in mind is that although each flux application will have only one dispatcher, it may
have more stores, depending on the application’s architecture and separation of concerns. If there are
more than store, all will be notified by the dispatcher and change their state (if needed of course).
The ui will pass the action type and any optional parameters to the dispatcher and the dispatcher
will notify all stores with these parameters.</p>
<p>An optional component in the Flux architecture is the Action. An action is a store related class that
acts as an intermediate between the ui and the dispatcher. So, when a user clicks a button, an action
will be called that will notify the dispatcher. As we will see we can just call the dispatcher directly
from the components ui, but calling it through the Action makes the calls more consistent and creates
an interface.</p>
</div>
<div class="section" id="the-react-flux-version">
<h2><a class="toc-backref" href="#toc-entry-3">The react-flux version</a></h2>
<p>Since we are using browserify, we will include a single file in our html file with a <tt class="docutils literal"><script></tt> tag
and everything else will be included through the <tt class="docutils literal">require</tt> function. We have the following packages
as requirements for browserify:</p>
<pre class="code literal-block">
"dependencies": {
"flux": "^2.0.3",
"jquery": "^2.1.4",
"react": "^0.13.3",
"reactify": "^1.1.1"
}
</pre>
<p>Also, in order to be able to use <span class="caps">JSX</span> with browserify, will use the <a class="reference external" href="https://github.com/andreypopp/reactify">reactify</a> transform. To apply it to
your project, change the <tt class="docutils literal">scripts</tt> of your <tt class="docutils literal">package.json</tt> to:</p>
<pre class="code literal-block">
"scripts": {
"watch": "watchify -v -d static/main.js -t reactify -o static/bundle.js",
"build": "browserify static/main.js -t reactify | uglifyjs -mc warnings=false > static/bundle.js"
},
</pre>
<div class="section" id="main-js">
<h3><a class="toc-backref" href="#toc-entry-4">main.js</a></h3>
<p>The <tt class="docutils literal">main.js</tt> file will
just render the BookPanel component (the <tt class="docutils literal">components.js</tt> file contains the source for all React components) and call
the <tt class="docutils literal">reloadBooks</tt> function from <tt class="docutils literal">stores.js</tt> that will reload all books from the <span class="caps">REST</span> <span class="caps">API</span>:</p>
<pre class="code literal-block">
var React = require('react');
var components = require('./components');
var stores = require('./stores');
React.render(<components.BookPanel url='/api/books/' />, document.getElementById('content'));
stores.reloadBooks();
</pre>
</div>
<div class="section" id="constants-js">
<h3><a class="toc-backref" href="#toc-entry-5">constants.js</a></h3>
<p>Before going into more complex modules, let’s present the <tt class="docutils literal">constants.js</tt> which just
exports some strings that will be passed to the dispatcher to differentiate between each
ui action:</p>
<pre class="code literal-block">
module.exports = {
BOOK_EDIT: 'BOOK_EDIT',
BOOK_EDIT_CANCEL: 'BOOK_EDIT_CANCEL',
BOOK_SAVE: 'BOOK_SAVE',
BOOK_SEARCH: 'BOOK_SEARCH',
BOOK_DELETE: 'BOOK_DELETE',
};
</pre>
<p>As we can see, these constants are exported as a single object so when we do something like
<tt class="docutils literal">var BookConstants = <span class="pre">require('./constants')</span></tt> we’ll the be able to refer to each constant
through <tt class="docutils literal">BookConstants.CONSTANT_NAME</tt>.</p>
</div>
<div class="section" id="actions-js">
<h3><a class="toc-backref" href="#toc-entry-6">actions.js</a></h3>
<p>The <tt class="docutils literal">actions.js</tt> creates the dispatcher singleton and a BookActions object that defines the
actions for books.</p>
<pre class="code literal-block">
var BookConstants = require('./constants')
var Dispatcher = require('flux').Dispatcher;
var AppDispatcher = new Dispatcher();
var BookActions = {
search: function(query) {
AppDispatcher.dispatch({
actionType: BookConstants.BOOK_SEARCH,
query: query
});
},
save: function(book) {
AppDispatcher.dispatch({
actionType: BookConstants.BOOK_SAVE,
book: book
});
},
edit: function(book) {
AppDispatcher.dispatch({
actionType: BookConstants.BOOK_EDIT,
book: book
});
},
edit_cancel: function() {
AppDispatcher.dispatch({
actionType: BookConstants.BOOK_EDIT_CANCEL
});
},
delete: function(bookId) {
AppDispatcher.dispatch({
actionType: BookConstants.BOOK_DELETE,
bookId: bookId
});
}
};
module.exports.BookActions = BookActions;
module.exports.AppDispatcher = AppDispatcher;
</pre>
<p>As we can see, the BookActions is just a collection of methods that will
be called from the ui. Instead of calling BookActions.search() we could
just call the dispatch method with the correct parameter object (actionType
and optional parameter), both the BookActions object and the AppDispatcher
singleton are exported.</p>
<p>The dispatcher is imported from the flux requirement: It offers a functionality
to register callbacks for the various actions as we will see in the
next module. This is a rather simple class that we could implement ourselves
(each store passes a callback to the dispatcher that is called on the dispatch
method, passing actionType and any other parameters). The dispatcher also
offers a <tt class="docutils literal">waitFor</tt> method that can be used to ensure that the dispatch callback
for a store will be finished before another store’s dispatch callback (
when the second store uses the state of the first store — for example
when implementing a series of related dropdowns ).</p>
</div>
<div class="section" id="stores-js">
<h3><a class="toc-backref" href="#toc-entry-7">stores.js</a></h3>
<p>The next module we will discuss is the <tt class="docutils literal">stores.js</tt> that contains the
<tt class="docutils literal">BookStore</tt> object.</p>
<pre class="code literal-block">
var $ = require('jquery');
var EventEmitter = require('events').EventEmitter;
var AppDispatcher = require('./actions').AppDispatcher;
var BookConstants = require('./constants')
var _state = {
books: [],
message:"",
editingBook: null
}
var _props = {
url: '/api/books/'
}
var _search = function(query) {
$.ajax({
url: _props.url+'?search='+query,
dataType: 'json',
cache: false,
success: function(data) {
_state.books = data;
BookStore.emitChange();
},
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
_state.message = err.toString();
BookStore.emitChange();
}
});
};
var _reloadBooks = function() {
_search('');
};
var _deleteBook = function(bookId) {
$.ajax({
url: _props.url+bookId,
method: 'DELETE',
cache: false,
success: function(data) {
_state.message = "Successfully deleted book!"
_clearEditingBook();
_reloadBooks();
},
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
_state.message = err.toString();
BookStore.emitChange();
}
});
};
var _saveBook = function(book) {
if(book.id) {
$.ajax({
url: _props.url+book.id,
dataType: 'json',
method: 'PUT',
data:book,
cache: false,
success: function(data) {
_state.message = "Successfully updated book!"
_clearEditingBook();
_reloadBooks();
},
error: function(xhr, status, err) {
_state.message = err.toString()
BookStore.emitChange();
}
});
} else {
$.ajax({
url: _props.url,
dataType: 'json',
method: 'POST',
data:book,
cache: false,
success: function(data) {
_state.message = "Successfully added book!"
_clearEditingBook();
_reloadBooks();
},
error: function(xhr, status, err) {
_state.message = err.toString()
BookStore.emitChange();
}
});
}
};
var _clearEditingBook = function() {
_state.editingBook = null;
};
var _editBook = function(book) {
_state.editingBook = book;
BookStore.emitChange();
};
var _cancelEditBook = function() {
_clearEditingBook();
BookStore.emitChange();
};
var BookStore = $.extend({}, EventEmitter.prototype, {
getState: function() {
return _state;
},
emitChange: function() {
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
},
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
AppDispatcher.register(function(action) {
switch(action.actionType) {
case BookConstants.BOOK_EDIT:
_editBook(action.book);
break;
case BookConstants.BOOK_EDIT_CANCEL:
_cancelEditBook();
break;
case BookConstants.BOOK_SAVE:
_saveBook(action.book);
break;
case BookConstants.BOOK_SEARCH:
_search(action.query);
break;
case BookConstants.BOOK_DELETE:
_deleteBook(action.bookId);
break;
}
return true;
});
module.exports.BookStore = BookStore;
module.exports.reloadBooks = _reloadBooks;
</pre>
<p>The <tt class="docutils literal">stores.js</tt> module exports only the <tt class="docutils literal">BookStore</tt> object and the <tt class="docutils literal">reloadBooks</tt> method (that could also be
called from inside the module since it’s just called when the application is loaded to load the books for the
first time). All other objects/funtions are private to the module.</p>
<p>As we saw, the <tt class="docutils literal">_state</tt> objects keep the global state of the application which are the list of books, the book
that is edited right now and the result message for any update we are doing. The ajax methods are more or less
the same as the ones in the react-only version of the application. However, please notice that when the ajax methods
return and have to set the result, instead of setting the state of a React object they are just calling the
<tt class="docutils literal">emitChange</tt> method of the <tt class="docutils literal">BookStore</tt> that will notify all react objects that “listen” to this store.
This is possible because the ajax (<span class="caps">DAO</span>) methods are in the same module with the store - if we wanted instead
to put them in different modules, we’d just need to add another action (e.g <tt class="docutils literal">ReloadBooks</tt>) that would
be called when the ajax method returns — this action would call the dispatcher which would in turn update the
state of the store.</p>
<p>We can see that we are importing the
AppDispatcher singleton and, depending on the action type we call the correct method that changes the state. So
when a BookActions action is called it will call the corresponding <tt class="docutils literal">AppDispatcher.register</tt> case branch which
will call the corresponding state-changing function.</p>
<p>The BookStore extends the <tt class="docutils literal">EventEmitter</tt> object (so we need to <tt class="docutils literal">require</tt> the <tt class="docutils literal">events</tt> module) in order to
notify the React components when the state of the store is changed. Instead of using <tt class="docutils literal">EventEmitter</tt> we could
just implement the emit change logic ourselves by saving all the listener callbacks to an array and calling them
all when there’s a state change (if we wanted to also add the ‘change’ parameter to group the listener
callbacks we’d just make the complex more complex, something not needed for our case):</p>
<pre class="code literal-block">
var BookStore = {
listeners: [],
getState: function() {
return _state;
},
emitChange: function() {
var i;
for(i=0;i<this.listeners.length;i++) {
this.listeners[i]();
}
},
addChangeListener: function(callback) {
this.listeners.push(callback);
},
removeChangeListener: function(callback) {
this.listeners.splice(this.listeners.indexOf(callback), 1);
}
};
</pre>
</div>
<div class="section" id="components-js">
<h3><a class="toc-backref" href="#toc-entry-8">components.js</a></h3>
<p>Finally, the <tt class="docutils literal">components.js</tt> module contains all the React components. These are more
or less the same with the react-only version with three differences:</p>
<ul class="simple">
<li>When something happens in the ui, the corresponding <tt class="docutils literal">BookAction</tt> action is called with the needed parameter — no callbacks are passed between the components</li>
<li>The <tt class="docutils literal">BookPanel</tt> component registers with the <tt class="docutils literal">BookStore</tt> in order to be notified when the state changes and just gets its state from the store — these values are propagated to all other components through properties</li>
<li>The <tt class="docutils literal">BookForm</tt> and <tt class="docutils literal">SearcchPanel</tt> now hold their own temporary state instead of using the global state — notice that when a book is edited this book will be propagated to the <tt class="docutils literal">BookForm</tt> through the book property, however <tt class="docutils literal">BookForm</tt> needs to update its state through the <tt class="docutils literal">componentWillReceiveProps</tt> method.</li>
</ul>
<pre class="code literal-block">
var React = require('react');
var BookStore = require('./stores').BookStore;
var BookActions = require('./actions').BookActions;
var BookTableRow = React.createClass({
render: function() {
return (
<tr>
<td>{this.props.book.id}</td>
<td>{this.props.book.title}</td>
<td>{this.props.book.category}</td>
<td><a href='#' onClick={this.onClick}>Edit</a></td>
</tr>
);
},
onClick: function(e) {
e.preventDefault();
BookActions.edit(this.props.book);
}
});
var BookTable = React.createClass({
render: function() {
var rows = [];
this.props.books.forEach(function(book) {
rows.push(<BookTableRow key={book.id} book={book} />);
});
return (
<table>
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Category</th>
<th>Edit</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
});
var BookForm = React.createClass({
getInitialState: function() {
if (this.props.book) {
return this.props.book;
} else {
return {};
}
},
componentWillReceiveProps: function(props) {
if (props.book) {
this.setState(props.book);
} else {
this.replaceState({});
}
},
render: function() {
return(
<form onSubmit={this.onSubmit}>
<label forHtml='title'>Title</label><input ref='title' name='title' type='text' value={this.state.title} onChange={this.onFormChange} />
<label forHtml='category'>Category</label>
<select ref='category' name='category' value={this.state.category} onChange={this.onFormChange} >
<option value='CRIME' >Crime</option>
<option value='HISTORY'>History</option>
<option value='HORROR'>Horror</option>
<option value='SCIFI'>SciFi</option>
</select>
<br />
<input type='submit' value={this.state.id?"Save (id = " +this.state.id+ ")":"Add"} />
{this.state.id?<button onClick={this.onDeleteClick}>Delete</button>:""}
{this.state.id?<button onClick={this.onCancelClick}>Cancel</button>:""}
{this.props.message?<div>{this.props.message}</div>:""}
</form>
);
},
onFormChange: function() {
this.setState({
title: React.findDOMNode(this.refs.title).value,
category: React.findDOMNode(this.refs.category).value
})
},
onSubmit: function(e) {
e.preventDefault();
BookActions.save(this.state)
},
onCancelClick: function(e) {
e.preventDefault();
BookActions.edit_cancel()
},
onDeleteClick: function(e) {
e.preventDefault();
BookActions.delete(this.state.id)
}
});
var SearchPanel = React.createClass({
getInitialState: function() {
return {
search: '',
}
},
render: function() {
return (
<div className="row">
<div className="one-fourth column">
Filter: &nbsp;
<input ref='search' name='search' type='text' value={this.state.search} onChange={this.onSearchChange} />
{this.state.search?<button onClick={this.onClearSearch} >x</button>:''}
</div>
</div>
)
},
onSearchChange: function() {
var query = React.findDOMNode(this.refs.search).value;
if (this.promise) {
clearInterval(this.promise)
}
this.setState({
search: query
});
this.promise = setTimeout(function () {
BookActions.search(query);
}.bind(this), 200);
},
onClearSearch: function() {
this.setState({
search: ''
});
BookActions.search('');
}
});
var BookPanel = React.createClass({
getInitialState: function() {
return BookStore.getState();
},
render: function() {
return(
<div className="row">
<div className="one-half column">
<SearchPanel></SearchPanel>
<BookTable books={this.state.books} />
</div>
<div className="one-half column">
<BookForm
book={this.state.editingBook}
message={this.state.message}
/>
</div>
<br />
</div>
);
},
_onChange: function() {
this.setState( BookStore.getState() );
},
componentWillUnmount: function() {
BookStore.removeChangeListener(this._onChange);
},
componentDidMount: function() {
BookStore.addChangeListener(this._onChange);
}
});
module.exports.BookPanel = BookPanel ;
</pre>
<p>Only the <tt class="docutils literal">BookPanel</tt> is exported — all other react components will be private to the module.</p>
<p>We can see that, beyond BookPanel, the code of all other components
are more or less the same. However, <em>not</em> having to pass callbacks for state upddates
is a huge win for readability and DRYness.</p>
</div>
</div>
<div class="section" id="explaining-the-data-flow">
<h2><a class="toc-backref" href="#toc-entry-9">Explaining the data flow</a></h2>
<p>I’ve added a bunch of console.log statements to see how the data/actions flow between
all the components when the “Edit” book is clicked. So, when we click “Edit” we see
the following messages to our console:</p>
<pre class="code literal-block">
Inside BookTableRow.onClick
Inside BookActions.edit
Inside AppDispatcher.register
Inside AppDispatcher.register case BookConstants.BOOK_EDIT
Inside _editBook
Inside BookStore.emitChange
Inside BookPanel._onChange
Inside BookForm.componentWillReceiveProps
Inside BookForm.render
</pre>
<p>First of all the <tt class="docutils literal">onClick</tt> method of <tt class="docutils literal">BookTableRow</tt> will be called (which is the onClick property of the
a href link) which will call <tt class="docutils literal">BookActions.edit</tt> and pass it the book of that specific row. The <tt class="docutils literal">edit</tt>
method will create a new dispatcher object by setting the <tt class="docutils literal">actionType</tt> and passing the <tt class="docutils literal">book</tt> and
pass it to <tt class="docutils literal">AppDispatcher.register</tt>. <tt class="docutils literal">register</tt> will go to the <tt class="docutils literal">BookConstants.BOOK_EDIT</tt> case branch
which will call the private <tt class="docutils literal">_editBook</tt> function. <tt class="docutils literal">_editBook</tt> will update the state of the store (by
setting the <tt class="docutils literal">_state.editingBook</tt> property and will call the <tt class="docutils literal">BookStore.emitChange</tt> method
which calls the dispatcher’s emit method, so all listening components will update. We only have one
component that listens to this emit, <tt class="docutils literal">BookPanel</tt> whose <tt class="docutils literal">_onChange</tt> method is called. This method
gets the application state from the <tt class="docutils literal">BookStore</tt> and updates its own state. Now, the state will be
propagated through properties - for example, for <tt class="docutils literal">BookForm</tt>, first its <tt class="docutils literal">componentWillReceiveProps</tt>
method will be called (with the new properties) and finally its <tt class="docutils literal">render</tt> method!</p>
<p>So the full data flow is something like this:</p>
<pre class="code literal-block">
user action/callback etc ->
component calls action ->
dispatcher informes stores ->
stores set their state ->
state holding components are notified and update their state ->
all other components are updated through properties
</pre>
</div>
<div class="section" id="a-better-code-organization">
<h2><a class="toc-backref" href="#toc-entry-10">A better code organization</a></h2>
<p>As you’ve seen, I’ve only created four javascript modules (components, stores, actions and constants)
and put them all in the same folder. I did this for clarity and to keep everything together since
our tutorial is a very small project. Facebook proposes a much better organization that what I did
as can be seen in the <a class="reference external" href="https://facebook.github.io/flux/docs/todo-list.html">TodoMVC tutorial</a>: Instead of putting everything to a single folder, create
a different foloder for each type of object: actions (for all your actions), components (for all
your React components), constants and stores and put inside the objects each in a different javascript
module, for example, the components folder should contain the following files:</p>
<ul class="simple">
<li>BookTable.react.js</li>
<li>BookTableRow.react.js</li>
<li>BookForm.react.js</li>
<li>BookPanel.react.js</li>
<li>SearchPanel.react.js</li>
</ul>
<p>Each one will export only the same-named React component and <tt class="docutils literal">require</tt> only the components that it uses.</p>
<p>If you want to see the code of this tutorial organized like this go to the tag <tt class="docutils literal"><span class="pre">react-flux-better-organization</span></tt>.</p>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-11">Conclusion</a></h2>
<p>In this two-part series we saw how we can create a full <span class="caps">CRUD</span> application with React.js and how can
we enable it with the Facebook proposed Flux architecture. Comparing the react-only with the react-flux
version we can see that we added a number of objects in the second version (dispatcher, store, actions, constants)
whose usefulness may not be obvious from our example. However, our created application (and especially
the better organized version) is war-ready and can easily fight any complexities that we throw to it!
Unfortunately, if we really wanted to show the usefulness of the Flux architecture we’d need to create
a really complex application that won’t be suitable for a tutorial.</p>
<p>However, we can already understand the obvious advantages of the React / Flux architecture:</p>
<ul class="simple">
<li>Components can easily be re-used by changing their properties - <span class="caps">DRY</span></li>
<li>Easy to grasp (but a little complex) data flow between components and stores</li>
<li>Separation of concerns - react components for the view, stores to hold the state/models, dispatcher to handle the data flow</li>
<li>Really easy to test - all components are simple objects and can be easily created fom tests</li>
<li>Works well for complex architectures - one dispatcher, multiple stores/action collections, react components only interact with actions and get their state from stores</li>
</ul>
<p>I’ve tried to make the above as comprehensive as possible for the readers of these posts
(and also resolve some of my own questions). I have to mention again that although React/Flux may
seem complex at a first glance, when it is used in a complex architecture it will shine and
make everything much easier. Everything is debuggable and we can always understand what’s
really going on! This is in contrast with more complex frameworks that do various hidden
stuff (two way data binding, magic in the <span class="caps">REST</span> etc) where, although it is easier to
create a simple app, moving to something more complex (and especially debugging it) is a real nightmare!</p>
</div>
A comprehensive React and Flux tutorial part 1: React2015-06-05T14:20:00+03:002015-06-05T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-06-05:/2015/06/05/comprehensive-react-flux-tutorial/<p class="first last">A React and Flux tutorial that tries to be as comprehensive as possible! Part 1 is about React.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#our-project" id="toc-entry-2">Our project</a></li>
<li><a class="reference internal" href="#a-top-level-view-of-the-components" id="toc-entry-3">A top-level view of the components</a></li>
<li><a class="reference internal" href="#the-react-only-version" id="toc-entry-4">The react-only version</a><ul>
<li><a class="reference internal" href="#searchpanel" id="toc-entry-5"><tt class="docutils literal">SearchPanel</tt></a></li>
<li><a class="reference internal" href="#booktablerow" id="toc-entry-6"><tt class="docutils literal">BookTableRow</tt></a></li>
<li><a class="reference internal" href="#booktable" id="toc-entry-7"><tt class="docutils literal">BookTable</tt></a></li>
<li><a class="reference internal" href="#interlude-the-bind-function-method" id="toc-entry-8">Interlude: The <tt class="docutils literal">bind</tt> function method</a></li>
<li><a class="reference internal" href="#bookform" id="toc-entry-9"><tt class="docutils literal">BookForm</tt></a></li>
<li><a class="reference internal" href="#bookpanel" id="toc-entry-10"><tt class="docutils literal">BookPanel</tt></a><ul>
<li><a class="reference internal" href="#component-methods" id="toc-entry-11">Component methods</a></li>
<li><a class="reference internal" href="#non-ajax-object-methods" id="toc-entry-12">Non-ajax object methods</a></li>
<li><a class="reference internal" href="#ajax-object-methods" id="toc-entry-13">Ajax object methods</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#local-state" id="toc-entry-14">Local state</a></li>
<li><a class="reference internal" href="#adding-form-validation" id="toc-entry-15">Adding form validation</a></li>
<li><a class="reference internal" href="#conclusion-to-the-first-part" id="toc-entry-16">Conclusion to the first part</a></li>
</ul>
</div>
<p><strong>Update 15/12/2015</strong>: Added some insights for form validation.</p>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p><a class="reference external" href="https://facebook.github.io/react/">React</a> is a rather new library from Facebook for building dynamic components for web pages.
It introduces a really simple, fresh approach to javascript user interface building. React
allows you to define composable and self-contained <span class="caps">HTML</span> components through Javascript (or a special syntax that compiles
to javascript named <span class="caps">JSX</span>) and that’s all — it doesn’t force you into any specific
framework or architecture, you may use whatever you like. It’s like writing pure
<span class="caps">HTML</span> but you have the advantage of using classes with all their advantages for your
components (self-contained, composable, reusable, mixins etc).</p>
<p>Although React components can be used however you like in your client-side applications, Facebook proposes a specific
architecture for the data flow of your events/data between the components called <a class="reference external" href="https://facebook.github.io/flux/docs/overview.html">Flux</a>.
It’s important to keep in your mind that Flux is not a
specific framework but a way to organize the flow of events and data in your applicition.
Unfortunately, although Flux is a rather simple architecture it is a little difficult to understand
without a proper example (at least it was difficult for me to understand by reading the
Facebook documentation and some tutorials I found).</p>
<p>So, in this two-part tutorial we are going to build a (not-so-simple) single page <span class="caps">CRUD</span> application using
React and Flux. Two versions of the same application will be built and explained: One with
React (this part) only and one with React and Flux (part two). This will help us understand how
Flux architecture fits to our project and why it greatly improves the experience with React.</p>
<p><strong>Warning</strong>: This is not an introduction to react. Before reading this you need
to be familiar with basic react usage, having read at least the following three
pages from react documentation:</p>
<ul class="simple">
<li><a class="reference external" href="https://facebook.github.io/react/docs/getting-started.html">https://facebook.github.io/react/docs/getting-started.html</a></li>
<li><a class="reference external" href="https://facebook.github.io/react/docs/tutorial.html">https://facebook.github.io/react/docs/tutorial.html</a></li>
<li><a class="reference external" href="https://facebook.github.io/react/docs/thinking-in-react.html">https://facebook.github.io/react/docs/thinking-in-react.html</a></li>
</ul>
</div>
<div class="section" id="our-project">
<h2><a class="toc-backref" href="#toc-entry-2">Our project</a></h2>
<p>We are going to built a <span class="caps">CRUD</span> single-page application for editing views, let’s take a look at how it works:</p>
<img alt="Our project" src="/images/demo.gif" style="width: 780px;" />
<p>Our application will be seperated to two panels: In the left one the user will be able to filter (search) for
a book and in the right panel she’ll be able to add / edit / delete a book. Everything is supported by
a <a class="reference external" href="http://www.django-rest-framework.org/">django-rest-framework</a> implemented <span class="caps">REST</span> <span class="caps">API</span>. You can find the complete source code at
<a class="reference external" href="https://github.com/spapas/react-tutorial">https://github.com/spapas/react-tutorial</a>. I’ve added a couple of git tags to the source history in
order to help us identify the differences between variou stages of the project (before and
after integrating the Flux architecture).</p>
<p>Django-rest-framework is used in the server-side back-end to create a really simple <span class="caps">REST</span> <span class="caps">API</span> - I won’t
provide any tutorial details on that (unless somebody wants it !), however
you may either use the source code as is or create it from scratch using a different language/framework
or even just a static json file (however you won’t see any changes to the data this way).</p>
<p>For styling (mainly to have a simple grid) we’ll use <a class="reference external" href="http://getskeleton.com/">skeleton</a>. For the ajax calls and some utils we’ll
use <a class="reference external" href="https://jquery.com/">jquery</a>.</p>
<p>All client-side code will be contained in the file static/main.js. The placeholder <span class="caps">HTML</span> for our
application is:</p>
<pre class="code literal-block">
<html>
<!-- styling etc ignored -->
<body>
<h1>Hello, React!</h1>
<div class="container"><div id="content"></div></div>
</body>
<script src="//fb.me/react-0.13.3.js"></script>
<script src="//fb.me/JSXTransformer-0.13.3.js"></script>
<script src="//code.jquery.com/jquery-2.1.3.min.js"></script>
<script type="text/jsx" src="{% static 'main.js' %}"></script>
</html>
</pre>
<p>We are using the version 0.13.3 of react and the same version of the JSXTransformer to translate <span class="caps">JSX</span>
code to pure javascript.</p>
</div>
<div class="section" id="a-top-level-view-of-the-components">
<h2><a class="toc-backref" href="#toc-entry-3">A top-level view of the components</a></h2>
<p>The following image shows how our components are composed:</p>
<img alt="Our components" src="/images/components.png" style="width: 780px;" />
<p>So, the main component of our application is <tt class="docutils literal">BookPanel</tt> which contains three components:</p>
<ul class="simple">
<li><tt class="docutils literal">SearchPanel</tt>: To allow search (filtering) books based on their title/category</li>
<li><tt class="docutils literal">BookForm</tt>: To add/update/delete books</li>
<li><tt class="docutils literal">BookTable</tt>: To display all available books - each book is displayed in a <tt class="docutils literal">BookTableRow</tt> component.</li>
</ul>
<p><tt class="docutils literal">BookPanel</tt> is the only component having state — all other components will be initialized by property
passing. The <tt class="docutils literal">BookPanel</tt> element will be mounted to the <tt class="docutils literal">#content</tt> element when the page loads.</p>
</div>
<div class="section" id="the-react-only-version">
<h2><a class="toc-backref" href="#toc-entry-4">The react-only version</a></h2>
<p>The first version, using only react (and not flux) will use <tt class="docutils literal">BookPanel</tt> as a central information <span class="caps">HUB</span>.</p>
<div class="section" id="searchpanel">
<h3><a class="toc-backref" href="#toc-entry-5"><tt class="docutils literal">SearchPanel</tt></a></h3>
<p><tt class="docutils literal">SearchPanel</tt> renders an input element with a value defined by the <tt class="docutils literal">search</tt> key of this component’same
properties. When this is changed the <tt class="docutils literal">onSearchChanged</tt> method of the component will be called, which in turn,
retrieves the value of the input (using refs) and passes it to the properties <tt class="docutils literal">onSearchChanged</tt> callback function.
Finally, with the line <tt class="docutils literal"><span class="pre">{this.props.search?<button</span> <span class="pre">onClick={this.props.onClearSearch}</span> <span class="pre">>x</button>:null}</span></tt>
we check if the search property contains any text and if yes, we display a clear filter button that will call
properties onClearSearch method:</p>
<pre class="code literal-block">
var SearchPanel = React.createClass({
render: function() {
return (
<div className="row">
<div className="one-fourth column">
Filter: &nbsp;
<input ref='search' type='text' value={this.props.search} onChange={this.onSearchChanged} />
{this.props.search?<button onClick={this.props.onClearSearch} >x</button>:null}
</div>
</div>
)
},
onSearchChanged: function() {
var query = React.findDOMNode(this.refs.search).value;
this.props.onSearchChanged(query);
}
});
</pre>
<p>So, this component has three properties:</p>
<ul class="simple">
<li>search which is the text to display in the input box</li>
<li>onSearchChanged (callback) which is called when the contents of the input box are changed</li>
<li>onClearSearch (callback) which is called when the button is pressed</li>
</ul>
<p>Notice that this component doesn’t do anything - for all actions it uses the callbacks passed to it —this
means that exactly the same component would easily be reused in a totally different application or could
be duplicated if we wanted to have a different search component for the book title and category.</p>
<p>Another thing to notice is that the local <tt class="docutils literal">onSearchChanged</tt> method is defined only to help us retrieve the
value of the input and use it to call the <tt class="docutils literal">onSearchChanged</tt> callback. Instead, we could just call the
passed
<tt class="docutils literal">this.props.onSearchChanged</tt> — however to do this we’d need a way to find the value of the input. This
could be done if we added a ref to the included <tt class="docutils literal">SearchPanel</tt> from the parent component, so
we’d be able to use something like
<tt class="docutils literal"><span class="pre">React.findDOMNode(this.refs.searchPanel.refs.search).value</span></tt> to find out the value of the input
(see that we use a ref to go to the searchPanel component and another ref to go to input component).</p>
<p>Both versions (getting the value directly from the child component or using the callback) could be used, however I
believe that the callback version defines a more clear interface since the parent component shouldn’t need
to know the implementation details of its children.</p>
</div>
<div class="section" id="booktablerow">
<h3><a class="toc-backref" href="#toc-entry-6"><tt class="docutils literal">BookTableRow</tt></a></h3>
<p><tt class="docutils literal">BookTableRow</tt> will render a table row by creating a simple table row that will contain the <tt class="docutils literal">title</tt>
and <tt class="docutils literal">category</tt> attributes of the passed book property and an edit link that will call the <tt class="docutils literal">handleEditClickPanel</tt>
property by passing the id of that book:</p>
<pre class="code literal-block">
var BookTableRow = React.createClass({
render: function() {
return (
<tr>
<td>{this.props.book.title}</td>
<td>{this.props.book.category}</td>
<td><a href='#' onClick={this.onClick}>Edit</a></td>
</tr>
);
},
onClick: function(id) {
this.props.handleEditClickPanel(this.props.book.id);
}
});
</pre>
<p>This component is used by <tt class="docutils literal">BookTable</tt> to render each one of the books.</p>
</div>
<div class="section" id="booktable">
<h3><a class="toc-backref" href="#toc-entry-7"><tt class="docutils literal">BookTable</tt></a></h3>
<p>This component create the left-side table using an array of <tt class="docutils literal"><span class="pre">BookTableRow``s</span> by passing it each one of the
books of the ``books</tt> array property. The <tt class="docutils literal">handleEditClickPanel</tt>
property is retrieved from the parent of the component and passed as is to the row.</p>
<pre class="code literal-block">
var BookTable = React.createClass({
render: function() {
var rows = [];
this.props.books.forEach(function(book) {
rows.push(<BookTableRow key={book.id} book={book} handleEditClickPanel={this.props.handleEditClickPanel} />);
}.bind(this));
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Edit</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
});
</pre>
<p>The key attribute is used by React to uniquely identify each component - we are using the id (primary key)
of each book. Before continuing with the other components, I’d like to explain what is the purpose of that
strange <tt class="docutils literal">bind</tt> call!</p>
</div>
<div class="section" id="interlude-the-bind-function-method">
<h3><a class="toc-backref" href="#toc-entry-8">Interlude: The <tt class="docutils literal">bind</tt> function method</a></h3>
<p><a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind">bind</a> is a method of the function javascript object, since in javascript <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function">functions are objects</a>! This
method is useful for a number of things, however here we use it to set the <tt class="docutils literal">this</tt> keyword of the
anonymous function that is passed to foreach to the <tt class="docutils literal">this</tt> keyword of the <tt class="docutils literal">render</tt> method of <tt class="docutils literal">BookTable</tt>,
which will be the current BookTable instance.</p>
<p>To make things crystal: Since we are using an anonymous function to pass to <tt class="docutils literal">forEach</tt>, this anonymous
function won’t have a <tt class="docutils literal">this</tt> keyword set to the current <tt class="docutils literal">BookTable</tt> object so we will get an error
that <tt class="docutils literal">this.props</tt> is undefined. That’s why we use bind to set <tt class="docutils literal">this</tt> to the current <tt class="docutils literal">BookTable</tt>. If
instead of using the anonymous function we had created a <tt class="docutils literal">createBookTableRow</tt> method inside the
<tt class="docutils literal">BookTable</tt> object that returned the <tt class="docutils literal">BookTableRow</tt> and passed <tt class="docutils literal">this.createBookTableRow</tt> to
<tt class="docutils literal">forEach</tt>, we wouldn’t have to use bind (notice that we’d also need to make rows a class attribute
and refer to it through <tt class="docutils literal">this.rows</tt> for this to work).</p>
</div>
<div class="section" id="bookform">
<h3><a class="toc-backref" href="#toc-entry-9"><tt class="docutils literal">BookForm</tt></a></h3>
<p><tt class="docutils literal">BookForm</tt> will create a form to either create a new book or update/delete an existing one. It has a <tt class="docutils literal">book</tt> object
property - when this object has an <tt class="docutils literal">id</tt> (so it is saved to the database) the update/delete/cancel buttons will be shown,
when it doesn’t have an id the add button will be shown.</p>
<pre class="code literal-block">
var BookForm = React.createClass({
render: function() {
return(
<form onSubmit={this.props.handleSubmitClick}>
<label forHtml='title'>Title</label><input ref='title' name='title' type='text' value={this.props.book.title} onChange={this.onChange}/>
<label forHtml='category'>Category</label>
<select ref='category' name='category' value={this.props.book.category} onChange={this.onChange} >
<option value='CRIME' >Crime</option>
<option value='HISTORY'>History</option>
<option value='HORROR'>Horror</option>
<option value='SCIFI'>SciFi</option>
</select>
<br />
<input type='submit' value={this.props.book.id?"Save (id = " +this.props.book.id+ ")":"Add"} />
{this.props.book.id?<button onClick={this.props.handleDeleteClick}>Delete</button>:null}
{this.props.book.id?<button onClick={this.props.handleCancelClick}>Cancel</button>:null}
{this.props.message?<div>{this.props.message}</div>:null}
</form>
);
},
onChange: function() {
var title = React.findDOMNode(this.refs.title).value;
var category = React.findDOMNode(this.refs.category).value;
this.props.handleChange(title, category);
}
});
</pre>
<p>As we can see, this component uses many properties — most are passed callbacks functions for various actions:</p>
<ul class="simple">
<li><tt class="docutils literal">book</tt>: This will either be a new book object (without and id) when adding one or an existing (from the database) book when updating one.</li>
<li><tt class="docutils literal">message</tt>: To display the result of the last operation (save/delete) — this is passed by the parent and probably it would be better if I had put it in a different component (and added styling etc).</li>
<li><tt class="docutils literal">handleSubmitClick</tt>: Will be called when the submit button is pressed to save the form (either by adding or updating).</li>
<li><tt class="docutils literal">handleCancelClick</tt>: Will be called when the cancel button is pressed — we decide that we want actually want to edit a book.</li>
<li><tt class="docutils literal">handleDeleteClick</tt>: Will be called when the delete button is pressed.</li>
<li><tt class="docutils literal">handleChange</tt>: Will be called whenever the title or the category of the currently edited book is changed through the local onChange method. The onChange will retrieve that values of title and category and pass them to handleChange to do the state update. As already discussed, we could retrieve the values immediately from the parent but this creates a better interface to our component.</li>
</ul>
</div>
<div class="section" id="bookpanel">
<h3><a class="toc-backref" href="#toc-entry-10"><tt class="docutils literal">BookPanel</tt></a></h3>
<p>The <tt class="docutils literal">BookPanel</tt> component will contain all other components, will keep the global state
and will also act as a central communications <span class="caps">HUB</span> between the components and the server. Because
it is rather large class, I will explain it in three parts:</p>
<div class="section" id="component-methods">
<h4><a class="toc-backref" href="#toc-entry-11">Component methods</a></h4>
<p>In the first part of <tt class="docutils literal">BookPanel</tt>, its react component methods will be presented:</p>
<pre class="code literal-block">
var BookPanel = React.createClass({
getInitialState: function() {
return {
books: [],
editingBook: {
title:"",
category:"",
},
search:"",
message:""
};
},
render: function() {
return(
<div className="row">
<div className="one-half column">
<SearchPanel
search={this.state.search}
onSearchChanged={this.onSearchChanged}
onClearSearch={this.onClearSearch}
/>
<BookTable books={this.state.books} handleEditClickPanel={this.handleEditClickPanel} />
</div>
<div className="one-half column">
<BookForm
book={this.state.editingBook}
message={this.state.message}
handleChange={this.handleChange}
handleSubmitClick={this.handleSubmitClick}
handleCancelClick={this.handleCancelClick}
handleDeleteClick={this.handleDeleteClick}
/>
</div>
</div>
);
},
componentDidMount: function() {
this.reloadBooks('');
},
// To be continued ...
</pre>
<p><tt class="docutils literal">getInitialState</tt> is called the first time the component is created or mounted (attached to an <span class="caps">HTML</span> component in the page
and should return the initial values of the state - here we return an object with empty placeholders. <tt class="docutils literal">componentDidMount</tt>
will be called <em>after</em> the component is mounted and that’s the place we should do any initializationn — here we call the
<tt class="docutils literal">reloadBooks</tt> method (with an empty search string) to just retrieve all books. Finally, the <tt class="docutils literal">render</tt> method creates a
<tt class="docutils literal">div</tt> that will contain all other components and initializes their properties with either state variables or object methods
(these are the callbacks that were used in all other components).</p>
</div>
<div class="section" id="non-ajax-object-methods">
<h4><a class="toc-backref" href="#toc-entry-12">Non-ajax object methods</a></h4>
<pre class="code literal-block">
// Continuing from above
onSearchChanged: function(query) {
if (this.promise) {
clearInterval(this.promise)
}
this.setState({
search: query
});
this.promise = setTimeout(function () {
this.reloadBooks(query);
}.bind(this), 200);
},
onClearSearch: function() {
this.setState({
search: ''
});
this.reloadBooks('');
},
handleEditClickPanel: function(id) {
var book = $.extend({}, this.state.books.filter(function(x) {
return x.id == id;
})[0] );
this.setState({
editingBook: book,
message: ''
});
},
handleChange: function(title, category) {
this.setState({
editingBook: {
title: title,
category: category,
id: this.state.editingBook.id
}
});
},
handleCancelClick: function(e) {
e.preventDefault();
this.setState({
editingBook: {}
});
},
// to be continued ...
</pre>
<p>All the above function change the <tt class="docutils literal">BookPanel</tt> state so that the properties of the child components will
also be updated:</p>
<ul class="simple">
<li><tt class="docutils literal">onSearchChanged</tt> is called when the search text is changed. The behavior here is interesting: Instead of immediately reloading the books, we create a timeout to be executed after 200 ms (also notice the usage of the <tt class="docutils literal">bind</tt> function method to allow us call the <tt class="docutils literal">reloadBooks</tt> method). If the user presses a key before these 200 ms, we cancel the previous timeout (using <tt class="docutils literal">clearInterval</tt>) and create a new one. This technique greatly reduces ajax calls to the server when the user is just typing something in the search box — we could even increase the delay to reduce even more the ajax calls (but hurt the user experience a bit since the user will notice that his search results won’t be updated immediately).</li>
<li><tt class="docutils literal">onClearSearch</tt> is called when the clear filter button is pressed and removes the search text and reloads all books.</li>
<li><tt class="docutils literal">handleEditClickPanel</tt> is called when the edit link of a <tt class="docutils literal">BookTableRow</tt> is clicked. The book with the passed <tt class="docutils literal">id</tt> will be found (using filter) and then a clone of it will be created with (<tt class="docutils literal">$.extend</tt>) and will be used to set the <tt class="docutils literal">editingBook</tt> state attribute. If instead of the clone we passed the filtered book object we’d see that when the title or category in the <tt class="docutils literal">BookForm</tt> were changed they’d also be changed in the <tt class="docutils literal">BookTableRow</tt>!</li>
<li><tt class="docutils literal">handleChange</tt> just changes the state of the currently edited book based on the values passed (it does not modify the id of the book)</li>
<li><tt class="docutils literal">handleCancelClick</tt> when the cancel editing is pressed we clear the <tt class="docutils literal">editingBook</tt> state attribute. Notice the <tt class="docutils literal">e.preventDefault()</tt> method that needs to be there in order to prevent the form from submitting since the form submitting would result in an undesirable full page reload!</li>
</ul>
</div>
<div class="section" id="ajax-object-methods">
<h4><a class="toc-backref" href="#toc-entry-13">Ajax object methods</a></h4>
<p>Finally, we need a buncch of object methods that use ajax calls to retrieve or update books:</p>
<pre class="code literal-block">
// Continuing from above
reloadBooks: function(query) {
$.ajax({
url: this.props.url+'?search='+query,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({
books: data
});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
this.setState({
message: err.toString(),
search: query
});
}.bind(this)
});
},
handleSubmitClick: function(e) {
e.preventDefault();
if(this.state.editingBook.id) {
$.ajax({
url: this.props.url+this.state.editingBook.id,
dataType: 'json',
method: 'PUT',
data:this.state.editingBook,
cache: false,
success: function(data) {
this.setState({
message: "Successfully updated book!"
});
this.reloadBooks('');
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
this.setState({
message: err.toString()
});
}.bind(this)
});
} else {
$.ajax({
url: this.props.url,
dataType: 'json',
method: 'POST',
data:this.state.editingBook,
cache: false,
success: function(data) {
this.setState({
message: "Successfully added book!"
});
this.reloadBooks('');
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
this.setState({
message: err.toString()
});
}.bind(this)
});
}
this.setState({
editingBook: {}
});
},
handleDeleteClick: function(e) {
e.preventDefault();
$.ajax({
url: this.props.url+this.state.editingBook.id,
method: 'DELETE',
cache: false,
success: function(data) {
this.setState({
message: "Successfully deleted book!",
editingBook: {}
});
this.reloadBooks('');
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
this.setState({
message: err.toString()
});
}.bind(this)
});
},
});
</pre>
<ul class="simple">
<li><tt class="docutils literal">reloadBooks</tt> will try to load the books using an ajax <span class="caps">GET</span> and pass its query parameter to filter the books (or get all books if query is an empty string). If the ajax call was successfull the state will be updated with the retrieved books and the search text (to clear the search text when we reload because of a save/edit/delete) while if there was an error the state will be updated with the error message. The books will be returned as a json array of book objects.</li>
<li><tt class="docutils literal">handleSubmitClick</tt> checks if the state’s <tt class="docutils literal">editingBook</tt> has an id and will do either a <span class="caps">POST</span> to create a new book or a <span class="caps">PUT</span> to update the existing one. Depending on the result of the operation will either reload books and clear the editingBook or set the error message.</li>
<li><tt class="docutils literal">handleDeleteClick</tt> will do a <span class="caps">DELETE</span> to delete the state’s <tt class="docutils literal">editingBook</tt> and clear it.</li>
</ul>
<p>Notice that all success and error functions above were binded to <tt class="docutils literal">this</tt> so that they could update the state of the current <tt class="docutils literal">BookPanel</tt> object.</p>
<p>After each succesfull ajax call we do a reload to the books to keep the state between server and client side consistent. We could
instead update the book array immediately before doing the ajax call - however this would increase complexity
(what should happen if an error happens at the ajax call) without improving the <span class="caps">UX</span> that much. A better solution
would be to add a loading css animation.</p>
</div>
</div>
</div>
<div class="section" id="local-state">
<h2><a class="toc-backref" href="#toc-entry-14">Local state</a></h2>
<p>One thing to keep in mind is that there should only be one place of truth for the state and
that the state should be as high in the component hierarch as possible. All changes should
be propagated to thhe child components through properties.
That’s the only way to be sure of the your data flow: When a change should happen
(either because of a user action or an asynchronous call e.g ajax or timeout) just
go to the state-holding component up in the hierarchy and change its state. Then the
changes will be propagated in a top-down fashion from that component to its children.</p>
<p>The above paragraph does not mean that the state should be contained in just a single
component! For example, let’s say that we had an <tt class="docutils literal">AuthorPanel</tt> in the same web application
and both <tt class="docutils literal">BookPanel</tt> and <tt class="docutils literal">AuthorPanel</tt> would be contained in a <tt class="docutils literal">BookAuthorPanel</tt>.
In this case, we should keep a different state object for <tt class="docutils literal">BookPanel</tt> and <tt class="docutils literal">AuthorPanel</tt>,
we wouldn’t need to combine them into a single object contained in the <tt class="docutils literal">BookAuthorPanel</tt>
since they are unrelated!</p>
<p>One decision that we took in <tt class="docutils literal">BookForm</tt> (and also to the <tt class="docutils literal">SearchPanel</tt> before) is to <em>not</em> keep a local state for the
book that is edited but move it to <tt class="docutils literal">BookPanel</tt>. This means that whenever the value of the <tt class="docutils literal">title</tt> or <tt class="docutils literal">category</tt> is changed the parent
component will be informed (through <tt class="docutils literal">handleChange</tt>) so that it will change its state and the new values will
be passed down to <tt class="docutils literal">BookForm</tt> as properties so our changes will be reflected to the inputs. To make it more
clear, when you press a letter on the <tt class="docutils literal">title</tt> input:</p>
<ul class="simple">
<li>the <tt class="docutils literal">onChange</tt> method of <tt class="docutils literal">BookForm</tt> will be called,</li>
<li>it will get the values of both <tt class="docutils literal">title</tt> and <tt class="docutils literal">category</tt> fields</li>
<li>and call <tt class="docutils literal">handleChange</tt> with these values.</li>
<li>The <tt class="docutils literal">handleChange</tt> method of <tt class="docutils literal">BookPanel</tt> will update the <tt class="docutils literal">editingBook</tt> state attribute,</li>
<li>so the <tt class="docutils literal">book</tt> property of <tt class="docutils literal">BookForm</tt> will be also updated</li>
<li>and the new value of the <tt class="docutils literal">title</tt> will be displayed (since the components will be re-rendered due to the state update)</li>
</ul>
<p>Conceptually, the above seems like a lot of work for just pressing a simple key! However, due to how react
is implemented (virtual <span class="caps">DOM</span>) it won’t actually introduce any performance problems in our application.
If nevertheless we wanted to have a local state of the currently edited book inside the <tt class="docutils literal">BookForm</tt> then we’d need to use
<tt class="docutils literal">state.book</tt> and update the state using the <tt class="docutils literal">componentWillReceiveProps</tt> method of <tt class="docutils literal">BookForm</tt>: If we
have a book to edit in the properties then copy it to the state or else just create an empty book. Also,
the <tt class="docutils literal">onChange</tt> method of the <tt class="docutils literal">BookForm</tt> won’t need to notify the parent component that there is a
state change (but only update the local state) and of course when the submit button is pressed the
current book should be passed to the parent component (to either save it or delete it) since it won’t
know the book that is currently edited.</p>
<p>So we could either have a local state object for the edited book in the <tt class="docutils literal">BookForm</tt> or
just save it as a property of the global state object in <tt class="docutils literal">BookPanel</tt> — both solutions are
correct. What wouldn’t be correct is if we had two copies of the same information in both the
states of <tt class="docutils literal">BookPanel</tt> and <tt class="docutils literal">BookForm</tt>, for example the book id that is edited.</p>
</div>
<div class="section" id="adding-form-validation">
<h2><a class="toc-backref" href="#toc-entry-15">Adding form validation</a></h2>
<p>Commenter Nitish Kumar made a nice comment about how we could do some validating to our form fields.</p>
<p>First of all, this adds to the complexity of the application so maybe it would be better to do it in
the <a class="reference external" href="https://spapas.github.io/2015/07/02/comprehensive-react-flux-tutorial-2/">flux-architectured version</a>. However, it’s not really difficult
to also have form validation using react-only:</p>
<p>Here’s what we need:</p>
<ul class="simple">
<li>A property (or properties) to pass the errors for each form field to the <tt class="docutils literal">BookForm</tt> component</li>
<li>A way to display the form errors if they exist in <tt class="docutils literal">BookForm</tt></li>
<li>Execute an error handler when the form contents change and update the state if there’s an error</li>
</ul>
<p>As an example, I’d like to check that the book title is not written in uppercase, so I will add another
attribute to the <tt class="docutils literal">book</tt> propery of <tt class="docutils literal">BookForm</tt> called <tt class="docutils literal">titleError</tt> and display it if it is defined,
something like this:</p>
<p><tt class="docutils literal"><span class="pre">{this.props.book.titleError?<div>{this.props.book.titleError}</div>:null}</span></tt></p>
<p>Now, the <tt class="docutils literal">handleChange</tt> method of <tt class="docutils literal">BookPanel</tt> should do the actual check and change the sate, so I’m
changing it like this:</p>
<pre class="code literal-block">
handleChange: function(title, category) {
var titleError = undefined;
if(title.length > 1 && title === title.toUpperCase()) {
titleError='Please don\'t use only capital letters for book titles';
}
this.setState({
editingBook: {
title: title,
titleError: titleError,
category: category,
id: this.state.editingBook.id
}
});
},
</pre>
<p>So, the titleError attribute of editingBook will be defined only if there’s an error.</p>
<p>Finally, we must remember to <em>not</em> create/update the book if there’s an error so
we add the following check to the <tt class="docutils literal">handleSubmitClick</tt> method:</p>
<pre class="code literal-block">
if(this.state.editingBook.titleError) {
this.setState({
message: "Please fix errors!"
});
return ;
}
</pre>
<p>The above could be easily generalized for checks on multiple fields. I am not really fond
of doing the form validation in the <tt class="docutils literal">BookPanel</tt> component (I’d prefer to do it on <tt class="docutils literal">BookForm</tt>
or even better on the corresponding field - if it was a component) however this would mean that
I’d need to keep a local state with the error to the <tt class="docutils literal">BookForm</tt> (please see the discussion of
the previous paragraph).</p>
<p>I’ve added a new tag named <tt class="docutils literal"><span class="pre">react-only-validation</span></tt> to the <a class="reference external" href="https://github.com/spapas/react-tutorial/">https://github.com/spapas/react-tutorial/</a>
repository that contains the changes to have the validation.</p>
</div>
<div class="section" id="conclusion-to-the-first-part">
<h2><a class="toc-backref" href="#toc-entry-16">Conclusion to the first part</a></h2>
<p>We just created a not-so-simple React single page application offering <span class="caps">CRUD</span> for an object.
This application of course could be improved by adding a pagination component to the table,
but I’m going to leave that as an excersie to the reader: To do that, I propose to create
another component named <tt class="docutils literal">TablePager</tt> that would have two properties: <tt class="docutils literal">currentPage</tt> and
a <tt class="docutils literal">changePageTo</tt> callback. The <tt class="docutils literal">currentPage</tt> would also be needed to added to the global
state. When the <tt class="docutils literal">changePageTo` is called by the ``TablePager</tt> it would update the global
<tt class="docutils literal">currentPage</tt> and will do a <tt class="docutils literal">reloadBooks</tt> that will use the <tt class="docutils literal">currentPage</tt> to fetch
the correct page.</p>
<p>For application state keeping, we have selected to store all state attributes in <tt class="docutils literal">BookPanel</tt>,
for every state changing action we create a method that we pass in the child components
so that the children can call it when the state needs to be updated.</p>
<p>As we will see <a class="reference external" href="https://spapas.github.io/2015/07/02/comprehensive-react-flux-tutorial-2/">in the next part</a>, with Flux, Facebook proposes a design pattern for
where to store the state and how to update it. We’ll see how to convert our React
application to also use Flux and find out how Flux will help us when developing
complex client-side applications!</p>
</div>
Using browserify and watchify to improve your client-side-javascript workflow2015-05-27T14:20:00+03:002015-05-27T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-05-27:/2015/05/27/using-browserify-watchify/<p class="first last">Using browserify and watchify you can greatly improve the workflow of your client-side javascript.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#the-problem" id="toc-entry-1">The problem</a></li>
<li><a class="reference internal" href="#the-solution" id="toc-entry-2">The solution</a></li>
<li><a class="reference internal" href="#installing-required-tools" id="toc-entry-3">Installing required tools</a></li>
<li><a class="reference internal" href="#starting-your-node-js-project" id="toc-entry-4">Starting your (node.js) project</a></li>
<li><a class="reference internal" href="#running-browserify-for-the-first-time" id="toc-entry-5">Running browserify for the first time</a></li>
<li><a class="reference internal" href="#using-external-libraries" id="toc-entry-6">Using external libraries</a></li>
<li><a class="reference internal" href="#introducing-watchify" id="toc-entry-7">Introducing watchify</a></li>
<li><a class="reference internal" href="#creating-your-own-modules" id="toc-entry-8">Creating your own modules</a></li>
<li><a class="reference internal" href="#uglifying-your-bundle" id="toc-entry-9">Uglifying your bundle</a></li>
<li><a class="reference internal" href="#the-client-side-javascript-workflow" id="toc-entry-10">The client-side javascript workflow</a></li>
<li><a class="reference internal" href="#conlusion" id="toc-entry-11">Conlusion</a></li>
</ul>
</div>
<div class="section" id="the-problem">
<h2><a class="toc-backref" href="#toc-entry-1">The problem</a></h2>
<p>Once upon a time, when people started using client-side code to their projects they (mainly
due to the lack of decent client-side libraries but also because of the <a class="reference external" href="http://en.wikipedia.org/wiki/Not_invented_here"><span class="caps">NIH</span> syndrome</a>)
were just adding their own code in script nodes or .js files using <tt class="docutils literal">document.getElementById</tt> to manipulate
the <span class="caps">DOM</span> (good luck checking for all possible browser-edge cases)
and <tt class="docutils literal">window.XMLHttpRequest</tt> to try doing <span class="caps">AJAX</span>.</p>
<p>After these dark-times, the
age of javascript-framework came: prototype, jquery, dojo. These were (and still are)
some great libraries (even <span class="caps">NIH</span>-sick people used them to handle browser incompatibilities):
You just downloaded the .js file with the framework, put it inside your project
static files and added it to your page with a script tag and then filled your client-side
code with funny $(‘#id’) symbols!</p>
<p>Coming to the modern age in client-side development, the number of decent libraries has greatly increased
and instead of monolithic frameworks there are different libraries for different needs. So instead of
just downloading a single file and adding the script node for that file to your <span class="caps">HTML</span>, you need to
download the required javascript files, put them all in your static files directory and then micro-manage
the script nodes for each of your pages depending on which libraries each page needs! So if you want
to use (for example) moment.js to your client-side code you need to go to <em>all</em> <span class="caps">HTML</span> pages that use that
specific client-side code and add a moment.js-script element!</p>
<p>As can be understood this leads to really ugly situations like people avoiding refactoring their code to use
external libraries, using a single-global module with all their client side code, using <span class="caps">CDN</span> to avoid
downloading the javascript libraries and of course never upgrade their javascript libraries!</p>
</div>
<div class="section" id="the-solution">
<h2><a class="toc-backref" href="#toc-entry-2">The solution</a></h2>
<p><a class="reference external" href="http://browserify.org/">browserify</a> and <a class="reference external" href="https://github.com/substack/watchify">watchify</a> are two sister tools from the server-side-javascript (node.js and friends)
world that greatly improve your javascript workflow: Using them, you no longer need to micro-manage
your script tags but instead you just declare the libraries each of your client-side modules is
using - or you can even create your own reusable modules! Also, installing (or updating) javascript
libraries is as easy as running a single command!</p>
<p>How are they working? With browserify you create a single <tt class="docutils literal">main.js</tt> for each of your <span class="caps">HTML</span>
pages and in it you declare its requirements using <a class="reference external" href="https://github.com/substack/browserify-handbook#require">require</a>. You’ll then pass your <tt class="docutils literal">main.js</tt>
through browserify and it will create a single file (e.g <tt class="docutils literal">bundle.js</tt>) that contains all the requirements
(of course each requirement could have other requirements - they’ll be automatically also
included in the resulting .js file). That’s the <em>only</em> file you need to put to the script tag of
your <span class="caps">HTML</span>! Using watchify, you can <em>watch</em> your <tt class="docutils literal">main.js</tt> for changes (the changes may also
be in the files included from main.js) and automatically generate the resulting <tt class="docutils literal">bundle.js</tt> so that
you’ll just need to hit F5 to refresh and get the new version!</p>
<p>browserify not only concatenates your javascript libraries to a single bundle but can also transform
your coffesscript, typescript, jsx etc files to javascrpt and <em>then</em> also add them to the bundle. This
is possible through a concept called transforms — there are <a class="reference external" href="https://github.com/substack/node-browserify/wiki/list-of-transforms">a lot of transforms</a> that you can use.</p>
<p>Below, I will propose a really simple and generic workflow that should cover most of your javascript needs.
I should mention that I mainly develop django apps and my development machine is running windows, however you
can easily use exactly the same workflow from any kind of server-side technology (ruby, python, javascript,
java, php or even static <span class="caps">HTML</span> pages!) or development machine (windows, linux, osx) - it’s exactly the same!</p>
</div>
<div class="section" id="installing-required-tools">
<h2><a class="toc-backref" href="#toc-entry-3">Installing required tools</a></h2>
<p>As already mentioned, you need two node.js tools. Just install them globally using npm (installing
node.js and npm is really easy - there’s even <a class="reference external" href="https://nodejs.org/download/">a package for windows</a>):</p>
<pre class="code literal-block">
npm install -g browserify watchify
</pre>
<p>The <tt class="docutils literal"><span class="pre">-g</span></tt> switch installs the packages globally so you can use the browserify and watchify commands from
your command prompt - after that entering <tt class="docutils literal">browserify</tt> or <tt class="docutils literal">watchify</tt> from your command prompt should be working.</p>
</div>
<div class="section" id="starting-your-node-js-project">
<h2><a class="toc-backref" href="#toc-entry-4">Starting your (node.js) project</a></h2>
<p>Although you may already have a project structure, in order to use browserify you’ll need to
create a node.js project (project from now on) that needs just two things:</p>
<ul class="simple">
<li>a <tt class="docutils literal">package.json</tt> that lists various options for your project</li>
<li>a <tt class="docutils literal">node_modules</tt> directory that contains the packages that your project uses</li>
</ul>
<p>To create the <tt class="docutils literal">package.json</tt> you can either copy paste a simple one or run <tt class="docutils literal">npm init</tt> inside
a folder of your project. After <tt class="docutils literal">npm init</tt> you’ll need to answer a bunch of questions and then
a <tt class="docutils literal">package.json</tt> will be created to the same folder. If you don’t want to answer these questions
(most probably you only want to use node.js for browserify - instead you wouldn’t be reading
this) then just put an empty json string <tt class="docutils literal">{}</tt> in <tt class="docutils literal">package.json</tt>.</p>
<p>I recommend adding <tt class="docutils literal">package.json</tt> to the top-level folder of your version-controlled souce-code tree -
please put this file in your version control - the <tt class="docutils literal">node_modules</tt> directory will be be created
to the same directory with <tt class="docutils literal">package.json</tt> and should be ignored by your version control.</p>
</div>
<div class="section" id="running-browserify-for-the-first-time">
<h2><a class="toc-backref" href="#toc-entry-5">Running browserify for the first time</a></h2>
<p>Not the time has come to create a <tt class="docutils literal">main.js</tt> file. This could be put anywhere you like (based on your project structure) -
I” suppose that <tt class="docutils literal">main.js</tt> is inside the <tt class="docutils literal">src/</tt> folder of your project.
Just put a <tt class="docutils literal"><span class="pre">console.log("Hello,</span> world")</tt> to the <tt class="docutils literal">main.js</tt> for now. To test that everything is working, run:</p>
<pre class="code literal-block">
browserify src/main.js
</pre>
<p>You should see some minified-js gibberish to your console (something like <tt class="docutils literal">(function <span class="pre">e(t,n,r){function</span> <span class="pre">s(o,u){if(!n[o]){if(!t[o]){var</span> a=typeof <span class="pre">...)</span></tt>
) which means that everything works fine. Now, create a <tt class="docutils literal">dist</tt> directory which would contain your bundle files and run</p>
<pre class="code literal-block">
browserify src/main.js -o dist/bundle.js
</pre>
<p>the -o switch will put the the same minified-js gibberish output to the <tt class="docutils literal">dist/bundle.js</tt> file instead of stdout.
Finally, include a script element with that file to your <span class="caps">HTML</span> and you
should see “Hello, world” to your javascript console when opening the <span class="caps">HTML</span> file!</p>
</div>
<div class="section" id="using-external-libraries">
<h2><a class="toc-backref" href="#toc-entry-6">Using external libraries</a></h2>
<p>To use a library from your main.js you need to install it and get a reference to it through require. Let’s try to use <a class="reference external" href="http://momentjs.com/">moment.js</a>:
To install the library run</p>
<pre class="code literal-block">
npm install moment --save
</pre>
<p>This will create a moment directory inside node_modules that will contain the moment.js library. It will also add a
dependency to your <tt class="docutils literal">package.json</tt> (that’s what the <tt class="docutils literal"><span class="pre">--save</span></tt> switch does), something like this:</p>
<pre class="code literal-block">
"dependencies": {
"moment": "^2.10.3"
}
</pre>
<p>Whenever you install more client-side libraries they’ll be saved there. When you want to re-install everything (for instance
when you clone your project) you can just do a</p>
<pre class="code literal-block">
npm install
</pre>
<p>and all dependencies of <tt class="docutils literal">package.json</tt> will be installed in <tt class="docutils literal">node_modules</tt> (that’s why <tt class="docutils literal">node_modules</tt> should not be tracked).</p>
<p>After you’ve installed moment.js to your project change <tt class="docutils literal">src/main.js</tt> to:</p>
<pre class="code literal-block">
moment = require('moment')
console.log(moment() );
</pre>
<p>and rerun <tt class="docutils literal">browserify src/main.js <span class="pre">-o</span> dist/bundle.js</tt>. When you reload your <span class="caps">HTML</span> you’ll see the that you are able to use
moment - all this without changing your <span class="caps">HTML</span>!!!</p>
<p>As you can understand, in order to use a library with browserify, this library must support it by having an npm package. The nice thing is that
most libraries already support it — let’s try for another example to use <a class="reference external" href="http://underscorejs.org/">underscore.js</a> and (for some reason) we need version underscore 1.7 :</p>
<pre class="code literal-block">
npm install underscore@1.7--save
</pre>
<p>you’ll se that your package.json dependencies will also contain underscore.js 1.7:</p>
<pre class="code literal-block">
{
"dependencies": {
"moment": "^2.10.3",
"underscore": "^1.7.0"
}
}
</pre>
<p>If you want to upgrade underscore to the latest version run a:</p>
<pre class="code literal-block">
npm install underscore --upgrade --save
</pre>
<p>and you’ll see that your <tt class="docutils literal">package.json</tt> will contan the latest version of underscore.js.</p>
<p>Finally, let’s change our <tt class="docutils literal">src/man.js</tt> to use underscore:</p>
<pre class="code literal-block">
moment = require('moment')
_ = require('underscore')
_([1,2,3]).map(function(x) {
console.log(x+1);
});
</pre>
<p>After you create your bundle you should se 2 3 4 in your console!</p>
</div>
<div class="section" id="introducing-watchify">
<h2><a class="toc-backref" href="#toc-entry-7">Introducing watchify</a></h2>
<p>Running browserify <em>every</em> time you change your js files to create the <tt class="docutils literal">bundle.js</tt> feels
like doing repetitive work - this is where wachify comes to the rescue; watchify is a
tool that watches your source code and dependencies and when a change is detected it will
recreate the bundle automagically!</p>
<p>To run it, you can use:</p>
<pre class="code literal-block">
watchify src/main.js -o dist/bundle.js -v
</pre>
<p>and you’ll see something like: <tt class="docutils literal">155544 bytes written to dist/bundle.js (0.57 seconds)</tt> — try
changing main.js and you’ll see that bundle.js will also be re-written!</p>
<p>Some things to keep in mind with watchify usage:</p>
<ul class="simple">
<li>The -v flag outputs the verbose text (or else you won’t se any postive messages) - I like using it to be sure that everything is ok.</li>
<li>You need to use the -o flag with watchify — you can’t output to stdout(we’ll see that this will change our workflow for production a bit later)</li>
<li>watchify takes the same parameters with browserify — so if you do any transformations with browserify you can also do them with watchify</li>
</ul>
<p>In the following, I’ll assume that you are running the <tt class="docutils literal">watchify src/main.js <span class="pre">-o</span> dist/bundle.js <span class="pre">-d</span></tt> so your bundles will
always be re-created when changes are found.</p>
</div>
<div class="section" id="creating-your-own-modules">
<h2><a class="toc-backref" href="#toc-entry-8">Creating your own modules</a></h2>
<p>Using browserify we can create our own modules and <em>require</em> them in other modules using the <tt class="docutils literal">module.exports</tt> mechanism!</p>
<p>Creating a module is really simple: In a normal javascript file either assign directly to module.exports or
include all local objects you want to be visible as an attribute to <tt class="docutils literal">module.exports</tt> — everything
else will be private to the module.</p>
<p>As an example, let’s create an <tt class="docutils literal">src/modules</tt> folder and put a file module1.js inside it, containing the following:</p>
<pre class="code literal-block">
var variable = 'variable'
var variable2 = 'variable2'
var funct = function(x) {
return x+1;
}
var funct2 = function(x) {
return x+1;
}
module.exports['variable'] = variable
module.exports['funct'] = funct
</pre>
<p>As you see, although we’ve defined a number of things in that module, only the variable and funct attributes
of module.exports will be visible when the module is used. To use the module, change main.js like this:</p>
<pre class="code literal-block">
module1 = require('./modules/module1')
console.log(module1.funct(9))
</pre>
<p>When you refresh your <span class="caps">HTML</span> you’ll see 10 in the console. So, require will return the <tt class="docutils literal">module.exports</tt> objects
of each module. It will either search in your project’s <tt class="docutils literal">node_modules</tt> (when you use just the modfule name,
for example <tt class="docutils literal">moment</tt>, or locally (when you start a path with either <tt class="docutils literal">./</tt> or <tt class="docutils literal">../</tt> — in our case we required
the module <tt class="docutils literal">module1.js</tt> from the folder <tt class="docutils literal">modules</tt>).</p>
<p>As a final example, we’ll create another module that is used by module1: Create a file named <tt class="docutils literal">module2.js</tt> inside the
<tt class="docutils literal">modules</tt> folder and the following contents:</p>
<pre class="code literal-block">
var funct = function(x) {
return x+1;
}
module.exports = funct
</pre>
<p>After that, change <tt class="docutils literal">module1.js</tt> to this:</p>
<pre class="code literal-block">
module2 = require('./module2')
var variable = 'variable'
var funct = function(x) {
return module2(x)+1;
}
module.exports['variable'] = variable
module.exports['funct'] = funct
</pre>
<p>So <tt class="docutils literal">module1</tt> will import the <tt class="docutils literal">module2</tt> module (from the same directory) and call it (since a function is assignedd to module.exports).
When you refresh your <span class="caps">HTML</span> you should see 11!</p>
</div>
<div class="section" id="uglifying-your-bundle">
<h2><a class="toc-backref" href="#toc-entry-9">Uglifying your bundle</a></h2>
<p>If had taken a look at the file size of your <tt class="docutils literal">bundle.js</tt> when you’d included moment.js or underscore.js you’d see
that the file size has been greatly increased. Take a peek at <tt class="docutils literal">bundle.js</tt> and you’ll see why: The contents of the module files
will be concatenated as they are, without any changes! This may be nice for development / debugging, however for production
we’d like our bundle.js to be minified — or uglyfied as it’s being said in the javascript world.</p>
<p>To help us with this, uglifying we’ll use <a class="reference external" href="https://www.npmjs.com/package/uglify-js">uglify-js</a>. First of all, please install it globally</p>
<pre class="code literal-block">
npm install uglify-js -g
</pre>
<p>and you’ll be able to use the <tt class="docutils literal">uglifyjs</tt> command to uglify your bundles! To use the <tt class="docutils literal">uglifyjs</tt> command for your <tt class="docutils literal">bundle.js</tt>
try this</p>
<pre class="code literal-block">
uglifyjs dist\bundle.js > dist\bundle.min.js
</pre>
<p>and you’ll see the size of the bundle.min.js greatly reduced! To achieve even better minification (and code mangling as an added
bonus) you could pass the -mc options to uglify:</p>
<pre class="code literal-block">
uglifyjs dist\bundle.js -mc > dist\bundle.min.js
</pre>
<p>and you’ll see an even smaller bundle.min.js!</p>
<p>As a final step, we can combine the output of browserify and uglify to a single command using a pipe:</p>
<pre class="code literal-block">
browserify src/main.js | uglifyjs -mc > dist/bundle.js
</pre>
<p>this will create the uglified bundle.js! Using the pipe to output to uglifyjs is not possible
with watchify since watchify cannot output to stdout — however, as we’ll see in the next section
this is not a real problem.</p>
</div>
<div class="section" id="the-client-side-javascript-workflow">
<h2><a class="toc-backref" href="#toc-entry-10">The client-side javascript workflow</a></h2>
<p>The proposed client-side javascript workflow uses two commands, one for the development and one
for creating the production bundle.</p>
<p>For the development, we’ll use watchify since we need to immediately re-create the bundle when a
javascript source file is changed and we don’t want any uglification:</p>
<pre class="code literal-block">
watchify src/main.js -o dist/bundle.js -v
</pre>
<p>For creating our production bundle, we’ll use browserify and uglify:</p>
<pre class="code literal-block">
browserify src/main.js | uglifyjs -mc warnings=false > dist/bundle.js
</pre>
<p>(i’ve added warnings=false to uglfiyjs to suppress warnings).</p>
<p>The above two commands can either be put to batch files or added to your existing workflow (for example
as <a class="reference external" href="http://www.fabfile.org/">fabric</a> commands if you use fabric). However, since we already have a javascrpt project (i.e a <tt class="docutils literal">package.json</tt>)
we can use that to run these commands. Just add a <tt class="docutils literal">scripts</tt> section to your package.json like this:</p>
<pre class="code literal-block">
{
"dependencies": {
"moment": "^2.10.3",
"underscore": "^1.8.3"
},
"scripts": {
"watch": "watchify src/main.js -o dist/bundle.js -v",
"build": "browserify src/main.js | uglifyjs -mc warnings=false > dist/bundle.js"
}
}
</pre>
<p>and you’ll be able to run <tt class="docutils literal">npm run watch</tt> to start watchifying for changes and <tt class="docutils literal">npm run build</tt> to
create your production bundle!</p>
</div>
<div class="section" id="conlusion">
<h2><a class="toc-backref" href="#toc-entry-11">Conlusion</a></h2>
<p>In the above we saw two (three if we include uglifyjs) javascript tools that will greatly improve our
javascript workflow. Using these we can easily <em>require</em> (import) external javascript libraries to
our project without any micromanagement of script tags in html files. We also can seperate our own
client-side code to self-contained modules that will only export interfaces and not pollute the global
namespace. The resulting production client-side javascript file will be output minimized and ready to
be used by the users’ browsers.</p>
<p>All the above are possible with minimal changes to our code and development workflow:</p>
<ul class="simple">
<li>create a package.json and install your dependencies</li>
<li>require the external libraries (instead of using them off the global namespace)</li>
<li>define your module’s interace through module.exports (instead of polluting the global namespace)</li>
<li>change your client javascript files to <tt class="docutils literal">bundle.js</tt></li>
<li>run <tt class="docutils literal">npm run watch</tt> when developing and <tt class="docutils literal">npm run build</tt> before deploying</li>
</ul>
</div>
Show 404 page on django when DEBUG=True2015-04-29T10:20:00+03:002015-04-29T10:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2015-04-29:/2015/04/29/django-show-404-page/<p class="first last">How to display the 404 error page on django when <span class="caps">DEBUG</span>=True</p>
<p>The default 404 error page on django can be <a class="reference external" href="https://docs.djangoproject.com/en/1.8/topics/http/views/#the-http404-exception">easily overriden</a> by adding
a template named <tt class="docutils literal">404.html</tt> to the top level directory of your templates.
However, on your development environment you’ll never be able to see this
template because when <tt class="docutils literal"><span class="caps">DEBUG</span>=True</tt> django will render the debug not found
page to help you debug your url configuration.</p>
<p>If you want to display that page in your development environment you can always
change the <span class="caps">DEBUG</span> setting to False, however there’s a better way: Add a url
pattern for django’s default 404 view - just add the following to your <tt class="docutils literal">urls.py</tt>:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">django.views.defaults</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="n">patterns</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span>
<span class="c1"># Other url patterns ...</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^404/$'</span><span class="p">,</span> <span class="n">django</span><span class="o">.</span><span class="n">views</span><span class="o">.</span><span class="n">defaults</span><span class="o">.</span><span class="n">page_not_found</span><span class="p">,</span> <span class="p">),</span>
<span class="p">)</span>
</pre></div>
<p>You’ll then be able to see your 404 page by visiting the defined <span class="caps">URL</span>!</p>
<p><strong>Upgrade for newer Django versions</strong></p>
<p>I’ve recently found out that for newer Django versions the <tt class="docutils literal">page_not_found</tt> view needs a second
parameter (beyond <tt class="docutils literal">request</tt>) named <tt class="docutils literal">exception</tt> (<a class="reference external" href="https://docs.djangoproject.com/el/2.1/ref/views/#django.views.defaults.page_not_found">see here</a>). Thus
if you just add the view to your urls as I propose you’ll get an exception. To fix it, you can do something like:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">django</span>
<span class="k">def</span> <span class="nf">custom_page_not_found</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
<span class="k">return</span> <span class="n">django</span><span class="o">.</span><span class="n">views</span><span class="o">.</span><span class="n">defaults</span><span class="o">.</span><span class="n">page_not_found</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">path</span><span class="p">(</span><span class="s2">"404/"</span><span class="p">,</span> <span class="n">custom_page_not_found</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>
<p>This creates a simple view that calls the builtin <tt class="docutils literal">page_not_found</tt> passing it <tt class="docutils literal">None</tt> as the <tt class="docutils literal">exception</tt> parameter.</p>
Calling the REST API of Pusher from python2015-02-06T12:20:00+02:002015-02-06T12:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2015-02-06:/2015/02/06/python-pusher-rest/<p class="first last">How to call the <span class="caps">REST</span> <span class="caps">API</span> of Pusher from python</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p><a class="reference external" href="https://pusher.com/">Pusher</a> is one of the best real time frameworks right now. Using it you can add real time
events in your projects without the need to configure and use <span class="caps">HTTP</span> servers that support
real-time events in your environment. I used it recently in a project and it worked really
good, having a very simple <span class="caps">API</span> and a nice interface for debugging your requests.</p>
<p>The only problem I’ve found was that the <a class="reference external" href="https://github.com/pusher/pusher_client_python">Pusher python <span class="caps">API</span></a> misses some features that
the APIs for other languages have, specifically finding out the users on a presence channel.</p>
<p>Pusher supports real-time events through the use of “channels”. Each pusher client will
subscribe to a channel and receive messages that are sent to that channel. A special kind
of channel are presence channels which keep a list of their subscribers. You can query the
<a class="reference external" href="https://pusher.com/docs/rest_api">Pusher <span class="caps">REST</span> <span class="caps">API</span></a> (or f.e the Pusher Javascript <span class="caps">API</span>) to find out the names of the users
in a presence channel - however this is <em>not</em> currently possible with the python <span class="caps">API</span>.</p>
<p>Unfortuanately, calling the Pusher <span class="caps">REST</span> <span class="caps">API</span> is <em>not</em> so easy, since it needs a complicated
singining of each request, so I’ve written this post to help developers that need to call
this <span class="caps">API</span> from python (to get the users of a presence channel or for any other method the
<span class="caps">REST</span> <span class="caps">API</span> supports).</p>
</div>
<div class="section" id="signing-the-request">
<h2>Signing the request</h2>
<p>Quoting from the <a class="reference external" href="https://pusher.com/docs/rest_api">Pusher <span class="caps">REST</span> <span class="caps">API</span></a>, to sign a request we need a signature, which:</p>
<blockquote>
<p>The signature is a <span class="caps">HMAC</span> <span class="caps">SHA256</span> hex digest. This is generated by signing a string made up of the following components concatenated with newline characters \n:</p>
<ul class="simple">
<li>The uppercase request method (e.g. <span class="caps">POST</span>)</li>
<li>The request path (e.g. /some/resource)</li>
<li>The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string. Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)</li>
</ul>
</blockquote>
<p>So, we need to create a string and then sign it using our Pusher api_key and secret. To help with this, we create a <tt class="docutils literal">Token</tt>
class which will be initialzed with out pusher key/secret and correctly sign a string:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">Token</span><span class="p">(</span><span class="nb">object</span><span class="p">,):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">secret</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
<span class="bp">self</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="n">secret</span>
<span class="k">def</span> <span class="nf">sign</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">string</span><span class="p">):</span>
<span class="k">return</span> <span class="n">hmac</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">secret</span><span class="p">,</span> <span class="n">string</span><span class="p">,</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">)</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</pre></div>
<p>It uses the <tt class="docutils literal">hmac</tt> and <tt class="docutils literal">hashlib</tt> python modules.</p>
</div>
<div class="section" id="generating-the-complete-query-string">
<h2>Generating the complete query string</h2>
<p>We can now create a function that will sign a request using an instance of the above token:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">create_signed_query_string</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">partial_path</span><span class="p">,</span> <span class="n">request_params</span><span class="p">):</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'auth_key'</span><span class="p">:</span> <span class="n">token</span><span class="o">.</span><span class="n">key</span><span class="p">,</span>
<span class="s1">'auth_timestamp'</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()),</span>
<span class="s1">'auth_version'</span><span class="p">:</span> <span class="s1">'1.0'</span>
<span class="p">}</span>
<span class="n">params</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">request_params</span><span class="p">)</span>
<span class="n">keys</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">params</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span> <span class="p">)</span>
<span class="n">params_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">keys</span><span class="p">:</span>
<span class="n">params_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="s1">'</span><span class="si">{0}</span><span class="s1">=</span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">params</span><span class="p">[</span><span class="n">k</span><span class="p">])</span> <span class="p">)</span>
<span class="n">query_string</span> <span class="o">=</span> <span class="s1">'&'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">params_list</span><span class="p">)</span>
<span class="n">sign_data</span> <span class="o">=</span> <span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s1">'GET'</span><span class="p">,</span> <span class="n">partial_path</span><span class="p">,</span> <span class="n">query_string</span><span class="p">])</span>
<span class="n">query_string</span> <span class="o">+=</span> <span class="s1">'&auth_signature='</span> <span class="o">+</span> <span class="n">token</span><span class="o">.</span><span class="n">sign</span><span class="p">(</span><span class="n">sign_data</span><span class="p">);</span>
<span class="k">return</span> <span class="n">query_string</span>
</pre></div>
<p><tt class="docutils literal">create_signed_query_string</tt> receives an instance of a <tt class="docutils literal">Token</tt>, the path that we want to request
without the server part (for example <tt class="docutils literal"><span class="pre">/apps/33/users/my-channel</span></tt>) and a dictionary of
request parameters. It then adds three extra fields to the request parameters (<tt class="docutils literal">auth_key, auth_timestamp, auth_version</tt>)
and creates a list of these parameters in the <tt class="docutils literal">key=value</tt> form, where the keys are alphabetically sorted.
After that it joins the above <tt class="docutils literal">key=value</tt> parameters using <tt class="docutils literal">&</tt> to create the <tt class="docutils literal">query_string</tt> and then it creates the string to be signed (<tt class="docutils literal">sign_data</tt>)
by concatenating the <span class="caps">HTTP</span> methdo (<span class="caps">GET</span>) with the path and the <tt class="docutils literal">query_string</tt>. Finally, it appends the signing result as an extra
query parameter named (<tt class="docutils literal">auth_signature</tt>).</p>
</div>
<div class="section" id="requesting-the-users-of-the-presence-channel">
<h2>Requesting the users of the presence channel</h2>
<p>The <tt class="docutils literal">create_signed_query_string</tt> can now be used to get the users of a presence channel like this:</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_users</span><span class="p">(</span><span class="n">app_id</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">secret</span><span class="p">,</span> <span class="n">channel</span><span class="p">):</span>
<span class="n">partial_path</span> <span class="o">=</span> <span class="s1">'/apps/</span><span class="si">{0}</span><span class="s1">/channels/</span><span class="si">{1}</span><span class="s1">/users'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">app_id</span><span class="p">,</span> <span class="n">channel</span><span class="p">)</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">Token</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">secret</span><span class="p">)</span>
<span class="n">qs</span> <span class="o">=</span> <span class="n">create_signed_query_string</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">partial_path</span><span class="p">,</span> <span class="p">{})</span>
<span class="n">full_path</span> <span class="o">=</span> <span class="s1">'http://api.pusherapp.com/</span><span class="si">{0}</span><span class="s1">?</span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">partial_path</span><span class="p">,</span> <span class="n">qs</span><span class="p">)</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">full_path</span><span class="p">)</span>
<span class="k">return</span> <span class="n">r</span><span class="o">.</span><span class="n">text</span>
</pre></div>
<p>The <tt class="docutils literal">get_users</tt> function will generate the path of the pusher <span class="caps">REST</span> <span class="caps">API</span> (using
our pusher app_id and channel name) and initialize a signing <tt class="docutils literal">Token</tt> using
the pusher key and secret. It will then pass the previous to <tt class="docutils literal">create_signed_query_string</tt>
to generate the complete <tt class="docutils literal">query_string</tt> and generate the <tt class="docutils literal">full_path</tt> to which
a simple <span class="caps">HTTP</span> <span class="caps">GET</span> request is issued. The result will be a <span class="caps">JSON</span> list of the users in the
presence channel.</p>
</div>
<div class="section" id="complete-example">
<h2>Complete example</h2>
<p>A complete example of getting the presence users of a channel is the following:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">hashlib</span>
<span class="kn">import</span> <span class="nn">hmac</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="n">app_id</span> <span class="o">=</span> <span class="s1">'pusher_app_id'</span>
<span class="n">key</span> <span class="o">=</span> <span class="s1">'pusher_key'</span>
<span class="n">secret</span> <span class="o">=</span> <span class="s1">'pusher_secret'</span>
<span class="n">channel</span> <span class="o">=</span> <span class="s1">'pusher_presence_channel'</span>
<span class="k">class</span> <span class="nc">Token</span><span class="p">(</span><span class="nb">object</span><span class="p">,):</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">secret</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
<span class="bp">self</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="n">secret</span>
<span class="k">def</span> <span class="nf">sign</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">string</span><span class="p">):</span>
<span class="k">return</span> <span class="n">hmac</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">secret</span><span class="p">,</span> <span class="n">string</span><span class="p">,</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">)</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">create_signed_query_string</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">partial_path</span><span class="p">,</span> <span class="n">method</span><span class="p">,</span> <span class="n">request_params</span><span class="p">):</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'auth_key'</span><span class="p">:</span> <span class="n">token</span><span class="o">.</span><span class="n">key</span><span class="p">,</span>
<span class="s1">'auth_timestamp'</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()),</span>
<span class="s1">'auth_version'</span><span class="p">:</span> <span class="s1">'1.0'</span>
<span class="p">}</span>
<span class="n">params</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">request_params</span><span class="p">)</span>
<span class="n">keys</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">params</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span> <span class="p">)</span>
<span class="n">params_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">keys</span><span class="p">:</span>
<span class="n">params_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span> <span class="s1">'</span><span class="si">{0}</span><span class="s1">=</span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">params</span><span class="p">[</span><span class="n">k</span><span class="p">])</span> <span class="p">)</span>
<span class="n">query_string</span> <span class="o">=</span> <span class="s1">'&'</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">params_list</span><span class="p">)</span>
<span class="n">sign_data</span> <span class="o">=</span> <span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">method</span><span class="p">,</span> <span class="n">partial_path</span><span class="p">,</span> <span class="n">query_string</span><span class="p">])</span>
<span class="n">query_string</span> <span class="o">+=</span> <span class="s1">'&auth_signature='</span> <span class="o">+</span> <span class="n">token</span><span class="o">.</span><span class="n">sign</span><span class="p">(</span><span class="n">sign_data</span><span class="p">);</span>
<span class="k">return</span> <span class="n">query_string</span>
<span class="k">def</span> <span class="nf">get_users</span><span class="p">(</span><span class="n">channel</span><span class="p">):</span>
<span class="n">partial_path</span> <span class="o">=</span> <span class="s1">'/apps/</span><span class="si">{0}</span><span class="s1">/channels/</span><span class="si">{1}</span><span class="s1">/users'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">app_id</span><span class="p">,</span> <span class="n">channel</span><span class="p">)</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">Token</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">secret</span><span class="p">)</span>
<span class="n">qs</span> <span class="o">=</span> <span class="n">create_signed_query_string</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">partial_path</span><span class="p">,</span> <span class="s1">'GET'</span> <span class="p">{})</span>
<span class="n">full_path</span> <span class="o">=</span> <span class="s1">'http://api.pusherapp.com/</span><span class="si">{0}</span><span class="s1">?</span><span class="si">{1}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">partial_path</span><span class="p">,</span> <span class="n">qs</span><span class="p">)</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">full_path</span><span class="p">)</span>
<span class="k">return</span> <span class="n">r</span><span class="o">.</span><span class="n">text</span>
<span class="nb">print</span> <span class="n">get_users</span><span class="p">(</span><span class="n">channel</span><span class="p">)</span>
</pre></div>
</div>
<div class="section" id="conclusion">
<h2>Conclusion</h2>
<p>With the above we are able to not only easily get the users of a Pusher presence
channel in python but to also call any method we want from the Pusher <span class="caps">REST</span> <span class="caps">API</span> by implementing a function
similar to <tt class="docutils literal">get_users</tt>.</p>
</div>
Asynchronous tasks in django with django-rq2015-01-27T14:20:00+02:002015-01-27T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2015-01-27:/2015/01/27/async-tasks-with-django-rq/<p class="first last">Using django-rq to add queuing jobs (asynchronous tasks) and scheduling (cron-like) capabilities to a django project.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#job-queues-in-python" id="toc-entry-2">Job queues in python</a></li>
<li><a class="reference internal" href="#django-test-rq" id="toc-entry-3">django-test-rq</a><ul>
<li><a class="reference internal" href="#models-py" id="toc-entry-4">models.py</a></li>
<li><a class="reference internal" href="#forms-py" id="toc-entry-5">forms.py</a></li>
<li><a class="reference internal" href="#views-py" id="toc-entry-6">views.py</a></li>
<li><a class="reference internal" href="#tasks-py" id="toc-entry-7">tasks.py</a></li>
<li><a class="reference internal" href="#settings-py" id="toc-entry-8">settings.py</a></li>
<li><a class="reference internal" href="#running-the-project" id="toc-entry-9">Running the project</a></li>
</ul>
</li>
<li><a class="reference internal" href="#rqworker-and-rqscheduler" id="toc-entry-10">rqworker and rqscheduler</a><ul>
<li><a class="reference internal" href="#for-development" id="toc-entry-11">For development</a></li>
<li><a class="reference internal" href="#for-production" id="toc-entry-12">For production</a></li>
</ul>
</li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-13">Conclusion</a></li>
</ul>
</div>
<p><strong>Update 03/05/2022</strong>: The github project has been updated to work with latest version of Django (4.0.4) and Python (3.10): <a class="reference external" href="https://github.com/spapas/django-test-rq">https://github.com/spapas/django-test-rq</a></p>
<p><strong>Update 01/09/15</strong>: I’ve written a new post about rq and django with some
<a class="reference external" href="https://spapas.github.io/2015/09/01/django-rq-redux/">more advanced techniques</a> !</p>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>Job queuing (asynchronous tasks) is a common requirement for non-trivial django projects. Whenever an operation
can take more than half a second it should be put to a job queue in order to be run asynchronously by a
seperate worker. This is really important since the response to a user request needs to be immediate
or else the users will experience laggy behavior and start complaining!
Even for fairly quick tasks (like sending email through an <span class="caps">SMTP</span> server) you need to use an asynchronous task
if you care about your users since
the time required for such a task is not really limited.</p>
<p>Using job queues is involved not only for the developers of the application (who need to create the
asynchronous tasks and give feedback to the users when the’ve finished since they can’t use the normal
<span class="caps">HTTP</span> response) and but also for the administrators, since, in order to support job queues at least two
more componets will be needed:</p>
<ul class="simple">
<li>One job queue that will store the jobs to be executed next in a first in first queue. This could be the normal database of the project however it’s not recommended for performance reasons and most of thetimes it is a specific component called “Message Broker”</li>
<li>One (or more) workers that will monitor the job queue and when there is work to do they will dequeue and execute it</li>
</ul>
<p>These can all run in the same server but if it gets saturated they can easily be seperated (even more work for administrators).</p>
<p>Beyond job queuing, another relative requirement for many projects is to schedule a task to be run in the future
(similar to the <tt class="docutils literal">at</tt> unix command) or at specific time intervals (similar to the <tt class="docutils literal">cron</tt> unix command). For
instance, if a user is registered today we may need to check after one or two days if he’s logged in and used our application -
if he hasn’t then probably he’s having problems and we can call him to help him. Also, we could check every night
to see if any users that have registered to our application don’t have activated their account through email activation
and delete these accounts. Scheduled tasks should be also run by the workers mentioned above.</p>
</div>
<div class="section" id="job-queues-in-python">
<h2><a class="toc-backref" href="#toc-entry-2">Job queues in python</a></h2>
<p>The most known application for using job queues in python is <a class="reference external" href="http://www.celeryproject.org/">celery</a> which is a really great project that supports
many brokers, integrates nicely
with python/django (but can be used even with other languages) and has
many more features (most of them are only useful on really big, enterprise projects). I’ve already used
it in a previous application, however, because celery is really complex I found it rather difficult to
configure it successfully and I never was perfectly sure that my asynchronous task would actually work or
that I’d used the correct configuration for my needs!</p>
<p>Celery also has <a class="reference external" href="http://celery.readthedocs.org/en/latest/faq.html#does-celery-have-many-dependencies">many dependencies</a> in order to be able to talk with the different broker backends it supports,
improve multithreading support etc. They may be required in enterprise apps but not for most Django web based projects.</p>
<p>So, for small-to-average projects I recommend using a different asynchronous task solution instead of celery, particularly
(as you’ve already guessed from the title of this post) <a class="reference external" href="http://python-rq.org/"><span class="caps">RQ</span></a>. <span class="caps">RQ</span> is simpler than celery, it integrates great with django
using the excellent <a class="reference external" href="https://github.com/ui/django-rq">django-rq</a> package and doesn’t actually have any more dependencies beyond redis support which is
used as a broker (however most modern django projects already use redis for their caching needs as an alternative
to memcached).</p>
<p>It even supports supports job scheduling through the <a class="reference external" href="https://github.com/ui/rq-scheduler">rq-scheduler</a> package (celery also supports
job scheduling through celery beat): Run a different process (scheduler) that polls the job
scheduling queue for any jobs that need to be run because of scheduling and if yes put them to
the normal job queue.</p>
<p>Although <span class="caps">RQ</span> and frieds are really easy to use (and have nice documentation) I wasn’t able to find
a <em>complete</em> example of using it with django, so I’ve implemented one
(found at <a class="reference external" href="https://github.com/spapas/django-test-rq">https://github.com/spapas/django-test-rq</a> — since I’ve updated this project a bit
with new stuff,
please checkout tag django-test-rq-simple <tt class="docutils literal">git checkout <span class="pre">django-test-rq-simple</span></tt>) mainly for my own testing
purposes. To help others that want to also use <span class="caps">RQ</span> in their project but don’t know from where
to start, I’ll present it in the following paragraphs, along with some comments on
how to actually use <span class="caps">RQ</span> in your production environment.</p>
</div>
<div class="section" id="django-test-rq">
<h2><a class="toc-backref" href="#toc-entry-3">django-test-rq</a></h2>
<p>This is a simple django project that can be used to asynchronously
run and schedule jobs and examine their behavior. The job to be scheduled just downloads a provided
<span class="caps">URL</span> and counts its length. There is only one django application (tasks) that contains two views, one
to display existing tasks and create new ones and one to display some info for the jobs.</p>
<div class="section" id="models-py">
<h3><a class="toc-backref" href="#toc-entry-4">models.py</a></h3>
<p>Two models (<tt class="docutils literal">Task</tt> and <tt class="docutils literal">ScheduledTask</tt>) for saving individual tasks and
scheduled tasks and one model (<tt class="docutils literal">ScheduledTaskInstance</tt>) to save scheduled
instances of each scheduled task.</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">from</span> <span class="nn">rq</span> <span class="kn">import</span> <span class="n">get_current_job</span>
<span class="k">class</span> <span class="nc">Task</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># A model to save information about an asynchronous task</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="n">job_id</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ScheduledTask</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># A model to save information about a scheduled task</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="c1"># A scheduled task has a common job id for all its occurences</span>
<span class="n">job_id</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ScheduledTaskInstance</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># A model to save information about instances of a scheduled task</span>
<span class="n">scheduled_task</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="s1">'ScheduledTask'</span><span class="p">)</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre></div>
</div>
<div class="section" id="forms-py">
<h3><a class="toc-backref" href="#toc-entry-5">forms.py</a></h3>
<p>A very simple form to create a new task.</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span>
<span class="k">class</span> <span class="nc">TaskForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="w"> </span><span class="sd">""" A simple form to read a url from the user in order to find out its length</span>
<span class="sd"> and either run it asynchronously or schedule it schedule_times times,</span>
<span class="sd"> every schedule_interval seconds.</span>
<span class="sd"> """</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s1">'URL'</span><span class="p">,</span> <span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">help_text</span><span class="o">=</span><span class="s1">'Enter a url (starting with http/https) to start a job that will download it and count its words'</span> <span class="p">)</span>
<span class="n">schedule_times</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">help_text</span><span class="o">=</span><span class="s1">'How many times to run this job. Leave empty or 0 to run it only once.'</span><span class="p">)</span>
<span class="n">schedule_interval</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">IntegerField</span><span class="p">(</span><span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">help_text</span><span class="o">=</span><span class="s1">'How much time (in seconds) between runs of the job. Leave empty to run it only once.'</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">data</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">TaskForm</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">clean</span><span class="p">()</span>
<span class="n">schedule_times</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'schedule_times'</span><span class="p">)</span>
<span class="n">schedule_interval</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'schedule_interval'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">schedule_times</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">schedule_interval</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">schedule_times</span> <span class="ow">and</span> <span class="n">schedule_interval</span><span class="p">:</span>
<span class="n">msg</span> <span class="o">=</span> <span class="s1">'Please fill both schedule_times and schedule_interval to schedule a job or leave them both empty'</span>
<span class="bp">self</span><span class="o">.</span><span class="n">add_error</span><span class="p">(</span><span class="s1">'schedule_times'</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">add_error</span><span class="p">(</span><span class="s1">'schedule_interval'</span><span class="p">,</span> <span class="n">msg</span><span class="p">)</span>
</pre></div>
</div>
<div class="section" id="views-py">
<h3><a class="toc-backref" href="#toc-entry-6">views.py</a></h3>
<p>This is actually very simple if you’re familiar with Class Based Views. Two CBVs
are defined, one for the Task form + Task display and another for the Job display.</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.views.generic.edit</span> <span class="kn">import</span> <span class="n">FormView</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">TemplateView</span>
<span class="kn">from</span> <span class="nn">forms</span> <span class="kn">import</span> <span class="n">TaskForm</span>
<span class="kn">from</span> <span class="nn">tasks</span> <span class="kn">import</span> <span class="n">get_url_words</span><span class="p">,</span> <span class="n">scheduled_get_url_words</span>
<span class="kn">from</span> <span class="nn">models</span> <span class="kn">import</span> <span class="n">Task</span><span class="p">,</span><span class="n">ScheduledTask</span>
<span class="kn">from</span> <span class="nn">rq.job</span> <span class="kn">import</span> <span class="n">Job</span>
<span class="kn">import</span> <span class="nn">django_rq</span>
<span class="kn">import</span> <span class="nn">datetime</span>
<span class="k">class</span> <span class="nc">TasksHomeFormView</span><span class="p">(</span><span class="n">FormView</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""</span>
<span class="sd"> A class that displays a form to read a url to read its contents and if the job</span>
<span class="sd"> is to be scheduled or not and information about all the tasks and scheduled tasks.</span>
<span class="sd"> When the form is submitted, the task will be either scheduled based on the</span>
<span class="sd"> parameters of the form or will be just executed asynchronously immediately.</span>
<span class="sd"> """</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">TaskForm</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'tasks_home.html'</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="s1">'/'</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="p">[</span><span class="s1">'url'</span><span class="p">]</span>
<span class="n">schedule_times</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'schedule_times'</span><span class="p">)</span>
<span class="n">schedule_interval</span> <span class="o">=</span> <span class="n">form</span><span class="o">.</span><span class="n">cleaned_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'schedule_interval'</span><span class="p">)</span>
<span class="k">if</span> <span class="n">schedule_times</span> <span class="ow">and</span> <span class="n">schedule_interval</span><span class="p">:</span>
<span class="c1"># Schedule the job with the form parameters</span>
<span class="n">scheduler</span> <span class="o">=</span> <span class="n">django_rq</span><span class="o">.</span><span class="n">get_scheduler</span><span class="p">(</span><span class="s1">'default'</span><span class="p">)</span>
<span class="n">job</span> <span class="o">=</span> <span class="n">scheduler</span><span class="o">.</span><span class="n">schedule</span><span class="p">(</span>
<span class="n">scheduled_time</span><span class="o">=</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(),</span>
<span class="n">func</span><span class="o">=</span><span class="n">scheduled_get_url_words</span><span class="p">,</span>
<span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="n">url</span><span class="p">],</span>
<span class="n">interval</span><span class="o">=</span><span class="n">schedule_interval</span><span class="p">,</span>
<span class="n">repeat</span><span class="o">=</span><span class="n">schedule_times</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># Just execute the job asynchronously</span>
<span class="n">get_url_words</span><span class="o">.</span><span class="n">delay</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">TasksHomeFormView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'tasks'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'-created_on'</span><span class="p">)</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'scheduled_tasks'</span><span class="p">]</span> <span class="o">=</span> <span class="n">ScheduledTask</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">all</span><span class="p">()</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">'-created_on'</span><span class="p">)</span>
<span class="k">return</span> <span class="n">ctx</span>
<span class="k">class</span> <span class="nc">JobTemplateView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""</span>
<span class="sd"> A simple template view that gets a job id as a kwarg parameter</span>
<span class="sd"> and tries to fetch that job from RQ. It will then print all attributes</span>
<span class="sd"> of that object using __dict__.</span>
<span class="sd"> """</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'job.html'</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">ctx</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">JobTemplateView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">redis_conn</span> <span class="o">=</span> <span class="n">django_rq</span><span class="o">.</span><span class="n">get_connection</span><span class="p">(</span><span class="s1">'default'</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">job</span> <span class="o">=</span> <span class="n">Job</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">kwargs</span><span class="p">[</span><span class="s1">'job'</span><span class="p">],</span> <span class="n">connection</span><span class="o">=</span><span class="n">redis_conn</span><span class="p">)</span>
<span class="n">job</span> <span class="o">=</span> <span class="n">job</span><span class="o">.</span><span class="vm">__dict__</span>
<span class="k">except</span><span class="p">:</span>
<span class="n">job</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">ctx</span><span class="p">[</span><span class="s1">'job'</span><span class="p">]</span> <span class="o">=</span> <span class="n">job</span>
<span class="k">return</span> <span class="n">ctx</span>
</pre></div>
</div>
<div class="section" id="tasks-py">
<h3><a class="toc-backref" href="#toc-entry-7">tasks.py</a></h3>
<p>Here two jobs are defined: One to be used for simple asynchronous tasks and the
other to be used for scheduled asynchronous tasks (since for asynchronous tasks
we wanted to group their runs per job id).</p>
<p>The <tt class="docutils literal">@job</tt> decorator will add the <tt class="docutils literal">delay()</tt> method (used in <tt class="docutils literal">views.py</tt>) to
the function. It’s not really required for <tt class="docutils literal">scheduled_get_url_words</tt> since
it’s called through the <tt class="docutils literal">scheduled.schedule</tt>.</p>
<p>When a task is finished, it can return a value (like we do in <tt class="docutils literal">return task.result</tt>)
which will be saved for a limited amount of time (500 seconds by default - could be
even saved for ever) to redis.
This may be useful in some cases, however, I think that for normal web applications it’s
not that useful, and since here we use normal django models
for each task, we can save it to that model’s instance instead.</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">requests</span>
<span class="kn">from</span> <span class="nn">models</span> <span class="kn">import</span> <span class="n">Task</span><span class="p">,</span> <span class="n">ScheduledTask</span><span class="p">,</span> <span class="n">ScheduledTaskInstance</span>
<span class="kn">from</span> <span class="nn">rq</span> <span class="kn">import</span> <span class="n">get_current_job</span>
<span class="kn">from</span> <span class="nn">django_rq</span> <span class="kn">import</span> <span class="n">job</span>
<span class="nd">@job</span>
<span class="k">def</span> <span class="nf">get_url_words</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
<span class="c1"># This creates a Task instance to save the job instance and job result</span>
<span class="n">job</span> <span class="o">=</span> <span class="n">get_current_job</span><span class="p">()</span>
<span class="n">task</span> <span class="o">=</span> <span class="n">Task</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
<span class="n">job_id</span><span class="o">=</span><span class="n">job</span><span class="o">.</span><span class="n">get_id</span><span class="p">(),</span>
<span class="n">name</span><span class="o">=</span><span class="n">url</span>
<span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">task</span><span class="o">.</span><span class="n">result</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
<span class="n">task</span><span class="o">.</span><span class="n">save</span><span class="p">()</span>
<span class="k">return</span> <span class="n">task</span><span class="o">.</span><span class="n">result</span>
<span class="nd">@job</span>
<span class="k">def</span> <span class="nf">scheduled_get_url_words</span><span class="p">(</span><span class="n">url</span><span class="p">):</span>
<span class="w"> </span><span class="sd">"""</span>
<span class="sd"> This creates a ScheduledTask instance for each group of</span>
<span class="sd"> scheduled task - each time this scheduled task is run</span>
<span class="sd"> a new instance of ScheduledTaskInstance will be created</span>
<span class="sd"> """</span>
<span class="n">job</span> <span class="o">=</span> <span class="n">get_current_job</span><span class="p">()</span>
<span class="n">task</span><span class="p">,</span> <span class="n">created</span> <span class="o">=</span> <span class="n">ScheduledTask</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">get_or_create</span><span class="p">(</span>
<span class="n">job_id</span><span class="o">=</span><span class="n">job</span><span class="o">.</span><span class="n">get_id</span><span class="p">(),</span>
<span class="n">name</span><span class="o">=</span><span class="n">url</span>
<span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="n">response_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
<span class="n">ScheduledTaskInstance</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
<span class="n">scheduled_task</span><span class="o">=</span><span class="n">task</span><span class="p">,</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">response_len</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">response_len</span>
</pre></div>
</div>
<div class="section" id="settings-py">
<h3><a class="toc-backref" href="#toc-entry-8">settings.py</a></h3>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">os</span>
<span class="n">BASE_DIR</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="vm">__file__</span><span class="p">))</span>
<span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="s1">'123'</span>
<span class="n">DEBUG</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">TEMPLATE_DEBUG</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">(</span>
<span class="s1">'django.contrib.admin'</span><span class="p">,</span>
<span class="s1">'django.contrib.auth'</span><span class="p">,</span>
<span class="s1">'django.contrib.contenttypes'</span><span class="p">,</span>
<span class="s1">'django.contrib.sessions'</span><span class="p">,</span>
<span class="s1">'django.contrib.messages'</span><span class="p">,</span>
<span class="s1">'django.contrib.staticfiles'</span><span class="p">,</span>
<span class="s1">'django_extensions'</span><span class="p">,</span>
<span class="s1">'django_rq'</span><span class="p">,</span>
<span class="s1">'tasks'</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">MIDDLEWARE_CLASSES</span> <span class="o">=</span> <span class="p">(</span>
<span class="s1">'django.contrib.sessions.middleware.SessionMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.common.CommonMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.csrf.CsrfViewMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.auth.middleware.AuthenticationMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.auth.middleware.SessionAuthenticationMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.messages.middleware.MessageMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.clickjacking.XFrameOptionsMiddleware'</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">ROOT_URLCONF</span> <span class="o">=</span> <span class="s1">'django_test_rq.urls'</span>
<span class="n">WSGI_APPLICATION</span> <span class="o">=</span> <span class="s1">'django_test_rq.wsgi.application'</span>
<span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'ENGINE'</span><span class="p">:</span> <span class="s1">'django.db.backends.sqlite3'</span><span class="p">,</span>
<span class="s1">'NAME'</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">BASE_DIR</span><span class="p">,</span> <span class="s1">'db.sqlite3'</span><span class="p">),</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">LANGUAGE_CODE</span> <span class="o">=</span> <span class="s1">'en-us'</span>
<span class="n">TIME_ZONE</span> <span class="o">=</span> <span class="s1">'UTC'</span>
<span class="n">USE_I18N</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">USE_L10N</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">USE_TZ</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s1">'/static/'</span>
<span class="c1"># Use redis for caches</span>
<span class="n">CACHES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"default"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"BACKEND"</span><span class="p">:</span> <span class="s2">"django_redis.cache.RedisCache"</span><span class="p">,</span>
<span class="s2">"LOCATION"</span><span class="p">:</span> <span class="s2">"redis://127.0.0.1:6379/0"</span><span class="p">,</span>
<span class="s2">"OPTIONS"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"CLIENT_CLASS"</span><span class="p">:</span> <span class="s2">"django_redis.client.DefaultClient"</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1"># Use the same redis as with caches for RQ</span>
<span class="n">RQ_QUEUES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'USE_REDIS_CACHE'</span><span class="p">:</span> <span class="s1">'default'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="n">SESSION_ENGINE</span> <span class="o">=</span> <span class="s2">"django.contrib.sessions.backends.cache"</span>
<span class="n">SESSION_CACHE_ALIAS</span> <span class="o">=</span> <span class="s2">"default"</span>
<span class="n">RQ_SHOW_ADMIN_LINK</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># Add a logger for rq_scheduler in order to display when jobs are queueud</span>
<span class="n">LOGGING</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'version'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="s1">'disable_existing_loggers'</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
<span class="s1">'formatters'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'simple'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'format'</span><span class="p">:</span> <span class="s1">'</span><span class="si">%(asctime)s</span><span class="s1"> </span><span class="si">%(levelname)s</span><span class="s1"> </span><span class="si">%(message)s</span><span class="s1">'</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'console'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="s1">'class'</span><span class="p">:</span> <span class="s1">'logging.StreamHandler'</span><span class="p">,</span>
<span class="s1">'formatter'</span><span class="p">:</span> <span class="s1">'simple'</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'loggers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'django.request'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'console'</span><span class="p">],</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="s1">'propagate'</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="p">},</span>
<span class="s1">'rq_scheduler'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'console'</span><span class="p">],</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="s1">'propagate'</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">}</span>
</pre></div>
<p>By default, rq_scheduler won’t log anything so we won’t be able to see
any output when new instances of each scheduled task are queued for execution.
That’s why we’ve overriden the <span class="caps">LOGGING</span> setting in order to actually log
rq_scheduler output to the console.</p>
</div>
<div class="section" id="running-the-project">
<h3><a class="toc-backref" href="#toc-entry-9">Running the project</a></h3>
<p>I recommend using <a class="reference external" href="https://www.vagrantup.com/">Vagrant</a> to start a stock ubuntu/trusty32 box. After that, install redis, virtualenv and virtualenvwrapper
and create/activate a virtualenv named <tt class="docutils literal">rq</tt>. You can go to the home directory of <tt class="docutils literal"><span class="pre">django-test-rq</span></tt>
and install requirements through <tt class="docutils literal">pip install requirements.txt</tt> and create the database tables with
<tt class="docutils literal">python manage.py migrate</tt>. Finally you may run the project with <tt class="docutils literal">python manage.py runserver_plus</tt>.</p>
</div>
</div>
<div class="section" id="rqworker-and-rqscheduler">
<h2><a class="toc-backref" href="#toc-entry-10">rqworker and rqscheduler</a></h2>
<p>Before scheduling any tasks we need to run two more processes:</p>
<ul class="simple">
<li>rqworker: This is a worker that dequeues jobs from the queue and executes them. We could run more than one onstance of this job if we need it.</li>
<li>rqscheduler: This is a process that runs every one minute and checks if there are scheduled jobs that have to be executed. If yes, it will add them to the queue in order to be executed by a worker.</li>
</ul>
<div class="section" id="for-development">
<h3><a class="toc-backref" href="#toc-entry-11">For development</a></h3>
<p>If you want to run rqworker and rqscheduler for your development environment you can just do it with
running <tt class="docutils literal">python manage.py rqworker</tt> and <tt class="docutils literal">python mange.py rqscheduler</tt> through screen/tmux. If everything
is allright you should see tasks being added to the queue and scheduled (you may need to refresh the
homepage before seeing everything since a task may be executed after the response is created).</p>
<p>Also, keep in mind that rqscheduler runs once every minute by default so you may need to wait up to
minute to see a <tt class="docutils literal">ScheduledTask</tt> instance. Also, this means that you can’t run more than one scheduled
task instance per minute.</p>
</div>
<div class="section" id="for-production">
<h3><a class="toc-backref" href="#toc-entry-12">For production</a></h3>
<p>Trying to create daemons through screen is not
sufficient for a production envornment since we’d like to actually have logging, monitoring and of course
automatically start rqworker and rqscheduler when the server boots.</p>
<p>For this, I recommend using the <a class="reference external" href="http://supervisord.org/">supervisord</a> tool which
can be used to monitor and control a number of processes. There are other similar tools, however I’ve
found supervisord the easier to use.</p>
<p>In order to monitor/control a process through supervisord you need to add a <tt class="docutils literal">[program:progrname]</tt> section in
supervisord’s configuration and pass a number of parameters. The <tt class="docutils literal">progname</tt> is the name of the monitoring
process. Here’s how rqworker can be configured using supervisord:</p>
<pre class="code literal-block">
[program:rqworker]
command=python manage.py rqworker
directory=/vagrant/progr/py/rq/django-test-rq
environment=PATH="/home/vagrant/.virtualenvs/rq/bin"
user=vagrant
</pre>
<p>The options used will chdir to <tt class="docutils literal">directory</tt> and execute <tt class="docutils literal">command</tt> as <tt class="docutils literal">user</tt>. The <tt class="docutils literal">environment</tt>
option can be used to set envirotnment variables - here we set <tt class="docutils literal"><span class="caps">PATH</span></tt> in order to use a specific
virtual environment. This will allow you to monitor rqworker through supervisord and log its
output to a file in <tt class="docutils literal">/var/log/supervisor</tt> (by default). A similar entry needs to be added for
rqscheduler of course. If everything has been configured correctly, when you reload the supervisord
settings you can run <tt class="docutils literal">sudo /usr/bin/supervisorctl</tt> and should see something like</p>
<pre class="code literal-block">
rqscheduler RUNNING pid 1561, uptime 0:00:03
rqworker RUNNING pid 1562, uptime 0:00:03
</pre>
<p>Also, tho log files should contain some debug info.</p>
</div>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-13">Conclusion</a></h2>
<p>Although using job queues makes it more difficult for the developer and adds at least one
(and probably more) points of failure to a project (the workers, the broker etc) their
usage, even for very simple projects is unavoidable.</p>
<p>Unless a complex, enterprise solution like celery is really required for a project
I recommend using the much simpler and easier to configure <span class="caps">RQ</span> for all your
asynchronous and scheduled task needs. Using <span class="caps">RQ</span> (and the relative projects django-rq
and rq-scheduler) we can easily add production ready queueued and scheduled jobs to
any django project.</p>
<p>In this article we presented a small introduction to <span class="caps">RQ</span> and its friends and saw how
to configure django to use it in a production ready environment using a small
django project (<a class="reference external" href="https://github.com/spapas/django-test-rq">https://github.com/spapas/django-test-rq</a>) which was implemented as a companion
to help readers quickly test the concepts presented here.</p>
</div>
Django model auditing2015-01-21T14:20:00+02:002015-01-21T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2015-01-21:/2015/01/21/django-model-auditing/<p class="first last">Model auditing (who-did-what) with Django</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#adding-simple-auditing-functionality-ourselves" id="toc-entry-2">Adding simple auditing functionality ourselves</a></li>
<li><a class="reference internal" href="#example" id="toc-entry-3">Example</a><ul>
<li><a class="reference internal" href="#models-py" id="toc-entry-4">models.py</a></li>
<li><a class="reference internal" href="#forms-py" id="toc-entry-5">forms.py</a></li>
<li><a class="reference internal" href="#views-py" id="toc-entry-6">views.py</a></li>
<li><a class="reference internal" href="#urls-py" id="toc-entry-7">urls.py</a></li>
<li><a class="reference internal" href="#templates" id="toc-entry-8">templates</a></li>
</ul>
</li>
<li><a class="reference internal" href="#using-django-simple-history" id="toc-entry-9">Using django-simple-history</a><ul>
<li><a class="reference internal" href="#installation" id="toc-entry-10">Installation</a></li>
<li><a class="reference internal" href="#example-1" id="toc-entry-11">Example</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-12">Usage</a></li>
</ul>
</li>
<li><a class="reference internal" href="#using-django-reversion" id="toc-entry-13">Using django-reversion</a><ul>
<li><a class="reference internal" href="#installation-1" id="toc-entry-14">Installation</a></li>
<li><a class="reference internal" href="#example-2" id="toc-entry-15">Example</a></li>
<li><a class="reference internal" href="#usage-1" id="toc-entry-16">Usage</a></li>
<li><a class="reference internal" href="#comparing-versions-with-django-reversion-compare" id="toc-entry-17">Comparing versions with django-reversion-compare</a></li>
</ul>
</li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-18">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>An auditing trail is a common requirement in most non-trivial applications. Organizations
need to know <em>who</em> did the change, <em>when</em> it was done and <em>what</em> was actually changed.
In this post we will see three
different solution in order to add this functionality in Django: doing it ourselves,
using django-simple-history and using django-reversion.</p>
<p><em>Update 24/09/2015:</em> Added a paragraph describing the django-reversion-compare which is
a great addon for django-reversion that makes finding differences between versions a breeze!</p>
</div>
<div class="section" id="adding-simple-auditing-functionality-ourselves">
<h2><a class="toc-backref" href="#toc-entry-2">Adding simple auditing functionality ourselves</a></h2>
<p>A simple way to actually do auditing is to keep four extra fields in our models:
<tt class="docutils literal">created_by</tt>, <tt class="docutils literal">created_on</tt>, <tt class="docutils literal">modified_by</tt> and <tt class="docutils literal">modified_on</tt>. The first two
will be filled when the model instance is created while the latter two will be
changed whenever the model instance is saved. So we only have <em>who</em> and <em>whe</em>.
Sometimes, these are enough so let’s see how easy it is to implement it in django.</p>
<p>We’ll need an abstract model that could be used as a base class for models that need auditing:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Auditable</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">created_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span> <span class="o">=</span> <span class="kc">True</span><span class="p">)</span>
<span class="n">created_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s1">'created_by'</span><span class="p">)</span>
<span class="n">modified_on</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateTimeField</span><span class="p">(</span><span class="n">auto_now</span> <span class="o">=</span> <span class="kc">True</span><span class="p">)</span>
<span class="n">modified_by</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">AUTH_USER_MODEL</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s1">'modified_by'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="kc">True</span>
</pre></div>
<p>Models inheriting from <tt class="docutils literal">Auditable</tt> will contain their datetime of creation and modification
which will be automatically filled using the very usefull <tt class="docutils literal">auto_now_add_</tt> (which will
set the current datetime when the model instance is created) and <tt class="docutils literal">auto_now_</tt> (which will
set the current datetime when the model instance is modified).</p>
<p>Such models will also have two foreign keys to <tt class="docutils literal">User</tt>, one for the user
that created the and one of the user that modified them. The problem with these two fields
is that they cannot be filled automatically (like the datetimes) because the user that
actually did create/change the objects must be provided!</p>
<p>Since I am really fond of CBVs I will present a simple mixin that can be used with CreateView
and UpdateView and does exactly that:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">AuditableMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">,):</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">,</span> <span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">created_by</span><span class="p">:</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">created_by</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span>
<span class="n">form</span><span class="o">.</span><span class="n">instance</span><span class="o">.</span><span class="n">modified_by</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">user</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">AuditableMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</pre></div>
<p>The above mixin overrides the <tt class="docutils literal">form_valid</tt> method of <tt class="docutils literal">CreateView</tt> and <tt class="docutils literal">UpdateView</tt>:
First it checks if the object is created (if it is created it won’t be saved in the
database yet thus it won’t have an id) in order to set the <tt class="docutils literal">created_by</tt> attribute to
the current user. After that it will set the <tt class="docutils literal">modified_by</tt> attribute of the object to
the current user. Finally, it will call the next <tt class="docutils literal">form_valid</tt> method to do whatever
is required (save the model instance and redirect to <tt class="docutils literal">success_url</tt> by default).</p>
<p>The views using <tt class="docutils literal">AuditableMixin</tt> should allow only logged in users (or else an
exception will be thrown). Also, don’t forget to exclude the <tt class="docutils literal">created_by</tt> and <tt class="docutils literal">modified_by</tt>
fields from your model form (<tt class="docutils literal">created_on</tt> and <tt class="docutils literal">modified_on</tt> will automatically be excluded).</p>
</div>
<div class="section" id="example">
<h2><a class="toc-backref" href="#toc-entry-3">Example</a></h2>
<p>Let’s see a simple example of creating a small django application using the previously defined abstract model and mixin:</p>
<div class="section" id="models-py">
<h3><a class="toc-backref" href="#toc-entry-4">models.py</a></h3>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="kn">from</span> <span class="nn">django.core.urlresolvers</span> <span class="kn">import</span> <span class="n">reverse</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">auditable.models</span> <span class="kn">import</span> <span class="n">Auditable</span>
<span class="k">class</span> <span class="nc">Book</span><span class="p">(</span><span class="n">Auditable</span><span class="p">):</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_absolute_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">reverse</span><span class="p">(</span><span class="s2">"book_list"</span><span class="p">)</span>
</pre></div>
<p>In the above we suppose that the <tt class="docutils literal">Auditable</tt> abstract model is imported from the
<tt class="docutils literal">auditable.models</tt> module and that a view named <tt class="docutils literal">book_list</tt> that shows all books exists.</p>
</div>
<div class="section" id="forms-py">
<h3><a class="toc-backref" href="#toc-entry-5">forms.py</a></h3>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.forms</span> <span class="kn">import</span> <span class="n">ModelForm</span>
<span class="k">class</span> <span class="nc">BookForm</span><span class="p">(</span><span class="n">ModelForm</span><span class="p">):</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">,</span> <span class="s1">'author'</span><span class="p">]</span>
</pre></div>
<p>Show only <tt class="docutils literal">name</tt> and <tt class="docutils literal">author</tt> fields (and not the auditable fields) in the <tt class="docutils literal">Book ModelForm</tt>.</p>
</div>
<div class="section" id="views-py">
<h3><a class="toc-backref" href="#toc-entry-6">views.py</a></h3>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.views.generic.edit</span> <span class="kn">import</span> <span class="n">CreateView</span><span class="p">,</span> <span class="n">UpdateView</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">ListView</span>
<span class="kn">from</span> <span class="nn">auditable.views</span> <span class="kn">import</span> <span class="n">AuditableMixin</span>
<span class="kn">from</span> <span class="nn">models</span> <span class="kn">import</span> <span class="n">Book</span>
<span class="kn">from</span> <span class="nn">forms</span> <span class="kn">import</span> <span class="n">BookForm</span>
<span class="k">class</span> <span class="nc">BookCreateView</span><span class="p">(</span><span class="n">AuditableMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">BookForm</span>
<span class="k">class</span> <span class="nc">BookUpdateView</span><span class="p">(</span><span class="n">AuditableMixin</span><span class="p">,</span> <span class="n">UpdateView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">BookForm</span>
<span class="k">class</span> <span class="nc">BookListView</span><span class="p">(</span><span class="n">ListView</span><span class="p">):</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">Book</span>
</pre></div>
<p>We import the <tt class="docutils literal">AuditableMixin</tt> from <tt class="docutils literal">auditable.views</tt> and make our Create and Update views
inherit from this mixin also in addition to <tt class="docutils literal">CreateView</tt> and <tt class="docutils literal">UpdateView</tt>. Pay attention that our
mixin is placed <em>before</em> CreateView in order to call <tt class="docutils literal">form_valid</tt> in the proper order: When multiple
inheritance is used like this python will check each class from left to right to find the proper method
and call it. For example, in our <tt class="docutils literal">BookCreateView</tt>, when the <tt class="docutils literal">form_valid</tt> method is called, python
will first check if <tt class="docutils literal">BookCreateView</tt> has a <tt class="docutils literal">form_valid</tt> method. Since it does not, it will check
if <tt class="docutils literal">AuditableMixin</tt> has a <tt class="docutils literal">form_valid</tt> method and call it. Now, we are calling the <tt class="docutils literal"><span class="pre">super(...).form_valid()</span></tt> in the
<tt class="docutils literal">AuditableMixin</tt> <tt class="docutils literal">form_valid</tt>, so the <tt class="docutils literal">form_valid</tt> of <tt class="docutils literal">CreateView</tt> will <em>also</em> be called.</p>
<p>A simple <tt class="docutils literal">ListView</tt> is also added to just show the info on all books.</p>
</div>
<div class="section" id="urls-py">
<h3><a class="toc-backref" href="#toc-entry-7">urls.py</a></h3>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">patterns</span><span class="p">,</span> <span class="n">include</span><span class="p">,</span> <span class="n">url</span>
<span class="kn">from</span> <span class="nn">views</span> <span class="kn">import</span> <span class="n">BookCreateView</span><span class="p">,</span> <span class="n">BookUpdateView</span><span class="p">,</span> <span class="n">BookListView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="n">patterns</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^accounts/login/$'</span><span class="p">,</span> <span class="s1">'django.contrib.auth.views.login'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^accounts/logout/$'</span><span class="p">,</span> <span class="s1">'django.contrib.auth.views.logout'</span><span class="p">,</span> <span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^create/$'</span><span class="p">,</span> <span class="n">BookCreateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'create_book'</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^update/(?P<pk>\d+)/$'</span><span class="p">,</span> <span class="n">BookUpdateView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'update_book'</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^$'</span><span class="p">,</span> <span class="n">BookListView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span> <span class="n">name</span><span class="o">=</span><span class="s1">'book_list'</span><span class="p">),</span>
<span class="p">)</span>
</pre></div>
<p>Just add the previously defined Create/Update/List views along with a login/logout views.</p>
</div>
<div class="section" id="templates">
<h3><a class="toc-backref" href="#toc-entry-8">templates</a></h3>
<p>You’ll need four templates:</p>
<ul class="simple">
<li>books/book_list.html: Show the list of books</li>
<li>books/book_form.html: Show the book editing form</li>
<li>registration/login.html: Login form</li>
<li>registration/logout.html: Logout message</li>
</ul>
</div>
</div>
<div class="section" id="using-django-simple-history">
<h2><a class="toc-backref" href="#toc-entry-9">Using django-simple-history</a></h2>
<p><a class="reference external" href="https://github.com/treyhunner/django-simple-history">django-simple-history</a> can be used to not only store the user and date of each modification
but a different version for each modification. To do that, for every model that is registered
to be used with django-simple-history, it wil create a second table in
the database hosting all versions (historical records) of that model. As we can understand this is really powerfull
since we can see exactly what was changed and also do normal <span class="caps">SQL</span> queries on that!</p>
<div class="section" id="installation">
<h3><a class="toc-backref" href="#toc-entry-10">Installation</a></h3>
<p>To use django-simple-history in a project, after we do a <tt class="docutils literal">pip install <span class="pre">django-simple-history</span></tt>,
we just need to add it to <tt class="docutils literal">INSTALLED_APPS</tt> and
add the <tt class="docutils literal">simple_history.middleware.HistoryRequestMiddleware</tt> to the <tt class="docutils literal">MIDDLEWARE_CLASSES</tt> list.</p>
<p>Finally, to keep the historical records for a model, just add an instace of <tt class="docutils literal">HistoricalRecords</tt> to this model.</p>
</div>
<div class="section" id="example-1">
<h3><a class="toc-backref" href="#toc-entry-11">Example</a></h3>
<p>For example, our previously defined <tt class="docutils literal">Book</tt> model will be modified like this:</p>
<div class="highlight"><pre><span></span>class SHBook(models.Model):
name = models.CharField(max_length=128)
author = models.CharField(max_length=128)
def get_absolute_url(self):
return reverse("shbook_list")
history = HistoricalRecords()
</pre></div>
<p>When we run <tt class="docutils literal">python manage.py makemigrations</tt> and <tt class="docutils literal">migrate</tt> this, we’ll see that beyond the table for SHBook, a table for HistoricalSHBook will be created:</p>
<pre class="code literal-block">
Migrations for 'sample':
0002_historicalshbook_shbook.py:
- Create model HistoricalSHBook
- Create model SHBook
</pre>
<p>Let’s see the schema of historicalshbook:</p>
<pre class="code literal-block">
CREATE TABLE "sample_historicalshbook" (
"id" integer NOT NULL,
"name" varchar(128) NOT NULL,
"author" varchar(128) NOT NULL,
"history_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"history_date" datetime NOT NULL,
"history_type" varchar(1) NOT NULL,
"history_user_id" integer NULL REFERENCES "auth_user" ("id")
);
</pre>
<p>So we see that it has the <em>same</em> fields as with <tt class="docutils literal">SHBook</tt> (<tt class="docutils literal">id, name, author</tt>) with the addition of
the primary key (<tt class="docutils literal">history_id</tt>) of this historical record, the date and user that did the change
(<tt class="docutils literal">history_date</tt>, <tt class="docutils literal">history_user_id</tt>) and the type of the record (created / update / delete).</p>
<p>So, just by adding a <tt class="docutils literal">HistoricalRecords()</tt> attribute to our model definition we’ll get complete auditing
for the instance of that model</p>
</div>
<div class="section" id="usage">
<h3><a class="toc-backref" href="#toc-entry-12">Usage</a></h3>
<p>To find out information about the historical records we’ll just use the <tt class="docutils literal">HistoricalRecords()</tt> attribute
of that model:</p>
<p>For example, running <tt class="docutils literal">SHBook.history.filter(id=1)</tt> will return all historical records of the book with
<tt class="docutils literal">id = 1</tt>. For each one of them we have can use the following:</p>
<ul class="simple">
<li>get the user that made the change through the <tt class="docutils literal">history_user</tt> attribute</li>
<li>get the date of the change through the <tt class="docutils literal">history_date</tt> attribute</li>
<li>get the type of the change through the <tt class="docutils literal">history_type</tt> attribute (and the corresponding <tt class="docutils literal">get_history_type_dispaly</tt>)</li>
<li>get a model instance as it was then through the <tt class="docutils literal">history_object</tt> attribute (in order to <tt class="docutils literal">save()</tt> it and revert to this version)</li>
</ul>
</div>
</div>
<div class="section" id="using-django-reversion">
<h2><a class="toc-backref" href="#toc-entry-13">Using django-reversion</a></h2>
<p><a class="reference external" href="https://github.com/etianen/django-reversion">django-reversion</a> offers more or less the same functionality of django-simple-history by following a different philosophy:
Instead of creating an extra table holding the history records for each model, it insteads converts all the fields of each model
to json and stores that <span class="caps">JSON</span> in the database in a text field.</p>
<p>This has the advantage that no extra tables are created to the database but the disadvantage that you can’t easily query
your historical records. So you may choose one or the other depending on your actual requirements.</p>
<div class="section" id="installation-1">
<h3><a class="toc-backref" href="#toc-entry-14">Installation</a></h3>
<p>To use django-reversion in a project, after we do a <tt class="docutils literal">pip install <span class="pre">django-reversion</span></tt>,
we just need to add it to <tt class="docutils literal">INSTALLED_APPS</tt> and
add the <tt class="docutils literal">reversion.middleware.RevisionMiddleware</tt> to the <tt class="docutils literal">MIDDLEWARE_CLASSES</tt> list.</p>
<p>In order to save the revisions of a model, you need to register this model to django-reversion. This can be
done either through the django-admin, by inheriting the admin class of that model from <tt class="docutils literal">reversion.VersionAdmin</tt>
or, if you don’t want to use the admin by <tt class="docutils literal">reversion.register</tt> decorator.</p>
</div>
<div class="section" id="example-2">
<h3><a class="toc-backref" href="#toc-entry-15">Example</a></h3>
<p>To use django-reversion to keep track of changes to <tt class="docutils literal">Book</tt> we can modify it like this:</p>
<div class="highlight"><pre><span></span>@reversion.register
class RBook(models.Model):
name = models.CharField(max_length=128)
author = models.CharField(max_length=128)
def get_absolute_url(self):
return reverse("rbook_list")
</pre></div>
<p>django-reversion uses two tables in the database to keep track of revisions: <tt class="docutils literal">revision</tt> and <tt class="docutils literal">version</tt>. Let’s
take a look at their schemata:</p>
<pre class="code literal-block">
.schema reversion_revision
CREATE TABLE "reversion_revision" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"manager_slug" varchar(200) NOT NULL,
"date_created" datetime NOT NULL,
"comment" text NOT NULL,
"user_id" integer NULL REFERENCES "auth_user" ("id")
);
.schema reversion_version
CREATE TABLE "reversion_version" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"object_id" text NOT NULL,
"object_id_int" integer NULL,
"format" varchar(255) NOT NULL,
"serialized_data" text NOT NULL,
"object_repr" text NOT NULL,
"content_type_id" integer NOT NULL REFERENCES "django_content_type" ("id"),
"revision_id" integer NOT NULL REFERENCES "reversion_revision" ("id")
);
</pre>
<p>As we can understand, the <tt class="docutils literal">revision</tt> table holds information like who created this
revison (<tt class="docutils literal">user_id</tt>) and when (<tt class="docutils literal">date_created</tt>) while the <tt class="docutils literal">version</tt> stores
a reference to the object that was modified (through a GenericForeignKey) and
the actual data (in the <tt class="docutils literal">serialized_data</tt> field). By default it uses <span class="caps">JSON</span>
to serialize the data (the serialization format is in the <tt class="docutils literal">format</tt> field). There’s
an one-to-one relation between <tt class="docutils literal">revision</tt> and <tt class="docutils literal">version</tt>.</p>
<p>If we create an instance of <tt class="docutils literal">RBook</tt> we’ll see the following in the database:</p>
<pre class="code literal-block">
sqlite> select * from reversion_revision;
1|default|2015-01-21 10:31:25.233000||1
sqlite> select * from reversion_version;
1|1|1|json|[{"fields": {"name": "asdasdasd", "author": "asdasd"}, "model": "sample.rbook", "pk": 1}]|RBook object|12|1
</pre>
<p><tt class="docutils literal">date_created</tt> and <tt class="docutils literal">user_id</tt> are stored on <tt class="docutils literal">revision</tt> while <tt class="docutils literal">format</tt>, <tt class="docutils literal">serialized_data</tt>, <tt class="docutils literal">content_type_id</tt> and
<tt class="docutils literal">object_id_int</tt> (the <tt class="docutils literal">GenericForeignKey</tt>) are stored in <tt class="docutils literal">version</tt>.</p>
</div>
<div class="section" id="usage-1">
<h3><a class="toc-backref" href="#toc-entry-16">Usage</a></h3>
<p>To find out information about an object you have to use the <tt class="docutils literal">reversion.get_for_object(object)</tt> method. In order to be
easily used in templates I recommend creating the following <tt class="docutils literal">get_versions()</tt> method in each model that is registered with django-reversion</p>
<pre class="code literal-block">
def get_versions(self):
return reversion.get_for_object(self)
</pre>
<p>Now, each version has a <tt class="docutils literal">revision</tt> attribute for the corresponding revision and can be used to do the following:</p>
<ul class="simple">
<li>get the user that made the change through the <tt class="docutils literal">revision.user</tt> attribute</li>
<li>get the date of the change through the <tt class="docutils literal">revision.date_created</tt> attribute</li>
<li>get the values of the object fields as they were in this revision using the <tt class="docutils literal">field_dict</tt> attribute</li>
<li>get a model instance as it was on that revision using the <tt class="docutils literal">object_version.object</tt> attribute</li>
<li>revert to that previous version of that object using the <tt class="docutils literal">revert()</tt> method</li>
</ul>
</div>
<div class="section" id="comparing-versions-with-django-reversion-compare">
<h3><a class="toc-backref" href="#toc-entry-17">Comparing versions with django-reversion-compare</a></h3>
<p>A great addon for django-version is <a class="reference external" href="github.com/jedie/django-reversion-compare">django-reversion-compare</a> which helps you find out differences
between versions of your objects. When you use django-reversion-compare, you’ll be able to select
two (different) versions of your object and you’ll be presented with a list of all the differences
found in the fields of that object between the two versions. The diff algorithm is smart, so you’ll
be able to easily recognise the changes.</p>
<p>To use django-reversion-compare, after installing it you should just inherit your admin views from
<tt class="docutils literal">reversion_compare.admin.CompareVersionAdmin</tt> (instead of <tt class="docutils literal">reversion.VersionAdmin</tt>) and you’ll
get the reversion-compare views instead of reversion views in the admin for the history of the object.</p>
<p>Also, in case you need to give access to normal, non-admin users to the history of an object (this is
useful for auditing reasons), you can use the <tt class="docutils literal">reversion_compare.views.HistoryCompareDetailView</tt>
as a normal <tt class="docutils literal">DetailView</tt> to create a non-admin history and compare diff view.</p>
</div>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-18">Conclusion</a></h2>
<p>In the above we say that it is really easy to add basic (<em>who</em> and <em>when</em>) auditing capabilities to your models: You just need to
inherit your models from the <tt class="docutils literal">Auditable</tt> abstract class and inherit your Create and Update CBVs from <tt class="docutils literal">AuditableMixin</tt>.
If you want to know exactly <em>what</em> was changed then you have two solutions: django-simple-history to create an extra table for
each of your models so you’ll be able to query your historical records (and easily extra aggregates, statistics etc) and
django-reversion to save each version as a json object, so no extra tables will be created.</p>
<p>All three solutions for auditing have been implemented in a sample project at <a class="reference external" href="https://github.com/spapas/auditing-sample">https://github.com/spapas/auditing-sample</a>.</p>
<p>You can clone the project and, preferrably in a virtual environment, install requirements (<tt class="docutils literal">pip install <span class="pre">-r</span> requirements.txt</tt>),
do a migrate (<tt class="docutils literal">python manage.py migrate</tt> — uses sqlite3 by default) and run the local development
server (<tt class="docutils literal">python manage.py ruinserver</tt>).</p>
</div>
Change the primary color of bootstrap material design2014-12-16T16:20:00+02:002014-12-16T16:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2014-12-16:/2014/12/16/change-bootstrap-material-primary-color/<p class="first last">A tutorial to help users change the primary color of Bootstrap material design</p>
<div class="section" id="introduction">
<h2>Introduction</h2>
<p><a class="reference external" href="https://github.com/FezVrasta/bootstrap-material-design">Bootstrap Material Design</a> is a great theme that sits on top of <a class="reference external" href="http://getbootstrap.com/">Bootstrap</a> and transforms it to
<a class="reference external" href="http://www.google.com/design/spec/material-design/introduction.html">Material Design</a>! The great thing about Bootstrap Material Design is that you just need to include
its css and js files after your Bootstrap files and …</p>
<p>boom! Your page is Material Design compatible!</p>
<object data="https://google.github.io/material-design-icons/action/svg/ic_thumb_up_24px.svg" type="image/svg+xml"></object>
<p>A nice feature of Bootstrap Material Design is that you can change its default color to a new one (I
don’t really like the current - greenish one). This is easy for people with less skills however I
found it rather challenging when I tried it. That’s why I will present a step by step tutorial on
changing the default primary color of the Bootstrap Material Design theme:</p>
</div>
<div class="section" id="step-1-get-the-code">
<h2>Step 1: Get the code</h2>
<p>Use git to make a local clone of the project with <tt class="docutils literal">git clone <span class="pre">https://github.com/FezVrasta/bootstrap-material-design.git</span></tt>. This will create a directory
named <tt class="docutils literal"><span class="pre">bootstrap-material-design</span></tt>. Or you can download the latest version of the code using (<a class="reference external" href="https://github.com/FezVrasta/bootstrap-material-design/archive/master.zip">https://github.com/FezVrasta/bootstrap-material-design/archive/master.zip</a>)
and unzip it to the <tt class="docutils literal"><span class="pre">bootstrap-material-design</span></tt> directory.</p>
</div>
<div class="section" id="step-2-install-node-js-and-npm">
<h2>Step 2: Install node.js and npm</h2>
<p>You need to have <a class="reference external" href="http://nodejs.org/">node.js</a> and npm installed in your system - this is something very easy so I won’t go into any details about this. After you have installed
both node.js and npm you need to put them in your path so that you’ll be able to run <tt class="docutils literal">npm <span class="pre">-v</span></tt> without errors and receive something like <tt class="docutils literal">1.4.14</tt>.</p>
</div>
<div class="section" id="step-3-install-less">
<h2>Step 3: Install less</h2>
<p><a class="reference external" href="http://lesscss.org/">less</a> is a <span class="caps">CSS</span> preprocessor in which Bootstrap Material Design has been written. To install it, just enter the command <tt class="docutils literal">npm install <span class="pre">-g</span> less</tt>. After that
you should have a command named <tt class="docutils literal">lessc</tt> which, when run would output something like: <tt class="docutils literal">lessc 2.1.1 (Less Compiler) [JavaScript]</tt>.</p>
</div>
<div class="section" id="step-4-create-the-customizations-files">
<h2>Step 4: Create the customizations files</h2>
<p>Go to the directory where you cloned (or unzipped) the Bootstrap Material Design code and create a file named <tt class="docutils literal">custom.less</tt> (so, that file should be
in the same folder as with <tt class="docutils literal">bower.json</tt>, <tt class="docutils literal">Gruntfile.js</tt> etc) with the following contents:</p>
<pre class="code literal-block">
@import "less/material.less";
// Override @primary color with one took from _colors.less
@primary: @indigo;
</pre>
<p>(I wanted to use the indigo color as my primary one - you may of course use whichever color from the ones defined in <tt class="docutils literal">less/_variables.less</tt> you like)</p>
<p>This file may contain other default values for variables - if I find anything useful I will add it to this post (also please reply with any recommendations).</p>
<p><strong>Update 13/10/2015</strong> After a request from commenter Jofferson Ramirez Tiquez, here’s a custom.less that overrides more colors from <tt class="docutils literal">_variables.css</tt>
(beyond the primary color, it changes the success color to teal and info and warning to the corresponding hex color values):</p>
<pre class="code literal-block">
@import "less/material.less";
@primary: @indigo;
@success: @teal;
@info: #CFD8DC;
@warning:#455A64;
</pre>
<p><strong>Update 08/02/2017</strong> Commenter Enrique SIlva informed me that the names of these color variables have been changed to
<tt class="docutils literal"><span class="pre">@brand-primary</span></tt>, <tt class="docutils literal"><span class="pre">@brand-success</span></tt> etc (as can be seen on <tt class="docutils literal">less/_variables.less</tt>) so you must change them accordingly when overriding them!</p>
</div>
<div class="section" id="step-5-create-your-custom-material-css-file">
<h2>Step 5: Create your custom material css file</h2>
<p>Finally, run the following command: <tt class="docutils literal">lessc custom.less > <span class="pre">material-custom.css</span></tt>. This will create a file named <tt class="docutils literal"><span class="pre">material-custom.css</span></tt> that contains your
custom version of Bootstrap Material Design! If you want your <tt class="docutils literal"><span class="pre">material-custom.css</span></tt> to be compressed, add the <tt class="docutils literal"><span class="pre">-x</span></tt> option like this: <tt class="docutils literal">lessc <span class="pre">-x</span> custom.less > <span class="pre">material-custom.css</span></tt>.</p>
<p>You may now include <tt class="docutils literal"><span class="pre">material-custom.css</span></tt> instead of <tt class="docutils literal">material.css</tt> (or the minified version of it) to your projects and you’ll have your own primary color!</p>
<p><strong>Update 09/06/2016</strong>: After some comments I returned to this blog post and tried to re-create the custom
css file. Unfortunately, for the latest versions of the bootstrap-material-design, the above step by step instructions need to be changed in two places:</p>
<ul class="simple">
<li>Step 1: Instead of only the bootstrap-material-design code, you also need to retrieve the code of the original bootstrap. To do that you can do a <tt class="docutils literal">git clone <span class="pre">https://github.com/twbs/bootstrap.git</span></tt> so that you’ll have the bootstrap directory next to the bootstrap-material-design directory. Be careful on that, you should have two directories side by side, one containing the bootstrap code and another containing the bootstrap-material-design code. This is required because bootstrap-material-design references some bootstrap variables (from <tt class="docutils literal"><span class="pre">less/_import-bs-less.less</span></tt>).</li>
<li>Step 4: For the customizations file (<tt class="docutils literal">custom.less</tt>), you should change the line <tt class="docutils literal">@import "less/material.less";</tt> to <tt class="docutils literal">@import <span class="pre">"less/bootstrap-material-design.less";</span></tt> because the name of that file has been changed.</li>
</ul>
<p>After these two changes you should once again be able to create your custom css file!</p>
<p><strong>Update 03/07/2017</strong> Commenter Nuraan Bader informed of an <cite>NameError: variable @line-height-base is undefined</cite> error when
creating the custom css file. This is because bootstrap has changed the default branch from <cite>master</cite> to <cite>v4-dev</cite>
so when you clone you’ll get the <cite>v4-dev</cite> branch instead of the <cite>master</cite> one! To fix this, after you clone (check out
my 09/06/2016 update) do the following:</p>
<pre class="code literal-block">
cd bootstrap
git checkout master
</pre>
<p>This should change the current branch to master and you should see the <cite>less</cite> folder inside the <cite>bootstrap</cite> folder. After that everything should work as expected.</p>
</div>
Retrieving Gmail blocked attachments2014-10-23T14:20:00+03:002014-10-23T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2014-10-23:/2014/10/23/retrieve-gmail-blocked-attachments/<p class="first last">A method to retrieve blocked attachments from your Gmail</p>
<p>Before services like Dropbox were widely available, some people (including me) were using
their Gmail account as a primitive backup solution: Compress your directory and send it to
your gmail. There. Backup complete.</p>
<p>However, nothing is so easy…</p>
<p>Recently, I wanted to retrieve one of these backups, a .rar containing the complete
source code (since it was written in TeX) of my PhD thesis. The problem was that Gmail blocked the access to these attachments saying</p>
<blockquote>
Anti-virus warning - 1 attachment contains a virus or blocked file. Downloading this attachment is disabled.</blockquote>
<p>probably because I had a number of .bat files inside that .rar archive to automate my work :(</p>
<p>Now what ?</p>
<p>After searching the internet and not founding any solutions, I tried the options that gmail gives for each email. One
particular one cought my interest: <em>Show original</em></p>
<img alt="Here it is!" src="/images/show_original.png" style="width: 780px;" />
<p>Clicking this option opened a text file with the original, <span class="caps">MIME</span> encoded message. The interesting thing of course was</p>
<pre class="code literal-block">
------=_NextPart_000_004F_01CA0AED.E63C2A30
Content-Type: application/octet-stream;
name="phdstuff.rar"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="phdstuff.rar"
UmFyIRoHAM+QcwAADQAAAAAAAAB0f3TAgCwANAMAAFQEAAACRbXCx8lr9TodMwwAIAAAAG5ld2Zp
bmFsLnR4dA3dEQzM082BF7sB+D3q6QPUNEfwG7vHQgNkiQDTkGvfhOE4mNltIJJlBFMOCQPzPeKD
...
</pre>
<p>So the whole attachment was contained in that text file, encoded in base64! Now I just
needed to extract it from the email and convert it back to binary.</p>
<blockquote>
<strong>Important: Before going the python way, please check the 2 June 2015 update at the end of the article for an easier solution!</strong></blockquote>
<p>This was very easy to do using Python - some people <a class="reference external" href="http://stackoverflow.com/questions/4067937/getting-mail-attachment-to-python-file-object">had already asked the same thing on <span class="caps">SO</span></a>.
So here’s a simple program that gets an email in text/mime format as input and dumps all attachments:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">email</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="k">if</span> <span class="vm">__name__</span><span class="o">==</span><span class="s1">'__main__'</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">)</span><span class="o"><</span><span class="mi">2</span><span class="p">:</span>
<span class="nb">print</span> <span class="s2">"Please enter a file to extract attachments from"</span>
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">msg</span> <span class="o">=</span> <span class="n">email</span><span class="o">.</span><span class="n">message_from_file</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]))</span>
<span class="k">for</span> <span class="n">pl</span> <span class="ow">in</span> <span class="n">msg</span><span class="o">.</span><span class="n">get_payload</span><span class="p">():</span>
<span class="k">if</span> <span class="n">pl</span><span class="o">.</span><span class="n">get_filename</span><span class="p">():</span> <span class="c1"># if it is an attachment</span>
<span class="nb">open</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">get_filename</span><span class="p">(),</span> <span class="s1">'wb'</span><span class="p">)</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">get_payload</span><span class="p">(</span><span class="n">decode</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
</pre></div>
<p>Save this to a file named <tt class="docutils literal">get_attachments.py</tt> and, after saving the original message to a file
named <tt class="docutils literal">0.txt</tt> run <tt class="docutils literal">python get_attachments.py 0.txt</tt> and you’ll see the attachments of your email in the same folder!</p>
<blockquote>
Disclaimer: I have to warn you that since Gmail claims that an attachment is <em>not safe</em> it may be <strong>actually not safe</strong>. So
you must be 100% sure that you know what you are doing before retrievening your email attachments like this.</blockquote>
<p><strong>Update</strong>: Stefan <a class="reference external" href="https://gist.github.com/stefansundin/a99bbfb6cda873d14fd2">created an improved version</a> of the attachment extractor which is also compatible with Python 3.4!</p>
<p><strong>Update, 12 January 2015</strong>: Ivana (at the comments section) proposed a different solution that may work
for some files: <em>Use a mobile Gmail client (I tested it with Android) and “Save to Drive” your attachment.
You’ll then be able to download it from the Google Drive!</em> I am not sure if this works for all attachments,
however it worked for the source of my PhD thesis! I’m writing it may not work for all attachments because
when you download something from Google Drive it does a virus check so it may not allow you to download the
attachment and then you’ll still need to do it manually using the method below (however <strong>in that case you
must be even more cautious for the case the attachment actualyl contains a malicious file</strong>).</p>
<p><strong>Update, 2 June 2015</strong>: Commenter Sumit Chauhan (and Yuri Marx) proposed to change the extension
of the downloaded <span class="caps">MIME</span> text file (original message) to eml and open it with Outlook. I don’t have
Outlook in my system, however I tried opening it with <a class="reference external" href="https://www.mozilla.org/el/thunderbird/">Thunderbird</a> and it worked!!! So please
try this solution before trying the pythonic way (especially if you’re not familiar with python).</p>
<p><strong>Update, 6 March 2016</strong>: Commenter Alex (Alexandre Barfuhok) proposed another clever trick (involving making visible
the initially invisible “Download to drive” button) for retrieving gmail blocked attachments!
Read more about it <a class="reference external" href="http://www.barfuhok.com/how-to-download-a-file-with-anti-virus-warning-on-gmail/">at his article</a>!</p>
Django non-HTML responses2014-09-15T14:20:00+03:002014-09-15T14:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2014-09-15:/2014/09/15/django-non-html-responses/<p class="first last">Implementing non-<span class="caps">HTML</span> (for instance <span class="caps">CSV</span>, <span class="caps">XSL</span>, etc) responses with Django and Class Based Views</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#how-are-cbvs-rendered" id="toc-entry-2">How are CBVs rendered</a></li>
<li><a class="reference internal" href="#rendering-to-non-html" id="toc-entry-3">Rendering to non-<span class="caps">HTML</span></a></li>
<li><a class="reference internal" href="#a-non-html-mixin" id="toc-entry-4">A non-<span class="caps">HTML</span> mixin</a></li>
<li><a class="reference internal" href="#a-more-complex-example" id="toc-entry-5">A more complex example</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-6">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>I have already written about the many advantages (<span class="caps">DRY</span>!) of using <a class="reference external" href="https://spapas.github.io/2014/04/11/django-generic-formviews-for-objects/">CBVs in a previous article.</a>
In this article I will present the correct (pythonic) way to allow a normal <span class="caps">CBV</span> to return non-<span class="caps">HTML</span> responses, like <span class="caps">PDF</span>, <span class="caps">CSV</span>, <span class="caps">XSL</span> etc.</p>
</div>
<div class="section" id="how-are-cbvs-rendered">
<h2><a class="toc-backref" href="#toc-entry-2">How are CBVs rendered</a></h2>
<p>Before proceeding, we need to understand how CBVs are rendered. By walking through the
class hierarchy in the <a class="reference external" href="http://ccbv.co.uk/"><span class="caps">CBV</span> inspector</a>, we can see that
all the normal Django CBVs (DetailView, CreateView, TemplateView etc) are using a mixin
named <a class="reference external" href="http://ccbv.co.uk/projects/Django/1.7/django.views.generic.base/TemplateResponseMixin/">TemplateResponseMixin</a> which defines a method named <tt class="docutils literal">render_to_response</tt>. This
is the method that is used for rendering the output of the Views. Let’s take a look at
<a class="reference external" href="http://ccbv.co.uk/projects/Django/1.7/django.views.generic.base/TemplateResponseMixin/">how it is defined</a> (I’ll remove the comments):</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">TemplateResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">response_class</span> <span class="o">=</span> <span class="n">TemplateResponse</span>
<span class="n">content_type</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">response_kwargs</span><span class="o">.</span><span class="n">setdefault</span><span class="p">(</span><span class="s1">'content_type'</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">content_type</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">response_class</span><span class="p">(</span>
<span class="n">request</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span>
<span class="n">template</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">get_template_names</span><span class="p">(),</span>
<span class="n">context</span><span class="o">=</span><span class="n">context</span><span class="p">,</span>
<span class="o">**</span><span class="n">response_kwargs</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">get_template_names</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">template_name</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">ImproperlyConfigured</span><span class="p">(</span>
<span class="s2">"TemplateResponseMixin requires either a definition of "</span>
<span class="s2">"'template_name' or an implementation of 'get_template_names()'"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">template_name</span><span class="p">]</span>
</pre></div>
<p>This method will try to find out which html template should be used to render the View
(using the <tt class="docutils literal">get_template_names</tt> method and template_name attribute of the mixin) and
then render this view by instantiating an instance of the <tt class="docutils literal">TemplateResponse</tt> class
(as defined in the <tt class="docutils literal">response_class</tt> attribute)
and passing the request, template, context and other response_args to it.</p>
<p>The <a class="reference external" href="https://github.com/django/django/blob/master/django/template/response.py">TemplateResponse</a> class which is instantiated in the <tt class="docutils literal">render_to_response</tt> method
inherits from a normal <tt class="docutils literal">HttpResponse</tt> and is used to render
the template passed to it as a parameter.</p>
</div>
<div class="section" id="rendering-to-non-html">
<h2><a class="toc-backref" href="#toc-entry-3">Rendering to non-<span class="caps">HTML</span></a></h2>
<p>From the previous discussion we can conclude that if your non-<span class="caps">HTML</span> response <em>needs</em>
a template then you just need to create a subclass of <tt class="docutils literal">TemplateResponse</tt> and
assign it to the <tt class="docutils literal">response_class</tt> attribute (and also change the <tt class="docutils literal">content_type</tt>
attribute). On the other hand, if your non-<span class="caps">HTML</span> respond does not need a template
to be rendered then you have to override <tt class="docutils literal">render_to_response</tt> completely
(since the template parameter does not need to be passed now) and either define
a subclass of HttpResponse or do the rendering in the render_to_response.</p>
<p>Since almost always you won’t need a template to create a non-<span class="caps">HTML</span> view and because
I believe that the solution is <span class="caps">DRY</span>-enough by implementing the rendering code to
the <tt class="docutils literal">render_to_response</tt> method (<em>without</em> subclassing <tt class="docutils literal">HttpResponse</tt>) I will
implement a mixin that does exactly that.</p>
<p>Subclassing <tt class="docutils literal">HttpResponse</tt> will not make our design more <span class="caps">DRY</span> because for every
subclass of <tt class="docutils literal">HttpResponse</tt> the <tt class="docutils literal">render_to_response</tt> method would also need to
be modified (by subclassing <tt class="docutils literal">TemplateResponseMixin) to instantiate the subclass of ``HttpResponse</tt> with the correct parameters.
For instance, the existing <tt class="docutils literal">TemplateResponseMixin</tt> cannot be used if the subclass
of <tt class="docutils literal">HttpResponse</tt> does not take a template as a parameter (solutions like
passing None to the template parameter are signals of bad design).</p>
<p>In any case, changing just the <tt class="docutils literal">render_to_response</tt> method using a Mixin is in my opinion the best solution
to the above problem.
A <a class="reference external" href="http://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful">Mixin</a> is a simple class that can be used to extend other classes either by overriding functionality of the base class or
by adding extra features. Django CBVs use various <a class="reference external" href="https://docs.djangoproject.com/en/dev/topics/class-based-views/mixins/">mixins</a> to extend the base Views and add functionality.</p>
</div>
<div class="section" id="a-non-html-mixin">
<h2><a class="toc-backref" href="#toc-entry-4">A non-<span class="caps">HTML</span> mixin</a></h2>
<p>So, a basic skeleton for our mixin would be something like this:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">NonHtmlResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'text/plain'</span><span class="p">)</span>
<span class="n">response</span><span class="o">.</span><span class="n">write</span><span class="p">(</span> <span class="s2">"Hello, world"</span> <span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>The previous mixin overrides the render_to_response method to just return the text “Hello, world”. For instance
we could define the following class:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DummyTextResponseView</span><span class="p">(</span><span class="n">NonHtmlResponseMixin</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">,):</span>
<span class="k">pass</span>
</pre></div>
<p>which can be added as a route to <tt class="docutils literal">urls.py</tt> (using the <tt class="docutils literal">as_view</tt> method) and will always return the “Hello, world” text.</p>
<p>Here’s something more complicated: A Mixin that can be used along with a DetailView and will output the properties of the
object as text:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">TextPropertiesResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">context</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'text/plain; charset=utf-8'</span><span class="p">)</span>
<span class="n">o</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_object</span><span class="p">()</span>
<span class="n">o</span><span class="o">.</span><span class="n">_meta</span><span class="o">.</span><span class="n">fields</span>
<span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">o</span><span class="o">.</span><span class="n">_meta</span><span class="o">.</span><span class="n">fields</span><span class="p">:</span>
<span class="n">response</span><span class="o">.</span><span class="n">write</span> <span class="p">(</span><span class="sa">u</span><span class="s1">'</span><span class="si">{0}</span><span class="s1">: </span><span class="si">{1}</span><span class="se">\n</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">unicode</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">name</span><span class="p">))</span> <span class="p">)</span> <span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>and can be used like this</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">TextPropertiesDetailView</span><span class="p">(</span><span class="n">TextPropertiesResponseMixin</span><span class="p">,</span> <span class="n">FooDetailView</span><span class="p">,):</span>
<span class="k">pass</span>
</pre></div>
<p>The above mixin will use the get_object() method of the DetailView to get the object and then output
it as text. We can create similar mixins that will integrate with other types of CBVs, for instance
to export a ListView as an <span class="caps">CSV</span> or generate an png from a DetailView of an Image file.</p>
</div>
<div class="section" id="a-more-complex-example">
<h2><a class="toc-backref" href="#toc-entry-5">A more complex example</a></h2>
<p>The previous examples all built upon an existing view (either a TemplateView, a DetailView or a ListView).
However, an existing view that will fit our requirements won’t always be available. For instance,
sometimes I want to export data from my database using a raw <span class="caps">SQL</span> query. Also I’d like to be able to easily
export this data as csv or excel.</p>
<p>First of all, we need to define a view that will inherit from <tt class="docutils literal">View</tt> and export the data as a <span class="caps">CSV</span>:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">unicodecsv</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">connection</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">View</span>
<span class="k">class</span> <span class="nc">CsvRawSqlExportView</span><span class="p">(</span><span class="n">View</span><span class="p">,</span> <span class="p">):</span>
<span class="n">sql</span> <span class="o">=</span> <span class="s1">'select 1+1'</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'res'</span><span class="p">]</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">generate_data</span><span class="p">(</span><span class="n">cursor</span><span class="p">):</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">cursor</span><span class="o">.</span><span class="n">fetchall</span><span class="p">():</span>
<span class="k">yield</span> <span class="n">row</span>
<span class="n">cursor</span> <span class="o">=</span> <span class="n">connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">sql</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">params</span><span class="p">)</span>
<span class="n">generator</span> <span class="o">=</span> <span class="n">generate_data</span><span class="p">(</span><span class="n">cursor</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">generator</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">generator</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'text/plain; charset=utf-8'</span><span class="p">)</span>
<span class="n">response</span><span class="p">[</span><span class="s1">'Content-Disposition'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'attachment; filename=export.csv'</span>
<span class="n">w</span> <span class="o">=</span> <span class="n">unicodecsv</span><span class="o">.</span><span class="n">writer</span><span class="p">(</span><span class="n">response</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s1">'utf-8'</span><span class="p">)</span>
<span class="n">w</span><span class="o">.</span><span class="n">writerow</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">)</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">generator</span><span class="p">:</span>
<span class="n">w</span><span class="o">.</span><span class="n">writerow</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>The above View has three attributes:
* sql, which is a string with the raw sql that will be executed
* headers, which is an array with the names of each header of the resulting data
* params, which is an array with parameters that may need to be passed to the query</p>
<p>The <tt class="docutils literal">get</tt> method executes the query and passes the result to <tt class="docutils literal">render_to_response</tt>
using a generator. The <tt class="docutils literal">render_to_response</tt> method instantiates an HttpResponse
object with the correct attributes and writes the <span class="caps">CSV</span> to the response object using unicodecsv.</p>
<p>We can now quickly create a route that will export data from the users table:</p>
<div class="highlight"><pre><span></span><span class="n">url</span><span class="p">(</span>
<span class="sa">r</span><span class="s1">'^raw_export_users/$'</span><span class="p">,</span>
<span class="n">views</span><span class="o">.</span><span class="n">CsvRawSqlExportView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(</span>
<span class="n">sql</span><span class="o">=</span><span class="s1">'select id, username from auth_user'</span><span class="p">,</span>
<span class="n">headers</span><span class="o">=</span><span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="s1">'username'</span><span class="p">]</span>
<span class="p">)</span> <span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'raw_export_users'</span>
<span class="p">),</span>
</pre></div>
<p>If instead of <span class="caps">CSV</span> we wanted to export to <span class="caps">XLS</span> (using xlwt), we’d just need to create a Mixin:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">XlsRawSqlResponseMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">render_to_response</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">generator</span><span class="p">,</span> <span class="o">**</span><span class="n">response_kwargs</span><span class="p">):</span>
<span class="n">response</span> <span class="o">=</span> <span class="n">HttpResponse</span><span class="p">(</span><span class="n">content_type</span><span class="o">=</span><span class="s1">'application/ms-excel'</span><span class="p">)</span>
<span class="n">response</span><span class="p">[</span><span class="s1">'Content-Disposition'</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'attachment; filename=export.xls'</span>
<span class="n">wb</span> <span class="o">=</span> <span class="n">xlwt</span><span class="o">.</span><span class="n">Workbook</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s1">'utf-8'</span><span class="p">)</span>
<span class="n">ws</span> <span class="o">=</span> <span class="n">wb</span><span class="o">.</span><span class="n">add_sheet</span><span class="p">(</span><span class="s2">"data"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">j</span><span class="p">,</span><span class="n">c</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">headers</span><span class="p">):</span>
<span class="n">ws</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="n">j</span><span class="p">,</span><span class="n">c</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span><span class="p">,</span><span class="n">row</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">generator</span><span class="p">):</span>
<span class="k">for</span> <span class="n">j</span><span class="p">,</span><span class="n">c</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">row</span><span class="p">):</span>
<span class="n">ws</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span><span class="n">j</span><span class="p">,</span><span class="n">c</span><span class="p">)</span>
<span class="n">wb</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="k">return</span> <span class="n">response</span>
</pre></div>
<p>and create a View that inherits from <tt class="docutils literal">CsvRawSqlExportView</tt> and uses the above mixin:</p>
<div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">XlsRawSqlExportView</span><span class="p">(</span> <span class="n">XlsRawSqlResponseMixin</span><span class="p">,</span> <span class="n">CsvRawSqlExportView</span> <span class="p">):</span>
<span class="k">pass</span>
</pre></div>
<p>and route to that view to get the <span class="caps">XLS</span>:</p>
<div class="highlight"><pre><span></span><span class="n">url</span><span class="p">(</span>
<span class="sa">r</span><span class="s1">'^raw_export_users/$'</span><span class="p">,</span>
<span class="n">views</span><span class="o">.</span><span class="n">XlsRawSqlExportView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(</span>
<span class="n">sql</span><span class="o">=</span><span class="s1">'select id, username from auth_user'</span><span class="p">,</span>
<span class="n">headers</span><span class="o">=</span><span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="s1">'username'</span><span class="p">]),</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'raw_export_users'</span>
<span class="p">),</span>
</pre></div>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-6">Conclusion</a></h2>
<p>Using the above techniques we can define CBVs that will output their content in various content types
beyond <span class="caps">HTML</span>. This will help us write write clean and <span class="caps">DRY</span> code.</p>
</div>
Implementing a simple, Heroku-hosted REST service using Flask and mongoDB2014-06-30T15:23:00+03:002014-06-30T15:23:00+03:00Serafeim Papastefanostag:spapas.github.io,2014-06-30:/2014/06/30/rest-flask-mongodb-heroku/<p class="first last">An implementation <span class="amp">&</span> discussion of a simple <span class="caps">REST</span> service with Flask that is hosted on Heroku and is using mongoDB for persistance.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#requirements" id="toc-entry-2">Requirements</a></li>
<li><a class="reference internal" href="#implementing-the-rest-service" id="toc-entry-3">Implementing the <span class="caps">REST</span> service</a></li>
<li><a class="reference internal" href="#testing-it-locally" id="toc-entry-4">Testing it locally</a></li>
<li><a class="reference internal" href="#deploying-to-heroku" id="toc-entry-5">Deploying to Heroku</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>In the following, I will describe how I used <a class="reference external" href="http://flask.pocoo.org/">Flask</a>, a very nice web <em>microframework</em> for python along with <a class="reference external" href="http://www.mongodb.org/">mongoDB</a>, the most
popular No-<span class="caps">SQL</span> database to implement a simple <span class="caps">REST</span> service that was hosted on <a class="reference external" href="https://dashboard.heroku.com/apps">Heroku</a>. This <span class="caps">REST</span> service would get readings from
a number of sensors from an Android device.</p>
<p>I chose Flask instead of Django mainly because the <span class="caps">REST</span> service that I needed to implement would be very simple and most of
the Django bells and whistles (<span class="caps">ORM</span>, auth, admin, etc) wouldn’t be needed anyway. Also, Flask is much quicker to set-up than
Django since almost everything (views, urls, etc) can be put inside one python module.</p>
<p>Concerning the choice of a NoSQL persistance solution (mongoDB), I wanted to have a table (or collection as it is called in the
mongoDB world) of readings from the sensor. Each reading would just have a timestamp and various other
arbitrary data depending on the type of the reading, so saving it as a <span class="caps">JSON</span> document in a NoSQL database is a good solution.</p>
<p>Finally, all the above will be deployed to Heroku which offers some great services for deploying python code in the cloud.</p>
</div>
<div class="section" id="requirements">
<h2><a class="toc-backref" href="#toc-entry-2">Requirements</a></h2>
<p>I propose creating a file named <tt class="docutils literal">requirements.txt</tt> that will host the required packages for your project, so you will be
able to setup your projects after creating a virtual environment with <a class="reference external" href="http://virtualenv.readthedocs.org/en/latest/">virtualenv</a> just by running <tt class="docutils literal">pip install <span class="pre">-r</span> requirements.txt</tt>.
Also, the requirements.txt is required for deploying python to Heroku.</p>
<p>So, for my case, the contents of requirements.txt are the following:</p>
<pre class="code literal-block">
Flask==0.10.1
Flask-PyMongo==0.3.0
Flask-RESTful==0.2.12
Jinja2==2.7.3
MarkupSafe==0.23
Werkzeug==0.9.6
aniso8601==0.82
gunicorn==19.0.0
itsdangerous==0.24
pymongo==2.7.1
pytz==2014.4
six==1.7.2
</pre>
<ul class="simple">
<li>Flask-PyMongo is a simple wrapper for Flask around pymongo which is the python mongoDB driver.</li>
<li>Flask-RESTful is a simple library for creating <span class="caps">REST</span> APIs - it needs aniso8601 and pytz.</li>
<li>Jinja2 is the template library for Flask (we won’t use it but it is required by Flask installation) - it needs MarkupSafe.</li>
<li>Werkzeug is a <span class="caps">WSGI</span> utility library - required by Flask</li>
<li>gunicorn is a <span class="caps">WSGI</span> <span class="caps">HTTP</span> server - needed for deployment to Heroku</li>
<li>itsdangerous is used to sign data for usage in untrusted environments</li>
<li>six is the python 2/3 compatibility layer</li>
</ul>
</div>
<div class="section" id="implementing-the-rest-service">
<h2><a class="toc-backref" href="#toc-entry-3">Implementing the <span class="caps">REST</span> service</a></h2>
<p>Instead of using just one single file for our Flask web application, we will create a python module to contain it and a
file named <tt class="docutils literal">runserver.py</tt> that will start a local development server to test it:</p>
<p>So, in the same folder as the <tt class="docutils literal">requirements.txt</tt> create a folder named <tt class="docutils literal">flask_rest_service</tt> and in there put
two files: <tt class="docutils literal">__init__.py</tt> and <tt class="docutils literal">resources.py</tt>.</p>
<p>The <tt class="docutils literal">__init__.py</tt> initializes our Flask application, our mongoDB connection and our Flask-RESTful api:</p>
<pre class="code literal-block">
import os
from flask import Flask
from flask.ext import restful
from flask.ext.pymongo import PyMongo
from flask import make_response
from bson.json_util import dumps
MONGO_URL = os.environ.get('MONGO_URL')
if not MONGO_URL:
MONGO_URL = "mongodb://localhost:27017/rest";
app = Flask(__name__)
app.config['MONGO_URI'] = MONGO_URL
mongo = PyMongo(app)
def output_json(obj, code, headers=None):
resp = make_response(dumps(obj), code)
resp.headers.extend(headers or {})
return resp
DEFAULT_REPRESENTATIONS = {'application/json': output_json}
api = restful.Api(app)
api.representations = DEFAULT_REPRESENTATIONS
import flask_rest_service.resources
</pre>
<p>So what happens here? After the imports, we check if we have a MONGO_URL environment variable. This is
how we set options in Heroku. If such option does not exist in the environment then we are in our
development environment so we set it to the localhost (we must have a running mongoDB installation in
our dev environment).</p>
<p>In the next lines, we initialize our Flask application and our mongoDB connection (pymongo
uses a <tt class="docutils literal">MONGO_URI</tt> configuration option to know the database <span class="caps">URI</span>).</p>
<p>The <tt class="docutils literal">output_json</tt> is used to dump the <span class="caps">BSON</span> encoded mongoDB objects to <span class="caps">JSON</span> and was borrowed from
<a class="reference external" href="http://blog.alienretro.com/using-mongodb-with-flask-restful/">alienretro’s blog</a> — we initialize our restful <span class="caps">REST</span> <span class="caps">API</span> with this function.</p>
<p>Finally, we import the <tt class="docutils literal">resources.py</tt> module which actually defines our <span class="caps">REST</span> resources.</p>
<pre class="code literal-block">
import json
from flask import request, abort
from flask.ext import restful
from flask.ext.restful import reqparse
from flask_rest_service import app, api, mongo
from bson.objectid import ObjectId
class ReadingList(restful.Resource):
def __init__(self, *args, **kwargs):
self.parser = reqparse.RequestParser()
self.parser.add_argument('reading', type=str)
super(ReadingList, self).__init__()
def get(self):
return [x for x in mongo.db.readings.find()]
def post(self):
args = self.parser.parse_args()
if not args['reading']:
abort(400)
jo = json.loads(args['reading'])
reading_id = mongo.db.readings.insert(jo)
return mongo.db.readings.find_one({"_id": reading_id})
class Reading(restful.Resource):
def get(self, reading_id):
return mongo.db.readings.find_one_or_404({"_id": reading_id})
def delete(self, reading_id):
mongo.db.readings.find_one_or_404({"_id": reading_id})
mongo.db.readings.remove({"_id": reading_id})
return '', 204
class Root(restful.Resource):
def get(self):
return {
'status': 'OK',
'mongo': str(mongo.db),
}
api.add_resource(Root, '/')
api.add_resource(ReadingList, '/readings/')
api.add_resource(Reading, '/readings/<ObjectId:reading_id>')
</pre>
<p>Here we define three <tt class="docutils literal">Resource</tt> classes and add them to our previously defined <tt class="docutils literal">api</tt>: <tt class="docutils literal">Root</tt>, <tt class="docutils literal">Reading</tt> and <tt class="docutils literal">ReadingList</tt>.</p>
<p><tt class="docutils literal">Root</tt> just returns a dictionary with an <span class="caps">OK</span> status and some info on our mongodb connection.</p>
<p><tt class="docutils literal">Reading</tt> has gets an ObjectId
(which is the mongodb primary key) as a parameter and depending on the <span class="caps">HTTP</span> operation, it returns the reading with that
id when receiving an <tt class="docutils literal"><span class="caps">HTTP</span> <span class="caps">GET</span></tt> and deletes the reading with that id when receiving an <tt class="docutils literal"><span class="caps">HTTP</span> <span class="caps">DELETE</span></tt>.</p>
<p><tt class="docutils literal">ReadingList</tt> will return all readings when receiving an <tt class="docutils literal"><span class="caps">HTTP</span> <span class="caps">GET</span></tt> and will create a new reading when
receiving an <tt class="docutils literal"><span class="caps">HTTP</span> <span class="caps">POST</span></tt> The <tt class="docutils literal">post</tt> function uses the parser defined in <tt class="docutils literal">__init__</tt> which requires
a <tt class="docutils literal">reading</tt> parameter with the actual reading to be inserted.</p>
</div>
<div class="section" id="testing-it-locally">
<h2><a class="toc-backref" href="#toc-entry-4">Testing it locally</a></h2>
<p>In order to run the development server, you will need to install and start mongodb locally which is beyond the scope of this post. After that
create a file named <tt class="docutils literal">runserver.py</tt> in the same folder as with the <tt class="docutils literal">requirements.txt</tt>
and the <tt class="docutils literal">flask_rest_service</tt> folder. The contents of this file should be:</p>
<pre class="code literal-block">
from flask_rest_service import app
app.run(debug=True)
</pre>
<p>When you run this file with <tt class="docutils literal">python runserver.py</tt> you should be able top visit your rest service at <a class="reference external" href="http://localhost:5000">http://localhost:5000</a> and get
an “<span class="caps">OK</span>” status.</p>
</div>
<div class="section" id="deploying-to-heroku">
<h2><a class="toc-backref" href="#toc-entry-5">Deploying to Heroku</a></h2>
<p>To deploy to Heroku, you must create a <tt class="docutils literal">Procfile</tt> that contains the workers of your application. In our case, the
<tt class="docutils literal">Procfile</tt> should contain the following:</p>
<pre class="code literal-block">
web: gunicorn flask_rest_service:app
</pre>
<p>Also, you should add a .gitignore file with the following:</p>
<pre class="code literal-block">
*.pyc
</pre>
<p>Finally, to deploy your application to Heroku you can follow the instructions here: <a class="reference external" href="https://devcenter.heroku.com/articles/getting-started-with-python">https://devcenter.heroku.com/articles/getting-started-with-python</a>:</p>
<ul class="simple">
<li>Initialize a git repository and commit everything:</li>
</ul>
<pre class="code literal-block">
git init
git add .
git commit -m
</pre>
<ul class="simple">
<li>Create a new Heroku application (after logging in to heroku with <tt class="docutils literal">heroku login</tt>) and set the MONGO_URL environment variable (of course you have to obtain ths MONGO_URL variable for your heroku envirotnment by adding a mongoDB database):</li>
</ul>
<pre class="code literal-block">
heroku create
heroku config:set MONGO_URL=mongodb://user:pass@mongoprovider.com:27409/rest
</pre>
<ul class="simple">
<li>And finally push your master branch to the heroku remote repository:</li>
</ul>
<pre class="code literal-block">
git push heroku master
</pre>
<p>If everything went ok you should be able to start a worker for your application, check that the worker is running, and finally visit it:</p>
<pre class="code literal-block">
heroku ps:scale web=1
heroku ps
heroku open
</pre>
<p>If everything was ok you should see an status-<span class="caps">OK</span> <span class="caps">JSON</span> !</p>
</div>
Django generic FormViews for objects2014-04-11T10:23:00+03:002014-04-11T10:23:00+03:00Serafeim Papastefanostag:spapas.github.io,2014-04-11:/2014/04/11/django-generic-formviews-for-objects/<p class="first last">An implementation <span class="amp">&</span> discussion of generic FormViews for acting on model instances</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#a-quick-introduction-to-the-formview" id="toc-entry-2">A quick introduction to the <tt class="docutils literal">FormView</tt></a></li>
<li><a class="reference internal" href="#a-quick-introduction-to-the-singleobjectmixin" id="toc-entry-3">A quick introduction to the <tt class="docutils literal">SingleObjectMixin</tt></a></li>
<li><a class="reference internal" href="#being-generic-and-dry" id="toc-entry-4">Being generic and <span class="caps">DRY</span></a></li>
<li><a class="reference internal" href="#being-more-generic-and-dry" id="toc-entry-5">Being more generic and <span class="caps">DRY</span></a></li>
<li><a class="reference internal" href="#other-options" id="toc-entry-6">Other options</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>We recently needed to create a number of views for changing the status of an Application model instance for our organization.
An Application model instance can be filled and then cancelled, submitted, acceptted etc - for each of these status changes a form should be
presented to the user. When the user submits the form the status of the Application will be changed.</p>
<p>To implement the above requirement we created a generic FormView that acts on the specific model instance. This
used two basic <span class="caps">CBV</span> components: The <tt class="docutils literal">FormView</tt> for the form manipulation and the <tt class="docutils literal">SingleObjectMixing</tt> for the
object handling.</p>
<p>Django <a class="reference external" href="https://docs.djangoproject.com/en/1.6/topics/class-based-views/">Class Based Views</a> (CBVs) can be used to create reusable Views using normal class inheritance. Most
people use the well-known <tt class="docutils literal">CreateView</tt>, <tt class="docutils literal">UpdateView</tt>, <tt class="docutils literal">DetailView</tt> and <tt class="docutils literal">ListView</tt>, however, as we
will see below, the <tt class="docutils literal">FormView</tt> will help us write <span class="caps">DRY</span> code.</p>
<p>I have to notice here that an invaluable tool to help you understanding CBVs is the <a class="reference external" href="http://ccbv.co.uk/"><span class="caps">CBV</span> inspector</a> which
has a nice web interface for browsing the <span class="caps">CBV</span> hierarchies, attributes and methods.</p>
</div>
<div class="section" id="a-quick-introduction-to-the-formview">
<h2><a class="toc-backref" href="#toc-entry-2">A quick introduction to the <tt class="docutils literal">FormView</tt></a></h2>
<p>A simple <tt class="docutils literal">FormView</tt> can be defined like this (<a class="reference external" href="http://ccbv.co.uk/projects/Django/1.6/django.views.generic.edit/FormView/"><span class="caps">CBV</span> FormView</a>):</p>
<pre class="code literal-block">
class MyFormView(FormView):
form_class = forms.MyFormView
template_name = 'my_template.html'
</pre>
<p>The above can be used in urls.py like this:</p>
<pre class="code literal-block">
urlpatterns = patterns('',
url(r'^my_formview/$', views.MyFormView.as_view() , name='my_formview' ),
</pre>
<p>This will present a form to the user when he visits the <tt class="docutils literal">my_formview</tt> url — however this form won’t do anything. To allow
the form to actually do something when it’s been submitted we need to override the <tt class="docutils literal">form_valid</tt> method.</p>
<pre class="code literal-block">
def form_valid(self, form):
value = form.cleaned_data['value']
messages.info(self.request, "MyForm submitted with value {0}!".format(value) )
return HttpResponseRedirect( reverse('my_formview') )
</pre>
<p>As you can see the submitted form is passed in the method and can be used to receive its <tt class="docutils literal">cleaned_data</tt>. The <tt class="docutils literal">FormView</tt>
has various other options for instance a <tt class="docutils literal">form_invalid</tt> method, an <tt class="docutils literal">initial</tt> attribute to set the initial values for the form etc.</p>
</div>
<div class="section" id="a-quick-introduction-to-the-singleobjectmixin">
<h2><a class="toc-backref" href="#toc-entry-3">A quick introduction to the <tt class="docutils literal">SingleObjectMixin</tt></a></h2>
<p>A <tt class="docutils literal">SingleObjectMixin</tt> adds a number of attributes <span class="amp">&</span> methods to a view that can be used for object manipulation. The
most important ones is the <tt class="docutils literal">model</tt> and <tt class="docutils literal">queryset</tt> attributes and the <tt class="docutils literal">get_queryset</tt> and <tt class="docutils literal">get_object()</tt>. To use
the <tt class="docutils literal">SingleObjectMixin</tt> in your <span class="caps">CBV</span> just add it to the list of the classes to inherit from and define either the
<tt class="docutils literal">model</tt> or the <tt class="docutils literal">queryset</tt> attribute. After that you may pass a <tt class="docutils literal">pk</tt> parameter to your view and you will get an
<tt class="docutils literal">object</tt> context variable in the template with the selected object!</p>
</div>
<div class="section" id="being-generic-and-dry">
<h2><a class="toc-backref" href="#toc-entry-4">Being generic and <span class="caps">DRY</span></a></h2>
<p>We can more or less now understand how we should use <tt class="docutils literal">FormView</tt> and <tt class="docutils literal">SingleObjectMixin</tt> to generate our
generic <tt class="docutils literal">FormView</tt> for acting on objects: Our <tt class="docutils literal">FormView</tt> should <em>get</em> the object using the <tt class="docutils literal">SingleObjectMixin</tt>
and change it when the form is submitted using the values from the form. A first implementation would be the following:</p>
<pre class="code literal-block">
class GenericObjectFormView1(FormView, SingleObjectMixin):
def form_valid(self, form):
obj = self.get_object()
obj.change_status(form)
return HttpResponseRedirect( obj.get_absolute_url() )
</pre>
<p>So our <tt class="docutils literal">GenericObjectFormView1</tt> class inherits from <tt class="docutils literal">FormView</tt> and <tt class="docutils literal">SingleObjectMixin</tt>. The only thing that we have
to assure is that the Model we want to act on needs to implement a <tt class="docutils literal">change_status</tt> method which gets the <tt class="docutils literal">form</tt> and
changes the status of that object based on its value. For instance, two implementations can be the following:</p>
<pre class="code literal-block">
class CancelObjectFormView(GenericObjectFormView1):
template_name = 'cancel.html'
form_class = forms.CancelForm
model = models.Application
class SubmitObjectFormView(GenericObjectFormView1):
template_name = 'submit.html'
form_class = forms.SubmitForm
model = models.Application
</pre>
</div>
<div class="section" id="being-more-generic-and-dry">
<h2><a class="toc-backref" href="#toc-entry-5">Being more generic and <span class="caps">DRY</span></a></h2>
<p>The previous implementation has two problems:</p>
<ul class="simple">
<li>What happens if the status of the object should not be changed even if the form <em>is</em> valid?</li>
<li>We shouldn’t need to create a new template for every new <tt class="docutils literal">GenericObjectFormView</tt> since all these templates will just output the object information, ask a question for the status change and output the form.</li>
</ul>
<p>Let’s write a new version of our GenericObjectFormView that actually resolves these:</p>
<pre class="code literal-block">
class GenericObjectFormView2(FormView, SingleObjectMixin):
template_name = 'generic_formview.html'
ok_message = ''
not_ok_message = ''
title = ''
question =''
def form_valid(self, form):
obj = self.get_object()
r = obj.change_status(form)
if r:
messages.info(self.request, self.yes_message)
else:
messages.info(self.request, self.not_ok_message)
return HttpResponseRedirect( obj.get_absolute_url() )
def get_context_data(self, **kwargs):
context = super(GenericYesNoFormView2, self).get_context_data(**kwargs)
context['title'] = self.title
context['question'] = self.question
return context
</pre>
<p>The above adds an ok and not ok message which will be outputed if the status can or cannot be changed. To accomplish this,
the <tt class="docutils literal">change_status</tt> method should now return a boolean value to mark if the action was ok or not. Also, a generic template
will now be used. This template has two placeholders: One for the title of the page (<tt class="docutils literal">title</tt> attribute) and one for the
question asked to the user (<tt class="docutils literal">question</tt> attribute). Now we can use it like this:</p>
<pre class="code literal-block">
class CancelObjectFormView(GenericObjectFormView2):
form_class = forms.CancelForm
model = models.Application
ok_message = 'Cancel success!'
not_ok_message = 'Not able to cancel!'
title = 'Cancel an object'
question = 'Do you want to cancel this object?'
class SubmitObjectFormView(GenericObjectFormView2):
form_class = forms.SubmitForm
model = models.Application
ok_message = 'Submit ok'
not_ok_message = 'Cannot submit!'
title = 'Submit an object'
question ='Do you want to submit this object?'
</pre>
</div>
<div class="section" id="other-options">
<h2><a class="toc-backref" href="#toc-entry-6">Other options</a></h2>
<p>We’ve just got a glimpse of how we can use CBVs to increase the DRYness of our Django applications. There are various
extra things that we can add to our <tt class="docutils literal">GenericObjectFormView2</tt> as attributes which will be defined by inheriting
classes. Some ideas is to check if the current user actually has access to modify the object (hint: override the
<tt class="docutils literal">get_object</tt> method of <tt class="docutils literal">SingleObjectMixin</tt>) or render the form diffirently depending on the current user (hint:
override the <tt class="docutils literal">get_form_kwargs</tt> method of <tt class="docutils literal">FormView</tt>).</p>
</div>
A Wagtail tutorial2014-02-13T21:20:00+02:002014-02-13T21:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2014-02-13:/2014/02/13/wagtail-tutorial/<p>This is a from scratch tutorial for creating a wagtail based Blog.</p><p><a href="http://wagtail.io">Wagtail</a> is a new Open Source <a href="https://www.djangoproject.com">Django</a>-based <span class="caps">CMS</span>. In this 20 minute tutorial we will see how you can create a blog from scratch using Wagtail. If you want to see some more examples of usage please take a look at the <a href="https://github.com/torchbox/wagtaildemo">wagtaildemo</a> GitHub project.</p>
<p>To follow this tutorial you will need to have <a href="http://python.org/">Python</a> 2.7 installed with a working version of <a href="https://pypi.python.org/pypi/pip">pip</a> and <a href="https://pypi.python.org/pypi/virtualenv">virtualenv</a>.</p>
<p><strong>Update</strong>: The result of this tutorial has been deployed to <a href="https://www.heroku.com/">Heroku</a>: <a href="http://gentle-refuge-2590.herokuapp.com/">http://gentle-refuge-2590.herokuapp.com/</a> - you may visit the <a href="http://gentle-refuge-2590.herokuapp.com/admin/">admin site</a> and login with root / 123 to play with Wagtail! Please don’t do anything naughty !!! Also, notice that <a href="https://devcenter.heroku.com/articles/dynos#isolation-and-security">because of how Heroku works</a> you won’t be able to upload anything.</p>
<p><strong>Update 08/09/2015: This tutorial has been written during the first days of Wagtail, when documentation and tutorials about it were sparce; right now it should be considered by all accounts obsolete and <em>not</em> be followed! Instead, you should read the official (and very well written) tutorial @ http://docs.wagtail.io/en/latest/getting_started/tutorial.html in the official Wagtail documentation!</strong></p>
<h2 id="installing-the-wagtail-dependencies">Installing the wagtail dependencies</h2>
<p>It is recomended to create a new virtual environment that will host the wagtail tutorial. After you have changed to the virtual environment you will need to installl the Wagtail requirements. Create a file named <code>requirements.txt</code> containing the following:</p>
<div class="highlight"><pre><span></span><code>Django==1.6.2
South==1.0.0
django-compressor==1.4
django-modelcluster==0.3
-e git://github.com/torchbox/wagtail.git#egg=wagtail
django-taggit==0.11.2
django-libsass==0.2
</code></pre></div>
<p>and run
<code>pip install -r requirements.txt</code>. If you use Microsoft Windows you <em>will</em> experience problems with Pillow and lxml. Please download the
installation executables from https://pypi.python.org/pypi/Pillow/2.3.0 and https://pypi.python.org/pypi/lxml/3.3.1, install
them using <code>easy_install Pillow-2.3.0.x-py2.7.exe</code> and <code>easy_install lxml-3.3.1.x-py2.7.exe</code> (from inside your virtual environment)
and then install the other requirements. Also please use the latest version of Wagtail (hosted on github) because it has some changes from
the pypi (so <em>don’t</em> do a <code>pip install wagtail</code>).</p>
<p><strong>Warning:</strong> Unfortuanately, no official binaries for libsass (which is a django-libsass requirement) are (yet) available for Windows. I
have compiled a version for win32 and python 2.7 (which I use) using the <a href="http://www.confusedbycode.com/articles/compiling-python-modules.html">instructions found here</a>. You can download this version
<a href="https://spapas.github.io/images/libsass-0.3.0-cp27-none-win32.whl">as a wheel package</a>. Notice that because libsass was compiled with <span class="caps">VC</span> Express 2013
you should also install the <a href="http://www.microsoft.com/en-us/download/details.aspx?id=40784">Visual C++ 2013 redistributable</a> package from Microsoft.</p>
<p><em>Please use it at your own risk !!!</em></p>
<p>To install <a href="http://pythonwheels.com/">wheels</a> you have to use a version of pip >= 1.4 (so do an <code>easy_install -U pip</code> from your
virtual environment if you have a previous version) and then you can just do a normal <code>pip install libsass-0.3.0-cp27-none-win32.whl</code>. </p>
<p><strong>Warning no2:</strong> More unfortuanately, there seem to be <a href="https://github.com/torchbox/wagtail/issues/133">a number of issues</a> with how libsass handles <code>@import</code> statements
in Windows. Until this is fixed, Windows users are recommened to use the command line (Ruby) sass compiler. To use it, please
install Ruby in your system and then install sass with <code>gem install sass -v ">=3.3.0alpha" --pre</code>. After that, in the <code>COMPRESS_PRECOMPILERS</code>
setting of your <code>settings.py</code> (discussed in the next section), change the line <code>('text/x-scss', 'django_libsass.SassCompiler'),</code> to
<code>('text/x-scss', 'sass --scss {infile} {outfile}'),</code>.</p>
<h2 id="creating-and-configuring-your-project">Creating and configuring your project</h2>
<p>Wagtail has to “live” inside a normal Django project so you now may create a new Django project by issuing:</p>
<div class="highlight"><pre><span></span><code>python<span class="w"> </span><PATH_OF_YOUR_VIRTUAL_ENV>/scripts/django-admin.py<span class="w"> </span>startproject<span class="w"> </span>wagtailtutorial
</code></pre></div>
<p>If you use Unix you can just run <code>django-admin.py</code> etc however if you try to do the same in windows you
will find out that windows tries to run the .py file with the python executable that is assigned through
explorer (which of course is your main python installation) and <em>not</em> through your path! That’s why
all our commands will be in the form <code>python script.py</code> to make sure that Windows picks the python
executable from your path (which, if you’re inside the virtual enviroment will be the correct one).</p>
<p>Inside the <code>wagtailtutorial</code> folder you will see a file named <code>manage.py</code> and another folder named <code>wagtailtutorial</code>. Inside this <code>wagtailtutorial</code> folder you will find <code>settings.py</code> and <code>urls.py</code> which need to be changed.</p>
<p>Starting with <code>urls.py</code>, remove everything and change it like this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">patterns</span><span class="p">,</span> <span class="n">include</span><span class="p">,</span> <span class="n">url</span>
<span class="kn">from</span> <span class="nn">django.conf.urls.static</span> <span class="kn">import</span> <span class="n">static</span>
<span class="kn">from</span> <span class="nn">django.views.generic.base</span> <span class="kn">import</span> <span class="n">RedirectView</span>
<span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">settings</span>
<span class="kn">import</span> <span class="nn">os.path</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailcore</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtail_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailadmin</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtailadmin_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailimages</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtailimages_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailembeds</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtailembeds_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtaildocs</span> <span class="kn">import</span> <span class="n">admin_urls</span> <span class="k">as</span> <span class="n">wagtaildocs_admin_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtaildocs</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtaildocs_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailsnippets</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtailsnippets_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailsearch.urls</span> <span class="kn">import</span> <span class="n">frontend</span> <span class="k">as</span> <span class="n">wagtailsearch_frontend_urls</span><span class="p">,</span> <span class="n">admin</span> <span class="k">as</span> <span class="n">wagtailsearch_admin_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailusers</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtailusers_urls</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailredirects</span> <span class="kn">import</span> <span class="n">urls</span> <span class="k">as</span> <span class="n">wagtailredirects_urls</span>
<span class="n">admin</span><span class="o">.</span><span class="n">autodiscover</span><span class="p">()</span>
<span class="c1"># Signal handlers</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailsearch</span> <span class="kn">import</span> <span class="n">register_signal_handlers</span> <span class="k">as</span> <span class="n">wagtailsearch_register_signal_handlers</span>
<span class="n">wagtailsearch_register_signal_handlers</span><span class="p">()</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="n">patterns</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^django-admin/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/images/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailimages_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/embeds/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailembeds_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/documents/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtaildocs_admin_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/snippets/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailsnippets_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/search/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailsearch_admin_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/users/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailusers_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/redirects/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailredirects_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^admin/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailadmin_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^search/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtailsearch_frontend_urls</span><span class="p">)),</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^documents/'</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtaildocs_urls</span><span class="p">)),</span>
<span class="c1"># For anything not caught by a more specific rule above, hand over to</span>
<span class="c1"># Wagtail's serving mechanism</span>
<span class="n">url</span><span class="p">(</span><span class="sa">r</span><span class="s1">''</span><span class="p">,</span> <span class="n">include</span><span class="p">(</span><span class="n">wagtail_urls</span><span class="p">)),</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">settings</span><span class="o">.</span><span class="n">DEBUG</span><span class="p">:</span>
<span class="kn">from</span> <span class="nn">django.contrib.staticfiles.urls</span> <span class="kn">import</span> <span class="n">staticfiles_urlpatterns</span>
<span class="n">urlpatterns</span> <span class="o">+=</span> <span class="n">staticfiles_urlpatterns</span><span class="p">()</span> <span class="c1"># tell gunicorn where static files are in dev mode</span>
<span class="n">urlpatterns</span> <span class="o">+=</span> <span class="n">static</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">MEDIA_URL</span> <span class="o">+</span> <span class="s1">'images/'</span><span class="p">,</span> <span class="n">document_root</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">MEDIA_ROOT</span><span class="p">,</span> <span class="s1">'images'</span><span class="p">))</span>
</code></pre></div>
<p>You can se that there is a signal handler when a searchable thing is added or changed to handle indexing for search, normal django admin is mapped under /django-admin since /admin is use for Wagtail (of course you may map Wagtail wherever you’d like), inclusion of various wagtail related urls and finally a Wagtail handling everything else. Finally there are some handlers for media and static files.</p>
<p>After that please change your <code>settings.py</code> like this:</p>
<div class="highlight"><pre><span></span><code><span class="c1"># Django settings for wagtailtutorial project.</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="n">PROJECT_ROOT</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span> <span class="s1">'..'</span><span class="p">,</span> <span class="s1">'..'</span><span class="p">)</span>
<span class="n">DEBUG</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">TEMPLATE_DEBUG</span> <span class="o">=</span> <span class="n">DEBUG</span>
<span class="n">ADMINS</span> <span class="o">=</span> <span class="p">()</span>
<span class="n">MANAGERS</span> <span class="o">=</span> <span class="n">ADMINS</span>
<span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'ENGINE'</span><span class="p">:</span> <span class="s1">'django.db.backends.sqlite3'</span><span class="p">,</span>
<span class="s1">'NAME'</span><span class="p">:</span> <span class="n">PROJECT_ROOT</span><span class="o">+</span><span class="s1">'/wagtailtutorial.db'</span><span class="p">,</span>
<span class="s1">'USER'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="s1">'PASSWORD'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
<span class="s1">'HOST'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="c1"># Set to empty string for localhost.</span>
<span class="s1">'PORT'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span> <span class="c1"># Set to empty string for default.</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">CONN_MAX_AGE</span> <span class="o">=</span> <span class="mi">600</span> <span class="c1"># number of seconds database connections should persist for</span>
<span class="n">ALLOWED_HOSTS</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">TIME_ZONE</span> <span class="o">=</span> <span class="s1">'Europe/London'</span>
<span class="n">LANGUAGE_CODE</span> <span class="o">=</span> <span class="s1">'en-gb'</span>
<span class="n">SITE_ID</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">USE_I18N</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">USE_L10N</span> <span class="o">=</span> <span class="kc">False</span>
<span class="n">USE_TZ</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">MEDIA_ROOT</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">PROJECT_ROOT</span><span class="p">,</span> <span class="s1">'media'</span><span class="p">)</span>
<span class="n">MEDIA_URL</span> <span class="o">=</span> <span class="s1">'/media/'</span>
<span class="n">STATIC_ROOT</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">PROJECT_ROOT</span><span class="p">,</span> <span class="s1">'static'</span><span class="p">)</span>
<span class="n">STATIC_URL</span> <span class="o">=</span> <span class="s1">'/static/'</span>
<span class="n">STATICFILES_DIRS</span> <span class="o">=</span> <span class="p">()</span>
<span class="n">STATICFILES_FINDERS</span> <span class="o">=</span> <span class="p">(</span>
<span class="s1">'django.contrib.staticfiles.finders.FileSystemFinder'</span><span class="p">,</span>
<span class="s1">'django.contrib.staticfiles.finders.AppDirectoriesFinder'</span><span class="p">,</span>
<span class="s1">'compressor.finders.CompressorFinder'</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">SECRET_KEY</span> <span class="o">=</span> <span class="s1">'wq21wtjo3@d_qfjvd-#td!</span><span class="si">%7g</span><span class="s1">fy2updj2z+nev^k$iy%=m4_tr'</span>
<span class="n">TEMPLATE_LOADERS</span> <span class="o">=</span> <span class="p">(</span>
<span class="s1">'django.template.loaders.filesystem.Loader'</span><span class="p">,</span>
<span class="s1">'django.template.loaders.app_directories.Loader'</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">MIDDLEWARE_CLASSES</span> <span class="o">=</span> <span class="p">(</span>
<span class="s1">'django.middleware.common.CommonMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.sessions.middleware.SessionMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.csrf.CsrfViewMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.auth.middleware.AuthenticationMiddleware'</span><span class="p">,</span>
<span class="s1">'django.contrib.messages.middleware.MessageMiddleware'</span><span class="p">,</span>
<span class="s1">'django.middleware.clickjacking.XFrameOptionsMiddleware'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailcore.middleware.SiteMiddleware'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailredirects.middleware.RedirectMiddleware'</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">django.conf</span> <span class="kn">import</span> <span class="n">global_settings</span>
<span class="n">TEMPLATE_CONTEXT_PROCESSORS</span> <span class="o">=</span> <span class="n">global_settings</span><span class="o">.</span><span class="n">TEMPLATE_CONTEXT_PROCESSORS</span> <span class="o">+</span> <span class="p">(</span>
<span class="s1">'django.core.context_processors.request'</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">ROOT_URLCONF</span> <span class="o">=</span> <span class="s1">'wagtailtutorial.urls'</span>
<span class="n">WSGI_APPLICATION</span> <span class="o">=</span> <span class="s1">'wagtailtutorial.wsgi.application'</span>
<span class="n">TEMPLATE_DIRS</span> <span class="o">=</span> <span class="p">()</span>
<span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">(</span>
<span class="s1">'django.contrib.auth'</span><span class="p">,</span>
<span class="s1">'django.contrib.contenttypes'</span><span class="p">,</span>
<span class="s1">'django.contrib.sessions'</span><span class="p">,</span>
<span class="c1"># 'django.contrib.sites', # Wagtail uses its own site management logic</span>
<span class="s1">'django.contrib.messages'</span><span class="p">,</span>
<span class="s1">'django.contrib.staticfiles'</span><span class="p">,</span>
<span class="s1">'south'</span><span class="p">,</span>
<span class="s1">'compressor'</span><span class="p">,</span>
<span class="s1">'taggit'</span><span class="p">,</span>
<span class="s1">'modelcluster'</span><span class="p">,</span>
<span class="s1">'django.contrib.admin'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailcore'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailadmin'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtaildocs'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailsnippets'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailusers'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailimages'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailembeds'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailsearch'</span><span class="p">,</span>
<span class="s1">'wagtail.wagtailredirects'</span><span class="p">,</span>
<span class="s1">'tutorial'</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">EMAIL_SUBJECT_PREFIX</span> <span class="o">=</span> <span class="s1">'[wagtailtutorial] '</span>
<span class="n">INTERNAL_IPS</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'127.0.0.1'</span><span class="p">,</span> <span class="s1">'10.0.2.2'</span><span class="p">)</span>
<span class="n">COMPRESS_PRECOMPILERS</span> <span class="o">=</span> <span class="p">(</span>
<span class="p">(</span><span class="s1">'text/x-scss'</span><span class="p">,</span> <span class="s1">'django_libsass.SassCompiler'</span><span class="p">),</span>
<span class="p">)</span>
<span class="c1"># Auth settings</span>
<span class="n">LOGIN_URL</span> <span class="o">=</span> <span class="s1">'django.contrib.auth.views.login'</span>
<span class="n">LOGIN_REDIRECT_URL</span> <span class="o">=</span> <span class="s1">'wagtailadmin_home'</span>
<span class="n">LOGGING</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'version'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="s1">'disable_existing_loggers'</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
<span class="s1">'filters'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'require_debug_false'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'()'</span><span class="p">:</span> <span class="s1">'django.utils.log.RequireDebugFalse'</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'mail_admins'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'ERROR'</span><span class="p">,</span>
<span class="s1">'filters'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'require_debug_false'</span><span class="p">],</span>
<span class="s1">'class'</span><span class="p">:</span> <span class="s1">'django.utils.log.AdminEmailHandler'</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="s1">'loggers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'django.request'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'mail_admins'</span><span class="p">],</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'ERROR'</span><span class="p">,</span>
<span class="s1">'propagate'</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1"># WAGTAIL SETTINGS</span>
<span class="n">WAGTAIL_SITE_NAME</span> <span class="o">=</span> <span class="s1">'wagtailtutorial'</span>
<span class="c1"># Override the search results template for wagtailsearch</span>
<span class="n">WAGTAILSEARCH_RESULTS_TEMPLATE</span> <span class="o">=</span> <span class="s1">'tutorial/search_results.html'</span>
<span class="n">WAGTAILSEARCH_RESULTS_TEMPLATE_AJAX</span> <span class="o">=</span> <span class="s1">'tutorial/includes/search_listing.html'</span>
<span class="n">WAGTAILSEARCH_ES_INDEX</span> <span class="o">=</span> <span class="s1">'wagtailtutorial'</span>
</code></pre></div>
<p>The most important thing to notice is that the <code>INSTALLED_APPS</code> contains the usual apps from django.*, <a href="http://south.aeracode.org/">south</a> for database migrations, <a href="https://github.com/django-compressor/django-compressor">django-compressor</a> to support compressing static files (and automatic translating from less to css), <a href="https://github.com/alex/django-taggit">django-taggit</a> to add support for tags, and <a href="https://github.com/torchbox/django-modelcluster/">django-modelcluster</a> which adds support from clusters (groups) of models. It also contains the wagtail.* applications and the tutorial application which is where we will create our blog. Also there are two Wagtail related middleware (one to add a site attribute to each request and one to hand redirects), configuring django-compressor to use <code>django_libsass</code> to compile <code>less</code> files, and some other, not so important Wagtail settings.</p>
<p>So let’s create the missing tutorial application by issuing:</p>
<div class="highlight"><pre><span></span><code>python<span class="w"> </span>manage.py<span class="w"> </span>startapp<span class="w"> </span>tutorial
</code></pre></div>
<p>Now we’ll have a <code>tutorial</code> folder waiting to define our blog structure inside the <code>wagtailtutorial</code> folder!</p>
<h2 id="checking-to-see-if-everything-works">Checking to see if everything works</h2>
<p>Before continuing with our blog creation let’s make sure that everything works, first of all by generating the database schema:</p>
<div class="highlight"><pre><span></span><code>python<span class="w"> </span>manage.py<span class="w"> </span>syncdb
</code></pre></div>
<p>In the superuser question answer yes and add a superuser. Then you can run the migrations - however because there are some problems with the way SQLite3 runs migrations it is recommended to run the migrations in two steps:</p>
<div class="highlight"><pre><span></span><code>python<span class="w"> </span>manage.py<span class="w"> </span>migrate<span class="w"> </span><span class="m">0001</span><span class="w"> </span>--all
python<span class="w"> </span>manage.py<span class="w"> </span>migrate
</code></pre></div>
<p>Finally, you now may try a</p>
<div class="highlight"><pre><span></span><code>python<span class="w"> </span>manage.py<span class="w"> </span>runserver
</code></pre></div>
<p>and visit <code>http://127.0.0.1:8000</code>. If everything worked fine you will get a</p>
<blockquote>
<p>Welcome to your new Wagtail site!</p>
</blockquote>
<p>page — congratulations !</p>
<p>The homepage is rather simple (for now!) but you may already navigate to http://127.0.0.1:8000/admin and from there, login to Wagtail admin with the superuser you created earlier.
Now you may start experiencing Wagtail ! </p>
<p><img alt="Wagtail admin index" src="https://spapas.github.io/images/wagtail-index.png"></p>
<h2 id="exploring-wagtail-admin">Exploring Wagtail admin</h2>
<p>When you login to Wagtail admin you will see a menu at the left with the options:</p>
<ul>
<li>Explorer is used to actually manage the content of your site</li>
<li>Search is used for searching through your content</li>
<li>Images is used to manage your images</li>
<li>Documents is used to manage your Documents</li>
<li>Snippets is used for side bars etc</li>
<li>Users is used for User management</li>
<li>Redirects is used to redirect to a specific page</li>
<li>Editors picks is used to promote search results</li>
</ul>
<p>Of the previous, the one needing more explanation is Explorer: Clicking it you will see that a label named “Welcome to your Wagtail site!” will open. This is the root Page of your site. If you click at it you will go to the actions of this page. The actions you can do here is:
- Add Child Page
- Edit
- View Live
- Move
- Delete
- Unpublish</p>
<p>The Pages (and more generally the Content) of Wagtail are Django Models that are saved through a Tree hierarchy. However, if you click “Add Child Page” you won’t be able to add anything because you must create your own Page types (we will see how it is done in the next section). </p>
<p>When you click edit you will be able to edit the parts of the page (each part is a normal Field of the model). Also, you will see that the form is split into two tabs: Content and Promote. In the Content for instance you will see that the “Welcome to your Wagtail site!” has only a “Title” CharField. These are the fields that will be available to all Pages since “Welcome to your Wagtail site!” has a class of <code>Page</code> from which every other page should inherit. After you finish editing a page you may save it as a draft, publish, sent it for moderation (if you don’t have the rights to publish it) etc.</p>
<h2 id="creating-our-blog">Creating our blog</h2>
<p>Each of our Page types is a normal Django Model which inherits from <code>Page</code>. Let’s suppose that our posts should contain a title, a body and a created date. Add the following to <code>tutorials/models.py</code>:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailcore.models</span> <span class="kn">import</span> <span class="n">Page</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailcore.fields</span> <span class="kn">import</span> <span class="n">RichTextField</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailadmin.edit_handlers</span> <span class="kn">import</span> <span class="n">FieldPanel</span>
<span class="k">class</span> <span class="nc">BlogPage</span><span class="p">(</span><span class="n">Page</span><span class="p">):</span>
<span class="n">body</span> <span class="o">=</span> <span class="n">RichTextField</span><span class="p">()</span>
<span class="n">date</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateField</span><span class="p">(</span><span class="s2">"Post date"</span><span class="p">)</span>
<span class="n">search_name</span> <span class="o">=</span> <span class="s2">"Blog Page"</span>
<span class="n">indexed_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'body'</span><span class="p">,</span> <span class="p">)</span>
<span class="n">BlogPage</span><span class="o">.</span><span class="n">content_panels</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'title'</span><span class="p">,</span> <span class="n">classname</span><span class="o">=</span><span class="s2">"full title"</span><span class="p">),</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'date'</span><span class="p">),</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'body'</span><span class="p">,</span> <span class="n">classname</span><span class="o">=</span><span class="s2">"full"</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div>
<p>and run <code>python manage.py syncdb</code> to create the <code>tutorial_blogpage</code> table. </p>
<p>Now, if you visit again the /admin and click on the “Add Child Page” action of “Welcome to your new Wagtail Site!” you will see the “Blog Page” page and after you click it you will be able to see a form with the Fields you defined <em>and</em> title(title, body, date). The title is a field inherited from Page, along with the fields in the Promote tab. </p>
<p>In our declaration of BlogPage we added three <code>FieldPanel</code>s on its content_panes. A <code>FieldPanel</code> is a special edit handler for each Field. That is why when you try to edit the body you will see a rich text toolbar that enables you to not only format text but also embed images, documents and even oembed links. If you hadn’t included the <code>FieldPanel('body', classname="full")</code> then you wouldn’t see the rich text editor.</p>
<p><img alt="Editing pages in Wagtail" src="https://spapas.github.io/images/wagtail-edit-page.png"></p>
<p>So now we can add as many posts as we like! </p>
<p>The time has come to take a look at our fine blog post: After we publish our page we click to the view live action and… </p>
<blockquote>
<p>TemplateDoesNotExist at /hello-world/</p>
<p>tutorial/blog_page.html</p>
</blockquote>
<p>:(</p>
<p>A template seems to be missing — actually we have totally forgotten about the presentation of our blog — we’ll talk about this in the next section !</p>
<p>However, before going there we should also create an Index page type collecting our posts. </p>
<p>For this, we will change our <code>models.py</code> to this:</p>
<div class="highlight"><pre><span></span><code><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailcore.models</span> <span class="kn">import</span> <span class="n">Page</span><span class="p">,</span> <span class="n">Orderable</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailcore.fields</span> <span class="kn">import</span> <span class="n">RichTextField</span>
<span class="kn">from</span> <span class="nn">wagtail.wagtailadmin.edit_handlers</span> <span class="kn">import</span> <span class="n">FieldPanel</span> <span class="p">,</span><span class="n">MultiFieldPanel</span><span class="p">,</span><span class="n">InlinePanel</span><span class="p">,</span> <span class="n">PageChooserPanel</span>
<span class="kn">from</span> <span class="nn">modelcluster.fields</span> <span class="kn">import</span> <span class="n">ParentalKey</span>
<span class="k">class</span> <span class="nc">BlogPage</span><span class="p">(</span><span class="n">Page</span><span class="p">):</span>
<span class="n">body</span> <span class="o">=</span> <span class="n">RichTextField</span><span class="p">()</span>
<span class="n">date</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateField</span><span class="p">(</span><span class="s2">"Post date"</span><span class="p">)</span>
<span class="n">indexed_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'body'</span><span class="p">,</span> <span class="p">)</span>
<span class="n">search_name</span> <span class="o">=</span> <span class="s2">"Blog Page"</span>
<span class="n">BlogPage</span><span class="o">.</span><span class="n">content_panels</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'title'</span><span class="p">,</span> <span class="n">classname</span><span class="o">=</span><span class="s2">"full title"</span><span class="p">),</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'date'</span><span class="p">),</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'body'</span><span class="p">,</span> <span class="n">classname</span><span class="o">=</span><span class="s2">"full"</span><span class="p">),</span>
<span class="p">]</span>
<span class="k">class</span> <span class="nc">LinkFields</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">link_page</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span>
<span class="s1">'wagtailcore.Page'</span><span class="p">,</span>
<span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">related_name</span><span class="o">=</span><span class="s1">'+'</span>
<span class="p">)</span>
<span class="n">panels</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">PageChooserPanel</span><span class="p">(</span><span class="s1">'link_page'</span><span class="p">),</span>
<span class="p">]</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">class</span> <span class="nc">RelatedLink</span><span class="p">(</span><span class="n">LinkFields</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">255</span><span class="p">,</span> <span class="n">help_text</span><span class="o">=</span><span class="s2">"Link title"</span><span class="p">)</span>
<span class="n">panels</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'title'</span><span class="p">),</span>
<span class="n">MultiFieldPanel</span><span class="p">(</span><span class="n">LinkFields</span><span class="o">.</span><span class="n">panels</span><span class="p">,</span> <span class="s2">"Link"</span><span class="p">),</span>
<span class="p">]</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">class</span> <span class="nc">BlogIndexPageRelatedLink</span><span class="p">(</span><span class="n">Orderable</span><span class="p">,</span> <span class="n">RelatedLink</span><span class="p">):</span>
<span class="n">page</span> <span class="o">=</span> <span class="n">ParentalKey</span><span class="p">(</span><span class="s1">'tutorial.BlogIndexPage'</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="s1">'related_links'</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">BlogIndexPage</span><span class="p">(</span><span class="n">Page</span><span class="p">):</span>
<span class="n">intro</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">256</span><span class="p">)</span>
<span class="n">indexed_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'body'</span><span class="p">,</span> <span class="p">)</span>
<span class="n">search_name</span> <span class="o">=</span> <span class="s2">"Blog Index Page"</span>
<span class="n">BlogIndexPage</span><span class="o">.</span><span class="n">content_panels</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'title'</span><span class="p">,</span> <span class="n">classname</span><span class="o">=</span><span class="s2">"full title"</span><span class="p">),</span>
<span class="n">FieldPanel</span><span class="p">(</span><span class="s1">'intro'</span><span class="p">,</span> <span class="n">classname</span><span class="o">=</span><span class="s2">"full"</span><span class="p">),</span>
<span class="n">InlinePanel</span><span class="p">(</span><span class="n">BlogIndexPage</span><span class="p">,</span> <span class="s1">'related_links'</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s2">"Related links"</span><span class="p">),</span>
<span class="p">]</span>
</code></pre></div>
<p>The above adds a way to put <code>related_links</code> to a <code>BlogIndexPage</code> — these related_links are the <code>BlogPage</code>s that actually belong to the blog - so now, we can add a blog index page and add all our blog posts to it!</p>
<p>Now that we’ve created the BlogPage and BlogIndexPage pages it’s time to take a look at how we will actually display our blog…</p>
<h2 id="creating-templates-for-pages">Creating templates for <em>Page</em>s</h2>
<p>A normal Django template can be used to display each page type. Wagtail either generates automatically (by seperating underscors with capital letters in camelcase for instance BlogIndexPage -> blog_index_page) or you can use the template class attribute. Let’s add a <code>templates</code> foler within the <code>tutorial</code> foler and add another folder named <code>tutorial</code> inside <code>templates</code> and ten add a file named <code>blog_index_page.html</code> to <code>templates</code> with the following content (you must have the following hierarchy <code>wagtailtutorial/tutorial/templates/tutorial/blog_index_page.html</code>):</p>
<div class="highlight"><pre><span></span><code><span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>{{ self.title }}<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
Intro: {{ self.intro }}
<span class="p"><</span><span class="nt">hr</span> <span class="p">/></span>
{% for rl in self.related_links.all %}
<span class="p"><</span><span class="nt">p</span><span class="p">></span>{{ rl.title }}: <span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">'{{ rl.link_page.url }}'</span><span class="p">></span>{{ rl.link_page }}<span class="p"></</span><span class="nt">a</span><span class="p">></</span><span class="nt">p</span><span class="p">></span>
{% endfor %}
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</code></pre></div>
<p>So self is the context name of the BlogPageIndex instance that is used to render this page. Beyond that, it’s normal django.</p>
<p><img alt="Rendering the Blog Index" src="https://spapas.github.io/images/wagtail-index-template.png"></p>
<p>Now you can view your Blog Index — however before clicking on a link to also view your posts add the template for your <code>BlogPost</code> (<code>tutorial/blog_page.html</code>):</p>
<div class="highlight"><pre><span></span><code>{% load rich_text %}
<span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>{{ self.title }}<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
Date: {{ self.date }}
{{ self.body | richtext }}
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</code></pre></div>
<p>The extra thing here is the <code>richtext</code> filter which renders a <code>RichTextField</code> correctly. Of course the above templates are just examples - check <code>wagtaildemo</code> for a much better template design.</p>
<p>Woo-hoo — now I can totally start blogging !!! \o/</p>
<p>…</p>
<p>…</p>
<p>But … when I go to 127.0.0.1:8000 it displays the ugly “Welcome to your new Wagtail site!”. I don’t want to see that any more !!!</p>
<p>No problemo - check the next section of the tutorial :)</p>
<h2 id="changing-your-home-page">Changing your home page</h2>
<p>Wagtail uses the concept of <em>sites</em> to define groups of pages hosted in the <em>same</em> server. To make more clear what site is, you have to use the good old django admin which can be found at <code>http://127.0.0.1:8000/django-admin/</code>. Check the localhost[default] site entry: You will see that each site has a name, a root page (currently the “Welcome to your new Wagtail site!”) and an is_default check. So, change the root page of the localhost site to your BlogIndexPage and go to <code>http://127.0.0.1:8000/</code> … Yes ! The blog is alive :)</p>
<h2 id="where-to-go-from-here">Where to go from here</h2>
<p>You are now ready to start adding more pages to your blog, adding functionality to it (don’t forget that everything is just Django models and templates so you can
change it at will), or even creating a completely different kind of site by adding other Page types. For a more complete site with lots of examples please check
<a href="https://github.com/torchbox/wagtaildemo">wagtaildemo</a>. There is
no complete documentation yet however you can </p>
<blockquote>
<p>Use the source Luke!</p>
</blockquote>Django dynamic forms2013-12-24T14:20:00+02:002013-12-24T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2013-12-24:/2013/12/24/django-dynamic-forms/<p class="first last">Creating dynamic (user generated) forms in django</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#describing-a-django-form-in-json" id="toc-entry-2">Describing a django form in <span class="caps">JSON</span></a></li>
<li><a class="reference internal" href="#creating-the-form-fields" id="toc-entry-3">Creating the form fields</a></li>
<li><a class="reference internal" href="#creating-the-actual-form" id="toc-entry-4">Creating the actual form</a></li>
<li><a class="reference internal" href="#using-the-dynamic-form" id="toc-entry-5">Using the dynamic form</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>To define a form in django, a developer has to create a class which extends
<tt class="docutils literal">django.forms.Form</tt>
and has a number of attributes extending from <tt class="docutils literal">django.forms.Field</tt>. This makes
it very easy for the developer to create static forms, but creating
dynamic forms whose fields can be changed depending on the data contributed by the
users of the application is not so obvious.</p>
<p>Of course, some people may argue that they can do whatever
they want just by spitting html input tags to their templates, however this totally violates
<span class="caps">DRY</span> and any serious django developer would prefer to write <a class="reference external" href="http://thedailywtf.com/Articles/A_Case_of_the_MUMPS.aspx"><span class="caps">MUMPS</span></a> than creating
html dynamically.</p>
<p>The implementation I will present here had been developed for an old project: In that
project there was a number of services which could be edited dynamically by the
moderators. For each service, the moderators would generate a questionnaire to
get input from the users which would be defined using <span class="caps">JSON</span>. When the users needed
to submit information for each service, a dynamic django form would be generated
from this <span class="caps">JSON</span> and the answers would be saved to no-<span class="caps">SQL</span> database like MongoDB.</p>
</div>
<div class="section" id="describing-a-django-form-in-json">
<h2><a class="toc-backref" href="#toc-entry-2">Describing a django form in <span class="caps">JSON</span></a></h2>
<p>A <span class="caps">JSON</span> described django form is just an array of field <span class="caps">JSON</span> objects. Each field
object has three required attributes: name which is the keyword of the field, label which is
how the label of the field and type which is the type of the input of that field. The
supported types are text, textarea, integer, radio, select, checkbox. Now, depending
on the type of the field, there could be also some more required attributes, for instace
text has a max_length attribute and select has a choices attribute (which is an array
of name/value objects). Also there are two optional attributes,
required with a default value of False and help_text with a default value of ”.</p>
<p>As you can understand these map one by one to the corresponding attributes of the
actual django form fields. An example containing a complete <span class="caps">JSON</span> described form
is the following:</p>
<pre class="code literal-block">
[
{
"name": "firstname",
"label": "First Name",
"type": "text",
"max_length": 25,
"required": 1
},
{
"name": "lastname",
"label": "Last Name",
"type": "text",
"max_length": 25,
"required": 1
},
{
"name": "smallcv",
"label": "Small CV",
"type": "textarea",
"help_text": "Please insert a small CV"
},
{
"name": "age",
"label": "Age",
"type": "integer",
"max_value": 200,
"min_value": 0
},
{
"name": "marital_status",
"label": "Marital Status",
"type": "radio",
"choices": [
{"name": "Single", "value":"single"},
{"name": "Married", "value":"married"},
{"name": "Divorced", "value":"divorced"},
{"name": "Widower", "value":"widower"}
]
},
{
"name": "occupation",
"label": "Occupation",
"type": "select",
"choices": [
{"name": "Farmer", "value":"farmer"},
{"name": "Engineer", "value":"engineer"},
{"name": "Teacher", "value":"teacher"},
{"name": "Office Clerk", "value":"office_clerk"},
{"name": "Merchant", "value":"merchant"},
{"name": "Unemployed", "value":"unemployed"},
{"name": "Retired", "value":"retired"},
{"name": "Other", "value":"other"}
]
},
{
"name": "internet",
"label": "Internet Access",
"type": "checkbox"
}
]
</pre>
<p>The above <span class="caps">JSON</span> string can be easily converted to an array of dictionaries with the following code:</p>
<pre class="code literal-block">
import json
fields=json.loads(json_fields)
</pre>
</div>
<div class="section" id="creating-the-form-fields">
<h2><a class="toc-backref" href="#toc-entry-3">Creating the form fields</a></h2>
<p>The most import part in the django dynamic form creation is to convert the above array
of field-describing dictionaries to actual objects of type <tt class="docutils literal">django.forms.Field</tt>.</p>
<p>To help with that I implemented a class named <tt class="docutils literal">FieldHandler</tt> which gets an
array of field dictionaries and after initialization will have an attribute named <tt class="docutils literal">formfields</tt> which
will be a dictionary with keys the names of each field an values the corresponding <tt class="docutils literal">django.forms.Field</tt> objects. The implementation is as follows:</p>
<pre class="code literal-block">
import django.forms
class FieldHandler():
formfields = {}
def __init__(self, fields):
for field in fields:
options = self.get_options(field)
f = getattr(self, "create_field_for_"+field['type'] )(field, options)
self.formfields[field['name']] = f
def get_options(self, field):
options = {}
options['label'] = field['label']
options['help_text'] = field.get("help_text", None)
options['required'] = bool(field.get("required", 0) )
return options
def create_field_for_text(self, field, options):
options['max_length'] = int(field.get("max_length", "20") )
return django.forms.CharField(**options)
def create_field_for_textarea(self, field, options):
options['max_length'] = int(field.get("max_value", "9999") )
return django.forms.CharField(widget=django.forms.Textarea, **options)
def create_field_for_integer(self, field, options):
options['max_value'] = int(field.get("max_value", "999999999") )
options['min_value'] = int(field.get("min_value", "-999999999") )
return django.forms.IntegerField(**options)
def create_field_for_radio(self, field, options):
options['choices'] = [ (c['value'], c['name'] ) for c in field['choices'] ]
return django.forms.ChoiceField(widget=django.forms.RadioSelect, **options)
def create_field_for_select(self, field, options):
options['choices'] = [ (c['value'], c['name'] ) for c in field['choices'] ]
return django.forms.ChoiceField( **options)
def create_field_for_checkbox(self, field, options):
return django.forms.BooleanField(widget=django.forms.CheckboxInput, **options)
</pre>
<p>As can be seen, in the <tt class="docutils literal">__init__</tt> method, the <tt class="docutils literal">get_options</tt> method is called first which
returns a dictionary with the common options (label, help_text, required). After that,
depending on the type of each field the correct method will be generated with
<tt class="docutils literal">getattr(self, <span class="pre">"create_field_for_"+field['type']</span> )</tt> (so if type is text this
will return a reference to the create_field_for_text method) and then called passing
the field dictinary and the options returned from <tt class="docutils literal">get_options</tt>. Each one of
the <tt class="docutils literal">create_field_for_xxx</tt> methods will extract the required (or optional)
attributes for the specific field type, update options and initialize the correct Field passing
the options as kwargs. Finally the formfields attribute will be updated with the name
and Field object.</p>
</div>
<div class="section" id="creating-the-actual-form">
<h2><a class="toc-backref" href="#toc-entry-4">Creating the actual form</a></h2>
<p>To create the actual dynamic <tt class="docutils literal">django.forms.Form</tt> I used the function <tt class="docutils literal">get_form</tt>
which receives a string with the json description, parses it to a python array,
creates the array of fields with the help of <tt class="docutils literal">FieldHandler</tt> and then generates
the <tt class="docutils literal">Form</tt> class with <tt class="docutils literal">type</tt> passing it <tt class="docutils literal">django.forms.Form</tt> as a parent
and the array of <tt class="docutils literal">django.forms.Field</tt> from <tt class="docutils literal">FieldHandler</tt> as attributes:</p>
<pre class="code literal-block">
def get_form(jstr):
fields=json.loads(jstr)
fh = FieldHandler(fields)
return type('DynaForm', (django.forms.Form,), fh.formfields )
</pre>
</div>
<div class="section" id="using-the-dynamic-form">
<h2><a class="toc-backref" href="#toc-entry-5">Using the dynamic form</a></h2>
<p>The result of <tt class="docutils literal">get_form</tt> can be used as a normal form class. As an example:</p>
<pre class="code literal-block">
import dynaform
def dform(request):
json_form = get_json_form_from_somewhere()
form_class = dynaform.get_form(json_form)
data = {}
if request.method == 'POST':
form = form_class(request.POST)
if form.is_valid():
data = form.cleaned_data
else:
form = form_class()
return render_to_response( "dform.html", {
'form': form, 'data': data,
}, RequestContext(request) )
</pre>
<p>So, we have to get our <span class="caps">JSON</span> form description from somewhere (for instance
a field in a model) and then generate the form class with <tt class="docutils literal">get_form</tt>.
After that we follow the normal procedure of checking if the <tt class="docutils literal">request.method</tt>
is <span class="caps">POST</span> so we pass the <span class="caps">POST</span> data to the form and check if it is value or
we just create an empty form. As a result we just pass the data that was
read from the form to the view for presentation.</p>
</div>
Django authority data2013-11-05T14:20:00+02:002013-11-05T14:20:00+02:00Serafeim Papastefanostag:spapas.github.io,2013-11-05:/2013/11/05/django-authoritiy-data/<p class="first last">An implementation <span class="amp">&</span> discussion of authority data for Django</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#defining-authorities" id="toc-entry-2">Defining authorities</a><ul>
<li><a class="reference internal" href="#using-groups" id="toc-entry-3">Using groups</a></li>
<li><a class="reference internal" href="#by-storing-the-authority-to-the-session" id="toc-entry-4">By storing the authority to the session</a></li>
<li><a class="reference internal" href="#by-using-a-custom-user-profile" id="toc-entry-5">By using a Custom User Profile</a></li>
<li><a class="reference internal" href="#getting-the-authority-of-the-user-has-to-be-dry" id="toc-entry-6">Getting the authority of the user has to be <span class="caps">DRY</span></a></li>
</ul>
</li>
<li><a class="reference internal" href="#adding-authority-data" id="toc-entry-7">Adding authority data</a></li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-8">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>One common requirement in an organization is to separate users in authorities (meaning departments / units / branches etc)
and each authority have its own data. So users belonging to the “Athens Branch” won’t be able to
edit data submitted from users of the “Thessaloniki Branch”.</p>
<p>This is a special case of the more general row-level-security in which each instance of a domain object will
have an <span class="caps">ACL</span>. Row-level-security would need a many-to-many relation between object instances and authorities, something
that would be overkill in our case.</p>
<p>Authority data is also a more general case of the user-data meaning that each user can have access
to data that he inserts in the system. Implementing user-data is easy using the techniques we will present below.</p>
<p>We have to notice that the django permissions do not support our requirements since they define security for all instances of a model.</p>
</div>
<div class="section" id="defining-authorities">
<h2><a class="toc-backref" href="#toc-entry-2">Defining authorities</a></h2>
<p>In order to have custom authorities I propose first of all to add an Authority model that would define the authority. Even if
your authorities only have a name I believe that adding the Authority model would be beneficial.
Now, there are many ways to separate normal django users (<tt class="docutils literal">django.contrib.auth.models.User</tt>) to authorities:</p>
<div class="section" id="using-groups">
<h3><a class="toc-backref" href="#toc-entry-3">Using groups</a></h3>
<p>Just define a <tt class="docutils literal">django.contrib.auth.models.Group</tt> for each authority and add the users to the groups you want using the django-admin.
Your Authority model would have an one-to-one relation with the <tt class="docutils literal">django.contrib.auth.models.Group</tt> so you will be able to find out the other
information of the authority (since django groups only have names).</p>
<p>Now you can just get the groups for the user and find out his authorities. This could lead to problems when users belong to django groups
that are not related to authorities so you must filter these out (for instance by checking which groups actually have a corresponding Authority).</p>
</div>
<div class="section" id="by-storing-the-authority-to-the-session">
<h3><a class="toc-backref" href="#toc-entry-4">By storing the authority to the session</a></h3>
<p>When the user logs in you can add an attribute to the session that would save the authority of the user. To do that, you should define
a custom middleware that checks to see if there is an authority attribute to the session and if not it will do whatever it needs to find it and set it.
An example is this:</p>
<pre class="code literal-block">
class CustomAuthorityMiddleware:
def process_request(self, request):
if not request.session.get('authority'):
authority = get_the_authority(request.user)
request.session['authority']=authority
</pre>
<p>This way, whenever you want to find out the authority of the user you just check the session.</p>
</div>
<div class="section" id="by-using-a-custom-user-profile">
<h3><a class="toc-backref" href="#toc-entry-5">By using a Custom User Profile</a></h3>
<p>Just create a <a class="reference external" href="https://docs.djangoproject.com/en/dev/topics/auth/customizing/#extending-the-existing-user-model">django user profile</a> and add to it a <tt class="docutils literal">ForeignKey</tt> to your Authority model:</p>
<pre class="code literal-block">
class Profile(models.Model):
user = models.OneToOneField('django.auth.User')
authority = models.ForeignKey('authorities.Authority', blank=True, null=True )
class Authority(models.Model):
id = models.IntegerField(primary_key = True)
name = models.CharField(max_length=64, )
auth_type = models.CharField(max_length=16, )
</pre>
<p>You can get the authority of the user through <tt class="docutils literal">request.user.profile.authority</tt>.</p>
</div>
<div class="section" id="getting-the-authority-of-the-user-has-to-be-dry">
<h3><a class="toc-backref" href="#toc-entry-6">Getting the authority of the user has to be <span class="caps">DRY</span></a></h3>
<p>Whatever method you use to define the authorities of your users you have to remember that it is very
important to define somewhere a function that will return the authority (or authorities) of a
user. You need to define a function even in the simple case in which your function would just return <tt class="docutils literal">request.user.profile.authority</tt>.
This will greatly help you when you wish to add some logic to this, for instance “quickly disable users belonging to Authority X
or temporary move users from Authority Y to authority Z”.</p>
<p>Let us suppose that you have defined a <tt class="docutils literal">get_user_authority</tt> function. Also, you need to define a <tt class="docutils literal">has_access</tt> function
that would decide if a users/request has access to a particular object. This also needs to be <span class="caps">DRY</span>.</p>
</div>
</div>
<div class="section" id="adding-authority-data">
<h2><a class="toc-backref" href="#toc-entry-7">Adding authority data</a></h2>
<p>To define authority data you have to add a field to your model that would define its authority, for instance like this:</p>
<pre class="code literal-block">
class AuthorityData(models.Model):
authority = models.ForeignKey('authorities.Authority', editable=False,)
</pre>
<p>This field should not be editable (at least by your end users) because they shouldn’t be able to change the authority of the data they insert.</p>
<p>If you want to have user-data then just add a <tt class="docutils literal"><span class="pre">models.ForeignKey('django.auth.User',</span> editable=False)</tt></p>
<p>Now, your Create and Update Class Based Views have to pass the request to your forms and also your Detail and Update <span class="caps">CBV</span> should allow only getting
objects that belong to the authority of the user:</p>
<pre class="code literal-block">
class AuthorityDataCreateView(CreateView):
model=models.AuthorityData
def get_form_kwargs(self):
kwargs = super(AuthorityDataCreateView, self).get_form_kwargs()
kwargs.update({'request': self.request})
return kwargs
class AuthorityDataDetailView(DetailView):
def get_object(self, queryset=None):
obj = super(AuthorityDataDetailView, self).get_object(queryset)
if if not user_has_access(obj, self.request):
raise Http404(u"Access Denied")
return obj
class AuthorityDataUpdateView(UpdateView):
model=models.AuthorityData
def get_form_kwargs(self):
kwargs = super(AuthorityDataUpdateView, self).get_form_kwargs()
kwargs.update({'request': self.request})
return kwargs
def get_object(self, queryset=None):
obj = super(AuthorityDataUpdateView, self).get_object(queryset)
if if not user_has_access(obj, self.request):
raise Http404(u"Access Denied")
return obj
</pre>
<p>Your ModelForm can now use the request to get the Authority and set it (don’t forget
that you should not use <tt class="docutils literal">Meta.exclude</tt> but instead use <tt class="docutils literal">Meta.include</tt>!):</p>
<pre class="code literal-block">
class AuthorityDataModelForm(forms.ModelForm):
class Meta:
model = models.AuthorityData
exclude = ('authority',)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(ActionModelForm, self).__init__(*args, **kwargs)
def save(self, force_insert=False, force_update=False, commit=True):
obj = super(AuthorityDataModelForm, self).save(commit=False)
if obj:
obj.authority = get_user_authority(self.request)
obj.save()
return obj
</pre>
<p>The previous work fine for Create/Detail/Update CBVs but not for ListsViews. List views querysets
and in general all queries to the object have to be filtered through authority.</p>
<pre class="code literal-block">
class AuthorityDataListView(ListView):
def get_queryset(self):
queryset = super(AuthorityDataModelForm, self).get_queryset()
return queryset.filter(authority = get_user_authority(request))
</pre>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-8">Conclusion</a></h2>
<p>Using the above techniques we can define authority (or just user) data. Your AuthorityData should
have a <tt class="docutils literal">ForeignKey</tt> to your Authority and you have configure your queries, ModelForms and CBVs
to use that. If you have more than one models that belong to an authority and want to stay <span class="caps">DRY</span> then you’d need
to define all the above as <a class="reference external" href="https://docs.djangoproject.com/en/dev/topics/class-based-views/mixins/">mixins</a>.</p>
</div>
Using custom authorities with spring-security LDAP authentication2013-10-14T08:55:00+03:002013-10-14T08:55:00+03:00Serafeim Papastefanostag:spapas.github.io,2013-10-14:/2013/10/14/spring-ldap-custom-authorities/<p class="first last">Configuring spring-security for logging in through <span class="caps">LDAP</span> but retrieving the user’s authorities from a custom (non-ldap) source.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#a-basic-spring-security-setup" id="toc-entry-2">A basic spring security setup</a></li>
<li><a class="reference internal" href="#spring-security-ldap-with-custom-authorities" id="toc-entry-3">Spring security <span class="caps">LDAP</span> with custom authorities</a><ul>
<li><a class="reference internal" href="#contextsource" id="toc-entry-4">contextSource</a></li>
<li><a class="reference internal" href="#usersearch" id="toc-entry-5">userSearch</a></li>
<li><a class="reference internal" href="#ldapauthprovider" id="toc-entry-6">ldapAuthProvider</a></li>
<li><a class="reference internal" href="#customldapauthoritiespopulator" id="toc-entry-7">CustomLdapAuthoritiesPopulator</a></li>
<li><a class="reference internal" href="#example" id="toc-entry-8">Example</a></li>
</ul>
</li>
<li><a class="reference internal" href="#conclusion" id="toc-entry-9">Conclusion</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>One very useful component of the <a class="reference external" href="http://spring.io/">spring</a> java framework is <a class="reference external" href="http://projects.spring.io/spring-security/">spring-security</a> since it allows consistent usage of various security providers for
authentication and authorization. Although I’ve found a great number of basic spring-security tutorials on the internet, I wasn’t able to find a complete solution for my own requirements:</p>
<p>Logging in with <span class="caps">LDAP</span> but configuring the authorities <a class="footnote-reference" href="#footnote-1" id="footnote-reference-1">[*]</a> of the logged in user with the help of a custom method and not through <span class="caps">LDAP</span>.</p>
<p>I think that the above is a common requirement in many organizations: There is a central <span class="caps">LDAP</span> repository in which the usernames and passwords of the users are stored, but the groups of the users are not stored there. Or maybe the groups that are actually stored in the <span class="caps">LDAP</span> cannot be transformed easily to application specific groups for each application.</p>
<p>You may find the working spring project that uses ldap and a custom groups populator here: <a class="reference external" href="https://github.com/spapas/SpringLdapCustomAuthorities/">https://github.com/spapas/SpringLdapCustomAuthorities/</a></p>
</div>
<div class="section" id="a-basic-spring-security-setup">
<h2><a class="toc-backref" href="#toc-entry-2">A basic spring security setup</a></h2>
<p>I’ve created a very basic setup for spring-security for a spring-mvc project. Please take a look here for a more thorough explanation of a simple spring-security project <a class="reference external" href="http://www.mkyong.com/spring-security/spring-security-hello-world-example/">http://www.mkyong.com/spring-security/spring-security-hello-world-example/</a> and here <a class="reference external" href="http://www.codeproject.com/Articles/253901/Getting-Started-Spring-Security">http://www.codeproject.com/Articles/253901/Getting-Started-Spring-Security</a> for a great explanation of the various spring-security classes.</p>
<p>In my setup there is a controller that defines two mappings, the “/” which is the homepage that has a link to the “/enter” and the “/enter” which is an internal page in which only authorized users have access. When the user clicks on “enter” he will be represented with a login form first. If the use logs in successfully, the enter.jsp will list the username and the authorities of the logged in user through the following spring-security tags:</p>
<div class="highlight"><pre><span></span><span class="k"><%@</span><span class="w"> </span><span class="n">taglib</span><span class="w"> </span><span class="n">prefix</span><span class="o">=</span><span class="s">"sec"</span><span class="w"> </span><span class="n">uri</span><span class="o">=</span><span class="s">"http://www.springframework.org/security/tags"</span><span class="w"> </span><span class="k">%></span>
[...]
Username:<span class="w"> </span><span class="nt"><sec:authentication</span><span class="w"> </span><span class="na">property=</span><span class="s">"principal.username"</span><span class="w"> </span><span class="nt">/><br</span><span class="w"> </span><span class="nt">/></span>
Authorities:<span class="w"> </span><span class="nt"><sec:authentication</span><span class="w"> </span><span class="na">property=</span><span class="s">"principal.authorities"</span><span class="nt">/><br</span><span class="w"> </span><span class="nt">/></span>
</pre></div>
<p>The authentication provider is an in memory service in which the username, password and authorities of each user are defined in the <span class="caps">XML</span>. So this is a simple spring-security example that can be found in a number of places on the internet. The security rules, login form and the authentication provider are configured with the following <tt class="docutils literal"><span class="pre">security-config.xml</span></tt>:</p>
<div class="highlight"><pre><span></span><span class="nt"><beans:beans</span><span class="w"> </span><span class="na">xmlns=</span><span class="s">"http://www.springframework.org/schema/security"</span>
<span class="w"> </span><span class="na">xmlns:beans=</span><span class="s">"http://www.springframework.org/schema/beans"</span>
<span class="w"> </span><span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
<span class="w"> </span><span class="na">xsi:schemaLocation=</span><span class="s">"http://www.springframework.org/schema/beans</span>
<span class="s"> http://www.springframework.org/schema/beans/spring-beans-3.1.xsd</span>
<span class="s"> http://www.springframework.org/schema/security</span>
<span class="s"> http://www.springframework.org/schema/security/spring-security-3.1.xsd"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><http</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/static/**"</span><span class="w"> </span><span class="na">security=</span><span class="s">"none"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><http</span><span class="w"> </span><span class="na">use-expressions=</span><span class="s">"true"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><intercept-url</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/"</span><span class="w"> </span><span class="na">access=</span><span class="s">"permitAll"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><intercept-url</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/enter"</span><span class="w"> </span><span class="na">access=</span><span class="s">"hasRole('user')"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><intercept-url</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/**"</span><span class="w"> </span><span class="na">access=</span><span class="s">"denyAll"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><form-login</span><span class="w"> </span><span class="na">default-target-url=</span><span class="s">"/"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><logout</span><span class="w"> </span><span class="na">logout-success-url=</span><span class="s">"/"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></http></span>
<span class="w"> </span><span class="nt"><authentication-manager></span>
<span class="w"> </span><span class="nt"><authentication-provider></span>
<span class="w"> </span><span class="nt"><user-service></span>
<span class="w"> </span><span class="nt"><user</span><span class="w"> </span><span class="na">name=</span><span class="s">"spapas"</span><span class="w"> </span><span class="na">password=</span><span class="s">"123"</span><span class="w"> </span><span class="na">authorities=</span><span class="s">"admin, user, nonldap"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><user</span><span class="w"> </span><span class="na">name=</span><span class="s">"serafeim"</span><span class="w"> </span><span class="na">password=</span><span class="s">"123"</span><span class="w"> </span><span class="na">authorities=</span><span class="s">"user"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></user-service></span>
<span class="w"> </span><span class="nt"></authentication-provider></span>
<span class="w"> </span><span class="nt"></authentication-manager></span>
<span class="nt"></beans:beans></span>
</pre></div>
<p>When we run this application and go to the /enter, we will get the following output:</p>
<blockquote>
<p>Username: spapas</p>
<p>Authorities: [admin, nonldap, user]</p>
</blockquote>
</div>
<div class="section" id="spring-security-ldap-with-custom-authorities">
<h2><a class="toc-backref" href="#toc-entry-3">Spring security <span class="caps">LDAP</span> with custom authorities</a></h2>
<p>The previous application can be modified to login through <span class="caps">LDAP</span> and get the authorities from a custom class. The main differences are in the <tt class="docutils literal">pom.xml</tt> which adsd the <tt class="docutils literal"><span class="pre">spring-security-ldap</span></tt> dependency, the addition of a <tt class="docutils literal">CustomLdapAuthoritiesPopulator.java</tt> which does the actual mapping of username to authority and various changes to the <tt class="docutils literal"><span class="pre">security-config.xml</span></tt>.</p>
<p>As you will see we had to define our security beans mainly using spring beans and not using the various elements from security namespace like <tt class="docutils literal"><span class="pre"><ldap-server></span></tt> and <tt class="docutils literal"><span class="pre"><ldap-authentication-provder></span></tt>. For a good tutorial on using these elements and ldap in spring security in general check these out: <a class="reference external" href="http://docs.spring.io/spring-security/site/docs/3.1.x/reference/ldap.html">http://docs.spring.io/spring-security/site/docs/3.1.x/reference/ldap.html</a> and <a class="reference external" href="http://krams915.blogspot.gr/2011/01/spring-security-mvc-using-ldap.html">http://krams915.blogspot.gr/2011/01/spring-security-mvc-using-ldap.html</a>.</p>
<div class="highlight"><pre><span></span><span class="nt"><beans:beans</span><span class="w"> </span><span class="na">xmlns=</span><span class="s">"http://www.springframework.org/schema/security"</span>
<span class="w"> </span><span class="na">xmlns:beans=</span><span class="s">"http://www.springframework.org/schema/beans"</span>
<span class="w"> </span><span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span>
<span class="w"> </span><span class="na">xsi:schemaLocation=</span><span class="s">"http://www.springframework.org/schema/beans</span>
<span class="s"> http://www.springframework.org/schema/beans/spring-beans-3.1.xsd</span>
<span class="s"> http://www.springframework.org/schema/security</span>
<span class="s"> http://www.springframework.org/schema/security/spring-security-3.1.xsd"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><http</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/static/**"</span><span class="w"> </span><span class="na">security=</span><span class="s">"none"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><http</span><span class="w"> </span><span class="na">use-expressions=</span><span class="s">"true"</span><span class="w"> </span><span class="nt">></span>
<span class="w"> </span><span class="nt"><intercept-url</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/"</span><span class="w"> </span><span class="na">access=</span><span class="s">"permitAll"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><intercept-url</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/enter"</span><span class="w"> </span><span class="na">access=</span><span class="s">"hasRole('user')"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><intercept-url</span><span class="w"> </span><span class="na">pattern=</span><span class="s">"/**"</span><span class="w"> </span><span class="na">access=</span><span class="s">"denyAll"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><form-login</span><span class="w"> </span><span class="na">default-target-url=</span><span class="s">"/"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><logout</span><span class="w"> </span><span class="na">logout-success-url=</span><span class="s">"/"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></http></span>
<span class="w"> </span><span class="nt"><beans:bean</span><span class="w"> </span><span class="na">id=</span><span class="s">"contextSource"</span>
<span class="w"> </span><span class="na">class=</span><span class="s">"org.springframework.security.ldap.DefaultSpringSecurityContextSource"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg</span><span class="w"> </span><span class="na">value=</span><span class="s">"ldap://login.serafeim.gr:389/dc=serafeim,dc=gr"</span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><beans:property</span><span class="w"> </span><span class="na">name=</span><span class="s">"anonymousReadOnly"</span><span class="w"> </span><span class="na">value=</span><span class="s">"true"</span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></beans:bean></span>
<span class="w"> </span><span class="nt"><beans:bean</span>
<span class="w"> </span><span class="na">id=</span><span class="s">"userSearch"</span>
<span class="w"> </span><span class="na">class=</span><span class="s">"org.springframework.security.ldap.search.FilterBasedLdapUserSearch"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg</span><span class="w"> </span><span class="na">index=</span><span class="s">"0"</span><span class="w"> </span><span class="na">value=</span><span class="s">""</span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg</span><span class="w"> </span><span class="na">index=</span><span class="s">"1"</span><span class="w"> </span><span class="na">value=</span><span class="s">"(uid={0})"</span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg</span><span class="w"> </span><span class="na">index=</span><span class="s">"2"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"contextSource"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></beans:bean></span>
<span class="w"> </span><span class="nt"><beans:bean</span>
<span class="w"> </span><span class="na">id=</span><span class="s">"ldapAuthProvider"</span>
<span class="w"> </span><span class="na">class=</span><span class="s">"org.springframework.security.ldap.authentication.LdapAuthenticationProvider"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg></span>
<span class="w"> </span><span class="nt"><beans:bean</span><span class="w"> </span><span class="na">class=</span><span class="s">"org.springframework.security.ldap.authentication.BindAuthenticator"</span><span class="nt">></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg</span><span class="w"> </span><span class="na">ref=</span><span class="s">"contextSource"</span><span class="nt">/></span>
<span class="w"> </span><span class="nt"><beans:property</span><span class="w"> </span><span class="na">name=</span><span class="s">"userSearch"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"userSearch"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="cm"><!--</span>
<span class="cm"> <beans:property name="userDnPatterns"></span>
<span class="cm"> <beans:list><beans:value>uid={0},ou=People</beans:value></beans:list></span>
<span class="cm"> </beans:property></span>
<span class="cm"> --></span>
<span class="w"> </span><span class="nt"></beans:bean></span>
<span class="w"> </span><span class="nt"></beans:constructor-arg></span>
<span class="w"> </span><span class="nt"><beans:constructor-arg></span>
<span class="w"> </span><span class="nt"><beans:bean</span><span class="w"> </span><span class="na">class=</span><span class="s">"gr.serafeim.springldapcustom.CustomLdapAuthoritiesPopulator"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></beans:constructor-arg></span>
<span class="w"> </span><span class="nt"></beans:bean></span>
<span class="w"> </span><span class="nt"><authentication-manager></span>
<span class="w"> </span><span class="nt"><authentication-provider</span><span class="w"> </span><span class="na">ref=</span><span class="s">"ldapAuthProvider"</span><span class="w"> </span><span class="nt">/></span>
<span class="w"> </span><span class="nt"></authentication-manager></span>
<span class="nt"></beans:beans></span>
</pre></div>
<p>So, in the above configuration we’ve defined three spring beans: <tt class="docutils literal">contextSource</tt>, <tt class="docutils literal">userSearch</tt> and <tt class="docutils literal">ldapAuthProvider</tt>. The <tt class="docutils literal"><span class="pre"><authentication-manager></span></tt> element uses the <tt class="docutils literal">ldapAuthProvider</tt> as an authentication provider. Below we will explain these beans:</p>
<div class="section" id="contextsource">
<h3><a class="toc-backref" href="#toc-entry-4">contextSource</a></h3>
<p>The contextSource bean defines the actual <span class="caps">LDAP</span> server that we are going to connect to. It has the class <tt class="docutils literal">o.s.s.ldap.DefaultSpringSecurityContextSource</tt>. This will need to be passed to other beans that would need to connect to the server for a number of operations. We pass to it the url of our <span class="caps">LDAP</span> server and set its <tt class="docutils literal">anonymousReadOnly</tt> property to true. The <tt class="docutils literal">anonymousReadOnly</tt> defines if we can anonymously connect to our <span class="caps">LDAP</span> server in order to perform the search operation below. If we cannot connect anonymously then we have to set its <tt class="docutils literal">userDn</tt> and <tt class="docutils literal">password</tt> properties.</p>
<p>A very interesting question is if the <tt class="docutils literal"><span class="pre"><ldap-server></span></tt> element of the spring security namespace is related to <tt class="docutils literal">the o.s.s.ldap.DefaultSpringSecurityContextSource</tt> like our <tt class="docutils literal">contextSource</tt>. To find out, we need to check the <tt class="docutils literal">o.s.s.config.SecurityNamespaceHandler</tt> class of the <tt class="docutils literal"><span class="pre">spring-security-config.jar</span></tt>. In there we see the <tt class="docutils literal">loadParsers</tt> method which has the line: <tt class="docutils literal">parsers.put(Elements.LDAP_SERVER, new <span class="pre">LdapServerBeanDefinitionParser());</span></tt>. The constant <tt class="docutils literal">o.s.s.config.Elements.LDAP_SERVER</tt> has the value of <tt class="docutils literal"><span class="pre">"ldap-server"</span></tt> as expected, so we need to see what does the class <tt class="docutils literal">o.s.s.config.ldap.LdapServerBeanDefinitionParser</tt> do. This class has a parse() method that receives the xml that was used to instantiate the <tt class="docutils literal"><span class="pre"><ldap-server></span></tt> element and, depending an on the actualy configuration, instantiates a bean of the class <tt class="docutils literal">o.s.s.ldap.DefaultSpringSecurityContextSource</tt> with an id of <tt class="docutils literal">o.s.s.securityContextSource</tt> that will be used by the other elements in the security namespace !</p>
<p>This actually solves another question I had concerning the following error:</p>
<blockquote>
No bean named ‘org.springframework.security.authenticationManager’ is defined: Did you forget to add a gobal <authentication-manager> element to your configuration (with child <authentication-provider> elements)? Alternatively you can use the authentication-manager-ref attribute on your <http> and <global-method-security> elements.</blockquote>
<p>What happens is that when spring-security-configuration encounters an <tt class="docutils literal"><span class="pre"><authentication-manager></span></tt> it will instantiate a bean named <tt class="docutils literal">o.s.s.authenticationManager</tt> having the class
<tt class="docutils literal">o.s.s.authentication.ProviderManager</tt> and will create and pass to it a <tt class="docutils literal">providers</tt> list with all the authentication providers that are defined inside the <tt class="docutils literal"><span class="pre"><authentication-manager></span></tt> element with <tt class="docutils literal"><span class="pre"><authentication-provider></span></tt> nodes. So, if you encounter the above error, the problem is that for some reason your <tt class="docutils literal"><span class="pre"><authentication-manager></span></tt> is not configured correctly, so no <tt class="docutils literal">o.s.s.authenticatioManager</tt> bean is created!</p>
</div>
<div class="section" id="usersearch">
<h3><a class="toc-backref" href="#toc-entry-5">userSearch</a></h3>
<p>The <tt class="docutils literal">userSearch</tt> bean is needed if we don’t know exactly where our users are stored in the <span class="caps">LDAP</span> directory so we will use this bean as a search filter. If we do know our user tree then we won’t need this bean at all as will be explained later. It has the class <tt class="docutils literal">o.s.s.ldap.search.FilterBasedLdapUserSearch</tt> and gets three constructor parameters: <tt class="docutils literal">searchBase</tt>, <tt class="docutils literal">searchFilter</tt> and <tt class="docutils literal">contextSource</tt>. The <tt class="docutils literal">searchBase</tt> is from where in the <span class="caps">LDAP</span> tree to start searching (empty in our case), the <tt class="docutils literal">searchFilter</tt> defines where is the username (uid in our case) and the <tt class="docutils literal">contextSource</tt> has been defined before.</p>
</div>
<div class="section" id="ldapauthprovider">
<h3><a class="toc-backref" href="#toc-entry-6">ldapAuthProvider</a></h3>
<p>This is the actual <tt class="docutils literal"><span class="pre">authentication-provider</span></tt> that the spring-security <tt class="docutils literal"><span class="pre">authentication-manager</span></tt> is going to use. It is an instance of class <tt class="docutils literal">o.s.s.ldap.authentication.LdapAuthenticationProvider</tt> which has two main properties: An <tt class="docutils literal">o.s.s.ldap.authentication.LdapAuthenticator</tt> implementation and an <tt class="docutils literal">o.s.s.ldap.userdetails.LdapAuthoritiesPopulator</tt> implementation. The first interface defines an <tt class="docutils literal">authenticate</tt> method and is used to actually authenticate the user with the <span class="caps">LDAP</span> server. The second interface defines a <tt class="docutils literal">getGrantedAuthorities</tt> which returns the roles for the authenticated user. The LdapAuthoritiesPopulator parameter is actually optional (so we can use <span class="caps">LDAP</span> to authenticate only the users) and we can provide our own implementation to have custom authorities for our application. That is exactly what we’ve done here.</p>
<p>The two arguments used to initialize the ldapAuthProvoder are one instance of <tt class="docutils literal">o.s.s.ldap.authentication.BindAuthenticator</tt> which is a simple authenticator that tries to bind with the given credentials to the <span class="caps">LDAP</span> server to check the credentials and one instance of a custom class named <tt class="docutils literal">g.s.s.CustomLdapAuthoritiesPopulator</tt> which is the actual implementation of the custom roles provider. The <tt class="docutils literal">BindAuthenticator</tt> gets the <tt class="docutils literal">contextSource</tt> as a constructor parameter and its <tt class="docutils literal">userSearch</tt> property is set with the <tt class="docutils literal">userSearch</tt> bean defined previously. If we instead knew the actual place of the users, we could use the commented out <tt class="docutils literal">userDnPatterns</tt> property which takes a list of possible places in the <span class="caps">LDAP</span> catalog which will be checked for the username.</p>
</div>
<div class="section" id="customldapauthoritiespopulator">
<h3><a class="toc-backref" href="#toc-entry-7">CustomLdapAuthoritiesPopulator</a></h3>
<p>The <tt class="docutils literal">CustomLdapAuthoritiesPopulator</tt> just needs to implement the <tt class="docutils literal">LdapAuthoritiesPopulator</tt> interface. Here’s our implmentation:</p>
<div class="highlight"><pre><span></span><span class="kn">package</span><span class="w"> </span><span class="nn">gr.serafeim.springldapcustom</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">java.util.Collection</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">java.util.HashSet</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.ldap.core.DirContextOperations</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.security.core.GrantedAuthority</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.security.core.authority.SimpleGrantedAuthority</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator</span><span class="p">;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.stereotype.Component</span><span class="p">;</span>
<span class="nd">@Component</span>
<span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">CustomLdapAuthoritiesPopulator</span><span class="w"> </span><span class="kd">implements</span><span class="w"> </span><span class="n">LdapAuthoritiesPopulator</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nd">@Override</span>
<span class="w"> </span><span class="kd">public</span><span class="w"> </span><span class="n">Collection</span><span class="o"><?</span><span class="w"> </span><span class="kd">extends</span><span class="w"> </span><span class="n">GrantedAuthority</span><span class="o">></span><span class="w"> </span><span class="nf">getGrantedAuthorities</span><span class="p">(</span>
<span class="w"> </span><span class="n">DirContextOperations</span><span class="w"> </span><span class="n">userData</span><span class="p">,</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">username</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">Collection</span><span class="o"><</span><span class="n">GrantedAuthority</span><span class="o">></span><span class="w"> </span><span class="n">gas</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">HashSet</span><span class="o"><</span><span class="n">GrantedAuthority</span><span class="o">></span><span class="p">();</span>
<span class="w"> </span><span class="k">if</span><span class="p">(</span><span class="n">username</span><span class="p">.</span><span class="na">equals</span><span class="p">(</span><span class="s">"spapas"</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="n">gas</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">SimpleGrantedAuthority</span><span class="p">(</span><span class="s">"admin"</span><span class="p">));</span>
<span class="w"> </span><span class="p">}</span>
<span class="w"> </span><span class="n">gas</span><span class="p">.</span><span class="na">add</span><span class="p">(</span><span class="k">new</span><span class="w"> </span><span class="n">SimpleGrantedAuthority</span><span class="p">(</span><span class="s">"user"</span><span class="p">));</span>
<span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">gas</span><span class="p">;</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>The <tt class="docutils literal">getGrantedAuthorities</tt> just checks the username and add another role if it is a specific one. Of course here we would autowire our user roles repository and query the database to get the roles of the user, however I’m not going to do that for the case of simplicity.</p>
</div>
<div class="section" id="example">
<h3><a class="toc-backref" href="#toc-entry-8">Example</a></h3>
<p>When we run this application and go to the /enter, after logging in with our <span class="caps">LDAP</span> credentials as spapas, we will get the following output:</p>
<blockquote>
<p>Username: spapas</p>
<p>Authorities: [admin, user]</p>
</blockquote>
</div>
</div>
<div class="section" id="conclusion">
<h2><a class="toc-backref" href="#toc-entry-9">Conclusion</a></h2>
<p>In the previous a complete example of configuring a custom authorities populator was represented. Using this configuration we can login through the <span class="caps">LDAP</span> server of our organization but use application specific roles for our logged-in users.</p>
<!-- font-size: 0.5em;
vertical-align: top; -->
<table class="docutils footnote" frame="void" id="footnote-1" rules="none">
<colgroup><col class="label" /><col /></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#footnote-reference-1">[*]</a></td><td>Which is how spring calls the groups/roles the user belongs to</td></tr>
</tbody>
</table>
</div>
git branches2013-10-08T13:20:00+03:002013-10-08T13:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2013-10-08:/2013/10/08/git-branches/<p class="first last">Experiments and answers for git branching</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#creating-a-new-git-repository" id="toc-entry-2">Creating a new git repository</a></li>
<li><a class="reference internal" href="#branching" id="toc-entry-3">Branching</a></li>
<li><a class="reference internal" href="#remote-branches" id="toc-entry-4">Remote branches</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p>A <a class="reference external" href="http://git-scm.com/book/en/Git-Branching-What-a-Branch-Is">branch</a> is a very interesting git feature. With this you may have more than one <em>branches</em> in the same repository. The main
usage of this feature would be to create different versions of your source code to parallel test development of different features.
When the development of each of these features has been finished then the different versions would need to be combined (or merged) to a
single version. Of course, merging is not always the result of branching - some branches may exist indefinitely or other may just be
deleted without merging.</p>
<p>I will try to experiment with it and comment on the results.</p>
</div>
<div class="section" id="creating-a-new-git-repository">
<h2><a class="toc-backref" href="#toc-entry-2">Creating a new git repository</a></h2>
<p>Let’s start by creating a new git repository:</p>
<pre class="code literal-block">
D:\>mkdir testgit
D:\>cd testgit
D:\testgit>echo contents 11 > file1.txt
D:\testgit>echo contents 222 > file2.txt
D:\testgit>echo 3333 > file2.txt
D:\testgit>copy con file3.txt
line 1 of file 3
line 3 of file 3
test
line 7 of file 3
^Z
1 files copied.
D:\testgit>git init
Initialized empty Git repository in D:/testgit/.git/
D:\testgit>git add .
D:\testgit>git commit -m Initial
[master (root-commit) 96ca9af] Initial
3 files changed, 9 insertions(+)
create mode 100644 file1.txt
create mode 100644 file2.txt
create mode 100644 file3.txt
</pre>
<p>To see the branch we are in we can use the <tt class="docutils literal">git branch</tt> command. Also <tt class="docutils literal">git status</tt> outputs the current branch:</p>
<pre class="code literal-block">
D:\testgit>git status
# On branch master
nothing to commit, working directory clean
D:\testgit>git branch
* master
</pre>
<p>So, it seems that when we create a new repository, a “master” branch is created.</p>
</div>
<div class="section" id="branching">
<h2><a class="toc-backref" href="#toc-entry-3">Branching</a></h2>
<p>Lets create a new branch and change our working branch to it:</p>
<pre class="code literal-block">
D:\testgit>git branch slave
D:\testgit>git branch
* master
slave
D:\testgit>git checkout slave
Switched to branch 'slave'
D:\testgit>git branch
master
* slave
</pre>
<p>We can see that now the slave branch is the current one. Let’s do some changes and add commit them to the slave branch:</p>
<pre class="code literal-block">
D:\testgit>git branch
master
* slave
D:\testgit>echo new file1 contents > file1.txt
D:\testgit>git commit -m "Slave modification"
[slave b6083ad] Slave modification
1 file changed, 1 insertion(+), 1 deletion(-)
D:\testgit>git checkout master
Switched to branch 'master'
D:\testgit>more file1.txt
contents 11
D:\testgit>git checkout slave
Switched to branch 'slave'
D:\testgit>more file1.txt
new file1 contents
</pre>
<p>So the contents of file1.txt in the branch master is <tt class="docutils literal">contents 11</tt> while the contents of the same file
in the branch slave is <tt class="docutils literal">new file1 contents</tt>.</p>
<p>An interested behaviour is what happens with uncommit changes when changing branches. Let’s try deleting a file:</p>
<pre class="code literal-block">
D:\testgit>del file2.txt
D:\testgit>git status
# On branch master
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# deleted: file2.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
D:\testgit>git checkout slave
D file2.txt
Switched to branch 'slave'
D:\testgit>git status
# On branch slave
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# deleted: file2.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
D:\testgit>git add -A
D:\testgit>git status
# On branch slave
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: file2.txt
#
D:\testgit>git checkout master
D file2.txt
Switched to branch 'master'
D:\testgit>git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: file2.txt
#
</pre>
<p>So, our changes are not correlated with a branch until we commit them! Let’s commit them to the master repository and confirm that:</p>
<pre class="code literal-block">
D:\testgit>git commit -m "Deleted file2.txt"
[master 6f8749d] Deleted file2.txt
1 file changed, 1 deletion(-)
delete mode 100644 file2.txt
D:\testgit>git status
# On branch master
nothing to commit, working directory clean
D:\testgit>dir file2.txt
[...]
File not found
D:\testgit>git checkout slave
Switched to branch 'slave'
D:\testgit>git status
# On branch slave
nothing to commit, working directory clean
D:\testgit>dir file2.txt
[...]
08/10/2013 05:59 pm 15 file2.txt
</pre>
<p>This is interesting… Let’s try modifying the file2.txt (which does not exist to the master branch):</p>
<pre class="code literal-block">
D:\testgit>git branch
master
* slave
D:\testgit>echo new file2 contents > file2.txt
D:\testgit>git add .
D:\testgit>git status
# On branch slave
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: file2.txt
#
D:\testgit>git checkout master
error: Your local changes to the following files would be overwritten by checkout:
file2.txt
Please, commit your changes or stash them before you can switch branches.
Aborting
</pre>
<p>We won’t be able to change the current branch until we commit the conflicting change:</p>
<pre class="code literal-block">
D:\testgit>git commit -m "Modified file2"
[slave b5af832] Modified file2
1 file changed, 1 insertion(+), 1 deletion(-)
D:\testgit>git checkout master
Switched to branch 'master'
</pre>
</div>
<div class="section" id="remote-branches">
<h2><a class="toc-backref" href="#toc-entry-4">Remote branches</a></h2>
<p>For each local repository you can define a number of remote repositories, or <a class="reference external" href="http://git-scm.com/book/en/Git-Basics-Working-with-Remotes">remotes</a> as git calls them.
When you clone a repository from github.com, your local repository will have one remote, named origin. We will
try to add the same remote by hand. Let’s suppose that we have created a repository in github.com named
testgit. After that we wil issue:</p>
<pre class="code literal-block">
D:\testgit>git remote
D:\testgit>git remote add origin https://github.com/spapas/testgit.git
D:\testgit>git remote
origin
</pre>
<p>So no we have one remote named origin that is linked with <a class="reference external" href="https://github.com/spapas/testgit.git">https://github.com/spapas/testgit.git</a>. Let’s try to push our master
branch to the origin remote:</p>
<pre class="code literal-block">
D:\testgit>git push origin master
Username for 'https://github.com': spapas
Password for 'https://spapas@github.com':
Counting objects: 7, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 531 bytes, done.
Total 7 (delta 1), reused 0 (delta 0)
To https://github.com/spapas/testgit.git
* [new branch] master -> master
D:\testgit>git branch -r
master
* slave
remote/origin/master
</pre>
<p>We see now that we have <em>three</em> branches. Two local (master slave) and one remote (origin/master). We will also add the slave remote (origin/slave):</p>
<pre class="code literal-block">
D:\testgit>git branch -r
origin/master
origin/slave
</pre>
<p>Let’s do a change to our local repository and then push them to the remote:</p>
<pre class="code literal-block">
D:\testgit>notepad file3.txt
D:\testgit>git add .
D:\testgit>git status
# On branch slave
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: file3.txt
#
D:\testgit>git commit -m "Changed file3.txt"
[slave ce3b7b9] Changed file3.txt
1 file changed, 1 insertion(+), 1 deletion(-)
D:\testgit>git push origin slave
Username for 'https://github.com': spapas
Password for 'https://spapas@github.com':
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 299 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To https://github.com/spapas/testgit.git
b5af832..ce3b7b9 slave -> slave
</pre>
<p>Everything works as expected. The final thing to test is to try checking out a remote branch:</p>
<pre class="code literal-block">
D:\testgit>git checkout master
Switched to branch 'master'
D:\testgit>echo new new file1 > file1.txt
D:\testgit>more file1.txt
new new file1
D:\testgit>git checkout origin/master
M file1.txt
Note: checking out 'origin/master'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at 6f8749d... Deleted file2.txt
D:\testgit>git status
# Not currently on any branch.
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: file1.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
D:\testgit>more file1.txt
new new file1
</pre>
<p>So, it seems that when we check out the remote branch, we won’t have any local branches, however the change we did to the file1.txt
is transfered just like when switching from one local repository to another. We can then add the changes and commit:</p>
<pre class="code literal-block">
D:\testgit>git add .
D:\testgit>git commit
[detached HEAD 506674c] foo
1 file changed, 1 insertion(+), 1 deletion(-)
D:\testgit>git status
# Not currently on any branch.
nothing to commit, working directory clean
D:\testgit>git branch
* (no branch)
master
slave
</pre>
<p>So we are working with an unnamed branch! We have to name it to be able to work without problems:</p>
<pre class="code literal-block">
D:\testgit>git checkout -b named_branch
Switched to a new branch 'named_branch'
D:\testgit>git branch
master
* named_branch
slave
</pre>
<p>Finally we may push again the named_branch to our remote origin.</p>
</div>
Using pelican to generate static sites on windows2013-10-07T10:20:00+03:002013-10-07T10:20:00+03:00Serafeim Papastefanostag:spapas.github.io,2013-10-07:/2013/10/07/pelican-static-windows/<p class="first last">A quick and dirty tutorial for generating a static site with pelican on Windows and hosting it to github.io as username.github.io.</p>
<div class="contents topic" id="contents">
<p class="topic-title">Contents</p>
<ul class="simple">
<li><a class="reference internal" href="#introduction" id="toc-entry-1">Introduction</a></li>
<li><a class="reference internal" href="#installing-pelican-and-generating-a-skeleton-for-your-site" id="toc-entry-2">Installing pelican and generating a skeleton for your site</a></li>
<li><a class="reference internal" href="#modifying-pelican-tools-for-windows" id="toc-entry-3">Modifying pelican tools for windows</a></li>
<li><a class="reference internal" href="#configuration-of-your-skeleton-site" id="toc-entry-4">Configuration of your skeleton site</a><ul>
<li><a class="reference internal" href="#settings" id="toc-entry-5">Settings</a></li>
<li><a class="reference internal" href="#themes" id="toc-entry-6">Themes</a></li>
<li><a class="reference internal" href="#plugins" id="toc-entry-7">Plugins</a></li>
</ul>
</li>
<li><a class="reference internal" href="#hosting-in-github-pages" id="toc-entry-8">Hosting in github pages</a></li>
<li><a class="reference internal" href="#publishing-changes" id="toc-entry-9">Publishing changes</a></li>
</ul>
</div>
<div class="section" id="introduction">
<h2><a class="toc-backref" href="#toc-entry-1">Introduction</a></h2>
<p><a class="reference external" href="http://docs.getpelican.com/en/3.3.0/">Pelican</a> is a great static site generator. A static site generator is a tool that users a number of input files to
generate a complete html site. No database or server side scripting is needed for the resulting site, that’s why
many such sites are hosted on <a class="reference external" href="http://pages.github.com/">github pages</a> (more on this later).</p>
<p>The input contains a number of html templates, css styles and the actual content of the site which most of the time is written in a
<a class="reference external" href="http://en.wikipedia.org/wiki/Lightweight_markup_language">lightweight markup language</a> like <a class="reference external" href="http://docutils.sourceforge.net/rst.html">reStructuredText</a> or <a class="reference external" href="http://daringfireball.net/projects/markdown/">Markdown</a>. The static site generator will generate the static pages by
inserting the content in the appropriate places in the templates.</p>
<p>In the following sections we will describe the installation of pelican on Windows
and the creation of the spapas.github.io site.</p>
</div>
<div class="section" id="installing-pelican-and-generating-a-skeleton-for-your-site">
<h2><a class="toc-backref" href="#toc-entry-2">Installing pelican and generating a skeleton for your site</a></h2>
<p>The official pelican quickstart can be found in <a class="reference external" href="http://docs.getpelican.com/en/latest/getting_started.html">http://docs.getpelican.com/en/latest/getting_started.html</a></p>
<p>To install pelican just enter:</p>
<pre class="code literal-block">
>pip install pelican
</pre>
<p>After installing pelican, I propose creating a parent directory that will
contain all your pelican sites, along with extra themes and plugins like this:</p>
<pre class="literal-block">
pelican/
├── pelican-themes
├── other-pelican-theme
├── spapas.github.io
└── other-sites
</pre>
<p>After creating the pelican directory just go in it with a command line and run the <tt class="docutils literal"><span class="pre">pelican-quickstart</span></tt> command.
It will ask you a number of questions, take a look at how I did answer these:</p>
<pre class="code literal-block">
pelican>pelican-quickstart
Welcome to pelican-quickstart v3.2.2.
This script will help you create a new Pelican-based website.
Please answer the following questions so this script can generate the files
needed by Pelican.
> Where do you want to create your new web site? [.] spapas.github.io
> What will be the title of this web site? Test github.io
> Who will be the author of this web site? Serafeim
> What will be the default language of this web site? [en]
> Do you want to specify a URL prefix? e.g., http://example.com (Y/n)
> What is your URL prefix? (see above example; no trailing slash) http://spapas.github.io
> Do you want to enable article pagination? (Y/n)
> How many articles per page do you want? [10]
> Do you want to generate a Makefile to easily manage your website? (Y/n) n
> Do you want an auto-reload & simpleHTTP script to assist with theme and site development? (Y/n) n
Done. Your new project is available at C:\progr\py\pelican\spapas.github.io
</pre>
<p>After that, you will have a pelican/spapas.github.io folder that will contain the following content:</p>
<pre class="literal-block">
spapas.github.io/
├── content
├── output
├── pelicanconf.py
└── publishconf.py
</pre>
<p>The content folder will contain your content (rst or markdown), the output will contain the generated html after you run pelican for your site.
The <tt class="docutils literal">pelicanconf.py</tt> will have a number of options for the generation of the development version of your site while the <tt class="docutils literal">publishconf.py</tt> will override some of the options
of <tt class="docutils literal">pelicanconf.py</tt> before generating the production version of your site that will actually be uploaded to github pages.</p>
</div>
<div class="section" id="modifying-pelican-tools-for-windows">
<h2><a class="toc-backref" href="#toc-entry-3">Modifying pelican tools for windows</a></h2>
<p>Pelican uses a Makefile and a unix shell script to generate the static html files and start an http server for development.
Because I prefer to use windows, I answered no to the questions of generating these when pelican-quickstarte asked me.
Instead I have included the following files inside the spapas.github.io directory:</p>
<ul class="simple">
<li><tt class="docutils literal">pelrun.bat</tt>, to generate the content for your debug site in the output directory:</li>
</ul>
<pre class="code literal-block">
pelican content --debug --autoreload --output output --settings pelicanconf.py
</pre>
<ul class="simple">
<li><tt class="docutils literal">pelserve.bat</tt>, to localy serve the generated debug site:</li>
</ul>
<pre class="code literal-block">
pushd output
python -m pelican.server
popd
</pre>
<ul class="simple">
<li><tt class="docutils literal">pelpub.bat</tt>, to generate the production site in the output directory:</li>
</ul>
<pre class="code literal-block">
pelican content --output output --settings publishconf.py
</pre>
<p>Now, when you want to develop your site locally, enter:</p>
<pre class="code literal-block">
spapas.github.io>start pelrun.bat
spapas.github.io>start pelserv.bat
</pre>
<p>If everything was ok until now, you can visit <a class="reference external" href="http://127.0.0.1:8000">http://127.0.0.1:8000</a> and will get the following output:</p>
<img alt="all ok!" src="/images/site1.png" style="width: 780px;" />
<p>Because of the -r option that is used in <tt class="docutils literal">pelrun.bat</tt> whenever you do a change (for instance when you add an rst file in the content directory)
it will be discovered and the output will be changed immediately!</p>
</div>
<div class="section" id="configuration-of-your-skeleton-site">
<h2><a class="toc-backref" href="#toc-entry-4">Configuration of your skeleton site</a></h2>
<div class="section" id="settings">
<h3><a class="toc-backref" href="#toc-entry-5">Settings</a></h3>
<p>There is a number of settings that you may configure in your site. The pelican settings reference can be found here: <a class="reference external" href="http://docs.getpelican.com/en/latest/settings.html">http://docs.getpelican.com/en/latest/settings.html</a>.
The pelicanconf.py and publishconf.py for this site can be found here:</p>
<p><a class="reference external" href="https://github.com/spapas/spapas.github.io/blob/source/pelicanconf.py">https://github.com/spapas/spapas.github.io/blob/source/pelicanconf.py</a>
<a class="reference external" href="https://github.com/spapas/spapas.github.io/blob/source/publishconf.py">https://github.com/spapas/spapas.github.io/blob/source/publishconf.py</a></p>
<p>The most important difference is the RELATIVE_URLS directive which must be True to the debug and False to the production.</p>
</div>
<div class="section" id="themes">
<h3><a class="toc-backref" href="#toc-entry-6">Themes</a></h3>
<p>Beyond the default pelican theme, you can use configure pelican to use various other themes. To enable them, go to your pelican parent directory and clone the pelican-themes github repository:</p>
<pre class="code literal-block">
pelican>git clone https://github.com/getpelican/pelican-themes.git
</pre>
<p>After that, you may select your theme from pelicanconf.py:</p>
<pre class="code literal-block">
THEME = "../pelican-themes/theme-name"
</pre>
<p>In my configuration, I am using the Octopress Theme for Pelican, which you may get from here <a class="reference external" href="https://github.com/duilio/pelican-octopress-theme">https://github.com/duilio/pelican-octopress-theme</a>. Just clone it to your
pelican directory and refer to it as above.</p>
</div>
<div class="section" id="plugins">
<h3><a class="toc-backref" href="#toc-entry-7">Plugins</a></h3>
<p>Pelican has a number of plugins. To enable them, go to your pelican parent directory and clone the pelican-plugins github repository:</p>
<pre class="code literal-block">
pelican>git clone https://github.com/getpelican/pelican-plugins.git
</pre>
<p>After that, you may add the following two lines to your pelicanconf.py:</p>
<pre class="code literal-block">
PLUGIN_PATH = '../pelican-plugins'
PLUGINS = ['a-plugin']
</pre>
</div>
</div>
<div class="section" id="hosting-in-github-pages">
<h2><a class="toc-backref" href="#toc-entry-8">Hosting in github pages</a></h2>
<p>To host your static site in github pages you must first of all create a repository named
username.github.io (for instance spapas.github.io) from github.</p>
<p>Then, generate your production output:</p>
<pre class="code literal-block">
spapas.github.io>pelpub.bat
</pre>
<p>Finally, go to your output directory, create a git repository, add everything and push it to your repository:</p>
<pre class="code literal-block">
spapas.github.io\output>git init
spapas.github.io\output>git add .
spapas.github.io\output>git commit -m Initial
spapas.github.io\output>git remote add origin https://github.com/spapas/spapas.github.io.git
spapas.github.io\output>git push origin master --force
</pre>
<p>The —force is to overwrite any previous versions - you don’t care about version control on your output (but you want it on your source).</p>
<p>You can now visit <a class="reference external" href="http://username.github.io">http://username.github.io</a> and see your statically generated site !</p>
<p>Don’t forget to add your source to the version control! To do that, add a .gitignore file in your pelican/username.github.io direcory
containing the following:</p>
<pre class="code literal-block">
output
</pre>
<p>The above file will ignore the contents of the output directory from version control. After that, do the following:</p>
<pre class="code literal-block">
spapas.github.io>git init
spapas.github.io>git add .
spapas.github.io>git commit -m Initial
spapas.github.io>git branch -m master source
spapas.github.io>git remote add origin https://github.com/spapas/spapas.github.io.git
spapas.github.io>git push origin source
</pre>
<p>The above will rename the master branch to source, will attach the origin remote to <a class="reference external" href="https://github.com/spapas/spapas.github.io.git">https://github.com/spapas/spapas.github.io.git</a> and will push the source
branch to it. Now you will have two branches in your username.github.io repository. One
named origin/master that will be your actual site and will be displayed through <a class="reference external" href="http://username.github.io">http://username.github.io</a> and one named origin/source that will contain the source of your site.</p>
<p>To learn more about branches and remotes you may check out <a class="reference external" href="https://spapas.github.io/2013/10/08/git-branches/">the git branches article</a>.</p>
</div>
<div class="section" id="publishing-changes">
<h2><a class="toc-backref" href="#toc-entry-9">Publishing changes</a></h2>
<p>Whenever you need to publish a new article or do changes to an existing one, you need to do the following:</p>
<ul class="simple">
<li>Run pelpub.bat to create the new output</li>
<li>Add/commit and push changes from your pelican site(source) folder to the source remote</li>
<li>Add/commit and push changes from your output folder to the master remote</li>
</ul>
<p>To help with this, here’s a ghdeploy.bat file that does all the above:</p>
<pre class="code literal-block">
call pelpub.bat
git add -A
git commit -m "Deploying changes"
git push origin source
pushd output
git add -A
git commit -m "Deploying changes"
git push origin master
popd
</pre>
<p>If you’ve followed this far, by running <tt class="docutils literal">pelpub.bat</tt> you’ll need to enter your github repository credentials (twice) and then
everything (source and master) will be deployed! To make things even better, I propose to use <a class="reference external" href="https://help.github.com/articles/generating-ssh-keys/">ssh based authentication</a>
to your github account and add new remote names to your source and master by running the following:</p>
<pre class="code literal-block">
git remote add origin2 git@github.com:spapas/spapas.github.io.git
</pre>
<p>to your pelican site and the output directories. After you change ghdeploy.bat to use <tt class="docutils literal">origin2</tt> instead of <tt class="docutils literal">origin</tt> you’ll be
able to deploy everything with just running it without entering any credentials!</p>
</div>