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

Revert "Revert "Calendar frontend"" #316

Merged
merged 2 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ tblib = "*"
[packages]
dj-database-url = "*"
djangorestframework = "*"
psycopg2 = "*"
psycopg2 = "==2.9.6"
uvloop = "==0.17.0"
sentry-sdk = "*"
django = "==3.1.7"
django-cors-headers = "*"
Expand All @@ -40,7 +41,7 @@ channels = "<3"
channels-redis = "*"
uvicorn = {extras = ["standard"],version = "*"}
gunicorn = "*"
django-scheduler = "*"
django-schedules-ohq = "*"
typing-extensions = "*"
drf-excel = "*"

Expand Down
2,388 changes: 1,293 additions & 1,095 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/ohq/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ def has_permission(self, request, view):

course = Course.objects.get(pk=course_pk)
membership = Membership.objects.filter(course=course, user=request.user).first()

if membership is None:
return False
return membership.is_ta
Expand Down
33 changes: 25 additions & 8 deletions backend/ohq/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ class RuleSerializer(serializers.ModelSerializer):

class Meta:
model = Rule
fields = ("frequency",)
fields = ("frequency", "params")


class EventSerializer(serializers.ModelSerializer):
Expand All @@ -491,7 +491,7 @@ class EventSerializer(serializers.ModelSerializer):
All times are converted to UTC+0
"""

rule = RuleSerializer(required=False)
rule = RuleSerializer(allow_null=True, required=False)
course_id = serializers.IntegerField(required=False)

class Meta:
Expand All @@ -501,6 +501,7 @@ class Meta:
"start",
"end",
"title",
"location",
"description",
"rule",
"end_recurring_period",
Expand All @@ -525,27 +526,43 @@ def update(self, instance, validated_data):
)
or (
"rule" in validated_data
and (rule is None or rule.frequency != validated_data["rule"]["frequency"])
and (
rule is None
or validated_data["rule"] is None
or rule.frequency != validated_data["rule"]["frequency"]
or rule.params != validated_data["rule"]["params"]
)
)
):
if "rule" in validated_data:
rule, _ = Rule.objects.get_or_create(frequency=validated_data["rule"]["frequency"])
if validated_data["rule"] is None:
rule = None
else:
rule, _ = Rule.objects.get_or_create(
frequency=validated_data["rule"]["frequency"],
params=validated_data["rule"]["params"],
)
validated_data.pop("rule")
Occurrence.objects.filter(event=instance).delete()

if "rule" in validated_data:
validated_data.pop("rule")
# can never change course_id, client should create a new event instead
validated_data.pop("course_id")

super().update(instance, validated_data)

instance.rule = rule
instance.save()

Occurrence.objects.filter(event=instance).delete()

return instance

def create(self, validated_data):
course = Course.objects.get(pk=validated_data["course_id"])
rule = None
if "rule" in validated_data:
rule, _ = Rule.objects.get_or_create(frequency=validated_data["rule"]["frequency"])
if "rule" in validated_data and validated_data["rule"] is not None:
rule, _ = Rule.objects.get_or_create(frequency=validated_data["rule"]["frequency"], params = validated_data["rule"]["params"])
validated_data.pop("rule")

validated_data.pop("course_id")
Expand Down Expand Up @@ -573,4 +590,4 @@ class OccurrenceSerializer(serializers.ModelSerializer):

class Meta:
model = Occurrence
fields = ("id", "title", "description", "start", "end", "cancelled", "event")
fields = ("id", "title", "description", "location", "start", "end", "cancelled", "event")
1 change: 1 addition & 0 deletions backend/ohq/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from http.client import HTTPResponse
import math
import re
from datetime import datetime, timedelta
Expand Down
134 changes: 134 additions & 0 deletions frontend/components/Calendar/CalendarCommon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Icon, Modal, SemanticICONS } from "semantic-ui-react";
import React from "react";
import Link from "next/link";
import { Occurrence, UserMembership } from "../../types";
import { dayNames, paramsToDays } from "./calendarUtils";

const IconTextBlock = (props: {
iconName: SemanticICONS;
children: React.JSX.Element;
}) => {
const { iconName, children } = props;

return (
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Icon
size="large"
name={iconName}
style={{ marginRight: "10px" }}
/>
{children}
</div>
);
};

export const EventInfoModal = (props: {
occurrence: Occurrence | null;
membership: UserMembership | undefined;
setOccurrence: (occurrence: Occurrence | null) => void;
}) => {
const { occurrence, membership, setOccurrence } = props;

return (
<Modal
size="tiny"
open={occurrence !== null}
onClose={() => setOccurrence(null)}
>
<Modal.Header>
{`${membership?.course.department} ${membership?.course.courseCode} – ${occurrence?.title}`}
<button
type="button"
style={{
float: "right",
cursor: "pointer",
background: "none",
border: "none",
}}
onClick={() => setOccurrence(null)}
>
<i className="close icon" />
</button>
</Modal.Header>
<Modal.Content>
<Modal.Description>
<IconTextBlock iconName="clock outline">
<span>
{occurrence?.start.toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}{" "}
-{" "}
{occurrence?.start.toDateString() ===
occurrence?.end.toDateString()
? occurrence?.end.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "numeric",
})
: occurrence?.end.toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}
{occurrence?.event.rule && (
<>
<br />
Weekly on{" "}
{paramsToDays(
occurrence.event.rule.params,
0
)
.map((dayNum) => dayNames[dayNum])
.join(", ")}
</>
)}
</span>
</IconTextBlock>
{occurrence?.description && (
<>
<br />
<IconTextBlock iconName="list">
<span style={{ whiteSpace: "pre-wrap" }}>
{occurrence.description}
</span>
</IconTextBlock>
</>
)}
{occurrence?.location && (
<>
<br />
<IconTextBlock iconName="map marker alternate">
<span style={{ whiteSpace: "pre-wrap" }}>
{occurrence.location}
</span>
</IconTextBlock>
</>
)}
<>
<br />
<IconTextBlock iconName="linkify">
<Link
href="/courses/[course]"
as={`/courses/${occurrence?.event.course_id}`}
legacyBehavior
>
Go to queue
</Link>
</IconTextBlock>
</>
</Modal.Description>
</Modal.Content>
</Modal>
);
};
74 changes: 74 additions & 0 deletions frontend/components/Calendar/DashboardCalendar/EventCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Grid, Header, Segment, SemanticCOLORS } from "semantic-ui-react";
import { useState } from "react";
import { Course, Occurrence } from "../../../types";

interface EventCardProps {
occurrence: Occurrence;
course: Course;
color: SemanticCOLORS;
onClick: () => void;
}

const EventCard = (props: EventCardProps) => {
const { occurrence, course, color, onClick } = props;

const startDate = new Date(occurrence.start);
const endDate = new Date(occurrence.end);

const [hover, setHover] = useState(false);

const formatDate = (date: Date) =>
date.toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true,
});

return (
<Segment
color={color as SemanticCOLORS}
style={{
cursor: "pointer",
title: occurrence.description,
}}
raised={hover}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={onClick}
>
{/* TODO: get rid of hardcoded width */}
<Grid style={{ width: "240px" }}>
<Grid.Column width={10}>
<Header
as="h4"
style={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{`${course.department} ${course.courseCode}`}
<Header.Subheader
style={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{occurrence.title}
</Header.Subheader>
</Header>
</Grid.Column>
<Grid.Column width={6} textAlign="right">
<Header as="h5">
{formatDate(startDate)}
<br />
{formatDate(endDate)}
</Header>
</Grid.Column>
</Grid>
</Segment>
);
};

export default EventCard;
Loading
Loading