Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Feedback system #31

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

5 changes: 3 additions & 2 deletions migrations/20240823213817_add_feedback.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ CREATE TABLE mod_feedback
id SERIAL PRIMARY KEY NOT NULL,
mod_version_id INTEGER NOT NULL,
reviewer_id INTEGER NOT NULL,
feedback TEXT COLLATE pg_catalog."default" NOT NULL DEFAULT 'No feedback provided.'::text,
feedback TEXT COLLATE pg_catalog."default" NOT NULL,
decision BOOLEAN NOT NULL DEFAULT false,
qimiko marked this conversation as resolved.
Show resolved Hide resolved
type feedback_type NOT NULL,
CONSTRAINT mod_feedback_mod_id_reviewer_id_key UNIQUE (mod_version_id, reviewer_id),
dev bool NOT NULL DEFAULT false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could get a little confusing, considering that "this person is a developer of this mod" is not constant. i'd rather this check be done during the sql query (like the admin field) instead of stored in the table

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah alr

created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT mod_feedback_mod_version_id_fkey FOREIGN KEY (mod_version_id)
REFERENCES public.mod_versions (id)
ON DELETE CASCADE,
Expand Down
44 changes: 40 additions & 4 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -681,8 +681,18 @@ paths:
description: The feedback given by the reviewer
example: "This mod is great!"
responses:
"204":
description: No Content (Feedback added)
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
error:
type: string
payload:
description: The ID of the new feedback (for use in deleting)
type: integer
"400":
$ref: "#/components/responses/BadRequest"
"401":
Expand All @@ -701,6 +711,16 @@ paths:
parameters:
- $ref: "#/components/parameters/ModID"
- $ref: "#/components/parameters/ModVersion"
requestBody:
content:
application/json:
schema:
type: object
properties:
id:
type: integer
description: The ID of the feedback to delete
example: 1
responses:
"204":
description: No Content (Feedback deleted)
Expand Down Expand Up @@ -1069,6 +1089,22 @@ components:
admin:
type: boolean
description: Whether the reviewer is an admin
dev:
type: boolean
description: Whether the reviewer is a developer of the mod

Score:
type: object
properties:
score:
type: integer
description: The score of the mod, calculated as a sum of all feedback where positive is 1 and negative is -1
positive:
type: integer
description: The number of positive feedback
negative:
type: integer
description: The number of negative feedback

ModFeedbackOne:
type: object
Expand All @@ -1095,8 +1131,8 @@ components:
type: object
properties:
score:
type: integer
description: The score of the mod, calculated as a sum of all feedback where positive is 1 and negative is -1
$ref: "#/components/schemas/Score"
description: The mod's score
mod_id:
$ref: "#/components/schemas/ModID"
mod_version:
Expand Down
46 changes: 20 additions & 26 deletions src/endpoints/mod_feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use crate::{
};
use crate::types::models::mod_version::ModVersion;
use crate::types::models::mod_feedback::{ModFeedback,FeedbackTypeEnum};
use crate::types::models::mod_version_status::ModVersionStatusEnum;

#[derive(Deserialize)]
pub struct GetModFeedbackPath {
Expand All @@ -28,6 +27,11 @@ pub struct PostModFeedbackPayload {
feedback: String,
}

#[derive(Deserialize)]
pub struct DeleteModFeedbackPayload {
id: i32
}

#[get("/v1/mods/{id}/versions/{version}/feedback")]
pub async fn get_mod_feedback(
data: web::Data<AppData>,
Expand All @@ -43,14 +47,11 @@ pub async fn get_mod_feedback(
return Err(ApiError::Forbidden);
}

let mut note_only = false;
if !access && !dev.admin {
note_only = true;
}
let note_only = !access && !dev.admin;

let mod_version = {
if path.version == "latest" {
ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut pool).await?
ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut pool).await?
} else {
ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut pool).await?
}
Expand Down Expand Up @@ -85,19 +86,15 @@ pub async fn post_mod_feedback(
return Err(ApiError::Forbidden);
}

if !access && payload.feedback_type == FeedbackTypeEnum::Note {
return Err(ApiError::BadRequest("Only mod owners can leave notes".to_string()));
}

let mod_version = {
if path.version == "latest" {
ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut transaction).await?
ModVersion::get_latest_for_mod(&path.id, None, vec![], None, &mut transaction).await?
} else {
ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await?
}
};

let result = ModFeedback::set(&mod_version, dev.id, payload.feedback_type.clone(), &payload.feedback, false, &mut transaction).await;
let result = ModFeedback::set(&mod_version, dev.id, payload.feedback_type.clone(), &payload.feedback, false, access, &mut transaction).await;

if result.is_err() {
transaction
Expand All @@ -112,34 +109,31 @@ pub async fn post_mod_feedback(
.await
.or(Err(ApiError::TransactionError))?;

Ok(HttpResponse::NoContent())
Ok(web::Json(ApiResponse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this behavior is somewhat inconsistent considering the rest of the post endpoints don't do this, but i can see why you did it. i'll probably think about it further

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i'll leave it as it is rn then

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on second thought, nocontent is probably the generally the best move

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would you get the id? through the GET endpoint?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ye, i think if the get feedback returns the author's feedback as well, then it makes it easy to associate feedback with the id on the client (so they can delete)

i wouldn't assume people are storing the id locally anyways

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

error: "".to_string(),
payload: result?,
}))
}

#[delete("/v1/mods/{id}/versions/{version}/feedback")]
pub async fn delete_mod_feedback(
data: web::Data<AppData>,
path: web::Path<GetModFeedbackPath>,
payload: web::Json<DeleteModFeedbackPayload>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of payload, put the id as part of the path:
/v1/mods/{id}/versions/{version}/feedback/{feedback_id}

this is more consistent with the rest of the api and also consistent with what a delete request is

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah ok

auth: Auth,
) -> Result<impl Responder, ApiError> {
let dev = auth.developer()?;
let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?;
let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?;

let access = Developer::has_access_to_mod(dev.id, &path.id, &mut transaction).await?;

if !access && !dev.verified && !dev.admin {
return Err(ApiError::Forbidden);
}

let mod_version = {
if path.version == "latest" {
ModVersion::get_latest_for_mod(&path.id, None, vec![], None, vec![ModVersionStatusEnum::Accepted, ModVersionStatusEnum::Pending, ModVersionStatusEnum::Rejected, ModVersionStatusEnum::Unlisted], &mut transaction).await?
} else {
ModVersion::get_one(path.id.strip_prefix('v').unwrap_or(&path.id), &path.version, false, false, &mut transaction).await?
if !dev.admin {
let feedback = ModFeedback::get_feedback_by_id(payload.id, &mut transaction).await?;
if feedback.reviewer.id != dev.id {
return Err(ApiError::Forbidden);
}
};
}

let result = ModFeedback::remove(&mod_version, dev.id, &mut transaction).await;
let result = ModFeedback::remove(payload.id, &mut transaction).await;

if result.is_err() {
transaction
Expand Down
5 changes: 3 additions & 2 deletions src/endpoints/mod_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ pub async fn get_one(
let platform_string = query.platforms.clone().unwrap_or_default();
let platforms = VerPlatform::parse_query_string(&platform_string);

ModVersion::get_latest_for_mod(&path.id, gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool).await?
ModVersion::get_latest_for_mod_statuses(&path.id, gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool).await?
} else {
ModVersion::get_one(&path.id, &path.version, true, false, &mut pool).await?
}
Expand Down Expand Up @@ -182,7 +182,7 @@ pub async fn download_version(
if path.version == "latest" {
let platform_str = query.platforms.clone().unwrap_or_default();
let platforms = VerPlatform::parse_query_string(&platform_str);
ModVersion::get_latest_for_mod(&path.id, query.gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool)
ModVersion::get_latest_for_mod_statuses(&path.id, query.gd, platforms, query.major, vec![ModVersionStatusEnum::Accepted], &mut pool)
.await?
} else {
ModVersion::get_one(&path.id, &path.version, false, false, &mut pool).await?
Expand Down Expand Up @@ -338,6 +338,7 @@ pub async fn update_version(
feedback_type,
payload.info.as_deref().unwrap_or_default(),
true,
Developer::has_access_to_mod(dev.id, &version.mod_id, &mut transaction).await?,
&mut transaction
).await {
transaction
Expand Down
Loading