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

Allow a course to open/close their queues #128

Merged
merged 21 commits into from
May 9, 2018
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ with the current date and the next changes should go under a **[Next]** header.
* Reorganize directory structure. ([@nwalters512](https://github.com/nwalters512) in [#108](https://github.com/illinois/queue/pull/108))
* Remove location from notifications if the queue is a fixed-location queue. ([@redsn0w422](https://github.com/redsn0w422) in [#123](https://github.com/illinois/queue/pull/123))
* Add `npm run fix-lint-js` to fix linter errors that can be fixed automatically. ([@redsn0w422](https://github.com/redsn0w422) in [#127](https://github.com/illinois/queue/pull/127))
* Allow a queue to be closed and reopened. ([@genevievehelsel](https://github.com/genevievehelsel) in [#128](https://github.com/illinois/queue/pull/128))

## 19 April 2018

Expand Down
8 changes: 4 additions & 4 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Developed by: University of Illinois Urbana-Champaign students and faculty

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution.
* Neither the names of University of Illinois Urbana-Champaign, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission.
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution.
* Neither the names of University of Illinois Urbana-Champaign, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
13 changes: 13 additions & 0 deletions src/actions/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ export function updateQueue(queueId, attributes) {
}
}

export function updateQueueStatus(queueId, attributes) {
return dispatch => {
dispatch(updateQueueRequest(queueId, attributes))
return axios.patch(`/api/queues/${queueId}/updateStatus`, attributes).then(
res => dispatch(updateQueueSuccess(queueId, res.data)),
err => {
console.error(err)
dispatch(updateQueueFailure(queueId))
}
)
}
}

/**
* Delete a queue
*/
Expand Down
5 changes: 5 additions & 0 deletions src/api/questions.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ router.post(
safeAsync(async (req, res, _next) => {
const data = matchedData(req)
const { userAuthz, queue: { id: queueId, courseId } } = res.locals
// make sure queue is open
if (!res.locals.queue.open) {
res.status(422).send('This queue is closed')
return
}

// First, let's check if the request is coming from course staff and
// includes a specific netid
Expand Down
8 changes: 8 additions & 0 deletions src/api/questions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ describe('Questions API', () => {
expect(res.body.askedBy.netid).toBe('otherstudent')
})

test('fails for student with well-formed request but the queue is closed', async () => {
const question = { name: 'a', location: 'b', topic: 'c' }
const res = await request(app)
.post('/api/queues/4/questions?forceuser=otherstudent')
.send(question)
expect(res.statusCode).toBe(422)
})

test('fails for student with specific netid', async () => {
const question = {
name: 'a',
Expand Down
20 changes: 19 additions & 1 deletion src/api/queues.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,36 @@ router.patch(
safeAsync(async (req, res, _next) => {
const { queue } = res.locals
const data = matchedData(req)

await queue.update({
name: data.name,
location: data.location,
})
const updatedQueue = await Queue.scope('questionCount').findOne({
where: { id: queue.id },
})

res.status(201).send(updatedQueue)
})
)

router.patch(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to stick to proper HTTP semantics (we already violate them in some places, but) we should really just have a single PATCH endpoint that can handle updating name, location, open/closed status, etc; it just updates any fields that are present in the request.

'/:queueId/updateStatus',
[
requireCourseStaffForQueue,
requireQueue,
check('open').exists(),
failIfErrors,
],
safeAsync(async (req, res, _next) => {
const { queue } = res.locals
const data = matchedData(req)
await queue.update({
open: data.open,
})
res.status(201).send(queue)
})
)

// Gets the on-duty staff list for a specific queue
router.get(
'/:queueId/staff',
Expand Down
49 changes: 40 additions & 9 deletions src/api/queues.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Queues API', () => {
const doGetTest = async user => {
const res = await request(app).get(`/api/queues?forceuser=${user}`)
expect(res.statusCode).toBe(200)
expect(res.body.length).toEqual(3)
expect(res.body.length).toEqual(4)
expect(res.body[0].name).toBe('CS225 Queue')
expect(res.body[1].name).toBe('CS241 Queue')
expect(res.body[0].location).toBe('Here')
Expand Down Expand Up @@ -353,13 +353,44 @@ describe('Queues API', () => {
})
})

describe('PATCH /api/queues/:queueId/updateStatus', () => {
test('succeeds for course staff to close queue', async () => {
const attributes = { open: false }
const res = await request(app)
.patch('/api/queues/1/updateStatus?forceuser=225staff')
.send(attributes)
expect(res.statusCode).toBe(201)
expect(res.body.open).toBe(false)
})

test('succeeds for admin to close queue', async () => {
const attributes = { open: false }
const res = await request(app)
.patch('/api/queues/1/updateStatus?forceuser=admin')
.send(attributes)
expect(res.statusCode).toBe(201)
expect(res.body.open).toBe(false)
})

test('fails for non course staff to close queue', async () => {
const attributes = { open: false }
const res = await request(app)
.patch('/api/queues/1/updateStatus?forceuser=student')
.send(attributes)
expect(res.statusCode).toBe(403)
const res2 = await request(app).get('/api/queues/1?forceuser=student')
expect(res2.statusCode).toBe(200)
expect(res2.body.open).toBe(true)
})
})

describe('DELETE /api/queues/1', () => {
test('succeeds for course staff', async () => {
const res = await request(app).delete('/api/queues/1?forceuser=225staff')
expect(res.statusCode).toBe(202)
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(2)
expect(res2.body).toHaveLength(3)
expect(res2.body[0].id).toBe(2)
})

Expand All @@ -368,7 +399,7 @@ describe('Queues API', () => {
expect(res.statusCode).toBe(202)
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(2)
expect(res2.body).toHaveLength(3)
expect(res2.body[0].id).toBe(2)
})

Expand All @@ -378,7 +409,7 @@ describe('Queues API', () => {
expect(res.body).toEqual({})
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(3)
expect(res2.body).toHaveLength(4)
expect(res2.body[0].id).toBe(1)
expect(res2.body[1].id).toBe(2)
})
Expand All @@ -389,7 +420,7 @@ describe('Queues API', () => {
expect(res.body).toEqual({})
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(3)
expect(res2.body).toHaveLength(4)
expect(res2.body[0].id).toBe(1)
expect(res2.body[1].id).toBe(2)
})
Expand Down Expand Up @@ -463,15 +494,15 @@ describe('Queues API', () => {
expect(res.statusCode).toBe(202)
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(2)
expect(res2.body).toHaveLength(3)
})

test('succeeds for admin', async () => {
const res = await request(app).delete('/api/queues/1?forceuser=admin')
expect(res.statusCode).toBe(202)
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(2)
expect(res2.body).toHaveLength(3)
})

test('fails for course staff of different course', async () => {
Expand All @@ -480,7 +511,7 @@ describe('Queues API', () => {
expect(res.body).toEqual({})
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(3)
expect(res2.body).toHaveLength(4)
})

test('fails for student', async () => {
Expand All @@ -489,7 +520,7 @@ describe('Queues API', () => {
expect(res.body).toEqual({})
const res2 = await request(app).get('/api/queues')
expect(res2.statusCode).toBe(200)
expect(res2.body).toHaveLength(3)
expect(res2.body).toHaveLength(4)
})
})
})
96 changes: 96 additions & 0 deletions src/components/ClosedQueueCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Card, CardBody, CardTitle, CardSubtitle, Button } from 'reactstrap'
import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import faMapMarker from '@fortawesome/fontawesome-free-solid/faMapMarker'
import faQuestion from '@fortawesome/fontawesome-free-solid/faQuestionCircle'

import ShowForCourseStaff from './ShowForCourseStaff'

const ClosedQueueCard = ({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a lot of code duplication - might be better to just have QueueCard take an open prop?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying this way earlier, I can do it like this, but if I did, then closed cards would have the hover animation. If thats okay, then I can switch it so they use the same component

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

earlier an issue was with the color of the card too, but now that would be an easy fix, so hover is the only issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to fix that by just applying a different set of styles?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v true, i was trying something overly complicated last night lol

queue,
courseName,
onDelete,
onUpdate,
...rest
}) => {
const { name: queueName, location, questionCount } = queue

const questionCountText = `${questionCount} Question${
questionCount !== 1 ? 's' : ''
}`
const locationText = location || 'No location specified'

const handleDelete = e => {
e.stopPropagation()
e.preventDefault()
onDelete()
}

const handleUpdate = e => {
e.stopPropagation()
e.preventDefault()
onUpdate()
}

const title = courseName || queueName
const showQueueNameInBody = !!courseName
return (
<Card className="closed-queue-card bg-light" {...rest}>
<CardBody>
<CardTitle className="d-flex flex-wrap align-items-center">
<span className="mb-2 mr-auto pr-3">{title}</span>
<div>
<ShowForCourseStaff courseId={queue.courseId}>
<Button color="danger" size="sm" outline onClick={handleDelete}>
Delete
</Button>
</ShowForCourseStaff>
<ShowForCourseStaff courseId={queue.courseId}>
<Button
color="primary"
size="sm"
className="mr-0 ml-1"
outline
onClick={handleUpdate}
>
Edit
</Button>
</ShowForCourseStaff>
</div>
</CardTitle>
{showQueueNameInBody && (
<CardSubtitle className="mb-2">{queueName}</CardSubtitle>
)}
<div className="text-muted">
<FontAwesomeIcon icon={faMapMarker} fixedWidth className="mr-2" />
{locationText}
<br />
<FontAwesomeIcon icon={faQuestion} fixedWidth className="mr-2" />
{questionCountText}
</div>
</CardBody>
<style global jsx>{`
.closed-queue-card {
transition: all 200ms;
cursor: pointer;
}
`}</style>
</Card>
)
}

ClosedQueueCard.defaultProps = {
courseName: null,
}

ClosedQueueCard.propTypes = {
queue: PropTypes.shape({
courseId: PropTypes.number,
}).isRequired,
courseName: PropTypes.string,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
}

export default ClosedQueueCard
21 changes: 19 additions & 2 deletions src/components/QueueCardList.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Col, Card, CardBody } from 'reactstrap'

import { Router } from '../routes'

import ClosedQueueCard from '../components/ClosedQueueCard'
import QueueCard from '../components/QueueCard'
import QueueEdit from '../components/QueueEdit'
import ConfirmDeleteQueueModal from '../components/ConfirmDeleteQueueModal'
Expand Down Expand Up @@ -89,9 +90,22 @@ class QueueCardList extends React.Component {
queues = this.props.queueIds.map(queueId => {
const queue = this.props.queues[queueId]
const courseName = this.props.courses[queue.courseId].name
if (this.props.openQueue) {
return (
<CardCol key={queue.id}>
<QueueCard
queue={queue}
courseName={this.props.showCourseName ? courseName : null}
onClick={() => handleQueueClick(queue.id)}
onDelete={() => this.deleteQueue(queue.courseId, queue.id)}
onUpdate={() => this.editQueue(queue.id)}
/>
</CardCol>
)
}
return (
<CardCol key={queue.id}>
<QueueCard
<ClosedQueueCard
queue={queue}
courseName={this.props.showCourseName ? courseName : null}
onClick={() => handleQueueClick(queue.id)}
Expand All @@ -106,7 +120,8 @@ class QueueCardList extends React.Component {
<Col>
<Card className="bg-light">
<CardBody className="text-center">
There aren&apos;t any open queues right now
There aren&apos;t any {this.props.openQueue ? 'open' : 'closed'}{' '}
queues right now
</CardBody>
</Card>
</Col>
Expand Down Expand Up @@ -150,6 +165,7 @@ QueueCardList.defaultProps = {
courses: {},
queues: {},
showCourseName: false,
openQueue: true,
}

QueueCardList.propTypes = {
Expand All @@ -166,6 +182,7 @@ QueueCardList.propTypes = {
})
),
showCourseName: PropTypes.bool,
openQueue: PropTypes.bool,
updateQueue: PropTypes.func.isRequired,
deleteQueue: PropTypes.func.isRequired,
}
Expand Down
Loading