Skip to content

Commit

Permalink
Merge branch 'staging' into Enhancement-ItemTable-sortButton
Browse files Browse the repository at this point in the history
  • Loading branch information
wrigh393 authored Oct 30, 2020
2 parents 190c75f + 605d165 commit c8782a4
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 114 deletions.
213 changes: 153 additions & 60 deletions api/ECNQueue.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
# TODO: Add ECNQueue module documentation
"""A library for interacting with Purdue ECN's ticketing system.
This library allows interacting with queue Items (called Items) and collections
of items (called Queues).
Example:
# Create a single Item (ce100)
>>> item = Item("ce", 100)
# Get the sender's email address from an Item
>>> item = Item("ce", 100)
>>> item.userEmail
# Create an entire Queue (ce)
>>> queue = Queue("ce")
# Get the number of items in a Queue
>>> queue = Queue("ce")
>>> numItems = len(queue)
# Get all queues (and their items)
>>> queues = getQueues()
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]
"""

#------------------------------------------------------------------------------#
# Imports
#------------------------------------------------------------------------------#
import os, time, email, re, datetime
from dateutil.parser import parse
from dateutil import tz
from typing import Union
import json

Expand All @@ -21,28 +50,43 @@
queueDirectory = os.path.join(currentFileDirectoryParent, "q-snapshot")

# Queues to not load in getQueues()
queuesToIgnore = ["archives", "drafts", "inbox"]

# B/c some items don't have a From field
# See coral259
queuesToIgnore.append("coral")
queuesToIgnore = ["archives", "drafts", "inbox", "coral"]



#------------------------------------------------------------------------------#
# Classes
#------------------------------------------------------------------------------#
class Item:
# TODO: Add Item class documentation
"""A single issue.
Example:
# Create an Item (ce100)
>>> item = Item("ce", 100)
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.
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.
userAlias: The Purdue career account alias of the person this item is from.
assignedTo: The Purdue career account alias of the person this item is assigned to
subject: The subject of the original message for this item.
status: The most recent status update for the item.
priority: The most recent priority for this item.
department: The most recent department for this item.
dateReceived: The date this item was created.
jsonData: A JSON serializable representation of the Item.
"""

def __init__(self, queue: str, number: int) -> None:

self.queue = queue
try:
self.number = int(number)
except ValueError:
raise ValueError("Could not convert \"" + number + "\" to an integer")
#self.number = number
raise ValueError(" Could not convert \"" + number + "\" to an integer")

self.__path = "/".join([queueDirectory, self.queue, str(self.number)])
self.lastUpdated = self.__getLastUpdated()
Expand All @@ -53,14 +97,15 @@ def __init__(self, queue: str, number: int) -> None:
self.userEmail = self.__parseFromData(data="userEmail")
self.userName = self.__parseFromData(data="userName")
self.userAlias = self.__getUserAlias()
self.assignedTo = self.__getAssignedTo()
self.assignedTo = self.__getMostRecentHeaderByType("Assigned-To")
self.subject = self.__getMostRecentHeaderByType("Subject")
self.status = self.__getMostRecentHeaderByType("Status")
self.priority = self.__getMostRecentHeaderByType("Priority")
self.department = self.__getMostRecentHeaderByType("Department")
self.building = self.__getMostRecentHeaderByType("Building")
self.dateReceived = self.__getFormattedDate(self.__getMostRecentHeaderByType("Date"))

# TODO: Autopopulate jsonData w/ __dir__() command. Exclude `^_` and `jsonData`.
self.jsonData = {
"queue": self.queue,
"number": self.number,
Expand Down Expand Up @@ -89,9 +134,10 @@ def __getLastUpdated(self) -> str:
Returns:
str: last modified time of item reported by the filesystem in mm-dd-yy hh:mm am/pm format.
"""
# TODO: Simplify this code block by allowing __getFormattedDate to accept milliseconds since the epoch.
unixTime = os.path.getmtime(self.__path)
formattedTime = time.strftime('%m-%d-%y %I:%M %p', time.localtime(unixTime))
return formattedTime
return self.__getFormattedDate(formattedTime)

def __getRawItem(self) -> list:
"""Returns a list of all lines in the item file
Expand Down Expand Up @@ -149,7 +195,8 @@ def __parseHeaders(self) -> list:

headerString += line

message = email.message_from_string(headerString + "".join(self.__getContent()))
# message = email.message_from_string(headerString + "".join(self.__getContent()))
message = email.message_from_string(headerString)

headers = []
for key in message.keys():
Expand All @@ -159,26 +206,6 @@ def __parseHeaders(self) -> list:

# TODO: Implement attachment parsing

def __getContent(self) -> list:
"""Returns a dictionary of lines of the item body.
Example:
"Hello. I need help.\\n\\n*** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\\nDont Delete" becomes
[
"Hello. I need help.\\n",
"\\n",
"*** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\\n",
"Don't Delete",
]
Returns:
list: Lines of the body item.
"""
contentStart = self.__getHeaderBoundary() + 1
contentEnd = len(self.__rawItem) - 1
return self.__rawItem[ contentStart : contentEnd ]


def __parseSections(self) -> list:
# List of all item events
sections = []
Expand Down Expand Up @@ -228,7 +255,12 @@ def __parseSections(self) -> list:
line = self.__rawItem[lineNumber]

# Looks for a starting delimiter and explicity excludes the reply-from-user ending delimiter
if line.startswith("***") or line.startswith("===") and not line.startswith("===="):
if (line.startswith("*** Edited by: ") or
line.startswith("*** Replied by: ") or
line.startswith("*** Status updated by: ") or
line == "=== Additional information supplied by user ===\n" and not
line == "===============================================\n"
):

# Sets the delimiter type based on the pattern within the delimiters list
for delimiter in delimiters:
Expand Down Expand Up @@ -278,7 +310,7 @@ def __parseSections(self) -> list:
# Checks for a parse error and appends it, returning the sections list which stops the parsing
if editInfo["type"] == "parse_error":
sections.append(editInfo)
return sections
return self.__getSortedSections(sections)

# Appends the edit dictionary to sections
sections.append(editInfo)
Expand All @@ -290,7 +322,7 @@ def __parseSections(self) -> list:
# Checks for a parse error and appends it, returning the sections list which stops the parsing
if replyToInfo["type"] == "parse_error":
sections.append(replyToInfo)
return sections
return self.__getSortedSections(sections)

# Appends the reply-to to sections
sections.append(replyToInfo)
Expand All @@ -301,7 +333,7 @@ def __parseSections(self) -> list:

if statusInfo["type"] == "parse_error":
sections.append(statusInfo)
return sections
return self.__getSortedSections(sections)

# Appends the status to sections
sections.append(statusInfo)
Expand All @@ -312,12 +344,15 @@ def __parseSections(self) -> list:

if replyFromInfo["type"] == "parse_error":
sections.append(replyFromInfo)
return sections
return self.__getSortedSections(sections)

# Appends the replyFrom to sections
sections.append(replyFromInfo)

return sections

sortedSections = self.__getSortedSections(sections)

return sortedSections
#return sections

def __directoryParsing(self, directoryStartLine: int) -> dict:
"""Returns a dictionary with directory information
Expand Down Expand Up @@ -585,7 +620,7 @@ def __editParsing(self, content: list, lineNum: int) -> dict:
editInfo = {}

for count, line in enumerate(content):
if line.startswith("===="):
if line == "===============================================\n" :
errorMessage = "Reply-from-user ending delimter encountered without Reply-from-user starting delimter"
return self.__errorParsing(line, lineNum + count + 1, errorMessage)

Expand Down Expand Up @@ -645,7 +680,7 @@ def __replyToParsing(self, content: list, lineNum: int) -> dict:
delimiterLine = content[0]

for count, line in enumerate(content):
if line.startswith("===="):
if line == "===============================================\n":
errorMessage = "Reply-from-user ending delimter encountered without Reply-from-user starting delimter"
return self.__errorParsing(line, lineNum + count + 1, errorMessage)

Expand Down Expand Up @@ -695,7 +730,7 @@ def __statusParsing(self, content: list, lineNum: int) -> dict:
delimiterLine = content[0]

for count, line in enumerate(content):
if line.startswith("===="):
if line == "===============================================\n":
errorMessage = "Reply-from-user ending delimter encountered without Reply-from-user starting delimter"
return self.__errorParsing(line, lineNum + count + 1, errorMessage)

Expand Down Expand Up @@ -793,15 +828,24 @@ def __userReplyParsing(self, replyContent: list, lineNumber: int) -> dict:
)
except:
lenReplyFromHeaders = len(replyFromHeaders)

replyFromHeaders[lenReplyFromHeaders - 1]["content"] = replyFromHeaders[lenReplyFromHeaders - 1]["content"] + " " + line
if lenReplyFromHeaders == 0:
errorMessage = ("Expected reply-from-user header information:\n" +
"=== Additional information supplied by user ===\n" +
"\n" +
"[Header Type]: [Header Value]\n" +
"\n"
)
return self.__errorParsing(line, lineNumber + lineNum + 1, errorMessage)

else:
replyFromHeaders[lenReplyFromHeaders - 1]["content"] = replyFromHeaders[lenReplyFromHeaders - 1]["content"] + " " + line

linesToRemove.append(lineNum)
#Checks for a newline and breaks for loop on second occurance of a newline
if line == "\n":
newLineCounter = newLineCounter + 1

elif line.startswith("===="):
elif line == "===============================================\n":
endingDelimiterCount = endingDelimiterCount + 1

elif line.startswith("From: ") and newLineCounter == 1:
Expand Down Expand Up @@ -880,7 +924,13 @@ def __getFormattedSectionContent(self, sectionContent: list) -> list:
"""
# Continually removes the first line of sectionContent if it is a newline or delimiter in each iteration
while len(sectionContent) > 1:
if sectionContent[0] == "\n" or sectionContent[0].startswith("***") or sectionContent[0].startswith("===") :
if (sectionContent[0] == "\n" or
sectionContent[0].startswith("*** Edited by: ") or
sectionContent[0].startswith("*** Replied by: ") or
sectionContent[0].startswith("*** Status updated by: ") or
sectionContent[0] == "=== Additional information supplied by user ===\n" or
sectionContent[0] == "===============================================\n"
):
sectionContent.pop(0)
else:
# Breaks the loop if the first line isn't a newline or delimiter
Expand All @@ -891,7 +941,9 @@ def __getFormattedSectionContent(self, sectionContent: list) -> list:
# Initializes the Length of sectionContent each iteration of the loop
sectionContentLength = len(sectionContent)

if sectionContent[sectionContentLength -1] == "\n" or sectionContent[sectionContentLength -1].startswith("===="):
if (sectionContent[sectionContentLength -1] == "\n" or
sectionContent[sectionContentLength -1] == "===============================================\n"
):
sectionContent.pop(sectionContentLength - 1)
else:
# Breaks the loop if the last line isn't a newline or delimiter
Expand Down Expand Up @@ -942,6 +994,45 @@ def __errorParsing(self, line: str, lineNum: int, expectedSyntax: str) -> dict:
# returns the error dictionary
return errorDictionary

def __getSortedSections(self, sectionsList: list) -> list:
"""Sorts the sections chronologically by datetime
Example:
[example] need to do
Args:
sections (list): the list of sections to be sorted
Returns:
list: a list of sections sorted by datetime
"""
sectionsLength = len(sectionsList)
sortedSections = []
oldestSection = {}

while len(sortedSections) < sectionsLength:

for iteration, currentSection in enumerate(sectionsList):

if currentSection["type"] == "directory_information":
sortedSections.append(currentSection)
sectionsList.remove(currentSection)
break

if iteration == 0:
oldestSection = currentSection

#datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S.%f')

elif parse(currentSection["datetime"]) < parse(oldestSection["datetime"]):
oldestSection = currentSection

if iteration == len(sectionsList) - 1:
sortedSections.append(oldestSection)
sectionsList.remove(oldestSection)

return sortedSections

def __isLocked(self) -> Union[str, bool]:
"""Returns a string info about the lock if true and a bool False if false
Expand Down Expand Up @@ -1022,16 +1113,6 @@ def __getUserAlias(self) -> str:
"""
emailUser, emailDomain = self.userEmail.split("@")
return emailUser if emailDomain.endswith("purdue.edu") else ""

def __getAssignedTo(self) -> str:
"""Returns the alias of the person this item was most recently assigned to.
Returns empty string if this item isn't assigned.
Returns:
str: Alias of the person item is assigned to or empty string.
"""
assignedTo = self.__getMostRecentHeaderByType("Assigned-To")
return assignedTo

def __getFormattedDate(self, date: str) -> str:
"""Returns the date/time formatted as RFC 8601 YYYY-MM-DDTHH:MM:SS+00:00.
Expand All @@ -1042,7 +1123,8 @@ def __getFormattedDate(self, date: str) -> str:
str: Properly formatted date/time recieved or empty string.
"""
try:
parsedDate = parse(date)
# This date is never meant to be used. The default attribute is just to set timezone.
parsedDate = parse(date, default=datetime.datetime(1970, 0, 1, tzinfo=tz.gettz('EDT')))
except:
return ""

Expand All @@ -1061,8 +1143,19 @@ 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:
# TODO: Add Queue class documentation
"""A collection of items.
Example:
# Create a queue (ce)
>>> queue = Queue("ce")
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:
self.name = name
Expand Down
Loading

0 comments on commit c8782a4

Please sign in to comment.