Fetch K-5 dashboard announcements and apps in batches
Updates the latest announcement-fetching logic for the K-5 dashboard homeroom page to retrieve all announcements in one request. A new "latest_only" parameter was added to the announcements API in order to support this. Also updates the dashboard_card logic for the K-5 dashboard to not show any info associated with courses with outstanding invitations. This means that if a user has not accepted an invite to be enrolled in a course, that course should not show up anywhere on the K-5 dashboard. fixes LS-2227 flag = canvas_for_elementary Test plan: - Log in as a teacher enrolled in a homeroom course and subject course - Create announcements for both of those courses (if recent announcements don't already exist for them) - Expect the homeroom announcement to show up at the top of the teacher's dashboard (like it did before this change) - Expect the subject announement to show up on the bottom of the subject course card on the dashboard (like before) - Log in as a student enrolled in the same courses and ensure that both announcements show up as expected on their dashboard too - Invite a student or teacher to a L-5 subject course that they're not already in - Log in as the invited user and go to their dashboard, but do not accept the invite yet - Except to not see cards, LTIs, grades, or planner items associated with that course Change-Id: I8fae07ec101ff34811f42e05e1f1845e76f482b7 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/265234 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jackson Howe <jackson.howe@instructure.com> QA-Review: Jackson Howe <jackson.howe@instructure.com> Product-Review: Jeff Largent <jeff.largent@instructure.com>
This commit is contained in:
parent
ed479b96a2
commit
7d71e6dc8f
|
@ -52,6 +52,11 @@ class AnnouncementsApiController < ApplicationController
|
|||
# unpublished items.
|
||||
# Defaults to false for users with access to view unpublished items,
|
||||
# otherwise true and unmodifiable.
|
||||
# @argument latest_only [Optional, Boolean]
|
||||
# Only return the latest announcement for each associated context.
|
||||
# The response will include at most one announcement for each
|
||||
# specified context in the context_codes[] parameter.
|
||||
# Defaults to false.
|
||||
#
|
||||
# @argument include [Optional, array]
|
||||
# Optional list of resources to include with the response. May include
|
||||
|
@ -97,7 +102,12 @@ class AnnouncementsApiController < ApplicationController
|
|||
|
||||
@start_date ||= 14.days.ago.beginning_of_day
|
||||
@end_date ||= @start_date + 28.days
|
||||
scope = scope.ordered_between(@start_date, @end_date)
|
||||
if value_to_boolean(params[:latest_only])
|
||||
scope = scope.ordered_between_by_context(@start_date, @end_date)
|
||||
scope = scope.select("DISTINCT ON (context_id) *")
|
||||
else
|
||||
scope = scope.ordered_between(@start_date, @end_date)
|
||||
end
|
||||
|
||||
# only filter by section visibility if user has no course manage rights
|
||||
skip_section_filtering = courses.all? do |course|
|
||||
|
|
|
@ -37,9 +37,16 @@ class Announcement < DiscussionTopic
|
|||
|
||||
acts_as_list scope: { context: self, type: 'Announcement' }
|
||||
|
||||
scope :ordered_between, lambda { |start_date, end_date|
|
||||
scope :between , lambda { |start_date, end_date|
|
||||
where('COALESCE(delayed_post_at, posted_at, created_at) BETWEEN ? AND ?', start_date, end_date)
|
||||
.order(Arel.sql('COALESCE(delayed_post_at, posted_at, created_at) DESC'))
|
||||
}
|
||||
|
||||
scope :ordered_between, lambda { |start_date, end_date|
|
||||
between(start_date, end_date).order(Arel.sql("COALESCE(delayed_post_at, posted_at, created_at) DESC"))
|
||||
}
|
||||
|
||||
scope :ordered_between_by_context, lambda { |start_date, end_date|
|
||||
between(start_date, end_date).order(Arel.sql("context_id, COALESCE(delayed_post_at, posted_at, created_at) DESC"))
|
||||
}
|
||||
|
||||
def validate_draft_state_change
|
||||
|
|
|
@ -51,6 +51,7 @@ class CourseForMenuPresenter
|
|||
href: course_path(course, invitation: course.read_attribute(:invitation)),
|
||||
term: term || nil,
|
||||
subtitle: subtitle,
|
||||
enrollmentState: course.primary_enrollment_state,
|
||||
enrollmentType: course.primary_enrollment_type,
|
||||
observee: observee,
|
||||
id: course.id,
|
||||
|
|
|
@ -161,6 +161,50 @@ describe "Announcements API", type: :request do
|
|||
expect(json).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "latest_only" do
|
||||
before :once do
|
||||
course_with_teacher :active_all => true, :user => @teacher
|
||||
student_in_course :active_enrollment => true, :user => @student
|
||||
@course3 = @course
|
||||
@ann3 = @course3.announcements.build :title => "Announcement New", :message => '<p>This is the latest</p>'
|
||||
@ann3.posted_at = 2.days.ago
|
||||
@ann3.save!
|
||||
old = @course3.announcements.build :title => "Announcement Old", :message => '<p>This is older</p>'
|
||||
old.posted_at = 5.days.ago
|
||||
old.save!
|
||||
end
|
||||
|
||||
let(:start_date) { 10.days.ago.iso8601 }
|
||||
let(:end_date) { 30.days.from_now.iso8601 }
|
||||
|
||||
it "only returns the latest announcement by posted date" do
|
||||
json = api_call_as_user(@teacher, :get, "/api/v1/announcements",
|
||||
@params.merge(:context_codes => %W[course_#{@course1.id} course_#{@course2.id} course_#{@course3.id}],
|
||||
start_date => start_date, :end_date => end_date, :latest_only => true))
|
||||
|
||||
expect(json.length).to be 3
|
||||
expect(json.map{|a| a['id']}).to include(@anns.last[:id], @ann2[:id], @ann3[:id])
|
||||
end
|
||||
|
||||
it "excludes courses not in the context_ids list" do
|
||||
json = api_call_as_user(@teacher, :get, "/api/v1/announcements",
|
||||
@params.merge(:context_codes => %W[course_#{@course1.id} course_#{@course3.id}],
|
||||
start_date => start_date, :end_date => end_date, :latest_only => true))
|
||||
|
||||
expect(json.length).to be 2
|
||||
expect(json.map{|a| a['id']}).to include(@anns.last[:id], @ann3[:id])
|
||||
end
|
||||
|
||||
it "works properly in conjunction with the active_only param" do
|
||||
json = api_call_as_user(@teacher, :get, "/api/v1/announcements",
|
||||
@params.merge(:context_codes => %W[course_#{@course1.id} course_#{@course2.id} course_#{@course3.id}],
|
||||
start_date => start_date, :end_date => end_date, :active_only => true, :latest_only => true))
|
||||
|
||||
expect(json.length).to be 2
|
||||
expect(json.map{|a| a['id']}).to include(@anns.last[:id], @ann3[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as student" do
|
||||
|
|
|
@ -32,28 +32,19 @@ import {Img} from '@instructure/ui-img'
|
|||
|
||||
import K5DashboardCard, {CARD_SIZE_PX} from './K5DashboardCard'
|
||||
import {createDashboardCards} from '@canvas/dashboard-card'
|
||||
import {fetchLatestAnnouncement, parseAnnouncementDetails} from '@canvas/k5/react/utils'
|
||||
import HomeroomAnnouncementsLayout from './HomeroomAnnouncementsLayout'
|
||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||
import LoadingSkeleton from '@canvas/k5/react/LoadingSkeleton'
|
||||
import {CreateCourseModal} from './CreateCourseModal'
|
||||
import EmptyDashPandaUrl from '../images/empty-dashboard.svg'
|
||||
|
||||
export const fetchHomeroomAnnouncements = cards =>
|
||||
Promise.all(
|
||||
cards
|
||||
.filter(c => c.isHomeroom)
|
||||
.map(course =>
|
||||
fetchLatestAnnouncement(course.id).then(announcement =>
|
||||
parseAnnouncementDetails(announcement, course)
|
||||
)
|
||||
)
|
||||
).then(announcements => announcements.filter(a => a))
|
||||
|
||||
export const HomeroomPage = ({cards, cardsSettled, visible, createPermissions}) => {
|
||||
export const HomeroomPage = ({
|
||||
cards,
|
||||
createPermissions,
|
||||
homeroomAnnouncements,
|
||||
loadingAnnouncements,
|
||||
visible
|
||||
}) => {
|
||||
const [dashboardCards, setDashboardCards] = useState([])
|
||||
const [homeroomAnnouncements, setHomeroomAnnouncements] = useState([])
|
||||
const [announcementsLoading, setAnnouncementsLoading] = useState(true)
|
||||
const [courseModalOpen, setCourseModalOpen] = useState(false)
|
||||
|
||||
useImmediate(
|
||||
|
@ -64,19 +55,10 @@ export const HomeroomPage = ({cards, cardsSettled, visible, createPermissions})
|
|||
headingLevel: 'h3'
|
||||
})
|
||||
)
|
||||
|
||||
if (cardsSettled) {
|
||||
setAnnouncementsLoading(true)
|
||||
fetchHomeroomAnnouncements(cards)
|
||||
.then(setHomeroomAnnouncements)
|
||||
.catch(showFlashError(I18n.t('Failed to load announcements.')))
|
||||
.finally(() => setAnnouncementsLoading(false))
|
||||
}
|
||||
}
|
||||
},
|
||||
[cards, cardsSettled],
|
||||
[cards],
|
||||
// Need to do deep comparison on cards to only re-trigger if they actually changed
|
||||
// (they shouldn't after they're set the first time)
|
||||
{deep: true}
|
||||
)
|
||||
|
||||
|
@ -108,7 +90,7 @@ export const HomeroomPage = ({cards, cardsSettled, visible, createPermissions})
|
|||
<View as="section">
|
||||
<HomeroomAnnouncementsLayout
|
||||
homeroomAnnouncements={homeroomAnnouncements}
|
||||
loading={announcementsLoading}
|
||||
loading={loadingAnnouncements}
|
||||
/>
|
||||
</View>
|
||||
<View as="section">
|
||||
|
@ -158,9 +140,10 @@ export const HomeroomPage = ({cards, cardsSettled, visible, createPermissions})
|
|||
|
||||
HomeroomPage.propTypes = {
|
||||
cards: PropTypes.array,
|
||||
cardsSettled: PropTypes.bool.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
createPermissions: PropTypes.oneOf(['admin', 'teacher', 'none']).isRequired
|
||||
createPermissions: PropTypes.oneOf(['admin', 'teacher', 'none']).isRequired,
|
||||
homeroomAnnouncements: PropTypes.array.isRequired,
|
||||
loadingAnnouncements: PropTypes.bool.isRequired,
|
||||
visible: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
export default HomeroomPage
|
||||
|
|
|
@ -15,11 +15,10 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import React, {useCallback, useEffect, useState} from 'react'
|
||||
import {connect, Provider} from 'react-redux'
|
||||
import I18n from 'i18n!k5_dashboard'
|
||||
import PropTypes from 'prop-types'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
|
||||
import {
|
||||
createTeacherPreview,
|
||||
|
@ -29,6 +28,7 @@ import {
|
|||
store,
|
||||
toggleMissingItems
|
||||
} from '@instructure/canvas-planner'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {
|
||||
IconBankLine,
|
||||
IconCalendarMonthLine,
|
||||
|
@ -46,10 +46,12 @@ import loadCardDashboard from '@canvas/dashboard-card'
|
|||
import {mapStateToProps} from '@canvas/k5/redux/redux-helpers'
|
||||
import SchedulePage from '@canvas/k5/react/SchedulePage'
|
||||
import ResourcesPage from '@canvas/k5/react/ResourcesPage'
|
||||
import {FOCUS_TARGETS, TAB_IDS} from '@canvas/k5/react/utils'
|
||||
import {groupAnnouncementsByHomeroom, FOCUS_TARGETS, TAB_IDS} from '@canvas/k5/react/utils'
|
||||
import {theme} from '@canvas/k5/react/k5-theme'
|
||||
import useTabState from '@canvas/k5/react/hooks/useTabState'
|
||||
import usePlanner from '@canvas/k5/react/hooks/usePlanner'
|
||||
import useFetchApi from '@canvas/use-fetch-api-hook'
|
||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||
|
||||
const DASHBOARD_TABS = [
|
||||
{
|
||||
|
@ -92,6 +94,9 @@ export const K5Dashboard = ({
|
|||
const {activeTab, currentTab, handleTabChange} = useTabState(defaultTab, DASHBOARD_TABS)
|
||||
const [cards, setCards] = useState(null)
|
||||
const [cardsSettled, setCardsSettled] = useState(false)
|
||||
const [homeroomAnnouncements, setHomeroomAnnouncements] = useState([])
|
||||
const [subjectAnnouncements, setSubjectAnnouncements] = useState([])
|
||||
const [loadingAnnouncements, setLoadingAnnouncements] = useState(true)
|
||||
const [tabsRef, setTabsRef] = useState(null)
|
||||
const plannerInitialized = usePlanner({
|
||||
plannerEnabled,
|
||||
|
@ -103,12 +108,38 @@ export const K5Dashboard = ({
|
|||
useEffect(() => {
|
||||
if (!cards && (currentTab === TAB_IDS.HOMEROOM || currentTab === TAB_IDS.RESOURCES)) {
|
||||
loadCardDashboard((dc, cardsFinishedLoading) => {
|
||||
setCards(dc)
|
||||
setCards(dc.filter(({enrollmentState}) => enrollmentState !== 'invited'))
|
||||
setCardsSettled(cardsFinishedLoading)
|
||||
})
|
||||
}
|
||||
}, [cards, currentTab])
|
||||
|
||||
useFetchApi({
|
||||
path: '/api/v1/announcements',
|
||||
loading: setLoadingAnnouncements,
|
||||
success: useCallback(
|
||||
data => {
|
||||
if (data) {
|
||||
const groupedAnnouncements = groupAnnouncementsByHomeroom(data, cards)
|
||||
setHomeroomAnnouncements(groupedAnnouncements.true)
|
||||
setSubjectAnnouncements(groupedAnnouncements.false)
|
||||
}
|
||||
},
|
||||
[cards]
|
||||
),
|
||||
error: useCallback(showFlashError(I18n.t('Failed to load announcements.')), []),
|
||||
// This is a bit hacky, but we need to wait to fetch the announcements until the cards have
|
||||
// settled. Setting forceResult skips the fetch until it changes to undefined.
|
||||
forceResult: cardsSettled ? undefined : false,
|
||||
fetchAllPages: true,
|
||||
params: {
|
||||
active_only: true,
|
||||
context_codes: cards && cards.map(({id}) => `course_${id}`),
|
||||
latest_only: true,
|
||||
per_page: '100'
|
||||
}
|
||||
})
|
||||
|
||||
const handleSwitchToToday = () => {
|
||||
handleTabChange(TAB_IDS.SCHEDULE, FOCUS_TARGETS.TODAY)
|
||||
switchToToday()
|
||||
|
@ -133,10 +164,11 @@ export const K5Dashboard = ({
|
|||
assignmentsDueToday,
|
||||
assignmentsMissing,
|
||||
assignmentsCompletedForToday,
|
||||
cardsSettled,
|
||||
loadingAnnouncements,
|
||||
loadingOpportunities,
|
||||
isStudent: plannerEnabled,
|
||||
responsiveSize,
|
||||
subjectAnnouncements,
|
||||
switchToMissingItems: handleSwitchToMissingItems,
|
||||
switchToToday: handleSwitchToToday
|
||||
}}
|
||||
|
@ -153,11 +185,10 @@ export const K5Dashboard = ({
|
|||
)}
|
||||
<HomeroomPage
|
||||
cards={cards}
|
||||
cardsSettled={cardsSettled}
|
||||
isStudent={plannerEnabled}
|
||||
responsiveSize={responsiveSize}
|
||||
visible={currentTab === TAB_IDS.HOMEROOM}
|
||||
createPermissions={createPermissions}
|
||||
homeroomAnnouncements={homeroomAnnouncements}
|
||||
loadingAnnouncements={loadingAnnouncements}
|
||||
visible={currentTab === TAB_IDS.HOMEROOM}
|
||||
/>
|
||||
{plannerInitialized && <SchedulePage visible={currentTab === TAB_IDS.SCHEDULE} />}
|
||||
{!plannerEnabled && currentTab === TAB_IDS.SCHEDULE && createTeacherPreview(timeZone)}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useContext, useState} from 'react'
|
||||
import React, {useContext} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import I18n from 'i18n!k5_dashboard'
|
||||
|
||||
|
@ -28,13 +28,11 @@ import {Link} from '@instructure/ui-link'
|
|||
import {Text} from '@instructure/ui-text'
|
||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
|
||||
import LoadingSkeleton from '@canvas/k5/react/LoadingSkeleton'
|
||||
|
||||
import useImmediate from '@canvas/use-immediate-hook'
|
||||
import k5Theme from '@canvas/k5/react/k5-theme'
|
||||
import K5DashboardContext from '@canvas/k5/react/K5DashboardContext'
|
||||
import {fetchLatestAnnouncement, DEFAULT_COURSE_COLOR, FOCUS_TARGETS} from '@canvas/k5/react/utils'
|
||||
import {DEFAULT_COURSE_COLOR, FOCUS_TARGETS} from '@canvas/k5/react/utils'
|
||||
|
||||
import instFSOptimizedImageUrl from '@canvas/dashboard-card/util/instFSOptimizedImageUrl'
|
||||
|
||||
|
@ -240,8 +238,6 @@ const K5DashboardCard = ({
|
|||
image,
|
||||
isDragging = false
|
||||
}) => {
|
||||
const [latestAnnouncement, setLatestAnnouncement] = useState(null)
|
||||
const [loadingAnnouncement, setLoadingAnnouncement] = useState(false)
|
||||
const backgroundColor = courseColor || DEFAULT_COURSE_COLOR
|
||||
|
||||
const k5Context = useContext(K5DashboardContext)
|
||||
|
@ -251,24 +247,15 @@ const K5DashboardCard = ({
|
|||
(k5Context?.assignmentsMissing && k5Context.assignmentsMissing[id]) || 0
|
||||
const assignmentsCompletedForToday =
|
||||
(k5Context?.assignmentsCompletedForToday && k5Context.assignmentsCompletedForToday[id]) || 0
|
||||
const cardsSettled = k5Context.cardsSettled || false
|
||||
const loadingAnnouncements = k5Context?.loadingAnnouncements || false
|
||||
const loadingOpportunities = k5Context?.loadingOpportunities || false
|
||||
const isStudent = k5Context?.isStudent || false
|
||||
const latestAnnouncement = k5Context.subjectAnnouncements.find(
|
||||
a => a.context_code === `course_${id}`
|
||||
)
|
||||
const switchToMissingItems = k5Context?.switchToMissingItems
|
||||
const switchToToday = k5Context?.switchToToday
|
||||
|
||||
useImmediate(() => {
|
||||
if (cardsSettled) {
|
||||
setLoadingAnnouncement(true)
|
||||
fetchLatestAnnouncement(id)
|
||||
.then(setLatestAnnouncement)
|
||||
.catch(
|
||||
showFlashError(I18n.t('Failed to load announcement for %{originalName}.', {originalName}))
|
||||
)
|
||||
.finally(() => setLoadingAnnouncement(false))
|
||||
}
|
||||
}, [cardsSettled, id, originalName])
|
||||
|
||||
const handleHeaderClick = e => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
|
@ -344,7 +331,7 @@ const K5DashboardCard = ({
|
|||
)}
|
||||
<LatestAnnouncementLink
|
||||
color={backgroundColor}
|
||||
loading={loadingAnnouncement}
|
||||
loading={loadingAnnouncements}
|
||||
{...latestAnnouncement}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('HomeroomPage', () => {
|
|||
})
|
||||
|
||||
it('shows loading skeletons while loading for announcements and cards', () => {
|
||||
const {getAllByText, getByText} = render(<HomeroomPage {...getProps()} />)
|
||||
const {getAllByText, getByText} = render(<HomeroomPage {...getProps()} loadingAnnouncements />)
|
||||
const cards = getAllByText('Loading Card')
|
||||
expect(cards[0]).toBeInTheDocument()
|
||||
expect(getByText('Loading Homeroom Announcement Content')).toBeInTheDocument()
|
||||
|
|
|
@ -44,6 +44,7 @@ const dashboardCards = [
|
|||
shortName: 'Econ 101',
|
||||
originalName: 'Economics 101',
|
||||
courseCode: 'ECON-001',
|
||||
enrollmentState: 'active',
|
||||
isHomeroom: false,
|
||||
canManage: true,
|
||||
published: true
|
||||
|
@ -55,13 +56,27 @@ const dashboardCards = [
|
|||
shortName: 'Homeroom1',
|
||||
originalName: 'Home Room',
|
||||
courseCode: 'HOME-001',
|
||||
enrollmentState: 'active',
|
||||
isHomeroom: true,
|
||||
canManage: true,
|
||||
published: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
assetString: 'course_3',
|
||||
href: '/courses/3',
|
||||
originalName: 'The Maths',
|
||||
courseCode: 'DA-MATHS',
|
||||
enrollmentState: 'invited',
|
||||
isHomeroom: false,
|
||||
canManage: true,
|
||||
published: true
|
||||
}
|
||||
]
|
||||
const homeroomAnnouncement = [
|
||||
const announcements = [
|
||||
{
|
||||
id: '20',
|
||||
context_code: 'course_2',
|
||||
title: 'Announcement here',
|
||||
message: '<p>This is the announcement</p>',
|
||||
html_url: 'http://google.com/announcement',
|
||||
|
@ -75,6 +90,13 @@ const homeroomAnnouncement = [
|
|||
filename: '1608134586_366__exam1.pdf'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
context_code: 'course_1',
|
||||
title: "This sure isn't a homeroom",
|
||||
message: '<p>Definitely not!</p>',
|
||||
html_url: '/courses/1/announcements/21'
|
||||
}
|
||||
]
|
||||
const gradeCourses = [
|
||||
|
@ -178,6 +200,7 @@ const defaultEnv = {
|
|||
}
|
||||
const defaultProps = {
|
||||
currentUser,
|
||||
createPermissions: 'none',
|
||||
plannerEnabled: false,
|
||||
loadAllOpportunities: () => {},
|
||||
timeZone: defaultEnv.TIMEZONE
|
||||
|
@ -278,11 +301,7 @@ beforeEach(() => {
|
|||
response: opportunities
|
||||
})
|
||||
fetchMock.get('/api/v1/courses/1/activity_stream/summary', JSON.stringify(cardSummary))
|
||||
fetchMock.get(
|
||||
/\/api\/v1\/announcements\?context_codes=course_2.*/,
|
||||
JSON.stringify(homeroomAnnouncement)
|
||||
)
|
||||
fetchMock.get(/\/api\/v1\/announcements\?context_codes=course_1.*/, '[]')
|
||||
fetchMock.get(/\/api\/v1\/announcements.*/, announcements)
|
||||
fetchMock.get(/\/api\/v1\/users\/self\/courses.*/, JSON.stringify(gradeCourses))
|
||||
fetchMock.get(encodeURI('api/v1/courses/2?include[]=syllabus_body'), JSON.stringify(syllabus))
|
||||
fetchMock.get('/api/v1/courses/1/external_tools/visible_course_nav_tools', JSON.stringify(apps))
|
||||
|
@ -358,10 +377,11 @@ describe('K-5 Dashboard', () => {
|
|||
expect(await findByText('My Subjects')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows course cards, excluding homerooms', async () => {
|
||||
it('shows course cards, excluding homerooms and subjects with pending invites', async () => {
|
||||
const {findByText, queryByText} = render(<K5Dashboard {...defaultProps} />)
|
||||
expect(await findByText('Economics 101')).toBeInTheDocument()
|
||||
expect(queryByText('Home Room')).not.toBeInTheDocument()
|
||||
expect(queryByText('The Maths')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows latest announcement from each homeroom', async () => {
|
||||
|
@ -390,6 +410,13 @@ describe('K-5 Dashboard', () => {
|
|||
// correct element is focused since that occurs at the end of the scrolling animation
|
||||
})
|
||||
|
||||
it('shows the latest announcement for each subject course if one exists', async () => {
|
||||
const {findByText} = render(<K5Dashboard {...defaultProps} />)
|
||||
const announcementLink = await findByText("This sure isn't a homeroom")
|
||||
expect(announcementLink).toBeInTheDocument()
|
||||
expect(announcementLink.closest('a').href).toMatch('/courses/1/announcements/21')
|
||||
})
|
||||
|
||||
it('shows a missing items link pointing to the missing items section on the schedule tab', async () => {
|
||||
const {findByText, getByRole, getByText} = render(
|
||||
<K5Dashboard {...defaultProps} plannerEnabled />
|
||||
|
@ -431,8 +458,8 @@ describe('K-5 Dashboard', () => {
|
|||
response: dashboardCards
|
||||
})
|
||||
.then(() => {
|
||||
// Expect one announcement request for each card
|
||||
expect(fetchMock.calls(/\/api\/v1\/announcements.*/).length).toBe(2)
|
||||
// Expect just one announcement request for all cards
|
||||
expect(fetchMock.calls(/\/api\/v1\/announcements.*/).length).toBe(1)
|
||||
// Expect one LTI request for each non-homeroom card
|
||||
expect(
|
||||
fetchMock.calls('/api/v1/courses/1/external_tools/visible_course_nav_tools').length
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {render} from '@testing-library/react'
|
||||
import K5DashboardCard, {
|
||||
DashboardCardHeaderHero,
|
||||
|
@ -30,27 +29,20 @@ const defaultContext = {
|
|||
assignmentsDueToday: {},
|
||||
assignmentsMissing: {},
|
||||
assignmentsCompletedForToday: {},
|
||||
cardsSettled: true,
|
||||
loadingAnnouncements: false,
|
||||
loadingOpportunities: false,
|
||||
isStudent: true,
|
||||
subjectAnnouncements: [],
|
||||
switchToMissingItems: jest.fn(),
|
||||
switchToToday: jest.fn()
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
id: 'test',
|
||||
href: '/courses/5',
|
||||
href: '/courses/test',
|
||||
originalName: 'test course'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get('/api/v1/announcements?context_codes=course_test&active_only=true&per_page=1', '[]')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('DashboardCardHeaderHero', () => {
|
||||
const heroProps = {
|
||||
backgroundColor: '#FFFFFF',
|
||||
|
@ -92,19 +84,16 @@ describe('K-5 Dashboard Card', () => {
|
|||
})
|
||||
|
||||
it('displays a link to the latest announcement if one exists', async () => {
|
||||
fetchMock.get(
|
||||
'/api/v1/announcements?context_codes=course_test&active_only=true&per_page=1',
|
||||
JSON.stringify([
|
||||
{
|
||||
id: '55',
|
||||
html_url: '/courses/test/discussion_topics/55',
|
||||
title: 'How do you do, fellow kids?'
|
||||
}
|
||||
]),
|
||||
{overwriteRoutes: true}
|
||||
)
|
||||
const subjectAnnouncements = [
|
||||
{
|
||||
id: '55',
|
||||
context_code: 'course_test',
|
||||
html_url: '/courses/test/discussion_topics/55',
|
||||
title: 'How do you do, fellow kids?'
|
||||
}
|
||||
]
|
||||
const {findByText} = render(
|
||||
<K5DashboardContext.Provider value={defaultContext}>
|
||||
<K5DashboardContext.Provider value={{...defaultContext, subjectAnnouncements}}>
|
||||
<K5DashboardCard {...defaultProps} />
|
||||
</K5DashboardContext.Provider>
|
||||
)
|
||||
|
@ -186,7 +175,14 @@ describe('LatestAnnouncementLink', () => {
|
|||
describe('AssignmentLinks', () => {
|
||||
it('renders loading skeleton while loading', () => {
|
||||
const {getByText, queryByText} = render(
|
||||
<AssignmentLinks loading color="red" requestTabChange={jest.fn()} numMissing={2} />
|
||||
<AssignmentLinks
|
||||
loading
|
||||
color="red"
|
||||
courseName="test"
|
||||
switchToMissingItems={jest.fn()}
|
||||
switchToToday={jest.fn()}
|
||||
numMissing={2}
|
||||
/>
|
||||
)
|
||||
expect(getByText('Loading missing assignments link')).toBeInTheDocument()
|
||||
expect(queryByText('2 missing')).not.toBeInTheDocument()
|
||||
|
|
|
@ -22,10 +22,11 @@ const DEFAULT_CONTEXT = {
|
|||
assignmentsDueToday: {},
|
||||
assignmentsMissing: {},
|
||||
assignmentsCompletedForToday: {},
|
||||
cardsSettled: false,
|
||||
loadingAnnouncements: false,
|
||||
loadingOpportunities: false,
|
||||
isStudent: false,
|
||||
responsiveSize: 'large',
|
||||
subjectAnnouncements: [],
|
||||
switchToMissingItems: () => {},
|
||||
switchToToday: () => {}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,8 @@ import {
|
|||
getAccountsFromEnrollments,
|
||||
getTotalGradeStringFromEnrollments,
|
||||
fetchImportantInfos,
|
||||
parseAnnouncementDetails
|
||||
parseAnnouncementDetails,
|
||||
groupAnnouncementsByHomeroom
|
||||
} from '../utils'
|
||||
|
||||
const ANNOUNCEMENT_URL =
|
||||
|
@ -634,3 +635,64 @@ describe('parseAnnouncementDetails', () => {
|
|||
expect(announcementDetails.announcement.attachment).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('groupAnnouncementsByHomeroom', () => {
|
||||
const announcements = [
|
||||
{
|
||||
id: '10',
|
||||
context_code: 'course_1'
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
context_code: 'course_2',
|
||||
permissions: {
|
||||
update: false
|
||||
},
|
||||
attachments: []
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
context_code: 'course_3'
|
||||
}
|
||||
]
|
||||
const courses = [
|
||||
{
|
||||
id: '1',
|
||||
isHomeroom: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
isHomeroom: true
|
||||
}
|
||||
]
|
||||
|
||||
it('groups returned announcements by whether they are associated with a homeroom or not', () => {
|
||||
const grouped = groupAnnouncementsByHomeroom(announcements, courses)
|
||||
expect(Object.keys(grouped)).toEqual(['true', 'false'])
|
||||
expect(grouped.true).toHaveLength(1)
|
||||
expect(grouped.false).toHaveLength(1)
|
||||
expect(grouped.true[0].announcement.id).toBe('11')
|
||||
expect(grouped.false[0].id).toBe('10')
|
||||
})
|
||||
|
||||
it('parses announcement details on homeroom announcements only', () => {
|
||||
const grouped = groupAnnouncementsByHomeroom(announcements, courses)
|
||||
expect(grouped.true[0].courseId).toBe('2')
|
||||
expect(grouped.false[0].courseId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('ignores announcements not associated with a passed-in course', () => {
|
||||
const grouped = groupAnnouncementsByHomeroom(announcements, courses)
|
||||
expect([...grouped.true, ...grouped.false]).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('handles missing announcements and courses gracefully', () => {
|
||||
const emptyGroups = {true: [], false: []}
|
||||
expect(groupAnnouncementsByHomeroom([], courses)).toEqual({
|
||||
true: [{courseId: '2'}],
|
||||
false: []
|
||||
})
|
||||
expect(groupAnnouncementsByHomeroom(announcements, [])).toEqual(emptyGroups)
|
||||
expect(groupAnnouncementsByHomeroom()).toEqual(emptyGroups)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -278,6 +278,7 @@ export const parseAnnouncementDetails = (announcement, course) => {
|
|||
canEdit: announcement.permissions.update,
|
||||
published: course.published,
|
||||
announcement: {
|
||||
id: announcement.id,
|
||||
title: announcement.title,
|
||||
message: announcement.message,
|
||||
url: announcement.html_url,
|
||||
|
@ -287,6 +288,22 @@ export const parseAnnouncementDetails = (announcement, course) => {
|
|||
}
|
||||
}
|
||||
|
||||
/* Helper function to take a list of announcements coming back from API
|
||||
and partition them into homeroom and non-homeroom groups */
|
||||
export const groupAnnouncementsByHomeroom = (announcements = [], courses = []) =>
|
||||
courses.reduce(
|
||||
(acc, course) => {
|
||||
const announcement = announcements.find(a => a.context_code === `course_${course.id}`)
|
||||
const group = acc[course.isHomeroom]
|
||||
const parsedAnnouncement = course.isHomeroom
|
||||
? parseAnnouncementDetails(announcement, course)
|
||||
: announcement
|
||||
if (parsedAnnouncement) acc[course.isHomeroom] = [...group, parsedAnnouncement]
|
||||
return acc
|
||||
},
|
||||
{true: [], false: []}
|
||||
)
|
||||
|
||||
export const TAB_IDS = {
|
||||
HOME: 'tab-home',
|
||||
HOMEROOM: 'tab-homeroom',
|
||||
|
|
Loading…
Reference in New Issue