Skip to content

Commit

Permalink
V2 of login prompts for mitx online/edx
Browse files Browse the repository at this point in the history
  • Loading branch information
rhysyngsun committed Sep 17, 2021
1 parent d2e389c commit 9a78149
Show file tree
Hide file tree
Showing 19 changed files with 263 additions and 110 deletions.
26 changes: 25 additions & 1 deletion courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,23 @@ def __str__(self):
return self.name


class ProgramQuerySet(models.QuerySet):
"""
Custom QuerySet for Programs
"""
def prefetch_course_runs(self):
"""Returns a new query that prefetches the course runs"""
return self.prefetch_related(
models.Prefetch("course_set__courserun_set", to_attr="course_runs")
)


class Program(TimestampedModel):
"""
A degree someone can pursue, e.g. "Supply Chain Management"
"""
objects = ProgramQuerySet.as_manager()

title = models.CharField(max_length=255)
live = models.BooleanField(default=False)
description = models.TextField(blank=True, null=True)
Expand All @@ -54,11 +67,22 @@ def has_frozen_grades_for_all_courses(self):
"""
return all([course.has_frozen_runs() for course in self.course_set.all()])

@property
def course_runs(self):
""" Return the set of course runs """
return CourseRun.objects.filter(course__program=self)

@property
def courseware_backends(self):
""" Return the set of courseware backends """
return list({run.courseware_backend for run in self.course_runs})

@property
def has_mitxonline_courses(self):
"""
Return true if as least one course has at least one run that is on mitxonline
"""
return CourseRun.objects.filter(course__program=self, courseware_backend=BACKEND_MITX_ONLINE).exists()
return BACKEND_MITX_ONLINE in self.courseware_backends


class Course(models.Model):
Expand Down
1 change: 1 addition & 0 deletions courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class Meta:
'enrolled',
'total_courses',
'topics',
'courseware_backends',
)


Expand Down
21 changes: 13 additions & 8 deletions courses/serializers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def setUpTestData(cls):
super().setUpTestData()

cls.program = ProgramFactory.create()
cls.course_run = CourseRunFactory.create(course__program=cls.program)
cls.user = UserFactory.create()
cls.context = {
"request": Mock(user=cls.user)
Expand All @@ -97,8 +98,9 @@ def test_program_no_programpage(self):
'title': self.program.title,
'programpage_url': None,
'enrolled': False,
'total_courses': 0,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()]
'total_courses': 1,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()],
"courseware_backends": ["edxorg"],
}

def test_program_with_programpage(self):
Expand All @@ -114,8 +116,9 @@ def test_program_with_programpage(self):
'title': self.program.title,
'programpage_url': programpage.get_full_url(),
'enrolled': False,
'total_courses': 0,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()]
'total_courses': 1,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()],
"courseware_backends": ["edxorg"],
}
assert len(programpage.url) > 0

Expand All @@ -130,8 +133,9 @@ def test_program_enrolled(self):
'title': self.program.title,
'programpage_url': None,
'enrolled': True,
'total_courses': 0,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()]
'total_courses': 1,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()],
"courseware_backends": ["edxorg"],
}

def test_program_courses(self):
Expand All @@ -145,8 +149,9 @@ def test_program_courses(self):
'title': self.program.title,
'programpage_url': None,
'enrolled': False,
'total_courses': 5,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()]
'total_courses': 6,
'topics': [{'name': topic.name} for topic in self.program.topics.iterator()],
"courseware_backends": ["edxorg"],
}


Expand Down
2 changes: 1 addition & 1 deletion courses/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = (
IsAuthenticated,
)
queryset = Program.objects.filter(live=True)
queryset = Program.objects.filter(live=True).prefetch_course_runs()
serializer_class = ProgramSerializer


Expand Down
8 changes: 8 additions & 0 deletions micromasters/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ class UserSerializer(serializers.ModelSerializer):
first_name = serializers.SerializerMethodField()
last_name = serializers.SerializerMethodField()
preferred_name = serializers.SerializerMethodField()
social_auth_providers = serializers.SerializerMethodField()

class Meta:
model = User
fields = (
"username", "email",
"first_name", "last_name", "preferred_name",
"social_auth_providers",
)

def get_username(self, obj):
Expand Down Expand Up @@ -59,6 +61,12 @@ def get_preferred_name(self, obj):
except ObjectDoesNotExist:
return None

def get_social_auth_providers(self, obj):
"""
Get the list of social auth providers
"""
return list(obj.social_auth.values_list("provider", flat=True).distinct())


def serialize_maybe_user(user):
"""
Expand Down
3 changes: 3 additions & 0 deletions micromasters/serializers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def test_basic_user(self):
"first_name": None,
"last_name": None,
"preferred_name": None,
"social_auth_providers": [],
}

def test_logged_in_user_through_maybe_wrapper(self):
Expand All @@ -45,6 +46,7 @@ def test_logged_in_user_through_maybe_wrapper(self):
"first_name": None,
"last_name": None,
"preferred_name": None,
"social_auth_providers": [],
}

def test_user_with_profile(self):
Expand All @@ -67,6 +69,7 @@ def test_user_with_profile(self):
"first_name": "Rando",
"last_name": "Cardrizzian",
"preferred_name": "Hobo",
"social_auth_providers": [],
}


Expand Down
11 changes: 6 additions & 5 deletions static/js/components/CouponNotificationDialog_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ const COURSE: Course = {
}

const PROGRAM: AvailableProgram = {
id: 1,
title: "Awesomesauce",
enrolled: true,
programpage_url: null,
total_courses: 0
id: 1,
title: "Awesomesauce",
enrolled: true,
programpage_url: null,
total_courses: 0,
courseware_backends: ["edxorg"]
}

describe("CouponNotificationDialog", () => {
Expand Down
71 changes: 71 additions & 0 deletions static/js/components/SocialAuthDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* global SETTINGS: false */
import React, { useState, useEffect } from "react"
import Button from "@material-ui/core/Button"
import Dialog from "@material-ui/core/Dialog"
import DialogActions from "@material-ui/core/DialogActions"
import DialogContent from "@material-ui/core/DialogContent"
import DialogTitle from "@material-ui/core/DialogTitle"
import Grid from "@material-ui/core/Grid"
import R from "ramda"

import { COURSEWARE_BACKEND_NAMES } from "../constants"

import type { AvailableProgram } from "../flow/enrollmentTypes"

type Props = {
currentProgramEnrollment: AvailableProgram
}

const SocialAuthDialog = (props: Props) => {
const { currentProgramEnrollment } = props
const [open, setOpen] = useState(false)
const missingBackend = R.head(
R.difference(
R.propOr([], "courseware_backends", currentProgramEnrollment),
R.propOr([], "social_auth_providers", SETTINGS.user)
)
)

useEffect(() => {
setOpen(!R.isNil(currentProgramEnrollment) && !R.isNil(missingBackend))
}, [currentProgramEnrollment, missingBackend])

if (R.isNil(currentProgramEnrollment)) {
return null
}

return (
<Dialog
classes={{
paper: "dialog"
}}
open={open}
onClose={() => setOpen(false)}
>
<DialogTitle className="dialog-title">Action Required</DialogTitle>
<DialogContent>
<Grid container style={{ padding: 20 }}>
<Grid item xs={12}>
<p>
Courses for <strong>{currentProgramEnrollment.title}</strong> are
offered on {COURSEWARE_BACKEND_NAMES[missingBackend]}. Continue to
create a new account and link it to your current MicroMasters
account.
</p>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
key="cancel"
className="primary-button ok-button"
href={`/login/${missingBackend}/`}
>
Continue to {COURSEWARE_BACKEND_NAMES[missingBackend]}
</Button>
</DialogActions>
</Dialog>
)
}

export default SocialAuthDialog
87 changes: 87 additions & 0 deletions static/js/components/SocialAuthDialog_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @flow
/* global SETTINGS: false */
import React from "react"
import { mount } from "enzyme"
import { assert } from "chai"
import Button from "@material-ui/core/Button"
import Grid from "@material-ui/core/Grid"
import { MuiThemeProvider, createMuiTheme } from "@material-ui/core/styles"

import SocialAuthDialog from "./SocialAuthDialog"
import { COURSEWARE_BACKEND_NAMES } from "../constants"
import { makeAvailableProgram } from "../factories/dashboard"

import type { AvailableProgram } from "../flow/enrollmentTypes"

describe("SocialAuthDialog", () => {
const renderDialog = (enrollment: AvailableProgram) =>
mount(
<MuiThemeProvider theme={createMuiTheme()}>
<SocialAuthDialog currentProgramEnrollment={enrollment} />
</MuiThemeProvider>
)
.find(SocialAuthDialog)
.children()

Object.entries(COURSEWARE_BACKEND_NAMES).forEach(
([missingBackend, backendLabel]) => {
describe(`for missing backend '${missingBackend}'`, () => {
let authenticatedEnrollment,
unauthenticatedEnrollment,
availablePrograms

beforeEach(() => {
availablePrograms = Object.keys(COURSEWARE_BACKEND_NAMES)
SETTINGS.user.social_auth_providers = availablePrograms.filter(
backend => backend !== missingBackend
)
authenticatedEnrollment = makeAvailableProgram(
undefined,
availablePrograms
)
unauthenticatedEnrollment = makeAvailableProgram(
undefined,
availablePrograms.filter(backend => backend === missingBackend)
)
})

it("should be closed if the learner is authenticated with the backend", () => {
SETTINGS.user.social_auth_providers = availablePrograms
const wrapper = renderDialog(authenticatedEnrollment)
assert.isBoolean(wrapper.prop("open"))
assert.notOk(wrapper.prop("open"))
})

it("should be open if the learner is not authenticated with the backend", () => {
const wrapper = renderDialog(unauthenticatedEnrollment)
assert.isBoolean(wrapper.prop("open"))
assert.ok(wrapper.prop("open"))
})

it("should have a continue button linking to the social auth login url", () => {
const wrapper = renderDialog(unauthenticatedEnrollment)
const btn = wrapper.find(Button)
assert.ok(btn.exists())
assert.equal(btn.prop("href"), `/login/${missingBackend}/`)
assert.equal(btn.text(), `Continue to ${String(backendLabel)}`)
})

it("should have a description of what the learner needs to do", () => {
const wrapper = renderDialog(unauthenticatedEnrollment)
const text = wrapper
.find(Grid)
.at(0)
.text()
assert.equal(
text,
`Courses for ${
unauthenticatedEnrollment.title
} are offered on ${String(
backendLabel
)}. Continue to create a new account and link it to your current MicroMasters account.`
)
})
})
}
)
})
5 changes: 3 additions & 2 deletions static/js/components/dashboard/courses/ProgressMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
STATUS_MISSED_DEADLINE,
STATUS_CURRENTLY_ENROLLED,
STATUS_PAID_BUT_NOT_ENROLLED,
DASHBOARD_FORMAT
DASHBOARD_FORMAT,
COURSEWARE_BACKEND_NAMES
} from "../../../constants"
import { renderSeparatedComponents } from "../../../util/util"
import { courseRunUrl } from "../../../util/courseware"
Expand Down Expand Up @@ -145,7 +146,7 @@ export default class ProgressMessage extends React.Component {
target="_blank"
rel="noopener noreferrer"
>
View on edX
View on {COURSEWARE_BACKEND_NAMES[courseRun.courseware_backend]}
</a>
) : null
}
Expand Down
Loading

0 comments on commit 9a78149

Please sign in to comment.