Template Rendering in Rust using Askama [Part-3]

on 2023-06-22

Introduction


This is a continuation of Part-2 where we added data to our music store and listed them. This is the last part of the tutorial where we will include functionality to edit and delete data in our music store.

Data Storage


Let's first add helper functions which will access the DB to update and delete data. Add the below code to db.rs.

pub fn get_data_by_id(id: u64) -> Result<Option<Music>, MusicError> {
    let db = music_db_read()?;
    let music = db.get(&id).map(|music| music.to_owned());
    Ok(music)
}

pub fn update_data_in_db(id: u64, mut music: Music) -> Result<(), MusicError> {
    let mut db = music_db_write()?;
    music.id = id;
    db.insert(id, music);
    Ok(())
}

pub fn delete_data_from_db(id: u64) -> Result<(), MusicError> {
    let mut db = music_db_write()?;
    db.remove(&id);
    Ok(())
}

The get_data_by_id function gets data from DB for a given id. The get function of HashMap returns a reference to the value of the given key. Since we cannot return a value referencing the local variable in rust(check E0515 for more details), we return an owned value.

The update_data_in_db and delete_data_from_db functions will update and delete data from DB respectively.

Create Templates


Adding HTML files

We will extend index.html file to include add_music block. Add the below edit_music.html file to the templates directory.

{% extends "index.html" %}

{% block add_music %}
<form action="{{ "/edit_music/{}"|format(music.id) }}" method="post">
  <div class="mb-3">
    <label for="songName" class="form-label">Song Name</label>
    <input type="text" class="form-control" id="songName" name="song" placeholder="Enter song name" value="{{ music.song }}">
  </div>
  <div class="mb-3">
    <label for="artistName" class="form-label">Artist Name</label>
    <input type="text" class="form-control" id="artistName" name="artist" placeholder="Enter artist name" value="{{ music.artist }}">
  </div>
  <div class="mb-3">
    <label for="genre" class="form-label">Genre</label>
    <input type="text" class="form-control" id="genre" name="genre" placeholder="Enter genre" value="{{ music.genre }}">
  </div>
  <button type="submit" class="btn btn-primary">Edit Song</button>
</form>
{% endblock %}

The above form displays data of the selected music record using value attribute. Users can edit the data of the selected music. On submission update_music function will be triggered.

Http handlers

Now let's add functions to edit and delete our music records in http_handlers.rs.

#[derive(Template)]
#[template(path = "edit_music.html")]
pub struct EditMusicPage {
    music: Music,
}

pub async fn edit_music(id: web::Path<u64>) -> Result<EditMusicPage, MusicError> {
    let music = get_data_by_id(id.into_inner())?
        .ok_or_else(|| MusicError::from("Music for id {id} doesn't exist in DB".to_string()))?;
    Ok(EditMusicPage { music })
}

pub async fn update_music(id: web::Path<u64>, data: web::Form<Music>) -> Result<web::Redirect, MusicError> {
    update_data_in_db(id.into_inner(), data.into_inner())?;
    Ok(web::Redirect::to("/list_music").see_other())
}

pub async fn delete_music(id: web::Path<u64>) -> Result<web::Redirect, MusicError> {
    delete_data_from_db(id.into_inner())?;
    Ok(web::Redirect::to("/list_music").see_other())
}

The edit_music function uses web::Path to extract the id from the request URL. The function gets music record from our DB for a given id and renders edit_music.html. If no data is present in DB for a given id, we return an Error.

If data is present in DB for the given id, an HTML form will be displayed with pre-filled values of the selected music record. The user can edit the required fields. On submission, update_music function is called that deserializes the form data to Music and updates data in the DB. After adding data to the DB we will redirect user to the list music page.

The delete_music function will delete data from DB and redirect the user to the list music page.

Let's configure routes in main.rs

fn routes(service_config: &mut ServiceConfig) {
    service_config
        .route("/", web::get().to(welcome))
        .route("/list_music", web::get().to(list_music))
        .route("/add_music", web::get().to(add_music))
        .route("/add_music_to_db", web::post().to(add_music_to_db))
        .route("/edit_music/{id}", web::get().to(edit_music))
        .route("/edit_music/{id}", web::post().to(update_music))
        .route("/delete_music/{id}", web::get().to(delete_music));
}

Build and run the project to start the server.

cargo run

After adding some music at http://127.0.0.1:8000/add_music, you will be able to edit records by clicking on the edit button. The edit page will look like this

add_music web page

After successfully updating the data, you will be redirected to http://127.0.0.1:8000/list_music

list_music web page

You can also delete records by clicking on the delete button of the record.

View the full code of this tutorial on GitHub.

Conclusion


In this tutorial, we explored the usage of Askama library by building a simple web application using actix-web. You can go through the official documentation of Askama here and check out all the available features.

Happy coding! 😀