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:
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
The Button will be used to add tasks to the list. So name it
add_btn, change its
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
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
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.
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.
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).
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
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.
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:
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.
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:
text property to
checked property to
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.
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
@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.
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()
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.
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.)
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.
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
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.
__init__ for Form1, call
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.
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.
Add a new row to the ‘tasks’ table called ‘owner’. When selecting the data type, use ‘Link to table’ and select Users->Single Row.
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() )
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!
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:
It’s also live on the internet already (find out more).