Initial wishlist code

This commit is contained in:
Chris Jean-Marie 2024-10-28 00:08:18 +00:00
parent efc724ef65
commit f3bc62e4c0
11 changed files with 283 additions and 9 deletions

View File

@ -5,3 +5,7 @@ FROM mcr.microsoft.com/devcontainers/rust:1-1-bookworm
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install clang lld \ && apt-get -y install clang lld \
&& apt-get autoremove -y && apt-get clean -y && apt-get autoremove -y && apt-get clean -y
RUN curl -fsSL https://ollama.com/install.sh | sh
RUN cargo instal sqlx-cli

View File

@ -0,0 +1,4 @@
-- Add down migration script here
drop table if exists `wishlist_items`;
delete from `role_permissions` where id = 9;

View File

@ -0,0 +1,16 @@
-- Add up migration script here
CREATE TABLE
`wishlist_items` (
`id` integer not null primary key autoincrement,
`created_at` INTEGER not null default CURRENT_TIMESTAMP,
`created_by` ineger null,
`updated_at` INTEGER null default CURRENT_TIMESTAMP,
`updated_by` integer null,
`user_id` INTEGER null,
`item` varchar(255) null,
`item_url` varchar(255) null,
`purchased_by` INTEGER null,
unique (`id`)
);
insert into `role_permissions` (`created_at`, `created_by`, `id`, `item`, `role_id`, `updated_at`, `updated_by`) values ('0', '0', '9', '/wishlist', '2', '0', '0')

View File

@ -244,7 +244,7 @@ pub async fn google_auth_return(
.await?; .await?;
} }
} }
Ok((headers, Redirect::to("/"))) Ok((headers, Redirect::to("/dashboard")))
} }
pub async fn logout( pub async fn logout(

View File

@ -15,7 +15,7 @@ mod user;
use error_handling::AppError; use error_handling::AppError;
use middlewares::inject_user_data; use middlewares::inject_user_data;
use google_oauth::{login, logout, google_auth_return}; use google_oauth::{login, logout, google_auth_return};
use routes::{dashboard, index, about, cottagecalendar, contact, profile, user_profile, useradmin}; use routes::{about, contact, cottagecalendar, dashboard, index, profile, user_profile, user_wishlist, user_wishlist_add, user_wishlist_add_item, useradmin, wishlists};
use user::{add_user_role, delete_user_role, UserData}; use user::{add_user_role, delete_user_role, UserData};
#[derive(Clone)] #[derive(Clone)]
@ -53,6 +53,9 @@ async fn main() {
.route("/users/:user_id", get(user_profile)) .route("/users/:user_id", get(user_profile))
.route("/roles/:user_id/:role_id/add", get(add_user_role)) .route("/roles/:user_id/:role_id/add", get(add_user_role))
.route("/roles/:user_role_id/delete", get(delete_user_role)) .route("/roles/:user_role_id/delete", get(delete_user_role))
.route("/wishlists", get(wishlists))
.route("/userwishlist/:user_id", get(user_wishlist))
.route("/userwishlist/add/:user_id", get(user_wishlist_add).post(user_wishlist_add_item))
.nest_service("/assets", ServeDir::new("templates/assets") .nest_service("/assets", ServeDir::new("templates/assets")
.fallback(get_service(ServeDir::new("templates/assets")))) .fallback(get_service(ServeDir::new("templates/assets"))))
.route("/", get(index)) .route("/", get(index))

View File

@ -2,14 +2,16 @@ use askama_axum::{Response, Template};
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{Html, IntoResponse, Redirect}, response::{Html, IntoResponse, Redirect},
Extension, Extension, Form,
}; };
use chrono::Utc;
use http::StatusCode; use http::StatusCode;
use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
middlewares::is_authorized, middlewares::is_authorized,
user::{get_other_roles_display, get_user_roles_display}, user::{get_other_roles_display, get_user_roles_display, get_user_wishlist_items},
UserData, UserData,
}; };
@ -249,3 +251,155 @@ pub async fn cottagecalendar(
Redirect::to("/").into_response() Redirect::to("/").into_response()
} }
} }
#[derive(Template)]
#[template(path = "userwishlists.html")]
struct WishListsTemplate {
logged_in: bool,
name: String,
users: Vec<UserData>,
}
pub async fn wishlists(
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<SqlitePool>,
) -> impl IntoResponse {
let user_name = user_data.as_ref().map(|s| s.name.clone());
let logged_in = user_name.is_some();
let name = user_name.unwrap_or_default();
let users = sqlx::query_as::<_, UserData>("SELECT * FROM users")
.fetch_all(&db_pool)
.await
.unwrap();
if is_authorized("/userwishlists", user_data, db_pool).await {
let template = WishListsTemplate {
logged_in,
name,
users,
};
HtmlTemplate(template).into_response()
} else {
Redirect::to("/").into_response()
}
}
#[derive(Template)]
#[template(path = "userwishlist.html")]
struct UserWishListTemplate {
logged_in: bool,
name: String,
user: UserData,
user_wishlist_items: Vec<crate::user::UserWishlistItem>,
}
pub async fn user_wishlist(
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Extract the user's name from the user data.
let user_name = user_data.as_ref().map(|s| s.name.clone());
let logged_in = user_data.is_some();
let name = user_name.unwrap_or_default();
// Extract the user data.
let user = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id)
.fetch_one(&db_pool)
.await
.unwrap();
if is_authorized("/wishlist", user_data, db_pool.clone()).await {
// Get user roles
let user_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await;
// Create the wishlist template.
let template = UserWishListTemplate {
logged_in,
name,
user: user,
user_wishlist_items,
};
return HtmlTemplate(template).into_response();
} else {
Redirect::to("/").into_response()
}
}
#[derive(Template)]
#[template(path = "userwishlistadd.html")]
struct UserWishListAddTemplate {
logged_in: bool,
name: String,
user: UserData,
user_wishlist_items: Vec<crate::user::UserWishlistItem>,
}
pub async fn user_wishlist_add(
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Extract the user's name from the user data.
let user_name = user_data.as_ref().map(|s| s.name.clone());
let logged_in = user_data.is_some();
let name = user_name.unwrap_or_default();
// Extract the user data.
let user = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id)
.fetch_one(&db_pool)
.await
.unwrap();
if is_authorized("/wishlist", user_data, db_pool.clone()).await {
// Get user roles
let user_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await;
// Create the wishlist template.
let template = UserWishListAddTemplate {
logged_in,
name,
user: user,
user_wishlist_items,
};
return HtmlTemplate(template).into_response();
} else {
Redirect::to("/").into_response()
}
}
#[derive(Deserialize, Debug)]
pub struct ItemForm {
item: String,
item_url: String,
}
pub async fn user_wishlist_add_item(
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
Form(item_form): Form<ItemForm>
) -> impl IntoResponse {
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
// Insert new item to database
let now = Utc::now().timestamp();
sqlx::query("insert into wishlist_items (created_at, created_by, updated_at, updated_by, user_id, item, item_url) values (?, ?, ?, ?, ?, ?, ?)")
.bind(now)// Created now
.bind(user_data.as_ref().unwrap().id)// Created by current user
.bind(now) // Updated now
.bind(user_data.as_ref().unwrap().id) // Updated by current user
.bind(user_id)
.bind(item_form.item)
.bind(item_form.item_url)
.execute(&db_pool)
.await
.unwrap();
let redirect_string = format!("/userwishlist/{user_id}");
Redirect::to(&redirect_string).into_response()
} else {
Redirect::to("/").into_response()
}
}

View File

@ -5,6 +5,7 @@ use axum::{
Extension, Extension,
}; };
use chrono::Utc; use chrono::Utc;
use reqwest::redirect;
///User related structs and functions ///User related structs and functions
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{prelude::FromRow, SqlitePool}; use sqlx::{prelude::FromRow, SqlitePool};
@ -58,6 +59,20 @@ pub struct UserRolesDisplay {
pub role_id: i64, pub role_id: i64,
pub role_name: String, pub role_name: String,
} }
#[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)]
pub struct UserWishlistItem {
pub id: i64,
pub created_at: i64,
pub created_by: i64,
pub updated_at: i64,
pub updated_by: i64,
pub user_id: i64,
pub item: String,
pub item_url: String,
pub purchased_by: i64,
}
/* /*
pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec<UserRoles> { pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec<UserRoles> {
// Get user roles // Get user roles
@ -107,11 +122,9 @@ pub async fn add_user_role(
if is_authorized("/roles", user_data.clone(), db_pool.clone()).await { if is_authorized("/roles", user_data.clone(), db_pool.clone()).await {
let now = Utc::now().timestamp(); let now = Utc::now().timestamp();
println!("Adding role {} to user {}", role_id, user_id);
sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)") sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)")
.bind(now)// Created now .bind(now)// Created now
.bind(user_data.as_ref().unwrap().id)// Created by current user .bind(user_data.as_ref().unwrap().id)// Created by current user
.bind(now) // Updated now .bind(now) // Updated now
.bind(user_data.as_ref().unwrap().id) // Updated by current user .bind(user_data.as_ref().unwrap().id) // Updated by current user
.bind(user_id) .bind(user_id)
@ -119,8 +132,12 @@ pub async fn add_user_role(
.execute(&db_pool) .execute(&db_pool)
.await .await
.unwrap(); .unwrap();
let redirect_url = format!("/users/{user_id}");
Redirect::to(&redirect_url).into_response()
} else {
Redirect::to("/").into_response()
} }
Redirect::to("/").into_response()
} }
pub async fn delete_user_role( pub async fn delete_user_role(
@ -137,3 +154,16 @@ pub async fn delete_user_role(
} }
Redirect::to("/").into_response() Redirect::to("/").into_response()
} }
pub async fn get_user_wishlist_items(user_id: i64, db_pool: &SqlitePool) -> Vec<UserWishlistItem> {
// Get wish list items for the user
let user_wishlist_items = sqlx::query_as(
r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by from wishlist_items where user_id = ?"#
)
.bind(user_id)
.fetch_all(db_pool)
.await
.unwrap();
user_wishlist_items
}

View File

@ -9,6 +9,7 @@
<li><a href="/dashboard">Web links</a></li> <li><a href="/dashboard">Web links</a></li>
<li><a href="/useradmin">User Administration</a></li> <li><a href="/useradmin">User Administration</a></li>
<li><a href="/cottagecalendar">Cottage Calendar</a></li> <li><a href="/cottagecalendar">Cottage Calendar</a></li>
<li><a href="/wishlists">Wish lists</a></li>
</ul> </ul>
</div> </div>
<div class="col-8"> <div class="col-8">

View File

@ -0,0 +1,22 @@
{% extends "authorized.html" %}
{% block title %}User Profile{% endblock %}
{% block center %}
<h1>{{ user.given_name }} Wishlist</h1>
<br/>
<h2>List</h2>
<a href="/userwishlist/add/{{ user.id }}">Add</a>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">Item</th>
</tr>
</thead>
<tbody>
{% for user_wishlist_item in user_wishlist_items %}
<tr>
<td><a href="{{ user_wishlist_item.item_url }}">{{ user_wishlist_item.item }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock center %}

View File

@ -0,0 +1,17 @@
{% extends "authorized.html" %}
{% block title %}Add to {{ user.given_name }} Wishlist{% endblock %}
{% block center %}
<h1>Add Item to Wishlist</h1>
<form method="post">
<div class="my-3">
<label for="item">Item</label>
<input type="text" class="form-control my-1" id="item" name="item" required>
</div>
<div class="my-3">
<label for="item_url">URL</label>
<input type="text" class="form-control my-1" id="item_url" name="item_url">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br/>
{% endblock center %}

View File

@ -0,0 +1,23 @@
{% extends "authorized.html" %}
{% block title %}Wish Lists{% endblock %}
{% block center %}
<h1>Users</h1>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">email</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td><a href="/userwishlist/{{ user.id }}">{{ user.id }}</a></td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock center %}