Skip to content

Commit

Permalink
Added React and Webpack
Browse files Browse the repository at this point in the history
- Added React components in `assets` folder
- Grade Distribution view is mostly complete
- Assignment Planning view is a work in progress
- Files Accessed view is not yet started
- Added webpack to compile React components to be used in Django
- Dockerfile now uses multi build stages (so webpack and js libraries can be built outside the main container)
- New django-webpack-loader library will include the compiled React components based on WEBPACK_LOADER settings in dashboard/settings.py
- dashboard/templates/frontend/index.html is the template used by react. App entry point is at ‘<div id="root"></div>’
- ‘/test/courses/grades/{courseId}’ route now renders Grades react app. The {courseId} specified in URL is used to fetch data from the grade_distribution endpoint.
- Modified grades distribution API call to send short course_id, no longer necessary to add ‘settings.UDW_ID_PREFIX’ to front-end.
- Using react router, it is possible to slowly switch dashboards/pages over to react one at a time
  • Loading branch information
justin0022 authored and pushyamig committed Jul 15, 2019
1 parent 7d36040 commit afd74b5
Show file tree
Hide file tree
Showing 76 changed files with 25,629 additions and 65 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,6 @@ dmypy.json

.data/
.data
node_modules
node_modules

webpack-stats.json
63 changes: 38 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,44 +1,57 @@
# FROM directive instructing base image to build upon
FROM python:3.6
# build react components for production mode
FROM node:11.10-alpine AS node-webpack
WORKDIR /usr/src/app

# NOTE: package.json and webpack.config.js not likely to change between dev builds
COPY package.json webpack.config.js /usr/src/app/
RUN npm install

COPY requirements.txt /requirements.txt
# NOTE: assets/ likely to change between dev builds
COPY assets /usr/src/app/assets
RUN npm run prod && \
# src is no longer needed (saves time for collect static)
rm -rf /usr/src/app/assets/src

RUN pip install -r /requirements.txt

# Yarn is not yet in the repo, need to add it
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

# apt-utils needs to be installed separately
RUN apt-get update && \
apt-get install -y --no-install-recommends vim-tiny jq nodejs yarn python3-dev xmlsec1 cron && \
apt-get clean -y

# COPY startup script into known file location in container
COPY start.sh /start.sh
# build node libraries for production mode
FROM node-webpack AS node-prod-deps

RUN npm install --save wait-port@"~0.2.2" && \
npm prune --production && \
# This is needed to clean up the examples files as these cause collectstatic to fail (and take up extra space)
find /usr/src/app/node_modules -type d -name "examples" -print0 | xargs -0 rm -rf

# FROM directive instructing base image to build upon
FROM python:3.6 AS app

# EXPOSE port 5000 to allow communication to/from server
EXPOSE 5000
WORKDIR /code/
COPY . /code/
WORKDIR /code

COPY manage.py /manage.py
# NOTE: requirements.txt not likely to change between dev builds
COPY requirements.txt /code/requirements.txt
RUN apt-get update && \
apt-get install -y --no-install-recommends vim-tiny jq python3-dev xmlsec1 cron && \
apt-get clean -y && \
pip install -r requirements.txt

COPY data/* /data/
# NOTE: project files likely to change between dev builds
COPY . /code/
# copy built react and node libraries for production mode
COPY --from=node-prod-deps /usr/src/app/package-lock.json /code/package-lock.json
COPY --from=node-prod-deps /usr/src/app/webpack-stats.json /code/webpack-stats.json
COPY --from=node-prod-deps /usr/src/app/assets /code/assets
COPY --from=node-prod-deps /usr/src/app/node_modules /code/node_modules

# Install wait-port globally, install rest from the package
RUN yarn global add wait-port@"~0.2.2" && \
yarn install && \
# This is needed to clean up the examples files as these cause collectstatic to fail (and take up extra space)
find /usr/lib/node_modules /code/node_modules -type d -name "examples" -print0 | xargs -0 rm -rf && \
# This DJANGO_SECRET_KEY is set here just so collectstatic runs with an empty key. It can be set to anything
echo yes | DJANGO_SECRET_KEY="collectstatic" python manage.py collectstatic --verbosity 0
RUN echo yes | DJANGO_SECRET_KEY="collectstatic" python manage.py collectstatic --verbosity 0

# Sets the local timezone of the docker image
ARG TZ
ENV TZ ${TZ:-America/Detroit}
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

CMD ["/start.sh"]
CMD ["/code/start.sh"]
# done!
64 changes: 64 additions & 0 deletions assets/src/__tests__/d3/createBarChart.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* global describe, test, expect */
import createBarChart from '../../components/d3/createBarChart'

const barChartData = Object.freeze(
[
{
'label': 'Bob',
'data': 12
},
{
'label': 'Robin',
'data': 34
},
{
'label': 'Anne',
'data': 78
},
{
'label': 'Mark',
'data': 23
},
{
'label': 'Joe',
'data': 10
},
{
'label': 'Eve',
'data': 44
},
{
'label': 'Karen',
'data': 4
}
]
)

describe('createBarChart', () => {
test('should build a bar chart', () => {
const div = document.createElement('div')
createBarChart({ data: barChartData, width: 1000, height: 500, el: div })
const svg = div.children[0]
expect(svg).toBeDefined()
expect(svg.tagName).toEqual('svg')
expect(Number(svg.getAttribute('width'))).toBeGreaterThan(0)
expect(Number(svg.getAttribute('height'))).toBeGreaterThan(0)

const tagNames = Array.from(svg.children).map(x => x.tagName)
expect(tagNames).toEqual(['rect', 'rect', 'rect', 'rect', 'rect', 'rect', 'rect', 'g', 'g'])

const rects = Array.from(svg.children).filter(x => x.tagName === 'rect')
const [x, y, width, height] = rects.reduce((acc, curRect) => {
acc[0].push(Number(curRect.getAttribute('x')))
acc[1].push(Number(curRect.getAttribute('y')))
acc[2].push(Number(curRect.getAttribute('width')))
acc[3].push(Number(curRect.getAttribute('height')))
return acc
}, [[], [], [], []])

x.forEach(x => expect(x).toBeGreaterThan(0))
y.forEach(x => expect(x).toBeGreaterThan(0))
width.forEach(x => expect(x).toBeGreaterThan(0))
height.forEach(x => expect(x).toBeGreaterThan(0))
})
})
82 changes: 82 additions & 0 deletions assets/src/components/AvatarModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react'
import { withStyles } from '@material-ui/core/styles'
import Grid from '@material-ui/core/Grid'
import Divider from '@material-ui/core/Divider'
import Typography from '@material-ui/core/Typography'
import Avatar from '@material-ui/core/Avatar'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import LogoutIcon from '@material-ui/icons/ExitToApp'
import HelpIcon from '@material-ui/icons/HelpOutline'
import SettingsIcon from '@material-ui/icons/Settings'

const styles = theme => ({
root: {
flexGrow: 1
},
typography: {
marginTop: theme.spacing.unit * 2
},
avatar: {
padding: '10px',
marginTop: '10px',
marginBottom: '10px',
marginLeft: 'auto',
marginRight: 'auto'
}
})

function AvatarModal (props) {
const {
classes,
user
} = props
return (
<div className={classes.root}>
<Grid container>
<Grid item xs={4}>
<Avatar className={classes.avatar}>
{user.firstName.slice(0, 1)}{user.lastName.slice(0, 1)}
</Avatar>
</Grid>
<Grid item xs={8} container direction='column'>
<Typography
className={classes.typography}
variant='subtitle2'>
{`${user.firstName} ${user.lastName}`}
</Typography>
<Typography variant='subtitle2'>{user.email}</Typography>
</Grid>
<Grid item xs={12}>
<List>
<Divider />
<ListItem button>
<ListItemIcon>
<HelpIcon />
</ListItemIcon>
<ListItemText inset primary='Help' onClick={() => (window.location.href = 'https://sites.google.com/umich.edu/my-learning-analytics-help/home/grade-distribution')} />
</ListItem>
<Divider />
<ListItem button>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText inset primary='Settings' />
</ListItem>
<Divider />
<ListItem button>
<ListItemIcon>
<LogoutIcon />
</ListItemIcon>
<ListItemText inset primary='Logout' onClick={() => (window.location.href = `${window.location.hostname}:${window.location.port}/accounts/logout`)} />
</ListItem>
</List>
</Grid>
</Grid>
</div>
)
}

export default withStyles(styles)(AvatarModal)
11 changes: 11 additions & 0 deletions assets/src/components/BarChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createBarChart from './d3/createBarChart'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const BarChart = compose(
withResponsiveness,
createChartComponent
)(createBarChart)

export default BarChart
13 changes: 13 additions & 0 deletions assets/src/components/CourseProgress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import createProgressChart from './d3/createProgressChart'
import compose from '../util/compose'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'

// probably compose two lines here, one for current, another for max possible

const CourseProgress = compose(
withResponsiveness,
createChartComponent
)(createProgressChart)

export default CourseProgress
11 changes: 11 additions & 0 deletions assets/src/components/DonutChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createDonutChart from './d3/createDonutChart'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const DonutChart = compose(
withResponsiveness,
createChartComponent
)(createDonutChart)

export default DonutChart
36 changes: 36 additions & 0 deletions assets/src/components/EmojiFeedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useEffect, memo } from 'react'
import emojiFeedback from '@justin0022/emoji-feedback'
import { withStyles } from '@material-ui/core/styles'
import withPopover from './withPopover'
import compose from '../util/compose'

const styles = ({
feedback: {
width: '310px',
height: '350px',
padding: '12px'
}
})

const EmojiFeedback = memo(props => {
const {
endpoints,
id,
options,
classes
} = props

useEffect(() => {
const feedback = emojiFeedback()
feedback.init(id, endpoints, options)
})

return (
<div id={id} className={classes.feedback} />
)
})

export default compose(
withStyles(styles),
withPopover
)(EmojiFeedback)
Empty file.
9 changes: 9 additions & 0 deletions assets/src/components/GradeDistribution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import createHistogram from './d3/createHistogram'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

export default compose(
withResponsiveness,
createChartComponent
)(createHistogram)
11 changes: 11 additions & 0 deletions assets/src/components/GroupedBarChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createGroupedBarChart from './d3/createGroupedBarChart'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const GroupedBarChart = compose(
withResponsiveness,
createChartComponent
)(createGroupedBarChart)

export default GroupedBarChart
11 changes: 11 additions & 0 deletions assets/src/components/Histogram.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createHistogram from './d3/createHistogram'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const Histogram = compose(
withResponsiveness,
createChartComponent
)(createHistogram)

export default Histogram
11 changes: 11 additions & 0 deletions assets/src/components/HorizontalBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createHorizontalBar from './d3/createHorizontalBar'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const HorizontalBar = compose(
withResponsiveness,
createChartComponent
)(createHorizontalBar)

export default HorizontalBar
11 changes: 11 additions & 0 deletions assets/src/components/HorizontalBarChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createHorizontalBarChart from './d3/createHorizontalBarChart'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const HorizontalBarChart = compose(
withResponsiveness,
createChartComponent
)(createHorizontalBarChart)

export default HorizontalBarChart
11 changes: 11 additions & 0 deletions assets/src/components/LineChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createLineChart from './d3/createLineChart'
import withResponsiveness from './withResponsiveness'
import createChartComponent from './createChartComponent'
import compose from '../util/compose'

const LineChart = compose(
withResponsiveness,
createChartComponent
)(createLineChart)

export default LineChart

0 comments on commit afd74b5

Please sign in to comment.