diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 0000000..e1abbc4 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 643a11cd737962a2858e21f592bec428 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.github/workflows/write_to_gh_pages.yml b/.github/workflows/write_to_gh_pages.yml deleted file mode 100644 index d9bf450..0000000 --- a/.github/workflows/write_to_gh_pages.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: write_to_gh_pages - -# execute this workflow automatically when we push to master -on: - push: - branches: [ main ] - -jobs: - - build_website_job: - runs-on: ubuntu-latest - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - - steps: - - name: Checkout - uses: actions/checkout@v2.3.4 - - - name: Set up Python - uses: actions/setup-python@v2.2.1 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - python -m pip install -U sphinx - python -m pip install myst-parser - python -m pip install furo - python -m pip install sphinx-copybutton - - - name: make the sphinx docs - run: | - make -C . clean - make -C . html - - - name: Init new repo in dist folder and commit generated files - run: | - cd _build/html/ - git init - touch .nojekyll - git add -A - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git commit -m 'deploy' - git config --global --add safe.directory /github/workspace/_build/html - - - name: Force push to destination branch - uses: ad-m/github-push-action@v0.6.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: gh-pages - force: true - directory: ./_build/html \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 212dcf1..0000000 --- a/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Environments -.venv - -# Built site -_build \ No newline at end of file diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/01_introduction.html b/01_introduction.html new file mode 100644 index 0000000..6d0bcb9 --- /dev/null +++ b/01_introduction.html @@ -0,0 +1,426 @@ + + + + + + + + + + Introduction - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Introduction

+

In this course, we will explore the exciting world of web development, learning how to create interactive and dynamic websites using Anvil, a platform that allows us to build web applications entirely with Python. By the end of the course, we will have the skills to design, develop, and deploy our very own web applications, which can be share with friends, family, and the broader community.

+
+

Importance of web development

+

Web development is crucial in today’s digital age as it serves as the backbone for creating and maintaining websites and web applications that are essential for businesses, education, and personal use. It enables us to establish an online presence, reach a global audience, and provide interactive and dynamic user experiences. By mastering web development, we will gain the ability to build platforms for e-commerce, social networking, information sharing, and much more.

+
+
+

The Project

+

To assist in this journey, we will create an application to assist students in planning their study. The app allows users to upload their assessment details and then it will then track assessments and provide a calendar which shows when the most assessment pressure is. Check the link below to see the finished app.

+

StudyM8

+
+
+

Required Knowledge

+

Since Anvil apps are written in Python, you will only need to know one programming language. You don’t even need to know HTML or CSS (they can be useful for more advanced features).

+

Before starting this course you will need foundational Python skills along with some more intermediate knowledge. The table below details the skillset, along with my other courses that will provide such knowledge.

+
+ + + + + + + + + + + + + + + + + +

Skills

Course

Basic syntax and structure
Control flow
Functions
Data structures

A Turtle Introduction to Python

Object Orientated Programming

Deepest Dungeon - Python OOP

Modules and Libraries

Space Rescue

+
+
+
+

Starting with Anvil

+

To access the Anvil platform go to https://anvil.works/ +https://anvil.works/.

+

For some of the features in this course, you will need to sign-up for a personal account.

+
+

Education Licences

+

If you are a teacher or student, Anvil offers free education licences. Just go to their pricing page and contact them.

+
+

Once you have an account, then it is time for us to start making websites.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/02_web_intro.html b/02_web_intro.html new file mode 100644 index 0000000..bc7c72a --- /dev/null +++ b/02_web_intro.html @@ -0,0 +1,442 @@ + + + + + + + + + + How websites work - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

How websites work

+ +
+

There is no cloud

+

We have all heard about ‘The Cloud’ as a term that refers to where files are located that aren’t on your device. It often seems like our computers are connected to some amorphous entity which magically stores and provide information.

+

cloud computer

+

As is often the case, this is a over-simplification for marketing purposes. It allows non-technical people to comprehend concepts enough to use them. In order to create websites, we need a much more technical understanding how the web works.

+
+
+

Frontend and Backend

+

In truth there is no cloud, it is just someone else’s computer (a phrase that has inspired many memes). The diagram below is better representation of how you access websites. It is still quite simple, but it will be suffice for our purposes.

+

frontend backend

+

Frontend:

+
    +
  • refers to the processes that occur on your device

  • +
  • also called client-side

  • +
  • is the part of the application that users interact with directly, such as the layout, design, buttons, images, and forms on a website.

  • +
+

Backend:

+
    +
  • refers to the processes that occur on the web server

  • +
  • also called server-side

  • +
  • is remote to the device and accessed through the internet

  • +
  • handles data processing, storage, and retrieval, ensuring that the frontend receives the necessary data to display to the user

  • +
+
+

The web restaurant

+

The easiest way to think about frontend and backend is if you imagine dining at a restaurant.

+

The front end is like the waiter, while the backend is like the chef:

+
    +
  • the waiter will provide you with the menu to choose your meal

  • +
  • the waiter will take your order and give it to the chef

  • +
  • the chef will use ingredients to complete your order

  • +
  • the chef will then get the waiter to take your order back to you

  • +
+

Notice that:

+
    +
  • all your interactions with the chef go through the waiter

  • +
  • the waiter doesn’t do any of the cooking

  • +
  • the chef will need to access stored resources to complete your order

  • +
+
+
+

Real world

+

In the same way the frontend and backend work together to serve you websites:

+
    +
  • you enter a website address

  • +
  • the frontend then goes to that address and asks the backend for the website

  • +
  • the backend uses the stored resources (images, text, code, etc.) to create the requested website

  • +
  • the backend then sends these to the frontend

  • +
  • the frontend then displays the website on your screen.

  • +
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/03_studyM8_design.html b/03_studyM8_design.html new file mode 100644 index 0000000..145ad13 --- /dev/null +++ b/03_studyM8_design.html @@ -0,0 +1,474 @@ + + + + + + + + + + StudyM8 design - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

StudyM8 design

+ +

Before we start creating our website, we need to plan what it will look like. You have already had a chance to use the finished app, but you will not normally have this opportunity. Therefore, it is best to start with an understanding of the different pages that your website is comprised of, how they fit together and what they will look like.

+
+

Site Map

+

The first step is to identify all the webpages your site contains, and how they fit together. We show this using a site map. A site map is a simple diagram that shows all the pages and the links between them.

+

Here is the sitemap for our StudyM8 app.

+

Site map

+

A few things worth noting:

+
    +
  • users cannot access any pages unless they have either logged in or registered

  • +
  • we don’t need to design the login and register pages as Anvil provides these features

  • +
  • the Set Details page can only be accessed via the Account page

  • +
+
+
+

Wireframes

+

Wireframes are diagrams that provide a clear, visual representation of a website’s structure and layout without the distraction of detailed design elements. This allows designers, developers, and clients to focus on the functionality, navigation, and user experience, ensuring that all key components are logically arranged and work together effectively.

+

Below are the wireframes for StudyM8. They follow web design principles called Material Design, which was developed by Google. It’s popularity on the web will make our app easier to use, as users will be familiar with different website components.

+
+

Welcome Page

+

The welcome page is the landing page for the website. This is where users first arrive and the only page that can be accessed without a user account. It will be used to explaining the app and encourage the user to register for an account.

+

Welcome Page

+

Notice the Material Design features:

+
    +
  • the menu bar across the top of the page, with links on the right-hand side

  • +
  • the hamburger icon (three horizontal lines) in the top left corner that will open and close the left panel menu

  • +
  • the left panel menu with links to the different web pages

  • +
  • the content is contained in cards (the white and grey boxes). Not only does this groups information for the user, but it also is a useful organising tool for the frontend

  • +
+
+

Lorem Ipsum

+

What is the gobbledygook text on the page? It is called Lorem Ipsum, and it is a text placeholder. It allows designers to focus on the layout of text without needing the actual content that will be used. You could just mash your keyboard, but Lorem Ipsum has the advantage of actually being words, albeit nonsensical. This means that it will act like text in regards to layout (word wrapping, punctuation, paragraph length, etc.).

+

If you’re not keen on the classic Lorem Ipsum, there are plenty of others to choose from.

+

The Ultimate List of Lorem Ipsum Generators

+
+
+
+

Home Page

+

The home page is what authenticated (signed in) uses will see. It provides a list of all the assessments and their details.

+

Home Page

+

Things to note:

+
    +
  • each assessment has it’s own card. This uses a design feature called repeating panels. The number of panels show will depend on the number of assessments displayed

  • +
  • each assessment also has an edit button that will allow the user to edit that assessments details.

  • +
+
+
+

Edit Page

+

The edit page is a variation of the home page in which the user can an assessment’s details. When the user clicks an assessment’s edit button, the card for that assessment changes

+

Edit Page

+

Things to note:

+
    +
  • both the start date and the due date are edited using a date picker component. Since dates are written differently in different countries, date pickers prevent users from entering the date incorrectly.

  • +
+
+
+

Calendar Page

+

The calendar page has a chart that shows all incomplete assessments, and when the user needs to be working on them. It is an easy way to identify when their academic load is the highest.

+

Calendar Page

+

Things to note:

+
    +
  • the actual chart displayed will look different to our finished app

  • +
+
+
+

Add Page

+

The add page allows the user to enter new assessment details. Again note the use of date pickers.

+

Add Page

+
+
+

Account Page

+

The account page shows the user their current details. The Edit button on this page take them to the set details page.

+

Account page

+
+
+

Set Details Page

+

The set details page allows the user to change their details saved on the app.

+

Set Details page

+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/04_create_app.html b/04_create_app.html new file mode 100644 index 0000000..5603a34 --- /dev/null +++ b/04_create_app.html @@ -0,0 +1,435 @@ + + + + + + + + + + Create new app - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Create new app

+ +

Now that we have planned our app, it is time to create. First you need to log into Anvil.

+

You will be presented with a choice between Create a new app and …or start from a template. We want to create a new app.

+

step 1

+
+

Choose Theme

+

You will then be asked to choose a theme. These tutorials will be using Material Design, but you can use Material Design 3 since they are structurally the same. Check the examples below to decide (don’t worry about the colours, you can change them latter).

+

step 2

+
+

Material Design example

+

material design

+
+
+

Material Design 3 example

+

material design 3

+

Once you have chosen your template, you will be taken to the Anvil interface. We’ll explore the interface in the next tutorial. Before we do, we are just going to adjust a few things on our new app.

+
+
+
+

Rename App

+

First we will rename the app. To do this go to settings by clicking the cog icon in the left hand side menu.

+

step 3

+

Fill out the details is steps 1-3 below and then click on the Change image button.

+

step 4

+

Right mouse click on the image of the logo below and save it to your computer.

+

logo

+

Click on the Upload button, select the logo.png file you just downloaded, then click Save

+

step 5

+
+
+

Colour Scheme

+

The last step is choosing your colour scheme. You can choose between pre-set schemes or customise your own. You can change the colour scheme at any time.

+

Click on the colour pallet icon in the left hand side menu to enter the App Theme menu.

+

step 6

+

Then choose Colour Scheme

+

step 7

+

The options available to your will depend if you have chosen Material Design or Material Design 3. Either way, you will be able to:

+
    +
  1. Choose from some pre-set options

  2. +
  3. Edit the individual HTML colour codes for different parts of your theme.

  4. +
+

step 8

+

Go ahead and choose a colour scheme.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/05_anvil_interface.html b/05_anvil_interface.html new file mode 100644 index 0000000..378de95 --- /dev/null +++ b/05_anvil_interface.html @@ -0,0 +1,484 @@ + + + + + + + + + + Anvil Interface - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Anvil Interface

+ +

Lets have a look at the different parts of the Anvil interface.

+

The interface is made up of five different parts:

+
    +
  • Main menu

  • +
  • Side menu

  • +
  • Submenu

  • +
  • Workspace

  • +
  • Terminal

  • +
+

interface

+

Lets look at each of these. along with the feature we will use

+
+

Viewing Instruction Images

+

Anvil is a web app, which means making instruction screen capture with adequate detail was difficult. Many of the instructions will only capture one part of the Anvil interface which should provide enough detail to read.

+

All screen captures on this site are actually larger than displayed. If you are find the instructions too difficult to follow, right mouse click on the image, the choose Open image in new tab. This will show you a larger version of the image.

+
+ +
+

Side menu

+

The side menu gives:

+
    +
  • Files - access the files that make up your website

  • +
  • Tables - work with the tables that make up your backend database

  • +
  • Setting - manage your apps meta data details

  • +
  • Theme - work with the CSS of your website

  • +
  • Add - include more features

  • +
+

side menu

+
+ +
+

Workspace

+

The workspace is the main area where you will create your web app. What it displays, depends on which of the three modes is selected. The workspace can hold multiple files with each file residing in it’s own tab.

+
+

Design mode

+

To the left of the design mode you have the layout. This is where you add and position components to make your website. To the right you have the components panel.

+

design

+
+

Components panel

+

The components panel has three sections:

+
    +
  • Add components - click and drag components from here to add them to your layout

  • +
  • Properties - edit the properties of the currently select component

  • +
  • Components - lists all the components on the current layout

  • +
+

component panel

+
+
+
+

Code mode

+

In the code mode of the workspace you will find the code panel where you write code and the code snippet panel where you can see the code hooks for the components.

+

code mode

+
+

Code snippets

+

The code snippets panel list all the name components, their properties and those properties current values.

+

code snippets

+
+
+
+
+

Terminal

+

The integrated terminal displays a range of output that normally occur in website development. It will display two types of outputs you are interested in:

+
    +
  • output from print statements

  • +
  • error messages

  • +
+

terminal

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/06_create_home_form.html b/06_create_home_form.html new file mode 100644 index 0000000..9f6d68c --- /dev/null +++ b/06_create_home_form.html @@ -0,0 +1,499 @@ + + + + + + + + + + Create the Main Form - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Create the Main Form

+ +

Almost time to start making our website, but before we do, there is one last concept we need to understand.

+

Looking at our wireframes you will notice that they all look very similar. The top bar, and the side menu all look the same, it is only the main content in the grey centre panel that changes. As the user navigates from one page to another, we could just send the entire page’s data each time, but this is extremely inefficient.

+

In our frontend backend diagram, it is the communication over the internet between frontend and backend (the lightening bolt) that is the slowest.

+

frontend backend

+

This means it is faster to minimise the amount of data being transferred. When changing from the Home page to the Calendar page, why send the side menu data when it is already on the user’s computer. This is where Single-Page Applications come in.

+
+

Single-Page Applications

+

A Single-Page Application doesn’t reload every time the user navigates to a new page, but rather, it only loads the parts of the website that change. This approach results in a smoother, faster user experience similar to that of a desktop application.

+

How does it work? Initially a main form is loaded containing the content that remains constant. Then different components or views are swapped in and out based on user interactions, this is called dynamic loading.

+

In our design our main form would look something like this:

+

main form

+

The parts that get dynamically loaded are called components, but we’ll deal with those in the next tutorial. For now, lets create our main form.

+
+
+

The MainForm

+

Anvil already has the start of your MainForm opened for you. If you cannot see it go:

+
    +
  • Files in the side menu

  • +
  • Form1 in the submenu

  • +
+

open form

+
+

Rename Form1

+

Form1 is a poor name for this file. So we will rename it to MainForm (check your spelling and capitalisation).

+
+

Naming Conventions

+

We are now making quite complex applications. The finished app will consist of 11 different files, and over 50 componets. All of these are named and some of the names are very similar. Therefore it is important to use naming conventions.

+

Naming conventions include information that makes it easier to remember what the component is. For example, all links will start with the word link, all web components will be in camel case and have the word component appended to the end (eg. CalendarComponent)

+

It seems like extra work, but it makes things easier by reducing your coignative load.

+
+

Click in the ellipses (three dots) then rename. Type MainForm then press enter.

+

rename

+
+
+

Add Title

+

To add the top left StudyM8 title you need to:

+
    +
  1. In the Add Component section, click on Label.

  2. +
  3. Then, in the MainForm’s menu bar, click on the Drop title here box.

  4. +
  5. Go to the Properties section and in the text box add StudyM8.

  6. +
  7. Still in the Properties section, rename the label to label_title.

  8. +
+

title

+
+ + + +
+
+

Testing

+

The layout of your MainForm is now complete. Click the green Run button and have a look at the design. It should look the same as below (except for colours).

+

test

+

Click on the red Stop button when you have finished.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/07_create_components.html b/07_create_components.html new file mode 100644 index 0000000..078ddda --- /dev/null +++ b/07_create_components.html @@ -0,0 +1,455 @@ + + + + + + + + + + Adding Components - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Adding Components

+ +

We now need to create all the components that will be dynamically loaded into the MainForm. Looking at our design, although we have seven pages, we only need six components, since the Edit Page is just a modified Home Page.

+

The six components we need to make are:

+
    +
  • WelcomeComponent for the Welcome Page

  • +
  • HomeComponent for the Home Page

  • +
  • CalendarComponent for the Calendar Page

  • +
  • AddComponent for the Add Page

  • +
  • AccountComponent for the Account Page

  • +
  • SetDetailsComponent for the Set Details Page

  • +
+

First we’ll work through creating the WelcomeComponent, then you can use the same process for creating the other five components.

+
+

Creating the WelcomeComponent

+
+

Add new form

+

First your will need to add a new form to the website. To do this:

+
    +
  1. In the File submenu, click on the Client Code ellipses

  2. +
  3. Choose Add form

  4. +
  5. Choose Blank Panel

  6. +
  7. Rename the form to WelcomeComponent

  8. +
+

new form

+
+
+

Add a title

+

We will gradually add the different elements to these components. In the meanwhile we will add a title to each component so we can distinguish between them.

+

To add a title:

+
    +
  1. In the Add Components click on Headline

  2. +
  3. Click at the top of the WelcomeComponent

  4. +
  5. In Properties change the Text to Welcome

  6. +
  7. Scroll down to align and choose centre

  8. +
+

add title

+

You have now added your first component.

+
+
+
+

Create other components

+

Follow the same steps to create the remaining components, choosing appropriate titles:

+
    +
  • HomeComponent for the Home Page

  • +
  • CalendarComponent for the Calendar Page

  • +
  • AddComponent for the Add Page

  • +
  • AccountComponent for the Account Page

  • +
  • SetDetailsComponent for the Set Details Page

  • +
+
+
+

Check your work

+

You should now have seven files in your file submenu:

+
    +
  • AccountComponent

  • +
  • AddComponent

  • +
  • CalendarComponent

  • +
  • HomeComponent

  • +
  • MainForm

  • +
  • SetDetailsComponent

  • +
  • WelcomeComponent

  • +
+

They should look similar to the following:

+

finished

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/08_connect_links.html b/08_connect_links.html new file mode 100644 index 0000000..1ab6a76 --- /dev/null +++ b/08_connect_links.html @@ -0,0 +1,590 @@ + + + + + + + + + + Connecting links - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/09_set_start_component.html b/09_set_start_component.html new file mode 100644 index 0000000..24fd5f9 --- /dev/null +++ b/09_set_start_component.html @@ -0,0 +1,481 @@ + + + + + + + + + + Set the Initial Component - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Set the Initial Component

+ +

When you tested your website in the last tutorial, did you notice that, at first, there was no component loaded on the MainForm. This isn’t a good look. So, in this tutorial, we will make the MainForm automatically load the HomeComponent as soon as it starts.

+
+

Running code upon loading

+

This is much easier than it first seems. If you look at the MainForm code you will notice a comment on line 14.

+
10  def __init__(self, **properties):
+11    # Set Form properties and Data Bindings.
+12    self.init_components(**properties)
+13
+14    # Any code you write here will run before the form opens.
+
+
+

It does exactly what is says. In order to make code run as soon as the form loads, we just need to add it to the __init__ after that comment.

+
+
+

Loading HomeComponent

+

So what code do we need to add? Well, you have already written code that loads the HomeComponent into the MainForm. Check line 18 below.

+
16  def link_home_click(self, **event_args):
+17    self.content_panel.clear()
+18    self.content_panel.add_component(HomeComponent())
+
+
+

So it would make sense to simply copy that line and paste it under line 14. So the __init__ will now look like:

+
10  def __init__(self, **properties):
+11    # Set Form properties and Data Bindings.
+12    self.init_components(**properties)
+13
+14    # Any code you write here will run before the form opens.
+15    self.content_panel.add_component(HomeComponent())
+
+
+
+
+

Testing

+

Run your website and the HomeComponent should show up straight away.

+

test

+
+
+

Common Errors

+
+

NameError

+

name error

+

This might mean that when you pasted the add_component line, the indentation was incorrect. Make sure that it lines up the the comment on the line above.

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3from ..HomeComponent import HomeComponent
+ 4from ..CalendarComponent import CalendarComponent
+ 5from ..AddComponent import AddComponent
+ 6from ..AccountComponent import AccountComponent
+ 7
+ 8
+ 9class MainForm(MainFormTemplate):
+10  def __init__(self, **properties):
+11    # Set Form properties and Data Bindings.
+12    self.init_components(**properties)
+13
+14    # Any code you write here will run before the form opens.
+15    self.content_panel.add_component(HomeComponent())
+16
+17  def link_home_click(self, **event_args):
+18    self.content_panel.clear()
+19    self.content_panel.add_component(HomeComponent())
+20
+21  def link_calendar_click(self, **event_args):
+22    self.content_panel.clear()
+23    self.content_panel.add_component(CalendarComponent())
+24
+25  def link_add_click(self, **event_args):
+26    self.content_panel.clear()
+27    self.content_panel.add_component(AddComponent())
+28
+29  def link_account_click(self, **event_args):
+30    """This method is called when the link is clicked"""
+31    self.content_panel.clear()
+32    self.content_panel.add_component(AccountComponent())
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/10_adjust_title.html b/10_adjust_title.html new file mode 100644 index 0000000..0b9e335 --- /dev/null +++ b/10_adjust_title.html @@ -0,0 +1,559 @@ + + + + + + + + + + Adjust Page Title - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Adjust Page Title

+ +

In this tutorial we are going to change the MainForm title into a breadcrumb.

+
+

Website Breadcrumbs

+

Breadcrumbs are a web design elements used for navigation. They typically display a horizontal list near the top of a page showing the path from the homepage to the current page, with each section separated by a symbol.

+
+
+

Planning

+

To achieve this, we want the MainForm title to display the following text for each component (at this stage we will only focus on the components we have linked):

+
    +
  • HomeComponent → StudyM8

  • +
  • CalendarComponent → StudyM8 - Calendar

  • +
  • AddComponent → StudyM8 - Add

  • +
  • AccountComponent → StudyM8 - Account

  • +
+ +
+
+

The Code

+
+

Determine the stem

+

Open the MainForm in code mode. The first thing we need to do is store the value for the breadcrumb stem. We will do that in the __init__.

+
10  def __init__(self, **properties):
+11    # Set Form properties and Data Bindings.
+12    self.init_components(**properties)
+
+
+

Check the comment on line 11. The breadcrumb stem will be a Form property, so it should go into this section of the code. Add the highlighted code.

+
10  def __init__(self, **properties):
+11    # Set Form properties and Data Bindings.
+12    self.init_components(**properties)
+13    self.breadcrumb_stem = self.label_title.text
+
+
+
+

Code explanation

+
    +
  • line 13:

    +
      +
    • self.label_title.text → take the current text value of the lable_title on this form (MainForm)

    • +
    • self.breadcrumb_stem = → store it in the breadcrumb_stem variable

    • +
    +
  • +
+
+
+
+

Change the title

+

We now need to change the title for each component. The event that will trigger this action will be link clicking. Luckily we already have event handlers for this, so we can simply add our code to these event handlers.

+
+

HomeComponent

+

Lets start with the HomeComponent. We need to return the title text back to the breadcrumb stem. This is important for when users return to the Home page from another page.

+

Adjust the link_home_click handler by adding the highlighted code below:

+
18  def link_home_click(self, **event_args):
+19    self.content_panel.clear()
+20    self.content_panel.add_component(HomeComponent())
+21    self.label_title.text = self.breadcrumb_stem
+
+
+
+

Code explanation

+
    +
  • line 21:

    +
      +
    • self.breadcrumb_stem → take the value in the breadcrumb_stem variable

    • +
    • self.label_title.text = → write it to the label_title

    • +
    +
  • +
+
+
+
+

Other components

+

Next we will do the CalendarComponent. Go to the link_calendar_click handler and add the highlighted code:

+
18  def link_calendar_click(self, **event_args):
+19    self.content_panel.clear()
+20    self.content_panel.add_component(CalendarComponent())
+21    self.label_title.text = self.breadcrumb_stem + " - Calendar"
+
+
+
+

Code explanation

+
    +
  • line 21:

    +
      +
    • self.breadcrumb_stem → take the value in the breadcrumb_stem variable

    • +
    • + " - Calendar" → add - Calendar to the end of it

    • +
    • self.label_title.text = → write it to the label_title

    • +
    +
  • +
+
+

The breadcrumb for both AddComponent and AccountComponent are the same process, so go ahead and make the changes.

+
+
+
+
+

Testing

+

Now launch your website and check that your breadcrumb changes when each component is loaded.

+

test

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3from ..HomeComponent import HomeComponent
+ 4from ..CalendarComponent import CalendarComponent
+ 5from ..AddComponent import AddComponent
+ 6from ..AccountComponent import AccountComponent
+ 7
+ 8
+ 9class MainForm(MainFormTemplate):
+10  def __init__(self, **properties):
+11    # Set Form properties and Data Bindings.
+12    self.init_components(**properties)
+13    self.breadcrumb_stem = self.label_title.text
+14
+15    # Any code you write here will run before the form opens.
+16    self.content_panel.add_component(HomeComponent())
+17
+18  def link_home_click(self, **event_args):
+19    self.content_panel.clear()
+20    self.content_panel.add_component(HomeComponent())
+21    self.label_title.text = self.breadcrumb_stem
+22
+23  def link_calendar_click(self, **event_args):
+24    self.content_panel.clear()
+25    self.content_panel.add_component(CalendarComponent())
+26    self.label_title.text = self.breadcrumb_stem + " - Calendar"
+27
+28  def link_add_click(self, **event_args):
+29    self.content_panel.clear()
+30    self.content_panel.add_component(AddComponent())
+31    self.label_title.text = self.breadcrumb_stem + " - Add"
+32
+33  def link_account_click(self, **event_args):
+34    """This method is called when the link is clicked"""
+35    self.content_panel.clear()
+36    self.content_panel.add_component(AccountComponent())
+37    self.label_title.text = self.breadcrumb_stem + " - Account"
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/11_active_link.html b/11_active_link.html new file mode 100644 index 0000000..1d0d6ee --- /dev/null +++ b/11_active_link.html @@ -0,0 +1,659 @@ + + + + + + + + + + Show Active Link - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/12_add_users.html b/12_add_users.html new file mode 100644 index 0000000..1ed5f5c --- /dev/null +++ b/12_add_users.html @@ -0,0 +1,428 @@ + + + + + + + + + + Adding Users - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Adding Users

+ +

Anvil provides a range of useful features. One of these features is user management. The user management tool simplifies the process of handling user signups, logins, and permissions within an Anvil application. It provides built-in functions to manage user authentication and maintain user data securely.

+

In this tutorial we will set up the user management system. In the next tutorial we will incorporate it’s features into our website.

+
+

Setting up user management

+

To setup our user management click on the Add button on the sidebar

+

add

+

Then choose Users

+

users

+

Notice that there is a new User icon in the sidebar. This is how you can get back to this page.

+

users icon

+

On the Users page:

+
    +
  1. Select the checkbox for Remember login between sessions → will save you login in every time you launch your website.

    +
      +
    • the default 30 days will be fine.

    • +
    +
  2. +
  3. Select the checkbox for Allow visitors to sign up → will allow you to use the registration system.

  4. +
  5. Deselect the checkbox for Confirm email address before users can log in → will allow you to use bogus emails for development.

  6. +
+

user settings

+

That’s all our setting done.

+

You can see that there are a heap more user settings. You may choose to turn some of these on in the future, but the current settings will make development run more smoothly.

+
+
+

Adding Fields

+

Down the bottom of the Users page, you will notice the users table. This table will store the details of all the users who register on your website. Most of the columns as self evident, but there are two more that we want to add:

+
    +
  • first_name

  • +
  • last_name

  • +
+

Scroll the table all the way to the right then click on the plus sign to add a new column.

+

In the Add a Column dialogue:

+
    +
  1. Name the column first_name

  2. +
  3. Choose Text as the Column Type

  4. +
  5. Then click OK

  6. +
+

first name

+

Now add another column called last_name

+

last_name

+

That’s it. Your user management tool is now ready.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/13_user_features.html b/13_user_features.html new file mode 100644 index 0000000..fb98bca --- /dev/null +++ b/13_user_features.html @@ -0,0 +1,604 @@ + + + + + + + + + + User Management Features - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

User Management Features

+ +

In this tutorial you will incorporate user management features to allow users to:

+
    +
  • Register for an account

  • +
  • Login with a registered account

  • +
  • Logout of the website

  • +
+

The events that trigger all these features will be link clicks, so we have to go back to the MainForm Design mode and create handlers for the Register, Login and Logout links.

+

Handlers

+
+

Code

+
+

Register

+

The first feature we need to add is the registration feature so we can create users.

+

Go to the link_register_click handler. Delete the comment and the pass statement. Then add the highlighted code below:

+
63  def link_register_click(self, **event_args):
+64    anvil.users.signup_with_form(allow_cancel=True)
+
+
+
+

Code explanation

+
    +
  • line 64:

    +
      +
    • signup_with_form → use the built-in registration form

    • +
    • anvil.users. → from the Anvil user managment tool

    • +
    • allow_cancel=True → add a cancel button to the registration form

    • +
    +
  • +
+
+

Launch your website and try to register an account.

+

register

+

Stop your website and check that your account has been created in the users table.

+

check user

+
+
+

Logout

+

Since we are remembering the user, we now need to be able to logout.

+

Go to the link_logout_click handler. Delete the comment and the pass statement. Then add the highlighted code below:

+
70  def link_logout_click(self, **event_args):
+71    anvil.users.logout()
+
+
+
+

Code explanation

+
    +
  • line 70:

    +
      +
    • logout() → use the built-in logout feature

    • +
    • anvil.users. → from the Anvil user management tool

    • +
    +
  • +
+
+
+
+

Login

+

Finally we need to allow users already registered to login

+

Go to the link_login_click handler. Delete the comment and the pass statement. Then add the highlighted code below:

+
66  def link_login_click(self, **event_args):
+67    anvil.users.login_with_form(allow_cancel=True)
+
+
+
+

Code explanation

+
    +
  • line 67:

    +
      +
    • login_with_form → use the built-in login form

    • +
    • anvil.users. → from the Anvil user managment tool

    • +
    • allow_cancel=True → add a cancel button to the registration form

    • +
    +
  • +
+
+
+
+
+

Test

+
+

Run the test

+

It is really hard to test if our user features work, as we have no indication if the user is logged in or not. To solve this we will add some testing code to the handlers.

+
63  def link_register_click(self, **event_args):
+64    anvil.users.signup_with_form(allow_cancel=True)
+65    print("Register", anvil.users.get_user())
+66
+67  def link_login_click(self, **event_args):
+68    anvil.users.login_with_form(allow_cancel=True)
+69    print("Login", anvil.users.get_user())
+70
+71  def link_logout_click(self, **event_args):
+72    anvil.users.logout()
+73    print("Logout", anvil.users.get_user())
+
+
+
+

Code explanation

+

All three are estentially the same, so I’ll just explain the first.

+
    +
  • line 65:

    +
      +
    • print → send to the terminal

    • +
    • "Register" → the name of the link just clicked

    • +
    • anvil.users.get_user() → what the current user is

    • +
    +
  • +
+
+

Launch your website, then:

+
    +
  • Register another user

  • +
  • Logout

  • +
  • Login using your new user

  • +
+

Notice your terminal has three new messages:

+

terminal

+

Click on the Terminal tab and you should have the three messages below.

+

messages

+
+
+

Tidy up

+

If you received the same messages, your test passed.

+

Go back into the MainForm code and remove the three print statements.

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6import anvil.users
+ 7from ..HomeComponent import HomeComponent
+ 8from ..CalendarComponent import CalendarComponent
+ 9from ..AddComponent import AddComponent
+10from ..AccountComponent import AccountComponent
+11
+12
+13class MainForm(MainFormTemplate):
+14  def __init__(self, **properties):
+15    # Set Form properties and Data Bindings.
+16    self.init_components(**properties)
+17    self.breadcrumb_stem = self.label_title.text
+18
+19    # Any code you write here will run before the form opens.
+20    self.content_panel.add_component(HomeComponent())
+21    self.set_active_link("home")
+22
+23  def set_active_link(self, state):
+24    if state == "home":
+25      self.link_home.role = "selected"
+26    else:
+27      self.link_home.role = None
+28    if state == "add":
+29      self.link_add.role = "selected"
+30    else:
+31      self.link_add.role = None
+32    if state == "calendar":
+33      self.link_calendar.role = "selected"
+34    else:
+35      self.link_calendar.role = None
+36  
+37  # --- link handlers
+38  def link_home_click(self, **event_args):
+39    self.content_panel.clear()
+40    self.content_panel.add_component(HomeComponent())
+41    self.label_title.text = self.breadcrumb_stem
+42    self.set_active_link("home")
+43
+44  def link_calendar_click(self, **event_args):
+45    self.content_panel.clear()
+46    self.content_panel.add_component(CalendarComponent())
+47    self.label_title.text = self.breadcrumb_stem + " - Calendar"
+48    self.set_active_link("calendar")
+49
+50  def link_add_click(self, **event_args):
+51    self.content_panel.clear()
+52    self.content_panel.add_component(AddComponent())
+53    self.label_title.text = self.breadcrumb_stem + " - Add"
+54    self.set_active_link("add")
+55
+56  def link_account_click(self, **event_args):
+57    """This method is called when the link is clicked"""
+58    self.content_panel.clear()
+59    self.content_panel.add_component(AccountComponent())
+60    self.label_title.text = self.breadcrumb_stem + " - Account"
+61    self.set_active_link(("account"))
+62
+63  def link_register_click(self, **event_args):
+64    anvil.users.signup_with_form(allow_cancel=True)
+65
+66  def link_login_click(self, **event_args):
+67    anvil.users.login_with_form(allow_cancel=True)
+68
+69  def link_logout_click(self, **event_args):
+70    anvil.users.logout()
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/14_account_links_visiability.html b/14_account_links_visiability.html new file mode 100644 index 0000000..42f923d --- /dev/null +++ b/14_account_links_visiability.html @@ -0,0 +1,692 @@ + + + + + + + + + + Account Links Visibility - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/15_set_details.html b/15_set_details.html new file mode 100644 index 0000000..d8f476f --- /dev/null +++ b/15_set_details.html @@ -0,0 +1,541 @@ + + + + + + + + + + SetDetailsComponent Design - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

SetDetailsComponent Design

+ +

If you look at the user table, you will notice that our registered users are missing their first and last names. In this tutorial we will fix this issue.

+
+

Planning

+

If you refer back to our design you will notice that we have a SetDetailsComponent which has a space for the user names.

+

SetDetailsComponent

+

At the moment, when a new user is registered, the link_register_click method redirects them to the HomeComponent. We need to change that so it redirects to the SetDetailsComponent.

+

We then need to create an event handler for the Save button that reads the values and writes them to the user table. Our user table is expecting text values, what if they accidently leave one of the names blank? We need to validate the data to make sure that both text boxes have values, and only then write those values to the user table.

+
+

Data validation

+

Data validation of user input is crucial for several reasons:

+
    +
  1. Security: Validating data helps prevent security vulnerabilities such as SQL injection and cross-site scripting (XSS), protecting the application and its users from malicious attacks.

  2. +
  3. Data Integrity: Ensures that the data entered is accurate, consistent, and usable, maintaining the quality of the information stored.

  4. +
  5. User Experience: Provides immediate feedback to users when they enter incorrect or incomplete data, improving the overall user experience and reducing errors.

  6. +
  7. Functionality: Ensures that the application functions correctly by preventing invalid data from causing issues or crashes.

  8. +
  9. Compliance: Helps meet legal and regulatory requirements for data handling and privacy by ensuring that only valid data is processed and stored.

  10. +
+
+

Finally, when that is all done, we need to load the AccountComponent, so they can see the names they just entered.

+

This is quite a bit, so we’ll split it over the next three tutorials.

+
    +
  1. Deals with the layout of the SetDetailsComponent

  2. +
  3. Coding the SetDetailsComponent

  4. +
  5. Addresses writing the information to the user table

  6. +
+
+
+

Designing

+

Before we can jump into our code we need to add all the appropriate elements to the layout of the SetDetailsComponent.

+

Open the SetDetailsComponent in design mode.

+
+

Title

+

Currently, the only element on the component is the title. We’ll start with adding an icon to the title.

+
    +
  1. Click on the title

  2. +
  3. Find Icon in the Properties panel

  4. +
  5. Choose the fa:edit icon

  6. +
+

title

+
+
+

Add Card

+

Cards are an essential design element in Material Design. They are a container used to group related information, which make content easy to scan and interact with. If you have used websites in the last decade, you have seen cards, you just might not have known what they are.

+

We are going to add a card under the title:

+
    +
  1. Find Card in the Add Components panel

  2. +
  3. Click on Card

  4. +
  5. Move to under the title (look for the blue line)

  6. +
  7. Click

  8. +
+

add card

+

Since our code is not going to interact with this card, we don’t need to change it’s name.

+
+
+

Add Column Panel

+

In order to organise our labels and textboxes, we need to use a layout element called a Column Panel.

+
    +
  1. In the Add Components panel, find and click Column Panel

  2. +
  3. Click inside the Card in layout (make sure you have the blue line inside the card)

  4. +
+

add column panel

+
+
+

Add First name elements

+

Now we can start adding the First name elements. We’ll start with the label.

+
    +
  1. In the Add Component panel, find and click Label

  2. +
  3. Click inside the Card in layout (make sure you have the blue line inside the card)

  4. +
  5. In Text in the Properties panel write First name

  6. +
  7. Find role in the Appearance section of the Properties panel and choose input-prompt

  8. +
  9. Again our code will not interact with this label, so we don’t have to change the name.

  10. +
+

first name label

+

Now the First name text box. Note, our code will interact with this element, so we should change the name to something that makes sense.

+
    +
  1. Click on Text Box in the Add Component panel

  2. +
  3. Click inside the Card. To ensure you’re inside the Column Panel you need to pay very close attention to where the blue line is before you click. The line should be right beside a brown box surrounding the First name label.

  4. +
  5. Change the name of the text box to text_box_first_name

  6. +
  7. Find the placeholder property and add Enter first name

  8. +
+

first name text box

+
+

Placeholders

+

Textbox placeholders are text displayed inside input fields on forms to give users a hint about what information is expected. They appear when the input field is empty and disappear once the user starts typing.

+
+
+
+

Add Last name elements

+

Now to add the Last name label.

+
    +
  1. Click Label in the Add Component panel

  2. +
  3. Click inside the Card, making sure that your blue line is directly under the the First name label

  4. +
  5. In Text in the Properties panel write Last name

  6. +
  7. Find role in the Appearance section of the Properties panel and choose input-prompt

  8. +
  9. Just like the First name label our code will not interact with this label, so we don’t have to change the name.

  10. +
+

last name label

+

Then the Last name text box, which is virtually the same the the First name text box.

+
    +
  1. Click on Text Box in the Add Component panel

  2. +
  3. Click inside the Card. Make sure the blue line is directly under the First name text box.

  4. +
  5. Change the name of the text box to text_box_last_name

  6. +
  7. Find the placeholder property and add Enter last name

  8. +
+

last name text box

+
+
+

Error Message

+

Since we are going to validate the user’s input, we need a way to explain any problems that might arrise. To do this we will add an error message label under the card. Our code will interact with this label, so we should name it appropriately.

+
    +
  1. Click Label in the Add Component panel

  2. +
  3. Click under the Card making sure the blue line is outside the card.

  4. +
  5. Change the name of the label to label_error

  6. +
  7. We are not adding text, since our code will dynamically set the text

  8. +
  9. Uncheck the visible property

  10. +
  11. Set foreground (text) colour to #ff0000 (red)

  12. +
  13. Set role to headline

  14. +
  15. Select fa:warning for the icon property

  16. +
  17. Finally set align to center

  18. +
+

error label

+
+
+

Save Button

+

To finish off the SetDetailsComponent layout we need to add a button.

+
    +
  1. Select Button from the Add Component panel

  2. +
  3. Place the button under the Error label

  4. +
  5. Change the name the button to button_save

  6. +
  7. Change the text to Save Details

  8. +
  9. Choose the role that suits your design

  10. +
  11. Set the icon to fa:save

  12. +
  13. Set align to right

  14. +
+

save button

+

That completes the layout of the SetDetailsComponent. Your layout should look like the one below.

+

layout

+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/15a_set_details_code.html b/15a_set_details_code.html new file mode 100644 index 0000000..8711228 --- /dev/null +++ b/15a_set_details_code.html @@ -0,0 +1,701 @@ + + + + + + + + + + SetDetailsComponent Code - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

SetDetailsComponent Code

+ +

Now we have the SetDetailsComponent layout complete, it is time to start working on the code. This code will:

+
    +
  • Check that both the First name and Last name text boxes contain text

  • +
  • Display an error message if either text box is empty

  • +
  • Prints a message to terminal displaying the info that would be sent to the User table (this will be built on in the next tutorial)

  • +
+
+

Save Button Code

+
+

Create Handler

+

The code for this component will reside within the Save button click handler. We start by:

+
    +
  1. clicking on the Save button

  2. +
  3. selecting … on click event

  4. +
  5. choosing the Code mode

  6. +
+

create handler

+
+
+

Check First name text box

+

First we will check that the First name text box is not blank. We will use a guard clause to achieve this. Basically it is an if statement that will use return to end the method if the the text box is empty. This will prevent the unnecessarily executing the rest of the method’s code.

+
+

Guard clauses

+

Guard clauses are used in programming to check conditions at the start of a function. If the condition is True, the function stops right there.

+

Guard clauses offer several benefits in programming:

+
    +
  • Improved Readability: They make the code easier to read by clearly showing conditions at the start of a function, allowing the main logic to follow without deep nesting.

  • +
  • Reduced Complexity: By removing unnecessary else blocks and nested if statements, guard clauses reduce code complexity, making it simpler and more straightforward.

  • +
  • Early Exit: They allow functions to exit early when conditions aren’t met, which can enhance performance and prevent errors later in the code.

  • +
  • Better Maintenance: With clearer code, maintenance becomes easier, as the logic is more apparent and debugging is simplified.

  • +
+
+

In the button_save_click hander, remove the comment and the pass statement, then add the highlighted code.

+
16  def button_save_click(self, **event_args):
+17    
+18    if self.text_box_first_name.text == "":
+19      self.label_error.text = "First name cannot be blank"
+20      self.label_error.visible = True
+21      return
+
+
+
+

Code explanation

+
    +
  • line 18:

    +
      +
    • self.text_box_first_name.text → get the current value of the text in text_box_first_name

      +
        +
      • note: placeholders aren’t considered text

      • +
      +
    • +
    • == "" check if that text value is an empty string (nothing)

    • +
    +
  • +
  • line 19:

    +
      +
    • self.label_error.text → change the text of the error message label to inform user that first name is missing.

    • +
    +
  • +
  • line 20:

    +
      +
    • self.label_error.visible = True → make the error message label visible

    • +
    +
  • +
  • line 21:

    +
      +
    • return → stop executing this method by returning controll to the main loop

    • +
    +
  • +
+
+
+
+

Check Last name text box

+

The obvious next step is to repeat this process for the Last name text box.

+

Try and work out the code yourself before looking at the code below.

+
16  def button_save_click(self, **event_args):
+17    
+18    if self.text_box_first_name.text == "":
+19      self.label_error.text = "First name cannot be blank"
+20      self.label_error.visible = True
+21      return
+22
+23    if self.text_box_last_name.text == "":
+24      self.label_error.text = "Last name cannot be blank"
+25      self.label_error.visible = True
+26      return
+
+
+
+
+

Save message

+

Once the user has passed both guard clauses, we want to save the details to the User table. We will write the code for this next tutorial, but in the meanwhile we need to test this code. So rather than writtng the results to the User table we will print them to the terminal. We also need to hide the error message.

+

Add the highlighted code to the button_save_click handler.

+
16  def button_save_click(self, **event_args):
+17    
+18    if self.text_box_first_name.text == "":
+19      self.label_error.text = "First name cannot be blank"
+20      self.label_error.visible = True
+21      return
+22
+23    if self.text_box_last_name.text == "":
+24      self.label_error.text = "Last name cannot be blank"
+25      self.label_error.visible = True
+26      return
+27
+28    self.label_error.visible = False
+29    print(self.text_box_first_name.text, self.text_box_last_name.text)
+
+
+
+

Code explanation

+
    +
  • line 28:

    +
      +
    • self.label_error.visible = False → hides the error message

    • +
    +
  • +
  • line 29:

    +
      +
    • self.text_box_first_name.text → current value of text_box_first_name

    • +
    • self.text_box_last_name.text → current value of text_box_last_name

    • +
    • print → display in the terminal

    • +
    +
  • +
+
+
+
+

Redirect Register

+

Currently, when a user finish registering, we load the HomeComponent, we will need to change that to loading the SetDetailsComponent.

+

Open the MainForm in code mode.

+

First we need to import the SetDetailsComponent. So in the import section, add the highlighted code.

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6import anvil.users
+ 7from ..HomeComponent import HomeComponent
+ 8from ..CalendarComponent import CalendarComponent
+ 9from ..AddComponent import AddComponent
+10from ..AccountComponent import AccountComponent
+11from ..SetDetailsComponent import SetDetailsComponent
+
+
+

Change the link_register_click handler by replacing line 72-74 with the highlighted code below

+
69  def link_register_click(self, **event_args):
+70    anvil.users.signup_with_form(allow_cancel=True)
+71    self.content_panel.clear()
+72    self.content_panel.add_component(SetDetailsComponent())
+73    self.label_title.text = self.breadcrumb_stem + " - Account - Details"
+74    self.set_active_link("details")
+
+
+
+
+
+

Testing

+

Before testing, we need to remove the last user you registered. So go to the Users table and delete all rows then launch your website:

+

Testing:

+
    +
  1. Register a new account

  2. +
  3. Leave the First name text box blank and click Save Details → should produce error

  4. +
  5. Leave the Last name text box blank and click Save Details → should produce error

  6. +
  7. Click save with both First name and Last name entered, the error message should disappear and their values should appear in the terminal

  8. +
+

test

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final SetDetailsComponent

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class SetDetailsComponent(SetDetailsComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16
+17  def button_save_click(self, **event_args):
+18    
+19    if self.text_box_first_name.text == "":
+20      self.label_error.text = "First name cannot be blank"
+21      self.label_error.visible = True
+22      return
+23
+24    if self.text_box_last_name.text == "":
+25      self.label_error.text = "Last name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    self.label_error.visible = False
+30    print(self.text_box_first_name.text, self.text_box_last_name.text)
+
+
+
+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6import anvil.users
+ 7from ..HomeComponent import HomeComponent
+ 8from ..CalendarComponent import CalendarComponent
+ 9from ..AddComponent import AddComponent
+10from ..AccountComponent import AccountComponent
+11from ..SetDetailsComponent import SetDetailsComponent
+12
+13
+14class MainForm(MainFormTemplate):
+15  def __init__(self, **properties):
+16    # Set Form properties and Data Bindings.
+17    self.init_components(**properties)
+18    self.breadcrumb_stem = self.label_title.text
+19
+20    # Any code you write here will run before the form opens.
+21    self.content_panel.add_component(HomeComponent())
+22    self.set_active_link("home")
+23
+24  def set_active_link(self, state):
+25    if state == "home":
+26      self.link_home.role = "selected"
+27    else:
+28      self.link_home.role = None
+29    if state == "add":
+30      self.link_add.role = "selected"
+31    else:
+32      self.link_add.role = None
+33    if state == "calendar":
+34      self.link_calendar.role = "selected"
+35    else:
+36      self.link_calendar.role = None
+37
+38    self.link_register.visible = not anvil.users.get_user()
+39    self.link_login.visible = not anvil.users.get_user()
+40    self.link_account.visible = anvil.users.get_user()
+41    self.link_logout.visible = anvil.users.get_user()
+42  
+43  # --- link handlers
+44  def link_home_click(self, **event_args):
+45    self.content_panel.clear()
+46    self.content_panel.add_component(HomeComponent())
+47    self.label_title.text = self.breadcrumb_stem
+48    self.set_active_link("home")
+49
+50  def link_calendar_click(self, **event_args):
+51    self.content_panel.clear()
+52    self.content_panel.add_component(CalendarComponent())
+53    self.label_title.text = self.breadcrumb_stem + " - Calendar"
+54    self.set_active_link("calendar")
+55
+56  def link_add_click(self, **event_args):
+57    self.content_panel.clear()
+58    self.content_panel.add_component(AddComponent())
+59    self.label_title.text = self.breadcrumb_stem + " - Add"
+60    self.set_active_link("add")
+61
+62  def link_account_click(self, **event_args):
+63    """This method is called when the link is clicked"""
+64    self.content_panel.clear()
+65    self.content_panel.add_component(AccountComponent())
+66    self.label_title.text = self.breadcrumb_stem + " - Account"
+67    self.set_active_link(("account"))
+68
+69  def link_register_click(self, **event_args):
+70    anvil.users.signup_with_form(allow_cancel=True)
+71    self.content_panel.clear()
+72    self.content_panel.add_component(SetDetailsComponent())
+73    self.label_title.text = self.breadcrumb_stem + " - Account - Details"
+74    self.set_active_link("details")
+75
+76  def link_login_click(self, **event_args):
+77    anvil.users.login_with_form(allow_cancel=True)
+78    self.content_panel.clear()
+79    self.content_panel.add_component(HomeComponent())
+80    self.label_title.text = self.breadcrumb_stem
+81    self.set_active_link("home")
+82
+83  def link_logout_click(self, **event_args):
+84    anvil.users.logout()
+85    self.content_panel.clear()
+86    self.content_panel.add_component(HomeComponent())
+87    self.label_title.text = self.breadcrumb_stem
+88    self.set_active_link("home")
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/16_user_service.html b/16_user_service.html new file mode 100644 index 0000000..522e499 --- /dev/null +++ b/16_user_service.html @@ -0,0 +1,596 @@ + + + + + + + + + + User Server Module - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

User Server Module

+ +
+

Backend databases

+

The User table that we have been dealing with is actually inside a database. At the moment the database has only one table, but eventually we will add extra tables to it. There are four basic operations that databases need to do - create, read, update and delete, referred to as CRUD. We have already been using a read operation, the built in get_user().

+
+

CRUD

+

CRUD stands for Create, Read, Update, and Delete. These are the four basic operations that are fundamental to database management:

+
    +
  • Create: These operations add new records to a database.

  • +
  • Read: These operations retrieve data from the database.

  • +
  • Update: These operations modify existing records in the database.

  • +
  • Delete: These operations remove records from the database.

  • +
+
+

All database operations occur server-side (backend). Server-side code provides methods that can be called by the client-side (frontend). When the client-side calls the methods, the server executes the server-side code and then sends the results back to the client-side.

+

frontend backend

+

To provide additional, custom methods we will need to create our first server-side code. We will call this module user-service as it will provide CRUD operations to user data.

+
+
+

Code

+

To start we will need to create a Server Module.

+
    +
  1. Go to the file submenu

  2. +
  3. Click on Add Server Module

  4. +
  5. Then name the new server module user_service

  6. +
+

add server module

+
+

Making a callable method

+

If you look at the comment in the user_service it tells you how to create callable function for your server code. Making a function callable allows it to be used in the client-side code.

+

To make a function callable, you need to add @anvil.server.callable above it. This is called a decorator. Decorators are a bit beyond the scope of this course, and this is the only place that you will use them, so for our purposes, just use it as is.

+
+

Decorators

+

Python decorators can change how other functions work. When you use a decorator, you attach it to another function to change its behavior. This lets you add extra features to your function without changing its main code.

+

More information can be found at Free Code Camps - Python Decorators Explained For Beginners

+
+
+
+

Create the update user function

+

So to create a function for our website to use we must:

+
    +
  • create a function that accepts a first name and last name

  • +
  • get’s the current user

  • +
  • updates the current user’s first name and last name with the values provided

  • +
  • then add the @anvil.server.callable decorator above the function

  • +
+

Remove all the comments within user_services, then add the code below:

+
 7@anvil.server.callable
+ 8def update_user(first_name, last_name):
+ 9  user = anvil.users.get_user()
+10
+11  user["first_name"] = first_name
+12  user["last_name"] = last_name
+
+
+
+

Code explanation

+
    +
  • line 7:

    +
      +
    • @anvil.server.callable → the decorator that allows the client-side code to call this function

    • +
    +
  • +
  • line 8:

    +
      +
    • def update_user → names the functionupdate_user

    • +
    • (first_name, last_name) → requires the values first_name and last_name to be passed when called

    • +
    +
  • +
  • line 9:

    +
      +
    • anvil.users.get_user() → get the details of the current user. Note these details are stored in a dictionary

    • +
    • user = → stores the dictionary of the current user details in a variable called user

    • +
    +
  • +
  • line 11:

    +
      +
    • user["first_name"] = first_name → change the first_name value of the user to the first_name passed to the function.

    • +
    +
  • +
  • line 12:

    +
      +
    • user["last_name"] = last_name → change the last_name value of the user to the last_name passed to the function.

    • +
    +
  • +
+
+
+
+

Call the update user function

+

Now that we have the a server-side function we need to call it, so return to the code of SetDetailsComponent.

+

Replace line 30 with the highlighted text.

+
17  def button_save_click(self, **event_args):
+18    
+19    if self.text_box_first_name.text == "":
+20      self.label_error.text = "First name cannot be blank"
+21      self.label_error.visible = True
+22      return
+23
+24    if self.text_box_last_name.text == "":
+25      self.label_error.text = "Last name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    self.label_error.visible = False
+30    anvil.server.call("update_user", 
+31                    self.text_box_first_name.text, 
+32                    self.text_box_last_name.text)
+
+
+
+

Code explanation

+

Note: lines 30 - 32 are one statement, it has just been spread over three lines to make it easier to read (you can split arguements after a ,).

+
    +
  • line 30:

    +
      +
    • anvil.server.call → this is how you call functions that have the @anvil.server.callable decorator

    • +
    • "update_user" → when using anvil.server.call the first argument passed is the name of the function you want to call.

    • +
    • self.text_box_first_name.text → the text of the first name text box that will passed as first_name to the update_user function.

    • +
    • self.text_box_last_name.text → the text of the last name text box that will passed as last_name to the update_user function.

    • +
    +
  • +
+
+
+
+
+

Testing

+

Time to test if this all works.

+
    +
  1. Go to the User table and delete your users

  2. +
  3. Launch your website

  4. +
  5. Register a new user

  6. +
  7. Enter a first name and last name

  8. +
  9. Click on Save Details

  10. +
  11. Stop your website

  12. +
  13. Go to the User table and check that your new user has the correct first_name and last_name.

  14. +
+

test

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final SetDetailsForm

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class SetDetailsComponent(SetDetailsComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16
+17  def button_save_click(self, **event_args):
+18    
+19    if self.text_box_first_name.text == "":
+20      self.label_error.text = "First name cannot be blank"
+21      self.label_error.visible = True
+22      return
+23
+24    if self.text_box_last_name.text == "":
+25      self.label_error.text = "Last name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    self.label_error.visible = False
+30    anvil.server.call("update_user", 
+31                      self.text_box_first_name.text, 
+32                      self.text_box_last_name.text)
+
+
+
+
+

Final user_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6
+ 7@anvil.server.callable
+ 8def update_user(first_name, last_name):
+ 9  user = anvil.users.get_user()
+10
+11  user["first_name"] = first_name
+12  user["last_name"] = last_name
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/17_set_details_nav.html b/17_set_details_nav.html new file mode 100644 index 0000000..e74b776 --- /dev/null +++ b/17_set_details_nav.html @@ -0,0 +1,628 @@ + + + + + + + + + + SetDetailsComponent Navigation - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

SetDetailsComponent Navigation

+ +

In last tutorial’s test you would notice that once you have saved the details, the SetDetailsComponent remained loaded. What we want to do is load the AccountComponent so the user can see the current value of their first name and last name.

+

Admittedly, at the moment, the AccountComponent only has a title, but we’ll fix that in the next tutorial.

+
+

Plan

+

The idea is pretty simple, we will need to add the step of loading the AccountComponent and change the lable_title_text to the end of the button_save_click handler. We can copy the appropriate code from link_account_click handler in the MainForm.

+
+
+

Code

+
+

Copying the code

+

First we will copy the text we need.

+
    +
  1. Open the MainForm in code mode and find the link_account_click handler.

  2. +
  3. Copy the highlighted code

  4. +
+
63  def link_account_click(self, **event_args):
+64    """This method is called when the link is clicked"""
+65    self.content_panel.clear()
+66    self.content_panel.add_component(AccountComponent())
+67    self.label_title.text = self.breadcrumb_stem + " - Account"
+68    self.set_active_link(("account"))
+
+
+

Now we need to paste it into the button_save_click handler.

+
    +
  1. Open SetDetailsComponent in code mode and find the button_save_click handler

  2. +
  3. Paste the code to the bottom of the handler, as below

  4. +
+
17  def button_save_click(self, **event_args):
+18    
+19    if self.text_box_first_name.text == "":
+20      self.label_error.text = "First name cannot be blank"
+21      self.label_error.visible = True
+22      return
+23
+24    if self.text_box_last_name.text == "":
+25      self.label_error.text = "Last name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    self.label_error.visible = False
+30    anvil.server.call("update_user", 
+31                      self.text_box_first_name.text, 
+32                      self.text_box_last_name.text)
+33
+34    self.content_panel.clear()
+35    self.content_panel.add_component(AccountComponent())
+36    self.label_title.text = self.breadcrumb_stem + " - Account"
+37    self.set_active_link(("account"))
+
+
+
+
+

Testing

+

Now launch your website for testing:

+
    +
  1. Register another user (you might have to logout first).

  2. +
  3. Enter their first name and last name

  4. +
  5. Click Save Details

  6. +
  7. WHAT JUST HAPPENED?

  8. +
+

You would have just got the following runtime error:

+

content panel error

+
+

Types of Programming Errors

+

Programming errors fall into three categories

+
    +
  1. Syntax Errors

    +
      +
    • Occur when the code doesn’t follow the rules of the programming language.

    • +
    • Detected by the compiler or interpreter before running the code.

    • +
    • Example: Missing a closing parenthesis.

    • +
    +
  2. +
  3. Runtime Errors

    +
      +
    • Occur during the execution of the program.

    • +
    • Cause the program to crash or produce unexpected results.

    • +
    • Example: Dividing by zero.

    • +
    +
  4. +
  5. Logic Errors

    +
      +
    • Occur when the code runs but produces incorrect results.

    • +
    • Due to flaws in the logic of the code.

    • +
    • Example: Incorrect formula in a calculation.

    • +
    +
  6. +
+
+
+

AttributeError analysis

+

Well if says the error occurred at line 34 so lets start there:

+

self.content_panel.clear() → clear content_panel in self (this component).

+

You can see that the self is the problem. It refers to the component the code resides in (in this case the SetDetailsComponent), when we actually want to refer to MainForm where content_panel resides.

+

So we need a way to refer to the MainForm from with in the SetDetailsComponent code. Luckily Anvil provide a command to do this → anvil.get_open_form()

+
+
+

Using the open form

+

The Anvil command anvil.get_open_form() returns the current open form, in our case it will be MainForm. This means we can get the MainForm, store it in a variable (main_form). This will enable fixing the code we just pasted by replacing self with main_form.

+

Make the changes highlighted below.

+
17  def button_save_click(self, **event_args):
+18    
+19    if self.text_box_first_name.text == "":
+20      self.label_error.text = "First name cannot be blank"
+21      self.label_error.visible = True
+22      return
+23
+24    if self.text_box_last_name.text == "":
+25      self.label_error.text = "Last name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    self.label_error.visible = False
+30    anvil.server.call("update_user", 
+31                      self.text_box_first_name.text, 
+32                      self.text_box_last_name.text)
+33
+34    main_form = get_open_form()
+35    main_form.content_panel.clear()
+36    main_form.content_panel.add_component(AccountComponent())
+37    main_form.label_title.text = main_form.breadcrumb_stem + " - Account"
+38    main_form.set_active_link(("account"))
+
+
+
+
+
+

Test again

+

Lets test again. The erroneous code came after writing the details to the User table, so you will need to delete those details to test again. then launch your website.

+

Now launch your website for testing:

+
    +
  1. Register another user

  2. +
  3. Enter their first name and last name

  4. +
  5. Click Save Details

  6. +
  7. WHAT AGAIN???

  8. +
+

We still have an error. But look at the error closely, it’s a different, so that’s progress.

+

account component error

+
+

NameError analysis

+

This error points to line 36.

+

main_form.content_panel.add_component(AccountComponent()) → in the content_panel of the MainForm load the AccountComponent

+

It’s complaining because there is no AccountComponent in the SetDetailsComponent. We resolved this issue in the MainForm by importing the AccountComponent using the following line:

+

from ..AccountComponent import AccountComponent

+

So we need to add this to the import statement at the top of SetDetailsComponent.

+
+
+

Importing AccountComponent

+

Open the SetDetailsComponent in code mode an then add the highlighted code below:

+
1from ._anvil_designer import SetDetailsComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from ..AccountComponent import AccountComponent
+
+
+
+
+
+

Third test lucky

+

Again, delete the user you just added and launch your website.

+
    +
  1. Register another user

  2. +
  3. Enter their first name and last name

  4. +
  5. Click Save Details

  6. +
  7. You should now be on the AccountComponent, stop your website and check that the user details have been saved to the User table

  8. +
+

Now that is all working, we have to add details to the AccountComponent.

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final SetDetailsComponent

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..AccountComponent import AccountComponent
+ 9
+10
+11class SetDetailsComponent(SetDetailsComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17
+18  def button_save_click(self, **event_args):
+19    
+20    if self.text_box_first_name.text == "":
+21      self.label_error.text = "First name cannot be blank"
+22      self.label_error.visible = True
+23      return
+24
+25    if self.text_box_last_name.text == "":
+26      self.label_error.text = "Last name cannot be blank"
+27      self.label_error.visible = True
+28      return
+29
+30    self.label_error.visible = False
+31    anvil.server.call("update_user", 
+32                      self.text_box_first_name.text, 
+33                      self.text_box_last_name.text)
+34
+35    main_form = get_open_form()
+36    main_form.content_panel.clear()
+37    main_form.content_panel.add_component(AccountComponent())
+38    main_form.label_title.text = main_form.breadcrumb_stem + " - Account"
+39    main_form.set_active_link(("account"))
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/18_account_cmpt.html b/18_account_cmpt.html new file mode 100644 index 0000000..4759892 --- /dev/null +++ b/18_account_cmpt.html @@ -0,0 +1,447 @@ + + + + + + + + + + AccountComponent Layout - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

AccountComponent Layout

+ +

Last tutorial we noted that we need to populate the AccountComponent. We will do this over two tutorials. This one will focus on creating the layout, while the next tutorial will address the code.

+
+

Design

+

If we check back at our wireframes we will see that the AccountComponent displays the first name and last name as well as an edit button. The layout is very similar to the SetDetailComponent, so this should seem very familiar.

+

account component

+

Unlike the SetDetailsComponent there are no text boxes, with labels being used to display the first name and last name values for the current user. We will still need to use a Column Panel and the Button will be used to load the SetDetailsComponent so users can change their details.

+
+
+

The layout

+

Open the AccountComponent in Design mode. Most of this is revisiting the layout for the SetDesignComponent so I will not be going into as much detail.

+
+

Organisational elements

+

First we must add a Card element to the layout.

+

Card

+

Then we need to add a Column Panel inside the Card

+

Column panel

+
+
+

First name elements

+

Now we need to add the first name label.

+
    +
  • This is the one that says First Name which the code will not interact with, so no need to change its name.

  • +
  • We will still set the role to input-prompt so we can keep the text formatting consistent between the AccountComponent and the SetDetailsComponent.

  • +
+

first name label

+

To the right of that label e need to add another label that will display the user’s data.

+
    +
  • We will need to name this label calling it label_first_name

  • +
  • We’ll also set the default text to "User first name"

  • +
  • The role will be input-prompt

  • +
  • Finally alignment should be right

  • +
+

label_first_name

+
+
+

Last name elements

+

Under the first name elements we need to add the exact same elements, only for the last name.

+

last name elements

+
+
+

Edit button

+

Finally we need to add an edit button:

+
    +
  • positioned under the Card

  • +
  • namebutton_edit

  • +
  • textEdit Details

  • +
  • role → same as you chose for the Save Details button on the SetDetailsComponent

  • +
  • iconfa:edit

  • +
  • alignright

  • +
+

edit button

+

Now that we have all the elements on the layout, it’s time to move to the next tutorial to start coding.

+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/18a_account_code.html b/18a_account_code.html new file mode 100644 index 0000000..215e18a --- /dev/null +++ b/18a_account_code.html @@ -0,0 +1,591 @@ + + + + + + + + + + AccountComponent Code - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

AccountComponent Code

+ +
+

Planning

+

Now that we have the AccountComponent layout completed we need to write the code that:

+
    +
  1. Retrieves and displays the user’s details

  2. +
  3. Connects the Edit Details button to the SetDetailsComponent

  4. +
+

We know that we can use anvil.users.get_user() to retrieve user details and we also know that we can access these details like a dictionary.

+

We also have create the code to load the SetDetailsComponent, so we just need to copy that.

+
+
+

Code

+
+

Display users details

+

To display the current user’s details:

+
    +
  1. Open the AccountComponent in code mode

  2. +
  3. We want to load the first and last name before the form opens, so go to the __init__ method.

  4. +
  5. Add the highlighted code below

  6. +
+
11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    user = anvil.users.get_user()
+17    self.label_first_name.text = user["first_name"]
+18    self.label_last_name.text = user["last_name"]
+
+
+

This should occur every time that the AccountComponent is loaded.

+
+

Test user details display

+

Let’s test this first stage. Launch your website.

+
    +
  1. Login if you have to

  2. +
  3. Click on the Account link

  4. +
  5. Check that the names have been populated

  6. +
+

testing account details

+
+
+ +
+
+

Solving our circular reference

+
+

What is a circular reference

+

Our circular refence is caused by our imports.

+

The SetDetailsComponent import statements are as below:

+
1from ._anvil_designer import SetDetailsComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from ..AccountComponent import AccountComponent
+
+
+

While our AccountComponent import statements are like this:

+
1from ._anvil_designer import AccountComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from ..SetDetailsComponent import SetDetailsComponent
+
+
+

If you look at the highlighted lines your will notice that:

+
    +
  • the AccountsComponent imports the SetDetailsComponent

  • +
  • the SetDetailsComponent imports the AccountsComponent

  • +
+

So when you website launches and it reads all the code, the AccountsComponent wants to load all the code from the SetDetailsComponent which wants to load all the code from the AccountsComponent which wants to load all the code from the SetDetailsComponent which wants to load all the code from the AccountsComponent which wants to load all the code from the SetDetailsComponent which wants to load all the code from the… you get the idea.

+

Two items of code are referring to each other, hence the term circular reference.

+
+
+

Why did it occur

+

This circular reference was caused because we have been very messy with out code, in particular our navigating code. You might have already noticed that we haven’t been too stringent in applying the DRY principle, for example:

+
    +
  • we have navigation code in the MainForm, the SetDetailsComponent and the AccountComponent

  • +
  • we also have repetition of similar code in each of the link click handlers in the MainCode

  • +
+
+
+

How to resolve it

+

We need to refactor our code and bring all the navigation code into one place. This we will do in the next tutorial.

+
+

Refactoring code

+

Refactoring code means improving the structure and readability of your code without changing what it does. It’s like cleaning up your room: you reorganize everything to make it easier to find and use, but you don’t throw anything away or change how your room works.

+

By refactoring, you make your code clearer, easier to understand, and simpler to maintain, which helps you and others work with it more effectively in the future.

+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/19_switch_components.html b/19_switch_components.html new file mode 100644 index 0000000..52150be --- /dev/null +++ b/19_switch_components.html @@ -0,0 +1,927 @@ + + + + + + + + + + Switch Component Method - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Switch Component Method

+ +

Last tutorial we encountered a problem that was caused by having our navigation code distributed throughout our codebase. This tutorial we will refactor the code so all this navigation code resides in the MainForm.

+
+

Preparing to refactor

+

When refactoring, it is important to ensure that your refactored code works just the same as the old code. This presents a problem for us, in that our current code does not work. So we need to adjust the code so that it at least runs without causing an error.

+

To do this we will simply remove the import that causes the error. Open your AccountComponent in code mode and delete line 8

+
1from ._anvil_designer import AccountComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from ..SetDetailsComponent import SetDetailsComponent
+
+
+
+

Code explanation

+

It is important to note that clicking the Edit Details button will still cause an error. We will worry about fixing this problem after we have refactored the code.

+
+
+
+

Planning

+

What we will do, is consolidate all the navigation code into one MainForm method called switch_component. Let’s look at the MainForm current navigation code to establish what this switch_component method needs to do.

+

Below is the navigation code in the link_home_click handler, what can we identify?

+
45  def link_home_click(self, **event_args):
+46    self.content_panel.clear()
+47    self.content_panel.add_component(HomeComponent())
+48    self.label_title.text = self.breadcrumb_stem
+49    self.set_active_link("home")
+
+
+

There are four highlighted lines indicate four steps to the navigation process:

+
    +
  1. line 46 → removes the current component

  2. +
  3. line 47 → adds the desired component

  4. +
  5. line 48 → sets the correct breadcrumb title

  6. +
  7. line 49 → set the correct active link

  8. +
+

We need to include all four of these processes into our new switch_component method.

+

Remember in the set_active_link method, we passed a string to set the state of the MainForm (check below).

+
24  def set_active_link(self, state):
+25    if state == "home":
+26      self.link_home.role = "selected"
+27    else:
+28      self.link_home.role = None
+29    if state == "add":
+30      self.link_add.role = "selected"
+31    else:
+32      self.link_add.role = None
+33    if state == "calendar":
+34      self.link_calendar.role = "selected"
+35    else:
+36      self.link_calendar.role = None
+
+
+

We can use this same concept to send a state to the switch_component (eg. "home") and the switch_component method will respond to that state and load the HomeComponent

+

Ok, now put that into practice.

+
+
+

Code

+
+

Create the switch_component

+ +
+

Make code generic

+

At the moment, the code just loads the HomeComponent. We want to change it so that it response to the state that is passed to switch_component.

+

Let’s consider each line of code:

+
25  def switch_component(self, state):
+26    # execution
+27    self.content_panel.clear()
+28    self.content_panel.add_component(HomeComponent())
+29    self.label_title.text = self.breadcrumb_stem
+30    self.set_active_link("home")
+
+
+
    +
  • line 27 → it is generic, so there is no need to change it

  • +
  • line 28 → we want to change the component loaded, so lets change HomeComponent() to a variable called cmpt. This variable can be set to different components

  • +
  • line 29 → similarly, we want to change the bread_crumb according to the state, so let’s replace self.breadcrumb_stem with a variable called breadcrumb

  • +
  • line 30 → we want to call set_active_link with the state that is passed, so replace home with state.

  • +
+

With these changes your code should look like:

+
25  def switch_component(self, state):
+26    # execution
+27    self.content_panel.clear()
+28    self.content_panel.add_component(cmpt)
+29    self.label_title.text = breadcrumb
+30    self.set_active_link(state)
+
+
+
+
+

Add response to state

+

Now we just need to use state to establish the values of cmpt and breadcrumb.

+

We will use an if ... elif statement to establish these values.

+

There are five possible states for our app:

+
    +
  • "home"cmpt = HomeComponent() and breadcrumb = self.breadcrumb_stem

  • +
  • "add"cmpt = AddComponent() and breadcrumb = self.breadcrumb_stem + " - Account"

  • +
  • "account"cmpt = Account() and breadcrumb = self.breadcrumb_stem + " - Add"

  • +
  • "calendar” → cmpt = CalendarComponent() and breadcrumb = self.breadcrumb_stem + " - Calendar"

  • +
  • "details"cmpt = SetDetailsComponent() and breadcrumb = self.breadcrumb_stem + " - Account - Set Details"

  • +
+

So let’s put this into an if ... elif statement in our code

+
25  def switch_component(self, state):
+26    # set state
+27    if state == "home":
+28      cmpt = HomeComponent()
+29      breadcrumb = self.breadcrumb_stem
+30    elif state == "account":
+31      cmpt = AccountComponent()
+32      breadcrumb = self.breadcrumb_stem + " - Account"
+33    elif state == "add":
+34      cmpt = AddComponent()
+35      breadcrumb = self.breadcrumb_stem + " - Add"
+36    elif state == "calendar":
+37      cmpt = CalendarComponent()
+38      breadcrumb = self.breadcrumb_stem + " - Calendar"
+39    elif state == "details":
+40      cmpt = SetDetailsComponent()
+41      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
+42    
+43    # execution
+44    self.content_panel.clear()
+45    self.content_panel.add_component(cmpt)
+46    self.label_title.text = breadcrumb
+47    self.set_active_link(state)
+
+
+
+
+
+

Replace old navigation

+

Now that we have create the switch_component method, we need to go through our codebase and replace all the navigation code with calls the the switch_component method.

+

The best way to find the navigation code is to do a search for the add_component method.

+
    +
  1. click on the search icon

  2. +
  3. type add_component into the search box

  4. +
+

search

+
+

MainForm

+

We can see that the MainForm has the majority of the navigation, so lets start there.

+
+
__init__
+

In the __init__ we have our first code to change, lines 22 and 23.

+
16  def __init__(self, **properties):
+17    # Set Form properties and Data Bindings.
+18    self.init_components(**properties)
+19    self.breadcrumb_stem = self.label_title.text
+20
+21    # Any code you write here will run before the form opens.
+22    self.content_panel.add_component(HomeComponent())
+23    self.set_active_link("home")
+
+
+

They need to be replaced with a single call to switch_component passing the "home" state.

+
16  def __init__(self, **properties):
+17    # Set Form properties and Data Bindings.
+18    self.init_components(**properties)
+19    self.breadcrumb_stem = self.label_title.text
+20
+21    # Any code you write here will run before the form opens.
+22    self.switch_component("home")
+
+
+
+ + + +
+
+

Check search results

+

After all those changes in MainForm, the only use of add_component should be in the switch_components method. This can be easily seen in the search box.

+

search 2

+
+
+

SetDetailsComponent

+

Next we will change the SetDetailsComponent, so open it in code mode.

+

Below is the old navigation code. Remember line 35 provides access to all methods in the MainForm. switch_component is on the MainForm, so we need to keep this line so we can access the method.

+
35    main_form = get_open_form()
+36    main_form.content_panel.clear()
+37    main_form.content_panel.add_component(AccountComponent())
+38    main_form.label_title.text = main_form.breadcrumb_stem + " - Account"
+39    main_form.set_active_link(("account"))
+
+
+

Remove the remaining navigation code, and replace it with a call to main_form.switch_component,

+
35    main_form = get_open_form()
+36    main_form.switch_component("account")
+
+
+

Since we are no longer referencing AccountComponent() we can delete its import from the import section.

+
1from ._anvil_designer import SetDetailsComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from ..AccountComponent import AccountComponent
+
+
+

We should now only have two calls the add_component in our search box.

+

search 3

+
+
+

AccountComponent

+

Finally we will replace the code that started us down this refactoring path.

+

Open the AccountComponent in code mode.

+

Delete the highlighted lines from the button_edit_click handler

+
20  def button_edit_click(self, **event_args):
+21    main_form = get_open_form()
+22    main_form.content_panel.clear()
+23    main_form.content_panel.add_component(SetDetailsComponent())
+24    main_form.label_title.text = main_form.breadcrumb_stem + " - Account - Details"
+25    main_form.set_active_link("details")
+
+
+

Then add your call to main_form.switch_component:

+
20  def button_edit_click(self, **event_args):
+21    main_form = get_open_form()
+22    main_form.switch_component("details")
+
+
+

This should leave you with only one reference to add_component in your search results.

+

search 4

+
+
+
+
+

Testing

+

Time to test our refactoring. This has been a significant change to our codebase so we have to be thorough in our testing.

+

Launch your website then:

+
    +
  1. Logout if you have to

  2. +
  3. Register a new user

  4. +
  5. Add user’s name details and then Save Details

  6. +
  7. Click Edit Details

  8. +
  9. Re-enter the user name details and click Save Details

  10. +
  11. Click Home

  12. +
  13. Click Calendar

  14. +
  15. Click Account

  16. +
  17. Click Logout

  18. +
  19. Click Login and enter user credentials

  20. +
+

If you have refactored correctly, you should get through all of these steps without error.

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final SetDetailsComponent

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class SetDetailsComponent(SetDetailsComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16
+17  def button_save_click(self, **event_args):
+18    
+19    if self.text_box_first_name.text == "":
+20      self.label_error.text = "First name cannot be blank"
+21      self.label_error.visible = True
+22      return
+23
+24    if self.text_box_last_name.text == "":
+25      self.label_error.text = "Last name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    self.label_error.visible = False
+30    anvil.server.call("update_user", 
+31                      self.text_box_first_name.text, 
+32                      self.text_box_last_name.text)
+33
+34    main_form = get_open_form()
+35    main_form.switch_component("account")
+
+
+
+
+

Final AccountComponent

+
 1from ._anvil_designer import AccountComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class AccountComponent(AccountComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    user = anvil.users.get_user()
+17    self.label_first_name.text = user["first_name"]
+18    self.label_last_name.text = user["last_name"]
+19
+20  def button_edit_click(self, **event_args):
+21    main_form = get_open_form()
+22    main_form.switch_component("details")
+
+
+
+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13
+14
+15class MainForm(MainFormTemplate):
+16  def __init__(self, **properties):
+17    # Set Form properties and Data Bindings.
+18    self.init_components(**properties)
+19    self.breadcrumb_stem = self.label_title.text
+20
+21    # Any code you write here will run before the form opens.
+22    self.switch_component("home")
+23
+24  def switch_component(self, state):
+25    # set state
+26    if state == "home":
+27      cmpt = HomeComponent()
+28      breadcrumb = self.breadcrumb_stem
+29    elif state == "account":
+30      cmpt = AccountComponent()
+31      breadcrumb = self.breadcrumb_stem + " - Account"
+32    elif state == "add":
+33      cmpt = AddComponent()
+34      breadcrumb = self.breadcrumb_stem + " - Add"
+35    elif state == "calendar":
+36      cmpt = CalendarComponent()
+37      breadcrumb = self.breadcrumb_stem + " - Calendar"
+38    elif state == "details":
+39      cmpt = SetDetailsComponent()
+40      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
+41    
+42    # execution
+43    self.content_panel.clear()
+44    self.content_panel.add_component(cmpt)
+45    self.label_title.text = breadcrumb
+46    self.set_active_link(state)
+47  
+48  def set_active_link(self, state):
+49    if state == "home":
+50      self.link_home.role = "selected"
+51    else:
+52      self.link_home.role = None
+53    if state == "add":
+54      self.link_add.role = "selected"
+55    else:
+56      self.link_add.role = None
+57    if state == "calendar":
+58      self.link_calendar.role = "selected"
+59    else:
+60      self.link_calendar.role = None
+61
+62    self.link_register.visible = not anvil.users.get_user()
+63    self.link_login.visible = not anvil.users.get_user()
+64    self.link_account.visible = anvil.users.get_user()
+65    self.link_logout.visible = anvil.users.get_user()
+66  
+67  # --- link handlers
+68  def link_home_click(self, **event_args):
+69    self.switch_component("home")
+70
+71  def link_calendar_click(self, **event_args):
+72    self.switch_component("calendar")
+73
+74  def link_add_click(self, **event_args):
+75    self.switch_component("add")
+76
+77  def link_account_click(self, **event_args):
+78    """This method is called when the link is clicked"""
+79    self.switch_component("account")
+80
+81  def link_register_click(self, **event_args):
+82    anvil.users.signup_with_form(allow_cancel=True)
+83    self.switch_component("details")
+84
+85  def link_login_click(self, **event_args):
+86    anvil.users.login_with_form(allow_cancel=True)
+87    self.switch_component("home")
+88
+89  def link_logout_click(self, **event_args):
+90    anvil.users.logout()
+91    self.switch_component("home")
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/20_complete_account_cmpt.html b/20_complete_account_cmpt.html new file mode 100644 index 0000000..2261166 --- /dev/null +++ b/20_complete_account_cmpt.html @@ -0,0 +1,517 @@ + + + + + + + + + + Complete Account Component - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Complete Account Component

+ +

Now that we have added the switch_component method, we have almost finished the AccountComponent code. If you recall your test, there was one annoying problem. When the SetDetailsComponent loaded, the First name and Last name text boxes were empty. Now this is fine when you are registering new users, but it is annoying when coming from the AccountComponent.

+

empty

+

This tutorial will fix that problem.

+
+

Planning

+

What were are planning to do is quite simple. If the user has name details, show these in the First name and Last name text boxes. This is going to be executed in the SetDetailsComponent code before the component is loaded.

+
+
+

Code

+

Open the SetDetailsComponent in code mode, then add the highlighted code to __init__:

+
11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    user = anvil.users.get_user()
+17    if user["first_name"]:
+18      self.text_box_first_name.text = user["first_name"]
+19    if user["last_name"]:
+20      self.text_box_last_name.text = user["last_name"]
+
+
+
+

Code explanation

+
    +
  • line 16:

    +
      +
    • user = anvil.users.get_user() → retrieves the current user’s row from the User table.

    • +
    +
  • +
  • line 17:

    +
      +
    • if user["first_name"]: → checks the truthiness of the current value of "first_name"

      +
        +
      • if there is a name, then user["first_name"] is a string and therefore True

      • +
      • if there is no name, then user["first_name"] is None and therefore False

      • +
      +
    • +
    +
  • +
  • line 18:

    +
      +
    • self.text_box_first_name.text = user["first_name"] → sets the text of the text_box_first_name to the first name from the Users table

    • +
    +
  • +
    • +
    • line 17:

    • +
    • if user["last_name"]: → checks the truthiness of the current value of "last_name"

      +
        +
      • if there is a name, then user["last_name"] is a string and therefore True

      • +
      • if there is no name, then user["last_name"] is None and therefore False

      • +
      +
    • +
    +
  • +
  • line 18:

    +
      +
    • self.text_box_first_name.text = user["last_name"] → sets the text of the text_box_last_name to the last name from the Users table

    • +
    +
  • +
+
+
+
+

Testing

+

Time to test the code.

+
    +
  1. Launch your website and navigate to the Account page

  2. +
  3. Click on Edit Detail

  4. +
  5. Check that both the first name and last name are displayed on the Set Details page

  6. +
  7. Change one of the names

  8. +
  9. Click on Save Details

  10. +
  11. Check that the changed name is correctly displayed on the Account page

  12. +
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final SetDetailsComponent

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class SetDetailsComponent(SetDetailsComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    user = anvil.users.get_user()
+17    if user["first_name"]:
+18      self.text_box_first_name.text = user["first_name"]
+19    if user["last_name"]:
+20      self.text_box_last_name.text = user["last_name"]
+21
+22  def button_save_click(self, **event_args):
+23    
+24    if self.text_box_first_name.text == "":
+25      self.label_error.text = "First name cannot be blank"
+26      self.label_error.visible = True
+27      return
+28
+29    if self.text_box_last_name.text == "":
+30      self.label_error.text = "Last name cannot be blank"
+31      self.label_error.visible = True
+32      return
+33
+34    self.label_error.visible = False
+35    anvil.server.call("update_user", 
+36                      self.text_box_first_name.text, 
+37                      self.text_box_last_name.text)
+38
+39    main_form = get_open_form()
+40    main_form.content_panel.clear()
+41    main_form.switch_component("account")
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/21_assessment_table.html b/21_assessment_table.html new file mode 100644 index 0000000..7819735 --- /dev/null +++ b/21_assessment_table.html @@ -0,0 +1,455 @@ + + + + + + + + + + The Assessments Table - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

The Assessments Table

+ +

Now that we have the user all sorted, it is time to start working on the assessments aspect of the website. To do this we will need to store all the assessment details in a table. This information doesn’t belong in the user table, so we will need to store it in a new table.

+
+

Create new table

+

First we will need to create a new table:

+
    +
  1. Go to the data menu

  2. +
  3. Click on + Add New Table in the submenu

  4. +
  5. Name the table Assessments

  6. +
+

add table

+
+
+

Add columns

+
+

User column

+

Now that the table is created we need to add columns.

+

The first column is very important, as it will link the assessment item with the correct user. Click on + New Column then:

+
    +
  1. Type user as the column name

  2. +
  3. Choose Single row from another table from the Column Type dropdown

  4. +
  5. Choose Users from the Linked Table dropdown

  6. +
+
+

Importance of the user column

+

This web app can accomodate multiple users. Each user will be able to enter their own assessment details. Therefore, when the assessment details are saved, is it important to identify which user each assessment item belongs to. We use the user column to do this.

+
+

user column

+
+
+

Other Columns

+

Now add the other columns by clicking the + sign. The column details are below.

+
+ + + + + + + + + + + + + + + + + + + + + + + +

Column Name

Column Type

subject

Text

details

Text

start_date

Date

due_date

Date

completed

True/False

+
+
+
+
+

Testing

+

This is all the columns we should need. When you have finished your table should look as per below.

+

finished table

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/22_add_component.html b/22_add_component.html new file mode 100644 index 0000000..96ec9ba --- /dev/null +++ b/22_add_component.html @@ -0,0 +1,515 @@ + + + + + + + + + + AddComponent Design - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

AddComponent Design

+ +

Now that we have somewhere to store assessment data, we need a way to input it. This will be completed through the AddComponent.

+
+

Design

+

Checking our design we will see the layout of this component.

+

add component

+
+
+

The layout

+

Open the AddComponent in the design mode.

+
+

Title

+

Currently the only element on the component is the title. We’ll start by adding an icon to the title.

+
    +
  1. Click on the title

  2. +
  3. Find Icon in the Properties panel

  4. +
  5. Choose the fa:plus-square

  6. +
+

title

+
+
+

Organisational elements

+

Looking at our design we can see that we will need a Card that contains a Column Panel so add these to your layout.

+

card

+
+
+

Labels and text boxes

+

Now we need to add the labels and text boxes.

+

First add the subject elements

+
    +
  1. Add a label

  2. +
  3. Change the text to Subject

  4. +
  5. Charge its role to input-prompt

  6. +
  7. Add a text box

  8. +
  9. Change the text box’s name to text_box_subject

  10. +
  11. Change the text box’s placeholder to Enter subject name

  12. +
  13. Change the text box’s align to right

  14. +
+

subject

+

Then add the details elements

+
    +
  1. Add a label

  2. +
  3. Change the text to Details

  4. +
  5. Charge its role to input-prompt

  6. +
  7. Add a text box

  8. +
  9. Change the text box’s name to text_box_details

  10. +
  11. Change the text box’s placeholder to Enter assessment details

  12. +
  13. Change the text box’s align to right

  14. +
+

subject

+
+
+

Dates

+

We now need to add the two date fields, Start date and Due date. Anvil uses Python’s datetime module to handle dates. This means that dates are stores as a Python object, with the day, month and year all stored as separate values (it does the same for time). This way you can use each of the values independently, which includes choose how to format the date. In our website, we will display our dates in the format of dd/mm/yyyy so that the 1st of April 2024 would be displayed as 01/04/2024.

+
+

Date formats

+

Date formats can be problematic because different regions use different conventions. The most common format is “DD/MM/YYYY”, but it is far from dominate. The US uses “MM/DD/YYYY” , while Japan, South Korea, China and others use “YYYY/MM/DD”. This can lead to confusion and errors when processing dates from various sources. Additionally, dates can be represented in many ways, such as strings, timestamps, or specific date objects, requiring careful handling to ensure accuracy.

+

Anvil simplifies working with dates by using Python’s datetime module, which provides a standard way to create, manipulate, and format dates. Anvil’s date pickers and forms automatically handle date input in a consistent format. When dates are stored in Anvil’s database, they are stored as datetime objects, ensuring that date operations are performed correctly and consistently, regardless of the user’s regional settings. Anvil also provides utilities to format dates for display and to parse dates from strings, making it easier to handle different date formats accurately.

+

For more information check out Geeks for Geeks datetime tutorial

+
+

First we need to add the start date:

+
    +
  1. Add a label

  2. +
  3. Change the text to Start date

  4. +
  5. Charge its role to input-prompt

  6. +
  7. Add a date picker

  8. +
  9. Change the date picker’s name to date_picker_start

  10. +
  11. Change the date picker’s placeholder to Enter start date

  12. +
  13. Change the date picker’s format to %d/%m/%Y

  14. +
  15. Change the date picker’s align to right

  16. +
+

start

+

Then we need to add the due date:

+
    +
  1. Add a label

  2. +
  3. Change the text to Due date

  4. +
  5. Charge its role to input-prompt

  6. +
  7. Add a date picker

  8. +
  9. Change the date picker’s name to date_picker_due

  10. +
  11. Change the date picker’s placeholder to Enter due date

  12. +
  13. Change the date picker’s format to %d/%m/%Y

  14. +
  15. Change the date picker’s align to right

  16. +
+

due

+
+
+

Error message

+

Just like the SetDetailsComponent we will be validating the user’s input, therefore we need an element to display warnings. Previously we have displayed warnings under the card, so we will continue this to maintain consistency.

+
    +
  1. Add a label

  2. +
  3. Change the name to label_error

  4. +
  5. Untick visibility

  6. +
  7. Change foreground to #ff0000

  8. +
  9. Change role to headline

  10. +
  11. Change icon to fa:warning

  12. +
  13. Change align to center

  14. +
+

error

+
+
+

Add Assessment Button

+

Finally we are going to insert an Add Assessment Button.

+
    +
  1. Add a button

  2. +
  3. Change the name to button_add

  4. +
  5. Change the text to Add Assessment

  6. +
  7. Change the role to the same as the other buttons you have used (check the SetDetailsComponent)

  8. +
  9. Change icon to fa:save

  10. +
  11. Change align to right

  12. +
+

add button

+
+
+
+

Testing

+

That is all the elements that the AddComponent needs. Your finished layout should look like:

+

finished

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/22a_add_component_code.html b/22a_add_component_code.html new file mode 100644 index 0000000..4f98333 --- /dev/null +++ b/22a_add_component_code.html @@ -0,0 +1,775 @@ + + + + + + + + + + AddComponent Code - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

AddComponent Code

+ +

Now that we have create the layout for the AddComponent, we need to write the code to run it. Eventually we want to save the details to a table, but we haven’t made a table yet, so we will simply display the data as a save message

+
+

Planning

+

There are two events that will trigger code:

+
    +
  • loading the component → initialise variable to store the user input

  • +
  • clicking the Add Assessment button → validate the user’s input, display save message and reset the form.

  • +
+
+
+

Coding

+

Now we know what we are doing, open the AddComponent in Code mode.

+
+

Initialising variable

+

We need to create variables to store all the user’s input. This needs to occur in the __init__ method.

+
10class AddComponent(AddComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14    self.subject = ""
+15    self.details = ""
+16    self.start = None
+17    self.due = None
+
+
+
+

Code explanation

+
    +
  • lines 14 & 15 → since Subject and Details store text, we create empty strings for their variables.

  • +
  • lines 16 & 15Start and Due are both dates, which are objects, so their empty variables are set using None

  • +
+
+

We also need to make sure the error message is invisible.

+
10class AddComponent(AddComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14    self.subject = ""
+15    self.details = ""
+16    self.start = None
+17    self.due = None
+18
+19    # Any code you write here will run before the form opens.
+20    self.label_error.visible = False
+
+
+
+

Code explanation

+
    +
  • line 20 → sets the label error to not vissible

  • +
+
+
+
+

Validation

+

Now we need to make the button_add_click event handler.

+

event handler

+

The first code we add to the button_add_click event handler checks that the user has entered data.

+
22  def button_add_click(self, **event_args):
+23    # validation
+24    if not self.text_box_subject.text:
+25      self.label_error.visible = True
+26      self.label_error.text = "Subject name needed"
+27    elif not self.text_box_details.text:
+28      self.label_error.visible = True
+29      self.label_error.text = "Assessment details needed"
+30    elif not self.date_picker_start.date:
+31      self.label_error.visible = True
+32      self.label_error.text = "Start date needed"
+33    elif not self.date_picker_due.date:
+34      self.label_error.visible = True
+35      self.label_error.text = "Due date needed"
+
+
+
+

Code explanation

+
    +
  • lines 24 & 27 → checks that the text box has a value (remember string truthiness)

  • +
  • lines 30 & 33 → checks that a date has been picked (remember object truthiness)

  • +
  • lines 25, 28, 31 & 34 → make the error message visible

  • +
  • lines 26, 29, 32 & 35 → display the error message

  • +
+
+
+

Validation testing

+

Launch your website and check that it produces an error message for each input element.

+
+
+
+

Save Message

+

Now we want to display a message to the user about what values will be saved.

+
22  def button_add_click(self, **event_args):
+23    # validation
+24    if not self.text_box_subject.text:
+25      self.label_error.visible = True
+26      self.label_error.text = "Subject name needed"
+27    elif not self.text_box_details.text:
+28      self.label_error.visible = True
+29      self.label_error.text = "Assessment details needed"
+30    elif not self.date_picker_start.date:
+31      self.label_error.visible = True
+32      self.label_error.text = "Start date needed"
+33    elif not self.date_picker_due.date:
+34      self.label_error.visible = True
+35      self.label_error.text = "Due date needed"
+36    else:
+37      self.label_error.visible = True
+38      self.subject = self.text_box_subject.text
+39      self.details = self.text_box_details.text
+40      self.start = self.date_picker_start.date
+41      self.due = self.date_picker_due.date
+42      self.label_error.text = f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded"
+
+
+
+

Code explanation

+
    +
  • line 37 → makes the message available

  • +
  • lines 38 - 41 → reads the values from the form and stores them in the variables

  • +
  • line 42 → creates a string displaying the message and displays it in the error message label.

  • +
+
+
+

Save testing

+

Launch your website and check if it works. Enter a subject, assessment details, start date and due date, then click the Add Assessment button.

+

testing save

+

We can make a few observations from this test:

+
    +
  1. The save message should not look the same as the error message

  2. +
  3. When the same message is presented, the input values in the form should be reset

  4. +
+

Let’s fix those up.

+
+
+
+

Fixing messaging

+

The problem we have with the save message is that it is using the same format at the error message. There are two solutions:

+
    +
  1. Add another label for the save message, or

  2. +
  3. Change the format of the error label.

  4. +
+

Neither one has an advantage over the other, so, for no particular reason, we will change the format of the error label.

+

This will involve changing the following label properties:

+
    +
  • foreground colour

  • +
  • icon

  • +
  • bold

  • +
+

If we look at our code, this will need to happen for each of the error messages, as well as the save message. That means in five different locations. This is started to get quite repetitive. If we put this code through, and then decide to change the error foreground colour, we will need to fix it in multiple places. This is a classic example of the DRY principle. The means, it’s time to refactor.

+
+

Refactor plan

+

What we will do is create two methods to:

+
    +
  • display an error message

  • +
  • display a save message

  • +
+

These methods will contain the appropriate formatting and then change the label text.

+

Speaking of the label, the name label_error doesn’t seem appropriate now, sow we will change it to label_message.

+

So, before refactoring the code, first change back to the Design mode and change the name of label_error to label_message.

+

change label

+
+
+

Display error

+

Now, back in the Code mode, we will first create the display_error method

+

Under the button_add_click handler add the following code (be careful with your indentations).

+
44  def display_error(self, message):
+45    self.label_message.visible = True
+46    self.label_message.foreground = "#ff0000"
+47    self.label_message.icon = "fa:exclamation-triangle"
+48    self.label_message.bold = True
+49    self.label_message.text = message
+
+
+
+

Code explanation

+
    +
  • line 44 → creates the display_error method, which accepts an argument called message

  • +
  • line 45 → turns the label visible

  • +
  • line 46 → changes the font colour to red

  • +
  • line 47 → sets the label’s icon

  • +
  • line 48 → makes the label bold

  • +
  • line 49 → makes the label display the message passed to it as an argument

  • +
+
+

Now go back to the button_add_click handler and replace all the error label code with a call to display_error.

+
22  def button_add_click(self, **event_args):
+23    # validation
+24    if not self.text_box_subject.text:
+25      self.display_error("Subject name needed")
+26    elif not self.text_box_details.text:
+27      self.display_error("Assessment details needed")
+28    elif not self.date_picker_start.date:
+29      self.display_error("Start date needed")
+30    elif not self.date_picker_due.date:
+31      self.display_error("Due date needed")
+32    else:
+33      self.label_error.visible = True
+34      self.subject = self.text_box_subject.text
+35      self.details = self.text_box_details.text
+36      self.start = self.date_picker_start.date
+37      self.due = self.date_picker_due.date
+38      self.label_error.text = f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded"
+
+
+
+
+

Display save

+

Now to add the display_save method.

+

Add the following code under the display_error method (remember to check indentation).

+
47  def display_save(self, message):
+48    self.label_message.visible = True
+49    self.label_message.foreground = "#000000"
+50    self.label_message.icon = "fa:save"
+51    self.label_message.bold = False
+52    self.label_message.text = message
+
+
+
+

Code explanation

+
    +
  • line 47 → creates the display_save method, which accepts an argument called message

  • +
  • line 48 → turns the label visible

  • +
  • line 49 → changes the font colour to black

  • +
  • line 50 → sets the label’s icon

  • +
  • line 51 → makes the label not bold

  • +
  • line 52 → makes the label display the message passed to it as an argument

  • +
+
+

Now go back to the button_add_click handler and replace the save message code with a call to display_save.

+
32    else:
+33      self.subject = self.text_box_subject.text
+34      self.details = self.text_box_details.text
+35      self.start = self.date_picker_start.date
+36      self.due = self.date_picker_due.date
+37      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+
+
+
+
+

Remove any other reference to label_error

+

Use Anvil’s search function to check if there are any more references to label_error in AddComponent and then fix them.

+

search

+
+
+

Test messaging

+

Launch your website and check that all the error messages and the save message work as planned.

+

test message

+
+
+
+

Clear Form

+

The final step is to clear the user input and ready the form for the next assessment.

+

Under the display_save method add the following code:

+
54  def reset_form(self):
+55    self.subject = ""
+56    self.details = ""
+57    self.start = None
+58    self.due = None
+59    self.text_box_subject.text = ""
+60    self.text_box_details.text = ""
+61    self.date_picker_start.date = None
+62    self.date_picker_due.date = None
+
+
+
+

Code explanation

+
    +
  • line 54 → creates the reset_form method

  • +
  • lines 55 - 58 → returns all the variables to their initialisation state

  • +
  • lines 59 - 62 → clears all the input from the elements

  • +
+
+

Since we only want to clear the form after it’s been saved, call the function from within the else section of the button_add_click handler.

+
32    else:
+33      self.subject = self.text_box_subject.text
+34      self.details = self.text_box_details.text
+35      self.start = self.date_picker_start.date
+36      self.due = self.date_picker_due.date
+37      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+38      self.reset_form()
+
+
+
+

Test clear form

+

Launch your website one more time and correctly enter all the required data. When you click the Add Assessment button, the save message should appear and all the input should be cleared.

+

test clear

+
+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final AddComponent

+
 1from ._anvil_designer import AddComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class AddComponent(AddComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14    self.subject = ""
+15    self.details = ""
+16    self.start = None
+17    self.due = None
+18
+19    # Any code you write here will run before the form opens.
+20    self.label_message.visible = False
+21
+22  def button_add_click(self, **event_args):
+23    # validation
+24    if not self.text_box_subject.text:
+25      self.display_error("Subject name needed")
+26    elif not self.text_box_details.text:
+27      self.display_error("Assessment details needed")
+28    elif not self.date_picker_start.date:
+29      self.display_error("Start date needed")
+30    elif not self.date_picker_due.date:
+31      self.display_error("Due date needed")
+32    else:
+33      self.subject = self.text_box_subject.text
+34      self.details = self.text_box_details.text
+35      self.start = self.date_picker_start.date
+36      self.due = self.date_picker_due.date
+37      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+38      self.reset_form()
+39
+40  def display_error(self, message):
+41    self.label_message.visible = True
+42    self.label_message.foreground = "#ff0000"
+43    self.label_message.icon = "fa:exclamation-triangle"
+44    self.label_message.bold = True
+45    self.label_message.text = message
+46
+47  def display_save(self, message):
+48    self.label_message.visible = True
+49    self.label_message.foreground = "#000000"
+50    self.label_message.icon = "fa:save"
+51    self.label_message.bold = False
+52    self.label_message.text = message
+53
+54  def reset_form(self):
+55    self.subject = ""
+56    self.details = ""
+57    self.start = None
+58    self.due = None
+59    self.text_box_subject.text = ""
+60    self.text_box_details.text = ""
+61    self.date_picker_start.date = None
+62    self.date_picker_due.date = None
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/23_assessment_service.html b/23_assessment_service.html new file mode 100644 index 0000000..98d1a24 --- /dev/null +++ b/23_assessment_service.html @@ -0,0 +1,579 @@ + + + + + + + + + + Assessment Service Module - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Assessment Service Module

+ +

Now that we have the AddComponent set and the user can input their data, we can save that data to the Assessments table. Just like we used a user_service to interact with the Users table, we will create an assessment_service to interact with the Assessments table.

+
+

Planning

+

We will need to make a method that accepts the assessment details and writes it to the Assessments table. This will reside in a Server-side module, so let’s start by creating that module.

+
    +
  1. Go to the file submenu

  2. +
  3. Click on the + Add Server Module button

  4. +
+

add server module

+
    +
  1. Name the module assessment_service

  2. +
  3. Delete all the comments from the code

  4. +
+

assessment service

+
+
+

Code

+

Now to add our assessment_service code

+
+

Add Assessment Function

+

Below the import section add the following code:

+
 7@anvil.server.callable
+ 8def add_assessment(subject, details, start_date, due_date):
+ 9  user = anvil.users.get_user()
+10  
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+
+
+
+

Code explanation

+
    +
  • line 7 → this is the decorator that tells Anvil that this function can be called from the frontend

  • +
  • line 8 → creates the add_assessment function which expects the values of subject, details, start_date, and due_date to be passed.

  • +
  • line 9 → gets the details of the current user from the user table

  • +
  • lines 11-16 → this is one command split over multiple lines and adds the provided details to the assessment table.

  • +
+
+
+

Splitting statements over multiple lines

+

Python commands can become quite long, especially if you are using meaningful names (eg. line 11 in the above code). This can make them difficult to read. Fortunately Python provides ways to split commmands over muilitple lines.

+
    +
  1. You can use a backslach \ after a operations symbol

  2. +
  3. Within parentheses (), brackets [] or braces {}, you can press return after a comma ,

  4. +
+
+

Now that we have a function to add the user data to the Assessments table, we need to call that function from the AddComponent.

+
+
+

Call from Add Component

+

Open the AddComponent in Code mode.

+

In the else section of the button_add_click handler, add the highlighted code between the call to display_save and the call to reset_form.

+
32    else:
+33      self.subject = self.text_box_subject.text
+34      self.details = self.text_box_details.text
+35      self.start = self.date_picker_start.date
+36      self.due = self.date_picker_due.date
+37      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+38      anvil.server.call('add_assessment', self.subject, self.details, self.start, self.due)
+39      self.reset_form()
+
+
+
+

Code explanation

+
    +
  • line 38 → calls the add_assessments function that we just created.

    +
      +
    • It is important that this is called before the reset_form, as reset_form deletes all the values we wish to save to the Assessments table.

    • +
    +
  • +
+
+
+
+

Testing

+

Let’s test the code.

+
    +
  1. Launch your web app and then navigate to the Add page.

  2. +
  3. Enter the details for an assessment, click on the Add Assessment button.

  4. +
  5. Once the save message is displayed, stop the web app

  6. +
  7. Go to the Assessments table and check if the data you entered is in the table.

  8. +
+

testing

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final AddComponent

+
 1from ._anvil_designer import AddComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class AddComponent(AddComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14    self.subject = ""
+15    self.details = ""
+16    self.start = None
+17    self.due = None
+18
+19    # Any code you write here will run before the form opens.
+20    self.label_message.visible = False
+21
+22  def button_add_click(self, **event_args):
+23    # validation
+24    if not self.text_box_subject.text:
+25      self.display_error("Subject name needed")
+26    elif not self.text_box_details.text:
+27      self.display_error("Assessment details needed")
+28    elif not self.date_picker_start.date:
+29      self.display_error("Start date needed")
+30    elif not self.date_picker_due.date:
+31      self.display_error("Due date needed")
+32    else:
+33      self.subject = self.text_box_subject.text
+34      self.details = self.text_box_details.text
+35      self.start = self.date_picker_start.date
+36      self.due = self.date_picker_due.date
+37      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+38      anvil.server.call('add_assessment', self.subject, self.details, self.start, self.due)
+39      self.reset_form()
+40
+41  def display_error(self, message):
+42    self.label_message.visible = True
+43    self.label_message.foreground = "#ff0000"
+44    self.label_message.icon = "fa:exclamation-triangle"
+45    self.label_message.bold = True
+46    self.label_message.text = message
+47
+48  def display_save(self, message):
+49    self.label_message.visible = True
+50    self.label_message.foreground = "#000000"
+51    self.label_message.icon = "fa:save"
+52    self.label_message.bold = False
+53    self.label_message.text = message
+54
+55  def reset_form(self):
+56    self.subject = ""
+57    self.details = ""
+58    self.start = None
+59    self.due = None
+60    self.text_box_subject.text = ""
+61    self.text_box_details.text = ""
+62    self.date_picker_start.date = None
+63    self.date_picker_due.date = None
+
+
+
+
+

Final assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6
+ 7@anvil.server.callable
+ 8def add_assessment(subject, details, start_date, due_date):
+ 9  user = anvil.users.get_user()
+10  
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/24_restrict_accessibility.html b/24_restrict_accessibility.html new file mode 100644 index 0000000..c085031 --- /dev/null +++ b/24_restrict_accessibility.html @@ -0,0 +1,764 @@ + + + + + + + + + + Prevent Unauthorised Access - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Prevent Unauthorised Access

+ +

Presently our web app is pretty much open to everyone. Even if you are not logged in, you can view the Home, Add and Calendar pages. Although these pages don’t hold sensitive private information, the functionality of these pages depend up a user being logged in:

+
    +
  • HomeComponent → displays the user’s assessment

  • +
  • CalendarComponent → displays the user’s assessment timeline

  • +
  • AddComponent → adds the user’s assessment details

  • +
+

This means that if there is no user logged in, then these components will not function correctly. In this tutorial, we will fix that problem.

+
+

Planning

+

For the AddComponent and the CalendarComponent we will add a second card with and error message. Then, when the component is loaded, either the default card or the error card will be displayed.

+

When it comes to home we will choose between loading the HomeComponent or the WelcomeComponent, depending upon the user login status.

+
+
+

Layout

+
+

AddComponent Layout

+

Open the AddComponent in Design mode.

+
    +
  1. Choose the current card

  2. +
  3. Change it’s name from self.card_1 to self.card_details

  4. +
+

card details

+
    +
  1. Add a new card between self.card_details and self.label_message (pay close attention to the brown lines)

  2. +
  3. Name the new card self.card_error

  4. +
+

card error

+
    +
  1. Add a label to the self.card_error

  2. +
  3. Change the text to Please login to access this feature

  4. +
  5. Change foreground to #ff0000

  6. +
  7. Change role to your desired formatting

  8. +
  9. Change icon to fa:warning

  10. +
  11. Change align to center

  12. +
+

login error

+
+
+

CalendarComponent Layout

+

We haven’t added anything to the CalendarComponent, so we will need to add two cards:

+
    +
  • call the first one self.card_details

  • +
  • call the second one self.card_error

  • +
+

calendar

+

Leave self.card_details blank, and then change self.card_error to be the same as AddComponent.

+
+
+

Home Page Layout

+

We don’t have to change the layout of the HomeComponent as we will simply load the WelcomeComponent instead. We will worry about the welcomeComponent layout later on.

+
+
+
+

Code

+
+

AddComponent code

+

The AddComponent code will simply check if someone is logged in and adjust the visibility of self.card_details and self.card_error accordingly.

+

Open AddComponent in Code mode.

+

In the __init__ method add the highlighted code:

+
11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14    self.subject = ""
+15    self.details = ""
+16    self.start = None
+17    self.due = None
+18
+19    # Any code you write here will run before the form opens.
+20    self.label_message.visible = False
+21    if anvil.users.get_user():
+22      self.card_details.visible = True
+23      self.card_error.visible = False
+24      self.button_add.visible = True
+25    else:
+26      self.card_details.visible = False
+27      self.card_error.visible = True
+28      self.button_add.visible = False
+
+
+
+

Code explanation

+
    +
  • line 21 → checks if there is a user currently logged in

  • +
  • lines 22 - 24 → sets the UI to display the elements appropriate for a logged in user

  • +
  • lines 26 - 28 → sets the UI to display the elements appropriate for no logged in user

  • +
+
+
+
+

CalendarComponent code

+

Use the same logic to display the correct elements in the CalendarComponent.

+

In CalendarComponents __init__ method, add the code below:

+
11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    if anvil.users.get_user():
+17      self.card_details.visible = True
+18      self.card_error.visible = False
+19    else:
+20      self.card_details.visible = False
+21      self.card_error.visible = True
+
+
+
+
+

Home page code

+

The Home page code is trickier, since it involves two components. Luckily we have centralised all component loading into the switch_component method in the MainForm. We can check for user status there and load the HomeComponent is a user is logged in or load the WelcomeComponent if there is no user logged in.

+

Open the MainForm in Code mode.

+

First, if we are going to load the WelcomeComponent, we need to import it. Add the highlighted code to the import statement.

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13from ..WelcomeComponent import WelcomeComponent
+
+
+

Now in the switch_component method add the following code:

+
25  def switch_component(self, state):
+26    # set state
+27    if state == "home":
+28      if anvil.users.get_user():
+29        cmpt = HomeComponent()
+30      else:
+31        cmpt = WelcomeComponent()
+32      breadcrumb = self.breadcrumb_stem
+
+
+
    +
  • line 27 → note this code is only executed when the home page is being loaded

  • +
  • line 28 → checks if there is a user currently logged in

  • +
  • line 29 → loads the HomeComponent

  • +
  • line 30 & 31 → loads the WelcomeComponent is no user is logged in

  • +
+
+
+

Testing

+

Launch your website. Your should be logged in. Make sure that you can access all three of the pages we have worked on:

+
    +
  • Home

  • +
  • Add

  • +
  • Calendar

  • +
+

Then logout and check all three pages again.

+

testing

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13from ..WelcomeComponent import WelcomeComponent
+14
+15
+16class MainForm(MainFormTemplate):
+17  def __init__(self, **properties):
+18    # Set Form properties and Data Bindings.
+19    self.init_components(**properties)
+20    self.breadcrumb_stem = self.label_title.text
+21
+22    # Any code you write here will run before the form opens.
+23    self.switch_component("home")
+24
+25  def switch_component(self, state):
+26    # set state
+27    if state == "home":
+28      if anvil.users.get_user():
+29        cmpt = HomeComponent()
+30      else:
+31        cmpt = WelcomeComponent()
+32      breadcrumb = self.breadcrumb_stem
+33    elif state == "account":
+34      cmpt = AccountComponent()
+35      breadcrumb = self.breadcrumb_stem + " - Account"
+36    elif state == "add":
+37      cmpt = AddComponent()
+38      breadcrumb = self.breadcrumb_stem + " - Add"
+39    elif state == "calendar":
+40      cmpt = CalendarComponent()
+41      breadcrumb = self.breadcrumb_stem + " - Calendar"
+42    elif state == "details":
+43      cmpt = SetDetailsComponent()
+44      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
+45    
+46    # execution
+47    self.content_panel.clear()
+48    self.content_panel.add_component(cmpt)
+49    self.label_title.text = breadcrumb
+50    self.set_active_link(state)
+51  
+52  def set_active_link(self, state):
+53    if state == "home":
+54      self.link_home.role = "selected"
+55    else:
+56      self.link_home.role = None
+57    if state == "add":
+58      self.link_add.role = "selected"
+59    else:
+60      self.link_add.role = None
+61    if state == "calendar":
+62      self.link_calendar.role = "selected"
+63    else:
+64      self.link_calendar.role = None
+65
+66    self.link_register.visible = not anvil.users.get_user()
+67    self.link_login.visible = not anvil.users.get_user()
+68    self.link_account.visible = anvil.users.get_user()
+69    self.link_logout.visible = anvil.users.get_user()
+70  
+71  # --- link handlers
+72  def link_home_click(self, **event_args):
+73    self.switch_component("home")
+74
+75  def link_calendar_click(self, **event_args):
+76    self.switch_component("calendar")
+77
+78  def link_add_click(self, **event_args):
+79    self.switch_component("add")
+80
+81  def link_account_click(self, **event_args):
+82    """This method is called when the link is clicked"""
+83    self.switch_component("account")
+84
+85  def link_register_click(self, **event_args):
+86    anvil.users.signup_with_form(allow_cancel=True)
+87    self.switch_component("details")
+88
+89  def link_login_click(self, **event_args):
+90    anvil.users.login_with_form(allow_cancel=True)
+91    self.switch_component("home")
+92
+93  def link_logout_click(self, **event_args):
+94    anvil.users.logout()
+95    self.switch_component("home")
+
+
+
+
+

Final AddComponent

+
 1from ._anvil_designer import AddComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class AddComponent(AddComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14    self.subject = ""
+15    self.details = ""
+16    self.start = None
+17    self.due = None
+18
+19    # Any code you write here will run before the form opens.
+20    self.label_message.visible = False
+21    if anvil.users.get_user():
+22      self.card_details.visible = True
+23      self.card_error.visible = False
+24      self.button_add.visible = True
+25    else:
+26      self.card_details.visible = False
+27      self.card_error.visible = True
+28      self.button_add.visible = False
+29
+30  def button_add_click(self, **event_args):
+31    # validation
+32    if not self.text_box_subject.text:
+33      self.display_error("Subject name needed")
+34    elif not self.text_box_details.text:
+35      self.display_error("Assessment details needed")
+36    elif not self.date_picker_start.date:
+37      self.display_error("Start date needed")
+38    elif not self.date_picker_due.date:
+39      self.display_error("Due date needed")
+40    else:
+41      self.subject = self.text_box_subject.text
+42      self.details = self.text_box_details.text
+43      self.start = self.date_picker_start.date
+44      self.due = self.date_picker_due.date
+45      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+46      anvil.server.call('add_assessment', self.subject, self.details, self.start, self.due)
+47      self.reset_form()
+48
+49  def display_error(self, message):
+50    self.label_message.visible = True
+51    self.label_message.foreground = "#ff0000"
+52    self.label_message.icon = "fa:exclamation-triangle"
+53    self.label_message.bold = True
+54    self.label_message.text = message
+55
+56  def display_save(self, message):
+57    self.label_message.visible = True
+58    self.label_message.foreground = "#000000"
+59    self.label_message.icon = "fa:save"
+60    self.label_message.bold = False
+61    self.label_message.text = message
+62
+63  def reset_form(self):
+64    self.subject = ""
+65    self.details = ""
+66    self.start = None
+67    self.due = None
+68    self.text_box_subject.text = ""
+69    self.text_box_details.text = ""
+70    self.date_picker_start.date = None
+71    self.date_picker_due.date = None
+
+
+
+
+

Finial CalendarComponent

+
 1from ._anvil_designer import CalendarComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class CalendarComponent(CalendarComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    if anvil.users.get_user():
+17      self.card_details.visible = True
+18      self.card_error.visible = False
+19    else:
+20      self.card_details.visible = False
+21      self.card_error.visible = True
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/25_home_user_cmpt.html b/25_home_user_cmpt.html new file mode 100644 index 0000000..0ad14f1 --- /dev/null +++ b/25_home_user_cmpt.html @@ -0,0 +1,473 @@ + + + + + + + + + + HomeComponent Design - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

HomeComponent Design

+ +

Now we can turn to the HomeComponent. This component lists all the current assessments and their respective details. Lets plan this out

+
+

Planning

+

This is where we start to get tricky. Remember this web app is dynamic, that means how it looks is dependent on the stored data. In the case, the number of rows in our HomeComponent is dependent on the number of relevant assessments stored in our Assessments table.

+

There are two things we need to achieve this:

+
    +
  1. We need to retrieve a list of assessments data items from the Assessments table.

  2. +
  3. We need a layout element that will display the rows from that list.

  4. +
+

To retrieve the list we will create a new function called get_assessments in the assessment_service module.

+

To display the list we will use a layout element called a Repeating Panel. Repeating Panels display a list of items in a repeated pattern. When it is connected to a list of data items, the panel repeats for each data item. It is probably easier to understand in practice, so let’s get started.

+
+
+

Layout

+
+

Add repeating panel

+

Open the HomeComponent in the Design mode.

+

First step:

+
    +
  1. add the fa:home icon to the title

  2. +
  3. add a card under the title

  4. +
+

adding card

+

Next you need to:

+
    +
  1. find and click on Repeating Panel in the Layout Components

  2. +
  3. place it in the layout by clicking on the card (check the blue line)

  4. +
  5. Anvil will automatically open the repeating panel, rename it AssessmentPanel

  6. +
  7. return to the HomeComponent click on repeating_panel_1 and change it’s role to card

  8. +
+

repeating panel

+
+
+

Repeating panel layout

+

Now we need to add elements to the AssessmentPanel, so open it in Design mode.

+
    +
  1. find the Column Panel and add it to the AssessmentPanel

  2. +
+

column panel

+
    +
  1. locate the Check Box and add it.

  2. +
  3. rename the self.check_box_1 to self.check_box_completed

  4. +
  5. delete text leaving it blank

  6. +
+

check box

+
    +
  1. add a label beside the check box (watch the blue line)

  2. +
  3. rename it as label_subject

  4. +
  5. change the role to the format you desire

  6. +
  7. find and check bold

  8. +
+

subject label

+
    +
  1. add another label beside the label_subject

  2. +
  3. rename the new label to label_details

  4. +
  5. change the role to be the same as label_subject

  6. +
+

details label

+
    +
  1. add another label beside label_details

  2. +
  3. rename it label_start

  4. +
  5. change the role to be the same as label_subject

  6. +
+

date start

+
    +
  1. add a label beside label_start

  2. +
  3. change the text to to

  4. +
  5. choose the same role as label_subject

  6. +
+

to label

+
    +
  1. add another label beside to

  2. +
  3. rename it label_due

  4. +
  5. change the role to be the same as label_subject

  6. +
+

due date

+
    +
  1. click on the column panel

  2. +
  3. using the blue lines, change the column widths to best suit your content

  4. +
+

resize

+

We have now finished the layout, time to turn towards the code.

+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/25a_home_component_code.html b/25a_home_component_code.html new file mode 100644 index 0000000..6819743 --- /dev/null +++ b/25a_home_component_code.html @@ -0,0 +1,596 @@ + + + + + + + + + + HomeComponent Code - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

HomeComponent Code

+ +
+

Assessments service

+

We noted in the last tutorial that the repeating panel needs to be linked to a list of items. For the HomeComponent the list of items will be a list of the assessments. This means that we will have to retrieve the list of assessments from the Assessments table, therefore, using the assessments_services.

+

Before we look at the code, we need to consider exactly what we are retrieving from the Assessments table:

+
    +
  • the app can have multiple users, so we only want the assessments that belong to the current user.

  • +
  • the assessments are listed in the order they were entered, which is not much use. We will need to sort the list so the assessments are listed according to their due date.

  • +
+
+

Assessment services code

+

In the Server Code open the assessment_services.

+

Then add the new get_assessments function below:

+
18@anvil.server.callable
+19def get_assessment():
+20  user = anvil.users.get_user()
+21
+22  return app_tables.assessments.search(tables.order_by('due_date'),
+23                                       user=user)
+
+
+
+

Code explanation

+
    +
  • line 18 → the decorator that allows the frontend to call this backend function

  • +
  • line 19 → defines the get_assessment function, note no arguments required

  • +
  • line 20 → retrieves the details of the currently logged in user

  • +
  • lines 22 - 23 → retrieves the assessments from the Assessments table and returns them to the front end:

    +
      +
    • app_table.assessments → working with the Assessments table

    • +
    • search → command used to retrieve row from the table

    • +
    • tables.order_by('due_date') → sorts the results according to the assessments due date

    • +
    • user=user → only retrieves assessments that belong to the current user

    • +
    +
  • +
+
+
+
+
+

HomeComponent

+

Now that we can retrieve the assessment items, we need to link them to the repeating panel. We do this in HomeComponent. It will also need to happen before the HomeComponent opens.

+
+

HomeComponent code

+

Open the HomeComponent in the code mode and then add the highlighted code to the __init__ method:

+
11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    self.repeating_panel_1.items = anvil.server.call('get_assessment')
+
+
+
+

Code explanation

+
    +
  • line 6 → calls the get_assessment function and then connects the returned list to repearing_panel_1 items.

  • +
+
+
+
+
+

AssessmentPanel

+

Now that we have connected the list of assessments to the repeating panel, we can now connect the assessment values to their respective element in the AssessmentPanel. This needs to happen in the AssessmentPanel before it opens.

+
+

AssessmentPanel code

+

Open the AssessmentPanel in the code mode. Before we work on the __init__ method we need to import datetime so we can format the date correctly. So in the import section add the highlighted code:

+
1from ._anvil_designer import AssessmentPanelTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.users
+5import anvil.tables as tables
+6import anvil.tables.query as q
+7from anvil.tables import app_tables
+8import datetime
+
+
+

Now add the following code to the __init__ method.

+
12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    self.check_box_completed.checked = self.item['completed']
+18    self.label_subject.text = self.item['subject']
+19    self.label_details.text = self.item['details']
+20    self.label_start.text = self.item['start_date'].strftime('%d/%m/%Y')
+21    self.label_due.text = self.item['due_date'].strftime('%d/%m/%Y')
+
+
+
+

Repeating panels and items

+

The repeating panel is connected to a list of items. This means that each instance of the panel is connected to one of the items.

+

When the code uses self.item is refers to the specific data item belonging to this specific panel. This means you can acces the values in the item just like a dicitonary.

+
+
+

Code explanation

+
    +
  • line 17 → read the completed value for this panel’s item and display it in the check_box_completed

  • +
  • line 18 → read the subject value for this panel’s item and display it in the label_subject

  • +
  • line 19 → read the details value for this panel’s item and display it in the label_details

  • +
  • line 20 → read the start_date value for this panel’s item. Format it to show dd/mm/yyyy and then display it in the label_start

  • +
  • line 21 → read the due_date value for this panel’s item. Format it to show dd/mm/yyyy and then display it in the label_due

  • +
+
+
+
+
+

Testing

+

Time to launch your website and see if the assessments are displayed on the Home page. Make sure that you are logged in.

+

The Home page should look like this:

+

test

+

We’re starting to get a functional web app. Try adding more assessment items and seeing how they display on the Home page.

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6
+ 7@anvil.server.callable
+ 8def add_assessment(subject, details, start_date, due_date):
+ 9  user = anvil.users.get_user()
+10  
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+17
+18@anvil.server.callable
+19def get_assessment():
+20  user = anvil.users.get_user()
+21
+22  return app_tables.assessments.search(tables.order_by('due_date'),
+23                                       user=user)
+
+
+
+
+

Final HomeComponent

+
 1from ._anvil_designer import HomeComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8
+ 9
+10class HomeComponent(HomeComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    self.repeating_panel_1.items = anvil.server.call('get_assessment')
+
+
+
+
+

Final AssessmentPanel

+
 1from ._anvil_designer import AssessmentPanelTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.users
+ 5import anvil.tables as tables
+ 6import anvil.tables.query as q
+ 7from anvil.tables import app_tables
+ 8import datetime
+ 9
+10
+11class AssessmentPanel(AssessmentPanelTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    self.check_box_completed.checked = self.item['completed']
+18    self.label_subject.text = self.item['subject']
+19    self.label_details.text = self.item['details']
+20    self.label_start.text = self.item['start_date'].strftime('%d/%m/%Y')
+21    self.label_due.text = self.item['due_date'].strftime('%d/%m/%Y')
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/26_complete_assessment.html b/26_complete_assessment.html new file mode 100644 index 0000000..7afa90e --- /dev/null +++ b/26_complete_assessment.html @@ -0,0 +1,547 @@ + + + + + + + + + + Complete Assessments - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Complete Assessments

+ +

We can enter assessment details and we can view them. We even have a check box on the AssessmentPanel that allows the user to tick whether they have completed an assessment. Only problem is, this doesn’t change the information saved in our Assessment table. let’s fix that.

+
+

Plan

+

We want Anvil to respond when the check_box_completed is ticked, this will require an event handler. We know from working with buttons and links, that we create event handlers in the Design screen.

+

We want that handler to take the current value of the check_box_completed and write it to the completed field for the item belonging to that specific panel. Since this happens server-side, we need to create a new function in the assessment_service.

+
+
+

Code

+
+

AssessmentsPanel Code

+

We will start with the code in the AssessmentsPanel, so open it in Design mode.

+
    +
  1. Click on the check_box_completed

  2. +
  3. Click on change event

  4. +
  5. Switch to Code mode

  6. +
+

event

+

In the check_box_completed_change event handler, replace lines 24 and 25 with the highlighted code.

+
23  def check_box_completed_change(self, **event_args):
+24    new_value = self.check_box_completed.checked
+25    anvil.server.call('update_assessment_completed', self.item.get_id(), new_value)
+
+
+
+

Code explanation

+
    +
  • line 24 → takes the current value from check_box_completed

  • +
  • line 25 → sends that value to the server

    +
      +
    • 'update_assessment_completed' → a function we will have to write

    • +
    • self.item.get_id() → gets the id for the item for this specific panel

    • +
    • new_value → the value we retrieved from check_box_completed

    • +
    +
  • +
+
+

Now we have to create the update_assessment_completed function.

+
+
+

assessment_service Code

+

Open the update_assessment_completed function in the assessment_service module under the Server Code.

+

At the bottom of the file add the following code:

+
25@anvil.server.callable
+26def update_assessment_completed(assessment_id, completed):
+27  assessment = app_tables.assessments.get_by_id(assessment_id)
+28  if assessment:
+29    assessment["completed"] = completed
+
+
+
+

Code explanation

+
    +
  • line 25 → makes the functional callable by frontend code

  • +
  • line 26 → creates the update_assessment_completed function which accept 2 arguments:

    +
      +
    • assessment_id → the unique identifier for the assessment

    • +
    • completed → the completed state (True or False) of the assessment

    • +
    +
  • +
  • line 27 → retrieves the row from the assessment table that has the passed assessment_id and stores it in the assessment variable

  • +
  • line 28 → checks the truthiness of the assessment variable

    +
      +
    • if an assessment was retrieved, it will be True

    • +
    • if an assessment wasn’t retrieved (ie. doesn’t exist), it will be False

    • +
    +
  • +
  • line 29 → changes the completed value of the assessment to match the value passed to the function

  • +
+
+
+
+
+

Testing

+

Now to test your code. Launch your web app.

+
    +
  1. Click on the completed checkbox for one of the assessments

  2. +
  3. Stop your web app

  4. +
  5. Click on the data menu

  6. +
  7. In the submenu choose Assessments

  8. +
  9. Check to see if the assessment’s completed box is now ticked.

  10. +
+

testing

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final AssessmentPanel

+
 1from ._anvil_designer import AssessmentPanelTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.users
+ 5import anvil.tables as tables
+ 6import anvil.tables.query as q
+ 7from anvil.tables import app_tables
+ 8import datetime
+ 9
+10
+11class AssessmentPanel(AssessmentPanelTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    self.check_box_completed.checked = self.item['completed']
+18    self.label_subject.text = self.item['subject']
+19    self.label_details.text = self.item['details']
+20    self.label_start.text = self.item['start_date'].strftime('%d/%m/%Y')
+21    self.label_due.text = self.item['due_date'].strftime('%d/%m/%Y')
+22
+23  def check_box_completed_change(self, **event_args):
+24    new_value = self.check_box_completed.checked
+25    anvil.server.call('update_assessment_completed', self.item.get_id(),new_value)
+
+
+
+
+

Final assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6
+ 7@anvil.server.callable
+ 8def add_assessment(subject, details, start_date, due_date):
+ 9  user = anvil.users.get_user()
+10  
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+17
+18@anvil.server.callable
+19def get_assessment():
+20  user = anvil.users.get_user()
+21
+22  return app_tables.assessments.search(tables.order_by('due_date'),
+23                                       user=user)
+24
+25@anvil.server.callable
+26def update_assessment_completed(assessment_id, completed):
+27  assessment = app_tables.assessments.get_by_id(assessment_id)
+28  if assessment:
+29    assessment["completed"] = completed
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/26a_adjust_assessment_display.html b/26a_adjust_assessment_display.html new file mode 100644 index 0000000..a1c968d --- /dev/null +++ b/26a_adjust_assessment_display.html @@ -0,0 +1,467 @@ + + + + + + + + + + Adjust Assessments Display - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Adjust Assessments Display

+ +

We can enter assessment details and we can view them. We even change the value of completed via a check box on the AssessmentPanel.

+

We can display all our assessments, but do we really want to display all assessments. What about assessments that have been completed?

+

In this tutorial you will adjust the HomeComponent so that it only displays the assessments that are still outstanding.

+
+

Change assessments displayed

+

Since the information on the repeating panel is derived from it’s items list, we will need to change the item list. The item list comes from the get_assessment function in the assessment_service, so we need to make changes there.

+
+

get_assessment code

+

Open the get_assessment function in the assessment_service and make the highlighted change to the code:

+
25  return app_tables.assessments.search(tables.order_by('due_date'),
+26                                       user=user,
+27                                       completed=False)
+
+
+
+

Code explanation

+
    +
  • completed=False → just like line 26, this is a restriction on the rows that will be retrieved. Only those assessments that have not been completed will be retrieved.

  • +
+
+
+
+
+

Testing

+

Time to check if the code works.

+
    +
  1. Launch your web app

  2. +
  3. You should not see the assignment that you marked as completed in the previous tutorial.

  4. +
  5. To make sure, stop your web app

  6. +
  7. Go to the assessments table in the data section

  8. +
  9. Find and un-tick the assessment you marked as complete in the last tutorial

  10. +
  11. Relaunch your web app

  12. +
  13. You should now see that assessment on the Home page

  14. +
+

testing

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6
+ 7@anvil.server.callable
+ 8def add_assessment(subject, details, start_date, due_date):
+ 9  user = anvil.users.get_user()
+10  
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+17
+18@anvil.server.callable
+19def get_assessment():
+20  user = anvil.users.get_user()
+21
+22  return app_tables.assessments.search(tables.order_by('due_date'),
+23                                      user=user,
+24                                      completed=False)
+25
+26@anvil.server.callable
+27def update_assessment_completed(assessment_id, completed):
+28  assessment = app_tables.assessments.get_by_id(assessment_id)
+29  if assessment:
+30    assessment["completed"] = completed
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/27_edit_assessment.html b/27_edit_assessment.html new file mode 100644 index 0000000..b8cb935 --- /dev/null +++ b/27_edit_assessment.html @@ -0,0 +1,452 @@ + + + + + + + + + + Edit Assessments Layout - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Edit Assessments Layout

+ +

The functionality of the website allows the user to add an assessment, and check an assessment off as being completed, but what if the details of the assessment changes. For example, what if the due date is changed? What the user needs is a way to edit the assessment details.

+

Rather than creating a whole new component to edit assessment details, we will redesign the AssessmentPanel to allow the user to edit the assessments.

+
+

Planning

+

What we will do is have each panel contain both the display and the editing elements for its specific assessment. The visibility of these elements will depend upon whether the panel is in display mode or edit mode.

+

We already have the display elements, so we will need to add the edit elements to the AssessmentPanel. These elements will be:

+
    +
  • subject text box

  • +
  • details text box

  • +
  • start date picker

  • +
  • due date picker

  • +
+

We won’t need edit elements for:

+
    +
  • completed check box - since the user can already edit this

  • +
  • to label - since it doesn’t change

  • +
+
+
+

Layout

+

Open the AssessmentPanel in Design mode, then:

+
    +
  1. add a text box under label_subject

  2. +
  3. rename it text_box_subject

  4. +
  5. uncheck the visible tick (warning: in the gif, I missed the click)

  6. +
+

subject text box

+
    +
  1. similarly, add a text box under label_details

  2. +
  3. rename it text_box_details

  4. +
  5. uncheck the visible tick

  6. +
+

details text box

+
    +
  1. next, add a date picker under label_start

  2. +
  3. rename it date_picker_start

  4. +
  5. untick visible

  6. +
+

start date

+
    +
  1. add a date picker under label_due

  2. +
  3. rename it date_picker_due

  4. +
  5. untick visible

  6. +
+

due date

+

Now we need to add the buttons to allow this switching.

+
    +
  1. add a button to the right of label_due

  2. +
  3. rename it button_edit

  4. +
  5. delete the text - the button is not big enough for text

  6. +
  7. change the role to your desired formatting

  8. +
  9. change icon to fa:edit - this icon is familiar enough that we don’t need text to explain the purpose of the button.

  10. +
+

edit button

+
    +
  1. add another button under the button_edit

  2. +
  3. rename it button_save

  4. +
  5. delete the text - the button is not big enough for text

  6. +
  7. untick the visible check box

  8. +
  9. change the role to your desired formatting

  10. +
  11. change icon to fa:save - this icon is familiar enough that we don’t need text to explain the purpose of the button.

  12. +
+

save button

+

That’s the layout. Time for the code.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/27a_edit_assessment_code.html b/27a_edit_assessment_code.html new file mode 100644 index 0000000..fcb7e4e --- /dev/null +++ b/27a_edit_assessment_code.html @@ -0,0 +1,749 @@ + + + + + + + + + + Edit Assessments Code - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Edit Assessments Code

+ +
+

Planning

+

The code is going to react to two events:

+
    +
  • button_edit click

  • +
  • button_save click

  • +
+

So, lets go ahead and create those two event handlers.

+

Open the AssessmentPanel in Design mode:

+
    +
  1. click on each button

  2. +
  3. select on click event

  4. +
+

event handlers

+
+
+

Code

+
+

Button edit click

+

When the user clicks the edit button, we want to web app to:

+
    +
  • take the values of the assessment item from this specific panel and write them into the edit elements

  • +
  • make all the display element invisible

  • +
  • make all the edit elements visible

  • +
+

Open the AssessmentPanel in Code mode.

+

In the button_edit_click handler use the code below to copy the values from the display elements.

+
27  def button_edit_click(self, **event_args):
+28    self.text_box_subject.text = self.item["subject"]
+29    self.text_box_details.text = self.item["details"]
+30    self.date_picker_start.date = self.item["start_date"]
+31    self.date_picker_due.date = self.item["due_date"]
+
+
+
+

Code explanation

+
    +
  • line 28 → takes subject value for specific assessment for this panel and saves it as the text for the subject text box.

  • +
  • line 29 → takes details value for specific assessment for this panel and saves it as the text for the details text box.

  • +
  • line 30 → takes start_date value for specific assessment for this panel and saves it as the date for the start date picker

  • +
  • line 31 → takes due_date value for specific assessment for this panel and saves it as the date for the due date picker

  • +
+
+

Now we need to change the visibility of the elements. To do this we will create a separate function that swaps the visibility of each element, ie. if visibility was True it will make it False.

+

We will do this with one function that will work for both event handlers. Add the highlighted code below to the bottom of the AssessmentPanel code.

+
33  def button_save_click(self, **event_args):
+34    """This method is called when the button is clicked"""
+35    pass
+36    
+37  def switch_components(self):
+38    # display elements
+39    self.label_subject.visible = not self.label_subject.visible
+40    self.label_details.visible = not self.label_details.visible
+41    self.label_start.visible = not self.label_start.visible
+42    self.label_due.visible = not self.label_due.visible
+43    self.button_edit.visible = not self.button_edit.visible
+44    
+45    # edit elements
+46    self.text_box_subject.visible = not self.text_box_subject.visible
+47    self.text_box_details.visible = not self.text_box_details.visible
+48    self.date_picker_start.visible = not self.date_picker_start.visible
+49    self.date_picker_due.visible = not self.date_picker_due.visible
+50    self.button_save.visible = not self.button_save.visible
+
+
+
+

Code explanation

+
    +
  • line 37 → create the switch_components method

  • +
  • lines 39 - 43 → swaps the visibility value for each of the display elements

  • +
  • lines 46 - 50 → swaps the visibility value for each of the edit elements

  • +
+
+

Finally, to call the switch_components method from the button_edit_click handler, add the highlighted code.

+
27  def button_edit_click(self, **event_args):
+28    self.text_box_subject.text = self.item["subject"]
+29    self.text_box_details.text = self.item["details"]
+30    self.date_picker_start.date = self.item["start_date"]
+31    self.date_picker_due.date = self.item["due_date"]
+32    self.switch_components()
+
+
+
+

Code explanation

+
    +
  • line 32 → called the switch_component method

  • +
+
+
+

Test the edit button

+

Launch your website and test if the edit button works. You may need to adjust the width of your columns to make everything fit.

+

edit button test

+

If you have fixed you column widths, then it is time to make the save button work.

+
+
+
+

Button save click

+

When the user clicks the Save button we want the following to happen:

+
    +
  • take the values of all the edit elements and write them the Assessments table

  • +
  • take the values from the edit elements and write them to the display elements

  • +
  • make all the edit elements invisible

  • +
  • make all the display elements visible

  • +
+
+

assessment_update

+

To achieve the first step, we will need to call a backend function to save the values to the Assessments table. So open the assessments_service, and add the highlighted code to the end.

+
26@anvil.server.callable
+27def update_assessment_completed(assessment_id, completed):
+28  assessment = app_tables.assessments.get_by_id(assessment_id)
+29  if assessment:
+30    assessment["completed"] = completed
+31
+32@anvil.server.callable
+33def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+34    assessment = app_tables.assessments.get_by_id(assessment_id)
+35    if assessment:
+36        assessment['subject'] = subject
+37        assessment['details'] = details
+38        assessment['start_date'] = start_date
+39        assessment['due_date'] = due_date
+40        assessment['completed'] = completed
+
+
+
+

Code explanation

+
    +
  • line 26 → makes this function callable by the frontend

  • +
  • line 27 → create the update_assessments function which requires the following data to be passed:

    +
      +
    • assessment_id

    • +
    • subject

    • +
    • details

    • +
    • start_date

    • +
    • due_date

    • +
    • completed

    • +
    +
  • +
  • line 34 → loads the assessment that has the passed assessment_id

  • +
  • line 35 → checks to see if there is an assessment with the passed assessment_id

  • +
  • lines 36 - 40 → overwrites the stored values of the assessment with the values passed

  • +
+
+

Now that we have our backend function to call, we can work on the button_save_click handler.

+
+
+

button_save_click

+

Open the AssessmentPanel in code mode.

+

First thing we will do is write the values to the Assessments table. Go to the button_save_click handler and replace lines 35 and 36 with the highlighted code below:

+
34  def button_save_click(self, **event_args):
+35    # write to server
+36    anvil.server.call('update_assessment', 
+37                     self.item.get_id(),
+38                     self.text_box_subject.text,
+39                     self.text_box_details.text,
+40                     self.date_picker_start.date,
+41                     self.date_picker_due.date,
+42                     self.check_box_completed.checked)
+
+
+
+

Code explanation

+
    +
  • line 35 → a comment to help structure the method

  • +
  • lines 36 - 42 → calls the function we just created in the assessment_service. The first argument needs to be the name of the function being called. The other arguments are the date the function needs passed.

  • +
  • line 37 → gets the id of the assessment for this specific panel.

  • +
+
+

Next we need to take all the values from the edit elements and write them to the display elements, so the display reflects the changes made.

+

Add the highlighted code to the end of the button_save_click event handler.

+
34  def button_save_click(self, **event_args):
+35    # write to server
+36    anvil.server.call('update_assessment', 
+37                     self.item.get_id(),
+38                     self.text_box_subject.text,
+39                     self.text_box_details.text,
+40                     self.date_picker_start.date,
+41                     self.date_picker_due.date,
+42                     self.check_box_completed.checked)
+43
+44    # update display
+45    self.label_subject.text = self.text_box_subject.text
+46    self.label_details.text = self.text_box_details.text
+47    self.label_start.text = self.date_picker_start.date.strftime('%d/%m/%Y')
+48    self.label_due.text = self.date_picker_due.date.strftime('%d/%m/%Y')
+
+
+
+

Code explanation

+
    +
  • line 44 → a comment to help structure the method

  • +
  • line 45 → assigns the value in the subject text box to the subject label

  • +
  • line 46 → assigns the value in the details text box to the details label

  • +
  • line 47 → assigns the date in the start date picker to the start label

  • +
  • line 48 → assigns the date in the due date picker to the due label

  • +
+
+

Finally, we need to turn all the edit elements invisible and all the display elements visible. Luckily we created a method that swaps all those elements visibility values, so we can just call that.

+
34  def button_save_click(self, **event_args):
+35    # write to server
+36    anvil.server.call('update_assessment', 
+37                     self.item.get_id(),
+38                     self.text_box_subject.text,
+39                     self.text_box_details.text,
+40                     self.date_picker_start.date,
+41                     self.date_picker_due.date,
+42                     self.check_box_completed.checked)
+43
+44    # update display
+45    self.label_subject.text = self.text_box_subject.text
+46    self.label_details.text = self.text_box_details.text
+47    self.label_start.text = self.date_picker_start.date.strftime('%d/%m/%Y')
+48    self.label_due.text = self.date_picker_due.date.strftime('%d/%m/%Y')
+49    self.switch_components()
+
+
+
    +
  • line 49 → call the switch component method to reverse the visibility of the edit and display elements.

  • +
+
+
+
+
+

Testing

+

You should be able to change the values of your assessments. Launch your webapp, and try to change the details. Make sure that you test all values (subject, details, start date and due date).

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

AssessmentPanel

+
 1from ._anvil_designer import AssessmentPanelTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.users
+ 5import anvil.tables as tables
+ 6import anvil.tables.query as q
+ 7from anvil.tables import app_tables
+ 8import datetime
+ 9
+10
+11class AssessmentPanel(AssessmentPanelTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    self.check_box_completed.checked = self.item['completed']
+18    self.label_subject.text = self.item['subject']
+19    self.label_details.text = self.item['details']
+20    self.label_start.text = self.item['start_date'].strftime('%d/%m/%Y')
+21    self.label_due.text = self.item['due_date'].strftime('%d/%m/%Y')
+22
+23  def check_box_completed_change(self, **event_args):
+24    new_value = self.check_box_completed.checked
+25    anvil.server.call('update_assessment_completed', self.item.get_id(),new_value)
+26
+27  def button_edit_click(self, **event_args):
+28    self.text_box_subject.text = self.item["subject"]
+29    self.text_box_details.text = self.item["details"]
+30    self.date_picker_start.date = self.item["start_date"]
+31    self.date_picker_due.date = self.item["due_date"]
+32    self.switch_components()
+33
+34  def button_save_click(self, **event_args):
+35    # write to server
+36    anvil.server.call('update_assessment', 
+37                     self.item.get_id(),
+38                     self.text_box_subject.text,
+39                     self.text_box_details.text,
+40                     self.date_picker_start.date,
+41                     self.date_picker_due.date,
+42                     self.check_box_completed.checked)
+43
+44    # update display
+45    self.label_subject.text = self.text_box_subject.text
+46    self.label_details.text = self.text_box_details.text
+47    self.label_start.text = self.date_picker_start.date.strftime('%d/%m/%Y')
+48    self.label_due.text = self.date_picker_due.date.strftime('%d/%m/%Y')
+49    self.switch_components()
+50  
+51  def switch_components(self):
+52    # display elements
+53    self.label_subject.visible = not self.label_subject.visible
+54    self.label_details.visible = not self.label_details.visible
+55    self.label_start.visible = not self.label_start.visible
+56    self.label_due.visible = not self.label_due.visible
+57    self.button_edit.visible = not self.button_edit.visible
+58    
+59    # edit elements
+60    self.text_box_subject.visible = not self.text_box_subject.visible
+61    self.text_box_details.visible = not self.text_box_details.visible
+62    self.date_picker_start.visible = not self.date_picker_start.visible
+63    self.date_picker_due.visible = not self.date_picker_due.visible
+64    self.button_save.visible = not self.button_save.visible
+
+
+
+
+

assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6
+ 7@anvil.server.callable
+ 8def add_assessment(subject, details, start_date, due_date):
+ 9  user = anvil.users.get_user()
+10  
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+17
+18@anvil.server.callable
+19def get_assessment():
+20  user = anvil.users.get_user()
+21
+22  return app_tables.assessments.search(tables.order_by('due_date'),
+23                                      user=user,
+24                                      completed=False)
+25
+26@anvil.server.callable
+27def update_assessment_completed(assessment_id, completed):
+28  assessment = app_tables.assessments.get_by_id(assessment_id)
+29  if assessment:
+30    assessment["completed"] = completed
+31
+32@anvil.server.callable
+33def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+34    assessment = app_tables.assessments.get_by_id(assessment_id)
+35    if assessment:
+36        assessment['subject'] = subject
+37        assessment['details'] = details
+38        assessment['start_date'] = start_date
+39        assessment['due_date'] = due_date
+40        assessment['completed'] = completed
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/28_calendar_cmpt.html b/28_calendar_cmpt.html new file mode 100644 index 0000000..e5f8439 --- /dev/null +++ b/28_calendar_cmpt.html @@ -0,0 +1,398 @@ + + + + + + + + + + CalendarComponent Layout - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

CalendarComponent Layout

+ +

The calendar component of our web app is actually a specific chart called a Gantt chart. We are going to use the Anvil plotting mechanism to create it.

+

Our current CalendarComponentLayout already has a card to contain our details, so all we have to do is add the plot.

+
+

Add Plot to Calendar

+

Let add our plot

+
    +
  1. Open CalendarComponentLayout in Design mode.

  2. +
  3. Find the Plot component

  4. +
  5. Add it to self.card_details

  6. +
  7. Rename the plot to self.plot_timeline

  8. +
+

add plot

+

That’s all done. Let’s add the code.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/28a_calendar_cmpt_code.html b/28a_calendar_cmpt_code.html new file mode 100644 index 0000000..657f55a --- /dev/null +++ b/28a_calendar_cmpt_code.html @@ -0,0 +1,861 @@ + + + + + + + + + + CalendarComponent Code - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

CalendarComponent Code

+ +

A note before we start. In this tutorial we will be using features of two libraries beyond the scope of course.

+
    +
  • Pandas - a fast and flexible Python library for data manipulation and analysis

  • +
  • Plotly - a versatile Python library for creating interactive, web-based data visualizations

  • +
+

Plotly is the library that supports Anvil’s plots, while Pandas provides the computing backbone for Plotly. We will only be scraping the surface of both of these libraries, so their functions will not be explained in detail.

+

If you wish to know more about these libraries, then you can do so through the following tutorials:

+ +
+

Plan

+

The nature of Plotly means that we will be creating the chart on the backend, and then sending it to frontend to be displayed. Therefore the majority of our code will be written in the assessment_service module.

+

We will need to create a get_chart method that:

+
    +
  • retrieve details from the assessments table

  • +
  • create a dataframe for the assessment details

  • +
  • use Plotly to create a timeline chart from the dataframe

  • +
  • send the chart back to the frontend

  • +
+
+

Dataframes

+

DataFrames are digital tables where Pandas stores, organises, and works with data, just like you would with rows and columns in a spreadsheet.

+
+

Now we have worked out what we need to do, we can start coding.

+
+
+

Coding

+
+

get_chart

+

To code the get_chart method we need to:

+
    +
  1. Open the assessment_service server module

  2. +
  3. add the highlighted code to the import section

  4. +
+
1import anvil.users
+2import anvil.tables as tables
+3import anvil.tables.query as q
+4from anvil.tables import app_tables
+5import anvil.server
+6import plotly.express as px
+7import pandas as pd
+
+
+
+

Code explanation

+
    +
  • line 6 → imports the plotly.express library and assigns it the px alias

  • +
  • line 7 → imports the pandas library and assigns it the pd alias

  • +
+
+
+

Import aliases

+

Import aliases in Python allow you to import a module or a function using a shorter or more convenient name, making your code easier to write and read.

+
+
    +
  1. move to the bottom of the assessments_service

  2. +
  3. the highlighted code to retrieve the necessary data

  4. +
+
44@anvil.server.callable
+45def get_chart():
+46    # Fetch assessments from the data table
+47    user = anvil.users.get_user()
+48    assessments = app_tables.assessments.search(tables.order_by('due_date'),
+49                                       user=user,
+50                                       completed=False)
+
+
+
+

Code explanation

+
    +
  • line 44 → makes get_chart callable from the frontend

  • +
  • line 45 → creates the get_chart method

  • +
  • line 47 → gets the current user

  • +
  • line 48 → retrieves all the outstanding assessments and stores them in the assessments variable

  • +
+
+
    +
  1. to process the data, add the code highlighted below

  2. +
+
44@anvil.server.callable
+45def get_chart():
+46    # Fetch assessments from the data table
+47    user = anvil.users.get_user()
+48    assessments = app_tables.assessments.search(tables.order_by('due_date'),
+49                                       user=user,
+50                                       completed=False)
+51    
+52    # Create a DataFrame from the assessments data
+53    data = []
+54    for assessment in assessments:
+55        data.append({
+56            "Subject": assessment['subject'],
+57            "Details": assessment['details'],
+58            "Start": assessment['start_date'],
+59            "Due": assessment['due_date']
+60        })
+61    
+62    df = pd.DataFrame(data)
+
+
+
+

Code explanation

+
    +
  • line 53 → creates the data list to be used for processing the data

  • +
  • line 54 → iterates over all the assessments stored in the assessments variable

  • +
  • line 55 - 60 → creates a dictionary of each assessment and then adds this to the data list

  • +
  • line 62 → converts all the assessment dictionaries in a dataframe

  • +
+
+
    +
  1. add the highlighted code to create the chart

  2. +
+
44@anvil.server.callable
+45def get_chart():
+46    # Fetch assessments from the data table
+47    user = anvil.users.get_user()
+48    assessments = app_tables.assessments.search(tables.order_by('due_date'),
+49                                       user=user,
+50                                       completed=False)
+51    
+52    # Create a DataFrame from the assessments data
+53    data = []
+54    for assessment in assessments:
+55        data.append({
+56            "Subject": assessment['subject'],
+57            "Details": assessment['details'],
+58            "Start": assessment['start_date'],
+59            "Due": assessment['due_date']
+60        })
+61    
+62    df = pd.DataFrame(data)
+63    
+64    # Create the Gantt chart using Plotly
+65    fig = px.timeline(df, 
+66                      x_start="Start", 
+67                      x_end="Due", 
+68                      y="Subject", 
+69                      text="Details", 
+70                      title="Assessment Schedule"
+71                     )
+72    
+73    return fig
+
+
+
+

Code explanation

+
    +
  • lines 65 - 71 → creates the timeline chart and stores it in the variable fig

    +
      +
    • df → the dataframe for the chart

    • +
    • x_start=”Start” → identifies the dataframe column which will be used at the starting point for each bar

    • +
    • x_end=”Due” → identifies the dataframe column which will be used at the end point for each bar

    • +
    • y=”Subject” → provide the column that will be the y axis

    • +
    • text=”Details” → labels each bar with the details

    • +
    +
  • +
  • line 73 → send the chart to the frontend

  • +
+
+
+
+

CalendarComponent

+

Now that we have a backend method that provides the chart, we need to call that from the CalendarComponent:

+
    +
  1. Open calendarComponent in the Code mode.

  2. +
  3. Add the highlighted code to create the load_chart method

  4. +
+
24  def load_chart(self):
+25    fig = anvil.server.call('get_chart')
+26        
+27    # Assign the Plotly figure to the Anvil Plot component
+28    self.plot_timeline.figure = fig
+
+
+
+

Code explanation

+
    +
  • line 24 → creates the load_chart method

  • +
  • line 25 → gets the chart by calling the get_chart backend function we just created

  • +
  • line 28 → displays the chart on the self.plot_timeline

  • +
+
+
    +
  1. Finally we need to call the load_chart method so it displays when the component is loaded

  2. +
  3. Go to the bottom of the __init__ method and had the highlighted line

  4. +
+
12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    if anvil.users.get_user():
+18      self.card_details.visible = True
+19      self.card_error.visible = False
+20      self.load_chart()
+21    else:
+22      self.card_details.visible = False
+23      self.card_error.visible = True
+
+
+
+

Code explanation

+
    +
  • line 20 → runs the load_chart method when the Calendar Component is loaded.

  • +
+
+
+
+

Testing

+

Time to test all that code. Launch your web app and navigate to the Calendar page. You should see something similar to below.

+

test

+

If you look closely, there are a couple of problems with our chart.

+
    +
  1. There is no bar for the English exam

  2. +
  3. The y-axis title (subject) is unnecessary

  4. +
+

test

+

So lets fix those two.

+
+
+

Exams not appearing

+

The reason that exams do not appear on the chart, is that the start date and the due date are the same. The timeline chart plots the time between the start and finish, and in the case of exams, this is nothing.

+

The easiest way to solve this problem is to move the due date back one day. But be careful, we only want to do this for exams, ie. where the due date is the same as the start date.

+

Lets implement this:

+
    +
  1. Open the assessment_service server module

  2. +
  3. Add the highlighted code inside the dataframe creation code

  4. +
+
52    # Create a DataFrame from the assessments data
+53    data = []
+54    for assessment in assessments:
+55        # adjust for exams
+56        start_date = assessment['start_date']
+57        due_date = assessment['due_date']
+58                
+59        if start_date == due_date:
+60            due_date += pd.Timedelta(days=1)
+61      
+62        data.append({
+63            "Subject": assessment['subject'],
+64            "Details": assessment['details'],
+65            "Start": assessment['start_date'],
+66            "Due": assessment['due_date']
+67        })
+68    
+69    df = pd.DataFrame(data)
+
+
+
+

Code explanation

+
    +
  • line 56 → stores the start date in the start_date variable

  • +
  • line 57 → stores the due date in the due_date variable

  • +
  • line 59 → checks to see if the start date and due date are the same

  • +
  • line 60 → uses Pandas’ Timedelta method to add one day to the due_date

  • +
+
+

Now we have adjusted the start and due dates, we need to put these values into the diction before it is added to the data list.

+
    +
  1. Adjust the highlighted line of code

  2. +
+
52    # Create a DataFrame from the assessments data
+53    data = []
+54    for assessment in assessments:
+55        # adjust for exams
+56        start_date = assessment['start_date']
+57        due_date = assessment['due_date']
+58                
+59        if start_date == due_date:
+60            due_date += pd.Timedelta(days=1)
+61      
+62        data.append({
+63            "Subject": assessment['subject'],
+64            "Details": assessment['details'],
+65            "Start": assessment['start_date'],
+66            "Due": due_date
+67        })
+68    
+69    df = pd.DataFrame(data)
+
+
+
+

Code explanation

+
    +
  • line 66 → add our adjusted due_date to the dataframe

  • +
+
+
+

Testing Exams

+

Launch your web app and navigate to the calendar page (note: make sure you have added an exam assessment). You should now see your exams.

+

exam test

+
+
+
+

y-axis title

+

Plotly provides a wide range of formatting options. These options can be found in the Anvil documentation or the Plotly documentation.

+

We will only be concerned with removing the y-axis title.

+
    +
  1. Open the assessment_service

  2. +
  3. At the bottom of the creating Gantt chart section add the highlighted code.

  4. +
+
71    # Create the Gantt chart using Plotly
+72    fig = px.timeline(df, 
+73                      x_start="Start", 
+74                      x_end="Due", 
+75                      y="Subject", 
+76                      text="Details", 
+77                      title="Assessment Schedule"
+78                     )
+79
+80    fig.update_yaxes(title_text="") 
+81    
+82    return fig
+
+
+
+

Code explanation

+
    +
  • line 80 → makes the y-axis blank

  • +
+
+
+

Test the y-axis

+

Once again launch you web app and check that the y-axis title has gone.

+

test y-axis

+
+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final CalendarComponent

+
 1from ._anvil_designer import CalendarComponentTemplate
+ 2from anvil import *
+ 3import plotly.graph_objects as go
+ 4import anvil.server
+ 5import anvil.tables as tables
+ 6import anvil.tables.query as q
+ 7from anvil.tables import app_tables
+ 8import anvil.users
+ 9
+10
+11class CalendarComponent(CalendarComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    if anvil.users.get_user():
+18      self.card_details.visible = True
+19      self.card_error.visible = False
+20      self.load_chart()
+21    else:
+22      self.card_details.visible = False
+23      self.card_error.visible = True
+24
+25  def load_chart(self):
+26    fig = anvil.server.call('get_chart')
+27        
+28    # Assign the Plotly figure to the Anvil Plot component
+29    self.plot_timeline.figure = fig
+
+
+
+
+

Final assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6import plotly.express as px
+ 7import pandas as pd
+ 8
+ 9@anvil.server.callable
+10def add_assessment(subject, details, start_date, due_date):
+11  user = anvil.users.get_user()
+12  
+13  app_tables.assessments.add_row(user= user,
+14                                 subject= subject,
+15                                 details=details,
+16                                 start_date=start_date,
+17                                 due_date=due_date,
+18                                 completed=False)
+19
+20@anvil.server.callable
+21def get_assessment():
+22  user = anvil.users.get_user()
+23
+24  return app_tables.assessments.search(tables.order_by('due_date'),
+25                                      user=user,
+26                                      completed=False)
+27
+28@anvil.server.callable
+29def update_assessment_completed(assessment_id, completed):
+30  assessment = app_tables.assessments.get_by_id(assessment_id)
+31  if assessment:
+32    assessment["completed"] = completed
+33
+34@anvil.server.callable
+35def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+36    assessment = app_tables.assessments.get_by_id(assessment_id)
+37    if assessment:
+38        assessment['subject'] = subject
+39        assessment['details'] = details
+40        assessment['start_date'] = start_date
+41        assessment['due_date'] = due_date
+42        assessment['completed'] = completed
+43
+44@anvil.server.callable
+45def get_chart():
+46    # Fetch assessments from the data table
+47    user = anvil.users.get_user()
+48    assessments = app_tables.assessments.search(tables.order_by('due_date'),
+49                                       user=user,
+50                                       completed=False)
+51    
+52    # Create a DataFrame from the assessments data
+53    data = []
+54    for assessment in assessments:
+55        # adjust for exams
+56        start_date = assessment['start_date']
+57        due_date = assessment['due_date']
+58                
+59        if start_date == due_date:
+60            due_date += pd.Timedelta(days=1)
+61      
+62        data.append({
+63            "Subject": assessment['subject'],
+64            "Details": assessment['details'],
+65            "Start": assessment['start_date'],
+66            "Due": due_date
+67        })
+68    
+69    df = pd.DataFrame(data)
+70    
+71    # Create the Gantt chart using Plotly
+72    fig = px.timeline(df, 
+73                      x_start="Start", 
+74                      x_end="Due", 
+75                      y="Subject", 
+76                      text="Details", 
+77                      title="Assessment Schedule"
+78                     )
+79
+80    fig.update_yaxes(title_text="") 
+81    
+82    return fig
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/29_welcome.html b/29_welcome.html new file mode 100644 index 0000000..d3325db --- /dev/null +++ b/29_welcome.html @@ -0,0 +1,506 @@ + + + + + + + + + + Welcome Page - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Welcome Page

+ +

In this tutorial we will create a landing page for our web app, so that new users (who do not have an account) can see what the app is about.

+
+

Plan

+

To achieve this we need to:

+
    +
  • format our WelcomeComponent as our landing page

  • +
+

This is all pretty straight forward. So lets get to it.

+
+
+

Layout

+
+

Main card

+

We need to setup the WelcomeComponent.

+
    +
  1. Open the WelcomeComponent in Design mode.

  2. +
  3. Change the title to Welcome to StudyM8.

  4. +
  5. Change the title’s font to bold.

  6. +
  7. Make the title bigger. I chose 34, but you can choose whatever you want.

  8. +
+

change title

+
    +
  1. Next we need to add a Card (Outline Card)

  2. +
+

add card

+

Now we will add an image to the welcome page

+
    +
  1. Right mouse click and save the image below.

  2. +
+

welcome image

+
    +
  1. Find the Image element and add it to the card.

  2. +
  3. In the Properties look for source and click the upload button to the right of the text box.

  4. +
  5. Select the welcome_image.png you just downloaded.

  6. +
  7. Use the handles to resize the image to your desired size.

  8. +
+

add welcome image

+

Next we want to add a the following text:

+
Tired of being stressed to the max every assessment period? Flatten your academic curve, by planning your work around your assessment dates.
+
+
+
    +
  1. Add a label to the card under the image

  2. +
  3. Copy the text above and paste it into the text property

  4. +
  5. Change the text formatting so that you are happy with it.

  6. +
+

add text

+
+
+

Features

+

Now we want advertise the web app features at the bottom of the card

+
    +
  1. Add another card element under the last label.

  2. +
  3. Change the new card’s formatting to make it contrast with the background card (I chose to change it’s background colour).

  4. +
  5. Add a Column Panel to the new card.

  6. +
+

add second card

+

First we will add the images. So save the following images to your computer

+

calendar icon

+

graph icon

+

checklist icon

+
    +
  1. Add a Image element to the column panel

  2. +
  3. Click on source the upload icon

  4. +
  5. Select calendar_icon.png

  6. +
  7. Repeat for the graph_icon.png and the checklist_icon.png

  8. +
+

adding images

+

Next we need to add text below each image.

+
    +
  1. Add a label below the calendar icon

  2. +
  3. Paste the following text into the text property

  4. +
+
Record your assessment details
+
+
+
    +
  1. Add a label below the graph icon

  2. +
  3. Paste the following text into the text property

  4. +
+
Identify times with high assessment demands
+
+
+
    +
  1. Add a label below the checklist icon

  2. +
  3. Paste the following text into the text property

  4. +
+
See all future assessments and when they are due
+
+
+

add feature text

+
+
+

Call to action

+

Finally we want to put a call-to-action at the bottom.

+
    +
  1. Add a label at the bottom under all the cards

  2. +
  3. Add the text below to the label

  4. +
+
Register to get started
+
+
+
    +
  1. Centre the text

  2. +
  3. Change the font_size

  4. +
+

call to action

+
+
+
+

Testing

+

Launch your website, and logout. You should now see the Welcome Page.

+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/30_optimisation.html b/30_optimisation.html new file mode 100644 index 0000000..e37b3fc --- /dev/null +++ b/30_optimisation.html @@ -0,0 +1,519 @@ + + + + + + + + + + Optimisation - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Optimisation

+ +

We now have a functioning web app. Congratulations, at this point you have all the skills you need to create a website in Anvil. The rest of these tutorials are about improving the web app. In particular we are going to speed it up.

+

You will notice that our webapp is not very responsive. Navigating between pages often shows a process swirl as the component loads. This is called latency and it is not ideal. People are used to websites loading quickly, so we need to do something about that.

+
+

Causes of Latency

+

There can be a wide range of causes for latency, involving networks, and service providers, but these are beyond our control. We are going to focus on the latency that is caused by the architecture of our code.

+
+

Remote Database Access

+

Remember talking about frontend and backend architecture?

+

frontend backend

+

Remember we also discussed that the slowest link in this process is the communication between the Frontend and the Backend? That’s why we created a single-page application, to minimise the amount for data being transferred between the our two halves.

+

Keeping all that in mind let’s look at our code.

+

Below is the code for the MainForm. Notice the highlighted code.

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13from ..WelcomeComponent import WelcomeComponent
+14
+15
+16class MainForm(MainFormTemplate):
+17  def __init__(self, **properties):
+18    # Set Form properties and Data Bindings.
+19    self.init_components(**properties)
+20    self.breadcrumb_stem = self.label_title.text
+21
+22    # Any code you write here will run before the form opens.
+23    self.switch_component("home")
+24
+25  def switch_component(self, state):
+26    # set state
+27    if state == "home":
+28      if anvil.users.get_user():
+29        cmpt = HomeComponent()
+30      else:
+31        cmpt = WelcomeComponent()
+32      breadcrumb = self.breadcrumb_stem
+33    elif state == "account":
+34      cmpt = AccountComponent()
+35      breadcrumb = self.breadcrumb_stem + " - Account"
+36    elif state == "add":
+37      cmpt = AddComponent()
+38      breadcrumb = self.breadcrumb_stem + " - Add"
+39    elif state == "calendar":
+40      cmpt = CalendarComponent()
+41      breadcrumb = self.breadcrumb_stem + " - Calendar"
+42    elif state == "details":
+43      cmpt = SetDetailsComponent()
+44      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
+45    
+46    # execution
+47    self.content_panel.clear()
+48    self.content_panel.add_component(cmpt)
+49    self.label_title.text = breadcrumb
+50    self.set_active_link(state)
+51  
+52  def set_active_link(self, state):
+53    if state == "home":
+54      self.link_home.role = "selected"
+55    else:
+56      self.link_home.role = None
+57    if state == "add":
+58      self.link_add.role = "selected"
+59    else:
+60      self.link_add.role = None
+61    if state == "calendar":
+62      self.link_calendar.role = "selected"
+63    else:
+64      self.link_calendar.role = None
+65
+66    self.link_register.visible = not anvil.users.get_user()
+67    self.link_login.visible = not anvil.users.get_user()
+68    self.link_account.visible = anvil.users.get_user()
+69    self.link_logout.visible = anvil.users.get_user()
+70  
+71  # --- link handlers
+72  def link_home_click(self, **event_args):
+73    self.switch_component("home")
+74
+75  def link_calendar_click(self, **event_args):
+76    self.switch_component("calendar")
+77
+78  def link_add_click(self, **event_args):
+79    self.switch_component("add")
+80
+81  def link_account_click(self, **event_args):
+82    """This method is called when the link is clicked"""
+83    self.switch_component("account")
+84
+85  def link_register_click(self, **event_args):
+86    anvil.users.signup_with_form(allow_cancel=True)
+87    self.switch_component("details")
+88
+89  def link_login_click(self, **event_args):
+90    anvil.users.login_with_form(allow_cancel=True)
+91    self.switch_component("home")
+92
+93  def link_logout_click(self, **event_args):
+94    anvil.users.logout()
+95    self.switch_component("home")
+
+
+

Each call to anvil.users.get_user() is a communication between the frontend and backend of our web app. It’s not just here. If you do a search for anvil.users.get_user() you will notice that we also call it in four other frontend modules.

+

search results

+

So there are a heap of times when the frontend is communicating with the backend. What’s more, most of these are unnecessary. It is not as if the user logs in on every page. This means that the frontend is repeatedly requesting the same information from the backend.

+

So our first step in optimising our code is to reduce the number of times the frontend remotely accesses the database on the backend.

+
+
+

Local Database Access

+

There is another way we can speed our web app up. We’ve already established that network communication is the slowest link in our chain. The next slowest link is retrieving data from the database, because it lives on a hard drive.

+

Returning to our search, we will notice that the server modules also calls to anvil.users.get_user() four times. Although, not as expensive as a remote access to the database, it will still slow the website down.

+

search server

+

So how do we improve this?

+
+
+

Caching data

+

The answer is to cache data. Caching is a way of temporarily storing data so that it can be accessed more quickly the next time it’s needed. The first time we make a call to the database, the returned data should be stored in a variable so it can be quickly reused when needed. The only time that the database is accessed is when that data has changed.

+

The principle of caching is this: store frequently used data in the fastest place. For our website that means in frontend variables.

+

The rest of the tutorials will show how we will apply this principle to our website.

+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/31_remote_database.html b/31_remote_database.html new file mode 100644 index 0000000..5b2ee37 --- /dev/null +++ b/31_remote_database.html @@ -0,0 +1,1082 @@ + + + + + + + + + + Reduce Remote Database Access - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Reduce Remote Database Access

+ +

To implement caching in our web app we need to create a variables in the frontend to store our data variables. These data will need to be accessed by multiple frontend modules. Best practice for storing cached data is in it’s own module. So that’s what we will do.

+
+

Create Data Access Module

+

We have previously made a server module, but this module will be made client-side, since the data needs to be stored in the frontend. To be precise, it will be stored in the user’s browser.

+
    +
  1. In the files menu

  2. +
  3. Click on the Client Code ellipses

  4. +
  5. Then choose Add Module

  6. +
  7. Name the module data_access

  8. +
+

create module

+
    +
  1. Clean up the module by deleting everything except the imports.

  2. +
+

delete

+
+

Cached User

+

We can now cache the user data. The idea is the front end will ask this module for the user data. If the data is saved in the variable, this module will return it. If there is no data in the variable, this module will retrieve it from the database, store the data for later user and then return the user data to the frontend.

+

First we need to create a variable to store the user data.

+
    +
  1. copy the code below into the data_access module.

  2. +
+
7# cached values
+8__user = None
+
+
+
+

Code explanation

+
    +
  • line 7 → a comment to help structure the code

  • +
  • line 8 → creates the variable we will use to store the user data

  • +
+
+
+

Dunder variables

+

In Python, we use double underscores before a variable (like __variable) for variables that can meant to be only used by the module they are in. This is called making them variable private to the module it’s in.

+

Doing this means the variable cannot be easily accessed or changed from outside the module. This is important because it makes sure that important parts of your code can’t be accidentally messed up.

+
+

Now we need to make a function that the frontend will use instead of anvil.users.get_user().

+
    +
  1. Add the highlighted code to the bottom of the data_access module.

  2. +
+
10def the_user():
+11  global __user
+12
+13  if __user:
+14    print("Using cached user")
+15    return __user
+16
+17  print("Accessing user from database")
+18  __user = anvil.users.get_user()
+19  return __user
+
+
+
+

Code explanation

+
    +
  • line 10 → creates the function the_user

  • +
  • line 11 → makes the __user global, which allows this function to edit it

  • +
  • line 13 → checks to see if there is any data stored in __user. Note __user starts with the value None which is False

  • +
  • line 14 → lets us know when the program is using cached user data

  • +
  • line 15 → returns the user data to the frontend (this will end the function if there is user data)

  • +
  • line 17 → lets us know when the program is retrieving user data from the backend

  • +
  • line 18 → gets current user data and stores it in the __user variable

  • +
  • line 19 → returns the user data to the frontend

  • +
+
+

We now have our caching user function, next we have to add it to our code

+
+
+
+

Refactor Code

+

Remember when we created the switch_component and we searched for code and replaced it. We will use that process now.

+
    +
  1. Click on the search icon in the side menu

  2. +
  3. Search for anvil.users.get_user()

  4. +
+

search

+

You can see that five components have a call to anvil.users.get_user(). lets work through those from top to bottom.

+
+

AccountComponent

+

First component on our list is the AccountComponent.

+
    +
  1. Open the AccountComponent in Code mode

  2. +
  3. In the import section add the following highlighted line

  4. +
+
1from ._anvil_designer import AccountComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from .. import data_access
+
+
+
+

Code explanation

+
    +
  • line 8 → allows AccountComponent to use the data_access module

  • +
+
+

We need to get rid of the anvil.users.get_user() in line 17

+
    +
  1. In the __init__ change line 17 to the highlighted code below

  2. +
+
12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    user = data_access.the_user()
+18    self.label_first_name.text = user["first_name"]
+19    self.label_last_name.text = user["last_name"]
+
+
+
+

Code explanation

+
    +
  • line 17 → uses our new the_user function to retrieve user data

  • +
+
+

Check that the AccountComponent is no longer in the search results, and then move onto the next one.

+
+
+

AddComponent

+

Next in our search results is the AddComponent

+
    +
  1. Open AddComponent in Code mode.

  2. +
  3. Add data_access to the import section

  4. +
+
1from ._anvil_designer import AddComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from .. import data_access
+
+
+
+

Code explanation

+
    +
  • line 8 → allows AccountComponent to use the data_access module

  • +
+
+

We need to get rid of the anvil.users.get_user() in line 22

+
    +
  1. In the __init__ change line 22 to the highlighted code below

  2. +
+
12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15    self.subject = ""
+16    self.details = ""
+17    self.start = None
+18    self.due = None
+19
+20    # Any code you write here will run before the form opens.
+21    self.label_message.visible = False
+22    if data_access.the_user():
+23      self.card_details.visible = True
+24      self.card_error.visible = False
+25      self.button_add.visible = True
+26    else:
+27      self.card_details.visible = False
+28      self.card_error.visible = True
+29      self.button_add.visible = False
+
+
+
+

Code explanation

+
    +
  • line 22 → uses our new the_user function to retrieve user data

  • +
+
+

Check the AddComponent is no longer in your search results, then onto the next one.

+
+
+

CalendarComponent

+

The next module we need to change is the CalendarComponent.

+
    +
  1. Open CalendarComponent in Code mode.

  2. +
  3. Add data_access to the import section

  4. +
+
1from ._anvil_designer import CalendarComponentTemplate
+2from anvil import *
+3import plotly.graph_objects as go
+4import anvil.server
+5import anvil.tables as tables
+6import anvil.tables.query as q
+7from anvil.tables import app_tables
+8import anvil.users
+9from .. import data_access
+
+
+
+

Code explanation

+
    +
  • line 9 → allows AccountComponent to use the data_access module

  • +
+
+

Once again we need to remove of the anvil.users.get_user() from the __init__

+
    +
  1. In the __init__ change line 18 to the highlighted code below

  2. +
+
13  def __init__(self, **properties):
+14    # Set Form properties and Data Bindings.
+15    self.init_components(**properties)
+16
+17    # Any code you write here will run before the form opens.
+18    if data_access.the_user():
+19      self.card_details.visible = True
+20      self.card_error.visible = False
+21      self.load_chart()
+22    else:
+23      self.card_details.visible = False
+24      self.card_error.visible = True
+
+
+
+

Code explanation

+
    +
  • line 18 → uses our new the_user function to retrieve user data

  • +
+
+

If CalendarComponent is no longer in your search results time to move on.

+
+
+

SetDetailsComponent

+

I know that MainForm is next in our search results, but we’ll come back to that, since SetDetailsComponent is pretty much the same as the all the other components.

+
    +
  1. Open SetDetailsComponent in Code mode.

  2. +
  3. Add data_access to the import section

  4. +
+
1from ._anvil_designer import SetDetailsComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from .. import data_access
+
+
+
+

Code explanation

+
    +
  • line 8 → allows AccountComponent to use the data_access module

  • +
+
+

Remove of the anvil.users.get_user() from the __init__

+
    +
  1. In the __init__ change line 17 to the highlighted code below

  2. +
+
12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    user = data_access.the_user()
+18    if user["first_name"]:
+19      self.text_box_first_name.text = user["first_name"]
+20    if user["last_name"]:
+21      self.text_box_last_name.text = user["last_name"]
+
+
+
+

Code explanation

+
    +
  • line 17 → uses our new the_user function to retrieve user data

  • +
+
+

Now, the only form in our search should be MainForm, so time to move onto that.

+
+
+

MainForm

+

In the MainForm we have a number of instances of anvil.users.get_user(). We will have to replace each one, but before we do, we need to import data_access

+
    +
  1. Open MainForm in Code mode

  2. +
  3. Add the highlighted code to the end of the import section

  4. +
+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13from ..WelcomeComponent import WelcomeComponent
+14from .. import data_access
+
+
+
+

Code explanation

+
    +
  • line 14 → allows AccountComponent to use the data_access module

  • +
+
+

Our first call to anvil.users.get_user() occurs in the switch_component method.

+
    +
  1. Change the highlighted code in the switch_component method.

  2. +
+
26  def switch_component(self, state):
+27    # set state
+28    if state == "home":
+29      if data_access.the_user():
+30        cmpt = HomeComponent()
+31      else:
+32        cmpt = WelcomeComponent()
+33      breadcrumb = self.breadcrumb_stem
+
+
+
+

Code explanation

+
    +
  • line 29 → uses our new the_user function to retrieve user data

  • +
+
+

The remaining four calls to anvil.users.get_user() are in the set_active_link method.

+
    +
  1. Change the four highlighted lines below:

  2. +
+
53  def set_active_link(self, state):
+54    if state == "home":
+55      self.link_home.role = "selected"
+56    else:
+57      self.link_home.role = None
+58    if state == "add":
+59      self.link_add.role = "selected"
+60    else:
+61      self.link_add.role = None
+62    if state == "calendar":
+63      self.link_calendar.role = "selected"
+64    else:
+65      self.link_calendar.role = None
+66
+67    self.link_register.visible = not data_access.the_user()
+68    self.link_login.visible = not data_access.the_user()
+69    self.link_account.visible = data_access.the_user()
+70    self.link_logout.visible = data_access.the_user()
+
+
+
+

Code explanation

+
    +
  • lines 67 - 70 → use our new the_user function to retrieve user data

  • +
+
+

Now, the only modules in our search results should be data_access and server modules.

+

Time to test.

+
+
+
+

Testing

+

Launch your web app and navigate to all the pages we change:

+
    +
  • Account Page

  • +
  • Add Page

  • +
  • Calendar Page

  • +
  • Set Details Page

  • +
  • Home Page

  • +
  • Logout

  • +
+

Are things notably faster?

+
+

Problems

+

You will notice that there are some problems with:

+
    +
  • Logging out

  • +
  • Updating user details

  • +
+

You will also notice that pages that load assessment data are still slow.

+

We will fix these problems in the next three tutorials.

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13from ..WelcomeComponent import WelcomeComponent
+14from .. import data_access
+15
+16
+17class MainForm(MainFormTemplate):
+18  def __init__(self, **properties):
+19    # Set Form properties and Data Bindings.
+20    self.init_components(**properties)
+21    self.breadcrumb_stem = self.label_title.text
+22
+23    # Any code you write here will run before the form opens.
+24    self.switch_component("home")
+25
+26  def switch_component(self, state):
+27    # set state
+28    if state == "home":
+29      if data_access.the_user():
+30        cmpt = HomeComponent()
+31      else:
+32        cmpt = WelcomeComponent()
+33      breadcrumb = self.breadcrumb_stem
+34    elif state == "account":
+35      cmpt = AccountComponent()
+36      breadcrumb = self.breadcrumb_stem + " - Account"
+37    elif state == "add":
+38      cmpt = AddComponent()
+39      breadcrumb = self.breadcrumb_stem + " - Add"
+40    elif state == "calendar":
+41      cmpt = CalendarComponent()
+42      breadcrumb = self.breadcrumb_stem + " - Calendar"
+43    elif state == "details":
+44      cmpt = SetDetailsComponent()
+45      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
+46    
+47    # execution
+48    self.content_panel.clear()
+49    self.content_panel.add_component(cmpt)
+50    self.label_title.text = breadcrumb
+51    self.set_active_link(state)
+52  
+53  def set_active_link(self, state):
+54    if state == "home":
+55      self.link_home.role = "selected"
+56    else:
+57      self.link_home.role = None
+58    if state == "add":
+59      self.link_add.role = "selected"
+60    else:
+61      self.link_add.role = None
+62    if state == "calendar":
+63      self.link_calendar.role = "selected"
+64    else:
+65      self.link_calendar.role = None
+66
+67    self.link_register.visible = not data_access.the_user()
+68    self.link_login.visible = not data_access.the_user()
+69    self.link_account.visible = data_access.the_user()
+70    self.link_logout.visible = data_access.the_user()
+71  
+72  # --- link handlers
+73  def link_home_click(self, **event_args):
+74    self.switch_component("home")
+75
+76  def link_calendar_click(self, **event_args):
+77    self.switch_component("calendar")
+78
+79  def link_add_click(self, **event_args):
+80    self.switch_component("add")
+81
+82  def link_account_click(self, **event_args):
+83    """This method is called when the link is clicked"""
+84    self.switch_component("account")
+85
+86  def link_register_click(self, **event_args):
+87    anvil.users.signup_with_form(allow_cancel=True)
+88    self.switch_component("details")
+89
+90  def link_login_click(self, **event_args):
+91    anvil.users.login_with_form(allow_cancel=True)
+92    self.switch_component("home")
+93
+94  def link_logout_click(self, **event_args):
+95    anvil.users.logout()
+96    self.switch_component("home")
+
+
+
+
+

Final AccountComponent

+
 1from ._anvil_designer import AccountComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from .. import data_access
+ 9
+10
+11class AccountComponent(AccountComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    user = data_access.the_user()
+18    self.label_first_name.text = user["first_name"]
+19    self.label_last_name.text = user["last_name"]
+20
+21  def button_edit_click(self, **event_args):
+22    main_form = get_open_form()
+23    main_form.switch_component("details")
+
+
+
+
+

Final AddComponent

+
 1from ._anvil_designer import AddComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from .. import data_access
+ 9
+10
+11class AddComponent(AddComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15    self.subject = ""
+16    self.details = ""
+17    self.start = None
+18    self.due = None
+19
+20    # Any code you write here will run before the form opens.
+21    self.label_message.visible = False
+22    if data_access.the_user():
+23      self.card_details.visible = True
+24      self.card_error.visible = False
+25      self.button_add.visible = True
+26    else:
+27      self.card_details.visible = False
+28      self.card_error.visible = True
+29      self.button_add.visible = False
+30
+31  def button_add_click(self, **event_args):
+32    # validation
+33    if not self.text_box_subject.text:
+34      self.display_error("Subject name needed")
+35    elif not self.text_box_details.text:
+36      self.display_error("Assessment details needed")
+37    elif not self.date_picker_start.date:
+38      self.display_error("Start date needed")
+39    elif not self.date_picker_due.date:
+40      self.display_error("Due date needed")
+41    else:
+42      self.subject = self.text_box_subject.text
+43      self.details = self.text_box_details.text
+44      self.start = self.date_picker_start.date
+45      self.due = self.date_picker_due.date
+46      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+47      anvil.server.call('add_assessment', self.subject, self.details, self.start, self.due)
+48      self.reset_form()
+49
+50  def display_error(self, message):
+51    self.label_message.visible = True
+52    self.label_message.foreground = "#ff0000"
+53    self.label_message.icon = "fa:exclamation-triangle"
+54    self.label_message.bold = True
+55    self.label_message.text = message
+56
+57  def display_save(self, message):
+58    self.label_message.visible = True
+59    self.label_message.foreground = "#000000"
+60    self.label_message.icon = "fa:save"
+61    self.label_message.bold = False
+62    self.label_message.text = message
+63
+64  def reset_form(self):
+65    self.subject = ""
+66    self.details = ""
+67    self.start = None
+68    self.due = None
+69    self.text_box_subject.text = ""
+70    self.text_box_details.text = ""
+71    self.date_picker_start.date = None
+72    self.date_picker_due.date = None
+
+
+
+
+

Final CalendarComponent

+
 1from ._anvil_designer import CalendarComponentTemplate
+ 2from anvil import *
+ 3import plotly.graph_objects as go
+ 4import anvil.server
+ 5import anvil.tables as tables
+ 6import anvil.tables.query as q
+ 7from anvil.tables import app_tables
+ 8import anvil.users
+ 9from .. import data_access
+10
+11
+12class CalendarComponent(CalendarComponentTemplate):
+13  def __init__(self, **properties):
+14    # Set Form properties and Data Bindings.
+15    self.init_components(**properties)
+16
+17    # Any code you write here will run before the form opens.
+18    if data_access.the_user():
+19      self.card_details.visible = True
+20      self.card_error.visible = False
+21      self.load_chart()
+22    else:
+23      self.card_details.visible = False
+24      self.card_error.visible = True
+25
+26  def load_chart(self):
+27    fig = anvil.server.call('get_chart')
+28        
+29    # Assign the Plotly figure to the Anvil Plot component
+30    self.plot_timeline.figure = fig
+
+
+
+
+

Final SetDetailsComponent

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from .. import data_access
+ 9
+10
+11class SetDetailsComponent(SetDetailsComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    user = data_access.the_user()
+18    if user["first_name"]:
+19      self.text_box_first_name.text = user["first_name"]
+20    if user["last_name"]:
+21      self.text_box_last_name.text = user["last_name"]
+22
+23  def button_save_click(self, **event_args):
+24    
+25    if self.text_box_first_name.text == "":
+26      self.label_error.text = "First name cannot be blank"
+27      self.label_error.visible = True
+28      return
+29
+30    if self.text_box_last_name.text == "":
+31      self.label_error.text = "Last name cannot be blank"
+32      self.label_error.visible = True
+33      return
+34
+35    self.label_error.visible = False
+36    anvil.server.call("update_user", 
+37                      self.text_box_first_name.text, 
+38                      self.text_box_last_name.text)
+39
+40    main_form = get_open_form()
+41    main_form.switch_component("account")
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/32_logout_func.html b/32_logout_func.html new file mode 100644 index 0000000..174e1f9 --- /dev/null +++ b/32_logout_func.html @@ -0,0 +1,582 @@ + + + + + + + + + + Create Logout Function - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Create Logout Function

+ +

In testing your website in the last tutorial, you would have noticed that when you logout, the website is still acting like your are logged in. This is a problem.

+
+

Planning

+

Although the website looks like it remains logged in, it is actually logged out. For example, interacting with the database through changing user details would produce an error. The reason for this discrepancy, is that the website continues to use the cached user after the user has logged out.

+

To resolve this issue we need the MainForm link_logout_click handler to:

+
    +
  1. call the anvil.users.logout() method

  2. +
  3. clear the cached user value

  4. +
+

We will create a new function in the data_access module to do this.

+
+
+

Code

+

First we need to make the new method in the data_access module.

+
    +
  1. Open the data_access module

  2. +
  3. Add the following code to the bottom of the module

  4. +
+
21def logout():
+22  global __user
+23  __user = None
+24  anvil.users.logout()
+
+
+
+

Code explanation

+
    +
  • line 21 → creates the logout function

  • +
  • line 22 → allows the function to edit the value of __user

  • +
  • line 23 → sets __user to None

  • +
  • line 24 → logs out the current user

  • +
+
+

Now we need to change the link_logout_click handler so it calls this function.

+
    +
  1. Open the MainForm in Code mode

  2. +
  3. Change line 95 to the highlighted code below

  4. +
+
94  def link_logout_click(self, **event_args):
+95    data_access.logout()
+96    self.switch_component("home")
+
+
+
+

Code explanation

+
    +
  • line 95 → calls the logout function we just made

  • +
+
+
+
+

Testing

+

Time to check if that worked.

+

Launch your website and then logout. You should be taken to the Welcome page.

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final data_access

+
 1import anvil.server
+ 2import anvil.users
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6
+ 7# cached values
+ 8__user = None
+ 9
+10def the_user():
+11  global __user
+12
+13  if __user:
+14    print("Using cached user")
+15    return __user
+16
+17  print("Accessing user from database")
+18  __user = anvil.users.get_user()
+19  return __user
+20
+21def logout():
+22  global __user
+23  __user = None
+24  anvil.users.logout()
+
+
+
+
+

Final MainForm

+
 1from ._anvil_designer import MainFormTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from ..HomeComponent import HomeComponent
+ 9from ..CalendarComponent import CalendarComponent
+10from ..AddComponent import AddComponent
+11from ..AccountComponent import AccountComponent
+12from ..SetDetailsComponent import SetDetailsComponent
+13from ..WelcomeComponent import WelcomeComponent
+14from .. import data_access
+15
+16
+17class MainForm(MainFormTemplate):
+18  def __init__(self, **properties):
+19    # Set Form properties and Data Bindings.
+20    self.init_components(**properties)
+21    self.breadcrumb_stem = self.label_title.text
+22
+23    # Any code you write here will run before the form opens.
+24    self.switch_component("home")
+25
+26  def switch_component(self, state):
+27    # set state
+28    if state == "home":
+29      if data_access.the_user():
+30        cmpt = HomeComponent()
+31      else:
+32        cmpt = WelcomeComponent()
+33      breadcrumb = self.breadcrumb_stem
+34    elif state == "account":
+35      cmpt = AccountComponent()
+36      breadcrumb = self.breadcrumb_stem + " - Account"
+37    elif state == "add":
+38      cmpt = AddComponent()
+39      breadcrumb = self.breadcrumb_stem + " - Add"
+40    elif state == "calendar":
+41      cmpt = CalendarComponent()
+42      breadcrumb = self.breadcrumb_stem + " - Calendar"
+43    elif state == "details":
+44      cmpt = SetDetailsComponent()
+45      breadcrumb = self.breadcrumb_stem + " - Account - Set Details"
+46    
+47    # execution
+48    self.content_panel.clear()
+49    self.content_panel.add_component(cmpt)
+50    self.label_title.text = breadcrumb
+51    self.set_active_link(state)
+52  
+53  def set_active_link(self, state):
+54    if state == "home":
+55      self.link_home.role = "selected"
+56    else:
+57      self.link_home.role = None
+58    if state == "add":
+59      self.link_add.role = "selected"
+60    else:
+61      self.link_add.role = None
+62    if state == "calendar":
+63      self.link_calendar.role = "selected"
+64    else:
+65      self.link_calendar.role = None
+66
+67    self.link_register.visible = not data_access.the_user()
+68    self.link_login.visible = not data_access.the_user()
+69    self.link_account.visible = data_access.the_user()
+70    self.link_logout.visible = data_access.the_user()
+71  
+72  # --- link handlers
+73  def link_home_click(self, **event_args):
+74    self.switch_component("home")
+75
+76  def link_calendar_click(self, **event_args):
+77    self.switch_component("calendar")
+78
+79  def link_add_click(self, **event_args):
+80    self.switch_component("add")
+81
+82  def link_account_click(self, **event_args):
+83    """This method is called when the link is clicked"""
+84    self.switch_component("account")
+85
+86  def link_register_click(self, **event_args):
+87    anvil.users.signup_with_form(allow_cancel=True)
+88    self.switch_component("details")
+89
+90  def link_login_click(self, **event_args):
+91    anvil.users.login_with_form(allow_cancel=True)
+92    self.switch_component("home")
+93
+94  def link_logout_click(self, **event_args):
+95    data_access.logout()
+96    self.switch_component("home")
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/33_update_user_func.html b/33_update_user_func.html new file mode 100644 index 0000000..b3ef797 --- /dev/null +++ b/33_update_user_func.html @@ -0,0 +1,552 @@ + + + + + + + + + + Create Update User Function - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Create Update User Function

+ +

Another problem that was identified through testing, is an issue with the SetDetailsComponent. When a user changes their details, and saves, the AccountComponent continues to display their old details.

+
+

Planning

+

This problem also stems from the system continuing to use the cached user data. The button_save_click hander in the SetDetailsComponent writes the new data to the database, but the cached user data doesn’t change. When the AccountComponent loads it uses the cached (i.e. old) user data.

+

To resolve this we will need reload the cached user data after the new data is sent to the database.

+
+
+

Code

+

Again, we will need to make a new function in the data_access module which will:

+
    +
  • write new data to the database

  • +
  • reload the cached user data

  • +
+
    +
  1. Open the data_access module

  2. +
  3. Add the following code to the bottom of the module

  4. +
+
26def update_user(first_name, last_name):
+27  global __user
+28  
+29  print("Writing user details to database")
+30  anvil.server.call('update_user', first_name, last_name)
+31  __user = None
+32  __user = the_user()
+
+
+
+

Code explanation

+
    +
  • line 26 → creates the update_user function which requires first_name and last_name to be passed.

  • +
  • line 27 → allows the function to edit the value of __user

  • +
  • line 29 → informs the developer that the database is being accessed

  • +
  • line 30 → writes the new user data to the database

  • +
  • line 31 → clears the cached user data

  • +
  • line 32 → caches the new user data

  • +
+
+

Then we need to change the code of the button_save_click hander in the SetDetailsComponent

+
    +
  1. Open SetDetailsComponent

  2. +
  3. Replace lines 36 - 38 with the highlighted code below

  4. +
+
23  def button_save_click(self, **event_args):
+24    
+25    if self.text_box_first_name.text == "":
+26      self.label_error.text = "First name cannot be blank"
+27      self.label_error.visible = True
+28      return
+29
+30    if self.text_box_last_name.text == "":
+31      self.label_error.text = "Last name cannot be blank"
+32      self.label_error.visible = True
+33      return
+34
+35    self.label_error.visible = False
+36    data_access.update_user(self.text_box_first_name.text, 
+37                            self.text_box_last_name.text)
+38
+39    main_form = get_open_form()
+40    main_form.switch_component("account")
+
+
+
+

Code explanation

+
    +
  • lines 36 - 37 → calls the update_user function pass in the values from the first_name and last_name text boxes.

  • +
+
+
+
+

Testing

+

Launch your web app and change the details of a user. When you click on the save button, the account page should show the new details.

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final data_access

+
 1import anvil.server
+ 2import anvil.users
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6
+ 7# cached values
+ 8__user = None
+ 9
+10def the_user():
+11  global __user
+12
+13  if __user:
+14    print("Using cached user")
+15    return __user
+16
+17  print("Accessing user from database")
+18  __user = anvil.users.get_user()
+19  return __user
+20
+21def logout():
+22  global __user
+23  __user = None
+24  anvil.users.logout()
+25
+26def update_user(first_name, last_name):
+27  global __user
+28  
+29  print("Writing user details to database")
+30  anvil.server.call('update_user', first_name, last_name)
+31  __user = None
+32  __user = the_user()
+
+
+
+
+

Final SetDetailsComponent

+
 1from ._anvil_designer import SetDetailsComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from .. import data_access
+ 9
+10
+11class SetDetailsComponent(SetDetailsComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15
+16    # Any code you write here will run before the form opens.
+17    user = data_access.the_user()
+18    if user["first_name"]:
+19      self.text_box_first_name.text = user["first_name"]
+20    if user["last_name"]:
+21      self.text_box_last_name.text = user["last_name"]
+22
+23  def button_save_click(self, **event_args):
+24    
+25    if self.text_box_first_name.text == "":
+26      self.label_error.text = "First name cannot be blank"
+27      self.label_error.visible = True
+28      return
+29
+30    if self.text_box_last_name.text == "":
+31      self.label_error.text = "Last name cannot be blank"
+32      self.label_error.visible = True
+33      return
+34
+35    self.label_error.visible = False
+36    data_access.update_user(self.text_box_first_name.text, 
+37                            self.text_box_last_name.text)
+38
+39    main_form = get_open_form()
+40    main_form.switch_component("account")
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/34_my_assess_func.html b/34_my_assess_func.html new file mode 100644 index 0000000..4c73d59 --- /dev/null +++ b/34_my_assess_func.html @@ -0,0 +1,946 @@ + + + + + + + + + + My Assessments Function - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

My Assessments Function

+ +

Now that we have cached the user data, our web app is fasters, but there are still some places where accessing the database slows it down.

+

If we look at the assessment_service we will notice two functions which retrieve data from the database:

+
    +
  • get_assessment

  • +
  • get_chart

  • +
+

This implies that there are two more sets of data that we can cache. In this tutorial we will look to cache the get_assessment data, and we will deal with the get_chart data next tutorial.

+
+

Planning

+

The impact of using the get_assessment method is not as significant as get_user. It is only used in the HomeComponent. Never-the-less, this will be called every time the user goes to the HomeComponent, so it will still have an impact.

+

We will use the same process that we used for the get_user method:

+
    +
  1. Create a variable to store assessment data in the data_access module.

  2. +
  3. Create a frontend method that caches the assessment data if it is not currently cached, and then returns the cached data.

  4. +
  5. Replace calls to get_assessment with calls to the new frontend method.

  6. +
+

Lets get to it.

+
+
+

Code

+
+

Create the caching method

+

First we need to make a new variable to cache the assessment data.

+
    +
  1. Open the data_access module,

  2. +
  3. At the top, under the cached values section add the highlighted code

  4. +
+
7# cached values
+8__user = None
+9__assessments = None
+
+
+
+

Code explanation

+
    +
  • line 9 → creates the private variable __assessments to store the assessment data

  • +
+
+

Now to create the method

+
    +
  1. At the bottom of the data_access module add the following highlighted code.

  2. +
+
35def my_assessment():
+36  global __assessments
+37
+38  if __assessments:
+39    print("Using cached assessments")
+40    return __assessments
+41
+42  print("Accessing assessments from database")
+43  __assessments = anvil.server.call('get_assessment')
+44  return __assessments
+
+
+
+

Code explanation

+
    +
  • line 35 → creates the my_assessment method

  • +
  • line 36 → allows the method to edit the value of __assessments

  • +
  • line 38 → checks if the assessment data is already cached

  • +
  • line 39 → informs the developer that cached data is being used

  • +
  • line 40 → returns the cached data and ends the method

  • +
  • line 42 → informs the developed that the database is being accessed

  • +
  • line 43 → retrieves the assessment data from the database and caches it

  • +
  • line 44 → returned the cached data

  • +
+
+
+
+

Replace calls to get_assessment

+

Now we need to replace the calls to get_assessment

+
    +
  1. Open HomeComponent in Code mode

  2. +
  3. In the import section, add the highlighted code

  4. +
+
1from ._anvil_designer import HomeComponentTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.tables as tables
+5import anvil.tables.query as q
+6from anvil.tables import app_tables
+7import anvil.users
+8from .. import data_access
+
+
+
+

Code explanation

+
    +
  • line 8 → gives HomeComponent access to the data_access methods

  • +
+
+
    +
  1. Insert the highlighted code at the bottom of the __init__ method

  2. +
+
11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    self.repeating_panel_1.items = data_access.my_assessment()
+
+
+
+

Code explanation

+
    +
  • line 16 → uses our new my_assessment method to provide the data for the repeating panels.

  • +
+
+
+
+

Testing

+

Time to test your web site and see if our changes worked.

+

Testing:

+
    +
  1. Launch your web app

  2. +
  3. Wait for the Home page to load - this should take some time

  4. +
  5. Go to the Account page - this should load instantly

  6. +
  7. Go back to the Home page - this should load instantly

  8. +
  9. Go to the Add page - this should load instantly

  10. +
  11. Add a new assessment item

  12. +
  13. Go back to the Home page

  14. +
  15. Check if the assessment item is there - spoiler, it won’t be

  16. +
+

testing

+
+
+

Fixing the add assessment problem

+

The problem with adding assessment, is similar to the one we had with changing user data. When we add a new assessment, it gets written to the database, but the cached assessment data does not change. We need to fix this.

+
    +
  1. Open the data_access module

  2. +
  3. Insert the highlighted code into the bottom of the module

  4. +
+
46def add_assessment(subject, details, start_date, due_date):
+47  global __assessments
+48  
+49  print("Writing assessment details to the database")
+50  anvil.server.call('add_assessment', subject, details, start_date, due_date)
+51  __assessments = None
+52  my_assessment()
+
+
+
+

Code explanation

+
    +
  • line 46 → creates the add_assessment method, which requires subject, details, start_date and due_date to be passed

  • +
  • line 47 → allows the method to edit the value of __assessments

  • +
  • line 49 → informs the developed that the database is being accessed

  • +
  • line 50 → writes the new assessment details to the database

  • +
  • line 51 → resets the __assessment value to None

  • +
  • line 52 → caches the updated assessment list

  • +
+
+

Now we need to replace the call to the database, with a call to our new method.

+
    +
  1. Open AddComponent in Code mode

  2. +
  3. Replace line 47 with the highlighted code below

  4. +
+
31  def button_add_click(self, **event_args):
+32    # validation
+33    if not self.text_box_subject.text:
+34      self.display_error("Subject name needed")
+35    elif not self.text_box_details.text:
+36      self.display_error("Assessment details needed")
+37    elif not self.date_picker_start.date:
+38      self.display_error("Start date needed")
+39    elif not self.date_picker_due.date:
+40      self.display_error("Due date needed")
+41    else:
+42      self.subject = self.text_box_subject.text
+43      self.details = self.text_box_details.text
+44      self.start = self.date_picker_start.date
+45      self.due = self.date_picker_due.date
+46      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+47      data_access.add_assessment(self.subject, self.details, self.start, self.due)
+48      self.reset_form()
+
+
+
+

Code explanation

+
    +
  • line 47 → calls our new caching method and passes the required values

  • +
+
+
+
+

Second test

+

Lets check that this worked.

+
    +
  1. Launch your website and navigate to the Add page.

  2. +
  3. Add a new assessment

  4. +
  5. Return to the Home page

  6. +
  7. Check if your new assessment is shown

  8. +
+

test 2

+

Good. But are there more places where our cache can cause problem? When else do we write data to the database? How about when we change assessment details?

+
    +
  1. Launch your web app

  2. +
  3. Edit the value of one of the assessments → good, it changes

  4. +
  5. Navigate to the Add page

  6. +
  7. Return to the Home → it’s the previous value

  8. +
+

test 3

+

Again this is the result of changing the database values without updating the cache. So lets fix that too.

+
+
+

Fix update assessment problem

+

Just like before, we will create a method in the data_access module that writes to the database and then reloads the cache.

+
    +
  1. Open the data_access module

  2. +
  3. At the bottom of the module add the highlighted code

  4. +
+
54def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+55  global __assessments
+56
+57  print("Updating assessment details on the database")
+58  anvil.server.call('update_assessment',
+59                    assessment_id,
+60                    subject,
+61                    details,
+62                    start_date,
+63                    due_date,
+64                    completed
+65                   )
+66  __assessments = None
+67  my_assessment()
+
+
+
+

Code explanation

+
    +
  • line 54 → creates the update_assessment method with all the required data to be passed

  • +
  • line 55 → allows the method to change the __assessments value

  • +
  • line 57 → lets the developer know that the database is being accessed

  • +
  • line 58 - 65 → writes the changes to the database

  • +
  • line 66 → resets the __assessments value to None

  • +
  • line 67 → caches the assessments data

  • +
+
+
    +
  1. Open the AssessmentPanel

  2. +
  3. In the import section add the highlighted line

  4. +
+
1from ._anvil_designer import AssessmentPanelTemplate
+2from anvil import *
+3import anvil.server
+4import anvil.users
+5import anvil.tables as tables
+6import anvil.tables.query as q
+7from anvil.tables import app_tables
+8import datetime
+9from ... import data_access
+
+
+
+

Code explanation

+
    +
  • line 9 → gives HomeComponent access to the data_access methods

  • +
+
+
    +
  1. In the button_save_click handler replace line 37 - 44 with the highlighted code below

  2. +
+
35  def button_save_click(self, **event_args):
+36    # write to server
+37    data_access.update_assessment(self.item.get_id(),
+38                                  self.text_box_subject.text,
+39                                  self.text_box_details.text,
+40                                  self.date_picker_start.date,
+41                                  self.date_picker_due.date,
+42                                  self.check_box_completed.checked
+43                                 )
+44
+45    # update display
+46    self.label_subject.text = self.text_box_subject.text
+47    self.label_details.text = self.text_box_details.text
+48    self.label_start.text = self.date_picker_start.date.strftime('%d/%m/%Y')
+49    self.label_due.text = self.date_picker_due.date.strftime('%d/%m/%Y')
+50    self.switch_components()
+
+
+
+

Code explanation

+
    +
  • line 37-43 → uses our new caching method to make changes to the assessment data

  • +
+
+
+
+

Third test

+

Lets check our latest iteration of code

+
    +
  1. Launch you website

  2. +
  3. Change an assessment’s detail

  4. +
  5. Navigate to another page

  6. +
  7. Return to Home

  8. +
  9. Check if your change is displayed.

  10. +
+

test 4

+

It should all be working now.

+
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final data_access

+
 1import anvil.server
+ 2import anvil.users
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6
+ 7# cached values
+ 8__user = None
+ 9__assessments = None
+10
+11def the_user():
+12  global __user
+13
+14  if __user:
+15    print("Using cached user")
+16    return __user
+17
+18  print("Accessing user from database")
+19  __user = anvil.users.get_user()
+20  return __user
+21
+22def logout():
+23  global __user
+24  __user = None
+25  anvil.users.logout()
+26
+27def update_user(first_name, last_name):
+28  global __user
+29  
+30  print("Writing user details to database")
+31  anvil.server.call('update_user', first_name, last_name)
+32  __user = None
+33  __user = the_user()
+34
+35def my_assessment():
+36  global __assessments
+37  
+38  if __assessments:
+39    print("Using cached assessments")
+40    return __assessments
+41
+42  print("Accessing assessments from database")
+43  __assessments = anvil.server.call('get_assessment')
+44  return __assessments
+45
+46def add_assessment(subject, details, start_date, due_date):
+47  global __assessments
+48  
+49  print("Writing assessment details to the database")
+50  anvil.server.call('add_assessment', subject, details, start_date, due_date)
+51  __assessments = None
+52  my_assessment()
+53
+54def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+55  global __assessments
+56
+57  print("Updating assessment details on the database")
+58  anvil.server.call('update_assessment',
+59                    assessment_id,
+60                    subject,
+61                    details,
+62                    start_date,
+63                    due_date,
+64                    completed
+65                   )
+66  __assessments = None
+67  my_assessment()
+
+
+
+
+

Final HomeComponent

+
 1from ._anvil_designer import HomeComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from .. import data_access
+ 9
+10class HomeComponent(HomeComponentTemplate):
+11  def __init__(self, **properties):
+12    # Set Form properties and Data Bindings.
+13    self.init_components(**properties)
+14
+15    # Any code you write here will run before the form opens.
+16    self.repeating_panel_1.items = data_access.my_assessment()
+
+
+
+
+

Final AddComponent

+
 1from ._anvil_designer import AddComponentTemplate
+ 2from anvil import *
+ 3import anvil.server
+ 4import anvil.tables as tables
+ 5import anvil.tables.query as q
+ 6from anvil.tables import app_tables
+ 7import anvil.users
+ 8from .. import data_access
+ 9
+10
+11class AddComponent(AddComponentTemplate):
+12  def __init__(self, **properties):
+13    # Set Form properties and Data Bindings.
+14    self.init_components(**properties)
+15    self.subject = ""
+16    self.details = ""
+17    self.start = None
+18    self.due = None
+19
+20    # Any code you write here will run before the form opens.
+21    self.label_message.visible = False
+22    if data_access.the_user():
+23      self.card_details.visible = True
+24      self.card_error.visible = False
+25      self.button_add.visible = True
+26    else:
+27      self.card_details.visible = False
+28      self.card_error.visible = True
+29      self.button_add.visible = False
+30
+31  def button_add_click(self, **event_args):
+32    # validation
+33    if not self.text_box_subject.text:
+34      self.display_error("Subject name needed")
+35    elif not self.text_box_details.text:
+36      self.display_error("Assessment details needed")
+37    elif not self.date_picker_start.date:
+38      self.display_error("Start date needed")
+39    elif not self.date_picker_due.date:
+40      self.display_error("Due date needed")
+41    else:
+42      self.subject = self.text_box_subject.text
+43      self.details = self.text_box_details.text
+44      self.start = self.date_picker_start.date
+45      self.due = self.date_picker_due.date
+46      self.display_save(f"{self.subject} {self.details} assessment: {self.start} to {self.due} recorded")
+47      data_access.add_assessment(self.subject, self.details, self.start, self.due)
+48      self.reset_form()
+49
+50  def display_error(self, message):
+51    self.label_message.visible = True
+52    self.label_message.foreground = "#ff0000"
+53    self.label_message.icon = "fa:exclamation-triangle"
+54    self.label_message.bold = True
+55    self.label_message.text = message
+56
+57  def display_save(self, message):
+58    self.label_message.visible = True
+59    self.label_message.foreground = "#000000"
+60    self.label_message.icon = "fa:save"
+61    self.label_message.bold = False
+62    self.label_message.text = message
+63
+64  def reset_form(self):
+65    self.subject = ""
+66    self.details = ""
+67    self.start = None
+68    self.due = None
+69    self.text_box_subject.text = ""
+70    self.text_box_details.text = ""
+71    self.date_picker_start.date = None
+72    self.date_picker_due.date = None
+
+
+
+
+

Final AssessmentPanel

+

from ._anvil_designer import AssessmentPanelTemplate +from anvil import * +import anvil.server +import anvil.users +import anvil.tables as tables +import anvil.tables.query as q +from anvil.tables import app_tables +import datetime +from … import data_access

+
 1class AssessmentPanel(AssessmentPanelTemplate):
+ 2  def __init__(self, **properties):
+ 3    # Set Form properties and Data Bindings.
+ 4    self.init_components(**properties)
+ 5
+ 6    # Any code you write here will run before the form opens.
+ 7    self.check_box_completed.checked = self.item['completed']
+ 8    self.label_subject.text = self.item['subject']
+ 9    self.label_details.text = self.item['details']
+10    self.label_start.text = self.item['start_date'].strftime('%d/%m/%Y')
+11    self.label_due.text = self.item['due_date'].strftime('%d/%m/%Y')
+12
+13  def check_box_completed_change(self, **event_args):
+14    new_value = self.check_box_completed.checked
+15    anvil.server.call('update_assessment_completed', self.item.get_id(),new_value)
+16
+17  def button_edit_click(self, **event_args):
+18    self.text_box_subject.text = self.item["subject"]
+19    self.text_box_details.text = self.item["details"]
+20    self.date_picker_start.date = self.item["start_date"]
+21    self.date_picker_due.date = self.item["due_date"]
+22    self.switch_components()
+23
+24  def button_save_click(self, **event_args):
+25    # write to server
+26    data_access.update_assessment(self.item.get_id(),
+27                                  self.text_box_subject.text,
+28                                  self.text_box_details.text,
+29                                  self.date_picker_start.date,
+30                                  self.date_picker_due.date,
+31                                  self.check_box_completed.checked
+32                                 )
+33
+34    # update display
+35    self.label_subject.text = self.text_box_subject.text
+36    self.label_details.text = self.text_box_details.text
+37    self.label_start.text = self.date_picker_start.date.strftime('%d/%m/%Y')
+38    self.label_due.text = self.date_picker_due.date.strftime('%d/%m/%Y')
+39    self.switch_components()
+40  
+41  def switch_components(self):
+42    # display elements
+43    self.label_subject.visible = not self.label_subject.visible
+44    self.label_details.visible = not self.label_details.visible
+45    self.label_start.visible = not self.label_start.visible
+46    self.label_due.visible = not self.label_due.visible
+47    self.button_edit.visible = not self.button_edit.visible
+48    
+49    # edit elements
+50    self.text_box_subject.visible = not self.text_box_subject.visible
+51    self.text_box_details.visible = not self.text_box_details.visible
+52    self.date_picker_start.visible = not self.date_picker_start.visible
+53    self.date_picker_due.visible = not self.date_picker_due.visible
+54    self.button_save.visible = not self.button_save.visible
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/34a_get_chart_func.html b/34a_get_chart_func.html new file mode 100644 index 0000000..e45191e --- /dev/null +++ b/34a_get_chart_func.html @@ -0,0 +1,629 @@ + + + + + + + + + + Get Chart Function - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Get Chart Function

+ +

The final frontend component that retrieves data from the backend is the CalendarComponent. In particular it retrieves the Gantt chart using the get_chart method.

+
+

Planning

+

Just like the previous data, we need to cache the diagram so that it only gets retrieved:

+
    +
  • the first time CalendarComponent is called.

  • +
  • when the data in the Assessments table is updated.

  • +
+

This should be very familiar by now, so let’s get to the coding

+
+
+

Coding

+

First we need to create a variable in the data_access module to store the diagram.

+
    +
  1. Open the data_access module

  2. +
  3. Add the highlighted line to the bottom of the cached values section.

  4. +
+
 7# cached values
+ 8__user = None
+ 9__assessments = None
+10__chart = None
+
+
+
+

Code explanation

+
    +
  • line 9 → creates the private variable __chart to store the diagram

  • +
+
+

Then we need to create a method that will cache and return the chart.

+
    +
  1. At the bottom of the data_access module add the highlighted code

  2. +
+
70def get_chart():
+71  global __chart
+72
+73  if __chart:
+74    print("Using cached chart")
+75    return __chart
+76
+77  print("Building new chart from database")
+78  __chart = anvil.server.call('get_chart', user, assessments)
+79  return __chart
+
+
+
+

Code explanation

+
    +
  • line 70 → creates the get_chart method

  • +
  • line 71 → allows the method to edit the value of __chart

  • +
  • line 73 → checks if the chart is already cached

  • +
  • line 74 → informs the developer that the cached chart is being used

  • +
  • line 75 → returns the cached chart and ends the method

  • +
  • line 77 → informs the developed that a new chart is being built

  • +
  • line 78 → builds a new chart and stores it in the __chart variable

  • +
  • line 79 → returned the cached chart

  • +
+
+

We also need to re-cache the chart when the assessments data is updated.

+
    +
  1. Go to the my_assessment function and add the highlighted line of code

  2. +
+
55def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+56  global __assessments
+57
+58  print("Updating assessment details on the database")
+59  anvil.server.call('update_assessment',
+60                    assessment_id,
+61                    subject,
+62                    details,
+63                    start_date,
+64                    due_date,
+65                    completed
+66                   )
+67  __assessments = None
+68  __chart = None
+69  my_assessment()
+
+
+
+

Code explanation

+
    +
  • line 68 → reset __chart to None meaning the next time get_chart is called, a new chart will be built and cached.

  • +
+
+

Finally we need to replace the call to backend in CalendarComponent

+
    +
  1. Open the CalendarComponent in Code mode

  2. +
  3. Change the highlighted code in the load_chart method

  4. +
+
26  def load_chart(self):
+27    fig = data_access.get_chart()
+28        
+29    # Assign the Plotly figure to the Anvil Plot component
+30    self.plot_timeline.figure = fig
+
+
+
+

Code explanation

+

line 27 → retrieves the cached chart

+
+
+
+

Testing

+

Launch your web site for testing.

+
    +
  1. Navigate to the Calendar page (this should still load slowly)

  2. +
  3. Navigate back to the Home page (this should be quick)

  4. +
  5. Navigate back to the Calendar page (this should be quick now)

  6. +
+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final data_access

+
 1import anvil.server
+ 2import anvil.users
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6
+ 7# cached values
+ 8__user = None
+ 9__assessments = None
+10__chart = None
+11
+12def the_user():
+13  global __user
+14
+15  if __user:
+16    print("Using cached user")
+17    return __user
+18
+19  print("Accessing user from database")
+20  __user = anvil.users.get_user()
+21  return __user
+22
+23def logout():
+24  global __user
+25  __user = None
+26  anvil.users.logout()
+27
+28def update_user(first_name, last_name):
+29  global __user
+30  
+31  print("Writing user details to database")
+32  anvil.server.call('update_user', first_name, last_name)
+33  __user = None
+34  __user = the_user()
+35
+36def my_assessment():
+37  global __assessments
+38  
+39  if __assessments:
+40    print("Using cached assessments")
+41    return __assessments
+42
+43  print("Accessing assessments from database")
+44  __assessments = anvil.server.call('get_assessment')
+45  return __assessments
+46
+47def add_assessment(subject, details, start_date, due_date):
+48  global __assessments
+49  
+50  print("Writing assessment details to the database")
+51  anvil.server.call('add_assessment', subject, details, start_date, due_date)
+52  __assessments = None
+53  my_assessment()
+54
+55def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+56  global __assessments
+57
+58  print("Updating assessment details on the database")
+59  anvil.server.call('update_assessment',
+60                    assessment_id,
+61                    subject,
+62                    details,
+63                    start_date,
+64                    due_date,
+65                    completed
+66                   )
+67  __assessments = None
+68  __chart = None
+69  my_assessment()
+70
+71def get_chart():
+72  global __chart
+73
+74  if __chart:
+75    print("Using cached chart")
+76    return __chart
+77
+78  print("Building new chart from database")
+79  __chart = anvil.server.call('get_chart')
+80  return __chart
+
+
+
+
+

Final CalendarComponent

+
 1from ._anvil_designer import CalendarComponentTemplate
+ 2from anvil import *
+ 3import plotly.graph_objects as go
+ 4import anvil.server
+ 5import anvil.tables as tables
+ 6import anvil.tables.query as q
+ 7from anvil.tables import app_tables
+ 8import anvil.users
+ 9from .. import data_access
+10
+11
+12class CalendarComponent(CalendarComponentTemplate):
+13  def __init__(self, **properties):
+14    # Set Form properties and Data Bindings.
+15    self.init_components(**properties)
+16
+17    # Any code you write here will run before the form opens.
+18    if data_access.the_user():
+19      self.card_details.visible = True
+20      self.card_error.visible = False
+21      self.load_chart()
+22    else:
+23      self.card_details.visible = False
+24      self.card_error.visible = True
+25
+26  def load_chart(self):
+27    fig = data_access.get_chart()
+28        
+29    # Assign the Plotly figure to the Anvil Plot component
+30    self.plot_timeline.figure = fig
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/35_local_database.html b/35_local_database.html new file mode 100644 index 0000000..72660db --- /dev/null +++ b/35_local_database.html @@ -0,0 +1,772 @@ + + + + + + + + + + Reduce Local Database Access - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Reduce Local Database Access

+ +

Remember when we discussed optimisation, we focused on preventing the frontend from unnecessarily retrieving data from the backend. This was a low-hanging fruit in improving our website speed, since the communication between the frontend and the backend is the slowest communication that occurs in our web app.

+
+

Planning

+

Now that we have addressed this, we can look at other slow routes of data communication. To do this we need to understand the different speed of data retrieval:

+
    +
  1. Fastest - retrieve from RAM (memory)

  2. +
  3. Medium - retrieve from storage (HDD / SSD / Servers)

  4. +
  5. Slowest - retrieve over internet

  6. +
+

frontend backend

+

Looking at the diagram of our web app we can see that there are two more places that communication occurs:

+
    +
  • On the frontend between the application and the device

  • +
  • On the backend between the server and the database

  • +
+

The frontend communication occurs by retrieving data from the RAM, so we can’t get a significant increase in speed here, but the backend communication involves server retrieval from storage, so we can speed that up.

+

In our current assessment_service, the database is unnecessarily calls cached data in:

+
    +
  • add_assessment - user

  • +
  • get_assessment - user

  • +
  • get_chart - user and chart

  • +
+

In all these cases, the cached data can be passed to the server when the assessment_service method is called.

+
+
+

Code

+
+

add_assessment function

+

Lets start with the add_assessment.

+
    +
  1. Open the assessment_service module and go to add_assessment

  2. +
  3. Delete the anvil.users.get_user() call in line 11

  4. +
  5. Add user as an argument to be passed - like the highlight below

  6. +
+
 9@anvil.server.callable
+10def add_assessment(user, subject, details, start_date, due_date):
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+
+
+
+

Code explanation

+

line 10 → adds user as a value to be passed when the method is called

+
+

Now we have to change the call to add_assessment to include the user

+
    +
  1. Open the data_access module

  2. +
  3. Add line 51 below and change line 52

  4. +
+
47def add_assessment(subject, details, start_date, due_date):
+48  global __assessments
+49  
+50  print("Writing assessment details to the database")
+51  user = the_user()
+52  anvil.server.call('add_assessment', user, subject, details, start_date, due_date)
+53  __assessments = None
+54  my_assessment()
+
+
+
+

Code explanation

+
    +
  • line 51 → retrieves the cached user data

  • +
  • line 52 → adds the cached user data to the arguments passed to add_assessment

  • +
+
+
+

Testing add_assessment function

+

Launch your website and add a new assessment item.

+

Notice all the caching messages - think of the time you are saving.

+
+
+
+
+

get_assessment function

+

New we will use caching data in the get_assessment function.

+
    +
  1. Open the assessment_service module and go to get_assessment

  2. +
  3. Delete the anvil.users.get_user() call in line 20

  4. +
  5. Add user as an argument to be passed - like the highlight below

  6. +
+
18@anvil.server.callable
+19def get_assessment(user):
+20  return app_tables.assessments.search(tables.order_by('due_date'),
+21                                      user=user,
+22                                      completed=False)
+
+
+
+

Code explanation

+

line 19 → adds user as a value to be passed when the method is called

+
+

Now we have to change the call to my_assessment to include the user

+
    +
  1. Open the data_access module

  2. +
  3. Add line 39 below and change line 46

  4. +
+
36def my_assessment():
+37  global __assessments
+38
+39  user = the_user()
+40  
+41  if __assessments:
+42    print("Using cached assessments")
+43    return __assessments
+44
+45  print("Accessing assessments from database")
+46  __assessments = anvil.server.call('get_assessment', user)
+47  return __assessments
+
+
+
+

Code explanation

+
    +
  • line 39 → retrieves the cached user data

  • +
  • line 46 → adds the cached user data to the arguments passed to get_assessment

  • +
+
+
+

Testing get_assessment function

+

Launch your web app and check that pages that use assessment data:

+
    +
  • all the assessments load on the Home page

  • +
  • the Calendar page

  • +
+
+
+
+

get_chart function

+

Finally the get_chart function:

+
    +
  1. Open the assessment_service module and go to

  2. +
  3. Delete the anvil.users.get_user() call in line 43

  4. +
  5. Delete the app_tables.assessments.search call in lines 44 - 46

  6. +
  7. Add assessments as arguments to be passed - like the highlight below

  8. +
+
40@anvil.server.callable
+41def get_chart(assessments):
+42    
+43    # Create a DataFrame from the assessments data
+44    data = []
+45    for assessment in assessments:
+46        # adjust for exams
+47        start_date = assessment['start_date']
+48        due_date = assessment['due_date']
+49                
+50        if start_date == due_date:
+51            due_date += pd.Timedelta(days=1)
+52      
+53        data.append({
+54            "Subject": assessment['subject'],
+55            "Details": assessment['details'],
+56            "Start": assessment['start_date'],
+57            "Due": due_date
+58        })
+59    
+60    df = pd.DataFrame(data)
+
+
+
+

Code explanation

+

line 19 → adds assessments as a value to be passed when the method is called

+
+

Now we have to change the call to my_assessment to include the user

+
    +
  1. Open the data_access module

  2. +
  3. Add line 82 below and change line 83

  4. +
+
74def get_chart():
+75  global __chart
+76
+77  if __chart:
+78    print("Using cached chart")
+79    return __chart
+80
+81  print("Building new chart from database")
+82  assessments = my_assessment()
+83  __chart = anvil.server.call('get_chart', assessments)
+84  return __chart
+
+
+
+

Code explanation

+
    +
  • line 82 → retrieves the cached assessments data

  • +
  • line 83 → adds the cached user data to the arguments passed to get_chart

  • +
+
+
+
+

Testing get_chart function

+

Launch your website, and navigate to the Calendar page.

+
+
+

Final code state

+

By the end of this tutorial your code should be the same as below:

+
+

Final data_access

+
 1import anvil.server
+ 2import anvil.users
+ 3import anvil.tables as tables
+ 4import anvil.tables.query as q
+ 5from anvil.tables import app_tables
+ 6
+ 7# cached values
+ 8__user = None
+ 9__assessments = None
+10__chart = None
+11
+12def the_user():
+13  global __user
+14
+15  if __user:
+16    print("Using cached user")
+17    return __user
+18
+19  print("Accessing user from database")
+20  __user = anvil.users.get_user()
+21  return __user
+22
+23def logout():
+24  global __user
+25  __user = None
+26  anvil.users.logout()
+27
+28def update_user(first_name, last_name):
+29  global __user
+30  
+31  print("Writing user details to database")
+32  anvil.server.call('update_user', first_name, last_name)
+33  __user = None
+34  __user = the_user()
+35
+36def my_assessment():
+37  global __assessments
+38
+39  user = the_user()
+40  
+41  if __assessments:
+42    print("Using cached assessments")
+43    return __assessments
+44
+45  print("Accessing assessments from database")
+46  __assessments = anvil.server.call('get_assessment', user)
+47  return __assessments
+48
+49def add_assessment(subject, details, start_date, due_date):
+50  global __assessments
+51  
+52  print("Writing assessment details to the database")
+53  user = the_user()
+54  anvil.server.call('add_assessment', user, subject, details, start_date, due_date)
+55  __assessments = None
+56  my_assessment()
+57
+58def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+59  global __assessments
+60
+61  print("Updating assessment details on the database")
+62  anvil.server.call('update_assessment',
+63                    assessment_id,
+64                    subject,
+65                    details,
+66                    start_date,
+67                    due_date,
+68                    completed
+69                   )
+70  __assessments = None
+71  __chart = None
+72  my_assessment()
+73
+74def get_chart():
+75  global __chart
+76
+77  if __chart:
+78    print("Using cached chart")
+79    return __chart
+80
+81  print("Building new chart from database")
+82  assessments = my_assessment()
+83  __chart = anvil.server.call('get_chart', assessments)
+84  return __chart
+
+
+
+
+

Final assessment_service

+
 1import anvil.users
+ 2import anvil.tables as tables
+ 3import anvil.tables.query as q
+ 4from anvil.tables import app_tables
+ 5import anvil.server
+ 6import plotly.express as px
+ 7import pandas as pd
+ 8
+ 9@anvil.server.callable
+10def add_assessment(user, subject, details, start_date, due_date): 
+11  app_tables.assessments.add_row(user= user,
+12                                 subject= subject,
+13                                 details=details,
+14                                 start_date=start_date,
+15                                 due_date=due_date,
+16                                 completed=False)
+17
+18@anvil.server.callable
+19def get_assessment(user):
+20  return app_tables.assessments.search(tables.order_by('due_date'),
+21                                      user=user,
+22                                      completed=False)
+23
+24@anvil.server.callable
+25def update_assessment_completed(assessment_id, completed):
+26  assessment = app_tables.assessments.get_by_id(assessment_id)
+27  if assessment:
+28    assessment["completed"] = completed
+29
+30@anvil.server.callable
+31def update_assessment(assessment_id, subject, details, start_date, due_date, completed):
+32    assessment = app_tables.assessments.get_by_id(assessment_id)
+33    if assessment:
+34        assessment['subject'] = subject
+35        assessment['details'] = details
+36        assessment['start_date'] = start_date
+37        assessment['due_date'] = due_date
+38        assessment['completed'] = completed
+39
+40@anvil.server.callable
+41def get_chart(assessments):
+42    
+43    # Create a DataFrame from the assessments data
+44    data = []
+45    for assessment in assessments:
+46        # adjust for exams
+47        start_date = assessment['start_date']
+48        due_date = assessment['due_date']
+49                
+50        if start_date == due_date:
+51            due_date += pd.Timedelta(days=1)
+52      
+53        data.append({
+54            "Subject": assessment['subject'],
+55            "Details": assessment['details'],
+56            "Start": assessment['start_date'],
+57            "Due": due_date
+58        })
+59    
+60    df = pd.DataFrame(data)
+61    
+62    # Create the Gantt chart using Plotly
+63    fig = px.timeline(df, 
+64                      x_start="Start", 
+65                      x_end="Due", 
+66                      y="Subject", 
+67                      text="Details", 
+68                      title="Assessment Schedule"
+69                     )
+70
+71    fig.update_yaxes(title_text="") 
+72    
+73    return fig
+
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/98_resources.html b/98_resources.html new file mode 100644 index 0000000..85a9fec --- /dev/null +++ b/98_resources.html @@ -0,0 +1,400 @@ + + + + + + + + + + Resources - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/99_topics.html b/99_topics.html new file mode 100644 index 0000000..baeebd8 --- /dev/null +++ b/99_topics.html @@ -0,0 +1,358 @@ + + + + + + + + + + Topics - StudyM8 + + + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+
+
StudyM8
+
+
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

Topics

+
+ +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md deleted file mode 100644 index 8c7ab72..0000000 --- a/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Anvil Python Tutorials - -This repository provides tutorials that introduce Python web app development using the Anvil platform. It includes step-by-step instructions on building apps with Anvil’s visual interface, covering essential concepts like user authentication, component creation, and database integration. - -## Features - -- **Detailed tutorials** for building web apps with Anvil. -- **Step-by-step guides** on creating user-friendly interfaces. -- **Assessment** and **calendar features** for managing tasks. - -## Website - -Visit the tutorial: [Anvil Python Tutorials](https://damom73.github.io/anvil-python-tutorials/) - -## License - -This project is licensed under the GPL-3.0 License. \ No newline at end of file diff --git a/assets/img/11/account_test.gif b/_images/account_test.gif similarity index 100% rename from assets/img/11/account_test.gif rename to _images/account_test.gif diff --git a/assets/img/17/accountcomponent_error.png b/_images/accountcomponent_error.png similarity index 100% rename from assets/img/17/accountcomponent_error.png rename to _images/accountcomponent_error.png diff --git a/assets/img/12/add.png b/_images/add.png similarity index 100% rename from assets/img/12/add.png rename to _images/add.png diff --git a/assets/img/22/add_button.gif b/_images/add_button.gif similarity index 100% rename from assets/img/22/add_button.gif rename to _images/add_button.gif diff --git a/assets/img/29/add_call_to_action.gif b/_images/add_call_to_action.gif similarity index 100% rename from assets/img/29/add_call_to_action.gif rename to _images/add_call_to_action.gif diff --git a/assets/img/15/add_card.gif b/_images/add_card.gif similarity index 100% rename from assets/img/15/add_card.gif rename to _images/add_card.gif diff --git a/assets/img/29/add_card.gif b/_images/add_card1.gif similarity index 100% rename from assets/img/29/add_card.gif rename to _images/add_card1.gif diff --git a/assets/img/24/add_card_details.gif b/_images/add_card_details.gif similarity index 100% rename from assets/img/24/add_card_details.gif rename to _images/add_card_details.gif diff --git a/assets/img/24/add_card_error.gif b/_images/add_card_error.gif similarity index 100% rename from assets/img/24/add_card_error.gif rename to _images/add_card_error.gif diff --git a/assets/img/15/add_column_panel.gif b/_images/add_column_panel.gif similarity index 100% rename from assets/img/15/add_column_panel.gif rename to _images/add_column_panel.gif diff --git a/assets/img/29/add_feature_text.gif b/_images/add_feature_text.gif similarity index 100% rename from assets/img/29/add_feature_text.gif rename to _images/add_feature_text.gif diff --git a/assets/img/29/add_features_images.gif b/_images/add_features_images.gif similarity index 100% rename from assets/img/29/add_features_images.gif rename to _images/add_features_images.gif diff --git a/assets/img/07/add_form.gif b/_images/add_form.gif similarity index 100% rename from assets/img/07/add_form.gif rename to _images/add_form.gif diff --git a/assets/img/28/add_plot.gif b/_images/add_plot.gif similarity index 100% rename from assets/img/28/add_plot.gif rename to _images/add_plot.gif diff --git a/assets/img/29/add_second_card.gif b/_images/add_second_card.gif similarity index 100% rename from assets/img/29/add_second_card.gif rename to _images/add_second_card.gif diff --git a/assets/img/16/add_server_module.png b/_images/add_server_module.png similarity index 100% rename from assets/img/16/add_server_module.png rename to _images/add_server_module.png diff --git a/assets/img/29/add_text.gif b/_images/add_text.gif similarity index 100% rename from assets/img/29/add_text.gif rename to _images/add_text.gif diff --git a/assets/img/07/add_title.gif b/_images/add_title.gif similarity index 100% rename from assets/img/07/add_title.gif rename to _images/add_title.gif diff --git a/assets/img/29/add_welcome_image.gif b/_images/add_welcome_image.gif similarity index 100% rename from assets/img/29/add_welcome_image.gif rename to _images/add_welcome_image.gif diff --git a/assets/img/23/assessment_service.png b/_images/assessment_service.png similarity index 100% rename from assets/img/23/assessment_service.png rename to _images/assessment_service.png diff --git a/assets/img/18a/button_edit_click_handler.gif b/_images/button_edit_click_handler.gif similarity index 100% rename from assets/img/18a/button_edit_click_handler.gif rename to _images/button_edit_click_handler.gif diff --git a/assets/img/11/cal_add_test.gif b/_images/cal_add_test.gif similarity index 100% rename from assets/img/11/cal_add_test.gif rename to _images/cal_add_test.gif diff --git a/assets/img/24/calendar.gif b/_images/calendar.gif similarity index 100% rename from assets/img/24/calendar.gif rename to _images/calendar.gif diff --git a/assets/img/29/calendar_icon.png b/_images/calendar_icon.png similarity index 100% rename from assets/img/29/calendar_icon.png rename to _images/calendar_icon.png diff --git a/assets/img/18/card.gif b/_images/card.gif similarity index 100% rename from assets/img/18/card.gif rename to _images/card.gif diff --git a/assets/img/22/card.gif b/_images/card1.gif similarity index 100% rename from assets/img/22/card.gif rename to _images/card1.gif diff --git a/assets/img/25/card.gif b/_images/card2.gif similarity index 100% rename from assets/img/25/card.gif rename to _images/card2.gif diff --git a/assets/img/22a/change_label.gif b/_images/change_label.gif similarity index 100% rename from assets/img/22a/change_label.gif rename to _images/change_label.gif diff --git a/assets/img/29/change_title.gif b/_images/change_title.gif similarity index 100% rename from assets/img/29/change_title.gif rename to _images/change_title.gif diff --git a/assets/img/25/check_box.gif b/_images/check_box.gif similarity index 100% rename from assets/img/25/check_box.gif rename to _images/check_box.gif diff --git a/assets/img/13/check_user.gif b/_images/check_user.gif similarity index 100% rename from assets/img/13/check_user.gif rename to _images/check_user.gif diff --git a/assets/img/29/checklist_icon.png b/_images/checklist_icon.png similarity index 100% rename from assets/img/29/checklist_icon.png rename to _images/checklist_icon.png diff --git a/assets/img/18a/circular_reference.png b/_images/circular_reference.png similarity index 100% rename from assets/img/18a/circular_reference.png rename to _images/circular_reference.png diff --git a/assets/img/02/cloud_computing.png b/_images/cloud_computing.png similarity index 100% rename from assets/img/02/cloud_computing.png rename to _images/cloud_computing.png diff --git a/assets/img/05/code_mode.png b/_images/code_mode.png similarity index 100% rename from assets/img/05/code_mode.png rename to _images/code_mode.png diff --git a/assets/img/05/code_snippets.png b/_images/code_snippets.png similarity index 100% rename from assets/img/05/code_snippets.png rename to _images/code_snippets.png diff --git a/assets/img/18/column_panel.gif b/_images/column_panel.gif similarity index 100% rename from assets/img/18/column_panel.gif rename to _images/column_panel.gif diff --git a/assets/img/25/column_panel.gif b/_images/column_panel1.gif similarity index 100% rename from assets/img/25/column_panel.gif rename to _images/column_panel1.gif diff --git a/assets/img/08/component_not_defined.png b/_images/component_not_defined.png similarity index 100% rename from assets/img/08/component_not_defined.png rename to _images/component_not_defined.png diff --git a/assets/img/05/components_panel.png b/_images/components_panel.png similarity index 100% rename from assets/img/05/components_panel.png rename to _images/components_panel.png diff --git a/assets/img/17/content_panel_error.png b/_images/content_panel_error.png similarity index 100% rename from assets/img/17/content_panel_error.png rename to _images/content_panel_error.png diff --git a/assets/img/22a/create_event.gif b/_images/create_event.gif similarity index 100% rename from assets/img/22a/create_event.gif rename to _images/create_event.gif diff --git a/assets/img/08/create_handler.gif b/_images/create_handler.gif similarity index 100% rename from assets/img/08/create_handler.gif rename to _images/create_handler.gif diff --git a/assets/img/15a/create_handler.gif b/_images/create_handler1.gif similarity index 100% rename from assets/img/15a/create_handler.gif rename to _images/create_handler1.gif diff --git a/assets/img/31/create_module.png b/_images/create_module.png similarity index 100% rename from assets/img/31/create_module.png rename to _images/create_module.png diff --git a/assets/img/21/create_table.png b/_images/create_table.png similarity index 100% rename from assets/img/21/create_table.png rename to _images/create_table.png diff --git a/assets/img/25/date_due.gif b/_images/date_due.gif similarity index 100% rename from assets/img/25/date_due.gif rename to _images/date_due.gif diff --git a/assets/img/25/date_start.gif b/_images/date_start.gif similarity index 100% rename from assets/img/25/date_start.gif rename to _images/date_start.gif diff --git a/assets/img/31/delete.png b/_images/delete.png similarity index 100% rename from assets/img/31/delete.png rename to _images/delete.png diff --git a/assets/img/05/design_mode.png b/_images/design_mode.png similarity index 100% rename from assets/img/05/design_mode.png rename to _images/design_mode.png diff --git a/assets/img/22/details.gif b/_images/details.gif similarity index 100% rename from assets/img/22/details.gif rename to _images/details.gif diff --git a/assets/img/27/details_text_box.gif b/_images/details_text_box.gif similarity index 100% rename from assets/img/27/details_text_box.gif rename to _images/details_text_box.gif diff --git a/assets/img/22/due.gif b/_images/due.gif similarity index 100% rename from assets/img/22/due.gif rename to _images/due.gif diff --git a/assets/img/27/due_date.gif b/_images/due_date.gif similarity index 100% rename from assets/img/27/due_date.gif rename to _images/due_date.gif diff --git a/assets/img/18/edit_button.gif b/_images/edit_button.gif similarity index 100% rename from assets/img/18/edit_button.gif rename to _images/edit_button.gif diff --git a/assets/img/27/edit_button.gif b/_images/edit_button1.gif similarity index 100% rename from assets/img/27/edit_button.gif rename to _images/edit_button1.gif diff --git a/assets/img/20/empty_cells.gif b/_images/empty_cells.gif similarity index 100% rename from assets/img/20/empty_cells.gif rename to _images/empty_cells.gif diff --git a/assets/img/07/end.gif b/_images/end.gif similarity index 100% rename from assets/img/07/end.gif rename to _images/end.gif diff --git a/assets/img/22/error.gif b/_images/error.gif similarity index 100% rename from assets/img/22/error.gif rename to _images/error.gif diff --git a/assets/img/08/error_create_event.gif b/_images/error_create_event.gif similarity index 100% rename from assets/img/08/error_create_event.gif rename to _images/error_create_event.gif diff --git a/assets/img/15/error_label.gif b/_images/error_label.gif similarity index 100% rename from assets/img/15/error_label.gif rename to _images/error_label.gif diff --git a/assets/img/24/error_message.gif b/_images/error_message.gif similarity index 100% rename from assets/img/24/error_message.gif rename to _images/error_message.gif diff --git a/assets/img/08/error_no_title.gif b/_images/error_no_title.gif similarity index 100% rename from assets/img/08/error_no_title.gif rename to _images/error_no_title.gif diff --git a/assets/img/26/event.gif b/_images/event.gif similarity index 100% rename from assets/img/26/event.gif rename to _images/event.gif diff --git a/assets/img/27/event_handlers.gif b/_images/event_handlers.gif similarity index 100% rename from assets/img/27/event_handlers.gif rename to _images/event_handlers.gif diff --git a/assets/img/11/final_test.gif b/_images/final_test.gif similarity index 100% rename from assets/img/11/final_test.gif rename to _images/final_test.gif diff --git a/assets/img/22/finished.png b/_images/finished.png similarity index 100% rename from assets/img/22/finished.png rename to _images/finished.png diff --git a/assets/img/21/finished_table.png b/_images/finished_table.png similarity index 100% rename from assets/img/21/finished_table.png rename to _images/finished_table.png diff --git a/assets/img/12/first_name.png b/_images/first_name.png similarity index 100% rename from assets/img/12/first_name.png rename to _images/first_name.png diff --git a/assets/img/15/first_name_label.gif b/_images/first_name_label.gif similarity index 100% rename from assets/img/15/first_name_label.gif rename to _images/first_name_label.gif diff --git a/assets/img/18/first_name_label.gif b/_images/first_name_label1.gif similarity index 100% rename from assets/img/18/first_name_label.gif rename to _images/first_name_label1.gif diff --git a/assets/img/15/first_name_text_box.gif b/_images/first_name_text_box.gif similarity index 100% rename from assets/img/15/first_name_text_box.gif rename to _images/first_name_text_box.gif diff --git a/assets/img/02/frontend_backend.png b/_images/frontend_backend.png similarity index 100% rename from assets/img/02/frontend_backend.png rename to _images/frontend_backend.png diff --git a/assets/img/29/graph_icon.png b/_images/graph_icon.png similarity index 100% rename from assets/img/29/graph_icon.png rename to _images/graph_icon.png diff --git a/assets/img/13/handlers.gif b/_images/handlers.gif similarity index 100% rename from assets/img/13/handlers.gif rename to _images/handlers.gif diff --git a/assets/img/06/home_link.gif b/_images/home_link.gif similarity index 100% rename from assets/img/06/home_link.gif rename to _images/home_link.gif diff --git a/assets/img/11/home_test.gif b/_images/home_test.gif similarity index 100% rename from assets/img/11/home_test.gif rename to _images/home_test.gif diff --git a/assets/img/05/interface.png b/_images/interface.png similarity index 100% rename from assets/img/05/interface.png rename to _images/interface.png diff --git a/assets/img/25/label_details.gif b/_images/label_details.gif similarity index 100% rename from assets/img/25/label_details.gif rename to _images/label_details.gif diff --git a/assets/img/18/label_first_name.gif b/_images/label_first_name.gif similarity index 100% rename from assets/img/18/label_first_name.gif rename to _images/label_first_name.gif diff --git a/assets/img/25/label_to.gif b/_images/label_to.gif similarity index 100% rename from assets/img/25/label_to.gif rename to _images/label_to.gif diff --git a/assets/img/12/last_name.png b/_images/last_name.png similarity index 100% rename from assets/img/12/last_name.png rename to _images/last_name.png diff --git a/assets/img/18/last_name_elements.gif b/_images/last_name_elements.gif similarity index 100% rename from assets/img/18/last_name_elements.gif rename to _images/last_name_elements.gif diff --git a/assets/img/15/last_name_label.gif b/_images/last_name_label.gif similarity index 100% rename from assets/img/15/last_name_label.gif rename to _images/last_name_label.gif diff --git a/assets/img/15/last_name_text_box.gif b/_images/last_name_text_box.gif similarity index 100% rename from assets/img/15/last_name_text_box.gif rename to _images/last_name_text_box.gif diff --git a/assets/img/15/layout.png b/_images/layout.png similarity index 100% rename from assets/img/15/layout.png rename to _images/layout.png diff --git a/assets/img/04/logo.png b/_images/logo.png similarity index 100% rename from assets/img/04/logo.png rename to _images/logo.png diff --git a/assets/img/06/main_form.png b/_images/main_form.png similarity index 100% rename from assets/img/06/main_form.png rename to _images/main_form.png diff --git a/assets/img/05/main_menu.png b/_images/main_menu.png similarity index 100% rename from assets/img/05/main_menu.png rename to _images/main_menu.png diff --git a/assets/img/04/material.png b/_images/material.png similarity index 100% rename from assets/img/04/material.png rename to _images/material.png diff --git a/assets/img/04/material_3.png b/_images/material_3.png similarity index 100% rename from assets/img/04/material_3.png rename to _images/material_3.png diff --git a/assets/img/13/messages.png b/_images/messages.png similarity index 100% rename from assets/img/13/messages.png rename to _images/messages.png diff --git a/assets/img/09/name_error.png b/_images/name_error.png similarity index 100% rename from assets/img/09/name_error.png rename to _images/name_error.png diff --git a/assets/img/06/open_form.png b/_images/open_form.png similarity index 100% rename from assets/img/06/open_form.png rename to _images/open_form.png diff --git a/assets/img/06/register_link.gif b/_images/register_link.gif similarity index 100% rename from assets/img/06/register_link.gif rename to _images/register_link.gif diff --git a/assets/img/13/register_user.gif b/_images/register_user.gif similarity index 100% rename from assets/img/13/register_user.gif rename to _images/register_user.gif diff --git a/assets/img/06/rename.png b/_images/rename.png similarity index 100% rename from assets/img/06/rename.png rename to _images/rename.png diff --git a/assets/img/25/repeating_panel.gif b/_images/repeating_panel.gif similarity index 100% rename from assets/img/25/repeating_panel.gif rename to _images/repeating_panel.gif diff --git a/assets/img/25/resize.gif b/_images/resize.gif similarity index 100% rename from assets/img/25/resize.gif rename to _images/resize.gif diff --git a/assets/img/15/save_button.gif b/_images/save_button.gif similarity index 100% rename from assets/img/15/save_button.gif rename to _images/save_button.gif diff --git a/assets/img/27/save_button.gif b/_images/save_button1.gif similarity index 100% rename from assets/img/27/save_button.gif rename to _images/save_button1.gif diff --git a/assets/img/22a/search.gif b/_images/search.gif similarity index 100% rename from assets/img/22a/search.gif rename to _images/search.gif diff --git a/assets/img/19/search.png b/_images/search.png similarity index 100% rename from assets/img/19/search.png rename to _images/search.png diff --git a/assets/img/30/search.png b/_images/search1.png similarity index 100% rename from assets/img/30/search.png rename to _images/search1.png diff --git a/assets/img/31/search.png b/_images/search2.png similarity index 100% rename from assets/img/31/search.png rename to _images/search2.png diff --git a/assets/img/19/search_2.png b/_images/search_2.png similarity index 100% rename from assets/img/19/search_2.png rename to _images/search_2.png diff --git a/assets/img/19/search_3.png b/_images/search_3.png similarity index 100% rename from assets/img/19/search_3.png rename to _images/search_3.png diff --git a/assets/img/19/search_4.png b/_images/search_4.png similarity index 100% rename from assets/img/19/search_4.png rename to _images/search_4.png diff --git a/assets/img/30/search_server.png b/_images/search_server.png similarity index 100% rename from assets/img/30/search_server.png rename to _images/search_server.png diff --git a/assets/img/11/selected_links.gif b/_images/selected_links.gif similarity index 100% rename from assets/img/11/selected_links.gif rename to _images/selected_links.gif diff --git a/assets/img/23/server_module.png b/_images/server_module.png similarity index 100% rename from assets/img/23/server_module.png rename to _images/server_module.png diff --git a/assets/img/05/side_menu.png b/_images/side_menu.png similarity index 100% rename from assets/img/05/side_menu.png rename to _images/side_menu.png diff --git a/assets/img/06/side_panel.gif b/_images/side_panel.gif similarity index 100% rename from assets/img/06/side_panel.gif rename to _images/side_panel.gif diff --git a/assets/img/03/site_map.png b/_images/site_map.png similarity index 100% rename from assets/img/03/site_map.png rename to _images/site_map.png diff --git a/assets/img/22/start.gif b/_images/start.gif similarity index 100% rename from assets/img/22/start.gif rename to _images/start.gif diff --git a/assets/img/27/start_date.gif b/_images/start_date.gif similarity index 100% rename from assets/img/27/start_date.gif rename to _images/start_date.gif diff --git a/assets/img/04/step_01.png b/_images/step_01.png similarity index 100% rename from assets/img/04/step_01.png rename to _images/step_01.png diff --git a/assets/img/04/step_02.png b/_images/step_02.png similarity index 100% rename from assets/img/04/step_02.png rename to _images/step_02.png diff --git a/assets/img/04/step_03.png b/_images/step_03.png similarity index 100% rename from assets/img/04/step_03.png rename to _images/step_03.png diff --git a/assets/img/04/step_04.png b/_images/step_04.png similarity index 100% rename from assets/img/04/step_04.png rename to _images/step_04.png diff --git a/assets/img/04/step_05.png b/_images/step_05.png similarity index 100% rename from assets/img/04/step_05.png rename to _images/step_05.png diff --git a/assets/img/04/step_06.png b/_images/step_06.png similarity index 100% rename from assets/img/04/step_06.png rename to _images/step_06.png diff --git a/assets/img/04/step_07.png b/_images/step_07.png similarity index 100% rename from assets/img/04/step_07.png rename to _images/step_07.png diff --git a/assets/img/04/step_08.png b/_images/step_08.png similarity index 100% rename from assets/img/04/step_08.png rename to _images/step_08.png diff --git a/assets/img/05/sub_menu.png b/_images/sub_menu.png similarity index 100% rename from assets/img/05/sub_menu.png rename to _images/sub_menu.png diff --git a/assets/img/22/subject.gif b/_images/subject.gif similarity index 100% rename from assets/img/22/subject.gif rename to _images/subject.gif diff --git a/assets/img/25/subject_label.gif b/_images/subject_label.gif similarity index 100% rename from assets/img/25/subject_label.gif rename to _images/subject_label.gif diff --git a/assets/img/27/subject_text_box.gif b/_images/subject_text_box.gif similarity index 100% rename from assets/img/27/subject_text_box.gif rename to _images/subject_text_box.gif diff --git a/assets/img/05/terminal.png b/_images/terminal.png similarity index 100% rename from assets/img/05/terminal.png rename to _images/terminal.png diff --git a/assets/img/13/terminal.png b/_images/terminal1.png similarity index 100% rename from assets/img/13/terminal.png rename to _images/terminal1.png diff --git a/assets/img/08/test.gif b/_images/test.gif similarity index 100% rename from assets/img/08/test.gif rename to _images/test.gif diff --git a/assets/img/06/test.png b/_images/test.png similarity index 100% rename from assets/img/06/test.png rename to _images/test.png diff --git a/assets/img/09/test.gif b/_images/test1.gif similarity index 100% rename from assets/img/09/test.gif rename to _images/test1.gif diff --git a/assets/img/10/test.gif b/_images/test2.gif similarity index 100% rename from assets/img/10/test.gif rename to _images/test2.gif diff --git a/assets/img/15a/test.gif b/_images/test3.gif similarity index 100% rename from assets/img/15a/test.gif rename to _images/test3.gif diff --git a/assets/img/16/test.gif b/_images/test4.gif similarity index 100% rename from assets/img/16/test.gif rename to _images/test4.gif diff --git a/assets/img/34/test_1.gif b/_images/test_1.gif similarity index 100% rename from assets/img/34/test_1.gif rename to _images/test_1.gif diff --git a/assets/img/28a/test_1.png b/_images/test_1.png similarity index 100% rename from assets/img/28a/test_1.png rename to _images/test_1.png diff --git a/assets/img/34/test_2.gif b/_images/test_2.gif similarity index 100% rename from assets/img/34/test_2.gif rename to _images/test_2.gif diff --git a/assets/img/34/test_3.gif b/_images/test_3.gif similarity index 100% rename from assets/img/34/test_3.gif rename to _images/test_3.gif diff --git a/assets/img/34/test_4.gif b/_images/test_4.gif similarity index 100% rename from assets/img/34/test_4.gif rename to _images/test_4.gif diff --git a/assets/img/22a/test_clear.gif b/_images/test_clear.gif similarity index 100% rename from assets/img/22a/test_clear.gif rename to _images/test_clear.gif diff --git a/assets/img/27a/test_edit.gif b/_images/test_edit.gif similarity index 100% rename from assets/img/27a/test_edit.gif rename to _images/test_edit.gif diff --git a/assets/img/28a/test_exam.png b/_images/test_exam.png similarity index 100% rename from assets/img/28a/test_exam.png rename to _images/test_exam.png diff --git a/assets/img/14/test_logged_in.png b/_images/test_logged_in.png similarity index 100% rename from assets/img/14/test_logged_in.png rename to _images/test_logged_in.png diff --git a/assets/img/22a/test_message.gif b/_images/test_message.gif similarity index 100% rename from assets/img/22a/test_message.gif rename to _images/test_message.gif diff --git a/assets/img/28a/test_problems.png b/_images/test_problems.png similarity index 100% rename from assets/img/28a/test_problems.png rename to _images/test_problems.png diff --git a/assets/img/14/test_register.png b/_images/test_register.png similarity index 100% rename from assets/img/14/test_register.png rename to _images/test_register.png diff --git a/assets/img/22a/test_save.gif b/_images/test_save.gif similarity index 100% rename from assets/img/22a/test_save.gif rename to _images/test_save.gif diff --git a/assets/img/23/testing.gif b/_images/testing.gif similarity index 100% rename from assets/img/23/testing.gif rename to _images/testing.gif diff --git a/assets/img/25a/testing.png b/_images/testing.png similarity index 100% rename from assets/img/25a/testing.png rename to _images/testing.png diff --git a/assets/img/24/testing.gif b/_images/testing1.gif similarity index 100% rename from assets/img/24/testing.gif rename to _images/testing1.gif diff --git a/assets/img/26/testing.gif b/_images/testing2.gif similarity index 100% rename from assets/img/26/testing.gif rename to _images/testing2.gif diff --git a/assets/img/26a/testing.gif b/_images/testing3.gif similarity index 100% rename from assets/img/26a/testing.gif rename to _images/testing3.gif diff --git a/assets/img/18a/testing_account.gif b/_images/testing_account.gif similarity index 100% rename from assets/img/18a/testing_account.gif rename to _images/testing_account.gif diff --git a/assets/img/28a/testing_y.png b/_images/testing_y.png similarity index 100% rename from assets/img/28a/testing_y.png rename to _images/testing_y.png diff --git a/assets/img/06/title.gif b/_images/title.gif similarity index 100% rename from assets/img/06/title.gif rename to _images/title.gif diff --git a/assets/img/15/title.gif b/_images/title1.gif similarity index 100% rename from assets/img/15/title.gif rename to _images/title1.gif diff --git a/assets/img/22/title.gif b/_images/title2.gif similarity index 100% rename from assets/img/22/title.gif rename to _images/title2.gif diff --git a/assets/img/08/type_error.png b/_images/type_error.png similarity index 100% rename from assets/img/08/type_error.png rename to _images/type_error.png diff --git a/assets/img/21/user_column.png b/_images/user_column.png similarity index 100% rename from assets/img/21/user_column.png rename to _images/user_column.png diff --git a/assets/img/12/user_icon.png b/_images/user_icon.png similarity index 100% rename from assets/img/12/user_icon.png rename to _images/user_icon.png diff --git a/assets/img/12/user_settings.png b/_images/user_settings.png similarity index 100% rename from assets/img/12/user_settings.png rename to _images/user_settings.png diff --git a/assets/img/12/users.png b/_images/users.png similarity index 100% rename from assets/img/12/users.png rename to _images/users.png diff --git a/assets/img/29/welcome_image.png b/_images/welcome_image.png similarity index 100% rename from assets/img/29/welcome_image.png rename to _images/welcome_image.png diff --git a/assets/img/03/wireframe_account.png b/_images/wireframe_account.png similarity index 100% rename from assets/img/03/wireframe_account.png rename to _images/wireframe_account.png diff --git a/assets/img/03/wireframe_add.png b/_images/wireframe_add.png similarity index 100% rename from assets/img/03/wireframe_add.png rename to _images/wireframe_add.png diff --git a/assets/img/03/wireframe_calendar.png b/_images/wireframe_calendar.png similarity index 100% rename from assets/img/03/wireframe_calendar.png rename to _images/wireframe_calendar.png diff --git a/assets/img/03/wireframe_home.png b/_images/wireframe_home.png similarity index 100% rename from assets/img/03/wireframe_home.png rename to _images/wireframe_home.png diff --git a/assets/img/03/wireframe_home_edit.png b/_images/wireframe_home_edit.png similarity index 100% rename from assets/img/03/wireframe_home_edit.png rename to _images/wireframe_home_edit.png diff --git a/assets/img/03/wireframe_set_details.png b/_images/wireframe_set_details.png similarity index 100% rename from assets/img/03/wireframe_set_details.png rename to _images/wireframe_set_details.png diff --git a/assets/img/03/wireframe_welcome.png b/_images/wireframe_welcome.png similarity index 100% rename from assets/img/03/wireframe_welcome.png rename to _images/wireframe_welcome.png diff --git a/01_introduction.md b/_sources/01_introduction.md.txt similarity index 100% rename from 01_introduction.md rename to _sources/01_introduction.md.txt diff --git a/02_web_intro.md b/_sources/02_web_intro.md.txt similarity index 100% rename from 02_web_intro.md rename to _sources/02_web_intro.md.txt diff --git a/03_studyM8_design.md b/_sources/03_studyM8_design.md.txt similarity index 100% rename from 03_studyM8_design.md rename to _sources/03_studyM8_design.md.txt diff --git a/04_create_app.md b/_sources/04_create_app.md.txt similarity index 100% rename from 04_create_app.md rename to _sources/04_create_app.md.txt diff --git a/05_anvil_interface.md b/_sources/05_anvil_interface.md.txt similarity index 100% rename from 05_anvil_interface.md rename to _sources/05_anvil_interface.md.txt diff --git a/06_create_home_form.md b/_sources/06_create_home_form.md.txt similarity index 100% rename from 06_create_home_form.md rename to _sources/06_create_home_form.md.txt diff --git a/07_create_components.md b/_sources/07_create_components.md.txt similarity index 100% rename from 07_create_components.md rename to _sources/07_create_components.md.txt diff --git a/08_connect_links.md b/_sources/08_connect_links.md.txt similarity index 100% rename from 08_connect_links.md rename to _sources/08_connect_links.md.txt diff --git a/09_set_start_component.md b/_sources/09_set_start_component.md.txt similarity index 100% rename from 09_set_start_component.md rename to _sources/09_set_start_component.md.txt diff --git a/10_adjust_title.md b/_sources/10_adjust_title.md.txt similarity index 100% rename from 10_adjust_title.md rename to _sources/10_adjust_title.md.txt diff --git a/11_active_link.md b/_sources/11_active_link.md.txt similarity index 100% rename from 11_active_link.md rename to _sources/11_active_link.md.txt diff --git a/12_add_users.md b/_sources/12_add_users.md.txt similarity index 100% rename from 12_add_users.md rename to _sources/12_add_users.md.txt diff --git a/13_user_features.md b/_sources/13_user_features.md.txt similarity index 100% rename from 13_user_features.md rename to _sources/13_user_features.md.txt diff --git a/14_account_links_visiability.md b/_sources/14_account_links_visiability.md.txt similarity index 100% rename from 14_account_links_visiability.md rename to _sources/14_account_links_visiability.md.txt diff --git a/15_set_details.md b/_sources/15_set_details.md.txt similarity index 100% rename from 15_set_details.md rename to _sources/15_set_details.md.txt diff --git a/15a_set_details_code.md b/_sources/15a_set_details_code.md.txt similarity index 100% rename from 15a_set_details_code.md rename to _sources/15a_set_details_code.md.txt diff --git a/16_user_service.md b/_sources/16_user_service.md.txt similarity index 100% rename from 16_user_service.md rename to _sources/16_user_service.md.txt diff --git a/17_set_details_nav.md b/_sources/17_set_details_nav.md.txt similarity index 100% rename from 17_set_details_nav.md rename to _sources/17_set_details_nav.md.txt diff --git a/18_account_cmpt.md b/_sources/18_account_cmpt.md.txt similarity index 100% rename from 18_account_cmpt.md rename to _sources/18_account_cmpt.md.txt diff --git a/18a_account_code.md b/_sources/18a_account_code.md.txt similarity index 100% rename from 18a_account_code.md rename to _sources/18a_account_code.md.txt diff --git a/19_switch_components.md b/_sources/19_switch_components.md.txt similarity index 100% rename from 19_switch_components.md rename to _sources/19_switch_components.md.txt diff --git a/20_complete_account_cmpt.md b/_sources/20_complete_account_cmpt.md.txt similarity index 100% rename from 20_complete_account_cmpt.md rename to _sources/20_complete_account_cmpt.md.txt diff --git a/21_assessment_table.md b/_sources/21_assessment_table.md.txt similarity index 100% rename from 21_assessment_table.md rename to _sources/21_assessment_table.md.txt diff --git a/22_add_component.md b/_sources/22_add_component.md.txt similarity index 100% rename from 22_add_component.md rename to _sources/22_add_component.md.txt diff --git a/22a_add_component_code.md b/_sources/22a_add_component_code.md.txt similarity index 100% rename from 22a_add_component_code.md rename to _sources/22a_add_component_code.md.txt diff --git a/23_assessment_service.md b/_sources/23_assessment_service.md.txt similarity index 100% rename from 23_assessment_service.md rename to _sources/23_assessment_service.md.txt diff --git a/24_restrict_accessibility.md b/_sources/24_restrict_accessibility.md.txt similarity index 100% rename from 24_restrict_accessibility.md rename to _sources/24_restrict_accessibility.md.txt diff --git a/25_home_user_cmpt.md b/_sources/25_home_user_cmpt.md.txt similarity index 100% rename from 25_home_user_cmpt.md rename to _sources/25_home_user_cmpt.md.txt diff --git a/25a_home_component_code.md b/_sources/25a_home_component_code.md.txt similarity index 100% rename from 25a_home_component_code.md rename to _sources/25a_home_component_code.md.txt diff --git a/26_complete_assessment.md b/_sources/26_complete_assessment.md.txt similarity index 100% rename from 26_complete_assessment.md rename to _sources/26_complete_assessment.md.txt diff --git a/26a_adjust_assessment_display.md b/_sources/26a_adjust_assessment_display.md.txt similarity index 100% rename from 26a_adjust_assessment_display.md rename to _sources/26a_adjust_assessment_display.md.txt diff --git a/27_edit_assessment.md b/_sources/27_edit_assessment.md.txt similarity index 100% rename from 27_edit_assessment.md rename to _sources/27_edit_assessment.md.txt diff --git a/27a_edit_assessment_code.md b/_sources/27a_edit_assessment_code.md.txt similarity index 100% rename from 27a_edit_assessment_code.md rename to _sources/27a_edit_assessment_code.md.txt diff --git a/28_calendar_cmpt.md b/_sources/28_calendar_cmpt.md.txt similarity index 100% rename from 28_calendar_cmpt.md rename to _sources/28_calendar_cmpt.md.txt diff --git a/28a_calendar_cmpt_code.md b/_sources/28a_calendar_cmpt_code.md.txt similarity index 100% rename from 28a_calendar_cmpt_code.md rename to _sources/28a_calendar_cmpt_code.md.txt diff --git a/29_welcome.md b/_sources/29_welcome.md.txt similarity index 100% rename from 29_welcome.md rename to _sources/29_welcome.md.txt diff --git a/30_optimisation.md b/_sources/30_optimisation.md.txt similarity index 100% rename from 30_optimisation.md rename to _sources/30_optimisation.md.txt diff --git a/31_remote_database.md b/_sources/31_remote_database.md.txt similarity index 100% rename from 31_remote_database.md rename to _sources/31_remote_database.md.txt diff --git a/32_logout_func.md b/_sources/32_logout_func.md.txt similarity index 100% rename from 32_logout_func.md rename to _sources/32_logout_func.md.txt diff --git a/33_update_user_func.md b/_sources/33_update_user_func.md.txt similarity index 100% rename from 33_update_user_func.md rename to _sources/33_update_user_func.md.txt diff --git a/34_my_assess_func.md b/_sources/34_my_assess_func.md.txt similarity index 100% rename from 34_my_assess_func.md rename to _sources/34_my_assess_func.md.txt diff --git a/34a_get_chart_func.md b/_sources/34a_get_chart_func.md.txt similarity index 100% rename from 34a_get_chart_func.md rename to _sources/34a_get_chart_func.md.txt diff --git a/35_local_database.md b/_sources/35_local_database.md.txt similarity index 100% rename from 35_local_database.md rename to _sources/35_local_database.md.txt diff --git a/98_resources.md b/_sources/98_resources.md.txt similarity index 100% rename from 98_resources.md rename to _sources/98_resources.md.txt diff --git a/99_topics.md b/_sources/99_topics.md.txt similarity index 100% rename from 99_topics.md rename to _sources/99_topics.md.txt diff --git a/index.md b/_sources/index.md.txt similarity index 100% rename from index.md rename to _sources/index.md.txt diff --git a/licencing.md b/_sources/licencing.md.txt similarity index 100% rename from licencing.md rename to _sources/licencing.md.txt diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000..f316efc --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/check-solid.svg b/_static/check-solid.svg new file mode 100644 index 0000000..92fad4b --- /dev/null +++ b/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/_static/clipboard.min.js b/_static/clipboard.min.js new file mode 100644 index 0000000..54b3c46 --- /dev/null +++ b/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/_static/copybutton.css b/_static/copybutton.css new file mode 100644 index 0000000..f1916ec --- /dev/null +++ b/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 0000000..2ea7ff3 --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 0000000..dbe1aaa --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/debug.css b/_static/debug.css new file mode 100644 index 0000000..74d4aec --- /dev/null +++ b/_static/debug.css @@ -0,0 +1,69 @@ +/* + This CSS file should be overridden by the theme authors. It's + meant for debugging and developing the skeleton that this theme provides. +*/ +body { + font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji"; + background: lavender; +} +.sb-announcement { + background: rgb(131, 131, 131); +} +.sb-announcement__inner { + background: black; + color: white; +} +.sb-header { + background: lightskyblue; +} +.sb-header__inner { + background: royalblue; + color: white; +} +.sb-header-secondary { + background: lightcyan; +} +.sb-header-secondary__inner { + background: cornflowerblue; + color: white; +} +.sb-sidebar-primary { + background: lightgreen; +} +.sb-main { + background: blanchedalmond; +} +.sb-main__inner { + background: antiquewhite; +} +.sb-header-article { + background: lightsteelblue; +} +.sb-article-container { + background: snow; +} +.sb-article-main { + background: white; +} +.sb-footer-article { + background: lightpink; +} +.sb-sidebar-secondary { + background: lightgoldenrodyellow; +} +.sb-footer-content { + background: plum; +} +.sb-footer-content__inner { + background: palevioletred; +} +.sb-footer { + background: pink; +} +.sb-footer__inner { + background: salmon; +} +.sb-article { + background: white; +} diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..4d67807 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000..eb981e6 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '2024', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 0000000..367b8ed --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/assets/img/logo.ico b/_static/logo.ico similarity index 100% rename from assets/img/logo.ico rename to _static/logo.ico diff --git a/assets/img/logo.png b/_static/logo.png similarity index 100% rename from assets/img/logo.png rename to _static/logo.png diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 0000000..9d1083b --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,250 @@ +.highlight pre { line-height: 125%; } +.highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #fdf2e2 } +.highlight { background: #f2f2f2; color: #1E1E1E } +.highlight .c { color: #515151 } /* Comment */ +.highlight .err { color: #D71835 } /* Error */ +.highlight .k { color: #8045E5 } /* Keyword */ +.highlight .l { color: #7F4707 } /* Literal */ +.highlight .n { color: #1E1E1E } /* Name */ +.highlight .o { color: #163 } /* Operator */ +.highlight .p { color: #1E1E1E } /* Punctuation */ +.highlight .ch { color: #515151 } /* Comment.Hashbang */ +.highlight .cm { color: #515151 } /* Comment.Multiline */ +.highlight .cp { color: #515151 } /* Comment.Preproc */ +.highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +.highlight .c1 { color: #515151 } /* Comment.Single */ +.highlight .cs { color: #515151 } /* Comment.Special */ +.highlight .gd { color: #00749C } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gh { color: #00749C } /* Generic.Heading */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #00749C } /* Generic.Subheading */ +.highlight .kc { color: #8045E5 } /* Keyword.Constant */ +.highlight .kd { color: #8045E5 } /* Keyword.Declaration */ +.highlight .kn { color: #8045E5 } /* Keyword.Namespace */ +.highlight .kp { color: #8045E5 } /* Keyword.Pseudo */ +.highlight .kr { color: #8045E5 } /* Keyword.Reserved */ +.highlight .kt { color: #7F4707 } /* Keyword.Type */ +.highlight .ld { color: #7F4707 } /* Literal.Date */ +.highlight .m { color: #7F4707 } /* Literal.Number */ +.highlight .s { color: #163 } /* Literal.String */ +.highlight .na { color: #7F4707 } /* Name.Attribute */ +.highlight .nb { color: #7F4707 } /* Name.Builtin */ +.highlight .nc { color: #00749C } /* Name.Class */ +.highlight .no { color: #00749C } /* Name.Constant */ +.highlight .nd { color: #7F4707 } /* Name.Decorator */ +.highlight .ni { color: #163 } /* Name.Entity */ +.highlight .ne { color: #8045E5 } /* Name.Exception */ +.highlight .nf { color: #00749C } /* Name.Function */ +.highlight .nl { color: #7F4707 } /* Name.Label */ +.highlight .nn { color: #1E1E1E } /* Name.Namespace */ +.highlight .nx { color: #1E1E1E } /* Name.Other */ +.highlight .py { color: #00749C } /* Name.Property */ +.highlight .nt { color: #00749C } /* Name.Tag */ +.highlight .nv { color: #D71835 } /* Name.Variable */ +.highlight .ow { color: #8045E5 } /* Operator.Word */ +.highlight .pm { color: #1E1E1E } /* Punctuation.Marker */ +.highlight .w { color: #1E1E1E } /* Text.Whitespace */ +.highlight .mb { color: #7F4707 } /* Literal.Number.Bin */ +.highlight .mf { color: #7F4707 } /* Literal.Number.Float */ +.highlight .mh { color: #7F4707 } /* Literal.Number.Hex */ +.highlight .mi { color: #7F4707 } /* Literal.Number.Integer */ +.highlight .mo { color: #7F4707 } /* Literal.Number.Oct */ +.highlight .sa { color: #163 } /* Literal.String.Affix */ +.highlight .sb { color: #163 } /* Literal.String.Backtick */ +.highlight .sc { color: #163 } /* Literal.String.Char */ +.highlight .dl { color: #163 } /* Literal.String.Delimiter */ +.highlight .sd { color: #163 } /* Literal.String.Doc */ +.highlight .s2 { color: #163 } /* Literal.String.Double */ +.highlight .se { color: #163 } /* Literal.String.Escape */ +.highlight .sh { color: #163 } /* Literal.String.Heredoc */ +.highlight .si { color: #163 } /* Literal.String.Interpol */ +.highlight .sx { color: #163 } /* Literal.String.Other */ +.highlight .sr { color: #D71835 } /* Literal.String.Regex */ +.highlight .s1 { color: #163 } /* Literal.String.Single */ +.highlight .ss { color: #00749C } /* Literal.String.Symbol */ +.highlight .bp { color: #7F4707 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00749C } /* Name.Function.Magic */ +.highlight .vc { color: #D71835 } /* Name.Variable.Class */ +.highlight .vg { color: #D71835 } /* Name.Variable.Global */ +.highlight .vi { color: #D71835 } /* Name.Variable.Instance */ +.highlight .vm { color: #7F4707 } /* Name.Variable.Magic */ +.highlight .il { color: #7F4707 } /* Literal.Number.Integer.Long */ +@media not print { +body[data-theme="dark"] .highlight pre { line-height: 125%; } +body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight .hll { background-color: #404040 } +body[data-theme="dark"] .highlight { background: #202020; color: #D0D0D0 } +body[data-theme="dark"] .highlight .c { color: #ABABAB; font-style: italic } /* Comment */ +body[data-theme="dark"] .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */ +body[data-theme="dark"] .highlight .esc { color: #D0D0D0 } /* Escape */ +body[data-theme="dark"] .highlight .g { color: #D0D0D0 } /* Generic */ +body[data-theme="dark"] .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */ +body[data-theme="dark"] .highlight .l { color: #D0D0D0 } /* Literal */ +body[data-theme="dark"] .highlight .n { color: #D0D0D0 } /* Name */ +body[data-theme="dark"] .highlight .o { color: #D0D0D0 } /* Operator */ +body[data-theme="dark"] .highlight .x { color: #D0D0D0 } /* Other */ +body[data-theme="dark"] .highlight .p { color: #D0D0D0 } /* Punctuation */ +body[data-theme="dark"] .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */ +body[data-theme="dark"] .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */ +body[data-theme="dark"] .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */ +body[data-theme="dark"] .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */ +body[data-theme="dark"] .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */ +body[data-theme="dark"] .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body[data-theme="dark"] .highlight .gd { color: #FF3A3A } /* Generic.Deleted */ +body[data-theme="dark"] .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */ +body[data-theme="dark"] .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body[data-theme="dark"] .highlight .gr { color: #FF3A3A } /* Generic.Error */ +body[data-theme="dark"] .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */ +body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */ +body[data-theme="dark"] .highlight .go { color: #CCC } /* Generic.Output */ +body[data-theme="dark"] .highlight .gp { color: #AAA } /* Generic.Prompt */ +body[data-theme="dark"] .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */ +body[data-theme="dark"] .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */ +body[data-theme="dark"] .highlight .gt { color: #FF3A3A } /* Generic.Traceback */ +body[data-theme="dark"] .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */ +body[data-theme="dark"] .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */ +body[data-theme="dark"] .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */ +body[data-theme="dark"] .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */ +body[data-theme="dark"] .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */ +body[data-theme="dark"] .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */ +body[data-theme="dark"] .highlight .ld { color: #D0D0D0 } /* Literal.Date */ +body[data-theme="dark"] .highlight .m { color: #51B2FD } /* Literal.Number */ +body[data-theme="dark"] .highlight .s { color: #ED9D13 } /* Literal.String */ +body[data-theme="dark"] .highlight .na { color: #BBB } /* Name.Attribute */ +body[data-theme="dark"] .highlight .nb { color: #2FBCCD } /* Name.Builtin */ +body[data-theme="dark"] .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */ +body[data-theme="dark"] .highlight .no { color: #40FFFF } /* Name.Constant */ +body[data-theme="dark"] .highlight .nd { color: #FFA500 } /* Name.Decorator */ +body[data-theme="dark"] .highlight .ni { color: #D0D0D0 } /* Name.Entity */ +body[data-theme="dark"] .highlight .ne { color: #BBB } /* Name.Exception */ +body[data-theme="dark"] .highlight .nf { color: #71ADFF } /* Name.Function */ +body[data-theme="dark"] .highlight .nl { color: #D0D0D0 } /* Name.Label */ +body[data-theme="dark"] .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */ +body[data-theme="dark"] .highlight .nx { color: #D0D0D0 } /* Name.Other */ +body[data-theme="dark"] .highlight .py { color: #D0D0D0 } /* Name.Property */ +body[data-theme="dark"] .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */ +body[data-theme="dark"] .highlight .nv { color: #40FFFF } /* Name.Variable */ +body[data-theme="dark"] .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */ +body[data-theme="dark"] .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */ +body[data-theme="dark"] .highlight .w { color: #666 } /* Text.Whitespace */ +body[data-theme="dark"] .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */ +body[data-theme="dark"] .highlight .mf { color: #51B2FD } /* Literal.Number.Float */ +body[data-theme="dark"] .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */ +body[data-theme="dark"] .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */ +body[data-theme="dark"] .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */ +body[data-theme="dark"] .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */ +body[data-theme="dark"] .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */ +body[data-theme="dark"] .highlight .sc { color: #ED9D13 } /* Literal.String.Char */ +body[data-theme="dark"] .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */ +body[data-theme="dark"] .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */ +body[data-theme="dark"] .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */ +body[data-theme="dark"] .highlight .se { color: #ED9D13 } /* Literal.String.Escape */ +body[data-theme="dark"] .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */ +body[data-theme="dark"] .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */ +body[data-theme="dark"] .highlight .sx { color: #FFA500 } /* Literal.String.Other */ +body[data-theme="dark"] .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */ +body[data-theme="dark"] .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */ +body[data-theme="dark"] .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */ +body[data-theme="dark"] .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */ +body[data-theme="dark"] .highlight .fm { color: #71ADFF } /* Name.Function.Magic */ +body[data-theme="dark"] .highlight .vc { color: #40FFFF } /* Name.Variable.Class */ +body[data-theme="dark"] .highlight .vg { color: #40FFFF } /* Name.Variable.Global */ +body[data-theme="dark"] .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */ +body[data-theme="dark"] .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */ +body[data-theme="dark"] .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */ +@media (prefers-color-scheme: dark) { +body:not([data-theme="light"]) .highlight pre { line-height: 125%; } +body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight .hll { background-color: #404040 } +body:not([data-theme="light"]) .highlight { background: #202020; color: #D0D0D0 } +body:not([data-theme="light"]) .highlight .c { color: #ABABAB; font-style: italic } /* Comment */ +body:not([data-theme="light"]) .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */ +body:not([data-theme="light"]) .highlight .esc { color: #D0D0D0 } /* Escape */ +body:not([data-theme="light"]) .highlight .g { color: #D0D0D0 } /* Generic */ +body:not([data-theme="light"]) .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */ +body:not([data-theme="light"]) .highlight .l { color: #D0D0D0 } /* Literal */ +body:not([data-theme="light"]) .highlight .n { color: #D0D0D0 } /* Name */ +body:not([data-theme="light"]) .highlight .o { color: #D0D0D0 } /* Operator */ +body:not([data-theme="light"]) .highlight .x { color: #D0D0D0 } /* Other */ +body:not([data-theme="light"]) .highlight .p { color: #D0D0D0 } /* Punctuation */ +body:not([data-theme="light"]) .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */ +body:not([data-theme="light"]) .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */ +body:not([data-theme="light"]) .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */ +body:not([data-theme="light"]) .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */ +body:not([data-theme="light"]) .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */ +body:not([data-theme="light"]) .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body:not([data-theme="light"]) .highlight .gd { color: #FF3A3A } /* Generic.Deleted */ +body:not([data-theme="light"]) .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */ +body:not([data-theme="light"]) .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body:not([data-theme="light"]) .highlight .gr { color: #FF3A3A } /* Generic.Error */ +body:not([data-theme="light"]) .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */ +body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */ +body:not([data-theme="light"]) .highlight .go { color: #CCC } /* Generic.Output */ +body:not([data-theme="light"]) .highlight .gp { color: #AAA } /* Generic.Prompt */ +body:not([data-theme="light"]) .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */ +body:not([data-theme="light"]) .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */ +body:not([data-theme="light"]) .highlight .gt { color: #FF3A3A } /* Generic.Traceback */ +body:not([data-theme="light"]) .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */ +body:not([data-theme="light"]) .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */ +body:not([data-theme="light"]) .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */ +body:not([data-theme="light"]) .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */ +body:not([data-theme="light"]) .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */ +body:not([data-theme="light"]) .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */ +body:not([data-theme="light"]) .highlight .ld { color: #D0D0D0 } /* Literal.Date */ +body:not([data-theme="light"]) .highlight .m { color: #51B2FD } /* Literal.Number */ +body:not([data-theme="light"]) .highlight .s { color: #ED9D13 } /* Literal.String */ +body:not([data-theme="light"]) .highlight .na { color: #BBB } /* Name.Attribute */ +body:not([data-theme="light"]) .highlight .nb { color: #2FBCCD } /* Name.Builtin */ +body:not([data-theme="light"]) .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */ +body:not([data-theme="light"]) .highlight .no { color: #40FFFF } /* Name.Constant */ +body:not([data-theme="light"]) .highlight .nd { color: #FFA500 } /* Name.Decorator */ +body:not([data-theme="light"]) .highlight .ni { color: #D0D0D0 } /* Name.Entity */ +body:not([data-theme="light"]) .highlight .ne { color: #BBB } /* Name.Exception */ +body:not([data-theme="light"]) .highlight .nf { color: #71ADFF } /* Name.Function */ +body:not([data-theme="light"]) .highlight .nl { color: #D0D0D0 } /* Name.Label */ +body:not([data-theme="light"]) .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */ +body:not([data-theme="light"]) .highlight .nx { color: #D0D0D0 } /* Name.Other */ +body:not([data-theme="light"]) .highlight .py { color: #D0D0D0 } /* Name.Property */ +body:not([data-theme="light"]) .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */ +body:not([data-theme="light"]) .highlight .nv { color: #40FFFF } /* Name.Variable */ +body:not([data-theme="light"]) .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */ +body:not([data-theme="light"]) .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */ +body:not([data-theme="light"]) .highlight .w { color: #666 } /* Text.Whitespace */ +body:not([data-theme="light"]) .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */ +body:not([data-theme="light"]) .highlight .mf { color: #51B2FD } /* Literal.Number.Float */ +body:not([data-theme="light"]) .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */ +body:not([data-theme="light"]) .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */ +body:not([data-theme="light"]) .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */ +body:not([data-theme="light"]) .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */ +body:not([data-theme="light"]) .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */ +body:not([data-theme="light"]) .highlight .sc { color: #ED9D13 } /* Literal.String.Char */ +body:not([data-theme="light"]) .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */ +body:not([data-theme="light"]) .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */ +body:not([data-theme="light"]) .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */ +body:not([data-theme="light"]) .highlight .se { color: #ED9D13 } /* Literal.String.Escape */ +body:not([data-theme="light"]) .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */ +body:not([data-theme="light"]) .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */ +body:not([data-theme="light"]) .highlight .sx { color: #FFA500 } /* Literal.String.Other */ +body:not([data-theme="light"]) .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */ +body:not([data-theme="light"]) .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */ +body:not([data-theme="light"]) .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */ +body:not([data-theme="light"]) .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */ +body:not([data-theme="light"]) .highlight .fm { color: #71ADFF } /* Name.Function.Magic */ +body:not([data-theme="light"]) .highlight .vc { color: #40FFFF } /* Name.Variable.Class */ +body:not([data-theme="light"]) .highlight .vg { color: #40FFFF } /* Name.Variable.Global */ +body:not([data-theme="light"]) .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */ +body:not([data-theme="light"]) .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */ +body:not([data-theme="light"]) .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */ +} +} \ No newline at end of file diff --git a/_static/scripts/furo-extensions.js b/_static/scripts/furo-extensions.js new file mode 100644 index 0000000..e69de29 diff --git a/_static/scripts/furo.js b/_static/scripts/furo.js new file mode 100644 index 0000000..87e1767 --- /dev/null +++ b/_static/scripts/furo.js @@ -0,0 +1,3 @@ +/*! For license information please see furo.js.LICENSE.txt */ +(()=>{var t={856:function(t,e,n){var o,r;r=void 0!==n.g?n.g:"undefined"!=typeof window?window:this,o=function(){return function(t){"use strict";var e={navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:0,reflow:!1,events:!0},n=function(t,e,n){if(n.settings.events){var o=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(o)}},o=function(t){var e=0;if(t.offsetParent)for(;t;)e+=t.offsetTop,t=t.offsetParent;return e>=0?e:0},r=function(t){t&&t.sort(function(t,e){return o(t.content)=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},l=function(t,e){var n=t[t.length-1];if(function(t,e){return!(!s()||!c(t.content,e,!0))}(n,e))return n;for(var o=t.length-1;o>=0;o--)if(c(t[o].content,e))return t[o]},a=function(t,e){if(e.nested&&t.parentNode){var n=t.parentNode.closest("li");n&&(n.classList.remove(e.nestedClass),a(n,e))}},i=function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.remove(e.navClass),t.content.classList.remove(e.contentClass),a(o,e),n("gumshoeDeactivate",o,{link:t.nav,content:t.content,settings:e}))}},u=function(t,e){if(e.nested){var n=t.parentNode.closest("li");n&&(n.classList.add(e.nestedClass),u(n,e))}};return function(o,c){var s,a,d,f,m,v={setup:function(){s=document.querySelectorAll(o),a=[],Array.prototype.forEach.call(s,function(t){var e=document.getElementById(decodeURIComponent(t.hash.substr(1)));e&&a.push({nav:t,content:e})}),r(a)},detect:function(){var t=l(a,m);t?d&&t.content===d.content||(i(d,m),function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.add(e.navClass),t.content.classList.add(e.contentClass),u(o,e),n("gumshoeActivate",o,{link:t.nav,content:t.content,settings:e}))}}(t,m),d=t):d&&(i(d,m),d=null)}},h=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame(v.detect)},g=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame(function(){r(a),v.detect()})};return v.destroy=function(){d&&i(d,m),t.removeEventListener("scroll",h,!1),m.reflow&&t.removeEventListener("resize",g,!1),a=null,s=null,d=null,f=null,m=null},m=function(){var t={};return Array.prototype.forEach.call(arguments,function(e){for(var n in e){if(!e.hasOwnProperty(n))return;t[n]=e[n]}}),t}(e,c||{}),v.setup(),v.detect(),t.addEventListener("scroll",h,!1),m.reflow&&t.addEventListener("resize",g,!1),v}}(r)}.apply(e,[]),void 0===o||(t.exports=o)}},e={};function n(o){var r=e[o];if(void 0!==r)return r.exports;var c=e[o]={exports:{}};return t[o].call(c.exports,c,c.exports,n),c.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=n(856),e=n.n(t),o=null,r=null,c=document.documentElement.scrollTop;function s(){const t=localStorage.getItem("theme")||"auto";var e;"light"!==(e=window.matchMedia("(prefers-color-scheme: dark)").matches?"auto"===t?"light":"light"==t?"dark":"auto":"auto"===t?"dark":"dark"==t?"light":"auto")&&"dark"!==e&&"auto"!==e&&(console.error(`Got invalid theme mode: ${e}. Resetting to auto.`),e="auto"),document.body.dataset.theme=e,localStorage.setItem("theme",e),console.log(`Changed to ${e} mode.`)}function l(){!function(){const t=document.getElementsByClassName("theme-toggle");Array.from(t).forEach(t=>{t.addEventListener("click",s)})}(),function(){let t=0,e=!1;window.addEventListener("scroll",function(n){t=window.scrollY,e||(window.requestAnimationFrame(function(){var n;(function(t){t>0?r.classList.add("scrolled"):r.classList.remove("scrolled")})(n=t),function(t){t<64?document.documentElement.classList.remove("show-back-to-top"):tc&&document.documentElement.classList.remove("show-back-to-top"),c=t}(n),function(t){null!==o&&(0==t?o.scrollTo(0,0):Math.ceil(t)>=Math.floor(document.documentElement.scrollHeight-window.innerHeight)?o.scrollTo(0,o.scrollHeight):document.querySelector(".scroll-current"))}(n),e=!1}),e=!0)}),window.scroll()}(),null!==o&&new(e())(".toc-tree a",{reflow:!0,recursive:!0,navClass:"scroll-current",offset:()=>{let t=parseFloat(getComputedStyle(document.documentElement).fontSize);const e=r.getBoundingClientRect();return e.top+e.height+2.5*t+1}})}document.addEventListener("DOMContentLoaded",function(){document.body.parentNode.classList.remove("no-js"),r=document.querySelector("header"),o=document.querySelector(".toc-scroll"),l()})})()})(); +//# sourceMappingURL=furo.js.map \ No newline at end of file diff --git a/_static/scripts/furo.js.LICENSE.txt b/_static/scripts/furo.js.LICENSE.txt new file mode 100644 index 0000000..1632189 --- /dev/null +++ b/_static/scripts/furo.js.LICENSE.txt @@ -0,0 +1,7 @@ +/*! + * gumshoejs v5.1.2 (patched by @pradyunsg) + * A simple, framework-agnostic scrollspy script. + * (c) 2019 Chris Ferdinandi + * MIT License + * http://github.com/cferdinandi/gumshoe + */ diff --git a/_static/scripts/furo.js.map b/_static/scripts/furo.js.map new file mode 100644 index 0000000..3b316f3 --- /dev/null +++ b/_static/scripts/furo.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/furo.js","mappings":";iCAAA,MAQWA,SAWS,IAAX,EAAAC,EACH,EAAAA,EACkB,oBAAXC,OACLA,OACAC,KAbO,EAAF,WACP,OAaJ,SAAUD,GACR,aAMA,IAAIE,EAAW,CAEbC,SAAU,SACVC,aAAc,SAGdC,QAAQ,EACRC,YAAa,SAGbC,OAAQ,EACRC,QAAQ,EAGRC,QAAQ,GA6BNC,EAAY,SAAUC,EAAMC,EAAMC,GAEpC,GAAKA,EAAOC,SAASL,OAArB,CAGA,IAAIM,EAAQ,IAAIC,YAAYL,EAAM,CAChCM,SAAS,EACTC,YAAY,EACZL,OAAQA,IAIVD,EAAKO,cAAcJ,EAVgB,CAWrC,EAOIK,EAAe,SAAUR,GAC3B,IAAIS,EAAW,EACf,GAAIT,EAAKU,aACP,KAAOV,GACLS,GAAYT,EAAKW,UACjBX,EAAOA,EAAKU,aAGhB,OAAOD,GAAY,EAAIA,EAAW,CACpC,EAMIG,EAAe,SAAUC,GACvBA,GACFA,EAASC,KAAK,SAAUC,EAAOC,GAG7B,OAFcR,EAAaO,EAAME,SACnBT,EAAaQ,EAAMC,UACF,EACxB,CACT,EAEJ,EAwCIC,EAAW,SAAUlB,EAAME,EAAUiB,GACvC,IAAIC,EAASpB,EAAKqB,wBACd1B,EAnCU,SAAUO,GAExB,MAA+B,mBAApBA,EAASP,OACX2B,WAAWpB,EAASP,UAItB2B,WAAWpB,EAASP,OAC7B,CA2Be4B,CAAUrB,GACvB,OAAIiB,EAEAK,SAASJ,EAAOD,OAAQ,KACvB/B,EAAOqC,aAAeC,SAASC,gBAAgBC,cAG7CJ,SAASJ,EAAOS,IAAK,KAAOlC,CACrC,EAMImC,EAAa,WACf,OACEC,KAAKC,KAAK5C,EAAOqC,YAAcrC,EAAO6C,cAnCjCF,KAAKG,IACVR,SAASS,KAAKC,aACdV,SAASC,gBAAgBS,aACzBV,SAASS,KAAKE,aACdX,SAASC,gBAAgBU,aACzBX,SAASS,KAAKP,aACdF,SAASC,gBAAgBC,aAkC7B,EAmBIU,EAAY,SAAUzB,EAAUX,GAClC,IAAIqC,EAAO1B,EAASA,EAAS2B,OAAS,GACtC,GAbgB,SAAUC,EAAMvC,GAChC,SAAI4B,MAAgBZ,EAASuB,EAAKxB,QAASf,GAAU,GAEvD,CAUMwC,CAAYH,EAAMrC,GAAW,OAAOqC,EACxC,IAAK,IAAII,EAAI9B,EAAS2B,OAAS,EAAGG,GAAK,EAAGA,IACxC,GAAIzB,EAASL,EAAS8B,GAAG1B,QAASf,GAAW,OAAOW,EAAS8B,EAEjE,EAOIC,EAAmB,SAAUC,EAAK3C,GAEpC,GAAKA,EAAST,QAAWoD,EAAIC,WAA7B,CAGA,IAAIC,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASR,aAG7BkD,EAAiBG,EAAI7C,GAV0B,CAWjD,EAOIiD,EAAa,SAAUC,EAAOlD,GAEhC,GAAKkD,EAAL,CAGA,IAAIL,EAAKK,EAAMP,IAAIG,QAAQ,MACtBD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASX,UAC7B6D,EAAMnC,QAAQgC,UAAUC,OAAOhD,EAASV,cAGxCoD,EAAiBG,EAAI7C,GAGrBJ,EAAU,oBAAqBiD,EAAI,CACjCM,KAAMD,EAAMP,IACZ5B,QAASmC,EAAMnC,QACff,SAAUA,IAjBM,CAmBpB,EAOIoD,EAAiB,SAAUT,EAAK3C,GAElC,GAAKA,EAAST,OAAd,CAGA,IAAIsD,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASR,aAG1B4D,EAAeP,EAAI7C,GAVS,CAW9B,EA6LA,OA1JkB,SAAUsD,EAAUC,GAKpC,IACIC,EAAU7C,EAAU8C,EAASC,EAAS1D,EADtC2D,EAAa,CAUjBA,MAAmB,WAEjBH,EAAWhC,SAASoC,iBAAiBN,GAGrC3C,EAAW,GAGXkD,MAAMC,UAAUC,QAAQC,KAAKR,EAAU,SAAUjB,GAE/C,IAAIxB,EAAUS,SAASyC,eACrBC,mBAAmB3B,EAAK4B,KAAKC,OAAO,KAEjCrD,GAGLJ,EAAS0D,KAAK,CACZ1B,IAAKJ,EACLxB,QAASA,GAEb,GAGAL,EAAaC,EACf,EAKAgD,OAAoB,WAElB,IAAIW,EAASlC,EAAUzB,EAAUX,GAG5BsE,EASDb,GAAWa,EAAOvD,UAAY0C,EAAQ1C,UAG1CkC,EAAWQ,EAASzD,GAzFT,SAAUkD,EAAOlD,GAE9B,GAAKkD,EAAL,CAGA,IAAIL,EAAKK,EAAMP,IAAIG,QAAQ,MACtBD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASX,UAC1B6D,EAAMnC,QAAQgC,UAAUM,IAAIrD,EAASV,cAGrC8D,EAAeP,EAAI7C,GAGnBJ,EAAU,kBAAmBiD,EAAI,CAC/BM,KAAMD,EAAMP,IACZ5B,QAASmC,EAAMnC,QACff,SAAUA,IAjBM,CAmBpB,CAqEIuE,CAASD,EAAQtE,GAGjByD,EAAUa,GAfJb,IACFR,EAAWQ,EAASzD,GACpByD,EAAU,KAchB,GAMIe,EAAgB,SAAUvE,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,sBAAsBf,EAAWgB,OACpD,EAMIC,EAAgB,SAAU3E,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,sBAAsB,WACrChE,EAAaC,GACbgD,EAAWgB,QACb,EACF,EAkDA,OA7CAhB,EAAWkB,QAAU,WAEfpB,GACFR,EAAWQ,EAASzD,GAItBd,EAAO4F,oBAAoB,SAAUN,GAAe,GAChDxE,EAASN,QACXR,EAAO4F,oBAAoB,SAAUF,GAAe,GAItDjE,EAAW,KACX6C,EAAW,KACXC,EAAU,KACVC,EAAU,KACV1D,EAAW,IACb,EAOEA,EA3XS,WACX,IAAI+E,EAAS,CAAC,EAOd,OANAlB,MAAMC,UAAUC,QAAQC,KAAKgB,UAAW,SAAUC,GAChD,IAAK,IAAIC,KAAOD,EAAK,CACnB,IAAKA,EAAIE,eAAeD,GAAM,OAC9BH,EAAOG,GAAOD,EAAIC,EACpB,CACF,GACOH,CACT,CAkXeK,CAAOhG,EAAUmE,GAAW,CAAC,GAGxCI,EAAW0B,QAGX1B,EAAWgB,SAGXzF,EAAOoG,iBAAiB,SAAUd,GAAe,GAC7CxE,EAASN,QACXR,EAAOoG,iBAAiB,SAAUV,GAAe,GAS9CjB,CACT,CAOF,CArcW4B,CAAQvG,EAChB,UAFM,SAEN,oB,GCXDwG,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaE,QAGrB,IAAIC,EAASN,EAAyBE,GAAY,CAGjDG,QAAS,CAAC,GAOX,OAHAE,EAAoBL,GAAU1B,KAAK8B,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAGpEK,EAAOD,OACf,CCrBAJ,EAAoBO,EAAKF,IACxB,IAAIG,EAASH,GAAUA,EAAOI,WAC7B,IAAOJ,EAAiB,QACxB,IAAM,EAEP,OADAL,EAAoBU,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,GCLRR,EAAoBU,EAAI,CAACN,EAASQ,KACjC,IAAI,IAAInB,KAAOmB,EACXZ,EAAoBa,EAAED,EAAYnB,KAASO,EAAoBa,EAAET,EAASX,IAC5EqB,OAAOC,eAAeX,EAASX,EAAK,CAAEuB,YAAY,EAAMC,IAAKL,EAAWnB,MCJ3EO,EAAoBxG,EAAI,WACvB,GAA0B,iBAAf0H,WAAyB,OAAOA,WAC3C,IACC,OAAOxH,MAAQ,IAAIyH,SAAS,cAAb,EAChB,CAAE,MAAOC,GACR,GAAsB,iBAAX3H,OAAqB,OAAOA,MACxC,CACA,CAPuB,GCAxBuG,EAAoBa,EAAI,CAACrB,EAAK6B,IAAUP,OAAOzC,UAAUqB,eAAenB,KAAKiB,EAAK6B,G,yCCK9EC,EAAY,KACZC,EAAS,KACTC,EAAgBzF,SAASC,gBAAgByF,UA4E7C,SAASC,IACP,MAAMC,EAAeC,aAAaC,QAAQ,UAAY,OAZxD,IAAkBC,EACH,WADGA,EAaIrI,OAAOsI,WAAW,gCAAgCC,QAI/C,SAAjBL,EACO,QACgB,SAAhBA,EACA,OAEA,OAIU,SAAjBA,EACO,OACgB,QAAhBA,EACA,QAEA,SA9BoB,SAATG,GAA4B,SAATA,IACzCG,QAAQC,MAAM,2BAA2BJ,yBACzCA,EAAO,QAGT/F,SAASS,KAAK2F,QAAQC,MAAQN,EAC9BF,aAAaS,QAAQ,QAASP,GAC9BG,QAAQK,IAAI,cAAcR,UA0B5B,CAmDA,SAASlC,KART,WAEE,MAAM2C,EAAUxG,SAASyG,uBAAuB,gBAChDpE,MAAMqE,KAAKF,GAASjE,QAASoE,IAC3BA,EAAI7C,iBAAiB,QAAS6B,IAElC,CAGEiB,GA/CF,WAEE,IAAIC,EAA6B,EAC7BC,GAAU,EAEdpJ,OAAOoG,iBAAiB,SAAU,SAAUuB,GAC1CwB,EAA6BnJ,OAAOqJ,QAE/BD,IACHpJ,OAAOwF,sBAAsB,WAzDnC,IAAuB8D,GArDvB,SAAgCA,GAC1BA,EAAY,EACdxB,EAAOjE,UAAUM,IAAI,YAErB2D,EAAOjE,UAAUC,OAAO,WAE5B,EAgDEyF,CADqBD,EA0DDH,GAvGtB,SAAmCG,GAC7BA,EAXmB,GAYrBhH,SAASC,gBAAgBsB,UAAUC,OAAO,oBAEtCwF,EAAYvB,EACdzF,SAASC,gBAAgBsB,UAAUM,IAAI,oBAC9BmF,EAAYvB,GACrBzF,SAASC,gBAAgBsB,UAAUC,OAAO,oBAG9CiE,EAAgBuB,CAClB,CAoCEE,CAA0BF,GAlC5B,SAA6BA,GACT,OAAdzB,IAKa,GAAbyB,EACFzB,EAAU4B,SAAS,EAAG,GAGtB9G,KAAKC,KAAK0G,IACV3G,KAAK+G,MAAMpH,SAASC,gBAAgBS,aAAehD,OAAOqC,aAE1DwF,EAAU4B,SAAS,EAAG5B,EAAU7E,cAGhBV,SAASqH,cAAc,mBAc3C,CAKEC,CAAoBN,GAwDdF,GAAU,CACZ,GAEAA,GAAU,EAEd,GACApJ,OAAO6J,QACT,CA8BEC,GA3BkB,OAAdjC,GAKJ,IAAI,IAAJ,CAAY,cAAe,CACzBrH,QAAQ,EACRuJ,WAAW,EACX5J,SAAU,iBACVI,OAAQ,KACN,IAAIyJ,EAAM9H,WAAW+H,iBAAiB3H,SAASC,iBAAiB2H,UAChE,MAAMC,EAAarC,EAAO7F,wBAC1B,OAAOkI,EAAW1H,IAAM0H,EAAWC,OAAS,IAAMJ,EAAM,IAiB9D,CAcA1H,SAAS8D,iBAAiB,mBAT1B,WACE9D,SAASS,KAAKW,WAAWG,UAAUC,OAAO,SAE1CgE,EAASxF,SAASqH,cAAc,UAChC9B,EAAYvF,SAASqH,cAAc,eAEnCxD,GACF,E","sources":["webpack:///./src/furo/assets/scripts/gumshoe-patched.js","webpack:///webpack/bootstrap","webpack:///webpack/runtime/compat get default export","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/global","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./src/furo/assets/scripts/furo.js"],"sourcesContent":["/*!\n * gumshoejs v5.1.2 (patched by @pradyunsg)\n * A simple, framework-agnostic scrollspy script.\n * (c) 2019 Chris Ferdinandi\n * MIT License\n * http://github.com/cferdinandi/gumshoe\n */\n\n(function (root, factory) {\n if (typeof define === \"function\" && define.amd) {\n define([], function () {\n return factory(root);\n });\n } else if (typeof exports === \"object\") {\n module.exports = factory(root);\n } else {\n root.Gumshoe = factory(root);\n }\n})(\n typeof global !== \"undefined\"\n ? global\n : typeof window !== \"undefined\"\n ? window\n : this,\n function (window) {\n \"use strict\";\n\n //\n // Defaults\n //\n\n var defaults = {\n // Active classes\n navClass: \"active\",\n contentClass: \"active\",\n\n // Nested navigation\n nested: false,\n nestedClass: \"active\",\n\n // Offset & reflow\n offset: 0,\n reflow: false,\n\n // Event support\n events: true,\n };\n\n //\n // Methods\n //\n\n /**\n * Merge two or more objects together.\n * @param {Object} objects The objects to merge together\n * @returns {Object} Merged values of defaults and options\n */\n var extend = function () {\n var merged = {};\n Array.prototype.forEach.call(arguments, function (obj) {\n for (var key in obj) {\n if (!obj.hasOwnProperty(key)) return;\n merged[key] = obj[key];\n }\n });\n return merged;\n };\n\n /**\n * Emit a custom event\n * @param {String} type The event type\n * @param {Node} elem The element to attach the event to\n * @param {Object} detail Any details to pass along with the event\n */\n var emitEvent = function (type, elem, detail) {\n // Make sure events are enabled\n if (!detail.settings.events) return;\n\n // Create a new event\n var event = new CustomEvent(type, {\n bubbles: true,\n cancelable: true,\n detail: detail,\n });\n\n // Dispatch the event\n elem.dispatchEvent(event);\n };\n\n /**\n * Get an element's distance from the top of the Document.\n * @param {Node} elem The element\n * @return {Number} Distance from the top in pixels\n */\n var getOffsetTop = function (elem) {\n var location = 0;\n if (elem.offsetParent) {\n while (elem) {\n location += elem.offsetTop;\n elem = elem.offsetParent;\n }\n }\n return location >= 0 ? location : 0;\n };\n\n /**\n * Sort content from first to last in the DOM\n * @param {Array} contents The content areas\n */\n var sortContents = function (contents) {\n if (contents) {\n contents.sort(function (item1, item2) {\n var offset1 = getOffsetTop(item1.content);\n var offset2 = getOffsetTop(item2.content);\n if (offset1 < offset2) return -1;\n return 1;\n });\n }\n };\n\n /**\n * Get the offset to use for calculating position\n * @param {Object} settings The settings for this instantiation\n * @return {Float} The number of pixels to offset the calculations\n */\n var getOffset = function (settings) {\n // if the offset is a function run it\n if (typeof settings.offset === \"function\") {\n return parseFloat(settings.offset());\n }\n\n // Otherwise, return it as-is\n return parseFloat(settings.offset);\n };\n\n /**\n * Get the document element's height\n * @private\n * @returns {Number}\n */\n var getDocumentHeight = function () {\n return Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight,\n );\n };\n\n /**\n * Determine if an element is in view\n * @param {Node} elem The element\n * @param {Object} settings The settings for this instantiation\n * @param {Boolean} bottom If true, check if element is above bottom of viewport instead\n * @return {Boolean} Returns true if element is in the viewport\n */\n var isInView = function (elem, settings, bottom) {\n var bounds = elem.getBoundingClientRect();\n var offset = getOffset(settings);\n if (bottom) {\n return (\n parseInt(bounds.bottom, 10) <\n (window.innerHeight || document.documentElement.clientHeight)\n );\n }\n return parseInt(bounds.top, 10) <= offset;\n };\n\n /**\n * Check if at the bottom of the viewport\n * @return {Boolean} If true, page is at the bottom of the viewport\n */\n var isAtBottom = function () {\n if (\n Math.ceil(window.innerHeight + window.pageYOffset) >=\n getDocumentHeight()\n )\n return true;\n return false;\n };\n\n /**\n * Check if the last item should be used (even if not at the top of the page)\n * @param {Object} item The last item\n * @param {Object} settings The settings for this instantiation\n * @return {Boolean} If true, use the last item\n */\n var useLastItem = function (item, settings) {\n if (isAtBottom() && isInView(item.content, settings, true)) return true;\n return false;\n };\n\n /**\n * Get the active content\n * @param {Array} contents The content areas\n * @param {Object} settings The settings for this instantiation\n * @return {Object} The content area and matching navigation link\n */\n var getActive = function (contents, settings) {\n var last = contents[contents.length - 1];\n if (useLastItem(last, settings)) return last;\n for (var i = contents.length - 1; i >= 0; i--) {\n if (isInView(contents[i].content, settings)) return contents[i];\n }\n };\n\n /**\n * Deactivate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var deactivateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested || !nav.parentNode) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Remove the active class\n li.classList.remove(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n deactivateNested(li, settings);\n };\n\n /**\n * Deactivate a nav and content area\n * @param {Object} items The nav item and content to deactivate\n * @param {Object} settings The settings for this instantiation\n */\n var deactivate = function (items, settings) {\n // Make sure there are items to deactivate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Remove the active class from the nav and content\n li.classList.remove(settings.navClass);\n items.content.classList.remove(settings.contentClass);\n\n // Deactivate any parent navs in a nested navigation\n deactivateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeDeactivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Activate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var activateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Add the active class\n li.classList.add(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n activateNested(li, settings);\n };\n\n /**\n * Activate a nav and content area\n * @param {Object} items The nav item and content to activate\n * @param {Object} settings The settings for this instantiation\n */\n var activate = function (items, settings) {\n // Make sure there are items to activate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Add the active class to the nav and content\n li.classList.add(settings.navClass);\n items.content.classList.add(settings.contentClass);\n\n // Activate any parent navs in a nested navigation\n activateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeActivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Create the Constructor object\n * @param {String} selector The selector to use for navigation items\n * @param {Object} options User options and settings\n */\n var Constructor = function (selector, options) {\n //\n // Variables\n //\n\n var publicAPIs = {};\n var navItems, contents, current, timeout, settings;\n\n //\n // Methods\n //\n\n /**\n * Set variables from DOM elements\n */\n publicAPIs.setup = function () {\n // Get all nav items\n navItems = document.querySelectorAll(selector);\n\n // Create contents array\n contents = [];\n\n // Loop through each item, get it's matching content, and push to the array\n Array.prototype.forEach.call(navItems, function (item) {\n // Get the content for the nav item\n var content = document.getElementById(\n decodeURIComponent(item.hash.substr(1)),\n );\n if (!content) return;\n\n // Push to the contents array\n contents.push({\n nav: item,\n content: content,\n });\n });\n\n // Sort contents by the order they appear in the DOM\n sortContents(contents);\n };\n\n /**\n * Detect which content is currently active\n */\n publicAPIs.detect = function () {\n // Get the active content\n var active = getActive(contents, settings);\n\n // if there's no active content, deactivate and bail\n if (!active) {\n if (current) {\n deactivate(current, settings);\n current = null;\n }\n return;\n }\n\n // If the active content is the one currently active, do nothing\n if (current && active.content === current.content) return;\n\n // Deactivate the current content and activate the new content\n deactivate(current, settings);\n activate(active, settings);\n\n // Update the currently active content\n current = active;\n };\n\n /**\n * Detect the active content on scroll\n * Debounced for performance\n */\n var scrollHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(publicAPIs.detect);\n };\n\n /**\n * Update content sorting on resize\n * Debounced for performance\n */\n var resizeHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(function () {\n sortContents(contents);\n publicAPIs.detect();\n });\n };\n\n /**\n * Destroy the current instantiation\n */\n publicAPIs.destroy = function () {\n // Undo DOM changes\n if (current) {\n deactivate(current, settings);\n }\n\n // Remove event listeners\n window.removeEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.removeEventListener(\"resize\", resizeHandler, false);\n }\n\n // Reset variables\n contents = null;\n navItems = null;\n current = null;\n timeout = null;\n settings = null;\n };\n\n /**\n * Initialize the current instantiation\n */\n var init = function () {\n // Merge user options into defaults\n settings = extend(defaults, options || {});\n\n // Setup variables based on the current DOM\n publicAPIs.setup();\n\n // Find the currently active content\n publicAPIs.detect();\n\n // Setup event listeners\n window.addEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.addEventListener(\"resize\", resizeHandler, false);\n }\n };\n\n //\n // Initialize and return the public APIs\n //\n\n init();\n return publicAPIs;\n };\n\n //\n // Return the Constructor\n //\n\n return Constructor;\n },\n);\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","import Gumshoe from \"./gumshoe-patched.js\";\n\n////////////////////////////////////////////////////////////////////////////////\n// Scroll Handling\n////////////////////////////////////////////////////////////////////////////////\nvar tocScroll = null;\nvar header = null;\nvar lastScrollTop = document.documentElement.scrollTop;\nconst GO_TO_TOP_OFFSET = 64;\n\nfunction scrollHandlerForHeader(positionY) {\n if (positionY > 0) {\n header.classList.add(\"scrolled\");\n } else {\n header.classList.remove(\"scrolled\");\n }\n}\n\nfunction scrollHandlerForBackToTop(positionY) {\n if (positionY < GO_TO_TOP_OFFSET) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n } else {\n if (positionY < lastScrollTop) {\n document.documentElement.classList.add(\"show-back-to-top\");\n } else if (positionY > lastScrollTop) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n }\n }\n lastScrollTop = positionY;\n}\n\nfunction scrollHandlerForTOC(positionY) {\n if (tocScroll === null) {\n return;\n }\n\n // top of page.\n if (positionY == 0) {\n tocScroll.scrollTo(0, 0);\n } else if (\n // bottom of page.\n Math.ceil(positionY) >=\n Math.floor(document.documentElement.scrollHeight - window.innerHeight)\n ) {\n tocScroll.scrollTo(0, tocScroll.scrollHeight);\n } else {\n // somewhere in the middle.\n const current = document.querySelector(\".scroll-current\");\n if (current == null) {\n return;\n }\n\n // https://github.com/pypa/pip/issues/9159 This breaks scroll behaviours.\n // // scroll the currently \"active\" heading in toc, into view.\n // const rect = current.getBoundingClientRect();\n // if (0 > rect.top) {\n // current.scrollIntoView(true); // the argument is \"alignTop\"\n // } else if (rect.bottom > window.innerHeight) {\n // current.scrollIntoView(false);\n // }\n }\n}\n\nfunction scrollHandler(positionY) {\n scrollHandlerForHeader(positionY);\n scrollHandlerForBackToTop(positionY);\n scrollHandlerForTOC(positionY);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Theme Toggle\n////////////////////////////////////////////////////////////////////////////////\nfunction setTheme(mode) {\n if (mode !== \"light\" && mode !== \"dark\" && mode !== \"auto\") {\n console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);\n mode = \"auto\";\n }\n\n document.body.dataset.theme = mode;\n localStorage.setItem(\"theme\", mode);\n console.log(`Changed to ${mode} mode.`);\n}\n\nfunction cycleThemeOnce() {\n const currentTheme = localStorage.getItem(\"theme\") || \"auto\";\n const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (prefersDark) {\n // Auto (dark) -> Light -> Dark\n if (currentTheme === \"auto\") {\n setTheme(\"light\");\n } else if (currentTheme == \"light\") {\n setTheme(\"dark\");\n } else {\n setTheme(\"auto\");\n }\n } else {\n // Auto (light) -> Dark -> Light\n if (currentTheme === \"auto\") {\n setTheme(\"dark\");\n } else if (currentTheme == \"dark\") {\n setTheme(\"light\");\n } else {\n setTheme(\"auto\");\n }\n }\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Setup\n////////////////////////////////////////////////////////////////////////////////\nfunction setupScrollHandler() {\n // Taken from https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event\n let last_known_scroll_position = 0;\n let ticking = false;\n\n window.addEventListener(\"scroll\", function (e) {\n last_known_scroll_position = window.scrollY;\n\n if (!ticking) {\n window.requestAnimationFrame(function () {\n scrollHandler(last_known_scroll_position);\n ticking = false;\n });\n\n ticking = true;\n }\n });\n window.scroll();\n}\n\nfunction setupScrollSpy() {\n if (tocScroll === null) {\n return;\n }\n\n // Scrollspy -- highlight table on contents, based on scroll\n new Gumshoe(\".toc-tree a\", {\n reflow: true,\n recursive: true,\n navClass: \"scroll-current\",\n offset: () => {\n let rem = parseFloat(getComputedStyle(document.documentElement).fontSize);\n const headerRect = header.getBoundingClientRect();\n return headerRect.top + headerRect.height + 2.5 * rem + 1;\n },\n });\n}\n\nfunction setupTheme() {\n // Attach event handlers for toggling themes\n const buttons = document.getElementsByClassName(\"theme-toggle\");\n Array.from(buttons).forEach((btn) => {\n btn.addEventListener(\"click\", cycleThemeOnce);\n });\n}\n\nfunction setup() {\n setupTheme();\n setupScrollHandler();\n setupScrollSpy();\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Main entrypoint\n////////////////////////////////////////////////////////////////////////////////\nfunction main() {\n document.body.parentNode.classList.remove(\"no-js\");\n\n header = document.querySelector(\"header\");\n tocScroll = document.querySelector(\".toc-scroll\");\n\n setup();\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", main);\n"],"names":["root","g","window","this","defaults","navClass","contentClass","nested","nestedClass","offset","reflow","events","emitEvent","type","elem","detail","settings","event","CustomEvent","bubbles","cancelable","dispatchEvent","getOffsetTop","location","offsetParent","offsetTop","sortContents","contents","sort","item1","item2","content","isInView","bottom","bounds","getBoundingClientRect","parseFloat","getOffset","parseInt","innerHeight","document","documentElement","clientHeight","top","isAtBottom","Math","ceil","pageYOffset","max","body","scrollHeight","offsetHeight","getActive","last","length","item","useLastItem","i","deactivateNested","nav","parentNode","li","closest","classList","remove","deactivate","items","link","activateNested","add","selector","options","navItems","current","timeout","publicAPIs","querySelectorAll","Array","prototype","forEach","call","getElementById","decodeURIComponent","hash","substr","push","active","activate","scrollHandler","cancelAnimationFrame","requestAnimationFrame","detect","resizeHandler","destroy","removeEventListener","merged","arguments","obj","key","hasOwnProperty","extend","setup","addEventListener","factory","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","n","getter","__esModule","d","a","definition","o","Object","defineProperty","enumerable","get","globalThis","Function","e","prop","tocScroll","header","lastScrollTop","scrollTop","cycleThemeOnce","currentTheme","localStorage","getItem","mode","matchMedia","matches","console","error","dataset","theme","setItem","log","buttons","getElementsByClassName","from","btn","setupTheme","last_known_scroll_position","ticking","scrollY","positionY","scrollHandlerForHeader","scrollHandlerForBackToTop","scrollTo","floor","querySelector","scrollHandlerForTOC","scroll","setupScrollHandler","recursive","rem","getComputedStyle","fontSize","headerRect","height"],"sourceRoot":""} \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 0000000..b08d58c --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,620 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/skeleton.css b/_static/skeleton.css new file mode 100644 index 0000000..467c878 --- /dev/null +++ b/_static/skeleton.css @@ -0,0 +1,296 @@ +/* Some sane resets. */ +html { + height: 100%; +} + +body { + margin: 0; + min-height: 100%; +} + +/* All the flexbox magic! */ +body, +.sb-announcement, +.sb-content, +.sb-main, +.sb-container, +.sb-container__inner, +.sb-article-container, +.sb-footer-content, +.sb-header, +.sb-header-secondary, +.sb-footer { + display: flex; +} + +/* These order things vertically */ +body, +.sb-main, +.sb-article-container { + flex-direction: column; +} + +/* Put elements in the center */ +.sb-header, +.sb-header-secondary, +.sb-container, +.sb-content, +.sb-footer, +.sb-footer-content { + justify-content: center; +} +/* Put elements at the ends */ +.sb-article-container { + justify-content: space-between; +} + +/* These elements grow. */ +.sb-main, +.sb-content, +.sb-container, +article { + flex-grow: 1; +} + +/* Because padding making this wider is not fun */ +article { + box-sizing: border-box; +} + +/* The announcements element should never be wider than the page. */ +.sb-announcement { + max-width: 100%; +} + +.sb-sidebar-primary, +.sb-sidebar-secondary { + flex-shrink: 0; + width: 17rem; +} + +.sb-announcement__inner { + justify-content: center; + + box-sizing: border-box; + height: 3rem; + + overflow-x: auto; + white-space: nowrap; +} + +/* Sidebars, with checkbox-based toggle */ +.sb-sidebar-primary, +.sb-sidebar-secondary { + position: fixed; + height: 100%; + top: 0; +} + +.sb-sidebar-primary { + left: -17rem; + transition: left 250ms ease-in-out; +} +.sb-sidebar-secondary { + right: -17rem; + transition: right 250ms ease-in-out; +} + +.sb-sidebar-toggle { + display: none; +} +.sb-sidebar-overlay { + position: fixed; + top: 0; + width: 0; + height: 0; + + transition: width 0ms ease 250ms, height 0ms ease 250ms, opacity 250ms ease; + + opacity: 0; + background-color: rgba(0, 0, 0, 0.54); +} + +#sb-sidebar-toggle--primary:checked + ~ .sb-sidebar-overlay[for="sb-sidebar-toggle--primary"], +#sb-sidebar-toggle--secondary:checked + ~ .sb-sidebar-overlay[for="sb-sidebar-toggle--secondary"] { + width: 100%; + height: 100%; + opacity: 1; + transition: width 0ms ease, height 0ms ease, opacity 250ms ease; +} + +#sb-sidebar-toggle--primary:checked ~ .sb-container .sb-sidebar-primary { + left: 0; +} +#sb-sidebar-toggle--secondary:checked ~ .sb-container .sb-sidebar-secondary { + right: 0; +} + +/* Full-width mode */ +.drop-secondary-sidebar-for-full-width-content + .hide-when-secondary-sidebar-shown { + display: none !important; +} +.drop-secondary-sidebar-for-full-width-content .sb-sidebar-secondary { + display: none !important; +} + +/* Mobile views */ +.sb-page-width { + width: 100%; +} + +.sb-article-container, +.sb-footer-content__inner, +.drop-secondary-sidebar-for-full-width-content .sb-article, +.drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 100vw; +} + +.sb-article, +.match-content-width { + padding: 0 1rem; + box-sizing: border-box; +} + +@media (min-width: 32rem) { + .sb-article, + .match-content-width { + padding: 0 2rem; + } +} + +/* Tablet views */ +@media (min-width: 42rem) { + .sb-article-container { + width: auto; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 42rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} +@media (min-width: 46rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 46rem; + } + .sb-article, + .match-content-width { + width: 46rem; + } +} +@media (min-width: 50rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 50rem; + } + .sb-article, + .match-content-width { + width: 50rem; + } +} + +/* Tablet views */ +@media (min-width: 59rem) { + .sb-sidebar-secondary { + position: static; + } + .hide-when-secondary-sidebar-shown { + display: none !important; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 59rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} +@media (min-width: 63rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 63rem; + } + .sb-article, + .match-content-width { + width: 46rem; + } +} +@media (min-width: 67rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } + .sb-article, + .match-content-width { + width: 50rem; + } +} + +/* Desktop views */ +@media (min-width: 76rem) { + .sb-sidebar-primary { + position: static; + } + .hide-when-primary-sidebar-shown { + display: none !important; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 59rem; + } + .sb-article, + .match-content-width { + width: 42rem; + } +} + +/* Full desktop views */ +@media (min-width: 80rem) { + .sb-article, + .match-content-width { + width: 46rem; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 63rem; + } +} + +@media (min-width: 84rem) { + .sb-article, + .match-content-width { + width: 50rem; + } + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } +} + +@media (min-width: 88rem) { + .sb-footer-content__inner, + .drop-secondary-sidebar-for-full-width-content .sb-article, + .drop-secondary-sidebar-for-full-width-content .match-content-width { + width: 67rem; + } + .sb-page-width { + width: 88rem; + } +} diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/_static/styles/furo-extensions.css b/_static/styles/furo-extensions.css new file mode 100644 index 0000000..2d74267 --- /dev/null +++ b/_static/styles/furo-extensions.css @@ -0,0 +1,2 @@ +#furo-sidebar-ad-placement{padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)}#furo-sidebar-ad-placement .ethical-sidebar{background:var(--color-background-secondary);border:none;box-shadow:none}#furo-sidebar-ad-placement .ethical-sidebar:hover{background:var(--color-background-hover)}#furo-sidebar-ad-placement .ethical-sidebar a{color:var(--color-foreground-primary)}#furo-sidebar-ad-placement .ethical-callout a{color:var(--color-foreground-secondary)!important}#furo-readthedocs-versions{background:transparent;display:block;position:static;width:100%}#furo-readthedocs-versions .rst-versions{background:#1a1c1e}#furo-readthedocs-versions .rst-current-version{background:var(--color-sidebar-item-background);cursor:unset}#furo-readthedocs-versions .rst-current-version:hover{background:var(--color-sidebar-item-background)}#furo-readthedocs-versions .rst-current-version .fa-book{color:var(--color-foreground-primary)}#furo-readthedocs-versions>.rst-other-versions{padding:0}#furo-readthedocs-versions>.rst-other-versions small{opacity:1}#furo-readthedocs-versions .injected .rst-versions{position:unset}#furo-readthedocs-versions:focus-within,#furo-readthedocs-versions:hover{box-shadow:0 0 0 1px var(--color-sidebar-background-border)}#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:hover .rst-current-version{background:#1a1c1e;font-size:inherit;height:auto;line-height:inherit;padding:12px;text-align:right}#furo-readthedocs-versions:focus-within .rst-current-version .fa-book,#furo-readthedocs-versions:hover .rst-current-version .fa-book{color:#fff;float:left}#furo-readthedocs-versions:focus-within .fa-caret-down,#furo-readthedocs-versions:hover .fa-caret-down{display:none}#furo-readthedocs-versions:focus-within .injected,#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:focus-within .rst-other-versions,#furo-readthedocs-versions:hover .injected,#furo-readthedocs-versions:hover .rst-current-version,#furo-readthedocs-versions:hover .rst-other-versions{display:block}#furo-readthedocs-versions:focus-within>.rst-current-version,#furo-readthedocs-versions:hover>.rst-current-version{display:none}.highlight:hover button.copybtn{color:var(--color-code-foreground)}.highlight button.copybtn{align-items:center;background-color:var(--color-code-background);border:none;color:var(--color-background-item);cursor:pointer;height:1.25em;right:.5rem;top:.625rem;transition:color .3s,opacity .3s;width:1.25em}.highlight button.copybtn:hover{background-color:var(--color-code-background);color:var(--color-brand-content)}.highlight button.copybtn:after{background-color:transparent;color:var(--color-code-foreground);display:none}.highlight button.copybtn.success{color:#22863a;transition:color 0s}.highlight button.copybtn.success:after{display:block}.highlight button.copybtn svg{padding:0}body{--sd-color-primary:var(--color-brand-primary);--sd-color-primary-highlight:var(--color-brand-content);--sd-color-primary-text:var(--color-background-primary);--sd-color-shadow:rgba(0,0,0,.05);--sd-color-card-border:var(--color-card-border);--sd-color-card-border-hover:var(--color-brand-content);--sd-color-card-background:var(--color-card-background);--sd-color-card-text:var(--color-foreground-primary);--sd-color-card-header:var(--color-card-marginals-background);--sd-color-card-footer:var(--color-card-marginals-background);--sd-color-tabs-label-active:var(--color-brand-content);--sd-color-tabs-label-hover:var(--color-foreground-muted);--sd-color-tabs-label-inactive:var(--color-foreground-muted);--sd-color-tabs-underline-active:var(--color-brand-content);--sd-color-tabs-underline-hover:var(--color-foreground-border);--sd-color-tabs-underline-inactive:var(--color-background-border);--sd-color-tabs-overline:var(--color-background-border);--sd-color-tabs-underline:var(--color-background-border)}.sd-tab-content{box-shadow:0 -2px var(--sd-color-tabs-overline),0 1px var(--sd-color-tabs-underline)}.sd-card{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)}.sd-shadow-sm{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-md{box-shadow:0 .3rem .75rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-lg{box-shadow:0 .6rem 1.5rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-card-hover:hover{transform:none}.sd-cards-carousel{gap:.25rem;padding:.25rem}body{--tabs--label-text:var(--color-foreground-muted);--tabs--label-text--hover:var(--color-foreground-muted);--tabs--label-text--active:var(--color-brand-content);--tabs--label-text--active--hover:var(--color-brand-content);--tabs--label-background:transparent;--tabs--label-background--hover:transparent;--tabs--label-background--active:transparent;--tabs--label-background--active--hover:transparent;--tabs--padding-x:0.25em;--tabs--margin-x:1em;--tabs--border:var(--color-background-border);--tabs--label-border:transparent;--tabs--label-border--hover:var(--color-foreground-muted);--tabs--label-border--active:var(--color-brand-content);--tabs--label-border--active--hover:var(--color-brand-content)}[role=main] .container{max-width:none;padding-left:0;padding-right:0}.shadow.docutils{border:none;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)!important}.sphinx-bs .card{background-color:var(--color-background-secondary);color:var(--color-foreground)} +/*# sourceMappingURL=furo-extensions.css.map*/ \ No newline at end of file diff --git a/_static/styles/furo-extensions.css.map b/_static/styles/furo-extensions.css.map new file mode 100644 index 0000000..68fb7fd --- /dev/null +++ b/_static/styles/furo-extensions.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles/furo-extensions.css","mappings":"AAGA,2BACE,oFACA,4CAKE,6CAHA,YACA,eAEA,CACA,kDACE,yCAEF,8CACE,sCAEJ,8CACE,kDAEJ,2BAGE,uBACA,cAHA,gBACA,UAEA,CAGA,yCACE,mBAEF,gDAEE,gDADA,YACA,CACA,sDACE,gDACF,yDACE,sCAEJ,+CACE,UACA,qDACE,UAGF,mDACE,eAEJ,yEAEE,4DAEA,mHASE,mBAPA,kBAEA,YADA,oBAGA,aADA,gBAIA,CAEA,qIAEE,WADA,UACA,CAEJ,uGACE,aAEF,iUAGE,cAEF,mHACE,aC1EJ,gCACE,mCAEF,0BAEE,mBAUA,8CACA,YAFA,mCAKA,eAZA,cAIA,YADA,YAYA,iCAdA,YAcA,CAEA,gCAEE,8CADA,gCACA,CAEF,gCAGE,6BADA,mCADA,YAEA,CAEF,kCAEE,cADA,mBACA,CACA,wCACE,cAEJ,8BACE,UCzCN,KAEE,6CAA8C,CAC9C,uDAAwD,CACxD,uDAAwD,CAGxD,iCAAsC,CAGtC,+CAAgD,CAChD,uDAAwD,CACxD,uDAAwD,CACxD,oDAAqD,CACrD,6DAA8D,CAC9D,6DAA8D,CAG9D,uDAAwD,CACxD,yDAA0D,CAC1D,4DAA6D,CAC7D,2DAA4D,CAC5D,8DAA+D,CAC/D,iEAAkE,CAClE,uDAAwD,CACxD,wDAAyD,CAG3D,gBACE,qFAGF,SACE,6EAEF,cACE,uFAEF,cACE,uFAEF,cACE,uFAGF,qBACE,eAEF,mBACE,WACA,eChDF,KACE,gDAAiD,CACjD,uDAAwD,CACxD,qDAAsD,CACtD,4DAA6D,CAC7D,oCAAqC,CACrC,2CAA4C,CAC5C,4CAA6C,CAC7C,mDAAoD,CACpD,wBAAyB,CACzB,oBAAqB,CACrB,6CAA8C,CAC9C,gCAAiC,CACjC,yDAA0D,CAC1D,uDAAwD,CACxD,8DAA+D,CCbjE,uBACE,eACA,eACA,gBAGF,iBACE,YACA,+EAGF,iBACE,mDACA","sources":["webpack:///./src/furo/assets/styles/extensions/_readthedocs.sass","webpack:///./src/furo/assets/styles/extensions/_copybutton.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-design.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-inline-tabs.sass","webpack:///./src/furo/assets/styles/extensions/_sphinx-panels.sass"],"sourcesContent":["// This file contains the styles used for tweaking how ReadTheDoc's embedded\n// contents would show up inside the theme.\n\n#furo-sidebar-ad-placement\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n .ethical-sidebar\n // Remove the border and box-shadow.\n border: none\n box-shadow: none\n // Manage the background colors.\n background: var(--color-background-secondary)\n &:hover\n background: var(--color-background-hover)\n // Ensure the text is legible.\n a\n color: var(--color-foreground-primary)\n\n .ethical-callout a\n color: var(--color-foreground-secondary) !important\n\n#furo-readthedocs-versions\n position: static\n width: 100%\n background: transparent\n display: block\n\n // Make the background color fit with the theme's aesthetic.\n .rst-versions\n background: rgb(26, 28, 30)\n\n .rst-current-version\n cursor: unset\n background: var(--color-sidebar-item-background)\n &:hover\n background: var(--color-sidebar-item-background)\n .fa-book\n color: var(--color-foreground-primary)\n\n > .rst-other-versions\n padding: 0\n small\n opacity: 1\n\n .injected\n .rst-versions\n position: unset\n\n &:hover,\n &:focus-within\n box-shadow: 0 0 0 1px var(--color-sidebar-background-border)\n\n .rst-current-version\n // Undo the tweaks done in RTD's CSS\n font-size: inherit\n line-height: inherit\n height: auto\n text-align: right\n padding: 12px\n\n // Match the rest of the body\n background: #1a1c1e\n\n .fa-book\n float: left\n color: white\n\n .fa-caret-down\n display: none\n\n .rst-current-version,\n .rst-other-versions,\n .injected\n display: block\n\n > .rst-current-version\n display: none\n",".highlight\n &:hover button.copybtn\n color: var(--color-code-foreground)\n\n button.copybtn\n // Align things correctly\n align-items: center\n\n height: 1.25em\n width: 1.25em\n\n top: 0.625rem // $code-spacing-vertical\n right: 0.5rem\n\n // Make it look better\n color: var(--color-background-item)\n background-color: var(--color-code-background)\n border: none\n\n // Change to cursor to make it obvious that you can click on it\n cursor: pointer\n\n // Transition smoothly, for aesthetics\n transition: color 300ms, opacity 300ms\n\n &:hover\n color: var(--color-brand-content)\n background-color: var(--color-code-background)\n\n &::after\n display: none\n color: var(--color-code-foreground)\n background-color: transparent\n\n &.success\n transition: color 0ms\n color: #22863a\n &::after\n display: block\n\n svg\n padding: 0\n","body\n // Colors\n --sd-color-primary: var(--color-brand-primary)\n --sd-color-primary-highlight: var(--color-brand-content)\n --sd-color-primary-text: var(--color-background-primary)\n\n // Shadows\n --sd-color-shadow: rgba(0, 0, 0, 0.05)\n\n // Cards\n --sd-color-card-border: var(--color-card-border)\n --sd-color-card-border-hover: var(--color-brand-content)\n --sd-color-card-background: var(--color-card-background)\n --sd-color-card-text: var(--color-foreground-primary)\n --sd-color-card-header: var(--color-card-marginals-background)\n --sd-color-card-footer: var(--color-card-marginals-background)\n\n // Tabs\n --sd-color-tabs-label-active: var(--color-brand-content)\n --sd-color-tabs-label-hover: var(--color-foreground-muted)\n --sd-color-tabs-label-inactive: var(--color-foreground-muted)\n --sd-color-tabs-underline-active: var(--color-brand-content)\n --sd-color-tabs-underline-hover: var(--color-foreground-border)\n --sd-color-tabs-underline-inactive: var(--color-background-border)\n --sd-color-tabs-overline: var(--color-background-border)\n --sd-color-tabs-underline: var(--color-background-border)\n\n// Tabs\n.sd-tab-content\n box-shadow: 0 -2px var(--sd-color-tabs-overline), 0 1px var(--sd-color-tabs-underline)\n\n// Shadows\n.sd-card // Have a shadow by default\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n.sd-shadow-sm\n box-shadow: 0 0.1rem 0.25rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-md\n box-shadow: 0 0.3rem 0.75rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n.sd-shadow-lg\n box-shadow: 0 0.6rem 1.5rem var(--sd-color-shadow), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Cards\n.sd-card-hover:hover // Don't change scale on hover\n transform: none\n\n.sd-cards-carousel // Have a bit of gap in the carousel by default\n gap: 0.25rem\n padding: 0.25rem\n","// This file contains styles to tweak sphinx-inline-tabs to work well with Furo.\n\nbody\n --tabs--label-text: var(--color-foreground-muted)\n --tabs--label-text--hover: var(--color-foreground-muted)\n --tabs--label-text--active: var(--color-brand-content)\n --tabs--label-text--active--hover: var(--color-brand-content)\n --tabs--label-background: transparent\n --tabs--label-background--hover: transparent\n --tabs--label-background--active: transparent\n --tabs--label-background--active--hover: transparent\n --tabs--padding-x: 0.25em\n --tabs--margin-x: 1em\n --tabs--border: var(--color-background-border)\n --tabs--label-border: transparent\n --tabs--label-border--hover: var(--color-foreground-muted)\n --tabs--label-border--active: var(--color-brand-content)\n --tabs--label-border--active--hover: var(--color-brand-content)\n","// This file contains styles to tweak sphinx-panels to work well with Furo.\n\n// sphinx-panels includes Bootstrap 4, which uses .container which can conflict\n// with docutils' `.. container::` directive.\n[role=\"main\"] .container\n max-width: initial\n padding-left: initial\n padding-right: initial\n\n// Make the panels look nicer!\n.shadow.docutils\n border: none\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1) !important\n\n// Make panel colors respond to dark mode\n.sphinx-bs .card\n background-color: var(--color-background-secondary)\n color: var(--color-foreground)\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/_static/styles/furo.css b/_static/styles/furo.css new file mode 100644 index 0000000..a5b614d --- /dev/null +++ b/_static/styles/furo.css @@ -0,0 +1,2 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}@media print{.content-icon-container,.headerlink,.mobile-header,.related-pages{display:none!important}.highlight{border:.1pt solid var(--color-foreground-border)}a,blockquote,dl,ol,p,pre,table,ul{page-break-inside:avoid}caption,figure,h1,h2,h3,h4,h5,h6,img{page-break-after:avoid;page-break-inside:avoid}dl,ol,ul{page-break-before:avoid}}.visually-hidden{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;clip:rect(0,0,0,0)!important;background:var(--color-background-primary);border:0!important;color:var(--color-foreground-primary);white-space:nowrap!important}:-moz-focusring{outline:auto}body{--font-stack:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;--font-stack--monospace:"SFMono-Regular",Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace;--font-stack--headings:var(--font-stack);--font-size--normal:100%;--font-size--small:87.5%;--font-size--small--2:81.25%;--font-size--small--3:75%;--font-size--small--4:62.5%;--sidebar-caption-font-size:var(--font-size--small--2);--sidebar-item-font-size:var(--font-size--small);--sidebar-search-input-font-size:var(--font-size--small);--toc-font-size:var(--font-size--small--3);--toc-font-size--mobile:var(--font-size--normal);--toc-title-font-size:var(--font-size--small--4);--admonition-font-size:0.8125rem;--admonition-title-font-size:0.8125rem;--code-font-size:var(--font-size--small--2);--api-font-size:var(--font-size--small);--header-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*4);--header-padding:0.5rem;--sidebar-tree-space-above:1.5rem;--sidebar-caption-space-above:1rem;--sidebar-item-line-height:1rem;--sidebar-item-spacing-vertical:0.5rem;--sidebar-item-spacing-horizontal:1rem;--sidebar-item-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*2);--sidebar-expander-width:var(--sidebar-item-height);--sidebar-search-space-above:0.5rem;--sidebar-search-input-spacing-vertical:0.5rem;--sidebar-search-input-spacing-horizontal:0.5rem;--sidebar-search-input-height:1rem;--sidebar-search-icon-size:var(--sidebar-search-input-height);--toc-title-padding:0.25rem 0;--toc-spacing-vertical:1.5rem;--toc-spacing-horizontal:1.5rem;--toc-item-spacing-vertical:0.4rem;--toc-item-spacing-horizontal:1rem;--icon-search:url('data:image/svg+xml;charset=utf-8,');--icon-pencil:url('data:image/svg+xml;charset=utf-8,');--icon-abstract:url('data:image/svg+xml;charset=utf-8,');--icon-info:url('data:image/svg+xml;charset=utf-8,');--icon-flame:url('data:image/svg+xml;charset=utf-8,');--icon-question:url('data:image/svg+xml;charset=utf-8,');--icon-warning:url('data:image/svg+xml;charset=utf-8,');--icon-failure:url('data:image/svg+xml;charset=utf-8,');--icon-spark:url('data:image/svg+xml;charset=utf-8,');--color-admonition-title--caution:#ff9100;--color-admonition-title-background--caution:rgba(255,145,0,.2);--color-admonition-title--warning:#ff9100;--color-admonition-title-background--warning:rgba(255,145,0,.2);--color-admonition-title--danger:#ff5252;--color-admonition-title-background--danger:rgba(255,82,82,.2);--color-admonition-title--attention:#ff5252;--color-admonition-title-background--attention:rgba(255,82,82,.2);--color-admonition-title--error:#ff5252;--color-admonition-title-background--error:rgba(255,82,82,.2);--color-admonition-title--hint:#00c852;--color-admonition-title-background--hint:rgba(0,200,82,.2);--color-admonition-title--tip:#00c852;--color-admonition-title-background--tip:rgba(0,200,82,.2);--color-admonition-title--important:#00bfa5;--color-admonition-title-background--important:rgba(0,191,165,.2);--color-admonition-title--note:#00b0ff;--color-admonition-title-background--note:rgba(0,176,255,.2);--color-admonition-title--seealso:#448aff;--color-admonition-title-background--seealso:rgba(68,138,255,.2);--color-admonition-title--admonition-todo:grey;--color-admonition-title-background--admonition-todo:hsla(0,0%,50%,.2);--color-admonition-title:#651fff;--color-admonition-title-background:rgba(101,31,255,.2);--icon-admonition-default:var(--icon-abstract);--color-topic-title:#14b8a6;--color-topic-title-background:rgba(20,184,166,.2);--icon-topic-default:var(--icon-pencil);--color-problematic:#b30000;--color-foreground-primary:#000;--color-foreground-secondary:#5a5c63;--color-foreground-muted:#6b6f76;--color-foreground-border:#878787;--color-background-primary:#fff;--color-background-secondary:#f8f9fb;--color-background-hover:#efeff4;--color-background-hover--transparent:#efeff400;--color-background-border:#eeebee;--color-background-item:#ccc;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#0a4bff;--color-brand-content:#2757dd;--color-brand-visited:#872ee0;--color-api-background:var(--color-background-hover--transparent);--color-api-background-hover:var(--color-background-hover);--color-api-overall:var(--color-foreground-secondary);--color-api-name:var(--color-problematic);--color-api-pre-name:var(--color-problematic);--color-api-paren:var(--color-foreground-secondary);--color-api-keyword:var(--color-foreground-primary);--color-api-added:#21632c;--color-api-added-border:#38a84d;--color-api-changed:#046172;--color-api-changed-border:#06a1bc;--color-api-deprecated:#605706;--color-api-deprecated-border:#f0d90f;--color-api-removed:#b30000;--color-api-removed-border:#ff5c5c;--color-highlight-on-target:#ffc;--color-inline-code-background:var(--color-background-secondary);--color-highlighted-background:#def;--color-highlighted-text:var(--color-foreground-primary);--color-guilabel-background:#ddeeff80;--color-guilabel-border:#bedaf580;--color-guilabel-text:var(--color-foreground-primary);--color-admonition-background:transparent;--color-table-header-background:var(--color-background-secondary);--color-table-border:var(--color-background-border);--color-card-border:var(--color-background-secondary);--color-card-background:transparent;--color-card-marginals-background:var(--color-background-secondary);--color-header-background:var(--color-background-primary);--color-header-border:var(--color-background-border);--color-header-text:var(--color-foreground-primary);--color-sidebar-background:var(--color-background-secondary);--color-sidebar-background-border:var(--color-background-border);--color-sidebar-brand-text:var(--color-foreground-primary);--color-sidebar-caption-text:var(--color-foreground-muted);--color-sidebar-link-text:var(--color-foreground-secondary);--color-sidebar-link-text--top-level:var(--color-brand-primary);--color-sidebar-item-background:var(--color-sidebar-background);--color-sidebar-item-background--current:var( --color-sidebar-item-background );--color-sidebar-item-background--hover:linear-gradient(90deg,var(--color-background-hover--transparent) 0%,var(--color-background-hover) var(--sidebar-item-spacing-horizontal),var(--color-background-hover) 100%);--color-sidebar-item-expander-background:transparent;--color-sidebar-item-expander-background--hover:var( --color-background-hover );--color-sidebar-search-text:var(--color-foreground-primary);--color-sidebar-search-background:var(--color-background-secondary);--color-sidebar-search-background--focus:var(--color-background-primary);--color-sidebar-search-border:var(--color-background-border);--color-sidebar-search-icon:var(--color-foreground-muted);--color-toc-background:var(--color-background-primary);--color-toc-title-text:var(--color-foreground-muted);--color-toc-item-text:var(--color-foreground-secondary);--color-toc-item-text--hover:var(--color-foreground-primary);--color-toc-item-text--active:var(--color-brand-primary);--color-content-foreground:var(--color-foreground-primary);--color-content-background:transparent;--color-link:var(--color-brand-content);--color-link-underline:var(--color-background-border);--color-link--hover:var(--color-brand-content);--color-link-underline--hover:var(--color-foreground-border);--color-link--visited:var(--color-brand-visited);--color-link-underline--visited:var(--color-background-border);--color-link--visited--hover:var(--color-brand-visited);--color-link-underline--visited--hover:var(--color-foreground-border)}.only-light{display:block!important}html body .only-dark{display:none!important}@media not print{body[data-theme=dark]{--color-problematic:#ee5151;--color-foreground-primary:#cfd0d0;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body[data-theme=dark] .only-light{display:none!important}body[data-theme=dark] .only-dark{display:block!important}@media(prefers-color-scheme:dark){body:not([data-theme=light]){--color-problematic:#ee5151;--color-foreground-primary:#cfd0d0;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body:not([data-theme=light]) .only-light{display:none!important}body:not([data-theme=light]) .only-dark{display:block!important}}}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:block}@media(prefers-color-scheme:dark){body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-dark{display:block}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:none}}body[data-theme=dark] .theme-toggle svg.theme-icon-when-dark,body[data-theme=light] .theme-toggle svg.theme-icon-when-light{display:block}body{font-family:var(--font-stack)}code,kbd,pre,samp{font-family:var(--font-stack--monospace)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article{line-height:1.5}h1,h2,h3,h4,h5,h6{border-radius:.5rem;font-family:var(--font-stack--headings);font-weight:700;line-height:1.25;margin:.5rem -.5rem;padding-left:.5rem;padding-right:.5rem}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p{margin-top:0}h1{font-size:2.5em;margin-bottom:1rem}h1,h2{margin-top:1.75rem}h2{font-size:2em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1.125em}h6{font-size:1em}small{font-size:80%;opacity:75%}p{margin-bottom:.75rem;margin-top:.5rem}hr.docutils{background-color:var(--color-background-border);border:0;height:1px;margin:2rem 0;padding:0}.centered{text-align:center}a{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}a:visited{color:var(--color-link--visited);text-decoration-color:var(--color-link-underline--visited)}a:visited:hover{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}a:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link{color:inherit}a.muted-link:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link:hover:visited{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}html{overflow-x:hidden;overflow-y:scroll;scroll-behavior:smooth}.sidebar-scroll,.toc-scroll,article[role=main] *{scrollbar-color:var(--color-foreground-border) transparent;scrollbar-width:thin}body,html{height:100%}.skip-to-content,body,html{background:var(--color-background-primary);color:var(--color-foreground-primary)}.skip-to-content{border-radius:1rem;left:.25rem;padding:1rem;position:fixed;top:.25rem;transform:translateY(-200%);transition:transform .3s ease-in-out;z-index:40}.skip-to-content:focus-within{transform:translateY(0)}article{background:var(--color-content-background);color:var(--color-content-foreground);overflow-wrap:break-word}.page{display:flex;min-height:100%}.mobile-header{background-color:var(--color-header-background);border-bottom:1px solid var(--color-header-border);color:var(--color-header-text);display:none;height:var(--header-height);width:100%;z-index:10}.mobile-header.scrolled{border-bottom:none;box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2)}.mobile-header .header-center a{color:var(--color-header-text);text-decoration:none}.main{display:flex;flex:1}.sidebar-drawer{background:var(--color-sidebar-background);border-right:1px solid var(--color-sidebar-background-border);box-sizing:border-box;display:flex;justify-content:flex-end;min-width:15em;width:calc(50% - 26em)}.sidebar-container,.toc-drawer{box-sizing:border-box;width:15em}.toc-drawer{background:var(--color-toc-background);padding-right:1rem}.sidebar-sticky,.toc-sticky{display:flex;flex-direction:column;height:min(100%,100vh);height:100vh;position:sticky;top:0}.sidebar-scroll,.toc-scroll{flex-grow:1;flex-shrink:1;overflow:auto;scroll-behavior:smooth}.content{display:flex;flex-direction:column;justify-content:space-between;padding:0 3em;width:46em}.icon{display:inline-block;height:1rem;width:1rem}.icon svg{height:100%;width:100%}.announcement{align-items:center;background-color:var(--color-announcement-background);color:var(--color-announcement-text);display:flex;height:var(--header-height);overflow-x:auto}.announcement+.page{min-height:calc(100% - var(--header-height))}.announcement-content{box-sizing:border-box;min-width:100%;padding:.5rem;text-align:center;white-space:nowrap}.announcement-content a{color:var(--color-announcement-text);text-decoration-color:var(--color-announcement-text)}.announcement-content a:hover{color:var(--color-announcement-text);text-decoration-color:var(--color-link--hover)}.no-js .theme-toggle-container{display:none}.theme-toggle-container{display:flex}.theme-toggle{background:transparent;border:none;cursor:pointer;display:flex;padding:0}.theme-toggle svg{color:var(--color-foreground-primary);display:none;height:1.25rem;width:1.25rem}.theme-toggle-header{align-items:center;display:flex;justify-content:center}.nav-overlay-icon,.toc-overlay-icon{cursor:pointer;display:none}.nav-overlay-icon .icon,.toc-overlay-icon .icon{color:var(--color-foreground-secondary);height:1.5rem;width:1.5rem}.nav-overlay-icon,.toc-header-icon{align-items:center;justify-content:center}.toc-content-icon{height:1.5rem;width:1.5rem}.content-icon-container{display:flex;float:right;gap:.5rem;margin-bottom:1rem;margin-left:1rem;margin-top:1.5rem}.content-icon-container .edit-this-page svg,.content-icon-container .view-this-page svg{color:inherit;height:1.25rem;width:1.25rem}.sidebar-toggle{display:none;position:absolute}.sidebar-toggle[name=__toc]{left:20px}.sidebar-toggle:checked{left:40px}.overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0s,height 0s,opacity .25s ease-out;width:0}.sidebar-overlay{z-index:20}.toc-overlay{z-index:40}.sidebar-drawer{transition:left .25s ease-in-out;z-index:30}.toc-drawer{transition:right .25s ease-in-out;z-index:50}#__navigation:checked~.sidebar-overlay{height:100%;opacity:1;width:100%}#__navigation:checked~.page .sidebar-drawer{left:0;top:0}#__toc:checked~.toc-overlay{height:100%;opacity:1;width:100%}#__toc:checked~.page .toc-drawer{right:0;top:0}.back-to-top{background:var(--color-background-primary);border-radius:1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 1px 0 hsla(220,9%,46%,.502);display:none;font-size:.8125rem;left:0;margin-left:50%;padding:.5rem .75rem .5rem .5rem;position:fixed;text-decoration:none;top:1rem;transform:translateX(-50%);z-index:10}.back-to-top svg{height:1rem;width:1rem;fill:currentColor;display:inline-block}.back-to-top span{margin-left:.25rem}.show-back-to-top .back-to-top{align-items:center;display:flex}@media(min-width:97em){html{font-size:110%}}@media(max-width:82em){.toc-content-icon{display:flex}.toc-drawer{border-left:1px solid var(--color-background-muted);height:100vh;position:fixed;right:-15em;top:0}.toc-tree{border-left:none;font-size:var(--toc-font-size--mobile)}.sidebar-drawer{width:calc(50% - 18.5em)}}@media(max-width:67em){.content{margin-left:auto;margin-right:auto;padding:0 1em}}@media(max-width:63em){.nav-overlay-icon{display:flex}.sidebar-drawer{height:100vh;left:-15em;position:fixed;top:0;width:15em}.theme-toggle-header,.toc-header-icon{display:flex}.theme-toggle-content,.toc-content-icon{display:none}.mobile-header{align-items:center;display:flex;justify-content:space-between;position:sticky;top:0}.mobile-header .header-left,.mobile-header .header-right{display:flex;height:var(--header-height);padding:0 var(--header-padding)}.mobile-header .header-left label,.mobile-header .header-right label{height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.nav-overlay-icon .icon,.theme-toggle svg{height:1.5rem;width:1.5rem}:target{scroll-margin-top:calc(var(--header-height) + 2.5rem)}.back-to-top{top:calc(var(--header-height) + .5rem)}.page{flex-direction:column;justify-content:center}}@media(max-width:48em){.content{overflow-x:auto;width:100%}}@media(max-width:46em){article[role=main] aside.sidebar{float:none;margin:1rem 0;width:100%}}.admonition,.topic{background:var(--color-admonition-background);border-radius:.2rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1);font-size:var(--admonition-font-size);margin:1rem auto;overflow:hidden;padding:0 .5rem .5rem;page-break-inside:avoid}.admonition>:nth-child(2),.topic>:nth-child(2){margin-top:0}.admonition>:last-child,.topic>:last-child{margin-bottom:0}.admonition p.admonition-title,p.topic-title{font-size:var(--admonition-title-font-size);font-weight:500;line-height:1.3;margin:0 -.5rem .5rem;padding:.4rem .5rem .4rem 2rem;position:relative}.admonition p.admonition-title:before,p.topic-title:before{content:"";height:1rem;left:.5rem;position:absolute;width:1rem}p.admonition-title{background-color:var(--color-admonition-title-background)}p.admonition-title:before{background-color:var(--color-admonition-title);-webkit-mask-image:var(--icon-admonition-default);mask-image:var(--icon-admonition-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}p.topic-title{background-color:var(--color-topic-title-background)}p.topic-title:before{background-color:var(--color-topic-title);-webkit-mask-image:var(--icon-topic-default);mask-image:var(--icon-topic-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.admonition{border-left:.2rem solid var(--color-admonition-title)}.admonition.caution{border-left-color:var(--color-admonition-title--caution)}.admonition.caution>.admonition-title{background-color:var(--color-admonition-title-background--caution)}.admonition.caution>.admonition-title:before{background-color:var(--color-admonition-title--caution);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.warning{border-left-color:var(--color-admonition-title--warning)}.admonition.warning>.admonition-title{background-color:var(--color-admonition-title-background--warning)}.admonition.warning>.admonition-title:before{background-color:var(--color-admonition-title--warning);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.danger{border-left-color:var(--color-admonition-title--danger)}.admonition.danger>.admonition-title{background-color:var(--color-admonition-title-background--danger)}.admonition.danger>.admonition-title:before{background-color:var(--color-admonition-title--danger);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.attention{border-left-color:var(--color-admonition-title--attention)}.admonition.attention>.admonition-title{background-color:var(--color-admonition-title-background--attention)}.admonition.attention>.admonition-title:before{background-color:var(--color-admonition-title--attention);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.error{border-left-color:var(--color-admonition-title--error)}.admonition.error>.admonition-title{background-color:var(--color-admonition-title-background--error)}.admonition.error>.admonition-title:before{background-color:var(--color-admonition-title--error);-webkit-mask-image:var(--icon-failure);mask-image:var(--icon-failure)}.admonition.hint{border-left-color:var(--color-admonition-title--hint)}.admonition.hint>.admonition-title{background-color:var(--color-admonition-title-background--hint)}.admonition.hint>.admonition-title:before{background-color:var(--color-admonition-title--hint);-webkit-mask-image:var(--icon-question);mask-image:var(--icon-question)}.admonition.tip{border-left-color:var(--color-admonition-title--tip)}.admonition.tip>.admonition-title{background-color:var(--color-admonition-title-background--tip)}.admonition.tip>.admonition-title:before{background-color:var(--color-admonition-title--tip);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.important{border-left-color:var(--color-admonition-title--important)}.admonition.important>.admonition-title{background-color:var(--color-admonition-title-background--important)}.admonition.important>.admonition-title:before{background-color:var(--color-admonition-title--important);-webkit-mask-image:var(--icon-flame);mask-image:var(--icon-flame)}.admonition.note{border-left-color:var(--color-admonition-title--note)}.admonition.note>.admonition-title{background-color:var(--color-admonition-title-background--note)}.admonition.note>.admonition-title:before{background-color:var(--color-admonition-title--note);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition.seealso{border-left-color:var(--color-admonition-title--seealso)}.admonition.seealso>.admonition-title{background-color:var(--color-admonition-title-background--seealso)}.admonition.seealso>.admonition-title:before{background-color:var(--color-admonition-title--seealso);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.admonition-todo{border-left-color:var(--color-admonition-title--admonition-todo)}.admonition.admonition-todo>.admonition-title{background-color:var(--color-admonition-title-background--admonition-todo)}.admonition.admonition-todo>.admonition-title:before{background-color:var(--color-admonition-title--admonition-todo);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition-todo>.admonition-title{text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd{margin-left:2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:first-child{margin-top:.125rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list,dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:last-child{margin-bottom:.75rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list>dt{font-size:var(--font-size--small);text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd:empty{margin-bottom:.5rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul{margin-left:-1.2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p:nth-child(2){margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p+p:last-child:empty{margin-bottom:0;margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{color:var(--color-api-overall)}.sig:not(.sig-inline){background:var(--color-api-background);border-radius:.25rem;font-family:var(--font-stack--monospace);font-size:var(--api-font-size);font-weight:700;margin-left:-.25rem;margin-right:-.25rem;padding:.25rem .5rem .25rem 3em;text-indent:-2.5em;transition:background .1s ease-out}.sig:not(.sig-inline):hover{background:var(--color-api-background-hover)}.sig:not(.sig-inline) a.reference .viewcode-link{font-weight:400;width:4.25rem}em.property,span.property{font-style:normal}em.property:first-child,span.property:first-child{color:var(--color-api-keyword)}.sig-name{color:var(--color-api-name)}.sig-prename{color:var(--color-api-pre-name);font-weight:400}.sig-paren{color:var(--color-api-paren)}.sig-param{font-style:normal}div.deprecated,div.versionadded,div.versionchanged,div.versionremoved{border-left:.1875rem solid;border-radius:.125rem;padding-left:.75rem}div.deprecated p,div.versionadded p,div.versionchanged p,div.versionremoved p{margin-bottom:.125rem;margin-top:.125rem}div.versionadded{border-color:var(--color-api-added-border)}div.versionadded .versionmodified{color:var(--color-api-added)}div.versionchanged{border-color:var(--color-api-changed-border)}div.versionchanged .versionmodified{color:var(--color-api-changed)}div.deprecated{border-color:var(--color-api-deprecated-border)}div.deprecated .versionmodified{color:var(--color-api-deprecated)}div.versionremoved{border-color:var(--color-api-removed-border)}div.versionremoved .versionmodified{color:var(--color-api-removed)}.viewcode-back,.viewcode-link{float:right;text-align:right}.line-block{margin-bottom:.75rem;margin-top:.5rem}.line-block .line-block{margin-bottom:0;margin-top:0;padding-left:1rem}.code-block-caption,article p.caption,table>caption{font-size:var(--font-size--small);text-align:center}.toctree-wrapper.compound .caption,.toctree-wrapper.compound :not(.caption)>.caption-text{font-size:var(--font-size--small);margin-bottom:0;text-align:initial;text-transform:uppercase}.toctree-wrapper.compound>ul{margin-bottom:0;margin-top:0}.sig-inline,code.literal{background:var(--color-inline-code-background);border-radius:.2em;font-size:var(--font-size--small--2);padding:.1em .2em}pre.literal-block .sig-inline,pre.literal-block code.literal{font-size:inherit;padding:0}p .sig-inline,p code.literal{border:1px solid var(--color-background-border)}.sig-inline{font-family:var(--font-stack--monospace)}div[class*=" highlight-"],div[class^=highlight-]{display:flex;margin:1em 0}div[class*=" highlight-"] .table-wrapper,div[class^=highlight-] .table-wrapper,pre{margin:0;padding:0}pre{overflow:auto}article[role=main] .highlight pre{line-height:1.5}.highlight pre,pre.literal-block{font-size:var(--code-font-size);padding:.625rem .875rem}pre.literal-block{background-color:var(--color-code-background);border-radius:.2rem;color:var(--color-code-foreground);margin-bottom:1rem;margin-top:1rem}.highlight{border-radius:.2rem;width:100%}.highlight .gp,.highlight span.linenos{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.highlight .hll{display:block;margin-left:-.875rem;margin-right:-.875rem;padding-left:.875rem;padding-right:.875rem}.code-block-caption{background-color:var(--color-code-background);border-bottom:1px solid;border-radius:.25rem;border-bottom-left-radius:0;border-bottom-right-radius:0;border-color:var(--color-background-border);color:var(--color-code-foreground);display:flex;font-weight:300;padding:.625rem .875rem}.code-block-caption+div[class]{margin-top:0}.code-block-caption+div[class]>.highlight{border-top-left-radius:0;border-top-right-radius:0}.highlighttable{display:block;width:100%}.highlighttable tbody{display:block}.highlighttable tr{display:flex}.highlighttable td.linenos{background-color:var(--color-code-background);border-bottom-left-radius:.2rem;border-top-left-radius:.2rem;color:var(--color-code-foreground);padding:.625rem 0 .625rem .875rem}.highlighttable .linenodiv{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;font-size:var(--code-font-size);padding-right:.875rem}.highlighttable td.code{display:block;flex:1;overflow:hidden;padding:0}.highlighttable td.code .highlight{border-bottom-left-radius:0;border-top-left-radius:0}.highlight span.linenos{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;display:inline-block;margin-right:.875rem;padding-left:0;padding-right:.875rem}.footnote-reference{font-size:var(--font-size--small--4);vertical-align:super}dl.footnote.brackets{color:var(--color-foreground-secondary);display:grid;font-size:var(--font-size--small);grid-template-columns:max-content auto}dl.footnote.brackets dt{margin:0}dl.footnote.brackets dt>.fn-backref{margin-left:.25rem}dl.footnote.brackets dt:after{content:":"}dl.footnote.brackets dt .brackets:before{content:"["}dl.footnote.brackets dt .brackets:after{content:"]"}dl.footnote.brackets dd{margin:0;padding:0 1rem}aside.footnote{color:var(--color-foreground-secondary);font-size:var(--font-size--small)}aside.footnote>span,div.citation>span{float:left;font-weight:500;padding-right:.25rem}aside.footnote>:not(span),div.citation>p{margin-left:2rem}img{box-sizing:border-box;height:auto;max-width:100%}article .figure,article figure{border-radius:.2rem;margin:0}article .figure :last-child,article figure :last-child{margin-bottom:0}article .align-left{clear:left;float:left;margin:0 1rem 1rem}article .align-right{clear:right;float:right;margin:0 1rem 1rem}article .align-center,article .align-default{display:block;margin-left:auto;margin-right:auto;text-align:center}article table.align-default{display:table;text-align:initial}.domainindex-jumpbox,.genindex-jumpbox{border-bottom:1px solid var(--color-background-border);border-top:1px solid var(--color-background-border);padding:.25rem}.domainindex-section h2,.genindex-section h2{margin-bottom:.5rem;margin-top:.75rem}.domainindex-section ul,.genindex-section ul{margin-bottom:0;margin-top:0}ol,ul{margin-bottom:1rem;margin-top:1rem;padding-left:1.2rem}ol li>p:first-child,ul li>p:first-child{margin-bottom:.25rem;margin-top:.25rem}ol li>p:last-child,ul li>p:last-child{margin-top:.25rem}ol li>ol,ol li>ul,ul li>ol,ul li>ul{margin-bottom:.5rem;margin-top:.5rem}ol.arabic{list-style:decimal}ol.loweralpha{list-style:lower-alpha}ol.upperalpha{list-style:upper-alpha}ol.lowerroman{list-style:lower-roman}ol.upperroman{list-style:upper-roman}.simple li>ol,.simple li>ul,.toctree-wrapper li>ol,.toctree-wrapper li>ul{margin-bottom:0;margin-top:0}.field-list dt,.option-list dt,dl.footnote dt,dl.glossary dt,dl.simple dt,dl:not([class]) dt{font-weight:500;margin-top:.25rem}.field-list dt+dt,.option-list dt+dt,dl.footnote dt+dt,dl.glossary dt+dt,dl.simple dt+dt,dl:not([class]) dt+dt{margin-top:0}.field-list dt .classifier:before,.option-list dt .classifier:before,dl.footnote dt .classifier:before,dl.glossary dt .classifier:before,dl.simple dt .classifier:before,dl:not([class]) dt .classifier:before{content:":";margin-left:.2rem;margin-right:.2rem}.field-list dd ul,.field-list dd>p:first-child,.option-list dd ul,.option-list dd>p:first-child,dl.footnote dd ul,dl.footnote dd>p:first-child,dl.glossary dd ul,dl.glossary dd>p:first-child,dl.simple dd ul,dl.simple dd>p:first-child,dl:not([class]) dd ul,dl:not([class]) dd>p:first-child{margin-top:.125rem}.field-list dd ul,.option-list dd ul,dl.footnote dd ul,dl.glossary dd ul,dl.simple dd ul,dl:not([class]) dd ul{margin-bottom:.125rem}.math-wrapper{overflow-x:auto;width:100%}div.math{position:relative;text-align:center}div.math .headerlink,div.math:focus .headerlink{display:none}div.math:hover .headerlink{display:inline-block}div.math span.eqno{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);z-index:1}abbr[title]{cursor:help}.problematic{color:var(--color-problematic)}kbd:not(.compound){background-color:var(--color-background-secondary);border:1px solid var(--color-foreground-border);border-radius:.2rem;box-shadow:0 .0625rem 0 rgba(0,0,0,.2),inset 0 0 0 .125rem var(--color-background-primary);color:var(--color-foreground-primary);display:inline-block;font-size:var(--font-size--small--3);margin:0 .2rem;padding:0 .2rem;vertical-align:text-bottom}blockquote{background:var(--color-background-secondary);border-left:4px solid var(--color-background-border);margin-left:0;margin-right:0;padding:.5rem 1rem}blockquote .attribution{font-weight:600;text-align:right}blockquote.highlights,blockquote.pull-quote{font-size:1.25em}blockquote.epigraph,blockquote.pull-quote{border-left-width:0;border-radius:.5rem}blockquote.highlights{background:transparent;border-left-width:0}p .reference img{vertical-align:middle}p.rubric{font-size:1.125em;font-weight:700;line-height:1.25}dd p.rubric{font-size:var(--font-size--small);font-weight:inherit;line-height:inherit;text-transform:uppercase}article .sidebar{background-color:var(--color-background-secondary);border:1px solid var(--color-background-border);border-radius:.2rem;clear:right;float:right;margin-left:1rem;margin-right:0;width:30%}article .sidebar>*{padding-left:1rem;padding-right:1rem}article .sidebar>ol,article .sidebar>ul{padding-left:2.2rem}article .sidebar .sidebar-title{border-bottom:1px solid var(--color-background-border);font-weight:500;margin:0;padding:.5rem 1rem}[role=main] .table-wrapper.container{margin-bottom:.5rem;margin-top:1rem;overflow-x:auto;padding:.2rem .2rem .75rem;width:100%}table.docutils{border-collapse:collapse;border-radius:.2rem;border-spacing:0;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)}table.docutils th{background:var(--color-table-header-background)}table.docutils td,table.docutils th{border-bottom:1px solid var(--color-table-border);border-left:1px solid var(--color-table-border);border-right:1px solid var(--color-table-border);padding:0 .25rem}table.docutils td p,table.docutils th p{margin:.25rem}table.docutils td:first-child,table.docutils th:first-child{border-left:none}table.docutils td:last-child,table.docutils th:last-child{border-right:none}table.docutils td.text-left,table.docutils th.text-left{text-align:left}table.docutils td.text-right,table.docutils th.text-right{text-align:right}table.docutils td.text-center,table.docutils th.text-center{text-align:center}:target{scroll-margin-top:2.5rem}@media(max-width:67em){:target{scroll-margin-top:calc(2.5rem + var(--header-height))}section>span:target{scroll-margin-top:calc(2.8rem + var(--header-height))}}.headerlink{font-weight:100;-webkit-user-select:none;-moz-user-select:none;user-select:none}.code-block-caption>.headerlink,dl dt>.headerlink,figcaption p>.headerlink,h1>.headerlink,h2>.headerlink,h3>.headerlink,h4>.headerlink,h5>.headerlink,h6>.headerlink,p.caption>.headerlink,table>caption>.headerlink{margin-left:.5rem;visibility:hidden}.code-block-caption:hover>.headerlink,dl dt:hover>.headerlink,figcaption p:hover>.headerlink,h1:hover>.headerlink,h2:hover>.headerlink,h3:hover>.headerlink,h4:hover>.headerlink,h5:hover>.headerlink,h6:hover>.headerlink,p.caption:hover>.headerlink,table>caption:hover>.headerlink{visibility:visible}.code-block-caption>.toc-backref,dl dt>.toc-backref,figcaption p>.toc-backref,h1>.toc-backref,h2>.toc-backref,h3>.toc-backref,h4>.toc-backref,h5>.toc-backref,h6>.toc-backref,p.caption>.toc-backref,table>caption>.toc-backref{color:inherit;text-decoration-line:none}figure:hover>figcaption>p>.headerlink,table:hover>caption>.headerlink{visibility:visible}:target>h1:first-of-type,:target>h2:first-of-type,:target>h3:first-of-type,:target>h4:first-of-type,:target>h5:first-of-type,:target>h6:first-of-type,span:target~h1:first-of-type,span:target~h2:first-of-type,span:target~h3:first-of-type,span:target~h4:first-of-type,span:target~h5:first-of-type,span:target~h6:first-of-type{background-color:var(--color-highlight-on-target)}:target>h1:first-of-type code.literal,:target>h2:first-of-type code.literal,:target>h3:first-of-type code.literal,:target>h4:first-of-type code.literal,:target>h5:first-of-type code.literal,:target>h6:first-of-type code.literal,span:target~h1:first-of-type code.literal,span:target~h2:first-of-type code.literal,span:target~h3:first-of-type code.literal,span:target~h4:first-of-type code.literal,span:target~h5:first-of-type code.literal,span:target~h6:first-of-type code.literal{background-color:transparent}.literal-block-wrapper:target .code-block-caption,.this-will-duplicate-information-and-it-is-still-useful-here li :target,figure:target,table:target>caption{background-color:var(--color-highlight-on-target)}dt:target{background-color:var(--color-highlight-on-target)!important}.footnote-reference:target,.footnote>dt:target+dd{background-color:var(--color-highlight-on-target)}.guilabel{background-color:var(--color-guilabel-background);border:1px solid var(--color-guilabel-border);border-radius:.5em;color:var(--color-guilabel-text);font-size:.9em;padding:0 .3em}footer{display:flex;flex-direction:column;font-size:var(--font-size--small);margin-top:2rem}.bottom-of-page{align-items:center;border-top:1px solid var(--color-background-border);color:var(--color-foreground-secondary);display:flex;justify-content:space-between;line-height:1.5;margin-top:1rem;padding-bottom:1rem;padding-top:1rem}@media(max-width:46em){.bottom-of-page{flex-direction:column-reverse;gap:.25rem;text-align:center}}.bottom-of-page .left-details{font-size:var(--font-size--small)}.bottom-of-page .right-details{display:flex;flex-direction:column;gap:.25rem;text-align:right}.bottom-of-page .icons{display:flex;font-size:1rem;gap:.25rem;justify-content:flex-end}.bottom-of-page .icons a{text-decoration:none}.bottom-of-page .icons img,.bottom-of-page .icons svg{font-size:1.125rem;height:1em;width:1em}.related-pages a{align-items:center;display:flex;text-decoration:none}.related-pages a:hover .page-info .title{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}.related-pages a svg.furo-related-icon,.related-pages a svg.furo-related-icon>use{color:var(--color-foreground-border);flex-shrink:0;height:.75rem;margin:0 .5rem;width:.75rem}.related-pages a.next-page{clear:right;float:right;max-width:50%;text-align:right}.related-pages a.prev-page{clear:left;float:left;max-width:50%}.related-pages a.prev-page svg{transform:rotate(180deg)}.page-info{display:flex;flex-direction:column;overflow-wrap:anywhere}.next-page .page-info{align-items:flex-end}.page-info .context{align-items:center;color:var(--color-foreground-muted);display:flex;font-size:var(--font-size--small);padding-bottom:.1rem;text-decoration:none}ul.search{list-style:none;padding-left:0}ul.search li{border-bottom:1px solid var(--color-background-border);padding:1rem 0}[role=main] .highlighted{background-color:var(--color-highlighted-background);color:var(--color-highlighted-text)}.sidebar-brand{display:flex;flex-direction:column;flex-shrink:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none}.sidebar-brand-text{color:var(--color-sidebar-brand-text);font-size:1.5rem;overflow-wrap:break-word}.sidebar-brand-text,.sidebar-logo-container{margin:var(--sidebar-item-spacing-vertical) 0}.sidebar-logo{display:block;margin:0 auto;max-width:100%}.sidebar-search-container{align-items:center;background:var(--color-sidebar-search-background);display:flex;margin-top:var(--sidebar-search-space-above);position:relative}.sidebar-search-container:focus-within,.sidebar-search-container:hover{background:var(--color-sidebar-search-background--focus)}.sidebar-search-container:before{background-color:var(--color-sidebar-search-icon);content:"";height:var(--sidebar-search-icon-size);left:var(--sidebar-item-spacing-horizontal);-webkit-mask-image:var(--icon-search);mask-image:var(--icon-search);position:absolute;width:var(--sidebar-search-icon-size)}.sidebar-search{background:transparent;border:none;border-bottom:1px solid var(--color-sidebar-search-border);border-top:1px solid var(--color-sidebar-search-border);box-sizing:border-box;color:var(--color-sidebar-search-foreground);padding:var(--sidebar-search-input-spacing-vertical) var(--sidebar-search-input-spacing-horizontal) var(--sidebar-search-input-spacing-vertical) calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size));width:100%;z-index:10}.sidebar-search:focus{outline:none}.sidebar-search::-moz-placeholder{font-size:var(--sidebar-search-input-font-size)}.sidebar-search::placeholder{font-size:var(--sidebar-search-input-font-size)}#searchbox .highlight-link{margin:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0;text-align:center}#searchbox .highlight-link a{color:var(--color-sidebar-search-icon);font-size:var(--font-size--small--2)}.sidebar-tree{font-size:var(--sidebar-item-font-size);margin-bottom:var(--sidebar-item-spacing-vertical);margin-top:var(--sidebar-tree-space-above)}.sidebar-tree ul{display:flex;flex-direction:column;list-style:none;margin-bottom:0;margin-top:0;padding:0}.sidebar-tree li{margin:0;position:relative}.sidebar-tree li>ul{margin-left:var(--sidebar-item-spacing-horizontal)}.sidebar-tree .icon,.sidebar-tree .reference{color:var(--color-sidebar-link-text)}.sidebar-tree .reference{box-sizing:border-box;display:inline-block;height:100%;line-height:var(--sidebar-item-line-height);overflow-wrap:anywhere;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none;width:100%}.sidebar-tree .reference:hover{background:var(--color-sidebar-item-background--hover);color:var(--color-sidebar-link-text)}.sidebar-tree .reference.external:after{color:var(--color-sidebar-link-text);content:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23607d8b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M11 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-5M10 14 20 4M15 4h5v5'/%3E%3C/svg%3E");margin:0 .25rem;vertical-align:middle}.sidebar-tree .current-page>.reference{font-weight:700}.sidebar-tree label{align-items:center;cursor:pointer;display:flex;height:var(--sidebar-item-height);justify-content:center;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--sidebar-expander-width)}.sidebar-tree .caption,.sidebar-tree :not(.caption)>.caption-text{color:var(--color-sidebar-caption-text);font-size:var(--sidebar-caption-font-size);font-weight:700;margin:var(--sidebar-caption-space-above) 0 0 0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-transform:uppercase}.sidebar-tree li.has-children>.reference{padding-right:var(--sidebar-expander-width)}.sidebar-tree .toctree-l1>.reference,.sidebar-tree .toctree-l1>label .icon{color:var(--color-sidebar-link-text--top-level)}.sidebar-tree label{background:var(--color-sidebar-item-expander-background)}.sidebar-tree label:hover{background:var(--color-sidebar-item-expander-background--hover)}.sidebar-tree .current>.reference{background:var(--color-sidebar-item-background--current)}.sidebar-tree .current>.reference:hover{background:var(--color-sidebar-item-background--hover)}.toctree-checkbox{display:none;position:absolute}.toctree-checkbox~ul{display:none}.toctree-checkbox~label .icon svg{transform:rotate(90deg)}.toctree-checkbox:checked~ul{display:block}.toctree-checkbox:checked~label .icon svg{transform:rotate(-90deg)}.toc-title-container{padding:var(--toc-title-padding);padding-top:var(--toc-spacing-vertical)}.toc-title{color:var(--color-toc-title-text);font-size:var(--toc-title-font-size);padding-left:var(--toc-spacing-horizontal);text-transform:uppercase}.no-toc{display:none}.toc-tree-container{padding-bottom:var(--toc-spacing-vertical)}.toc-tree{border-left:1px solid var(--color-background-border);font-size:var(--toc-font-size);line-height:1.3;padding-left:calc(var(--toc-spacing-horizontal) - var(--toc-item-spacing-horizontal))}.toc-tree>ul>li:first-child{padding-top:0}.toc-tree>ul>li:first-child>ul{padding-left:0}.toc-tree>ul>li:first-child>a{display:none}.toc-tree ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:var(--toc-item-spacing-horizontal)}.toc-tree li{padding-top:var(--toc-item-spacing-vertical)}.toc-tree li.scroll-current>.reference{color:var(--color-toc-item-text--active);font-weight:700}.toc-tree a.reference{color:var(--color-toc-item-text);overflow-wrap:anywhere;text-decoration:none}.toc-scroll{max-height:100vh;overflow-y:scroll}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here){background:rgba(255,0,0,.25);color:var(--color-problematic)}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here):before{content:"ERROR: Adding a table of contents in Furo-based documentation is unnecessary, and does not work well with existing styling. Add a 'this-will-duplicate-information-and-it-is-still-useful-here' class, if you want an escape hatch."}.text-align\:left>p{text-align:left}.text-align\:center>p{text-align:center}.text-align\:right>p{text-align:right} +/*# sourceMappingURL=furo.css.map*/ \ No newline at end of file diff --git a/_static/styles/furo.css.map b/_static/styles/furo.css.map new file mode 100644 index 0000000..db1dec1 --- /dev/null +++ b/_static/styles/furo.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles/furo.css","mappings":"AAAA,2EAA2E,CAU3E,KACE,gBAAiB,CACjB,6BACF,CASA,KACE,QACF,CAMA,KACE,aACF,CAOA,GACE,aAAc,CACd,cACF,CAUA,GACE,sBAAuB,CACvB,QAAS,CACT,gBACF,CAOA,IACE,+BAAiC,CACjC,aACF,CASA,EACE,4BACF,CAOA,YACE,kBAAmB,CACnB,yBAA0B,CAC1B,gCACF,CAMA,SAEE,kBACF,CAOA,cAGE,+BAAiC,CACjC,aACF,CAeA,QAEE,aAAc,CACd,aAAc,CACd,iBAAkB,CAClB,uBACF,CAEA,IACE,aACF,CAEA,IACE,SACF,CASA,IACE,iBACF,CAUA,sCAKE,mBAAoB,CACpB,cAAe,CACf,gBAAiB,CACjB,QACF,CAOA,aAEE,gBACF,CAOA,cAEE,mBACF,CAMA,gDAIE,yBACF,CAMA,wHAIE,iBAAkB,CAClB,SACF,CAMA,4GAIE,6BACF,CAMA,SACE,0BACF,CASA,OACE,qBAAsB,CACtB,aAAc,CACd,aAAc,CACd,cAAe,CACf,SAAU,CACV,kBACF,CAMA,SACE,uBACF,CAMA,SACE,aACF,CAOA,6BAEE,qBAAsB,CACtB,SACF,CAMA,kFAEE,WACF,CAOA,cACE,4BAA6B,CAC7B,mBACF,CAMA,yCACE,uBACF,CAOA,6BACE,yBAA0B,CAC1B,YACF,CASA,QACE,aACF,CAMA,QACE,iBACF,CAiBA,kBACE,YACF,CCvVA,aAcE,kEACE,uBAOF,WACE,iDAMF,kCACE,wBAEF,qCAEE,uBADA,uBACA,CAEF,SACE,wBAtBA,CCpBJ,iBAGE,qBAEA,sBACA,0BAFA,oBAHA,4BACA,oBAKA,6BAIA,2CAFA,mBACA,sCAFA,4BAGA,CAEF,gBACE,aCPF,KCCE,mHAGA,wGAGA,wCAAyC,CAEzC,wBAAyB,CACzB,wBAAyB,CACzB,4BAA6B,CAC7B,yBAA0B,CAC1B,2BAA4B,CAG5B,sDAAuD,CACvD,gDAAiD,CACjD,wDAAyD,CAGzD,0CAA2C,CAC3C,gDAAiD,CACjD,gDAAiD,CAKjD,gCAAiC,CACjC,sCAAuC,CAGvC,2CAA4C,CAG5C,uCAAwC,CCnCxC,+FAIA,uBAAwB,CAGxB,iCAAkC,CAClC,kCAAmC,CAEnC,+BAAgC,CAChC,sCAAuC,CACvC,sCAAuC,CACvC,qGAIA,mDAAoD,CAEpD,mCAAoC,CACpC,8CAA+C,CAC/C,gDAAiD,CACjD,kCAAmC,CACnC,6DAA8D,CAG9D,6BAA8B,CAC9B,6BAA8B,CAC9B,+BAAgC,CAChC,kCAAmC,CACnC,kCAAmC,CCRjC,+jBCaA,iqCAZF,iaCXA,8KAOA,4SAWA,4SAUA,0CACA,gEAGA,0CAGA,gEAGA,yCACA,+DAIA,4CACA,kEAGA,wCAUA,8DACA,uCAGA,4DACA,sCACA,2DAGA,4CACA,kEACA,uCAGA,6DACA,2GAGA,sHAEA,yFAEA,+CACA,+EAGA,4MAOA,gCACA,sHAIA,kCACA,uEACA,gEACA,4DACA,kEAGA,2DACA,sDACA,0CACA,8CACA,wGAGA,0BACA,iCAGA,+DACA,+BACA,sCACA,+DAEA,kGACA,oCACA,yDACA,sCL3HF,kCAEA,sDAIA,0CKyHE,kEAIA,oDACA,sDAGA,oCACA,oEAEA,0DACA,qDAIA,oDACA,6DAIA,iEAIA,2DAIA,2DAGA,4DACA,gEAIA,gEAEA,gFAEA,oNASA,qDLtKE,gFAGE,4DAIF,oEKgHF,yEAEA,6DAGA,0DAEA,uDACA,qDACA,wDAIA,6DAIA,yDACA,2DAIA,uCAGA,wCACA,sDAGA,+CAGA,6DAEA,iDACA,+DAEA,wDAEA,sEAMA,0DACA,sBACA,mEL5JI,wEAEA,iCACE,+BAMN,wEAGA,iCACE,kFAEA,uEAIF,gEACE,8BAGF,qEMzDA,sCAKA,wFAKA,iCAIA,0BAWA,iCACA,4BACA,mCAGA,+BAEA,sCACA,4BAEA,mCAEA,sCAKA,sDAIA,gCAEA,gEAQF,wCAME,sBACA,kCAKA,uBAEA,gEAIA,2BAIA,mCAEA,qCACA,iCAGE,+BACA,wEAEE,iCACA,kFAGF,6BACA,0CACF,kCAEE,8BACE,8BACA,qEAEE,sCACA,wFClFN,iCAGF,2DACE,4BACA,oCAKF,8BAGE,sCACA,+DAIA,sCAEA,sDAGA,gCACA,gEAGA,+CAEA,sBACE,yCAGF,uBACA,sEAIA,aAEA,mCAIA,kEACA,aACA,oEACA,YAIA,EAQE,4HAGA,gDACE,mBACA,wCAON,wCAGE,0DACA,mBAKA,mBACA,CANA,uCAKA,iBALA,iBAWA,mBAGF,mBACE,mDAIF,+BAEE,CAEA,yBAFA,kBAMA,CAJA,GACA,aAGA,mBAEF,wBAEE,iBACA,iBAEA,OACA,aAGF,CAHE,WAGF,GAEE,oBAEA,CAJF,gBAIE,aAEA,+CAKA,UANA,WACA,cADA,SAMA,WACA,iBAEE,GAMF,wBANE,yBAMF,kDACA,WAEA,gCACA,2DAGA,iBACE,uCAEJ,kEAIE,uCAGA,yDACE,cACA,+DAEA,yDAEE,mEAMJ,kEAMA,uBACA,kBAEA,uBACA,kDAKA,0DAIA,CALA,oBAKA,WACA,WAQA,4BAFF,0CAEE,CARA,qCAsBA,CAdA,iBAEA,kBACE,aADF,4BACE,WAMF,2BAGF,qCAEE,CAXE,UAWF,+BAGA,uBAEA,SAEA,0CAIE,CANF,qCAEA,CAIE,2DACE,gBAIN,+CAIA,CAEA,kDAKE,CAPF,8BAEA,CAOE,YACA,CAjBI,2BAGN,CAHM,WAcJ,UAGA,CAEA,2GAIF,iCAGE,8BAIA,qBACA,oBACF,uBAOI,0CAIA,CATF,6DAKE,CALF,sBASE,qCAKF,CACE,cACA,CAFF,sBAEE,CACA,+BAEA,qBAEE,WAKN,aACE,sCAGA,mBAEA,6BAMA,kCACA,CAJA,sBACA,aAEA,CAJA,eACA,MAIA,2FAEA,UAGA,YACA,sBACE,8BAEA,CALF,aACA,WAIE,OACA,oBAEF,uBACE,WAEF,YAFE,UAEF,eAgBA,kBACE,CAhBA,qDAQF,qCAGF,CAGI,YACF,CAJF,2BAGI,CAEA,eACA,qBAGA,mEAEA,qBACA,8BAIA,kBADF,kBACE,yBAEJ,oCAGI,qDAIJ,+BAGI,oCAEA,+CAQF,4CACE,yBACF,2BAOE,sBACA,CAHA,WACA,CAFF,cACE,CAJA,YAGF,CAEE,SAEA,mBAGA,kDAEE,CAJF,cAEA,cAEE,sBAEA,mBADA,YACA,uBACA,mDACE,CADF,YACE,iDAEA,uCAEN,+DAOE,mBADF,sBACE,mBAGF,aACE,sCAIA,aADF,WACE,CAKF,SACE,CAHJ,kBAEE,CAJE,gBAEJ,CAHI,iBAMA,yFAKA,aACA,eACA,cCxaJ,iBAEE,aADA,iBACA,6BAEA,kCAEA,SACA,UAIA,gCACA,CALA,SAEA,SAEA,CAJA,wEAEA,CAFA,OAKA,CAGA,mDACE,iBAGF,gCACE,CADF,UACE,aAEJ,iCAEE,CAFF,UAEE,wCAEA,WACA,WADA,UACA,CACA,4CAGA,MACA,CADA,KACA,wCACA,UAGA,CAJA,UAIA,6DAUA,0CACE,CAFF,mBAEE,wEACA,CAVA,YACA,CAMF,mBAJE,OAOA,gBAJJ,gCACE,CANE,cACA,CAHA,oBACA,CAGA,QAGJ,CAII,0BACA,CADA,UACA,wCAEJ,kBACE,0DACA,gCACE,kBACA,CADA,YACA,oEACA,2CAMF,mDAII,CALN,YACE,CANE,cAKJ,CACE,iBAII,kEACA,yCACE,kDACA,yDACE,+CACA,uBANN,CAMM,+BANN,uCACE,qDACA,4BAEE,mBADA,0CACA,CADA,qBACA,0DACE,wCACA,sGALJ,oCACA,sBACE,kBAFF,UAEE,2CACA,wFACE,cACA,kEANN,uBACE,iDACA,CADA,UACA,0DACE,wDAEE,iEACA,qEANN,sCACE,CAGE,iBAHF,gBAGE,qBACE,CAJJ,uBACA,gDACE,wDACA,6DAHF,2CACA,CADA,gBACA,eACE,CAGE,sBANN,8BACE,CAII,iBAFF,4DACA,WACE,YADF,uCACE,6EACA,2BANN,8CACE,kDACA,0CACE,8BACA,yFACE,sBACA,sFALJ,mEACA,sBACE,kEACA,6EACE,uCACA,kEALJ,qGAEE,kEACA,6EACE,uCACA,kEALJ,8CACA,uDACE,sEACA,2EACE,sCACA,iEALJ,mGACA,qCACE,oDACA,0DACE,6GACA,gDAGR,yDCvEA,sEACE,CACA,6GACE,gEACF,iGAIF,wFACE,qDAGA,mGAEE,2CAEF,4FACE,gCACF,wGACE,8DAEE,6FAIA,iJAKN,6GACE,gDAKF,yDACA,qCAGA,6BACA,kBACA,qDAKA,oCAEA,+DAGA,2CAGE,oDAIA,oEAEE,qBAEN,wDAEE,uCACE,kEAGJ,CACE,6CACA,uDAGF,CACE,mCAEF,yDAIE,gEAGA,CAEA,wHAIF,sDACE,+DAEE,sCAGF,8BACA,oCACE,oHAIF,gBACE,yGAIF,mBChHA,2MCDF,4HAQE,wKAOA,8HCbA,mBAEA,6HAIE,YACA,mIAaJ,gBAPE,YAOF,4FAKE,qDAuBE,sCACA,CAHA,oBAEA,CAbF,wCACE,CALF,8BAIA,CARE,eAIF,CAKE,mBAEF,qBAEE,CAIF,+BACE,mBACA,CAGA,kCACA,6BAIF,4CAIA,kDACE,6BACA,2BAGF,iBACE,mDAGA,8BACA,WAGJ,2BACE,cAGA,+BACA,CAHA,eAGA,wCACA,YACA,iBACA,uEAGA,0BACA,2CAEA,8EAGI,qBACA,CAFF,kBAEE,4DAMJ,mCACE,4BAGA,oBAGF,4CACE,qCACA,8BACA,gBACA,+CAEA,iCAEF,iCACE,oBACA,4CACA,qCAGF,8BAEE,+BAEA,WAEA,8BACE,oBACA,CADA,gBACA,yBAKF,gBADF,YACE,CACA,iBACA,qDAEA,mDCvIJ,2FAMA,iCACE,CACA,eAEA,CAFA,mBADA,wBAIA,8BACA,gBADA,YACA,0BAEE,8CAGA,wDAIE,gFAGE,iBAEN,wCAKF,+CACE,CACA,oDAEF,kDAIE,YAEF,CAHE,YAGF,CCpCE,mFAFA,QACA,UAIA,CAHA,IAGA,gDAGE,eACA,iEAGF,wBAEE,mBAMA,6CAEF,CAJE,mBACA,CAGF,kCAGE,CARF,kBACE,CAHA,eAUA,YACA,mBACA,CAFA,UAEA,wCC/BJ,mBACE,CDkCE,wBACA,sBCpCJ,iBACE,mDACA,2CACA,sBAGA,qBCDA,6CAIE,CATJ,uBAKE,CDGE,oBACF,yDAEE,CCDE,2CAGF,CAJA,kCACE,CDJJ,aAKE,eCXJ,CDME,uBCOE,gCACE,YAEF,2CAEE,wBACA,0BAIF,iBAEA,cADF,UACE,uBAEA,iCAEA,wCAEA,6CAMA,CAYF,gCATI,4BASJ,CAZE,mCAEE,iCAUJ,4BAGE,4DADA,+BACA,CAHF,qBAGE,sCACE,OAEF,iBAHA,SAGA,iHACE,2DAKF,CANA,8EAMA,uSAEE,kBAEF,+FACE,yCCjEJ,WACA,yBAGA,uBACA,gBAEA,uCAIA,CAJA,iCAIA,uCAGA,UACE,gBACA,qBAEA,0CClBJ,gBACE,KAGF,qBACE,YAGF,CAHE,cAGF,gCAEE,mBACA,iEAEA,oCACA,wCAEA,sBACA,WAEA,CAFA,YAEA,8EAEA,mCAFA,iBAEA,6BAIA,wEAKA,sDAIE,CARF,mDAIA,CAIE,cAEF,8CAIA,oBAFE,iBAEF,8CAGE,eAEF,CAFE,YAEF,OAEE,kBAGJ,CAJI,eACA,CAFF,mBAKF,yCCjDE,oBACA,CAFA,iBAEA,uCAKE,iBACA,qCAGA,mBCZJ,CDWI,gBCXJ,6BAEE,eACA,sBAGA,eAEA,sBACA,oDACA,iGAMA,gBAFE,YAEF,8FAME,iJCnBF,YACA,gNAWE,gDAEF,iSAaE,kBACE,gHAKF,oCACE,eACF,CADE,UACF,8CACE,gDACF,wCACE,oBCtCJ,oBAEF,6BACE,QACE,kDAGF,yBACE,kDAmBA,kDAEF,CAhBA,+CAaA,CAbA,oBAaA,0FACE,CADF,gGAfF,cACE,gBACA,CAaA,0BAGA,mQACE,gBAGF,oMACE,iBACA,CAFF,eACE,CADF,gBAEE,aAGJ,iCAEE,CAFF,wCAEE,wBAUE,+VAIE,uEAHA,2BAGA,wXAKJ,iDAGF,CARM,+CACE,iDAIN,CALI,gBAQN,mHACE,gBAGF,2DACE,0EAOA,0EAGF,gBAEE,6DCjFA,kDACA,gCACA,qDAGA,qBACA,qDCDA,cACA,eAEA,yBAGF,sBAEE,iBACA,sNAWA,iBACE,kBACA,wRAgBA,kBAEA,iOAgBA,uCACE,uEAEA,kBAEF,qUAuBE,iDAIJ,CACA,geCzFF,4BAEE,CAQA,6JACA,iDAIA,sEAGA,mDAOF,iDAGE,4DAIA,8CACA,qDAEE,eAFF,cAEE,oBAEF,uBAFE,kCAGA,eACA,iBACA,mBAIA,mDACA,CAHA,uCAEA,CAJA,0CACA,CAIA,gBAJA,gBACA,oBADA,gBAIA,wBAEJ,gBAGE,6BACA,YAHA,iBAGA,gCACA,iEAEA,6CACA,sDACA,0BADA,wBACA,0BACA,oIAIA,mBAFA,YAEA,qBACA,0CAIE,uBAEF,CAHA,yBACE,CAEF,iDACE,mFAKJ,oCACE,CANE,aAKJ,CACE,qEAIA,YAFA,WAEA,CAHA,aACA,CAEA,gBACE,4BACA,sBADA,aACA,gCAMF,oCACA,yDACA,2CAEA,qBAGE,kBAEA,CACA,mCAIF,CARE,YACA,CAOF,iCAEE,CAPA,oBACA,CAQA,oBACE,uDAEJ,sDAGA,CAHA,cAGA,0BACE,oDAIA,oCACA,4BACA,sBAGA,cAEA,oFAGA,sBAEA,yDACE,CAIF,iBAJE,wBAIF,6CAHE,6CAKA,eACA,aACA,CADA,cACA,yCAGJ,kBACE,CAKA,iDAEA,CARF,aACE,4CAGA,kBAIA,wEAGA,wDAGA,kCAOA,iDAGA,CAPF,WAEE,sCAEA,CAJF,2CACE,CAMA,qCACA,+BARF,kBACE,qCAOA,iBAsBA,sBACE,CAvBF,WAKA,CACE,0DAIF,CALA,uDACE,CANF,sBAqBA,4CACA,CALA,gRAIA,YAEE,6CAEN,mCAEE,+CASA,6EAIA,4BChNA,SDmNA,qFCnNA,gDACA,sCAGA,qCACA,sDACA,CAKA,kDAGA,CARA,0CAQA,kBAGA,YACA,sBACA,iBAFA,gBADF,YACE,CAHA,SAKA,kBAEA,SAFA,iBAEA,uEAGA,CAEE,6CAFF,oCAgBI,CAdF,yBACE,qBACF,CAGF,oBACE,CAIF,WACE,CALA,2CAGA,uBACF,CACE,mFAGE,CALF,qBAEA,UAGE,gCAIF,sDAEA,CALE,oCAKF,yCC7CJ,oCACE,CD+CA,yXAQE,sCCrDJ,wCAGA,oCACE","sources":["webpack:///./node_modules/normalize.css/normalize.css","webpack:///./src/furo/assets/styles/base/_print.sass","webpack:///./src/furo/assets/styles/base/_screen-readers.sass","webpack:///./src/furo/assets/styles/base/_theme.sass","webpack:///./src/furo/assets/styles/variables/_fonts.scss","webpack:///./src/furo/assets/styles/variables/_spacing.scss","webpack:///./src/furo/assets/styles/variables/_icons.scss","webpack:///./src/furo/assets/styles/variables/_admonitions.scss","webpack:///./src/furo/assets/styles/variables/_colors.scss","webpack:///./src/furo/assets/styles/base/_typography.sass","webpack:///./src/furo/assets/styles/_scaffold.sass","webpack:///./src/furo/assets/styles/content/_admonitions.sass","webpack:///./src/furo/assets/styles/content/_api.sass","webpack:///./src/furo/assets/styles/content/_blocks.sass","webpack:///./src/furo/assets/styles/content/_captions.sass","webpack:///./src/furo/assets/styles/content/_code.sass","webpack:///./src/furo/assets/styles/content/_footnotes.sass","webpack:///./src/furo/assets/styles/content/_images.sass","webpack:///./src/furo/assets/styles/content/_indexes.sass","webpack:///./src/furo/assets/styles/content/_lists.sass","webpack:///./src/furo/assets/styles/content/_math.sass","webpack:///./src/furo/assets/styles/content/_misc.sass","webpack:///./src/furo/assets/styles/content/_rubrics.sass","webpack:///./src/furo/assets/styles/content/_sidebar.sass","webpack:///./src/furo/assets/styles/content/_tables.sass","webpack:///./src/furo/assets/styles/content/_target.sass","webpack:///./src/furo/assets/styles/content/_gui-labels.sass","webpack:///./src/furo/assets/styles/components/_footer.sass","webpack:///./src/furo/assets/styles/components/_sidebar.sass","webpack:///./src/furo/assets/styles/components/_table_of_contents.sass","webpack:///./src/furo/assets/styles/_shame.sass"],"sourcesContent":["/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n line-height: 1.15; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n/* Grouping content\n ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n box-sizing: content-box; /* 1 */\n height: 0; /* 1 */\n overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n border-bottom: none; /* 1 */\n text-decoration: underline; /* 2 */\n text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/* Embedded content\n ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n border-style: none;\n}\n\n/* Forms\n ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-size: 100%; /* 1 */\n line-height: 1.15; /* 1 */\n margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n border-style: none;\n padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n * `fieldset` elements in all browsers.\n */\n\nlegend {\n box-sizing: border-box; /* 1 */\n color: inherit; /* 2 */\n display: table; /* 1 */\n max-width: 100%; /* 1 */\n padding: 0; /* 3 */\n white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n box-sizing: border-box; /* 1 */\n padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/* Interactive\n ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n display: list-item;\n}\n\n/* Misc\n ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n display: none;\n}\n","// This file contains styles for managing print media.\n\n////////////////////////////////////////////////////////////////////////////////\n// Hide elements not relevant to print media.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n // Hide icon container.\n .content-icon-container\n display: none !important\n\n // Hide showing header links if hovering over when printing.\n .headerlink\n display: none !important\n\n // Hide mobile header.\n .mobile-header\n display: none !important\n\n // Hide navigation links.\n .related-pages\n display: none !important\n\n////////////////////////////////////////////////////////////////////////////////\n// Tweaks related to decolorization.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n // Apply a border around code which no longer have a color background.\n .highlight\n border: 0.1pt solid var(--color-foreground-border)\n\n////////////////////////////////////////////////////////////////////////////////\n// Avoid page break in some relevant cases.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n ul, ol, dl, a, table, pre, blockquote, p\n page-break-inside: avoid\n\n h1, h2, h3, h4, h5, h6, img, figure, caption\n page-break-inside: avoid\n page-break-after: avoid\n\n ul, ol, dl\n page-break-before: avoid\n",".visually-hidden\n position: absolute !important\n width: 1px !important\n height: 1px !important\n padding: 0 !important\n margin: -1px !important\n overflow: hidden !important\n clip: rect(0,0,0,0) !important\n white-space: nowrap !important\n border: 0 !important\n color: var(--color-foreground-primary)\n background: var(--color-background-primary)\n\n:-moz-focusring\n outline: auto\n","// This file serves as the \"skeleton\" of the theming logic.\n//\n// This contains the bulk of the logic for handling dark mode, color scheme\n// toggling and the handling of color-scheme-specific hiding of elements.\n\n@use \"../variables\" as *\n\nbody\n @include fonts\n @include spacing\n @include icons\n @include admonitions\n @include default-admonition(#651fff, \"abstract\")\n @include default-topic(#14B8A6, \"pencil\")\n\n @include colors\n\n.only-light\n display: block !important\nhtml body .only-dark\n display: none !important\n\n// Ignore dark-mode hints if print media.\n@media not print\n // Enable dark-mode, if requested.\n body[data-theme=\"dark\"]\n @include colors-dark\n\n html & .only-light\n display: none !important\n .only-dark\n display: block !important\n\n // Enable dark mode, unless explicitly told to avoid.\n @media (prefers-color-scheme: dark)\n body:not([data-theme=\"light\"])\n @include colors-dark\n\n html & .only-light\n display: none !important\n .only-dark\n display: block !important\n\n//\n// Theme toggle presentation\n//\nbody[data-theme=\"auto\"]\n .theme-toggle svg.theme-icon-when-auto-light\n display: block\n\n @media (prefers-color-scheme: dark)\n .theme-toggle svg.theme-icon-when-auto-dark\n display: block\n .theme-toggle svg.theme-icon-when-auto-light\n display: none\n\nbody[data-theme=\"dark\"]\n .theme-toggle svg.theme-icon-when-dark\n display: block\n\nbody[data-theme=\"light\"]\n .theme-toggle svg.theme-icon-when-light\n display: block\n","// Fonts used by this theme.\n//\n// There are basically two things here -- using the system font stack and\n// defining sizes for various elements in %ages. We could have also used `em`\n// but %age is easier to reason about for me.\n\n@mixin fonts {\n // These are adapted from https://systemfontstack.com/\n --font-stack:\n -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,\n Apple Color Emoji, Segoe UI Emoji;\n --font-stack--monospace:\n \"SFMono-Regular\", Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,\n monospace;\n --font-stack--headings: var(--font-stack);\n\n --font-size--normal: 100%;\n --font-size--small: 87.5%;\n --font-size--small--2: 81.25%;\n --font-size--small--3: 75%;\n --font-size--small--4: 62.5%;\n\n // Sidebar\n --sidebar-caption-font-size: var(--font-size--small--2);\n --sidebar-item-font-size: var(--font-size--small);\n --sidebar-search-input-font-size: var(--font-size--small);\n\n // Table of Contents\n --toc-font-size: var(--font-size--small--3);\n --toc-font-size--mobile: var(--font-size--normal);\n --toc-title-font-size: var(--font-size--small--4);\n\n // Admonitions\n //\n // These aren't defined in terms of %ages, since nesting these is permitted.\n --admonition-font-size: 0.8125rem;\n --admonition-title-font-size: 0.8125rem;\n\n // Code\n --code-font-size: var(--font-size--small--2);\n\n // API\n --api-font-size: var(--font-size--small);\n}\n","// Spacing for various elements on the page\n//\n// If the user wants to tweak things in a certain way, they are permitted to.\n// They also have to deal with the consequences though!\n\n@mixin spacing {\n // Header!\n --header-height: calc(\n var(--sidebar-item-line-height) + 4 *\n #{var(--sidebar-item-spacing-vertical)}\n );\n --header-padding: 0.5rem;\n\n // Sidebar\n --sidebar-tree-space-above: 1.5rem;\n --sidebar-caption-space-above: 1rem;\n\n --sidebar-item-line-height: 1rem;\n --sidebar-item-spacing-vertical: 0.5rem;\n --sidebar-item-spacing-horizontal: 1rem;\n --sidebar-item-height: calc(\n var(--sidebar-item-line-height) + 2 *#{var(--sidebar-item-spacing-vertical)}\n );\n\n --sidebar-expander-width: var(--sidebar-item-height); // be square\n\n --sidebar-search-space-above: 0.5rem;\n --sidebar-search-input-spacing-vertical: 0.5rem;\n --sidebar-search-input-spacing-horizontal: 0.5rem;\n --sidebar-search-input-height: 1rem;\n --sidebar-search-icon-size: var(--sidebar-search-input-height);\n\n // Table of Contents\n --toc-title-padding: 0.25rem 0;\n --toc-spacing-vertical: 1.5rem;\n --toc-spacing-horizontal: 1.5rem;\n --toc-item-spacing-vertical: 0.4rem;\n --toc-item-spacing-horizontal: 1rem;\n}\n","// Expose theme icons as CSS variables.\n\n$icons: (\n // Adapted from tabler-icons\n // url: https://tablericons.com/\n \"search\":\n url('data:image/svg+xml;charset=utf-8,'),\n // Factored out from mkdocs-material on 24-Aug-2020.\n // url: https://squidfunk.github.io/mkdocs-material/reference/admonitions/\n \"pencil\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"abstract\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"info\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"flame\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"question\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"warning\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"failure\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"spark\":\n url('data:image/svg+xml;charset=utf-8,')\n);\n\n@mixin icons {\n @each $name, $glyph in $icons {\n --icon-#{$name}: #{$glyph};\n }\n}\n","@use \"sass:list\";\n// Admonitions\n\n// Structure of these is:\n// admonition-class: color \"icon-name\";\n//\n// The colors are translated into CSS variables below. The icons are\n// used directly in the main declarations to set the `mask-image` in\n// the title.\n\n// prettier-ignore\n$admonitions: (\n // Each of these has an reST directives for it.\n \"caution\": #ff9100 \"spark\",\n \"warning\": #ff9100 \"warning\",\n \"danger\": #ff5252 \"spark\",\n \"attention\": #ff5252 \"warning\",\n \"error\": #ff5252 \"failure\",\n \"hint\": #00c852 \"question\",\n \"tip\": #00c852 \"info\",\n \"important\": #00bfa5 \"flame\",\n \"note\": #00b0ff \"pencil\",\n \"seealso\": #448aff \"info\",\n \"admonition-todo\": #808080 \"pencil\"\n);\n\n@mixin default-admonition($color, $icon-name) {\n --color-admonition-title: #{$color};\n --color-admonition-title-background: #{rgba($color, 0.2)};\n\n --icon-admonition-default: var(--icon-#{$icon-name});\n}\n\n@mixin default-topic($color, $icon-name) {\n --color-topic-title: #{$color};\n --color-topic-title-background: #{rgba($color, 0.2)};\n\n --icon-topic-default: var(--icon-#{$icon-name});\n}\n\n@mixin admonitions {\n @each $name, $values in $admonitions {\n --color-admonition-title--#{$name}: #{list.nth($values, 1)};\n --color-admonition-title-background--#{$name}: #{rgba(\n list.nth($values, 1),\n 0.2\n )};\n }\n}\n","// Colors used throughout this theme.\n//\n// The aim is to give the user more control. Thus, instead of hard-coding colors\n// in various parts of the stylesheet, the approach taken is to define all\n// colors as CSS variables and reusing them in all the places.\n//\n// `colors-dark` depends on `colors` being included at a lower specificity.\n\n@mixin colors {\n --color-problematic: #b30000;\n\n // Base Colors\n --color-foreground-primary: black; // for main text and headings\n --color-foreground-secondary: #5a5c63; // for secondary text\n --color-foreground-muted: #6b6f76; // for muted text\n --color-foreground-border: #878787; // for content borders\n\n --color-background-primary: white; // for content\n --color-background-secondary: #f8f9fb; // for navigation + ToC\n --color-background-hover: #efeff4ff; // for navigation-item hover\n --color-background-hover--transparent: #efeff400;\n --color-background-border: #eeebee; // for UI borders\n --color-background-item: #ccc; // for \"background\" items (eg: copybutton)\n\n // Announcements\n --color-announcement-background: #000000dd;\n --color-announcement-text: #eeebee;\n\n // Brand colors\n --color-brand-primary: #0a4bff;\n --color-brand-content: #2757dd;\n --color-brand-visited: #872ee0;\n\n // API documentation\n --color-api-background: var(--color-background-hover--transparent);\n --color-api-background-hover: var(--color-background-hover);\n --color-api-overall: var(--color-foreground-secondary);\n --color-api-name: var(--color-problematic);\n --color-api-pre-name: var(--color-problematic);\n --color-api-paren: var(--color-foreground-secondary);\n --color-api-keyword: var(--color-foreground-primary);\n\n --color-api-added: #21632c;\n --color-api-added-border: #38a84d;\n --color-api-changed: #046172;\n --color-api-changed-border: #06a1bc;\n --color-api-deprecated: #605706;\n --color-api-deprecated-border: #f0d90f;\n --color-api-removed: #b30000;\n --color-api-removed-border: #ff5c5c;\n\n --color-highlight-on-target: #ffffcc;\n\n // Inline code background\n --color-inline-code-background: var(--color-background-secondary);\n\n // Highlighted text (search)\n --color-highlighted-background: #ddeeff;\n --color-highlighted-text: var(--color-foreground-primary);\n\n // GUI Labels\n --color-guilabel-background: #ddeeff80;\n --color-guilabel-border: #bedaf580;\n --color-guilabel-text: var(--color-foreground-primary);\n\n // Admonitions!\n --color-admonition-background: transparent;\n\n //////////////////////////////////////////////////////////////////////////////\n // Everything below this should be one of:\n // - var(...)\n // - *-gradient(...)\n // - special literal values (eg: transparent, none)\n //////////////////////////////////////////////////////////////////////////////\n\n // Tables\n --color-table-header-background: var(--color-background-secondary);\n --color-table-border: var(--color-background-border);\n\n // Cards\n --color-card-border: var(--color-background-secondary);\n --color-card-background: transparent;\n --color-card-marginals-background: var(--color-background-secondary);\n\n // Header\n --color-header-background: var(--color-background-primary);\n --color-header-border: var(--color-background-border);\n --color-header-text: var(--color-foreground-primary);\n\n // Sidebar (left)\n --color-sidebar-background: var(--color-background-secondary);\n --color-sidebar-background-border: var(--color-background-border);\n\n --color-sidebar-brand-text: var(--color-foreground-primary);\n --color-sidebar-caption-text: var(--color-foreground-muted);\n --color-sidebar-link-text: var(--color-foreground-secondary);\n --color-sidebar-link-text--top-level: var(--color-brand-primary);\n\n --color-sidebar-item-background: var(--color-sidebar-background);\n --color-sidebar-item-background--current: var(\n --color-sidebar-item-background\n );\n --color-sidebar-item-background--hover: linear-gradient(\n 90deg,\n var(--color-background-hover--transparent) 0%,\n var(--color-background-hover) var(--sidebar-item-spacing-horizontal),\n var(--color-background-hover) 100%\n );\n\n --color-sidebar-item-expander-background: transparent;\n --color-sidebar-item-expander-background--hover: var(\n --color-background-hover\n );\n\n --color-sidebar-search-text: var(--color-foreground-primary);\n --color-sidebar-search-background: var(--color-background-secondary);\n --color-sidebar-search-background--focus: var(--color-background-primary);\n --color-sidebar-search-border: var(--color-background-border);\n --color-sidebar-search-icon: var(--color-foreground-muted);\n\n // Table of Contents (right)\n --color-toc-background: var(--color-background-primary);\n --color-toc-title-text: var(--color-foreground-muted);\n --color-toc-item-text: var(--color-foreground-secondary);\n --color-toc-item-text--hover: var(--color-foreground-primary);\n --color-toc-item-text--active: var(--color-brand-primary);\n\n // Actual page contents\n --color-content-foreground: var(--color-foreground-primary);\n --color-content-background: transparent;\n\n // Links\n --color-link: var(--color-brand-content);\n --color-link-underline: var(--color-background-border);\n --color-link--hover: var(--color-brand-content);\n --color-link-underline--hover: var(--color-foreground-border);\n\n --color-link--visited: var(--color-brand-visited);\n --color-link-underline--visited: var(--color-background-border);\n --color-link--visited--hover: var(--color-brand-visited);\n --color-link-underline--visited--hover: var(--color-foreground-border);\n}\n\n@mixin colors-dark {\n --color-problematic: #ee5151;\n\n // Base Colors\n --color-foreground-primary: #cfd0d0; // for main text and headings\n --color-foreground-secondary: #9ca0a5; // for secondary text\n --color-foreground-muted: #81868d; // for muted text\n --color-foreground-border: #666666; // for content borders\n\n --color-background-primary: #131416; // for content\n --color-background-secondary: #1a1c1e; // for navigation + ToC\n --color-background-hover: #1e2124ff; // for navigation-item hover\n --color-background-hover--transparent: #1e212400;\n --color-background-border: #303335; // for UI borders\n --color-background-item: #444; // for \"background\" items (eg: copybutton)\n\n // Announcements\n --color-announcement-background: #000000dd;\n --color-announcement-text: #eeebee;\n\n // Brand colors\n --color-brand-primary: #3d94ff;\n --color-brand-content: #5ca5ff;\n --color-brand-visited: #b27aeb;\n\n // Highlighted text (search)\n --color-highlighted-background: #083563;\n\n // GUI Labels\n --color-guilabel-background: #08356380;\n --color-guilabel-border: #13395f80;\n\n // API documentation\n --color-api-keyword: var(--color-foreground-secondary);\n --color-highlight-on-target: #333300;\n\n --color-api-added: #3db854;\n --color-api-added-border: #267334;\n --color-api-changed: #09b0ce;\n --color-api-changed-border: #056d80;\n --color-api-deprecated: #b1a10b;\n --color-api-deprecated-border: #6e6407;\n --color-api-removed: #ff7575;\n --color-api-removed-border: #b03b3b;\n\n // Admonitions\n --color-admonition-background: #18181a;\n\n // Cards\n --color-card-border: var(--color-background-secondary);\n --color-card-background: #18181a;\n --color-card-marginals-background: var(--color-background-hover);\n}\n","// This file contains the styling for making the content throughout the page,\n// including fonts, paragraphs, headings and spacing among these elements.\n\nbody\n font-family: var(--font-stack)\npre,\ncode,\nkbd,\nsamp\n font-family: var(--font-stack--monospace)\n\n// Make fonts look slightly nicer.\nbody\n -webkit-font-smoothing: antialiased\n -moz-osx-font-smoothing: grayscale\n\n// Line height from Bootstrap 4.1\narticle\n line-height: 1.5\n\n//\n// Headings\n//\nh1,\nh2,\nh3,\nh4,\nh5,\nh6\n line-height: 1.25\n font-family: var(--font-stack--headings)\n font-weight: bold\n\n border-radius: 0.5rem\n margin-top: 0.5rem\n margin-bottom: 0.5rem\n margin-left: -0.5rem\n margin-right: -0.5rem\n padding-left: 0.5rem\n padding-right: 0.5rem\n\n + p\n margin-top: 0\n\nh1\n font-size: 2.5em\n margin-top: 1.75rem\n margin-bottom: 1rem\nh2\n font-size: 2em\n margin-top: 1.75rem\nh3\n font-size: 1.5em\nh4\n font-size: 1.25em\nh5\n font-size: 1.125em\nh6\n font-size: 1em\n\nsmall\n opacity: 75%\n font-size: 80%\n\n// Paragraph\np\n margin-top: 0.5rem\n margin-bottom: 0.75rem\n\n// Horizontal rules\nhr.docutils\n height: 1px\n padding: 0\n margin: 2rem 0\n background-color: var(--color-background-border)\n border: 0\n\n.centered\n text-align: center\n\n// Links\na\n text-decoration: underline\n\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline)\n\n &:visited\n color: var(--color-link--visited)\n text-decoration-color: var(--color-link-underline--visited)\n &:hover\n color: var(--color-link--visited--hover)\n text-decoration-color: var(--color-link-underline--visited--hover)\n\n &:hover\n color: var(--color-link--hover)\n text-decoration-color: var(--color-link-underline--hover)\n &.muted-link\n color: inherit\n &:hover\n color: var(--color-link--hover)\n text-decoration-color: var(--color-link-underline--hover)\n &:visited\n color: var(--color-link--visited--hover)\n text-decoration-color: var(--color-link-underline--visited--hover)\n","// This file contains the styles for the overall layouting of the documentation\n// skeleton, including the responsive changes as well as sidebar toggles.\n//\n// This is implemented as a mobile-last design, which isn't ideal, but it is\n// reasonably good-enough and I got pretty tired by the time I'd finished this\n// to move the rules around to fix this. Shouldn't take more than 3-4 hours,\n// if you know what you're doing tho.\n\n// HACK: Not all browsers account for the scrollbar width in media queries.\n// This results in horizontal scrollbars in the breakpoint where we go\n// from displaying everything to hiding the ToC. We accomodate for this by\n// adding a bit of padding to the TOC drawer, disabling the horizontal\n// scrollbar and allowing the scrollbars to cover the padding.\n// https://www.456bereastreet.com/archive/201301/media_query_width_and_vertical_scrollbars/\n\n// HACK: Always having the scrollbar visible, prevents certain browsers from\n// causing the content to stutter horizontally between taller-than-viewport and\n// not-taller-than-viewport pages.\n@use \"variables\" as *\n\nhtml\n overflow-x: hidden\n overflow-y: scroll\n scroll-behavior: smooth\n\n.sidebar-scroll, .toc-scroll, article[role=main] *\n scrollbar-width: thin\n scrollbar-color: var(--color-foreground-border) transparent\n\n//\n// Overalls\n//\nhtml,\nbody\n height: 100%\n color: var(--color-foreground-primary)\n background: var(--color-background-primary)\n\n.skip-to-content\n position: fixed\n padding: 1rem\n border-radius: 1rem\n left: 0.25rem\n top: 0.25rem\n z-index: 40\n background: var(--color-background-primary)\n color: var(--color-foreground-primary)\n\n transform: translateY(-200%)\n transition: transform 300ms ease-in-out\n\n &:focus-within\n transform: translateY(0%)\n\narticle\n color: var(--color-content-foreground)\n background: var(--color-content-background)\n overflow-wrap: break-word\n\n.page\n display: flex\n // fill the viewport for pages with little content.\n min-height: 100%\n\n.mobile-header\n width: 100%\n height: var(--header-height)\n background-color: var(--color-header-background)\n color: var(--color-header-text)\n border-bottom: 1px solid var(--color-header-border)\n\n // Looks like sub-script/super-script have this, and we need this to\n // be \"on top\" of those.\n z-index: 10\n\n // We don't show the header on large screens.\n display: none\n\n // Add shadow when scrolled\n &.scrolled\n border-bottom: none\n box-shadow: 0 0 0.2rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2)\n\n .header-center\n a\n color: var(--color-header-text)\n text-decoration: none\n\n.main\n display: flex\n flex: 1\n\n// Sidebar (left) also covers the entire left portion of screen.\n.sidebar-drawer\n box-sizing: border-box\n\n border-right: 1px solid var(--color-sidebar-background-border)\n background: var(--color-sidebar-background)\n\n display: flex\n justify-content: flex-end\n // These next two lines took me two days to figure out.\n width: calc((100% - #{$full-width}) / 2 + #{$sidebar-width})\n min-width: $sidebar-width\n\n// Scroll-along sidebars\n.sidebar-container,\n.toc-drawer\n box-sizing: border-box\n width: $sidebar-width\n\n.toc-drawer\n background: var(--color-toc-background)\n // See HACK described on top of this document\n padding-right: 1rem\n\n.sidebar-sticky,\n.toc-sticky\n position: sticky\n top: 0\n height: min(100%, 100vh)\n height: 100vh\n\n display: flex\n flex-direction: column\n\n.sidebar-scroll,\n.toc-scroll\n flex-grow: 1\n flex-shrink: 1\n\n overflow: auto\n scroll-behavior: smooth\n\n// Central items.\n.content\n padding: 0 $content-padding\n width: $content-width\n\n display: flex\n flex-direction: column\n justify-content: space-between\n\n.icon\n display: inline-block\n height: 1rem\n width: 1rem\n svg\n width: 100%\n height: 100%\n\n//\n// Accommodate announcement banner\n//\n.announcement\n background-color: var(--color-announcement-background)\n color: var(--color-announcement-text)\n\n height: var(--header-height)\n display: flex\n align-items: center\n overflow-x: auto\n & + .page\n min-height: calc(100% - var(--header-height))\n\n.announcement-content\n box-sizing: border-box\n padding: 0.5rem\n min-width: 100%\n white-space: nowrap\n text-align: center\n\n a\n color: var(--color-announcement-text)\n text-decoration-color: var(--color-announcement-text)\n\n &:hover\n color: var(--color-announcement-text)\n text-decoration-color: var(--color-link--hover)\n\n////////////////////////////////////////////////////////////////////////////////\n// Toggles for theme\n////////////////////////////////////////////////////////////////////////////////\n.no-js .theme-toggle-container // don't show theme toggle if there's no JS\n display: none\n\n.theme-toggle-container\n display: flex\n\n.theme-toggle\n display: flex\n cursor: pointer\n border: none\n padding: 0\n background: transparent\n\n.theme-toggle svg\n height: 1.25rem\n width: 1.25rem\n color: var(--color-foreground-primary)\n display: none\n\n.theme-toggle-header\n display: flex\n align-items: center\n justify-content: center\n\n////////////////////////////////////////////////////////////////////////////////\n// Toggles for elements\n////////////////////////////////////////////////////////////////////////////////\n.toc-overlay-icon, .nav-overlay-icon\n display: none\n cursor: pointer\n\n .icon\n color: var(--color-foreground-secondary)\n height: 1.5rem\n width: 1.5rem\n\n.toc-header-icon, .nav-overlay-icon\n // for when we set display: flex\n justify-content: center\n align-items: center\n\n.toc-content-icon\n height: 1.5rem\n width: 1.5rem\n\n.content-icon-container\n float: right\n display: flex\n margin-top: 1.5rem\n margin-left: 1rem\n margin-bottom: 1rem\n gap: 0.5rem\n\n .edit-this-page, .view-this-page\n svg\n color: inherit\n height: 1.25rem\n width: 1.25rem\n\n.sidebar-toggle\n position: absolute\n display: none\n// \n.sidebar-toggle[name=\"__toc\"]\n left: 20px\n.sidebar-toggle:checked\n left: 40px\n// \n\n.overlay\n position: fixed\n top: 0\n width: 0\n height: 0\n\n transition: width 0ms, height 0ms, opacity 250ms ease-out\n\n opacity: 0\n background-color: rgba(0, 0, 0, 0.54)\n.sidebar-overlay\n z-index: 20\n.toc-overlay\n z-index: 40\n\n// Keep things on top and smooth.\n.sidebar-drawer\n z-index: 30\n transition: left 250ms ease-in-out\n.toc-drawer\n z-index: 50\n transition: right 250ms ease-in-out\n\n// Show the Sidebar\n#__navigation:checked\n & ~ .sidebar-overlay\n width: 100%\n height: 100%\n opacity: 1\n & ~ .page\n .sidebar-drawer\n top: 0\n left: 0\n // Show the toc sidebar\n#__toc:checked\n & ~ .toc-overlay\n width: 100%\n height: 100%\n opacity: 1\n & ~ .page\n .toc-drawer\n top: 0\n right: 0\n\n////////////////////////////////////////////////////////////////////////////////\n// Back to top\n////////////////////////////////////////////////////////////////////////////////\n.back-to-top\n text-decoration: none\n\n display: none\n position: fixed\n left: 0\n top: 1rem\n padding: 0.5rem\n padding-right: 0.75rem\n border-radius: 1rem\n font-size: 0.8125rem\n\n background: var(--color-background-primary)\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), #6b728080 0px 0px 1px 0px\n\n z-index: 10\n\n margin-left: 50%\n transform: translateX(-50%)\n svg\n height: 1rem\n width: 1rem\n fill: currentColor\n display: inline-block\n\n span\n margin-left: 0.25rem\n\n .show-back-to-top &\n display: flex\n align-items: center\n\n////////////////////////////////////////////////////////////////////////////////\n// Responsive layouting\n////////////////////////////////////////////////////////////////////////////////\n// Make things a bit bigger on bigger screens.\n@media (min-width: $full-width + $sidebar-width)\n html\n font-size: 110%\n\n@media (max-width: $full-width)\n // Collapse \"toc\" into the icon.\n .toc-content-icon\n display: flex\n .toc-drawer\n position: fixed\n height: 100vh\n top: 0\n right: -$sidebar-width\n border-left: 1px solid var(--color-background-muted)\n .toc-tree\n border-left: none\n font-size: var(--toc-font-size--mobile)\n\n // Accomodate for a changed content width.\n .sidebar-drawer\n width: calc((100% - #{$full-width - $sidebar-width}) / 2 + #{$sidebar-width})\n\n@media (max-width: $content-padded-width + $sidebar-width)\n // Center the page\n .content\n margin-left: auto\n margin-right: auto\n padding: 0 $content-padding--small\n\n@media (max-width: $content-padded-width--small + $sidebar-width)\n // Collapse \"navigation\".\n .nav-overlay-icon\n display: flex\n .sidebar-drawer\n position: fixed\n height: 100vh\n width: $sidebar-width\n\n top: 0\n left: -$sidebar-width\n\n // Swap which icon is visible.\n .toc-header-icon, .theme-toggle-header\n display: flex\n .toc-content-icon, .theme-toggle-content\n display: none\n\n // Show the header.\n .mobile-header\n position: sticky\n top: 0\n display: flex\n justify-content: space-between\n align-items: center\n\n .header-left,\n .header-right\n display: flex\n height: var(--header-height)\n padding: 0 var(--header-padding)\n label\n height: 100%\n width: 100%\n user-select: none\n\n .nav-overlay-icon .icon,\n .theme-toggle svg\n height: 1.5rem\n width: 1.5rem\n\n // Add a scroll margin for the content\n :target\n scroll-margin-top: calc(var(--header-height) + 2.5rem)\n\n // Show back-to-top below the header\n .back-to-top\n top: calc(var(--header-height) + 0.5rem)\n\n // Accommodate for the header.\n .page\n flex-direction: column\n justify-content: center\n\n@media (max-width: $content-width + 2* $content-padding--small)\n // Content should respect window limits.\n .content\n width: 100%\n overflow-x: auto\n\n@media (max-width: $content-width)\n article[role=main] aside.sidebar\n float: none\n width: 100%\n margin: 1rem 0\n","@use \"sass:list\"\n@use \"../variables\" as *\n\n// The design here is strongly inspired by mkdocs-material.\n.admonition, .topic\n margin: 1rem auto\n padding: 0 0.5rem 0.5rem 0.5rem\n\n background: var(--color-admonition-background)\n\n border-radius: 0.2rem\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n font-size: var(--admonition-font-size)\n\n overflow: hidden\n page-break-inside: avoid\n\n // First element should have no margin, since the title has it.\n > :nth-child(2)\n margin-top: 0\n\n // Last item should have no margin, since we'll control that w/ padding\n > :last-child\n margin-bottom: 0\n\n.admonition p.admonition-title,\np.topic-title\n position: relative\n margin: 0 -0.5rem 0.5rem\n padding-left: 2rem\n padding-right: .5rem\n padding-top: .4rem\n padding-bottom: .4rem\n\n font-weight: 500\n font-size: var(--admonition-title-font-size)\n line-height: 1.3\n\n // Our fancy icon\n &::before\n content: \"\"\n position: absolute\n left: 0.5rem\n width: 1rem\n height: 1rem\n\n// Default styles\np.admonition-title\n background-color: var(--color-admonition-title-background)\n &::before\n background-color: var(--color-admonition-title)\n mask-image: var(--icon-admonition-default)\n mask-repeat: no-repeat\n\np.topic-title\n background-color: var(--color-topic-title-background)\n &::before\n background-color: var(--color-topic-title)\n mask-image: var(--icon-topic-default)\n mask-repeat: no-repeat\n\n//\n// Variants\n//\n.admonition\n border-left: 0.2rem solid var(--color-admonition-title)\n\n @each $type, $value in $admonitions\n &.#{$type}\n border-left-color: var(--color-admonition-title--#{$type})\n > .admonition-title\n background-color: var(--color-admonition-title-background--#{$type})\n &::before\n background-color: var(--color-admonition-title--#{$type})\n mask-image: var(--icon-#{list.nth($value, 2)})\n\n.admonition-todo > .admonition-title\n text-transform: uppercase\n","// This file stylizes the API documentation (stuff generated by autodoc). It's\n// deeply nested due to how autodoc structures the HTML without enough classes\n// to select the relevant items.\n\n// API docs!\ndl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)\n // Tweak the spacing of all the things!\n dd\n margin-left: 2rem\n > :first-child\n margin-top: 0.125rem\n > :last-child\n margin-bottom: 0.75rem\n\n // This is used for the arguments\n .field-list\n margin-bottom: 0.75rem\n\n // \"Headings\" (like \"Parameters\" and \"Return\")\n > dt\n text-transform: uppercase\n font-size: var(--font-size--small)\n\n dd:empty\n margin-bottom: 0.5rem\n dd > ul\n margin-left: -1.2rem\n > li\n > p:nth-child(2)\n margin-top: 0\n // When the last-empty-paragraph follows a paragraph, it doesn't need\n // to augument the existing spacing.\n > p + p:last-child:empty\n margin-top: 0\n margin-bottom: 0\n\n // Colorize the elements\n > dt\n color: var(--color-api-overall)\n\n.sig:not(.sig-inline)\n font-weight: bold\n\n font-size: var(--api-font-size)\n font-family: var(--font-stack--monospace)\n\n margin-left: -0.25rem\n margin-right: -0.25rem\n padding-top: 0.25rem\n padding-bottom: 0.25rem\n padding-right: 0.5rem\n\n // These are intentionally em, to properly match the font size.\n padding-left: 3em\n text-indent: -2.5em\n\n border-radius: 0.25rem\n\n background: var(--color-api-background)\n transition: background 100ms ease-out\n\n &:hover\n background: var(--color-api-background-hover)\n\n // adjust the size of the [source] link on the right.\n a.reference\n .viewcode-link\n font-weight: normal\n width: 4.25rem\n\nem.property, span.property\n font-style: normal\n &:first-child\n color: var(--color-api-keyword)\n.sig-name\n color: var(--color-api-name)\n.sig-prename\n font-weight: normal\n color: var(--color-api-pre-name)\n.sig-paren\n color: var(--color-api-paren)\n.sig-param\n font-style: normal\n\ndiv.versionadded,\ndiv.versionchanged,\ndiv.deprecated,\ndiv.versionremoved\n border-left: 0.1875rem solid\n border-radius: 0.125rem\n\n padding-left: 0.75rem\n\n p\n margin-top: 0.125rem\n margin-bottom: 0.125rem\n\ndiv.versionadded\n border-color: var(--color-api-added-border)\n .versionmodified\n color: var(--color-api-added)\n\ndiv.versionchanged\n border-color: var(--color-api-changed-border)\n .versionmodified\n color: var(--color-api-changed)\n\ndiv.deprecated\n border-color: var(--color-api-deprecated-border)\n .versionmodified\n color: var(--color-api-deprecated)\n\ndiv.versionremoved\n border-color: var(--color-api-removed-border)\n .versionmodified\n color: var(--color-api-removed)\n\n// Align the [docs] and [source] to the right.\n.viewcode-link, .viewcode-back\n float: right\n text-align: right\n",".line-block\n margin-top: 0.5rem\n margin-bottom: 0.75rem\n .line-block\n margin-top: 0rem\n margin-bottom: 0rem\n padding-left: 1rem\n","// Captions\narticle p.caption,\ntable > caption,\n.code-block-caption\n font-size: var(--font-size--small)\n text-align: center\n\n// Caption above a TOCTree\n.toctree-wrapper.compound\n .caption, :not(.caption) > .caption-text\n font-size: var(--font-size--small)\n text-transform: uppercase\n\n text-align: initial\n margin-bottom: 0\n\n > ul\n margin-top: 0\n margin-bottom: 0\n","// Inline code\ncode.literal, .sig-inline\n background: var(--color-inline-code-background)\n border-radius: 0.2em\n // Make the font smaller, and use padding to recover.\n font-size: var(--font-size--small--2)\n padding: 0.1em 0.2em\n\n pre.literal-block &\n font-size: inherit\n padding: 0\n\n p &\n border: 1px solid var(--color-background-border)\n\n.sig-inline\n font-family: var(--font-stack--monospace)\n\n// Code and Literal Blocks\n$code-spacing-vertical: 0.625rem\n$code-spacing-horizontal: 0.875rem\n\n// Wraps every literal block + line numbers.\ndiv[class*=\" highlight-\"],\ndiv[class^=\"highlight-\"]\n margin: 1em 0\n display: flex\n\n .table-wrapper\n margin: 0\n padding: 0\n\npre\n margin: 0\n padding: 0\n overflow: auto\n\n // Needed to have more specificity than pygments' \"pre\" selector. :(\n article[role=\"main\"] .highlight &\n line-height: 1.5\n\n &.literal-block,\n .highlight &\n font-size: var(--code-font-size)\n padding: $code-spacing-vertical $code-spacing-horizontal\n\n // Make it look like all the other blocks.\n &.literal-block\n margin-top: 1rem\n margin-bottom: 1rem\n\n border-radius: 0.2rem\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n\n// All code is always contained in this.\n.highlight\n width: 100%\n border-radius: 0.2rem\n\n // Make line numbers and prompts un-selectable.\n .gp, span.linenos\n user-select: none\n pointer-events: none\n\n // Expand the line-highlighting.\n .hll\n display: block\n margin-left: -$code-spacing-horizontal\n margin-right: -$code-spacing-horizontal\n padding-left: $code-spacing-horizontal\n padding-right: $code-spacing-horizontal\n\n/* Make code block captions be nicely integrated */\n.code-block-caption\n display: flex\n padding: $code-spacing-vertical $code-spacing-horizontal\n\n border-radius: 0.25rem\n border-bottom-left-radius: 0\n border-bottom-right-radius: 0\n font-weight: 300\n border-bottom: 1px solid\n\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n border-color: var(--color-background-border)\n\n + div[class]\n margin-top: 0\n > .highlight\n border-top-left-radius: 0\n border-top-right-radius: 0\n\n// When `html_codeblock_linenos_style` is table.\n.highlighttable\n width: 100%\n display: block\n tbody\n display: block\n\n tr\n display: flex\n\n // Line numbers\n td.linenos\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n padding: $code-spacing-vertical $code-spacing-horizontal\n padding-right: 0\n border-top-left-radius: 0.2rem\n border-bottom-left-radius: 0.2rem\n\n .linenodiv\n padding-right: $code-spacing-horizontal\n font-size: var(--code-font-size)\n box-shadow: -0.0625rem 0 var(--color-foreground-border) inset\n\n // Actual code\n td.code\n padding: 0\n display: block\n flex: 1\n overflow: hidden\n\n .highlight\n border-top-left-radius: 0\n border-bottom-left-radius: 0\n\n// When `html_codeblock_linenos_style` is inline.\n.highlight\n span.linenos\n display: inline-block\n padding-left: 0\n padding-right: $code-spacing-horizontal\n margin-right: $code-spacing-horizontal\n box-shadow: -0.0625rem 0 var(--color-foreground-border) inset\n","// Inline Footnote Reference\n.footnote-reference\n font-size: var(--font-size--small--4)\n vertical-align: super\n\n// Definition list, listing the content of each note.\n// docutils <= 0.17\ndl.footnote.brackets\n font-size: var(--font-size--small)\n color: var(--color-foreground-secondary)\n\n display: grid\n grid-template-columns: max-content auto\n dt\n margin: 0\n > .fn-backref\n margin-left: 0.25rem\n\n &:after\n content: \":\"\n\n .brackets\n &:before\n content: \"[\"\n &:after\n content: \"]\"\n\n dd\n margin: 0\n padding: 0 1rem\n\n// docutils >= 0.18\naside.footnote\n font-size: var(--font-size--small)\n color: var(--color-foreground-secondary)\n\naside.footnote > span,\ndiv.citation > span\n float: left\n font-weight: 500\n padding-right: 0.25rem\n\naside.footnote > *:not(span),\ndiv.citation > p\n margin-left: 2rem\n","//\n// Figures\n//\nimg\n box-sizing: border-box\n max-width: 100%\n height: auto\n\narticle\n figure, .figure\n border-radius: 0.2rem\n\n margin: 0\n :last-child\n margin-bottom: 0\n\n .align-left\n float: left\n clear: left\n margin: 0 1rem 1rem\n\n .align-right\n float: right\n clear: right\n margin: 0 1rem 1rem\n\n .align-default,\n .align-center\n display: block\n text-align: center\n margin-left: auto\n margin-right: auto\n\n // WELL, table needs to be stylised like a table.\n table.align-default\n display: table\n text-align: initial\n",".genindex-jumpbox, .domainindex-jumpbox\n border-top: 1px solid var(--color-background-border)\n border-bottom: 1px solid var(--color-background-border)\n padding: 0.25rem\n\n.genindex-section, .domainindex-section\n h2\n margin-top: 0.75rem\n margin-bottom: 0.5rem\n ul\n margin-top: 0\n margin-bottom: 0\n","ul,\nol\n padding-left: 1.2rem\n\n // Space lists out like paragraphs\n margin-top: 1rem\n margin-bottom: 1rem\n // reduce margins within li.\n li\n > p:first-child\n margin-top: 0.25rem\n margin-bottom: 0.25rem\n\n > p:last-child\n margin-top: 0.25rem\n\n > ul,\n > ol\n margin-top: 0.5rem\n margin-bottom: 0.5rem\n\nol\n &.arabic\n list-style: decimal\n &.loweralpha\n list-style: lower-alpha\n &.upperalpha\n list-style: upper-alpha\n &.lowerroman\n list-style: lower-roman\n &.upperroman\n list-style: upper-roman\n\n// Don't space lists out when they're \"simple\" or in a `.. toctree::`\n.simple,\n.toctree-wrapper\n li\n > ul,\n > ol\n margin-top: 0\n margin-bottom: 0\n\n// Definition Lists\n.field-list,\n.option-list,\ndl:not([class]),\ndl.simple,\ndl.footnote,\ndl.glossary\n dt\n font-weight: 500\n margin-top: 0.25rem\n + dt\n margin-top: 0\n\n .classifier::before\n content: \":\"\n margin-left: 0.2rem\n margin-right: 0.2rem\n\n dd\n > p:first-child,\n ul\n margin-top: 0.125rem\n\n ul\n margin-bottom: 0.125rem\n",".math-wrapper\n width: 100%\n overflow-x: auto\n\ndiv.math\n position: relative\n text-align: center\n\n .headerlink,\n &:focus .headerlink\n display: none\n\n &:hover .headerlink\n display: inline-block\n\n span.eqno\n position: absolute\n right: 0.5rem\n top: 50%\n transform: translate(0, -50%)\n z-index: 1\n","// Abbreviations\nabbr[title]\n cursor: help\n\n// \"Problematic\" content, as identified by Sphinx\n.problematic\n color: var(--color-problematic)\n\n// Keyboard / Mouse \"instructions\"\nkbd:not(.compound)\n margin: 0 0.2rem\n padding: 0 0.2rem\n border-radius: 0.2rem\n border: 1px solid var(--color-foreground-border)\n color: var(--color-foreground-primary)\n vertical-align: text-bottom\n\n font-size: var(--font-size--small--3)\n display: inline-block\n\n box-shadow: 0 0.0625rem 0 rgba(0, 0, 0, 0.2), inset 0 0 0 0.125rem var(--color-background-primary)\n\n background-color: var(--color-background-secondary)\n\n// Blockquote\nblockquote\n border-left: 4px solid var(--color-background-border)\n background: var(--color-background-secondary)\n\n margin-left: 0\n margin-right: 0\n padding: 0.5rem 1rem\n\n .attribution\n font-weight: 600\n text-align: right\n\n &.pull-quote,\n &.highlights\n font-size: 1.25em\n\n &.epigraph,\n &.pull-quote\n border-left-width: 0\n border-radius: 0.5rem\n\n &.highlights\n border-left-width: 0\n background: transparent\n\n// Center align embedded-in-text images\np .reference img\n vertical-align: middle\n","p.rubric\n line-height: 1.25\n font-weight: bold\n font-size: 1.125em\n\n // For Numpy-style documentation that's got rubrics within it.\n // https://github.com/pradyunsg/furo/discussions/505\n dd &\n line-height: inherit\n font-weight: inherit\n\n font-size: var(--font-size--small)\n text-transform: uppercase\n","article .sidebar\n float: right\n clear: right\n width: 30%\n\n margin-left: 1rem\n margin-right: 0\n\n border-radius: 0.2rem\n background-color: var(--color-background-secondary)\n border: var(--color-background-border) 1px solid\n\n > *\n padding-left: 1rem\n padding-right: 1rem\n\n > ul, > ol // lists need additional padding, because bullets.\n padding-left: 2.2rem\n\n .sidebar-title\n margin: 0\n padding: 0.5rem 1rem\n border-bottom: var(--color-background-border) 1px solid\n\n font-weight: 500\n\n// TODO: subtitle\n// TODO: dedicated variables?\n","[role=main] .table-wrapper.container\n width: 100%\n overflow-x: auto\n margin-top: 1rem\n margin-bottom: 0.5rem\n padding: 0.2rem 0.2rem 0.75rem\n\ntable.docutils\n border-radius: 0.2rem\n border-spacing: 0\n border-collapse: collapse\n\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n th\n background: var(--color-table-header-background)\n\n td,\n th\n // Space things out properly\n padding: 0 0.25rem\n\n // Get the borders looking just-right.\n border-left: 1px solid var(--color-table-border)\n border-right: 1px solid var(--color-table-border)\n border-bottom: 1px solid var(--color-table-border)\n\n p\n margin: 0.25rem\n\n &:first-child\n border-left: none\n &:last-child\n border-right: none\n\n // MyST-parser tables set these classes for control of column alignment\n &.text-left\n text-align: left\n &.text-right\n text-align: right\n &.text-center\n text-align: center\n","@use \"../variables\" as *\n\n:target\n scroll-margin-top: 2.5rem\n\n@media (max-width: $full-width - $sidebar-width)\n :target\n scroll-margin-top: calc(2.5rem + var(--header-height))\n\n // When a heading is selected\n section > span:target\n scroll-margin-top: calc(2.8rem + var(--header-height))\n\n// Permalinks\n.headerlink\n font-weight: 100\n user-select: none\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\ndl dt,\np.caption,\nfigcaption p,\ntable > caption,\n.code-block-caption\n > .headerlink\n margin-left: 0.5rem\n visibility: hidden\n &:hover > .headerlink\n visibility: visible\n\n // Don't change to link-like, if someone adds the contents directive.\n > .toc-backref\n color: inherit\n text-decoration-line: none\n\n// Figure and table captions are special.\nfigure:hover > figcaption > p > .headerlink,\ntable:hover > caption > .headerlink\n visibility: visible\n\n:target >, // Regular section[id] style anchors\nspan:target ~ // Non-regular span[id] style \"extra\" anchors\n h1,\n h2,\n h3,\n h4,\n h5,\n h6\n &:nth-of-type(1)\n background-color: var(--color-highlight-on-target)\n // .headerlink\n // visibility: visible\n code.literal\n background-color: transparent\n\ntable:target > caption,\nfigure:target\n background-color: var(--color-highlight-on-target)\n\n// Inline page contents\n.this-will-duplicate-information-and-it-is-still-useful-here li :target\n background-color: var(--color-highlight-on-target)\n\n// Code block permalinks\n.literal-block-wrapper:target .code-block-caption\n background-color: var(--color-highlight-on-target)\n\n// When a definition list item is selected\n//\n// There isn't really an alternative to !important here, due to the\n// high-specificity of API documentation's selector.\ndt:target\n background-color: var(--color-highlight-on-target) !important\n\n// When a footnote reference is selected\n.footnote > dt:target + dd,\n.footnote-reference:target\n background-color: var(--color-highlight-on-target)\n",".guilabel\n background-color: var(--color-guilabel-background)\n border: 1px solid var(--color-guilabel-border)\n color: var(--color-guilabel-text)\n\n padding: 0 0.3em\n border-radius: 0.5em\n font-size: 0.9em\n","// This file contains the styles used for stylizing the footer that's shown\n// below the content.\n@use \"../variables\" as *\n\nfooter\n font-size: var(--font-size--small)\n display: flex\n flex-direction: column\n\n margin-top: 2rem\n\n// Bottom of page information\n.bottom-of-page\n display: flex\n align-items: center\n justify-content: space-between\n\n margin-top: 1rem\n padding-top: 1rem\n padding-bottom: 1rem\n\n color: var(--color-foreground-secondary)\n border-top: 1px solid var(--color-background-border)\n\n line-height: 1.5\n\n @media (max-width: $content-width)\n text-align: center\n flex-direction: column-reverse\n gap: 0.25rem\n\n .left-details\n font-size: var(--font-size--small)\n\n .right-details\n display: flex\n flex-direction: column\n gap: 0.25rem\n text-align: right\n\n .icons\n display: flex\n justify-content: flex-end\n gap: 0.25rem\n font-size: 1rem\n\n a\n text-decoration: none\n\n svg,\n img\n font-size: 1.125rem\n height: 1em\n width: 1em\n\n// Next/Prev page information\n.related-pages\n a\n display: flex\n align-items: center\n\n text-decoration: none\n &:hover .page-info .title\n text-decoration: underline\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline)\n\n svg.furo-related-icon,\n svg.furo-related-icon > use\n flex-shrink: 0\n\n color: var(--color-foreground-border)\n\n width: 0.75rem\n height: 0.75rem\n margin: 0 0.5rem\n\n &.next-page\n max-width: 50%\n\n float: right\n clear: right\n text-align: right\n\n &.prev-page\n max-width: 50%\n\n float: left\n clear: left\n\n svg\n transform: rotate(180deg)\n\n.page-info\n display: flex\n flex-direction: column\n overflow-wrap: anywhere\n\n .next-page &\n align-items: flex-end\n\n .context\n display: flex\n align-items: center\n\n padding-bottom: 0.1rem\n\n color: var(--color-foreground-muted)\n font-size: var(--font-size--small)\n text-decoration: none\n","// This file contains the styles for the contents of the left sidebar, which\n// contains the navigation tree, logo, search etc.\n\n////////////////////////////////////////////////////////////////////////////////\n// Brand on top of the scrollable tree.\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-brand\n display: flex\n flex-direction: column\n flex-shrink: 0\n\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n text-decoration: none\n\n.sidebar-brand-text\n color: var(--color-sidebar-brand-text)\n overflow-wrap: break-word\n margin: var(--sidebar-item-spacing-vertical) 0\n font-size: 1.5rem\n\n.sidebar-logo-container\n margin: var(--sidebar-item-spacing-vertical) 0\n\n.sidebar-logo\n margin: 0 auto\n display: block\n max-width: 100%\n\n////////////////////////////////////////////////////////////////////////////////\n// Search\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-search-container\n display: flex\n align-items: center\n margin-top: var(--sidebar-search-space-above)\n\n position: relative\n\n background: var(--color-sidebar-search-background)\n &:hover,\n &:focus-within\n background: var(--color-sidebar-search-background--focus)\n\n &::before\n content: \"\"\n position: absolute\n left: var(--sidebar-item-spacing-horizontal)\n width: var(--sidebar-search-icon-size)\n height: var(--sidebar-search-icon-size)\n\n background-color: var(--color-sidebar-search-icon)\n mask-image: var(--icon-search)\n\n.sidebar-search\n box-sizing: border-box\n\n border: none\n border-top: 1px solid var(--color-sidebar-search-border)\n border-bottom: 1px solid var(--color-sidebar-search-border)\n\n padding-top: var(--sidebar-search-input-spacing-vertical)\n padding-bottom: var(--sidebar-search-input-spacing-vertical)\n padding-right: var(--sidebar-search-input-spacing-horizontal)\n padding-left: calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size))\n\n width: 100%\n\n color: var(--color-sidebar-search-foreground)\n background: transparent\n z-index: 10\n\n &:focus\n outline: none\n\n &::placeholder\n font-size: var(--sidebar-search-input-font-size)\n\n//\n// Hide Search Matches link\n//\n#searchbox .highlight-link\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0\n margin: 0\n text-align: center\n\n a\n color: var(--color-sidebar-search-icon)\n font-size: var(--font-size--small--2)\n\n////////////////////////////////////////////////////////////////////////////////\n// Structure/Skeleton of the navigation tree (left)\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-tree\n font-size: var(--sidebar-item-font-size)\n margin-top: var(--sidebar-tree-space-above)\n margin-bottom: var(--sidebar-item-spacing-vertical)\n\n ul\n padding: 0\n margin-top: 0\n margin-bottom: 0\n\n display: flex\n flex-direction: column\n\n list-style: none\n\n li\n position: relative\n margin: 0\n\n > ul\n margin-left: var(--sidebar-item-spacing-horizontal)\n\n .icon\n color: var(--color-sidebar-link-text)\n\n .reference\n box-sizing: border-box\n color: var(--color-sidebar-link-text)\n\n // Fill the parent.\n display: inline-block\n line-height: var(--sidebar-item-line-height)\n text-decoration: none\n\n // Don't allow long words to cause wrapping.\n overflow-wrap: anywhere\n\n height: 100%\n width: 100%\n\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n\n &:hover\n color: var(--color-sidebar-link-text)\n background: var(--color-sidebar-item-background--hover)\n\n // Add a nice little \"external-link\" arrow here.\n &.external::after\n content: url('data:image/svg+xml,')\n margin: 0 0.25rem\n vertical-align: middle\n color: var(--color-sidebar-link-text)\n\n // Make the current page reference bold.\n .current-page > .reference\n font-weight: bold\n\n label\n position: absolute\n top: 0\n right: 0\n height: var(--sidebar-item-height)\n width: var(--sidebar-expander-width)\n\n cursor: pointer\n user-select: none\n\n display: flex\n justify-content: center\n align-items: center\n\n .caption, :not(.caption) > .caption-text\n font-size: var(--sidebar-caption-font-size)\n color: var(--color-sidebar-caption-text)\n\n font-weight: bold\n text-transform: uppercase\n\n margin: var(--sidebar-caption-space-above) 0 0 0\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n\n // If it has children, add a bit more padding to wrap the content to avoid\n // overlapping with the