Skip to content

Feature load item headers only #113

Merged
merged 35 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1dd473c
Implementation of headers only by passing a boolean to the Item class…
Nov 2, 2020
afaaf81
update documentation for header-only and more efficient jsondata defi…
Nov 3, 2020
0123593
Remove debugging comments
Nov 29, 2020
db08a0f
Change wholeItem to headersOnly and add to Item, Queue and loadAllQueues
Nov 29, 2020
5dc0aa7
Update docs
Nov 29, 2020
21f347c
Update docs
Nov 29, 2020
d8225d8
Add headersOnly flags to API
Nov 30, 2020
fd49dd7
Add headersOnly to docstrings
campb303 Feb 21, 2021
f960cd3
Make Queue objects iterable
campb303 Feb 21, 2021
c435fe1
Update iterable interface to avoid parse error
campb303 Feb 21, 2021
6a1b660
Add example to Queue endpoint docs
campb303 Feb 21, 2021
f52daa5
Merge branch 'staging' into Feature-Load-Item-Headers-Only
campb303 Feb 22, 2021
748a49c
Fix Queue endpoint error from merge
campb303 Feb 22, 2021
666135f
Create ItemProvider w/ Docs
campb303 Feb 22, 2021
27f9fba
Replace activeItem state variable with context provider values
campb303 Feb 22, 2021
abb9a79
Implement code for testing skeleton view UI in ItemBodyView
wrigh393 Feb 22, 2021
c73fb0e
WIP Skeleton view for ItemTable
wrigh393 Feb 23, 2021
4b12f9e
Merge branch 'Feature-Load-Item-Headers-Only' of github.itap.purdue.e…
wrigh393 Feb 23, 2021
c343963
Refactored queue loading
campb303 Feb 23, 2021
b0173b9
Merge branch 'Feature-Load-Item-Headers-Only' of github.itap.purdue.e…
campb303 Feb 23, 2021
5fcb7d7
Implement loading UI
campb303 Feb 23, 2021
ffeb3fe
Remove feature placeholder
campb303 Feb 23, 2021
25f0635
Replace prop with context
campb303 Feb 23, 2021
75de4ad
Remove debug logging
campb303 Feb 23, 2021
d4961b5
Remove hardcoded ID for uniqueness
campb303 Feb 23, 2021
0470bbe
Create timeline skeleton loader
campb303 Feb 23, 2021
abf98df
Add support for static files to react-styleguidist
campb303 Feb 23, 2021
f144667
Add loading state and refactor props
campb303 Mar 1, 2021
2a7b674
Fix validateDOM warning
campb303 Mar 2, 2021
e534ef6
Fix proptype specifications
campb303 Mar 2, 2021
6e7acf1
Fix validateDOM warning
campb303 Mar 2, 2021
d0e5649
Implement async item loading in ItemView
campb303 Mar 12, 2021
322a075
Update ItemView docs
campb303 Mar 12, 2021
6afaf23
Add error page to ItemView
campb303 Mar 12, 2021
ebbafcb
Remove unused icon
campb303 Mar 12, 2021
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
97 changes: 55 additions & 42 deletions api/ECNQueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
Attributes:
queueDirectory: The directory to load queues from.
queuesToIgnore: Queues that will not be loaded when running getQueues()
Raises:
# TODO: Add description(s) of when a ValueError is raised.
ValueError: [description]
"""

#------------------------------------------------------------------------------#
Expand Down Expand Up @@ -62,7 +58,10 @@
#------------------------------------------------------------------------------#

def isValidItemName(name: str) -> bool:
"""Returns true if file name is a valid item name
"""Returns true if file name is a valid item name.
A file name is true if it contains between 1 and 3 integer numbers allowing for
any integer between 0 and 999.
Example:
isValidItemName("21") -> true
Expand All @@ -83,16 +82,23 @@ def isValidItemName(name: str) -> bool:
# Classes
#------------------------------------------------------------------------------#
class Item:
"""A single issue.
"""A chronological representation of an interaction with a user.
Example:
# Create an Item (ce100)
>>> item = Item("ce", 100)
>>> item = Item("ce", 100, headersOnly=false)
# Create an Item without parsing its contents (ce100)
>>> item = Item("ce", 100, headersOnly=True)
Args:
queue (str): The name of the Item's queue.
number (int): The number of the Item.
headersOnly (bool, optional): Whether or not to parse headers only. Defaults to True.
Attributes:
lastUpdated: An ISO 8601 formatted time string showing the last time the file was updated according to the filesystem.
headers: A list of dictionaries containing header keys and values.
content: A list of section dictionaries.
content: A list of section dictionaries (only included if headersOnly is False).
isLocked: A boolean showing whether or not a lockfile for the item is present.
userEmail: The email address of the person who this item is from.
userName: The real name of the person who this item is from.
Expand All @@ -104,21 +110,22 @@ class Item:
department: The most recent department for this item.
dateReceived: The date this item was created.
jsonData: A JSON serializable representation of the Item.
Raises:
ValueError: When the number passed to the constructor cannot be parsed.
"""

def __init__(self, queue: str, number: int) -> None:
def __init__(self, queue: str, number: int, headersOnly: bool = False) -> None:
self.queue = queue
try:
self.number = int(number)
except ValueError:
raise ValueError(" Could not convert \"" +
number + "\" to an integer")

raise ValueError(f'Could not convert "{number}" to an integer')
self.__path = "/".join([queueDirectory, self.queue, str(self.number)])
self.lastUpdated = self.__getLastUpdated()
self.__rawItem = self.__getRawItem()
self.headers = self.__parseHeaders()
self.content = self.__parseSections()
if not headersOnly: self.content = self.__parseSections()
self.isLocked = self.__isLocked()
self.userEmail = self.__parseFromData(data="userEmail")
self.userName = self.__parseFromData(data="userName")
Expand All @@ -129,28 +136,12 @@ def __init__(self, queue: str, number: int) -> None:
self.priority = self.__getMostRecentHeaderByType("Priority")
self.department = self.__getMostRecentHeaderByType("Department")
self.building = self.__getMostRecentHeaderByType("Building")
self.dateReceived = self.__getFormattedDate(
self.__getMostRecentHeaderByType("Date"))
self.dateReceived = self.__getFormattedDate(self.__getMostRecentHeaderByType("Date"))
self.jsonData = {}

# TODO: Autopopulate jsonData w/ __dir__() command. Exclude `^_` and `jsonData`.
self.jsonData = {
"queue": self.queue,
"number": self.number,
"lastUpdated": self.lastUpdated,
"headers": self.headers,
"content": self.content,
"isLocked": self.isLocked,
"userEmail": self.userEmail,
"userName": self.userName,
"userAlias": self.userAlias,
"assignedTo": self.assignedTo,
"subject": self.subject,
"status": self.status,
"priority": self.priority,
"department": self.department,
"building": self.building,
"dateReceived": self.dateReceived
}
for attribute in self.__dir__():
if "_" not in attribute and attribute != "toJson" and attribute != "jsonData":
self.jsonData[attribute] = self.__getattribute__(attribute)

def __getLastUpdated(self) -> str:
"""Returns last modified time of item reported by the filesystem in mm-dd-yy hh:mm am/pm format.
Expand Down Expand Up @@ -1245,24 +1236,31 @@ def toJson(self) -> dict:
def __repr__(self) -> str:
return self.queue + str(self.number)

# TODO: Make Queue iterable using __iter__. See: https://thispointer.com/python-how-to-make-a-class-iterable-create-iterator-class-for-it/
class Queue:
"""A collection of items.
"""A collection of Items.
Example:
# Create a queue (ce)
>>> queue = Queue("ce")
# Create a queue without parsing item contents (ce)
>>> queue = Queue("ce", headersOnly=False)
Args:
queue (str): The name of the queue.
headersOnly (bool, optional): Whether or not to parse headers only. Defaults to True.
Attributes:
name: The name of the queue.
items: A list of Items in the queue.
jsonData: A JSON serializable representation of the Queue.
"""

def __init__(self, name: str) -> None:
def __init__(self, name: str, headersOnly: bool = True) -> None:
self.name = name
self.headersOnly = headersOnly
self.__directory = queueDirectory + "/" + self.name + "/"
self.items = self.__getItems()
self._index = 0

self.jsonData = {
"name": self.name,
Expand All @@ -1283,7 +1281,7 @@ def __getItems(self) -> list:
isFile = True if os.path.isfile(itemPath) else False

if isFile and isValidItemName(item):
items.append(Item(self.name, item))
items.append(Item(self.name, item, headersOnly=self.headersOnly))

return items

Expand All @@ -1309,6 +1307,13 @@ def __len__(self) -> int:
def __repr__(self) -> str:
return f'{self.name}_queue'

# Implements the interable interface requirements by passing direct references
# to the item list's interable values.
def __iter__(self) -> list:
return iter(self.items)
def __next__(self) -> int:
return iter(self.items).__next__()

def getValidQueues() -> list:
"""Returns a list of queues on the filesystem excluding ignored queues.
Expand Down Expand Up @@ -1359,16 +1364,24 @@ def getQueueCounts() -> list:

return sortedQueueInfo


def loadQueues() -> list:
def loadAllQueues(headersOnly: bool = True) -> list:
"""Return a list of Queues for each queue.
Example:
# Load all Queues without parsing Item content
>>> loadAllQueues();
Load all Queues and parsing Item content
>>> loadAllQueues(headersOnly=False)
Args:
headersOnly (bool, optional): Whether or not to parse headers only. Defaults to True.
Returns:
list: list of Queues for each queue.
list: List of Queues for each queue.
"""
queues = []

for queue in getValidQueues():
queues.append(Queue(queue))
queues.append(Queue(queue, headersOnly=headersOnly))

return queues
19 changes: 12 additions & 7 deletions api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ def get(self, queue: str, number: int) -> tuple:
200 (OK): On success.
Example:
/api/ce/100 returns:
{
"lastUpdated": "07-23-20 10:11 PM",
"headers": [...],
Expand All @@ -187,13 +186,21 @@ def get(self, queue: str, number: int) -> tuple:
Returns:
tuple: Item as JSON and HTTP response code.
"""
return (ECNQueue.Item(queue, number).toJson(), 200)

headersOnly = True if request.args.get("headersOnly") == "True" else False
return ECNQueue.Item(queue, number, headersOnly=headersOnly).toJson()

class Queue(Resource):
@jwt_required
def get(self, queues: str) -> tuple:
"""Returns the JSON representation of the queue requested.
Example:
{
"name": ce,
"items": [...]
}
Return Codes:
200 (OK): On success.
Expand All @@ -203,12 +210,12 @@ def get(self, queues: str) -> tuple:
Returns:
tuple: Queues as JSON and HTTP response code.
"""
headersOnly = False if request.args.get("headersOnly") == "False" else True
queues_requested = queues.split("+")

queue_list = []
for queue in queues_requested:
queue_list.append(ECNQueue.Queue(queue).toJson())

queue_list.append(ECNQueue.Queue(queue, headersOnly=headersOnly).toJson())
return (queue_list, 200)

class QueueList(Resource):
Expand All @@ -235,9 +242,7 @@ def get(self) -> tuple:
tuple: Queues and item counts as JSON and HTTP response code.
"""
return (ECNQueue.getQueueCounts(), 200)




api.add_resource(Login, "/login")
api.add_resource(RefreshAccessToken, "/tokens/refresh")
api.add_resource(Item, "/api/<string:queue>/<int:number>")
Expand Down
90 changes: 46 additions & 44 deletions src/components/AppView/AppView.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,59 @@ import QueueSelector from "../QueueSelector/";
import { useToken } from "../AuthProvider/";

export default function AppView({ setDarkMode }){
const [activeItem, setActiveItem] = useState({});
// Create stateful variables.
const [sidebarOpen, setSidebarOpen] = useState(false);
const [queues, setQueues] = useState([]);
const [items, setItems] = useState([]);
const [selectedQueues, setSelectedQueues] = useState([]);
const [queueSelectorOpen, setQueueSelectorOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const access_token = useToken();

// Get Queues from API.
useEffect( _ => {
async function getQueues(){
(async function getQueues(){
if (access_token === null){
return undefined
return undefined;
}

if (queueSelectorOpen){
return undefined
return undefined;
}

if (selectedQueues.length > 0){
let queuesToLoad = "";

for (let selectedQueue of selectedQueues){
queuesToLoad += `+${selectedQueue.name}`;
}
if (selectedQueues.length === 0){
setQueues([])
return undefined;
}

let myHeaders = new Headers();
myHeaders.append("Authorization", `Bearer ${access_token}`);
let requestOptions = { headers: myHeaders };
setIsLoading(true);
let queuesToLoad = "";

const apiResponse = await fetch(`/api/${queuesToLoad}`, requestOptions);
const queueJson = await apiResponse.json();
setQueues(queueJson);
} else {
setQueues([])
if (selectedQueues.length === 1){
queuesToLoad = selectedQueues[0].name;
}
}
getQueues();
else if (selectedQueues.length > 0){
selectedQueues.forEach( (queue, index) => (
index === 0
? queuesToLoad += queue.name
: queuesToLoad += `+${queue.name}`
));
}

let myHeaders = new Headers();
myHeaders.append("Authorization", `Bearer ${access_token}`);
let requestOptions = { headers: myHeaders };

const apiResponse = await fetch(`/api/${queuesToLoad}`, requestOptions);
const queueJson = await apiResponse.json();

setQueues(queueJson);
setIsLoading(false)
})();
}, [selectedQueues, access_token, queueSelectorOpen]);

// Populate items array.
useEffect( _ => {
let tempItems = [];
for (let queue of queues){
Expand Down Expand Up @@ -94,7 +107,6 @@ export default function AppView({ setDarkMode }){

return(
<Box component={Paper} display="flex" square elevation={0}>

<Box className={classes.leftCol}>
<ItemTableAppBar title="webqueue2" setDarkMode={setDarkMode} />
<QueueSelector
Expand All @@ -103,34 +115,24 @@ export default function AppView({ setDarkMode }){
value={selectedQueues}
setValue={setSelectedQueues}
/>
<ItemTable data={items} rowCanBeSelected={sidebarOpen}/>
<ItemTable data={items} rowCanBeSelected={sidebarOpen} loading={isLoading}/>
</Box>

<Box className={clsx(classes.rightCol, sidebarOpen && classes.rightColShift)}>
{items.length === 0 ? null :
<Route
path="/:queue/:number"
render={({ match }) => {
const item = items.find((item) => {
return item.queue === match.params.queue && item.number === Number(match.params.number);
});

if (item === undefined) {
return (
<ItemViewAppBar title="Item Not Found" setSidebarOpen={setSidebarOpen} />
);
}

setActiveItem(item);

return (
<>
<ItemViewAppBar title={activeItem["queue"] + " " + activeItem["number"]} setSidebarOpen={setSidebarOpen} />
<ItemView activeItem={activeItem} />
</>
);
}
}
render={({ match }) => (
<>
<ItemViewAppBar
title={`${match.params.queue} ${match.params.number}`}
setSidebarOpen={setSidebarOpen}
/>
<ItemView
queue={match.params.queue}
number={match.params.number}
/>
</>
)}
/>
}
</Box>
Expand Down
Loading