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