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

Create a cleaner way of managing course status #52

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion app/controllers/relationships_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def create
user.subscribe_to_section(section)
render(json: { msg: "Subscribed" })
else
user.follow(section)
user.follow_section(section)
render(json: { msg: "Followed" })
end
end
Expand Down
43 changes: 42 additions & 1 deletion app/models/relationship.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,49 @@ class Relationship < ApplicationRecord
belongs_to :section
has_one :review, dependent: :restrict_with_exception

enum stored_status: {
planned: "planned",
subscribed: "subscribed",
enrolled: "enrolled",
}

class Status < T::Enum
enums do
# A relationship can be in one of the following states:
Planned = new("planned") # A user has put this section into their class plan for a quarter
Subscribed = new("subscribed") # The section is in a class plan and the user wants enrollment notifications for the section
Enrolled = new("enrolled") # The section has been enrolled in. No need to send further notifications.

Completed = new("completed") # Where enrolled sections go after the quarter ends. The user has taken the course!
Reviewed = new("reviewed") # The user has taken the section and reviewed it.

# How does Status differ from stored_status above? We track the first three states (planned, subscribed, enrolled) in the database on this relationship model. The other two states (completed, reviewed) are derived based off other information (term calendar and if a review was written, respectively).
end
end

sig { returns(Status) }
def status
if reviewed?
Status::Reviewed
elsif completed?
Status::Completed
else
Status.deserialize(stored_status)
end
end

sig { returns(T::Boolean) }
def review?
def reviewed?
review.present?
end

sig { returns(T::Boolean) }
def completed?
section&.term&.enrollment_period == Term::EnrollmentPeriod::Post
end

sig { returns(T::Boolean) }
def reviewed_or_completed?
reviewed? || completed?
end
end
20 changes: 14 additions & 6 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,25 @@ def self.from_auth_payload(payload, referral_code:)

# Marks a section as being followed by the given user.
sig { params(section: Section).void }
def follow(section)
def follow_section(section)
sections << section unless following?(section)
end

# Marks a section as being followed with notifications by the given user.
sig { params(section: Section).void }
def subscribe_to_section(section)
follow(section)
relationship = relationships.find_by(section:)
relationship&.update(notify: true) if use_notification_token
follow_section(section)
relationship = relationships.find_by!(section:)
raise ArgumentError, "Section cannot be subscribed to, it's past enrollment" if relationship.completed?

relationship.subscribed! if use_notification_token
end

# Whether a user is subscribed to a given section.
sig { params(section: Section).returns(T::Boolean) }
def subscribed_to_section?(section)
relationship = relationships.find_by(section:)
relationship&.notify? || false
!!relationship&.subscribed?
end

# Whether a user is following a given section.
Expand All @@ -138,11 +140,17 @@ def following?(section)
sections.include?(section)
end

sig { params(section: Section).returns(Relationship::Status) }
def section_status(section)
relationship = relationships.find_by!(section:)
relationship.status
end

# Stops following a section the user was following.
sig { params(section: Section).void }
def unfollow(section)
relationship = relationships.find_by(section:)
raise ActiveRecord::RecordNotDestroyed, "Section is reviewed by user, cannot unfollow." if relationship&.review?
raise ActiveRecord::RecordNotDestroyed, "Section is reviewed by user, cannot unfollow." if relationship&.reviewed?

sections.delete(section)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# typed: true
# frozen_string_literal: true

class MoveNotifyBooleanToRelationshipStatus < ActiveRecord::Migration[7.0]
def up
Relationship.all.find_each do |relationship|
# We have no way of telling whether a user enrolled in a section -- that's a new state!
relationship.stored_status = if relationship.notify
:subscribed
else
:planned
end
relationship.save!
end
end

def down
Relationship.update_all(stored_status: nil)
end
end
1 change: 1 addition & 0 deletions db/data_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DataMigrate::Data.define(version: 20230827195318)
25 changes: 25 additions & 0 deletions db/migrate/20230827154404_track_relationship_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# typed: true
# frozen_string_literal: true

class TrackRelationshipStatus < ActiveRecord::Migration[7.0]
def change
reversible do |dir|
dir.up do
execute(<<-SQL.squish)
CREATE TYPE relationship_status AS ENUM ('planned', 'subscribed', 'enrolled');
SQL
end

dir.down do
execute(<<-SQL.squish)
DROP TYPE relationship_status;
SQL
end
end

change_table(:relationships, bulk: true) do |t|
# Will make this non-null in a future migration
t.column(:stored_status, :relationship_status, default: "planned")
end
end
end
34 changes: 32 additions & 2 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ CREATE TYPE public.grade_type AS ENUM (
);


--
-- Name: relationship_status; Type: TYPE; Schema: public; Owner: -
--

CREATE TYPE public.relationship_status AS ENUM (
'planned',
'subscribed',
'enrolled'
);


--
-- Name: review_status; Type: TYPE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -521,6 +532,15 @@ CREATE TABLE public.courses_terms (
);


--
-- Name: data_migrations; Type: TABLE; Schema: public; Owner: -
--

CREATE TABLE public.data_migrations (
version character varying NOT NULL
);


--
-- Name: enrollment_appointments; Type: TABLE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -970,7 +990,8 @@ CREATE TABLE public.relationships (
section_id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
notify boolean DEFAULT false NOT NULL
notify boolean DEFAULT false NOT NULL,
stored_status public.relationship_status DEFAULT 'planned'::public.relationship_status
);


Expand Down Expand Up @@ -1633,6 +1654,14 @@ ALTER TABLE ONLY public.courses
ADD CONSTRAINT courses_pkey PRIMARY KEY (id);


--
-- Name: data_migrations data_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--

ALTER TABLE ONLY public.data_migrations
ADD CONSTRAINT data_migrations_pkey PRIMARY KEY (version);


--
-- Name: enrollment_appointments enrollment_appointments_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
Expand Down Expand Up @@ -2517,6 +2546,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230430161016'),
('20230511033013'),
('20230718192047'),
('20230820174337');
('20230820174337'),
('20230827154404');


Loading