Skip to content

Commit d6b748e

Browse files
Merge pull request #118 from dipamsen/replace-papers
Replace Papers from Admin Dashboard
2 parents f110673 + fa14e81 commit d6b748e

File tree

8 files changed

+65
-11
lines changed

8 files changed

+65
-11
lines changed

backend/src/db/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ impl Database {
9595
/// - Sets the `filelink` to:
9696
/// - For library papers, remains unchanged
9797
/// - For uploaded papers, approved papers are moved to the approved directory and renamed `id_coursecode_coursename_year_semester_exam.pdf` and unapproved papers are moved to the unapproved directory and named `id.pdf`
98+
/// - Deletes `replace` papers from the database.
9899
///
99100
/// Returns the database transaction, the old filelink and the new paper details ([`crate::qp::AdminDashboardQP`])
100101
pub async fn edit_paper<'c>(
@@ -113,6 +114,7 @@ impl Database {
113114
exam,
114115
approve_status,
115116
note,
117+
replace,
116118
} = edit_req;
117119

118120
let current_details = self.get_paper_by_id(id).await?;
@@ -177,6 +179,23 @@ impl Database {
177179
let new_qp: DBAdminDashboardQP = query.fetch_one(&mut *tx).await?;
178180
let new_qp = AdminDashboardQP::from(new_qp);
179181

182+
// Delete the replaced papers
183+
for replace_id in replace {
184+
let rows_affected = sqlx::query(queries::SOFT_DELETE_ANY_BY_ID)
185+
.bind(replace_id)
186+
.execute(&mut *tx)
187+
.await?
188+
.rows_affected();
189+
190+
if rows_affected > 1 {
191+
tx.rollback().await?;
192+
return Err(eyre!(
193+
"Error: {} (> 1) papers were deleted. Rolling back.",
194+
rows_affected
195+
));
196+
}
197+
}
198+
180199
Ok((tx, old_filelink, new_qp))
181200
}
182201

backend/src/db/queries.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ pub fn get_similar_papers_query(year: bool, semester: bool, exam: bool) -> Strin
6565
pub const SOFT_DELETE_BY_ID: &str =
6666
"UPDATE iqps SET approve_status=false, is_deleted = true WHERE id=$1 AND from_library = false";
6767

68+
/// Soft deletes a paper (sets `approve_status` to false and `is_deleted` to true) of any paper.
69+
pub const SOFT_DELETE_ANY_BY_ID: &str =
70+
"UPDATE iqps SET approve_status=false, is_deleted = true WHERE id=$1";
71+
6872
/// Get a paper ([`crate::db::models::DBAdminDashboardQP`]) with the given id (first parameter `$1`)
6973
pub fn get_get_paper_by_id_query() -> String {
7074
format!(

backend/src/routing/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ pub struct EditReq {
162162
pub exam: Option<String>,
163163
pub note: Option<String>,
164164
pub approve_status: Option<bool>,
165+
pub replace: Vec<i32>,
165166
}
166167

167168
/// Paper edit endpoint (for admin dashboard)

frontend/.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
VITE_BACKEND_URL=http://localhost:8080
22
VITE_MAX_UPLOAD_LIMIT=10
3-
VITE_GH_OAUTH_CLIENT_ID=
3+
VITE_GH_OAUTH_CLIENT_ID=

frontend/src/components/AdminDashboard/QPCard.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { FaRegTrashAlt } from "react-icons/fa";
22
import { IAdminDashboardQP } from "../../types/question_paper";
33
import { isQPValid, validate } from "../../utils/validateInput";
4-
import { FaFilePdf, FaRegPenToSquare } from "react-icons/fa6";
5-
4+
import { FaFilePdf, FaRegPenToSquare, FaRegSquareCheck, FaRegSquare } from "react-icons/fa6";
65
import "./styles/qp_card.scss";
76
import { formatBackendTimestamp } from "../../utils/backend";
7+
import { useState } from "react";
88

99
interface IQPCardProps {
1010
qPaper: IAdminDashboardQP;
1111
onEdit?: React.MouseEventHandler<HTMLButtonElement>;
1212
onDelete?: React.MouseEventHandler<HTMLButtonElement>;
13+
onToggle?: (prev: boolean, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
1314
hasOcr?: boolean;
1415
}
1516

16-
export function QPCard({ qPaper, onEdit, onDelete, hasOcr }: IQPCardProps) {
17+
export function QPCard({ qPaper, onEdit, onDelete, hasOcr, onToggle }: IQPCardProps) {
1718
const errorMsg = validate(qPaper);
1819
const isValid = isQPValid(qPaper);
1920

21+
const [selected, setSelected] = useState<boolean>(false);
22+
2023
return (
2124
<div className={`qp-card ${qPaper.approve_status ? 'approved' : ''}`}>
2225
<div className="qp-data">
@@ -63,6 +66,21 @@ export function QPCard({ qPaper, onEdit, onDelete, hasOcr }: IQPCardProps) {
6366
}
6467
</>
6568
}
69+
70+
{
71+
onToggle !== undefined &&
72+
<div
73+
className={`select-btn btn ${selected ? 'selected' : ''}`}
74+
onClick={(e) => {
75+
e.stopPropagation();
76+
setSelected(!selected);
77+
onToggle(selected, e);
78+
}}
79+
title={"Replace Paper"}
80+
>
81+
{selected ? <FaRegSquareCheck size="1.5rem" /> : <FaRegSquare size="1.5rem" />}
82+
</div>
83+
}
6684
</div>
6785
</div>
6886
);

frontend/src/components/Common/PaperEditModal.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { IoClose } from "react-icons/io5";
1919
import { FaCalendarAlt, FaRegTrashAlt, FaSync } from "react-icons/fa";
2020
import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "react-icons/ai";
2121

22-
type UpdateQPHandler<T> = (qp: T) => void;
22+
type UpdateQPHandler<T> = (qp: T, replace: number[]) => void;
2323
interface IPaperEditModalProps<T> {
2424
onClose: () => void;
2525
selectPrev?: (() => void) | null;
@@ -39,6 +39,7 @@ function PaperEditModal<T extends IQuestionPaperFile | IAdminDashboardQP>(props:
3939

4040
const [similarPapers, setSimilarPapers] = useState<IAdminDashboardQP[]>([]);
4141
const [awaitingSimilarPapers, setAwaitingSimilarPapers] = useState<boolean>(false);
42+
const [replacingPapers, setReplacingPapers] = useState<IAdminDashboardQP[]>([]);
4243

4344
const [courseCodeSuggestions, setCourseCodeSuggestions] = useState<ISuggestion<null>[]>([]);
4445
const [courseNameSuggestions, setCourseNameSuggestions] = useState<ISuggestion<[course_code: string, course_name: string]>[]>([]);
@@ -119,7 +120,7 @@ function PaperEditModal<T extends IQuestionPaperFile | IAdminDashboardQP>(props:
119120
getSimilarPapers(similarityDetails, 'id' in data ? data.id : -1);
120121
}
121122

122-
}, [data.course_code, data.year, data.exam, data.semester])
123+
}, [data.course_code, data.year, data.exam, data.semester, 'id' in data ? data.id : -1])
123124
}
124125

125126
const courseCodeNameMap = Object.entries(COURSE_CODE_MAP);
@@ -453,7 +454,7 @@ function PaperEditModal<T extends IQuestionPaperFile | IAdminDashboardQP>(props:
453454
if (!('approve_status' in data)) {
454455
toast.success("File details updated successfully");
455456
}
456-
props.updateQPaper(data);
457+
props.updateQPaper(data, replacingPapers.map(x => x.id));
457458
}}
458459
disabled={!isDataValid}
459460
className="save-btn"
@@ -494,13 +495,22 @@ function PaperEditModal<T extends IQuestionPaperFile | IAdminDashboardQP>(props:
494495
{
495496
awaitingSimilarPapers ? <div style={{ justifyContent: 'center', display: 'flex' }}><Spinner /></div> :
496497
<div>
498+
{similarPapers.length > 0 && <p style={{margin: "0 0 4px 0"}}>Select papers to replace.</p>}
497499
{
498500
similarPapers.length === 0 ? <p>No similar papers found.</p> :
499501
similarPapers.map((paper, i) => <QPCard
500502
qPaper={paper}
501503
key={i}
502-
/>
503-
)
504+
onToggle={(selected) => {
505+
if (!selected) { // select
506+
if (!replacingPapers.some((p) => p.id === paper.id)) {
507+
setReplacingPapers((prev) => [...prev, paper]);
508+
}
509+
} else { // unselect
510+
setReplacingPapers((prev) => prev.filter((p) => p.id !== paper.id));
511+
}
512+
}}
513+
/>)
504514
}
505515
</div>
506516
}

frontend/src/pages/AdminDashboard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ function AdminDashboard() {
3535
number | null
3636
>(null);
3737

38-
const handlePaperEdit = async (qp: IAdminDashboardQP) => {
38+
const handlePaperEdit = async (qp: IAdminDashboardQP, replace: number[]) => {
3939
const response = await makeRequest(
4040
"edit",
4141
"post",
4242
{
4343
...qp,
44+
replace,
4445
},
4546
auth.jwt,
4647
);
@@ -281,7 +282,7 @@ function AdminDashboard() {
281282
: null
282283
}
283284
qPaper={unapprovedPapers[selectedQPaperIndex]}
284-
updateQPaper={(qp) => handlePaperEdit(qp)}
285+
updateQPaper={(qp, replace) => handlePaperEdit(qp, replace)}
285286
ocrDetails={ocrDetails.get(
286287
unapprovedPapers[selectedQPaperIndex].id,
287288
)}

frontend/src/types/backend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface IEndpointTypes {
5656
exam?: string,
5757
note?: string,
5858
approve_status?: boolean,
59+
replace: number[]
5960
},
6061
response: {
6162
id: number;

0 commit comments

Comments
 (0)