Live Chat

We'll need to share your messages (and your email address if you're logged in) with our live chat provider, Drift. Here's their privacy policy.

If you don't want to do this, you can email us instead at contact@anvil.works.

TODO List App

Are you new here?

Anvil is a tool for building full-stack web apps with nothing but Python and a drag-and-drop designer. Learn more on our website, or sign up and try it yourself -- it's free!

We’re going to build a To-Do list app with Anvil, and publish it on the Web, using nothing but Python.

To follow along, you need to be able to access the Anvil Editor. Create a free account using the following link:

Create an account


Step 1: Building the UI

Open the Anvil Editor to get started.

In the top-left there is a ‘Create New App’ button. Click it and select the Material Design theme.

You are now in the Anvil Editor.

First, let’s give the app a sensible name - it will currently be called something like ‘Material Design 1’. Click on the name at the top of the screen and type in a name like ‘TODO List’.

Now let’s start building a UI. The toolbox on the right contains components that you can drag-and-drop onto the Design view in the centre. Drop a Label into the blue bar at the top, where it says ‘Drop title here’. The Properties tool on the right allows you to modify this new component - fill in the text section to put text into the label.

Now for the main TODO list UI. Add a Card to the page. This is a container that can hold other components. Inside the card, put a Label, a TextBox, and a Button.

The Label should say ‘New Task’ and have its Role set to ‘Subheading’.

The TextBox will be where users enter the TODO tasks. Let’s change its name to new_task_box.

The Button will be used to add tasks to the list. So name it add_btn, change its text to add and align it to the right.

We’ve just designed a data entry UI for adding tasks to the TODO list.

The next step is to make it do something. For now, we’ll pop up an alert when the user clicks the ‘add’ button. At the bottom of the Properties tool for the Button is a list of events that we can bind methods to. Click the arrow next to the click event:

You will now see the code that defines the behaviour of this part of your app. The method that’s highlighted will run when the Button is clicked. We want it to pop up a dialog box, so we’ll call the built-in alert function:

def add_btn_click(self, **event_args):
  """This method is called when the button is clicked"""
  alert(self.new_task_box.text, title="new task")

When you click the button, you’ll get a dialog box displaying the text you entered into the text box.

Step 2: Create

We’ve built an app with some UI that echoes back what you enter into it, in an alert box.

The next step of building a CRUD app is to create rows in a database in response to user input.

2A: Set up a Data Table

Click on ‘Services’ in the panel on the left.

Click on ‘Data Tables’.

Add a table called ‘tasks’.

Add columns called ‘title’ (type: text) and ‘done’ (type: True/False).

2B: Make the add button populate the Data Table

Now you need to hook the button up so that it adds a row to the table.

Click on ‘Server Modules’ in the panel on the left.

Define a new function called new_task. It should call app_tables.tasks.add_row() with the title it is given:

@anvil.server.callable
def new_task(title):
  app_tables.tasks.add_row(title=title, done=False)

​ This function runs in a Python runtime on a server. The @anvil.server.callable decorator means it can be called from the client.

In the client code (Form1), make the add_btn_click method call the new_task function and clear the new_task_box:

def add_btn_click(self, **event_args):
  """This method is called when the button is clicked"""
  anvil.server.call('new_task', self.new_task_box.text)
  self.new_task_box.text = ""

​ Now when the button is clicked, rows go into the database.

Step 3: Read

You should now have a data-entry app that can record new tasks. Next, we want to display our tasks on the screen. We’ll do this in two steps:

3A: Get tasks from the database

We’ll use a server function to get all the rows from the tasks table, and then print them in the client. In your server module, write:

@anvil.server.callable
def get_tasks():
  return app_tables.tasks.search()

Now, call that function from the client, in your Form’s __init__ method. It returns an iterable object, so we can loop through the rows printing the title of each task:

tasks = anvil.server.call('get_tasks')
for row in tasks:
  print(row['title'])

If you run this app, it will print all the tasks in your database, in the Output window.

3B: Display them on the screen

Add a new card above the “new task” card. Use a Label to set its title to “Tasks” (set its role to “Subheading” to make it look consistent.)

Add a RepeatingPanel to this card. This takes a template and repeats it for every item its items property. Double-click on the RepeatingPanel to edit its template. (If Anvil asks, say that you’ll be displaying rows from the Tasks table.)

Add a CheckBox to this template. Go to the Properties section and add two data bindings: Bind the text property to self.item[‘title’] Bind the checked property to self.item[‘done’]

Now all you need to do is set the items property to the database search you got from the server. Replace the for loop in your init function with this:

  tasks = anvil.server.call('get_tasks')
  self.repeating_panel_1.items = tasks

If you run your app, you will now see all the tasks from your database.

Step 4: Update

Now we’re displaying our tasks. However, if you try to check one of the CheckBoxes, you’ll see a “Permission Denied” error - something like this:

This is because when we used a Data Binding to set the checked property of the CheckBox to self.item[‘done’], we left write-back enabled. This means that, whenever the user checks or unchecks the CheckBox, Anvil runs:

self.item['done'] = self.check_box_1.checked

This means updating the database (great!). But when we returned those tasks from the server module, we returned read-only database rows - so we get a “permission denied” error when we tried to update one.

To fix this, we can return client-writable rows from the server. Go back to the Server Module, and change the get_tasks() function to search a client_writable() version of the tasks table:

@anvil.server.callable
def get_tasks():
  return app_tables.tasks.client_writable().search()

Now, you can check and uncheck those CheckBoxes, and it will update the done column in the Data Table.

Refresh the list when you add an item

It’s all working great, but when you add a new task, it doesn’t show up! That’s because we only fetch the list of tasks once, when we start up. Let’s put that refresh code into its own method, and call it when we add a new task, as well as on startup.

Here’s a full code listing with that modification applied:

class Form1(Form1Template):

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

    # Any code you write here will run when the form opens.
    self.refresh()

  def refresh(self):
    tasks = anvil.server.call('get_tasks')
    self.repeating_panel_1.items = tasks

  def add_btn_click(self, **event_args):
    """This method is called when the button is clicked"""
    anvil.server.call('add_task', self.new_task_box.text)
    self.refresh()

Step 5: Delete

Create, read and update are all working. All that remains for this to be a full-fledged CRUD app is the ability to delete items.

Add a Button to the ItemTemplate from the ToolBox and style it as you think a delete button should look.

Create a click handler for it in the same way as for the ‘add’ button in Step 1. It will be a method on ItemTemplate1.

The self.item of ItemTemplate1 is a Python object representing a row from the database. Calling its delete method deletes it from the database.

You also need to remove the task from the UI. So call self.remove_from_parent() in the click handler as well. This removes the present instance of ItemTemplate1 from the RepeatingPanel it belongs to.

The final click handler is:

def delete_btn_click(self, **event_args):
  """This method is called when the button is clicked"""
  self.item.delete()
  self.remove_from_parent()

Congratulations - you’ve now written a full CRUD application!

This pattern can be adapted to any application that requires storage of relational data. In fact, you can literally copy this app and modify it to suit your use-case (see the end of this tutorial to find out how.)

Step 6: Users

We have a fully-working CRUD application. It’s already published online at a private, unguessable URL. You can also publish it on a public URL using the Publish App dialog:

Now that your app is visible to the public, you may wish to restrict access to a limited group of users.

Step 6A: Creating users

Click on the ‘Services’ panel on the left and click on ‘Users’ to add the Users service.

Disable the check box marked ‘Allow visitors to sign up’. We’ll enable this in Step 7, but for now we’ll add users manually.

You’ll see a screen with a table at the bottom headed ‘Users’, with columns ‘email’ and ‘enabled’. This table is also present in the Data Tables window.

Add a couple of users manually by simply filling in the table. Remember to select the checkbox in the enabled column!

Set a password for your users by clicking the arrow next to their row and clicking ‘set password’. This will add a column for the password hash, and populate it automatically based on a password you enter.

Step 6B: Restricting access

In the __init__ for Form1, call anvil.users.login_with_form():

class Form1(Form1Template):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    # Any code you write here will run when the form opens.
    anvil.users.login_with_form()

Now a login form is displayed when anybody accesses the app.

Of course, someone might bypass this login box by tinkering with the page source in their browser. To be properly secure, we need to enforce our access control on the server.

So in the Server Module, add an if statement to each of your functions that checks if a user is logged in before reading/creating tasks:

@anvil.server.callable
def new_task(title):
  if anvil.users.get_user() is not None:
    app_tables.tasks.add_row()

@anvil.server.callable
def get_tasks():
  if anvil.users.get_user() is not None:
    return app_tables.tasks.client_writable().search()

​Now a user has to log in before they can see the tasks and add/edit them.

Step 7: Multi user app

Now you have a single TODO list that can be accessed by a restricted set of users.

What if you want to give each user their own TODO list?

Let’s restrict each user’s view so that their own list is private to them.

Step 7A: Associating tasks with users

Add a new row to the ‘tasks’ table called ‘owner’. When selecting the data type, use ‘Link to table’ and select Users->Single Row.

Modify the new_task server function to fill out the owner to be the current user:

@anvil.server.callable
def new_task(title):
  if anvil.users.get_user() is not None:
    app_tables.tasks.add_row(
      title=title,
      done=False,
      owner=anvil.users.get_user()
    )

Step 7B: Displaying only logged-in user’s tasks

To ensure the logged-in user sees only their tasks, restrict the client-writable view to only rows where the owner is the current user:

@anvil.server.callable
def get_tasks():
  if anvil.users.get_user() is not None:
    return app_tables.tasks.client_writable(owner=anvil.users.get_user()).search()

​Now try adding a new user and logging in as them. You should see an empty TODO list. Add tasks as normal - they show up as normal. Checking the database, you can see that they’ve been added to the ‘tasks’ table and they’re linked to the new user.

Now that users can’t see each others’ tasks, it’s safe to enable the Sign Up functionality. That’s the check box in the Users screen marked ‘Allow visitors to sign up’. If you access your app now, you’ll find that the login screen includes a signup process with email verification.

Congratulations, you’ve just built a multi-user CRUD app in Anvil!

Clone the finished app

Every app in Anvil has a URL that allows it to be imported by another Anvil user.

Click the following link to clone the finished app from this workshop:

Clone Finished App


It’s also live on the internet already (find out more).