Skip to content
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
7 changes: 7 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

Version 0.20.3
--------------

- add is_incomplete_or_stale (#1627)
- "unfollow" confirmation modal and "Unfollow All" button (#1628)
- raise SystemExit as a RetryError (#1635)

Version 0.20.2 (Released October 01, 2024)
--------------

Expand Down
48 changes: 41 additions & 7 deletions frontends/mit-learn/src/pages/DashboardPage/SettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ const setupApis = ({
`${urls.userSubscription.check(subscriptionRequest)}`,
subscribeResponse,
)
const unsubscribeUrl = urls.userSubscription.delete(subscribeResponse[0]?.id)
setMockResponse.delete(unsubscribeUrl, subscribeResponse[0])
const unsubscribeUrls = []
for (const sub of subscribeResponse) {
const unsubscribeUrl = urls.userSubscription.delete(sub?.id)
unsubscribeUrls.push(unsubscribeUrl)
setMockResponse.delete(unsubscribeUrl, sub)
}

return {
unsubscribeUrl,
unsubscribeUrls,
}
}

Expand All @@ -46,21 +51,50 @@ describe("SettingsPage", () => {
})

test("Clicking 'Unfollow' removes the subscription", async () => {
const { unsubscribeUrl } = setupApis({
const { unsubscribeUrls } = setupApis({
isAuthenticated: true,
isSubscribed: true,
subscriptionRequest: {},
})
renderWithProviders(<SettingsPage />)

const followList = await screen.findByTestId("follow-list")
const unsubscribeButton = within(followList).getAllByText("Unfollow")[0]
await user.click(unsubscribeButton)
const unsubscribeLink = within(followList).getAllByText("Unfollow")[0]
await user.click(unsubscribeLink)

const unsubscribeButton = await screen.findByTestId("dialog-unfollow")
await user.click(unsubscribeButton)
expect(makeRequest).toHaveBeenCalledWith(
"delete",
unsubscribeUrl,
unsubscribeUrls[0],
undefined,
)
})

test("Clicking 'Unfollow All' removes all subscriptions", async () => {
const { unsubscribeUrls } = setupApis({
isAuthenticated: true,
isSubscribed: true,
subscriptionRequest: {},
})
renderWithProviders(<SettingsPage />)
const unsubscribeLink = await screen.findByTestId("unfollow-all")
await user.click(unsubscribeLink)

const unsubscribeButton = await screen.findByTestId("dialog-unfollow")
await user.click(unsubscribeButton)
for (const unsubUrl of unsubscribeUrls) {
expect(makeRequest).toHaveBeenCalledWith("delete", unsubUrl, undefined)
}
})
test("Unsubscribe from all is hidden if there are no subscriptions", async () => {
setupApis({
isAuthenticated: true,
isSubscribed: false,
subscriptionRequest: {},
})
renderWithProviders(<SettingsPage />)
const unfollowButton = screen.queryByText("Unfollow All")
expect(unfollowButton).not.toBeInTheDocument()
})
})
140 changes: 128 additions & 12 deletions frontends/mit-learn/src/pages/DashboardPage/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import React from "react"
import { PlainList, Typography, Link, styled } from "ol-components"
import {
PlainList,
Typography,
Link,
styled,
Button,
Dialog,
DialogActions,
} from "ol-components"
import { useUserMe } from "api/hooks/user"
import {
useSearchSubscriptionDelete,
useSearchSubscriptionList,
} from "api/hooks/searchSubscription"

import * as NiceModal from "@ebay/nice-modal-react"
const SOURCE_LABEL_DISPLAY = {
topic: "Topic",
unit: "MIT Unit",
department: "MIT Academic Department",
saved_search: "Saved Search",
}

const Actions = styled(DialogActions)({
display: "flex",
"> *": { flex: 1 },
})
const FollowList = styled(PlainList)(({ theme }) => ({
borderRadius: "8px",
background: theme.custom.colors.white,
Expand All @@ -37,6 +49,29 @@ const SubTitleText = styled(Typography)(({ theme }) => ({
...theme.typography.body2,
}))

const SettingsHeader = styled.div(({ theme }) => ({
display: "flex",
alignItems: "center",
alignSelf: "stretch",
[theme.breakpoints.down("md")]: {
paddingBottom: "8px",
},
}))

const SettingsHeaderLeft = styled.div({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
flex: "1 0 0",
})

const SettingsHeaderRight = styled.div(({ theme }) => ({
display: "flex",
[theme.breakpoints.down("md")]: {
display: "none",
},
}))

const ListItem = styled.li(({ theme }) => [
{
padding: "16px 32px",
Expand Down Expand Up @@ -83,22 +118,100 @@ const ListItemBody: React.FC<ListItemBodyProps> = ({
)
}

type UnfollowDialogProps = {
subscriptionIds?: number[]
subscriptionName?: string
}
const UnfollowDialog = NiceModal.create(
({ subscriptionIds, subscriptionName }: UnfollowDialogProps) => {
const modal = NiceModal.useModal()
const subscriptionDelete = useSearchSubscriptionDelete()
const unsubscribe = subscriptionDelete.mutate
return (
<Dialog
{...NiceModal.muiDialogV5(modal)}
title={subscriptionIds?.length === 1 ? "Unfollow" : "Unfollow All"}
actions={
<Actions>
<Button variant="secondary" onClick={() => modal.remove()}>
Cancel
</Button>

<Button
data-testid="dialog-unfollow"
onClick={async () =>
subscriptionIds?.map((subscriptionId) =>
unsubscribe(subscriptionId, {
onSuccess: () => {
modal.remove()
},
}),
)
}
>
{subscriptionIds?.length === 1
? "Yes, Unfollow"
: "Yes, Unfollow All"}
</Button>
</Actions>
}
>
{subscriptionIds?.length === 1 ? (
<>
Are you sure you want to unfollow <b>{subscriptionName}</b>?
</>
) : (
<>
Are you sure you want to <b>Unfollow All</b>? You will stop getting
emails for all topics, academic departments, and MIT units you are
following.
</>
)}
</Dialog>
)
},
)

const SettingsPage: React.FC = () => {
const { data: user } = useUserMe()
const subscriptionDelete = useSearchSubscriptionDelete()

const subscriptionList = useSearchSubscriptionList({
enabled: !!user?.is_authenticated,
})

const unsubscribe = subscriptionDelete.mutate
if (!user || subscriptionList.isLoading) return null

return (
<>
<TitleText>Following</TitleText>
<SubTitleText>
All topics, academic departments, and MIT units you are following.
</SubTitleText>
<SettingsHeader>
<SettingsHeaderLeft>
<TitleText>Following</TitleText>
<SubTitleText>
All topics, academic departments, and MIT units you are following.
</SubTitleText>
</SettingsHeaderLeft>
{subscriptionList?.data && subscriptionList?.data?.length > 1 ? (
<SettingsHeaderRight>
<Button
data-testid="unfollow-all"
variant="tertiary"
onClick={() =>
NiceModal.show(UnfollowDialog, {
subscriptionIds: subscriptionList?.data?.map(
(subscriptionItem) => subscriptionItem.id,
),
subscriptionName: "All",
id: "all",
})
}
>
Unfollow All
</Button>
</SettingsHeaderRight>
) : (
<></>
)}
</SettingsHeader>
<FollowList data-testid="follow-list">
{subscriptionList?.data?.map((subscriptionItem) => (
<ListItem key={subscriptionItem.id}>
Expand All @@ -111,10 +224,13 @@ const SettingsPage: React.FC = () => {
}
/>
<StyledLink
onClick={(event) => {
event.preventDefault()
unsubscribe(subscriptionItem.id)
}}
onClick={() =>
NiceModal.show(UnfollowDialog, {
subscriptionIds: [subscriptionItem.id],
subscriptionName: subscriptionItem.source_description,
id: subscriptionItem.id.toString(),
})
}
>
Unfollow
</StyledLink>
Expand Down
1 change: 1 addition & 0 deletions learning_resources/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ class LearningResourceFactory(DjangoModelFactory):
departments = factory.PostGeneration(_post_gen_departments)
topics = factory.PostGeneration(_post_gen_topics)
content_tags = factory.PostGeneration(_post_gen_tags)
completeness = 1
published = True
delivery = factory.List(random.choices(LearningResourceDelivery.names())) # noqa: S311
professional = factory.LazyAttribute(
Expand Down
4 changes: 4 additions & 0 deletions learning_resources_search/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,7 @@ def test_execute_learn_search_for_learning_resource_query(opensearch):
"is_learning_material",
"resource_age_date",
"featured_rank",
"is_incomplete_or_stale",
]
},
}
Expand Down Expand Up @@ -1921,6 +1922,7 @@ def test_execute_learn_search_with_script_score(
"is_learning_material",
"resource_age_date",
"featured_rank",
"is_incomplete_or_stale",
]
},
}
Expand Down Expand Up @@ -2349,6 +2351,7 @@ def test_execute_learn_search_with_min_score(mocker, settings, opensearch):
"is_learning_material",
"resource_age_date",
"featured_rank",
"is_incomplete_or_stale",
]
},
}
Expand Down Expand Up @@ -2559,6 +2562,7 @@ def test_execute_learn_search_for_content_file_query(opensearch):
"is_learning_material",
"resource_age_date",
"featured_rank",
"is_incomplete_or_stale",
]
},
}
Expand Down
2 changes: 2 additions & 0 deletions learning_resources_search/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class FilterConfig:
},
"free": {"type": "boolean"},
"is_learning_material": {"type": "boolean"},
"is_incomplete_or_stale": {"type": "boolean"},
"delivery": {
"type": "nested",
"properties": {
Expand Down Expand Up @@ -416,6 +417,7 @@ class FilterConfig:
"is_learning_material",
"resource_age_date",
"featured_rank",
"is_incomplete_or_stale",
]

LEARNING_RESOURCE_SEARCH_SORTBY_OPTIONS = {
Expand Down
16 changes: 13 additions & 3 deletions learning_resources_search/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ def serialize_learning_resource_for_update(
dict: The serialized and transformed resource data

"""
STALENESS_CUTOFF = 2010
COMPLETENESS_CUTOFF = 0.5

serialized_data = LearningResourceSerializer(instance=learning_resource_obj).data

if learning_resource_obj.resource_type == LearningResourceType.course.name:
Expand All @@ -146,15 +149,22 @@ def serialize_learning_resource_for_update(
else:
featured_rank = None

resource_age_date = get_resource_age_date(
learning_resource_obj, serialized_data["resource_category"]
)

is_incomplete_or_stale = (
resource_age_date and resource_age_date.year <= STALENESS_CUTOFF
) or (learning_resource_obj.completeness < COMPLETENESS_CUTOFF)

return {
"resource_relations": {"name": "resource"},
"created_on": learning_resource_obj.created_on,
"is_learning_material": serialized_data["resource_category"]
== LEARNING_MATERIAL_RESOURCE_CATEGORY,
"resource_age_date": get_resource_age_date(
learning_resource_obj, serialized_data["resource_category"]
),
"resource_age_date": resource_age_date,
"featured_rank": featured_rank,
"is_incomplete_or_stale": is_incomplete_or_stale,
**serialized_data,
}

Expand Down
Loading
Loading