Skip to content

Commit

Permalink
Task/CSXL-653: Update Recurring Events (#683)
Browse files Browse the repository at this point in the history
* (task:CSXL-653) implement functionality for updating recurring events

* (fix:CSXL-653) resolve failing tests
  • Loading branch information
jadekeegan authored Feb 4, 2025
1 parent 77e71c8 commit 0c7ab03
Show file tree
Hide file tree
Showing 16 changed files with 244 additions and 38 deletions.
20 changes: 18 additions & 2 deletions backend/api/office_hours/office_hours.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from fastapi import APIRouter, Depends

from ...models.office_hours.office_hours_details import PrimaryOfficeHoursDetails

from ...models.office_hours.office_hours_recurrence_pattern import (
NewOfficeHoursRecurrencePattern,
)
Expand Down Expand Up @@ -122,6 +124,20 @@ def update_office_hours(
return oh_event_svc.update(subject, site_id, oh)


@api.put("/{site_id}/recurring", tags=["Office Hours"])
def update_recurring_office_hours(
site_id: int,
oh: OfficeHours,
recur: NewOfficeHoursRecurrencePattern,
subject: User = Depends(registered_user),
oh_event_recurrence_svc: OfficeHoursRecurrenceService = Depends(),
) -> list[OfficeHours]:
"""
Updates an existing office hours event and future events in the recurrence pattern.
"""
return oh_event_recurrence_svc.update_recurring(subject, site_id, oh, recur)


@api.delete("/{site_id}/{oh_id}", tags=["Office Hours"])
def delete_office_hours(
site_id: int,
Expand All @@ -143,7 +159,7 @@ def delete_recurring_office_hours(
oh_event_recurrence_svc: OfficeHoursRecurrenceService = Depends(),
):
"""
Deletes an existing office hours event and future events in the reucrrence pattern.
Deletes an existing office hours event and future events in the recurrence pattern.
"""
oh_event_recurrence_svc.delete_recurring(subject, site_id, oh_id)

Expand All @@ -154,7 +170,7 @@ def get_office_hours(
oh_id: int,
subject: User = Depends(registered_user),
oh_event_svc: OfficeHoursService = Depends(),
) -> OfficeHours:
) -> PrimaryOfficeHoursDetails:
"""
Gets office hours.
"""
Expand Down
28 changes: 27 additions & 1 deletion backend/entities/office_hours/office_hours_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from sqlalchemy import DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ...models.office_hours.office_hours import OfficeHours, NewOfficeHours
from ...models.office_hours.office_hours_details import OfficeHoursDetails
from ...models.office_hours.office_hours_details import (
OfficeHoursDetails,
PrimaryOfficeHoursDetails,
)

from ...models.office_hours.event_type import (
OfficeHoursEventModeType,
Expand Down Expand Up @@ -147,6 +150,29 @@ def to_model(self) -> OfficeHours:
recurrence_pattern_id=self.recurrence_pattern_id,
)

def to_primary_details_model(self) -> PrimaryOfficeHoursDetails:
"""
Converts a `OfficeHoursEntity` object into a `PrimaryOfficeHoursDetails` model object
Returns:
OfficeHours: `OfficeHours` object from the entity
"""
return PrimaryOfficeHoursDetails(
id=self.id,
type=self.type,
mode=self.mode,
description=self.description,
location_description=self.location_description,
start_time=self.start_time,
end_time=self.end_time,
course_site_id=self.course_site_id,
room_id=self.room_id,
recurrence_pattern_id=self.recurrence_pattern_id,
recurrence_pattern=(
self.recurrence_pattern.to_model() if self.recurrence_pattern else None
),
)

def to_details_model(self) -> OfficeHoursDetails:
"""
Converts a `OfficeHoursEntity` object into a `OfficeHoursDetails` model object
Expand Down
2 changes: 2 additions & 0 deletions backend/models/office_hours/office_hours.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from zoneinfo import ZoneInfo
from enum import Enum
from pydantic import BaseModel, field_validator, ValidationInfo

from .office_hours_recurrence_pattern import OfficeHoursRecurrencePattern
from .event_type import OfficeHoursEventModeType, OfficeHoursEventType

__authors__ = [
Expand Down
6 changes: 5 additions & 1 deletion backend/models/office_hours/office_hours_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
__license__ = "MIT"


class PrimaryOfficeHoursDetails(OfficeHours):
recurrence_pattern: OfficeHoursRecurrencePattern | None = None


class OfficeHoursDetails(OfficeHours):
"""
Pydantic model to represent an `OfficeHours`, including back-populated
Expand All @@ -29,5 +33,5 @@ class OfficeHoursDetails(OfficeHours):

course_site: CourseSite
room: Room
recurrence_pattern: OfficeHoursRecurrencePattern
recurrence_pattern: OfficeHoursRecurrencePattern | None = None
tickets: list[OfficeHoursTicket]
6 changes: 4 additions & 2 deletions backend/services/office_hours/office_hours.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from fastapi import Depends
from sqlalchemy import select, exists, and_, func
from sqlalchemy.orm import Session, joinedload, selectinload

from ...models.office_hours.office_hours_details import PrimaryOfficeHoursDetails
from ...database import db_session
from ...models.user import User
from ...models.academics.section_member import RosterRole
Expand Down Expand Up @@ -373,7 +375,7 @@ def delete(self, user: User, site_id: int, event_id: int):
self._session.delete(office_hours_entity)
self._session.commit()

def get(self, user: User, site_id: int, event_id: int) -> OfficeHours:
def get(self, user: User, site_id: int, event_id: int) -> PrimaryOfficeHoursDetails:
"""
Gets an existing office hours event.
"""
Expand All @@ -388,7 +390,7 @@ def get(self, user: User, site_id: int, event_id: int) -> OfficeHours:
# Check permissions
self._check_site_admin_permissions(user, site_id)

return office_hours_entity.to_model()
return office_hours_entity.to_primary_details_model()

def _check_site_admin_permissions(self, user: User, site_id: int):

Expand Down
56 changes: 44 additions & 12 deletions backend/services/office_hours/office_hours_recurrence.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ def create_recurring(
# Check permissions
self._office_hours_svc._check_site_admin_permissions(user, site_id)

# Create events
new_events = self.create_events(event, recurrence_pattern)

# Commit changes
self._session.commit()
return [entity.to_model() for entity in new_events]

def create_events(
self, event: NewOfficeHours, recurrence_pattern: NewOfficeHoursRecurrencePattern
):
# Create recurrence entity
recurrence_pattern_entity = OfficeHoursRecurrencePatternEntity.from_new_model(
recurrence_pattern
Expand Down Expand Up @@ -124,22 +134,49 @@ def create_recurring(
# Increment date
current_date += timedelta(days=1)

# commit changes
self._session.commit()

result = [entity.to_model() for entity in new_events]

if len(result) == 0:
if len(new_events) == 0:
raise RecurringOfficeHourEventException(
"Cannot create any with the given recurrence pattern before the recurrence end date."
)

return result
return new_events

def update_recurring(
self,
user: User,
site_id: int,
event: OfficeHours,
recurrence_pattern: NewOfficeHoursRecurrencePattern,
):
"""
Updates an existing office hours event and future events in the recurrence pattern.
"""
# Check permissions
self._office_hours_svc._check_site_admin_permissions(user, site_id)

# Delete all future events.
self.delete_events(event.id)

# Recreate events according to the new recurrence pattern.
new_events = self.create_events(event, recurrence_pattern)

# Commit changes
self._session.commit()

return [entity.to_model() for entity in new_events]

def delete_recurring(self, user: User, site_id: int, event_id: int):
"""
Deletes an existing office hours event and future events in the event's recurrence pattern.
"""
# Check permissions
self._office_hours_svc._check_site_admin_permissions(user, site_id)

self.delete_events(event_id)

self._session.commit()

def delete_events(self, event_id: int):
# Find existing event
office_hours_entity = self._session.get(OfficeHoursEntity, event_id)

Expand All @@ -148,9 +185,6 @@ def delete_recurring(self, user: User, site_id: int, event_id: int):
"Office hours event with id: {event_id} does not exist."
)

# Check permissions
self._office_hours_svc._check_site_admin_permissions(user, site_id)

# Find future events in recurrence pattern
start_date = (
office_hours_entity.start_time.date()
Expand All @@ -172,5 +206,3 @@ def delete_recurring(self, user: User, site_id: int, event_id: int):

for entity in future_event_entities:
self._session.delete(entity)

self._session.commit()
26 changes: 20 additions & 6 deletions backend/test/services/office_hours/office_hours_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,6 @@ def insert_fake_data(session: Session):
session.commit()

# Step 3: Add office hours to database
for pattern in recurrence_patterns:
recurrence_pattern_entity = OfficeHoursRecurrencePatternEntity.from_model(
pattern
)
session.add(recurrence_pattern_entity)

for oh in office_hours:
office_hours_entity = OfficeHoursEntity.from_model(oh)
session.add(office_hours_entity)
Expand All @@ -336,6 +330,14 @@ def insert_fake_data(session: Session):
len(office_hours) + 1,
)

session.commit()

for pattern in recurrence_patterns:
recurrence_pattern_entity = OfficeHoursRecurrencePatternEntity.from_model(
pattern
)
session.add(recurrence_pattern_entity)

reset_table_id_seq(
session,
OfficeHoursRecurrencePatternEntity,
Expand Down Expand Up @@ -534,6 +536,18 @@ def fake_data_fixture(session: Session):
recur_sunday=True,
)

updated_recurrence_pattern = NewOfficeHoursRecurrencePattern(
start_date=datetime.now(),
end_date=datetime.now() + timedelta(days=14),
recur_monday=True,
recur_tuesday=False,
recur_wednesday=True,
recur_thursday=False,
recur_friday=True,
recur_saturday=True,
recur_sunday=True,
)

invalid_recurrence_pattern_days = NewOfficeHoursRecurrencePattern(
start_date=datetime.now(),
end_date=datetime.now() + timedelta(days=14),
Expand Down
15 changes: 15 additions & 0 deletions backend/test/services/office_hours/office_hours_recurrence_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ def test_create_recurring_oh_event_invalid_recurrence_end(
pytest.fail()


def test_update_recurring_oh_event_instructor(
oh_recurrence_svc: OfficeHoursRecurrenceService,
):
"""Ensures that instructors can modify recurring office hours events."""
modified_events = oh_recurrence_svc.update_recurring(
user_data.instructor,
office_hours_data.comp_110_site.id,
office_hours_data.first_recurring_event,
office_hours_data.updated_recurrence_pattern,
)

assert len(modified_events) == 10
assert modified_events[0].recurrence_pattern_id is not None


def test_delete_recurring_oh_event_instructor(
oh_recurrence_svc: OfficeHoursRecurrenceService,
):
Expand Down
1 change: 1 addition & 0 deletions backend/test/services/office_hours/office_hours_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def test_get_oh_event_role_not_member(oh_svc: OfficeHoursService):

def test_create_oh_event_instructor(oh_svc: OfficeHoursService):
"""Ensures that instructors can create office hour events."""
office_hours_data.new_event.recurrence_pattern_id = None
new_event = oh_svc.create(
user_data.instructor,
office_hours_data.comp_110_site.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ mat-form-field {
gap: 12px;
}

.recurrence-update-toggle {
/* Move up to account */
margin-top: -20px;
}

.time-form-fields-container {
display: flex;
flex-direction: row;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
</mat-form-field>
</div>

<!-- Recurrence Pattern Update Options -->
@if (!isNew()) {
<mat-checkbox className="recurrence-update-toggle" (change)="this.toggleUpdateRecurrencePattern($event.checked)">Update future events in recurrence pattern.</mat-checkbox>
}

<!-- Recurrence Pattern Field -->
<mat-form-field appearance="outline">
<mat-label>Recurrence Pattern</mat-label>
Expand All @@ -104,8 +109,8 @@
[hideMultipleSelectionIndicator]="true"
multiple
>
@for (day of this.days | keyvalue: maintainOriginalOrder; track day) {
<mat-button-toggle (change)="toggleDay(day.key)" value="{{day.key}}" [disabled]="!isNew()">{{day.key}}</mat-button-toggle>
@for (day of this.days | keyvalue: maintainOriginalOrder; track day.key) {
<mat-button-toggle (change)="toggleDay(day.key)" value="{{day.key}}" [checked]="this.days[day.key]" [disabled]="!this.isNew() && !this.updateRecurrencePattern">{{day.key}}</mat-button-toggle>
}
</mat-button-toggle-group>

Expand Down
Loading

0 comments on commit 0c7ab03

Please sign in to comment.