CRUD for project
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod website;
|
||||
|
||||
pub mod data;
|
||||
pub mod data;
|
||||
pub mod project;
|
46
src/controllers/project.rs
Normal file
46
src/controllers/project.rs
Normal 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))
|
||||
}
|
@@ -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))
|
||||
|
@@ -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,
|
||||
|
@@ -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(())
|
||||
}
|
@@ -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(())
|
||||
}
|
30
src/tasks/delete_project.rs
Normal file
30
src/tasks/delete_project.rs
Normal 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(())
|
||||
}
|
||||
}
|
@@ -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;
|
@@ -1,3 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod data;
|
||||
pub mod website;
|
||||
pub mod projects;
|
24
src/views/projects.rs
Normal file
24
src/views/projects.rs
Normal 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!({}))
|
||||
}
|
@@ -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();
|
||||
|
||||
|
Reference in New Issue
Block a user