diff --git a/.gitignore b/.gitignore index e238c91..184e9b6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ yarn-error.log* /api/venv __pycache__/ venv-manager.log -/api/.env \ No newline at end of file +/api/.env +/api/webqueueapi.egg-info \ No newline at end of file diff --git a/api/ECNQueue.py b/api/ECNQueue.py index 2cfcf98..17d9c5a 100644 --- a/api/ECNQueue.py +++ b/api/ECNQueue.py @@ -212,7 +212,7 @@ def __parseHeaders(self) -> list: # Example: # [ce] QTime-Updated-By: campb303 becomes # QTime-Updated-By: campb303 - queuePrefixPattern = re.compile("\[.*\] {1}") + queuePrefixPattern = re.compile(r"\[.*?\] {1}") for lineNumber in range(self.__getHeaderBoundary()): line = self.__rawItem[lineNumber] lineHasQueuePrefix = queuePrefixPattern.match(line) @@ -263,6 +263,12 @@ def __parseSections(self) -> list: for assignment in assignementLsit: sections.append(assignment) + # Checks for empty content within an item and returns and + if contentEnd <= contentStart: + blankInitialMessage = self.__initialMessageParsing([""]) + sections.append(blankInitialMessage) + return sections + # Checks for Directory Identifiers if self.__rawItem[contentStart] == "\n" and self.__rawItem[contentStart + 1].startswith("\t"): @@ -915,6 +921,10 @@ def __userReplyParsing(self, replyContent: list, lineNumber: int) -> dict: if line == "\n": newLineCounter = newLineCounter + 1 + if newLineCounter == 2 and "datetime" not in replyFromInfo.keys(): + errorMessage = "Expected \"Date: [datetime]\" in the header info" + return self.__errorParsing(line, lineNumber + lineNum + 1, errorMessage) + elif line == "===============================================\n": endingDelimiterCount = endingDelimiterCount + 1 @@ -1190,7 +1200,19 @@ def __getUserAlias(self) -> str: Returns: str: User's Career Account alias if present or empty string """ - emailUser, emailDomain = self.userEmail.split("@") + + + try: + emailUser, emailDomain = self.userEmail.split("@") + + # Returns an error parse if the self.useremail doesn't contain exactally one "@" symbol + except ValueError: + # Parses through the self.headers list to find the "From" header and its line number + for lineNum, header in enumerate(self.headers): + if header["type"] == "From": + headerString = header["type"] + ": " + header["content"] + return self.__errorParsing(headerString, lineNum + 1, "Expected valid email Address") + return emailUser if emailDomain.endswith("purdue.edu") else "" def __getFormattedDate(self, date: str) -> str: @@ -1331,7 +1353,11 @@ def getQueueCounts() -> list: possibleItems = os.listdir(queueDirectory + "/" + queue) validItems = [isValidItemName for file in possibleItems] queueInfo.append( {"name": queue, "number_of_items": len(validItems)} ) - return queueInfo + + # Sorts list of queue info alphabetically + sortedQueueInfo = sorted(queueInfo, key = lambda queueInfoList: queueInfoList['name']) + + return sortedQueueInfo def loadQueues() -> list: @@ -1345,4 +1371,4 @@ def loadQueues() -> list: for queue in getValidQueues(): queues.append(Queue(queue)) - return queues + return queues \ No newline at end of file diff --git a/api/requirements.txt b/api/requirements.txt index d87bdac..59e3e3c 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -11,4 +11,9 @@ Flask-RESTful python-dateutil Flask-JWT-Extended # Prevent upgrade to 2.x until Flask-JWT-Extended is updated to support it -PyJWT == 1.* \ No newline at end of file +PyJWT == 1.* + +# API Documentation +mkdocs +mkdocs-material +mkautodoc \ No newline at end of file diff --git a/src/components/ItemTable/ItemTable.js b/src/components/ItemTable/ItemTable.js index c4fd422..d94dc72 100644 --- a/src/components/ItemTable/ItemTable.js +++ b/src/components/ItemTable/ItemTable.js @@ -6,20 +6,20 @@ import { useHistory } from "react-router-dom"; import RelativeTime from "react-relative-time"; import ItemTableFilter from "../ItemTableFilter/" import { ArrowDownward, ArrowUpward } from "@material-ui/icons"; - +import ItemTableCell from "../ItemTableCell"; +import LastUpdatedCell from "../LastUpdatedCell/"; export default function ItemTable({ data, rowCanBeSelected }) { const [selectedRow, setSelectedRow] = useState({ queue: null, number: null }); const theme = useTheme(); const useStyles = makeStyles({ - // Fully visible for active icons - activeSortIcon: { - opacity: 1, - }, - // Half visible for inactive icons - inactiveSortIcon: { - opacity: 0.2, + hoverBackgroundColor: { + "&:hover": { + // The !important is placed here to enforce CSS specificity. + // See: https://material-ui.com/styles/advanced/#css-injection-order + backgroundColor: `${theme.palette.primary[200]} !important`, + }, }, rowSelected: { backgroundColor: theme.palette.type === 'light' ? theme.palette.primary[100] : theme.palette.primary[600], @@ -39,6 +39,7 @@ export default function ItemTable({ data, rowCanBeSelected }) { const history = useHistory(); + // See React Table Column Settings: https://react-table.tanstack.com/docs/api/useTable#column-properties const columns = React.useMemo( () => [ { Header: 'Queue', accessor: 'queue', }, @@ -51,8 +52,11 @@ export default function ItemTable({ data, rowCanBeSelected }) { { Header: 'Department', accessor: 'department' }, { Header: 'Building', accessor: 'building' }, { Header: 'Date Received', accessor: 'dateReceived', sortInverted: true, Cell: ({ value }) => }, + { Header: 'Last Updated', accessor: 'lastUpdated', }, + { Header: 'Department', accessor: 'department' }, + { Header: 'Building', accessor: 'building' }, + { Header: 'Date Received', accessor: 'dateReceived', }, ], []); - const tableInstance = useTable( { columns, @@ -67,7 +71,7 @@ export default function ItemTable({ data, rowCanBeSelected }) { onChange={(event) => setFilter(event.target.value)} /> ); - } + }, }, initialState: { sortBy: [ @@ -83,7 +87,11 @@ export default function ItemTable({ data, rowCanBeSelected }) { return ( - +
{headerGroups.map(headerGroup => ( @@ -130,11 +138,12 @@ export default function ItemTable({ data, rowCanBeSelected }) { ))} - {rows.map((row, i) => { + {rows.map((row) => { prepareRow(row); let isSelected = selectedRow.queue === row.original.queue && selectedRow.number === row.original.number return ( { history.push(`/${row.original.queue}/${row.original.number}`); setSelectedRow({ queue: row.original.queue, number: row.original.number }); @@ -142,16 +151,37 @@ export default function ItemTable({ data, rowCanBeSelected }) { // This functionality should be achieved by using the selected prop and // overriding the selected class but this applied the secondary color at 0.08% opacity. // Overridding the root class is a workaround. - classes={{ root: (isSelected && rowCanBeSelected) ? classes.rowSelected : classes.bandedRows }} - {...row.getRowProps()} > + classes={{ + root: (isSelected && rowCanBeSelected) ? classes.rowSelected : classes.bandedRows, + hover: classes.hoverBackgroundColor + }} + {...row.getRowProps()} + > {row.cells.map(cell => ( - - {cell.render("Cell")} - - ))} + cell.render(_ => { + switch (cell.column.id) { + case "dateReceived": + return ( + + + + ); + case "lastUpdated": + return ( + + ); + default: + return ( + + {cell.value} + + ); + } + }) + ))}; ); })} diff --git a/src/components/ItemTableCell/ItemTableCell.js b/src/components/ItemTableCell/ItemTableCell.js new file mode 100644 index 0000000..cf13a74 --- /dev/null +++ b/src/components/ItemTableCell/ItemTableCell.js @@ -0,0 +1,37 @@ +import React from 'react' +import PropTypes from "prop-types"; +import { makeStyles, TableCell, useTheme } from '@material-ui/core' + +export default function ItemTableCell({ children, TableCellProps }) { + const theme = useTheme(); + const useStyles = makeStyles({ + columnBorders: { + // Add column borders + borderLeftWidth: "1px", + borderLeftStyle: "solid", + borderColor: theme.palette.type === "light" ? theme.palette.grey[300] : theme.palette.grey[500] + }, + }) + + const classes = useStyles(); + return ( + + {children} + + ); +} + +ItemTableCell.propTypes = { + /** Child object passed to display cell data. */ + "children": PropTypes.object, + /** Props applied to the TableCell component. */ + "TableCellProps": PropTypes.object +}; + +ItemTableCell.defaultProps = { + "children": {}, + "TableCellProps": {}, +}; \ No newline at end of file diff --git a/src/components/ItemTableCell/ItemTableCell.md b/src/components/ItemTableCell/ItemTableCell.md new file mode 100644 index 0000000..5a3bcea --- /dev/null +++ b/src/components/ItemTableCell/ItemTableCell.md @@ -0,0 +1,38 @@ +The ItemTableCell wraps an [MUI TableCell](https://material-ui.com/api/table-cell/) and adds styling. + +## Default Usage +```jsx +import { Paper } from '@material-ui/core'; +import ItemTableCell from "./ItemTableCell"; + + + + Hello, moto! + + +``` + +```jsx static + + + Hello, moto! + + +``` + +## Forwarded TableCell Props +Props can be passed to the TableCell component using the TableCellProps prop. +```jsx +import { Paper } from '@material-ui/core'; +import ItemTableCell from "./ItemTableCell"; + + + Hello, moto! + +``` + +```jsx static + + Hello, moto! + +``` \ No newline at end of file diff --git a/src/components/ItemTableCell/index.js b/src/components/ItemTableCell/index.js new file mode 100644 index 0000000..a358a7b --- /dev/null +++ b/src/components/ItemTableCell/index.js @@ -0,0 +1,3 @@ +import ItemTableCell from "./ItemTableCell"; + +export default ItemTableCell; \ No newline at end of file diff --git a/src/components/LastUpdatedCell/LastUpdatedCell.js b/src/components/LastUpdatedCell/LastUpdatedCell.js new file mode 100644 index 0000000..82221fc --- /dev/null +++ b/src/components/LastUpdatedCell/LastUpdatedCell.js @@ -0,0 +1,73 @@ +import React from 'react' +import PropTypes from "prop-types"; +import { useTheme } from '@material-ui/core'; +import { red } from '@material-ui/core/colors'; +import RelativeTime from 'react-relative-time'; +import ItemTableCell from '../ItemTableCell'; + +export default function LastUpdatedCell({ time, ItemTableCellProps }) { + + const theme = useTheme(); + + /** + * Returns a color showing how old an item is. + * @param {string} time ISO 8601 formatted time string. + * @example + * // Returns "#e57373" + * timeToBackgroundColor("2021-01-04T11:47:00-0500") + * @returns {string} Hex color code. + */ + const timeToBackgroundColor = (time) => { + const lastUpdated = new Date(time).getTime(); + const now = new Date().getTime(); + const timeDelta = now - lastUpdated; + + const day = 24 * 60 * 60 * 1000; + const week = day * 7; + const month = week * 4; + + let backgroundColor = theme.palette.background.paper; + + // 1-6 days old + if (timeDelta > day && timeDelta <= week) { + backgroundColor = red[100]; + } + // 7-28 days old + else if (timeDelta > week && timeDelta <= month) { + backgroundColor = red[300]; + } + // 29+ days old + else if (timeDelta > month) { + backgroundColor = red[500]; + } + + return backgroundColor; + } + + // Insert the calculated background color into props so it isn't overriden. + // Inspired by: https://github.com/mui-org/material-ui/issues/19479 + ItemTableCellProps = { + ...ItemTableCellProps, + style: { + ...ItemTableCellProps.style, + backgroundColor: timeToBackgroundColor(time) + } + }; + + return ( + + + + ); +}; + +LastUpdatedCell.propTypes = { + /** ISO 8601 formatted time string, Date object or UNIX time. See: https://www.npmjs.com/package/react-relative-time */ + "time": PropTypes.string.isRequired, + /** Props to be applied to the ItemTableCell. */ + "ItemTableCellProps": PropTypes.object, +}; + +LastUpdatedCell.defaultProps = { + "ItemTableCellProps": {}, +}; \ No newline at end of file diff --git a/src/components/LastUpdatedCell/LastUpdatedCell.md b/src/components/LastUpdatedCell/LastUpdatedCell.md new file mode 100644 index 0000000..ce823a5 --- /dev/null +++ b/src/components/LastUpdatedCell/LastUpdatedCell.md @@ -0,0 +1,38 @@ +A table cell that takes a time value and returns a relative time with a background color to indicate how old an item is. + +The LastUpdatedCell wraps an [ItemTableCell](/#/Components/ItemTableCell) + +## Default Usage +```jsx +import { Paper } from '@material-ui/core'; +import LastUpdatedCell from "./LastUpdatedCell"; + +let today = new Date(); +let threeDaysAgo = new Date().setDate(today.getDate() - 3); +let lastWeek = new Date().setDate(today.getDate() - 8); +let lastMonth = new Date().setDate(today.getDate() - 28); + + + { /* Today */ } + + { /* Three Days Ago */ } + + { /* Last Week */ } + + { /* Last Month */ } + + +``` + +```jsx static + + { /* Today */ } + + { /* Three Days Ago */ } + + { /* Last Week */ } + + { /* Last Month */ } + + +``` \ No newline at end of file diff --git a/src/components/LastUpdatedCell/index.js b/src/components/LastUpdatedCell/index.js new file mode 100644 index 0000000..35380d3 --- /dev/null +++ b/src/components/LastUpdatedCell/index.js @@ -0,0 +1,3 @@ +import LastUpdatedCell from "./LastUpdatedCell"; + +export default LastUpdatedCell; \ No newline at end of file