More project data (#406)

* More project data

* Array_agg fixes + cleanup

* fix prepare

* Add approval dates to search

* Update migrations/20220725204351_more-project-data.sql

Co-authored-by: wafflecoffee <emmaffle@modrinth.com>

* Add category labels + display categories

Co-authored-by: wafflecoffee <emmaffle@modrinth.com>
This commit is contained in:
Geometrically
2022-07-31 13:29:20 -07:00
committed by GitHub
parent 13335cadc6
commit b04bced37f
38 changed files with 3673 additions and 3517 deletions

View File

@@ -21,10 +21,10 @@ use crate::util::auth::get_github_user_from_token;
use actix_web::http::StatusCode;
use actix_web::web::{scope, Data, Query, ServiceConfig};
use actix_web::{get, HttpResponse};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use thiserror::Error;
use time::OffsetDateTime;
pub fn config(cfg: &mut ServiceConfig) {
cfg.service(scope("auth").service(auth_callback).service(init));
@@ -176,9 +176,9 @@ pub async fn auth_callback(
.await?;
if let Some(result) = result_option {
let duration = result.expires - OffsetDateTime::now_utc();
let duration: chrono::Duration = result.expires - Utc::now();
if duration.whole_seconds() < 0 {
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentials);
}
@@ -255,7 +255,7 @@ pub async fn auth_callback(
email: user.email,
avatar_url: Some(user.avatar_url),
bio: user.bio,
created: OffsetDateTime::now_utc(),
created: Utc::now(),
role: Role::Developer.to_string(),
}
.insert(&mut transaction)

View File

@@ -108,7 +108,7 @@ pub async fn maven_metadata(
.map(|x| x.version_number.clone())
.collect::<Vec<_>>(),
},
last_updated: data.inner.updated.format("%Y%m%d%H%M%S"),
last_updated: data.inner.updated.format("%Y%m%d%H%M%S").to_string(),
},
};

View File

@@ -14,13 +14,13 @@ use actix_multipart::{Field, Multipart};
use actix_web::http::StatusCode;
use actix_web::web::Data;
use actix_web::{post, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::stream::StreamExt;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::collections::HashSet;
use std::sync::Arc;
use thiserror::Error;
use time::OffsetDateTime;
use validator::Validate;
#[derive(Error, Debug)]
@@ -166,6 +166,9 @@ struct ProjectCreateData {
#[validate(length(max = 3))]
/// A list of the categories that the project is in.
pub categories: Vec<String>,
#[validate(length(max = 256))]
/// A list of the categories that the project is in.
pub additional_categories: Vec<String>,
#[validate(url, length(max = 2048))]
/// An optional link to where to submit bugs or issues with the project.
@@ -388,7 +391,7 @@ pub async fn project_create_inner(
{
let results = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM mods WHERE slug = $1)
SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1))
",
create_data.slug
)
@@ -522,7 +525,7 @@ pub async fn project_create_inner(
featured: item.featured,
title: item.title.clone(),
description: item.description.clone(),
created: OffsetDateTime::now_utc(),
created: Utc::now(),
});
continue;
@@ -593,6 +596,19 @@ pub async fn project_create_inner(
categories.push(id);
}
let mut additional_categories =
Vec::with_capacity(project_create_data.additional_categories.len());
for category in &project_create_data.additional_categories {
let id = models::categories::Category::get_id_project(
category,
project_type_id,
&mut *transaction,
)
.await?
.ok_or_else(|| CreateError::InvalidCategory(category.clone()))?;
additional_categories.push(id);
}
let team = models::team_item::TeamBuilder {
members: vec![models::team_item::TeamMemberBuilder {
user_id: current_user.id.into(),
@@ -698,6 +714,7 @@ pub async fn project_create_inner(
license_url: project_create_data.license_url,
discord_url: project_create_data.discord_url,
categories,
additional_categories,
initial_versions: versions,
status: status_id,
client_side: client_side_id,
@@ -718,7 +735,7 @@ pub async fn project_create_inner(
.collect(),
};
let now = OffsetDateTime::now_utc();
let now = Utc::now();
let response = crate::models::projects::Project {
id: project_id,
@@ -731,6 +748,7 @@ pub async fn project_create_inner(
body_url: None,
published: now,
updated: now,
approved: None,
status: status.clone(),
moderator_message: None,
license: License {
@@ -743,6 +761,7 @@ pub async fn project_create_inner(
downloads: 0,
followers: 0,
categories: project_create_data.categories,
additional_categories: project_create_data.additional_categories,
versions: project_builder
.initial_versions
.iter()

View File

@@ -11,12 +11,12 @@ use crate::util::auth::{get_user_from_headers, is_authorized};
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{PgPool, Row};
use std::sync::Arc;
use time::OffsetDateTime;
use validator::Validate;
#[get("search")]
@@ -112,7 +112,7 @@ pub async fn project_get_check(
sqlx::query!(
"
SELECT id FROM mods
WHERE LOWER(slug) = LOWER($1)
WHERE slug = LOWER($1)
",
&slug
)
@@ -126,7 +126,7 @@ pub async fn project_get_check(
sqlx::query!(
"
SELECT id FROM mods
WHERE LOWER(slug) = LOWER($1)
WHERE slug = LOWER($1)
",
&slug
)
@@ -259,6 +259,8 @@ pub struct EditProject {
pub body: Option<String>,
#[validate(length(max = 3))]
pub categories: Option<Vec<String>>,
#[validate(length(max = 256))]
pub additional_categories: Option<Vec<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
@@ -483,6 +485,21 @@ pub async fn project_edit(
)
})?;
if status == &ProjectStatus::Approved
|| status == &ProjectStatus::Unlisted
{
sqlx::query!(
"
UPDATE mods
SET published = NOW()
WHERE id = $1 AND approved = NULL
",
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
sqlx::query!(
"
UPDATE mods
@@ -513,7 +530,7 @@ pub async fn project_edit(
sqlx::query!(
"
DELETE FROM mods_categories
WHERE joining_mod_id = $1
WHERE joining_mod_id = $1 AND is_additional = FALSE
",
id as database::models::ids::ProjectId,
)
@@ -536,8 +553,8 @@ pub async fn project_edit(
sqlx::query!(
"
INSERT INTO mods_categories (joining_mod_id, joining_category_id)
VALUES ($1, $2)
INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)
VALUES ($1, $2, FALSE)
",
id as database::models::ids::ProjectId,
category_id as database::models::ids::CategoryId,
@@ -547,6 +564,51 @@ pub async fn project_edit(
}
}
if let Some(categories) = &new_project.additional_categories {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the additional categories of this project!"
.to_string(),
));
}
sqlx::query!(
"
DELETE FROM mods_categories
WHERE joining_mod_id = $1 AND is_additional = TRUE
",
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
for category in categories {
let category_id =
database::models::categories::Category::get_id(
category,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Category {} does not exist.",
category.clone()
))
})?;
sqlx::query!(
"
INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)
VALUES ($1, $2, TRUE)
",
id as database::models::ids::ProjectId,
category_id as database::models::ids::CategoryId,
)
.execute(&mut *transaction)
.await?;
}
}
if let Some(issues_url) = &new_project.issues_url {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
@@ -1178,7 +1240,7 @@ pub async fn add_gallery_item(
featured: item.featured,
title: item.title,
description: item.description,
created: OffsetDateTime::now_utc(),
created: Utc::now(),
}
.insert(&mut transaction)
.await?;

View File

@@ -5,10 +5,10 @@ use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers,
};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
use serde::Deserialize;
use sqlx::PgPool;
use time::OffsetDateTime;
#[derive(Deserialize)]
pub struct CreateReport {
@@ -60,7 +60,7 @@ pub async fn report_create(
user_id: None,
body: new_report.body.clone(),
reporter: current_user.id.into(),
created: OffsetDateTime::now_utc(),
created: Utc::now(),
};
match new_report.item_type {
@@ -109,7 +109,7 @@ pub async fn report_create(
item_type: new_report.item_type.clone(),
reporter: current_user.id,
body: new_report.body.clone(),
created: OffsetDateTime::now_utc(),
created: Utc::now(),
}))
}

View File

@@ -5,9 +5,9 @@ use crate::database::models::categories::{
};
use crate::util::auth::check_is_admin_from_headers;
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use models::categories::{Category, GameVersion, Loader};
use sqlx::PgPool;
use time::OffsetDateTime;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
@@ -38,6 +38,7 @@ pub struct CategoryData {
icon: String,
name: String,
project_type: String,
header: String,
}
// TODO: searching / filtering? Could be used to implement a live
@@ -53,6 +54,7 @@ pub async fn category_list(
icon: x.icon,
name: x.category,
project_type: x.project_type,
header: x.header,
})
.collect::<Vec<_>>();
@@ -84,6 +86,7 @@ pub async fn category_create(
.name(&new_category.name)?
.project_type(&project_type)?
.icon(&new_category.icon)?
.header(&new_category.header)?
.insert(&**pool)
.await?;
@@ -202,8 +205,7 @@ pub async fn loader_delete(
pub struct GameVersionQueryData {
pub version: String,
pub version_type: String,
#[serde(with = "crate::util::time_ser")]
pub date: OffsetDateTime,
pub date: DateTime<Utc>,
pub major: bool,
}
@@ -243,7 +245,7 @@ pub async fn game_version_list(
pub struct GameVersionData {
#[serde(rename = "type")]
type_: String,
date: Option<OffsetDateTime>,
date: Option<DateTime<Utc>>,
}
#[put("game_version/{name}")]

View File

@@ -7,10 +7,10 @@ use crate::util::auth::{
};
use actix_web::web;
use actix_web::{get, post, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize)]
pub struct Report {
@@ -20,8 +20,7 @@ pub struct Report {
pub item_type: ItemType,
pub reporter: UserId,
pub body: String,
#[serde(with = "crate::util::time_ser")]
pub created: OffsetDateTime,
pub created: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Clone)]
@@ -93,7 +92,7 @@ pub async fn report_create(
user_id: None,
body: new_report.body.clone(),
reporter: current_user.id.into(),
created: OffsetDateTime::now_utc(),
created: Utc::now(),
};
match new_report.item_type {
@@ -142,7 +141,7 @@ pub async fn report_create(
item_type: new_report.item_type.clone(),
reporter: current_user.id,
body: new_report.body.clone(),
created: OffsetDateTime::now_utc(),
created: Utc::now(),
}))
}

View File

@@ -9,10 +9,10 @@ use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::{database, models};
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use time::OffsetDateTime;
/// A specific version of a mod
#[derive(Serialize, Deserialize)]
@@ -25,8 +25,7 @@ pub struct LegacyVersion {
pub version_number: String,
pub changelog: String,
pub changelog_url: Option<String>,
#[serde(with = "crate::util::time_ser")]
pub date_published: OffsetDateTime,
pub date_published: DateTime<Utc>,
pub downloads: u32,
pub version_type: VersionType,
pub files: Vec<VersionFile>,

View File

@@ -18,10 +18,10 @@ use crate::validate::{validate_file, ValidationResult};
use actix_multipart::{Field, Multipart};
use actix_web::web::Data;
use actix_web::{post, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::stream::StreamExt;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use time::OffsetDateTime;
use validator::Validate;
#[derive(Serialize, Deserialize, Validate, Clone)]
@@ -402,7 +402,7 @@ async fn version_create_inner(
version_number: builder.version_number.clone(),
changelog: builder.changelog.clone(),
changelog_url: None,
date_published: OffsetDateTime::now_utc(),
date_published: Utc::now(),
downloads: 0,
version_type: version_data.release_channel,
files: builder
@@ -722,23 +722,18 @@ pub async fn upload_file(
for file in &format.files {
if let Some(dep) = res.iter().find(|x| {
x.hash.as_deref()
Some(&*x.hash)
== file
.hashes
.get(&PackFileHash::Sha1)
.map(|x| x.as_bytes())
}) {
if let Some(project_id) = dep.project_id {
if let Some(version_id) = dep.version_id {
dependencies.push(DependencyBuilder {
project_id: Some(models::ProjectId(project_id)),
version_id: Some(models::VersionId(version_id)),
file_name: None,
dependency_type: DependencyType::Embedded
.to_string(),
});
}
}
dependencies.push(DependencyBuilder {
project_id: Some(models::ProjectId(dep.project_id)),
version_id: Some(models::VersionId(dep.version_id)),
file_name: None,
dependency_type: DependencyType::Embedded.to_string(),
});
} else if let Some(first_download) = file.downloads.first() {
dependencies.push(DependencyBuilder {
project_id: None,