diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs index c928d4d..db525ea 100644 --- a/backend/src/db/mod.rs +++ b/backend/src/db/mod.rs @@ -120,6 +120,7 @@ impl Database { semester, exam, approve_status, + note, } = edit_req; let current_details = self.get_paper_by_id(id).await?; @@ -171,6 +172,7 @@ impl Database { .bind(year) .bind(&semester) .bind(&exam) + .bind(¬e) .bind(approve_status) .bind(&new_filelink); @@ -258,6 +260,7 @@ impl Database { year, exam, semester, + note, .. } = file_details; @@ -267,6 +270,7 @@ impl Database { .bind(year) .bind(exam) .bind(semester) + .bind(note) .bind("placeholder_filelink") .bind(false); diff --git a/backend/src/db/models.rs b/backend/src/db/models.rs index 3fc4316..d469912 100644 --- a/backend/src/db/models.rs +++ b/backend/src/db/models.rs @@ -20,6 +20,7 @@ pub struct DBBaseQP { year: i32, semester: String, exam: String, + note: String, } #[derive(FromRow, Clone)] @@ -52,6 +53,7 @@ impl From for qp::BaseQP { year: value.year, semester: (&value.semester).try_into().unwrap_or(Semester::Unknown), exam: (&value.exam).try_into().unwrap_or(qp::Exam::Unknown), + note: value.note, } } } diff --git a/backend/src/db/queries.rs b/backend/src/db/queries.rs index 71b7d50..803cbb3 100644 --- a/backend/src/db/queries.rs +++ b/backend/src/db/queries.rs @@ -10,10 +10,11 @@ const INIT_DB: &str = " CREATE TABLE IF NOT EXISTS iqps ( id integer primary key GENERATED ALWAYS AS identity, course_code TEXT NOT NULL DEFAULT '', - course_name TEXT NOT NULL, + course_name TEXT NOT NULL DEFAULT '', year INTEGER NOT NULL, exam TEXT NOT NULL DEFAULT '', semester TEXT NOT NULL DEFAULT '', + note TEXT NOT NULL DEFAULT '', filelink TEXT NOT NULL, from_library BOOLEAN DEFAULT FALSE, upload_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -70,7 +71,7 @@ pub fn get_get_paper_by_id_query() -> String { ) } -/// Returns a query that updates a paper's details by id ($1) (course_code, course_name, year, semester, exam, approve_status, filelink). `approved_by` optionally included if the edit is also used for approval. +/// Returns a query that updates a paper's details by id ($1) (course_code, course_name, year, semester, exam, note, approve_status, filelink). `approved_by` optionally included if the edit is also used for approval. /// /// The query also returns all the admin dashboard qp fields of the edited paper /// @@ -81,13 +82,14 @@ pub fn get_get_paper_by_id_query() -> String { /// - $4: `year` /// - $5: `semester` /// - $6: `exam` -/// - $7: `approve_status` -/// - $8: `filelink` -/// - $9: `approved_by` +/// - $7: `note` +/// - $8: `approve_status` +/// - $9: `filelink` +/// - $10: `approved_by` pub fn get_edit_paper_query(approval: bool) -> String { format!( - "UPDATE iqps set course_code=$2, course_name=$3, year=$4, semester=$5, exam=$6, approve_status=$7, filelink=$8{} WHERE id=$1 AND is_deleted=false RETURNING {}", - if approval {", approved_by=$9"} else {""}, + "UPDATE iqps set course_code=$2, course_name=$3, year=$4, semester=$5, exam=$6, note=$7, approve_status=$8, filelink=$9{} WHERE id=$1 AND is_deleted=false RETURNING {}", + if approval {", approved_by=$10"} else {""}, ADMIN_DASHBOARD_QP_FIELDS ) } @@ -193,15 +195,15 @@ pub fn get_qp_search_query(exam_filter: ExamFilter) -> (String, bool) { } /// List of fields in the [`crate::db::models::DBAdminDashboardQP`] to be used with SELECT clauses -pub const ADMIN_DASHBOARD_QP_FIELDS: &str = "id, filelink, from_library, course_code, course_name, year, semester, exam, upload_timestamp, approve_status"; +pub const ADMIN_DASHBOARD_QP_FIELDS: &str = "id, filelink, from_library, course_code, course_name, year, semester, exam, note, upload_timestamp, approve_status"; /// List of fields in the [`crate::db::models::DBSearchQP`] to be used with SELECT clauses pub const SEARCH_QP_FIELDS: &str = - "id, filelink, from_library, course_code, course_name, year, semester, exam"; + "id, filelink, from_library, course_code, course_name, year, semester, exam, note"; /// Insert a newly uploaded file in the db (and return the id) -/// Parameters in the following order: `course_code`, `course_name`, `year`, `exam`, `semester`, `filelink`, `from_library` -pub const INSERT_NEW_QP: &str = "INSERT INTO iqps (course_code, course_name, year, exam, semester, filelink, from_library) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id"; +/// Parameters in the following order: `course_code`, `course_name`, `year`, `exam`, `semester`, `note`, `filelink`, `from_library` +pub const INSERT_NEW_QP: &str = "INSERT INTO iqps (course_code, course_name, year, exam, semester, note, filelink, from_library) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id"; /// Updates the filelink ($2) of a paper with the given id ($1). Used to update the filelink after a paper is uploaded. pub const UPDATE_FILELINK: &str = "UPDATE iqps SET filelink=$2 WHERE id=$1"; diff --git a/backend/src/qp.rs b/backend/src/qp.rs index 0c6e630..794ff25 100644 --- a/backend/src/qp.rs +++ b/backend/src/qp.rs @@ -105,11 +105,11 @@ impl From for String { } #[duplicate_item( - ExamSem; + Serializable; [ Exam ]; [ Semester ]; )] -impl Serialize for ExamSem { +impl Serialize for Serializable { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -134,6 +134,7 @@ pub struct BaseQP { pub year: i32, pub semester: Semester, pub exam: Exam, + pub note: String, } #[derive(Serialize, Clone)] diff --git a/backend/src/routing/handlers.rs b/backend/src/routing/handlers.rs index 9a5253b..f40ed41 100644 --- a/backend/src/routing/handlers.rs +++ b/backend/src/routing/handlers.rs @@ -158,6 +158,7 @@ pub struct EditReq { pub year: Option, pub semester: Option, pub exam: Option, + pub note: Option, pub approve_status: Option, } @@ -216,6 +217,7 @@ pub struct FileDetails { pub exam: String, pub semester: String, pub filename: String, + pub note: String, } /// 10 MiB file size limit diff --git a/frontend/src/components/AdminDashboard/QPCard.tsx b/frontend/src/components/AdminDashboard/QPCard.tsx index 17a5881..8dbc3f3 100644 --- a/frontend/src/components/AdminDashboard/QPCard.tsx +++ b/frontend/src/components/AdminDashboard/QPCard.tsx @@ -66,6 +66,7 @@ export function QPCard({ qPaper, onEdit, onDelete, hasOcr }: IQPCardProps) {
{qPaper.year}
{qPaper.exam}
{qPaper.semester}
+ {qPaper.note !== "" &&
{qPaper.note}
} {!isValid &&

{Object.values(errorMsg).filter((msg) => msg !== null).join(', ')}

diff --git a/frontend/src/components/Common/Form.tsx b/frontend/src/components/Common/Form.tsx index cd266af..22f9262 100644 --- a/frontend/src/components/Common/Form.tsx +++ b/frontend/src/components/Common/Form.tsx @@ -71,25 +71,45 @@ export function Select(props: ISelectProps) { interface INumberInputProps extends React.InputHTMLAttributes { value: number; + // Displays letters A-Z instead of numbers and restricts the value to their ASCII character codes + alphabetical?: boolean; setValue: (x: number) => void; } export function NumberInput(props: INumberInputProps) { + const clampAlphabet = (num: number) => { + return Math.max('A'.charCodeAt(0), Math.min('Z'.charCodeAt(0), num)); + } + + const alphabeticalInput = props.alphabetical ?? false; + const getClickHandler = (change: number) => { return (e: React.MouseEvent) => { e.preventDefault(); - const newValue = props.value + change; + let newValue = props.value + change; + + if (alphabeticalInput) newValue = clampAlphabet(newValue); + props.setValue(isNaN(newValue) ? 1 : newValue); } } return
{ e.preventDefault(); - props.setValue(parseInt(e.target.value)); + + if (alphabeticalInput) { + const charCode = e.target.value.toUpperCase().charCodeAt(0); + + props.setValue(isNaN(charCode) ? 'A'.charCodeAt(0) : clampAlphabet(charCode)); + } + else { + props.setValue(parseInt(e.target.value)); + } }} - {...props} />
@@ -108,7 +128,7 @@ interface ISuggestionTextInputProps { placeholder?: string; onSuggestionSelect?: (sugg: ISuggestion) => void; onValueChange: (newValue: string) => void; - inputProps: React.InputHTMLAttributes | {}; + inputProps?: React.InputHTMLAttributes | {}; } export function SuggestionTextInput(props: ISuggestionTextInputProps) { const [suggShown, setSuggShown] = useState(false); @@ -161,7 +181,7 @@ export function SuggestionTextInput(props: ISuggestionTextInputProps) { onKeyDown={handleKeyDown} > = (qp: T) => void; interface IPaperEditModalProps { @@ -317,6 +318,71 @@ function PaperEditModal(props: onSelect={(value: Semester) => changeData('semester', value)} /> + +
+
+ + + +
+
+ { + data.note.match(/^Slot [A-Z]$/) && +
+ + { + console.log('changing', value, String.fromCharCode(value)) + changeData('note', `Slot ${String.fromCharCode(value)}`) + } + } + /> +
+ } + { + 'approve_status' in data && +
+ + changeData('note', value)} + suggestions={[]} + /> +
+ } +
+
+
{ 'approve_status' in data && diff --git a/frontend/src/components/Common/styles/paper_edit_modal.scss b/frontend/src/components/Common/styles/paper_edit_modal.scss index 5fee90a..ed69874 100644 --- a/frontend/src/components/Common/styles/paper_edit_modal.scss +++ b/frontend/src/components/Common/styles/paper_edit_modal.scss @@ -58,11 +58,13 @@ background-color: $bg-disabled; font-size: 1rem; - &:hover, &:disabled { + &:hover, + &:disabled { &.approve-btn { background-color: $approved-color; color: $fg-inverse; } + &.unapprove-btn { background-color: $rejected-color; } @@ -74,6 +76,58 @@ } } + .additional-note { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .note-options { + display: flex; + gap: 0.5rem; + + .note-option { + padding: 0.6rem 0.6rem; + border-radius: 5px; + border: none; + color: $fg-color; + background-color: $surface-2; + font-size: 1rem; + + display: flex; + align-items: center; + gap: 10px; + + user-select: none; + + &:hover:not(.enabled) { + cursor: pointer; + background-color: $surface-3; + } + + &.enabled { + border-color: $accent-color-darker; + background-color: $accent-color-darker; + + &.none { + background-color: $bg-disabled; + } + } + } + } + + .note-customize { + display: flex; + flex-direction: column; + gap: 0.5rem; + + div { + display: flex; + gap: 0.5rem; + align-items: center; + } + } + } + h2 { text-align: center; margin-top: 0; @@ -134,7 +188,8 @@ } } - &.next-btn, &.prev-btn { + &.next-btn, + &.prev-btn { background-color: $accent-color; &:hover { diff --git a/frontend/src/components/Search/SearchResults.tsx b/frontend/src/components/Search/SearchResults.tsx index e26a098..4f7040f 100644 --- a/frontend/src/components/Search/SearchResults.tsx +++ b/frontend/src/components/Search/SearchResults.tsx @@ -208,6 +208,7 @@ function ResultCard(result: ISearchResult) { {result.year} {getExamTag(result.exam)} {getSemesterTag(result.semester)} + {result.note !== "" && {result.note}} {auth.isAuthenticated && id: {result.id}}

diff --git a/frontend/src/components/Upload/FileCard.tsx b/frontend/src/components/Upload/FileCard.tsx index ef30a89..8a96af9 100644 --- a/frontend/src/components/Upload/FileCard.tsx +++ b/frontend/src/components/Upload/FileCard.tsx @@ -29,6 +29,7 @@ export function FileCard({ file: { qp: qPaper, ocr }, removeQPaper, edit, invali
{qPaper.year}
{qPaper.exam}
{qPaper.semester}
+ {qPaper.note !== "" &&
{qPaper.note}
}
{invalidDetails &&

Invalid course details

diff --git a/frontend/src/components/Upload/UploadForm.tsx b/frontend/src/components/Upload/UploadForm.tsx index f46f9a1..3a1dab0 100644 --- a/frontend/src/components/Upload/UploadForm.tsx +++ b/frontend/src/components/Upload/UploadForm.tsx @@ -39,7 +39,8 @@ export function UploadForm(props: IUploadFormProps) { course_name: '', year: 1984, semester: 'autumn', - exam: 'ct' + exam: 'ct', + note: '' } } }) diff --git a/frontend/src/components/Upload/styles/file_card.scss b/frontend/src/components/Upload/styles/file_card.scss index ec3a28d..5e800a3 100644 --- a/frontend/src/components/Upload/styles/file_card.scss +++ b/frontend/src/components/Upload/styles/file_card.scss @@ -100,8 +100,8 @@ .pill { padding: 0.25rem 0.5rem; - border-radius: 1rem; - font-size: 0.7rem; + border-radius: 10px; + font-size: 0.8rem; font-weight: bold; color: white; background-color: $accent-color-darker; diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx index 8e23c54..7716275 100644 --- a/frontend/src/pages/UploadPage.tsx +++ b/frontend/src/pages/UploadPage.tsx @@ -43,6 +43,7 @@ export default function UploadPage() { year: number, exam: string, semester: string, + note: string, filename: string, }[] = []; @@ -54,6 +55,7 @@ export default function UploadPage() { year, exam, semester, + note, file_name, } = await sanitizeQP(qp); @@ -65,6 +67,7 @@ export default function UploadPage() { year, exam, semester, + note, filename: file_name }) } diff --git a/frontend/src/types/backend.ts b/frontend/src/types/backend.ts index d1600ca..71fdb7e 100644 --- a/frontend/src/types/backend.ts +++ b/frontend/src/types/backend.ts @@ -54,6 +54,7 @@ export interface IEndpointTypes { year?: number, semester?: string, exam?: string, + note?: string, approve_status?: boolean, }, response: { diff --git a/frontend/src/types/question_paper.ts b/frontend/src/types/question_paper.ts index 2e6ca6b..2048321 100644 --- a/frontend/src/types/question_paper.ts +++ b/frontend/src/types/question_paper.ts @@ -6,32 +6,33 @@ export type Exam = "midsem" | "endsem" | `ct${number}`; export type Semester = "spring" | "autumn"; export interface IQuestionPaper { - course_code: string; - course_name: string; - year: number; - semester: Semester | ""; - exam: Exam | "ct" | ""; -}; + course_code: string; + course_name: string; + year: number; + note: string; + semester: Semester | ""; + exam: Exam | "ct" | ""; +} export interface ISearchResult extends IQuestionPaper { - id: number; - filelink: string; - from_library: boolean; -}; + id: number; + filelink: string; + from_library: boolean; +} export interface IAdminDashboardQP extends ISearchResult { - upload_timestamp: string; - approve_status: boolean; + upload_timestamp: string; + approve_status: boolean; } export interface IQuestionPaperFile extends IQuestionPaper { - file: File; -}; + file: File; +} export interface IErrorMessage { - courseCodeErr: string | null; - courseNameErr: string | null; - yearErr: string | null; - examErr: string | null; - semesterErr: string | null; -}; \ No newline at end of file + courseCodeErr: string | null; + courseNameErr: string | null; + yearErr: string | null; + examErr: string | null; + semesterErr: string | null; +} diff --git a/frontend/src/utils/autofillData.ts b/frontend/src/utils/autofillData.ts index fa3f216..39da32b 100644 --- a/frontend/src/utils/autofillData.ts +++ b/frontend/src/utils/autofillData.ts @@ -44,7 +44,8 @@ export interface IExtractedDetails { course_code: string | null, year: number | null, exam: Exam | 'ct' | null, - semester: Semester | null + semester: Semester | null, + note: string | null, } export function extractDetailsFromText(text: string): IExtractedDetails { @@ -79,11 +80,21 @@ export function extractDetailsFromText(text: string): IExtractedDetails { const semesterMatch = lines.match(/[^\w]*(spring|autumn)[^\w]*/i); const semester = semesterMatch ? semesterMatch[1].toLowerCase() as Semester : null; + let note = null; + if (lines.toLowerCase().includes('supplementary')) note = 'Supplementary'; + else { + const slotMatch = lines.match(/[^\w]*slot\s+([a-z])[^\w]*/i); + if (slotMatch) { + note = `Slot ${slotMatch[1].toUpperCase()}`; + } + } + return { course_code: courseCode, year, exam: examType, - semester + semester, + note }; } @@ -112,8 +123,7 @@ async function getAutofillDataFromPDF(file: File): Promise { const pdfData = await file.arrayBuffer(); const text = await extractTextFromPDF(pdfData); - const { course_code, year, exam, semester } = extractDetailsFromText(text); - return { course_code, year, exam, semester }; + return extractDetailsFromText(text); } catch (e) { console.log("Error extracting details from PDF: ", e); @@ -121,7 +131,8 @@ async function getAutofillDataFromPDF(file: File): Promise { course_code: null, year: null, exam: null, - semester: null + semester: null, + note: null } } } @@ -130,10 +141,10 @@ export const autofillData = async ( filename: string, file: File, ): Promise => { // Try to extract details from the PDF - const { course_code: pdfCourseCode, year: pdfYear, exam: pdfExam, semester: pdfSemester } = await getAutofillDataFromPDF(file); + const { course_code: pdfCourseCode, year: pdfYear, exam: pdfExam, semester: pdfSemester, note: pdfNote } = await getAutofillDataFromPDF(file); // Try to extract details from the filename const dotIndex = filename.lastIndexOf("."); // Split the filename at the last `.`, ie, remove the extension - const { course_code: filenameCourseCode, year: filenameYear, exam: filenameExam, semester: filenameSemester } = extractDetailsFromText(filename.substring(0, dotIndex)); + const { course_code: filenameCourseCode, year: filenameYear, exam: filenameExam, semester: filenameSemester, note: filenameNote } = extractDetailsFromText(filename.substring(0, dotIndex)); const filenameOrPdfFallback = ( filenameData: T | null, @@ -151,6 +162,7 @@ export const autofillData = async ( const year = filenameOrPdfFallback(filenameYear, pdfYear, validateYear, new Date().getFullYear()); const exam = filenameOrPdfFallback(filenameExam, pdfExam, validateExam, ''); const semester = filenameOrPdfFallback(filenameSemester, pdfSemester, validateSemester, new Date().getMonth() > 7 ? "autumn" : "spring"); + const note = filenameOrPdfFallback(filenameNote, pdfNote, (note) => note !== null, ''); const qpDetails: IQuestionPaper = { course_code, @@ -158,6 +170,7 @@ export const autofillData = async ( exam, semester, course_name: getCourseFromCode(course_code) ?? "Unknown Course", + note, }; return qpDetails;