Skip to content

Commit

Permalink
Add delete review feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ankith26 committed Dec 14, 2024
1 parent 1969843 commit c0e4188
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 29 deletions.
10 changes: 10 additions & 0 deletions backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Review(BaseModel):
rating: Literal[1, 2, 3, 4, 5]
content: str = Field(..., min_length=1, max_length=MSG_MAX_LEN)
dtime: AwareDatetime = Field(default_factory=lambda: datetime.now(timezone.utc))

# TODO: upvote/downvote system
# upvoters: set[str] # set of student emails
# downvoters: set[str] # set of student emails
Expand All @@ -49,6 +50,15 @@ def convert_naive_to_aware(cls, values):
return values


class ReviewFrontend(Review):
"""
This represents a Review as it is seen from the frontend. Some attributes
with the backend are common, but some are not.
"""

is_reviewer: bool


class Member(BaseModel):
"""
Base class for representing a Member, can be a Student or Prof
Expand Down
25 changes: 21 additions & 4 deletions backend/routes/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from routes.members import prof_exists
from config import db
from utils import get_auth_id, get_auth_id_admin
from models import Course, Review, Sem, CourseCode
from models import Course, Review, ReviewFrontend, Sem, CourseCode

# The get_auth_id Dependency validates authentication of the caller
router = APIRouter(dependencies=[Depends(get_auth_id)])
Expand All @@ -19,7 +19,7 @@ async def course_list(
prof_filter: EmailStr | None = None,
):
"""
List all courses.
List all courses.
This does not return the reviews attribute, that must be queried individually.
Can optionally pass filters for:
- course semester
Expand Down Expand Up @@ -74,7 +74,9 @@ async def course_post(courses: list[Course]):


@router.get("/reviews/{sem}/{code}")
async def course_reviews_get(sem: Sem, code: CourseCode):
async def course_reviews_get(
sem: Sem, code: CourseCode, auth_id: str = Depends(get_auth_id)
):
"""
Helper to return all reviews under a given course.
This function returns None if the course does not exist
Expand All @@ -86,7 +88,8 @@ async def course_reviews_get(sem: Sem, code: CourseCode):
return None

return [
Review(**i).model_dump() for i in course_reviews.get("reviews", {}).values()
ReviewFrontend(**v, is_reviewer=(k == auth_id)).model_dump()
for k, v in course_reviews.get("reviews", {}).items()
]


Expand All @@ -103,3 +106,17 @@ async def course_reviews_post(
{"sem": sem, "code": code},
{"$set": {f"reviews.{auth_id}": review.model_dump()}},
)


@router.delete("/reviews/{sem}/{code}")
async def course_reviews_delete(
sem: Sem, code: CourseCode, auth_id: str = Depends(get_auth_id)
):
"""
Helper to delete a review posted by the authenticated user on a professor.
If the user hasn't posted a review, no action will be taken.
"""
await course_collection.update_one(
{"sem": sem, "code": code},
{"$unset": {f"reviews.{auth_id}": ""}}
)
20 changes: 17 additions & 3 deletions backend/routes/members.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from config import db
from utils import get_auth_id, get_auth_id_admin
from models import Prof, Review, Student
from models import Prof, Review, ReviewFrontend, Student

# The get_auth_id Dependency validates authentication of the caller
router = APIRouter(dependencies=[Depends(get_auth_id)])
Expand Down Expand Up @@ -54,7 +54,7 @@ async def prof_post(profs: list[Prof]):


@router.get("/reviews/{email}")
async def prof_reviews_get(email: EmailStr):
async def prof_reviews_get(email: EmailStr, auth_id: str = Depends(get_auth_id)):
"""
Helper to return all reviews under a given Prof email.
This function returns None if the prof does not exist
Expand All @@ -65,7 +65,10 @@ async def prof_reviews_get(email: EmailStr):
if not prof_reviews:
return None

return [Review(**i).model_dump() for i in prof_reviews.get("reviews", {}).values()]
return [
ReviewFrontend(**v, is_reviewer=(k == auth_id)).model_dump()
for k, v in prof_reviews.get("reviews", {}).items()
]


@router.post("/reviews/{email}")
Expand All @@ -82,6 +85,17 @@ async def prof_reviews_post(
)


@router.delete("/reviews/{email}")
async def prof_reviews_delete(email: EmailStr, auth_id: str = Depends(get_auth_id)):
"""
Helper to delete a review posted by the authenticated user on a professor.
If the user hasn't posted a review, no action will be taken.
"""
await profs_collection.update_one(
{"email": email}, {"$unset": {f"reviews.{auth_id}": ""}}
)


async def student_hash(user: Student):
"""
Internal function to hash a Student object. This hash is used as a review key
Expand Down
26 changes: 26 additions & 0 deletions frontend/package-lock.json

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

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.2.0",
"@mui/material": "^6.2.0",
"@vitejs/plugin-react": "^4.3.4",
"axios": "^1.7.9",
Expand Down
122 changes: 100 additions & 22 deletions frontend/src/components/ReviewBox.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,112 @@ import {
Typography,
Box,
Rating,
IconButton,
useTheme,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Button,
} from '@mui/material';
import DeleteIcon from "@mui/icons-material/Delete";

import theme from '../theme';
import ReviewInput from './ReviewInput';

import { api } from '../api';

const Review = ({ datetime, rating, message }) => {
const formattedDate = new Date(datetime).toLocaleString();

const Review = ({ review, endpoint, onUpdate }) => {
const theme = useTheme(); // Access the theme

const [openDialog, setOpenDialog] = useState(false); // State for the dialog

const formattedDate = new Date(review.dtime).toLocaleString();

const handleDelete = async () => {
try {
await api.delete(endpoint);
await onUpdate();
setOpenDialog(false); // Close dialog after deletion
} catch (error) {
// TODO: convey message to frontend
console.error("Error deleting the review:", error);
}
};

const handleDialogClose = () => {
setOpenDialog(false);
};

const handleDialogOpen = () => {
setOpenDialog(true);
};

return (
<Card variant="outlined" sx={{ margin: 2 }}>
<CardContent>
<Box display="flex" alignItems="center" sx={{ marginTop: 1 }}>
<Rating value={rating} readOnly precision={1} />
<Typography
variant="body2"
color="text.secondary"
sx={{ marginLeft: 1 }}
>
{formattedDate}
<>
<Card
variant="outlined"
sx={{
margin: 2,
backgroundColor: review.is_reviewer
? theme.palette.action.hover
: theme.palette.background.paper,
border: review.is_reviewer
? `1px solid ${theme.palette.secondary.main}`
: `1px solid ${theme.palette.divider}`,
}}
>
<CardContent>
{review.is_reviewer && (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Typography
variant="subtitle2"
color="text.secondary"
sx={{ fontStyle: 'italic' }}
>
Your review (this is only visible to you)
</Typography>
<IconButton onClick={handleDialogOpen} color="error" size="small">
<DeleteIcon />
</IconButton>
</Box>
)}
<Box display="flex" alignItems="center" sx={{ marginTop: 1 }}>
<Rating value={review.rating} readOnly precision={1} />
<Typography
variant="body2"
color="text.secondary"
sx={{ marginLeft: 1 }}
>
{formattedDate}
</Typography>
</Box>
<Typography variant="body1" sx={{ marginTop: 1 }}>
{review.content}
</Typography>
</Box>
<Typography variant="body1" sx={{ marginTop: 1 }}>
{message}
</Typography>
</CardContent>
</Card>
</CardContent>
</Card>

{/* Confirmation Dialog */}
<Dialog
open={openDialog}
onClose={handleDialogClose}
>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogContent>
<Typography>Are you sure you want to delete this review?</Typography>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogClose} color="primary">
Cancel
</Button>
<Button onClick={handleDelete} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
</>
);
};

Expand Down Expand Up @@ -86,10 +165,9 @@ const ReviewBox = ({ children, endpoint }) => {
) : (
reviewsList.map((review, index) => (
<Review
key={index}
datetime={review.dtime}
rating={review.rating}
message={review.content}
review={review}
endpoint={endpoint}
onUpdate={fetchReviews}
/>
))
)}
Expand Down

0 comments on commit c0e4188

Please sign in to comment.