DEV Community

Nivethan
Nivethan

Posted on • Updated on

A Web App in Rust - 11 User Profiles

*** This should be part 11, not sure why it got re-ordered to the first. Ah, it got re-ordered because I had accidentally published it a few days ago, and it's sorting based off that date. Funny little bug, I wonder how you would solve it, maybe publish date should be editable or I should have deleted and re-made it at the time.

Welcome back! At this point we have a functional website with all the traditional functions of news aggregator like hacker news. (Except for voting and replying to comments which arguably are very important but lets ignore that). Now we'll build a user profile page because who doesn't want to be able to see your submissions and comments!?

User Profile Page

In a previous chapter we already set up a url to access our profile page on the index page.

./templates/index.html

...
        <a href="{{ p.link }}">{{ p.title }}</a>
        <br>
        <small>
            submitted by 
            <a href="/users/{{u.username}}">
                {{ u.username }}
            </a>
        </small>
...
Enter fullscreen mode Exit fullscreen mode

This is very similar to what we set up when trying to go to our post page and so the steps to start are very much the same. We'll need to register a new route and we will be passing in data via the url.

Before we set up our route however, we'll first need to define our relationships. Both comments and posts belong to a user. Like how we defined the relationship between comments and posts, we'll need to do the same thing for comments and users, and posts and users.

./src/models.rs

...
#[derive(Debug, Serialize, Queryable, Identifiable)]
pub struct User {
    pub id: i32,
    pub username: String,
    pub email: String,
    pub password: String,
}
...
Enter fullscreen mode Exit fullscreen mode

We first add the Identifiable trait to our parent, User.

./src/models.rs

..
#[derive(Debug, Serialize, Queryable, Identifiable, Associations)]
#[belongs_to(User, foreign_key="author")]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub link: Option<String>,
    pub author: i32,
    pub created_at: chrono::NaiveDateTime,
}
...
Enter fullscreen mode Exit fullscreen mode

We then add the Associations trait to Post and we add the belongs_to macro. Here we define our foreign key, by default diesel assumes the keys are tablename_id so for the User table it would assume user_id. However we defined it as author on our Post table.

./src/models.rs

...
#[derive(Debug, Serialize, Queryable, Identifiable, Associations)]
#[belongs_to(Post)]
#[belongs_to(User)]
pub struct Comment {
    pub id: i32,
    pub comment: String,
    pub post_id: i32,
    pub user_id: i32,
    pub parent_comment_id: Option<i32>,
    pub created_at: chrono::NaiveDateTime,
}
...
Enter fullscreen mode Exit fullscreen mode

In our Comment table, we just need to add the belongs_to macro, here we don't need specify our foreign key because the assumption made by diesel holds.

Now we have our relationships defined! Let's set up our route.

./src/main.rs

...
.service(
                web::resource("/post/{post_id}")
                    .route(web::get().to(post_page))
                    .route(web::post().to(comment))
            )
            .service(
                web::resource("/user/{username}")
                    .route(web::get().to(user_profile))
            )
...
Enter fullscreen mode Exit fullscreen mode

Here we add another service for /user and we route this to our user_profile function.

./src/main.rs

...
async fn user_profile(tera: web::Data<Tera>, 
    web::Path(requested_user): web::Path<String>
) -> impl Responder {
    use schema::users::dsl::{username, users};

    let connection = establish_connection();
    let user :User = users.filter(username.eq(requested_user))
        .get_result(&connection)
        .expect("Failed to find user.");

    let posts :Vec<Post> = Post::belonging_to(&user)
        .load(&connection)
        .expect("Failed to find posts.");

    let comments :Vec<Comment> = Comment::belonging_to(&user)
        .load(&connection)
        .expect("Failed to find comments.");

    let mut data = Context::new();
    data.insert("title", &format!("{} - Profile", user.username));  
    data.insert("user", &user);
    data.insert("posts", &posts);
    data.insert("comments", &comments);

    let rendered = tera.render("profile.html", &data).unwrap();
    HttpResponse::Ok().body(rendered)
}
...
Enter fullscreen mode Exit fullscreen mode
  • The title was original missing in the context object, added thanks to the commenter below.

This will look similar to what we've done but lets go through the key portion.

We first get the requested user and if we don't find them we cause a panic.

We then do two calls to belonging_to against the Post and Comment using the user we found. Once we have our results we pass them to our tera context object and render them.

./templates/profile.html

{% extends "base.html" %}

{% block content %}
<h3>{{user.username}} Profile</h3>

<h5>Posts</h5>
<hr>
{% for post in posts %}
<div>
    <a href="/post/{{post.id}}">{{post.title}}</a>
</div>
{% endfor %}


<h5>Comments</h5>
<hr>
{% for comment in comments %}
<div>
    <a href="/post/{{comment.post_id}}">{{comment.comment}}</a>
    <br>
</div>
{% endfor %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Now we should have everything getting display on our profile page.

If we navigate to 127.0.0.1:8000/users/username, we should be able to see all of our posts and comments!

And with that we are done!!
We have finished up quite a bit now. Our website can handle user registration, logging in, making new posts, making comments and finally user profiles.

With that you should now be able to see the structure of how a web app works and how the different pieces all come together. We have routes, functions that handle routes, models that correspond to our database and finally our database. We have templates and sessions acting as middleware. All of these things come together to make our web application!

In the next few chapters we're going to do some clean up, but for now pat yourself on the back for making it this far.

Latest comments (2)

Collapse
 
lwshang profile image
Linwei Shang

In the function user_profile, there is a missing data.insert("title", "User Profile") among the following code block:

let mut data = Context::new();
data.insert("user", &user);
data.insert("posts", &posts);
data.insert("comments", &comments);
Enter fullscreen mode Exit fullscreen mode

title is required in base.html.

Collapse
 
krowemoh profile image
Nivethan

Fixed, thank you!