CRUD for project

This commit is contained in:
2024-11-10 10:54:20 +01:00
parent 1ccded15cc
commit 41ad5ab612
21 changed files with 568 additions and 163 deletions

View File

@@ -1,4 +1,3 @@
use core::task;
use std::path::Path;
use async_trait::async_trait;
@@ -49,6 +48,7 @@ impl Hooks for App {
fn routes(_ctx: &AppContext) -> AppRoutes {
AppRoutes::with_default_routes() // controller routes below
.add_route(controllers::project::routes())
.add_route(controllers::data::routes())
.add_route(controllers::auth::routes())
.add_route(controllers::website::routes())
@@ -76,6 +76,7 @@ impl Hooks for App {
tasks.register(tasks::add_data_file::AddDataFile);
tasks.register(tasks::delete_data::DeleteData);
tasks.register(tasks::clear_data::ClearData);
tasks.register(tasks::delete_project::DeleteProject);
}
async fn truncate(db: &DatabaseConnection) -> Result<()> {
@@ -87,4 +88,4 @@ impl Hooks for App {
db::seed::<users::ActiveModel>(db, &base.join("users.yaml").display().to_string()).await?;
Ok(())
}
}
}

View File

@@ -1,4 +1,5 @@
pub mod auth;
pub mod website;
pub mod data;
pub mod data;
pub mod project;

View File

@@ -0,0 +1,46 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use crate::{
models::{
projects::{get_category_from_string, CreateProject, CreateProjectForm},
users,
},
services,
shared::get_technologies_from_string::get_technologies_from_string,
};
async fn create_project(
auth: auth::JWT,
State(ctx): State<AppContext>,
Form(project_data): Form<CreateProjectForm>,
) -> Result<Response> {
match users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await {
Ok(_) => {}
Err(_) => return unauthorized("Unauthorized"),
}
let technologies = get_technologies_from_string(&project_data.technologies);
let project_data = CreateProject {
name: project_data.name,
description: project_data.description,
technologies,
category: get_category_from_string(&project_data.category),
download_url: project_data.download_url,
github_url: project_data.github_url,
visit_url: project_data.visit_url,
short_description: project_data.short_description,
};
let project = services::projects::add_project(&ctx, project_data).await?;
format::json(&project)
}
pub fn routes() -> Routes {
Routes::new()
.prefix("api/projects/")
.add("/", post(create_project))
}

View File

@@ -35,7 +35,7 @@ pub async fn render_projects(
ViewEngine(v): ViewEngine<TeraView>,
State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
views::website::projects(v, &ctx).await
views::projects::projects(v, &ctx).await
}
pub async fn render_project_detail(
@@ -43,7 +43,7 @@ pub async fn render_project_detail(
State(ctx): State<AppContext>,
Path(id): Path<i32>,
) -> Result<impl IntoResponse> {
views::website::project_detail(v, &ctx, id).await
views::projects::project_detail(v, &ctx, id).await
}
pub async fn render_project_detail_from_name(
@@ -51,7 +51,13 @@ pub async fn render_project_detail_from_name(
State(ctx): State<AppContext>,
Path(name): Path<String>,
) -> Result<impl IntoResponse> {
views::website::project_detail_from_name(v, &ctx, name).await
views::projects::project_detail_from_name(v, &ctx, name).await
}
pub async fn render_create_project(
ViewEngine(v): ViewEngine<TeraView>,
) -> Result<impl IntoResponse> {
views::projects::create_project(v).await
}
pub async fn render_data(
@@ -73,6 +79,7 @@ pub fn routes() -> Routes {
.add("/upload", get(render_upload))
.add("/login", get(render_login))
.add("/projects", get(render_projects))
.add("/projects/create", get(render_create_project))
.add("/projects/:id", get(render_project_detail))
.add("/projects/project/:name", get(render_project_detail_from_name))
.add("/data", get(render_data))

View File

@@ -42,6 +42,30 @@ pub struct CreateProject {
pub technologies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateProjectForm {
pub name: String,
pub short_description: String,
pub description: Option<String>,
pub category: String,
pub github_url: Option<String>,
pub download_url: Option<String>,
pub visit_url: Option<String>,
pub technologies: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateProject {
pub name: Option<String>,
pub short_description: Option<String>,
pub description: Option<String>,
pub category: Option<Category>,
pub github_url: Option<String>,
pub download_url: Option<String>,
pub visit_url: Option<String>,
pub technologies: Option<Vec<String>>,
}
pub fn get_category_from_string(category: &str) -> Category {
match category {
"Web" => Category::Web,

View File

@@ -224,5 +224,24 @@ pub async fn delete_multiple_data_by_file_names(
for file_name in file_names {
delete_data_by_file_name(&file_name, &ctx).await?;
}
Ok(())
}
pub async fn delete_data_by_id(id: i32, ctx: &AppContext) -> ModelResult<()> {
let data = Entity::find().filter(data::Column::Id.eq(id)).one(&ctx.db).await?;
match data {
Some(data) => {
let path = PathBuf::from(&data.file_url);
match ctx.storage.as_ref().delete(&path).await {
Ok(_) => {}
Err(_) => return Err(ModelError::Any("Failed to delete file from storage".into())),
}
data.delete(&ctx.db).await?;
}
None => return Err(ModelError::EntityNotFound),
}
Ok(())
}

View File

@@ -6,23 +6,24 @@ use crate::{
project_thumbnails,
projects::{self, ActiveModel, Entity, Model},
},
projects::{get_category_from_string, get_string_from_category, Category, CreateProject, ProjectDto},
},
shared::get_technologies_from_string::get_technologies_from_string,
projects::{get_category_from_string, get_string_from_category, CreateProject, ProjectDto, UpdateProject},
}, services::data::add_data_file_from_path, shared::get_technologies_from_string::get_technologies_from_string
};
pub async fn get_all_projects(ctx: &AppContext) -> Result<Vec<Model>> {
use super::data::delete_data_by_id;
pub async fn get_all_projects(ctx: &AppContext) -> ModelResult<Vec<Model>> {
let projects = Entity::find().all(&ctx.db).await?;
Ok(projects)
}
pub async fn get_project_by_id(ctx: &AppContext, id: i32) -> Result<Model> {
pub async fn get_project_by_id(ctx: &AppContext, id: i32) -> ModelResult<Model> {
let project = Entity::find_by_id(id).one(&ctx.db).await?;
let project = project.ok_or_else(|| ModelError::EntityNotFound)?;
Ok(project)
}
pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> Result<Model> {
pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> ModelResult<Model> {
let project = Entity::find()
.filter(projects::Column::Name.contains(name))
.one(&ctx.db)
@@ -31,7 +32,7 @@ pub async fn get_project_by_name(ctx: &AppContext, name: &str) -> Result<Model>
Ok(project)
}
pub async fn get_archived_projects(ctx: &AppContext) -> Result<Vec<Model>> {
pub async fn get_archived_projects(ctx: &AppContext) -> ModelResult<Vec<Model>> {
let archived_projects = Entity::find()
.filter(
model::query::condition()
@@ -43,7 +44,7 @@ pub async fn get_archived_projects(ctx: &AppContext) -> Result<Vec<Model>> {
Ok(archived_projects)
}
pub async fn get_highlighted_projects(ctx: &AppContext) -> Result<Vec<Model>> {
pub async fn get_highlighted_projects(ctx: &AppContext) -> ModelResult<Vec<Model>> {
let highlighted_projects = Entity::find()
.filter(
model::query::condition()
@@ -161,3 +162,124 @@ pub async fn add_project(
Ok(item)
}
pub async fn add_project_with_thumbnails(
ctx: &AppContext,
thumbnails_paths: Vec<String>,
data: CreateProject,
) -> Result<()> {
let txn = ctx.db.begin().await?;
let project = add_project(ctx, data).await?;
let project_id = project.id;
for thumbnail_path in thumbnails_paths {
let thumbnail_data = add_data_file_from_path(ctx, &thumbnail_path, "thumbnail.png", false, true).await?;
let thumbnail = project_thumbnails::ActiveModel {
project_id: Set(project_id),
data_id: Set(thumbnail_data.id),
..Default::default()
};
thumbnail.insert(&txn).await?;
}
txn.commit().await?;
Ok(())
}
pub async fn update_project(
ctx: &AppContext,
id: i32,
data: UpdateProject,
) -> ModelResult<Model> {
let item = get_project_by_id(ctx, id).await?;
let mut item = item.into_active_model();
if let Some(name) = data.name {
item.name = Set(name);
}
if let Some(short_description) = data.short_description {
item.short_description = Set(short_description);
}
item.description = Set(data.description);
if let Some(category) = data.category {
item.category = Set(get_string_from_category(&category));
}
item.github_url = Set(data.github_url);
item.download_url = Set(data.download_url);
item.visit_url = Set(data.visit_url);
if let Some(technologies) = data.technologies {
item.technology = Set(technologies.join(","));
}
let item = item.update(&ctx.db).await?;
Ok(item)
}
pub async fn update_thumbnails_for_project(
ctx: &AppContext,
id: i32,
thumbnails_paths: Vec<String>,
) -> Result<()> {
let txn = ctx.db.begin().await?;
let project = get_project_by_id(ctx, id).await?;
let project_id = project.id;
let thumbnails = project
.find_related(project_thumbnails::Entity)
.all(&ctx.db)
.await?;
for thumbnail in thumbnails {
let _ = delete_data_by_id(thumbnail.data_id, ctx);
let _ = thumbnail.delete(&txn).await?;
}
for thumbnail_path in thumbnails_paths {
let thumbnail_data = add_data_file_from_path(ctx, &thumbnail_path, "thumbnail.png", false, true).await?;
let thumbnail = project_thumbnails::ActiveModel {
project_id: Set(project_id),
data_id: Set(thumbnail_data.id),
..Default::default()
};
thumbnail.insert(&txn).await?;
}
txn.commit().await?;
Ok(())
}
pub async fn delete_project(ctx: &AppContext, id: i32) -> Result<()> {
let item = get_project_by_id(ctx, id).await?;
let thumbnails = item.find_related(project_thumbnails::Entity).all(&ctx.db).await?;
let thumbnails_data_ids = thumbnails.into_iter().map(|thumbnail| thumbnail.data_id).collect::<Vec<i32>>();
for data_id in thumbnails_data_ids {
let _ = delete_data_by_id(data_id, ctx);
}
let _ = item.delete(&ctx.db).await?;
Ok(())
}
pub async fn delete_thumbnails_for_project(ctx: &AppContext, id: i32) -> Result<()> {
let project = get_project_by_id(ctx, id).await?;
let thumbnails = project
.find_related(project_thumbnails::Entity)
.all(&ctx.db)
.await?;
for thumbnail in thumbnails {
let _ = delete_data_by_id(thumbnail.data_id, ctx);
let _ = thumbnail.delete(&ctx.db).await?;
}
Ok(())
}

View File

@@ -0,0 +1,30 @@
use loco_rs::prelude::*;
use crate::services::projects;
pub struct DeleteProject;
#[async_trait]
impl Task for DeleteProject {
fn task(&self) -> TaskInfo {
TaskInfo {
name: "delete_project".to_string(),
detail: "Task for deleting a project".to_string(),
}
}
async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> {
let project_id = vars.cli_arg("id")?;
let project_id = project_id.parse::<i32>();
let project_id = match project_id {
Ok(project_id) => project_id,
Err(_) => return Err(Error::Any("Invalid project ID".into())),
};
projects::delete_project(app_context, project_id).await?;
tracing::info!("Project {} deleted successfully", project_id);
Ok(())
}
}

View File

@@ -4,4 +4,5 @@ pub mod create_skill;
pub mod create_user;
pub mod seed;
pub mod clear_data;
pub mod delete_data;
pub mod delete_data;
pub mod delete_project;

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod data;
pub mod website;
pub mod projects;

24
src/views/projects.rs Normal file
View File

@@ -0,0 +1,24 @@
use loco_rs::prelude::*;
use crate::services;
pub async fn projects(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoResponse> {
let projects = services::projects::get_all_projects_dto(ctx).await?;
format::render().view(&v, "website/projects.html", data!({"projects": projects}))
}
pub async fn project_detail(v: impl ViewRenderer, ctx: &AppContext, id: i32) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto(ctx, id).await?;
format::render().view(&v, "website/project-detail.html", data!({"project": project}))
}
pub async fn project_detail_from_name(v: impl ViewRenderer, ctx: &AppContext, name: String) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto_by_name(ctx, &name).await?;
format::render().view(&v, "website/project-detail.html", data!({"project": project}))
}
pub async fn create_project(v: impl ViewRenderer) -> Result<impl IntoResponse> {
format::render().view(&v, "website/create-project.html", data!({}))
}

View File

@@ -13,24 +13,6 @@ pub async fn index(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoRe
)
}
pub async fn projects(v: impl ViewRenderer, ctx: &AppContext) -> Result<impl IntoResponse> {
let projects = services::projects::get_all_projects_dto(ctx).await?;
format::render().view(&v, "website/projects.html", data!({"projects": projects}))
}
pub async fn project_detail(v: impl ViewRenderer, ctx: &AppContext, id: i32) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto(ctx, id).await?;
format::render().view(&v, "website/project_detail.html", data!({"project": project}))
}
pub async fn project_detail_from_name(v: impl ViewRenderer, ctx: &AppContext, name: String) -> Result<impl IntoResponse> {
let project = services::projects::get_project_dto_by_name(ctx, &name).await?;
format::render().view(&v, "website/project_detail.html", data!({"project": project}))
}
pub async fn about(v: impl ViewRenderer) -> Result<impl IntoResponse> {
let age = services::website::get_current_age();