Skip to content

Commit

Permalink
Merge pull request #107 from metakgp/notes
Browse files Browse the repository at this point in the history
Notes
  • Loading branch information
harshkhandeparkar authored Jan 15, 2025
2 parents 5ca37a1 + 06c8f45 commit 6aae7cb
Show file tree
Hide file tree
Showing 17 changed files with 226 additions and 52 deletions.
4 changes: 4 additions & 0 deletions backend/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ impl Database {
semester,
exam,
approve_status,
note,
} = edit_req;

let current_details = self.get_paper_by_id(id).await?;
Expand Down Expand Up @@ -171,6 +172,7 @@ impl Database {
.bind(year)
.bind(&semester)
.bind(&exam)
.bind(&note)
.bind(approve_status)
.bind(&new_filelink);

Expand Down Expand Up @@ -258,6 +260,7 @@ impl Database {
year,
exam,
semester,
note,
..
} = file_details;

Expand All @@ -267,6 +270,7 @@ impl Database {
.bind(year)
.bind(exam)
.bind(semester)
.bind(note)
.bind("placeholder_filelink")
.bind(false);

Expand Down
2 changes: 2 additions & 0 deletions backend/src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct DBBaseQP {
year: i32,
semester: String,
exam: String,
note: String,
}

#[derive(FromRow, Clone)]
Expand Down Expand Up @@ -52,6 +53,7 @@ impl From<DBBaseQP> 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,
}
}
}
24 changes: 13 additions & 11 deletions backend/src/db/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
///
Expand All @@ -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
)
}
Expand Down Expand Up @@ -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";
5 changes: 3 additions & 2 deletions backend/src/qp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ impl From<Exam> for String {
}

#[duplicate_item(
ExamSem;
Serializable;
[ Exam ];
[ Semester ];
)]
impl Serialize for ExamSem {
impl Serialize for Serializable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
Expand All @@ -134,6 +134,7 @@ pub struct BaseQP {
pub year: i32,
pub semester: Semester,
pub exam: Exam,
pub note: String,
}

#[derive(Serialize, Clone)]
Expand Down
2 changes: 2 additions & 0 deletions backend/src/routing/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ pub struct EditReq {
pub year: Option<i32>,
pub semester: Option<String>,
pub exam: Option<String>,
pub note: Option<String>,
pub approve_status: Option<bool>,
}

Expand Down Expand Up @@ -216,6 +217,7 @@ pub struct FileDetails {
pub exam: String,
pub semester: String,
pub filename: String,
pub note: String,
}

/// 10 MiB file size limit
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/AdminDashboard/QPCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function QPCard({ qPaper, onEdit, onDelete, hasOcr }: IQPCardProps) {
<div className="pill">{qPaper.year}</div>
<div className="pill">{qPaper.exam}</div>
<div className="pill">{qPaper.semester}</div>
{qPaper.note !== "" && <div className="pill">{qPaper.note}</div>}
</div>
{!isValid &&
<p className="error-msg">{Object.values(errorMsg).filter((msg) => msg !== null).join(', ')}</p>
Expand Down
32 changes: 26 additions & 6 deletions frontend/src/components/Common/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,45 @@ export function Select(props: ISelectProps) {

interface INumberInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
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<HTMLButtonElement>) => {
e.preventDefault();
const newValue = props.value + change;
let newValue = props.value + change;

if (alphabeticalInput) newValue = clampAlphabet(newValue);

props.setValue(isNaN(newValue) ? 1 : newValue);
}
}

return <div className="number-input">
<input
type="number"
{...props}
type={alphabeticalInput ? 'text' : 'number'}
value={alphabeticalInput ? String.fromCharCode(props.value) : props.value}
onChange={(e) => {
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}
/>
<div className="number-input-controls">
<button className="btn inc" onClick={getClickHandler(1)}><FaChevronUp size="0.7rem" /></button>
Expand All @@ -108,7 +128,7 @@ interface ISuggestionTextInputProps<T> {
placeholder?: string;
onSuggestionSelect?: (sugg: ISuggestion<T>) => void;
onValueChange: (newValue: string) => void;
inputProps: React.InputHTMLAttributes<HTMLInputElement> | {};
inputProps?: React.InputHTMLAttributes<HTMLInputElement> | {};
}
export function SuggestionTextInput<T>(props: ISuggestionTextInputProps<T>) {
const [suggShown, setSuggShown] = useState<boolean>(false);
Expand Down Expand Up @@ -161,7 +181,7 @@ export function SuggestionTextInput<T>(props: ISuggestionTextInputProps<T>) {
onKeyDown={handleKeyDown}
>
<input
{...props.inputProps}
{...(props.inputProps ?? {})}
type="text"
className="sugg-text-input"
value={props.value}
Expand Down
68 changes: 67 additions & 1 deletion frontend/src/components/Common/PaperEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { validate, validateCourseCode, validateExam, validateSemester, validateY
import { Exam, IAdminDashboardQP, IErrorMessage, IQuestionPaperFile, Semester } from "../../types/question_paper";
import { IExtractedDetails } from "../../utils/autofillData";
import './styles/paper_edit_modal.scss';
import { FaArrowLeft, FaArrowRight, FaFilePdf } from "react-icons/fa6";
import { FaArrowLeft, FaArrowRight, FaBan, FaFilePdf } from "react-icons/fa6";
import Spinner from "../Spinner/Spinner";
import { FormGroup, RadioGroup, NumberInput, SuggestionTextInput, ISuggestion } from "./Form";

Expand All @@ -16,6 +16,7 @@ import { IEndpointTypes } from "../../types/backend";
import { useAuthContext } from "../../utils/auth";
import { QPCard } from "../AdminDashboard/QPCard";
import { IoClose } from "react-icons/io5";
import { FaCalendarAlt, FaSync } from "react-icons/fa";

type UpdateQPHandler<T> = (qp: T) => void;
interface IPaperEditModalProps<T> {
Expand Down Expand Up @@ -317,6 +318,71 @@ function PaperEditModal<T extends IQuestionPaperFile | IAdminDashboardQP>(props:
onSelect={(value: Semester) => changeData('semester', value)}
/>
</FormGroup>
<FormGroup
label="Additional Note:"
validationError={null}
>
<div className="additional-note">
<div className="note-options">
<button
className={`note-option none ${data.note === '' ? 'enabled' : ''}`}
onClick={(e) => {
e.preventDefault();
changeData('note', '');
}}
>
<FaBan /> None
</button>
<button
className={`note-option ${data.note === 'Supplementary' ? 'enabled' : ''}`}
onClick={(e) => {
e.preventDefault();
changeData('note', 'Supplementary');
}}
>
<FaSync /> Supplementary Exam
</button>
<button
className={`note-option ${data.note.match(/^Slot [A-Z]$/) !== null ? 'enabled' : ''}`}
onClick={(e) => {
e.preventDefault();
changeData('note', 'Slot A');
}}
>
<FaCalendarAlt /> Multiple Slots
</button>
</div>
<div className="note-customize">
{
data.note.match(/^Slot [A-Z]$/) &&
<div>
<label>Slot:</label>
<NumberInput
alphabetical={true}
value={data.note.charCodeAt(data.note.length - 1)}
setValue={(value) => {
console.log('changing', value, String.fromCharCode(value))
changeData('note', `Slot ${String.fromCharCode(value)}`)
}
}
/>
</div>
}
{
'approve_status' in data &&
<div>
<label>Custom Note:</label>
<SuggestionTextInput
placeholder="Custom Note"
value={data.note}
onValueChange={(value) => changeData('note', value)}
suggestions={[]}
/>
</div>
}
</div>
</div>
</FormGroup>

{
'approve_status' in data &&
Expand Down
59 changes: 57 additions & 2 deletions frontend/src/components/Common/styles/paper_edit_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -134,7 +188,8 @@
}
}

&.next-btn, &.prev-btn {
&.next-btn,
&.prev-btn {
background-color: $accent-color;

&:hover {
Expand Down
Loading

0 comments on commit 6aae7cb

Please sign in to comment.