From a1f5f6c7920d39b2ea9601588889d8784b200564 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 12 Jul 2021 15:22:04 -0400 Subject: [PATCH 1/4] Remove old API files Co-authored-by: Tyler wright --- api/ECNQueue.py | 1387 ------------------------------------------- api/ECNQueue_old.py | 473 --------------- api/api.py | 255 -------- 3 files changed, 2115 deletions(-) delete mode 100644 api/ECNQueue.py delete mode 100755 api/ECNQueue_old.py delete mode 100644 api/api.py diff --git a/api/ECNQueue.py b/api/ECNQueue.py deleted file mode 100644 index e3f4c72..0000000 --- a/api/ECNQueue.py +++ /dev/null @@ -1,1387 +0,0 @@ -"""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() -""" - -#------------------------------------------------------------------------------# -# Imports -#------------------------------------------------------------------------------# -import os -import time -import email -import re -import datetime -from dateutil.parser import parse -from dateutil import tz -from typing import Union -import json - - -#------------------------------------------------------------------------------# -# Configuration -#------------------------------------------------------------------------------# - -# The directory where queue items are -currentFilePath = __file__ -currentFileDirectory = os.path.dirname(currentFilePath) -currentFileDirectoryParent = os.path.dirname(currentFileDirectory) -queueDirectory = os.path.join(currentFileDirectoryParent, "q-snapshot") - -# Queues to not load in getQueues() -queuesToIgnore = ["archives", "drafts", "inbox", "coral"] - - - -#------------------------------------------------------------------------------# -# Utilities -#------------------------------------------------------------------------------# - -def isValidItemName(name: str) -> bool: - """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 - isValidItemName("twentyone") -> false - - Args: - name (str): The name to test. - - Returns: - bool: Name is valid item name. - """ - itemPattern = re.compile("^[0123456789]{1,3}$") - return True if itemPattern.match(name) else False - - - -#------------------------------------------------------------------------------# -# Classes -#------------------------------------------------------------------------------# -class Item: - """A chronological representation of an interaction with a user. - - Example: - # Create an Item (ce100) - >>> 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 (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. - 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. - - Raises: - ValueError: When the number passed to the constructor cannot be parsed. - """ - - def __init__(self, queue: str, number: int, headersOnly: bool = False) -> None: - self.queue = queue - try: - self.number = int(number) - except ValueError: - 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() - if not headersOnly: self.content = self.__parseSections() - self.isLocked = self.__isLocked() - self.userEmail = self.__parseFromData(data="userEmail") - self.userName = self.__parseFromData(data="userName") - self.userAlias = self.__getUserAlias() - 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")) - self.jsonData = {} - - 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. - - Example: - 07-23-20 10:34 AM - - 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 self.__getFormattedDate(formattedTime) - - def __getRawItem(self) -> list: - """Returns a list of all lines in the item file - - Returns: - list: List of all the lines in the item file - """ - with open(self.__path, errors="replace") as file: - return file.readlines() - - def __getHeaderBoundary(self) -> int: - """Returns the 0 based line number where the Item headers stop. - - Example: The header end would be on line 13 - 12: X-ECN-Queue-Original-URL: - 13: - 14: I need help. - - Returns: - int: line number where the Item headers end - """ - for lineNumber, line in enumerate(self.__rawItem): - if line == "\n": - return lineNumber - - def __parseHeaders(self) -> list: - """Returns a list containing dictionaries of header type and data. - Removes queue prefixes and whitespace. - - Examples: - "[ce] QStatus: Dont Delete\\nFrom: Justin Campbell \\n" - becomes - [ - {"QStatus": "Don't Delete"}, - {"From": "Justin Campbell "} - ] - - Returns: - list: Header dicts - """ - headerString = "" - - # Remove '[queue] ' prefixes: - # Example: - # [ce] QTime-Updated-By: campb303 becomes - # QTime-Updated-By: campb303 - queuePrefixPattern = re.compile(r"\[.*?\] {1}") - for lineNumber in range(self.__getHeaderBoundary()): - line = self.__rawItem[lineNumber] - lineHasQueuePrefix = queuePrefixPattern.match(line) - - if lineHasQueuePrefix: - queuePrefix = line[lineHasQueuePrefix.regs[0] - [0]: lineHasQueuePrefix.regs[0][1]] - line = line.replace(queuePrefix, "") - - headerString += line - - # message = email.message_from_string(headerString + "".join(self.__getContent())) - message = email.message_from_string(headerString) - - headers = [] - dateHeaders=[ - "QStatus-Updated-Time", - "Status-Updated-Time", - "Edited-Time", - "QTime-Updated-Time", - "Merged-Time", - "Time-Updated-Time", - "Replied-Time", - "Assigned-To-Updated-Time", - "QAssigned-To-Updated-Time", - "Date", - "Sent" - ] - - for key in message.keys(): - headers.append({"type": key, "content": self.__getFormattedDate(message[key]) if key in dateHeaders else message[key]}) - - return headers - - # TODO: Implement attachment parsing - - def __parseSections(self) -> list: - # List of all item events - sections = [] - - contentStart = self.__getHeaderBoundary() + 1 - contentEnd = len(self.__rawItem) - 1 - - # List of assignments for the item - assignementLsit = self.__assignmentParsing(contentStart) - - # Appends each assignment individually to sections - 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"): - - directoryStartLine = contentStart + 1 - - # Parses the directory information and returns a dictionary of directory values - directoryInfo = self.__directoryParsing(directoryStartLine) - - # Appends Directory Information into the sections array - sections.append(directoryInfo) - - # Sets the initial message start to the next line after all directory lines and newlines - contentStart = contentStart + len(directoryInfo) + 1 - - # The start line, type, and end line for item events - sectionBoundaries = [] - - # Delimiter info - delimiters = [ - {"name": "edit", "pattern": "*** Edited"}, - {"name": "status", "pattern": "*** Status"}, - {"name": "replyToUser", "pattern": "*** Replied"}, - {"name": "replyFromUser", "pattern": "=== "}, - ] - - # Signifies that there is an initial message to parse - initialMessageSection = True - - # Parses the entire contents of the message, stores everything before any delimiter as the initial message - # and the line number of any delimiters as well as the type - for lineNumber in range(contentStart, contentEnd + 1): - - line = self.__rawItem[lineNumber] - - # Looks for a starting delimiter and explicity excludes the reply-from-user ending delimiter - 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: - - if line.startswith(delimiter["pattern"]): - sectionBoundaries.append( - {"start": lineNumber, "type": delimiter["name"]}) - break - - # If a starting delimiter was encountered, then there is no initial message - if initialMessageSection: - initialMessageSection = False - - elif initialMessageSection == True: - # Delimiter not encountered yet, so append initial message starting line as the current lin number - sectionBoundaries.append( - {"start": lineNumber, "type": "initial_message"}) - initialMessageSection = False - - # Used to set the end line of the last delimiter - sectionBoundaries.append({"start": contentEnd + 1}) - - # Sets the end of the section boundary to the begining of the next section boundary - for boundaryIndex in range(0, len(sectionBoundaries) - 1): - - sectionBoundaries[boundaryIndex]["end"] = sectionBoundaries[boundaryIndex + 1]["start"] - - # Remove End of File boundary since the line number has been assigned to the last delimiter - del sectionBoundaries[-1] - - # Parses through all the boundaries in section boundaries - for boundary in sectionBoundaries: - - # Sets line to the first line of the boundary (which is always the delimiter) - line = self.__rawItem[boundary["start"]] - - # Returns all of the lines within the current section - sectionContent = self.__rawItem[boundary["start"]: boundary["end"]] - - # Appends an initial message dictionary to sections - if boundary["type"] == "initial_message": - initialMessageDictionary = self.__initialMessageParsing( - sectionContent) - sections.append(initialMessageDictionary) - - elif boundary["type"] == "edit": - # Returns a dictionary with edit information - editInfo = self.__editParsing( - sectionContent, boundary["start"]) - - # 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 self.__getSortedSections(sections) - - # Appends the edit dictionary to sections - sections.append(editInfo) - - elif boundary["type"] == "replyToUser": - # Returns a dictionary with reply-to information - replyToInfo = self.__replyToParsing( - sectionContent, boundary["start"]) - - # 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 self.__getSortedSections(sections) - - # Appends the reply-to to sections - sections.append(replyToInfo) - - elif boundary["type"] == "status": - # Returns a dictionary with status information - statusInfo = self.__statusParsing( - sectionContent, boundary["start"]) - - if statusInfo["type"] == "parse_error": - sections.append(statusInfo) - return self.__getSortedSections(sections) - - # Appends the status to sections - sections.append(statusInfo) - - elif boundary["type"] == "replyFromUser": - # Returns a dictionary with userReply information - replyFromInfo = self.__userReplyParsing( - sectionContent, boundary["start"]) - - if replyFromInfo["type"] == "parse_error": - sections.append(replyFromInfo) - return self.__getSortedSections(sections) - - # Appends the replyFrom to sections - sections.append(replyFromInfo) - - sortedSections = self.__getSortedSections(sections) - - return sortedSections - # return sections - - def __directoryParsing(self, directoryStartLine: int) -> dict: - """Returns a dictionary with directory information - - Example: - Name: Nestor Fabian Rodriguez Buitrago - Login: rodri563 - Computer: ce-205-38 (128.46.205.67) - Location: HAMP G230 - Email: rodri563@purdue.edu - Phone: 7654766893 - Office: HAMP G230 - UNIX Dir: /home/bridge/b/rodri563 - Zero Dir: U=\\bridge.ecn.purdue.edu\rodri563 - User ECNDB: http://eng.purdue.edu/jump/2e8399a - Host ECNDB: http://eng.purdue.edu/jump/2e83999 - Subject: Autocad installation - - Args: - directoryStartLine (int): line number within the item that the directory starts on - - Returns: - dict: dictionary that splits each line within the directory into a key and a value - """ - directoryInformation = {"type": "directory_information"} - - directoryPossibleKeys = [ - "Name", - "Login", - "Computer", - "Location", - "Email", - "Phone", - "Office", - "UNIX Dir", - "Zero Dir", - "User ECNDB", - "Host ECNDB", - "Subject" - ] - # Executies until the directory start line is greater than the directory ending line - while True: - - # Returns the line number at directory start line - info = self.__rawItem[directoryStartLine] - - # Breaks the loop if it encountrs a newline, signifying the end of the directory information - if info == "\n": - - break - - else: - - # Removes white including space, newlines, and tabs from the directory info line - strippedInfo = info.strip() - - # Attempts to find ": " but will accept ":", denoting a blank entry for a directory item - if ": " in strippedInfo: - - # Seperates the directory info line into two variables, the first variable being the key, the second being the value - # swt1 - key, value = strippedInfo.split(": ", 1) - - if key in directoryPossibleKeys: - # Adds the key value pair to the directory info dictionary - directoryInformation[key] = value - else: - # Casts the list type on to a dictionary - dictionaryList = list(directoryInformation) - # Length of dictionary list - lenDictionaryList = len(dictionaryList) - # The last key appended to the directory dictionary - lastKeyAppended = dictionaryList[lenDictionaryList - 1] - - directoryInformation[lastKeyAppended] = directoryInformation[lastKeyAppended] + \ - " " + strippedInfo - - elif ":" in strippedInfo: - - # Seperates the directory info line into two variables, the first variable being the key, the second being the value - key, value = strippedInfo.split(":", 1) - - if key in directoryPossibleKeys: - # Adds the key value pair to the directory info dictionary - directoryInformation[key] = value - else: - # Casts the list type on to a dictionary - dictionaryList = list(directoryInformation) - # Length of dictionary list - lenDictionaryList = len(dictionaryList) - # The last key appended to the directory dictionary - lastKeyAppended = dictionaryList[lenDictionaryList - 1] - - directoryInformation[lastKeyAppended] = directoryInformation[lastKeyAppended] + \ - " " + strippedInfo - - # Signifies that this line belongs to the most previous line - elif ": " not in strippedInfo and ":" not in strippedInfo: - # Casts the list type on to a dictionary - dictionaryList = list(directoryInformation) - # Length of dictionary list - lenDictionaryList = len(dictionaryList) - # The last key appended to the directory dictionary - lastKeyAppended = dictionaryList[lenDictionaryList - 1] - - directoryInformation[lastKeyAppended] = directoryInformation[lastKeyAppended] + \ - " " + strippedInfo - # Counter to denote the end of the directory - directoryStartLine = directoryStartLine + 1 - - # Returns the directory information dictionary - return directoryInformation - - def __assignmentParsing(self, contentStart: int) -> list: - """Returns a list with assignment information dictionaries - - Example: - Assigned-To: campb303 - Assigned-To-Updated-Time: Tue, 23 Jun 2020 13:27:00 EDT - Assigned-To-Updated-By: campb303 - - Args: - contentStart (int): line number where the content starts - - Returns: - list: [ - {"type": "assignment", - "datetime": datetime of the assignment, - "by": user who initiated the assignment, - "to": user who was assigned - }, - ] - """ - assignmentList = [] - - # Assignment Information - assignedBy = "" - assignedDateTime = "" - assignedTo = "" - - # Parses the header looking for assignment delimeters and stores info into their respective variables - for headerContent in range(0, contentStart): - - line = self.__rawItem[headerContent] - - # Gets who the Item was assigned to - if line.startswith("Assigned-To: "): - - assignedTo = ( - re.search("(?<=Assigned-To: )(.*)", line)).group() - - # Gets the date the Item was assigned - elif line.startswith("Assigned-To-Updated-Time: "): - - dateFromLine = ( - re.search("(?<=Assigned-To-Updated-Time: )(.*)", line)).group() - - assignedDateTime = self.__getFormattedDate(dateFromLine) - - # Gets who assigned the Item - elif line.startswith("Assigned-To-Updated-By: "): - - assignedBy = ( - re.search("(?<=Assigned-To-Updated-By: )(.*)", line)).group() - - # Appends the assignment to the sections list - assignmentList.append( - {"type": "assignment", - "datetime": assignedDateTime, - "by": assignedBy, - "to": assignedTo} - ) - - return assignmentList - - def __initialMessageParsing(self, content: list) -> dict: - """Returns a dictionary with initial message information - - Example: - \n - Testtest\n - \n - - Args: - content (list): content of the initial message - - Returns: - dict: - "type": "initial_message", - "datetime": datetime the initial message was sent, - "from_name": from_name, - "from_email": user_email, - "to": [{email, name}], - "cc": [{email, name}], - "subject": initial message subject - "content": content of the initial message - """ - initialMessageDictionary = {} - - initialMessageDictionary["type"] = "initial_message" - - # Gets the initial message date from the header - rawMessageDateStr = self.__getMostRecentHeaderByType("Date") - - # Sets datetime in the intialMessage dictionary to UTC formatted date - initialMessageDictionary["datetime"] = self.__getFormattedDate( - rawMessageDateStr) - - initialMessageDictionary["from_name"] = self.__parseFromData( - data="userName") - - initialMessageDictionary["from_email"] = self.__parseFromData( - data="userEmail") - - # Stores list of dictionaries for the recipients of the initial message - initialMessageDictionary["to"] = [] - - # Parses the header looking for recipients of the initial message and stores it in a list of tuples - rawMessageRecipientsList = email.utils.getaddresses( - [self.__getMostRecentHeaderByType("To")]) - - # Parses the CC list and stores the cc recipient information in a list of dictionaries - for recipients in rawMessageRecipientsList: - - initialMessageDictionary["to"].append( - {"name": recipients[0], - "email": recipients[1]} - ) - - # Stores list of dictionaries for CC information - initialMessageDictionary["cc"] = [] - - # Parses the header looking for CC recipients of the initial message and stores it in a list of tuples - rawMessageCCList = email.utils.getaddresses( - [self.__getMostRecentHeaderByType("CC")]) - - # Parses the CC list and stores the cc recipient information in a list of dictionaries - for ccRecipients in rawMessageCCList: - - initialMessageDictionary["cc"].append( - {"name": ccRecipients[0], - "email": ccRecipients[1]} - ) - - initialMessageDictionary["subject"] = self.__getMostRecentHeaderByType( - "Subject") - - # Removes unecessary newlines from the begining and the end of the initial message - initialMessageDictionary["content"] = self.__getFormattedSectionContent( - content) - - return initialMessageDictionary - - def __editParsing(self, content: list, lineNum: int) -> dict: - """Returns a dictionary with edit information - - Example: - *** Edited by: campb303 at: 06/23/20 13:27:56 ***\n - \n - This be an edit my boy\n - \n - \n - \n - - Args: - content (list): content of an edit - lineNum (int): line number of an edit within an item - - Returns: - dict: a dictionary with these keys, - "type": "edi", - "by": initiator of the edit, - "datetime": datetime of the edit, - "content": content of the edit - """ - - # Edit Info dictionary - editInfo = {} - - for count, line in enumerate(content): - if line == "===============================================\n": - errorMessage = "Reply-from-user ending delimter encountered without Reply-from-user starting delimter" - return self.__errorParsing(line, lineNum + count + 1, errorMessage) - - editInfo["type"] = "edit" - - delimiterLine = content[0] - # Parses for the author of the edit, which is located between the "*** Edited by: " and " at:" substrings - try: - editInfo["by"] = ( - re.search("(?<=\*{3} Edited by: )(.*)(?= at:)", delimiterLine)).group() - except: - errorMessage = "*** Edited by: [username] at: [date and time] ***\n" - return self.__errorParsing(delimiterLine, lineNum, errorMessage) - - try: - # Parses for the date and time of the edit, which is located between the " at: " and "***\n" substrings - dateTimeString = ( - re.search("(?<= at: )(.*)(?= \*\*\*\n)", delimiterLine)).group() - except: - # Returns an error message if there is no space after "at:" - errorMessage = "*** Edited by: [username] at: [date and time] ***\n" - return self.__errorParsing(delimiterLine, lineNum, errorMessage) - - # Attempts to format the date and time into utc format - editInfo["datetime"] = self.__getFormattedDate(dateTimeString) - - # Remove the delimiter String and unecessary newlines - editInfo["content"] = self.__getFormattedSectionContent(content) - - return editInfo - - def __replyToParsing(self, content: list, lineNum: int) -> dict: - """Returns a dictionary with reply to user information - - Example: - *** Replied by: campb303 at: 06/23/20 13:28:18 ***\n - \n - This be a reply my son\n - \n - Justin\n - ECN\n - \n - - Args: - content (list): content of a reply to user - lineNum (int): line number of a reply to user in an item - - Returns: - dict: a dictionary with these keys, - "type": "reply_to_user", - "by": initiator of the reply to user, - "datetime": datetime of the reply to user, - "content": content of the reply to user - """ - replyInfo = {} - - replyInfo["type"] = "reply_to_user" - - delimiterLine = content[0] - - for count, line in enumerate(content): - if line == "===============================================\n": - errorMessage = "Reply-from-user ending delimter encountered without Reply-from-user starting delimter" - return self.__errorParsing(line, lineNum + count + 1, errorMessage) - - try: - # Parses for the author of the reply, which is located between the "*** Replied by: " and " at:" substrings - replyInfo["by"] = ( - re.search("(?<=\*{3} Replied by: )(.*)(?= at:)", delimiterLine)).group() - except: - errorMessage = "*** Replied by: [username] at: [date and time] ***\n" - return self.__errorParsing(delimiterLine, lineNum, errorMessage) - - # Parses for the date and time of the reply, which is located between the " at: " and "***\n" substrings - try: - dateTimeString = ( - re.search("(?<= at: )(.*)(?= \*\*\*\n)", delimiterLine)).group() - except: - errorMessage = "*** Replied by: [username] at: [date and time] ***\n" - return self.__errorParsing(delimiterLine, lineNum, errorMessage) - - # Formats date to UTC - replyInfo["datetime"] = self.__getFormattedDate(dateTimeString) - - replyInfo["content"] = self.__getFormattedSectionContent(content) - - return replyInfo - - def __statusParsing(self, content: list, lineNum: int) -> dict: - """Returns a dictionary with status information - - Example: - *** Status updated by: campb303 at: 6/23/2020 13:26:55 ***\n - Dont Delete\n - - Args: - content (list): The content of a status update - lineNum (int): The line number of a status update in an item - - Returns: - dict: a dictionary with these keys, - "type": "status", - "by": initiator of the status update, - "datetime": datetime of the status update, - "content": content of the status update - """ - statusInfo = {} - - statusInfo["type"] = "status" - - delimiterLine = content[0] - - for count, line in enumerate(content): - if line == "===============================================\n": - errorMessage = "Reply-from-user ending delimter encountered without Reply-from-user starting delimter" - return self.__errorParsing(line, lineNum + count + 1, errorMessage) - - # Parses for the author of the status change, which is located between the "*** Status updated by: " and " at:" substrings - try: - statusInfo["by"] = ( - re.search("(?<=\*{3} Status updated by: )(.*)(?= at:)", delimiterLine)).group() - except: - errorMessage = "*** Status updated by: [username] at: [date and time] ***\n" - - return self.__errorParsing(delimiterLine, lineNum, errorMessage) - - # Parses for the date and time of the status change, which is located between the " at: " and "***\n" substrings - try: - dateTimeString = re.search( - "(?<= at: )(.*)(?= \*\*\*\n)", delimiterLine).group() - except: - errorMessage = "*** Status updated by: [username] at: [date and time] ***\n" - - return self.__errorParsing(delimiterLine, lineNum, errorMessage) - - # Formats the date to UTC - statusInfo["datetime"] = self.__getFormattedDate(dateTimeString) - - # Remove the delimiter String and unecessary newlines - statusInfo["content"] = self.__getFormattedSectionContent(content) - - return statusInfo - - def __userReplyParsing(self, replyContent: list, lineNumber: int) -> dict: - """Returns a dictionary with user reply information - - Example: - === Additional information supplied by user ===\n - \n - Subject: Re: Beepboop\n - From: Justin Campbell \n - Date: Tue, 23 Jun 2020 13:30:45 -0400\n - X-ECN-Queue-Original-Path: /home/pier/e/queue/Attachments/inbox/2020-06-23/212-original.txt\n - X-ECN-Queue-Original-URL: https://engineering.purdue.edu/webqueue/Attachments/inbox/2020-06-23/212-original.txt\n - \n - Huzzah!\n - \n - ===============================================\n - \n - Args: - replyContent (list): The entire section of a reply-from-user - lineNumber (int): The line number of the begining of a reply-from-user section within and item - - Returns: - dict: a dictionary with these keys, - "type": "reply_from_user", - "from_name": name of the user that sent the reply, - "from_email": email of the user that sent the reply, - "subject": subject of the reply, - "datetime": the datetime of the reply, - "cc": [ - {"name": name of the carbon copied recipient, - "email": email of the carbon copied recipient - }, - ] - "content": content of the reply - "headers": [ - {"type": headerType, - "content": content - }, - ] - """ - replyFromInfo = {} - - replyFromInfo["type"] = "reply_from_user" - - replyFromHeaders = [] - newLineCounter = 0 - endingDelimiterCount = 0 - - # Delimiter information line numbers to remove from reply from user - linesToRemove = [] - - # Parses the section content looking for any line that starts with a metadata, also tracks the line - # number with the enumerate function - for lineNum, line in enumerate(replyContent): - - if endingDelimiterCount == 0 and lineNum == len(replyContent) - 1: - errorMessage = "Did not encounter a reply-from-user ending delimiter" - return self.__errorParsing(line, lineNumber + lineNum + 1, errorMessage) - - if newLineCounter == 1 and line != "\n": - - try: - # Append header information for each headr line - headerType, content = line.split(": ", 1) - replyFromHeaders.append( - {"type": headerType, - "content": content - } - ) - except: - lenReplyFromHeaders = len(replyFromHeaders) - 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 - - 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 - - elif line.startswith("From: ") and newLineCounter == 1: - # Returns a list of one tuples with a name stored in the first index of the tuple and an email stored in the second index of the tuple - emailList = email.utils.getaddresses([line]) - replyFromInfo["from_name"] = emailList[0][0] - replyFromInfo["from_email"] = emailList[0][1] - - elif line.startswith("Subject: ") and newLineCounter == 1: - # Matches everything after "Subject: " - try: - subjectStr = ( - re.search("(?<=Subject: )(.*)", line)).group() - except: - errorMessage = "Expeted syntax of \"Subject: [subject]\"" - return self.__errorParsing(line, lineNumber + lineNum + 1, errorMessage) - - # Formatts the date to UTC - replyFromInfo["subject"] = subjectStr - - elif line.startswith("Date: ") and newLineCounter == 1: - # Matches everything after "Date: " - try: - dateStr = (re.search("(?<=Date: )(.*)", line)).group() - except: - errorMessage = "\"Date: [datetime]\"" - return self.__errorParsing(line, lineNumber + lineNum + 1, errorMessage) - - # Formatts the date to UTC - replyFromInfo["datetime"] = self.__getFormattedDate(dateStr) - - elif line.startswith("Cc: ") and newLineCounter == 1: - - replyFromInfo["cc"] = [] - - # Returns a list of tuples with email information - recipientsList = email.utils.getaddresses([line]) - - # Parses through the cc tuple list - for cc in recipientsList: - # Stores the cc information in a dictionary and appends it to the ccRecipientsList - replyFromInfo["cc"].append( - {"name": cc[0], - "email": cc[1]} - ) - - # Deletes reduntant lines from the message content in reverse order - for lineNum in sorted(linesToRemove, reverse=True): - replyContent.pop(lineNum) - - # Strips any unnecessary newlines or any delimiters frm the message content - replyFromInfo["content"] = self.__getFormattedSectionContent( - replyContent) - - replyFromInfo["headers"] = replyFromHeaders - - return replyFromInfo - - def __getFormattedSectionContent(self, sectionContent: list) -> list: - """Returns a list with message content that is stripped of unnecessary newlines and begining delimiters - - Example: - *** Edited by: mph at: 02/21/20 10:27:16 ***\n - \n - Still need to rename machines - but the networking issue now seems to \n - be resolved via another ticket.\n - \n - \n - \n - \n - \n - - Args: - sectionContent (list): The section content of a parsed section - - Returns: - list: the section content of a parsed section without any delimiters and unnecessary newlines - """ - # 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("*** 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 - break - - # Continually removes the last line of sectionContent if it is a newline or delimiter in each iteration - while len(sectionContent) > 1: - # Initializes the Length of sectionContent each iteration of the loop - sectionContentLength = len(sectionContent) - - 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 - break - - return sectionContent - - def __errorParsing(self, line: str, lineNum: int, expectedSyntax: str) -> dict: - """Returns a dictionary with error parse information when a line is malformed - - Example: - "*** Status updated by: ewhile at: 5/7/2020 10:59:11 *** sharing between\n" - - Args: - line (str): line of that threw error - lineNum (int): line number in the item that threw error - expectedSyntax (str): a message stating the syntax the line should follow - - Returns: - dict: a dictionary with these keys, - "type": "parse_error", - "datetime": time the error was encountered, - "file_path": path of the item with erroneos line, - "expected": expectedSyntax, - "got": line, - "line_num": lineNum - """ - errorDictionary = {} - - # Type - errorDictionary["type"] = "parse_error" - - # Dateime of the parse error - errorDictionary["datetime"] = self.__getFormattedDate( - str(datetime.datetime.now())) - - # Item filepath - errorDictionary["file_path"] = self.__path - - # Expected value - errorDictionary["expected"] = expectedSyntax - - # line that threw error - errorDictionary["got"] = line - - # line number that threw error - errorDictionary["line_num"] = lineNum - - # 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 - - Example: A file is locked - "CE 100 is locked by campb303 using qvi" - - Example: a file is not locked - False - - Returns: - Union[str, bool]: String with info about lock if true, bool False if false - """ - lockFile = self.__path + ".lck" - if os.path.exists(lockFile): - with open(lockFile) as file: - lockInfo = file.readline().split(" ") - lockedBy = lockInfo[4] - lockedUsing = lockInfo[1] - return "{queue} {number} is locked by {lockedBy} using {lockedUsing}".format(queue=self.queue, number=self.number, lockedBy=lockedBy, lockedUsing=lockedUsing) - else: - return False - - def __getMostRecentHeaderByType(self, headerType: str) -> str: - """Return the data of most recent header of the given type. - If no header of that type exists, return an empty string. - - Example: Requesting a Status header that does exist - __getMostRecentHeaderByType("Status") - becomes "Waiting for Reply" - - Example: Requesting a Status header that doesn't exist - __getMostRecentHeaderByType("Status") - becomes "" - - Args: - headerType (str): Type of header to return. - - Returns: - str: data of most recent header of the given type or empty string. - """ - for header in self.headers: - if header["type"] == headerType: - return header["content"] - return "" - - def __parseFromData(self, data: str) -> str: - """Parse From header and return requested data. - Returns empty string if requested data is unavailable. - - Examples: From data is "From: Campbell, Justin " - __parseFromData(data="userName") returns "Campbell, Justin" - __parseFromData(data="userEmail") returns "campb303@purdue.edu" - - Args: - data (str): The data desired; can be "userName" or "userEmail". - - Returns: - str: userName, userEmail or empty string. - """ - fromHeader = self.__getMostRecentHeaderByType("From") - userName, userEmail = email.utils.parseaddr(fromHeader) - - if data == "userName": - return userName - elif data == "userEmail": - return userEmail - else: - raise ValueError( - "data='" + str(data) + "' is not a valid option. data must be \"userName\" or \"userEmail\".") - - def __getUserAlias(self) -> str: - """Returns user's Career Account alias if present. - If Career Account alias isn't present, returns empty string. - - Example: Email from campb303@purdue.edu - userAlias = "campb303" - - Example: Email from spam@spammer.net - userAlias = "" - - Returns: - str: User's Career Account alias if present or empty string - """ - - - 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: - """Returns the date/time formatted as RFC 8601 YYYY-MM-DDTHH:MM:SS+00:00. - Returns empty string if the string argument passed to the function is not a datetime. - See: https://en.wikipedia.org/wiki/ISO_8601 - - Returns: - str: Properly formatted date/time recieved or empty string. - """ - try: - # This date is never meant to be used. The default attribute is just to set timezone. - parsedDate = parse(date, default=datetime.datetime( - 1970, 1, 1, tzinfo=tz.gettz('EDT'))) - except: - return "" - - parsedDateString = parsedDate.strftime("%Y-%m-%dT%H:%M:%S%z") - - return parsedDateString - - def toJson(self) -> dict: - """Returns a JSON safe representation of the item. - - Returns: - dict: JSON safe representation of the item. - """ - return self.jsonData - - def __repr__(self) -> str: - return self.queue + str(self.number) - -class Queue: - """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, 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, - "length": len(self) - } - - def __getItems(self) -> list: - """Returns a list of items for this Queue - - Returns: - list: a list of items for this Queue - """ - items = [] - - for item in os.listdir(self.__directory): - itemPath = self.__directory + "/" + item - - isFile = True if os.path.isfile(itemPath) else False - - if isFile and isValidItemName(item): - items.append(Item(self.name, item, headersOnly=self.headersOnly)) - - return items - - def toJson(self) -> dict: - """Return JSON safe representation of the Queue - - The JSON representation of every item in the Queue is added to the - Queue's JSON data then the Queue's JSON data is returned. - - Returns: - dict: JSON safe representation of the Queue - """ - items = [] - for item in self.items: - items.append(item.toJson()) - self.jsonData["items"] = items - - return self.jsonData - - def __len__(self) -> int: - return len(self.items) - - 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. - - Example: - ["bidc", "me", "ce"] - - Returns: - list: Valid queues - """ - queues = [] - - for file in os.listdir(queueDirectory): - currentFile = queueDirectory + "/" + file - isDirectory = os.path.isdir(currentFile) - isValid = file not in queuesToIgnore - - if isDirectory and isValid: - queues.append(file) - - return queues - -def getQueueCounts() -> list: - """Returns a list of dictionaries with the number of items in each queue. - - Example: - [ - { - name: "me", - number_of_items: 42 - }, - { - name: "bidc", - number_of_items: 3 - } - ] - - Returns: - list: Dictionaries with the number of items in each queue. - """ - queueInfo = [] - for queue in getValidQueues(): - possibleItems = os.listdir(queueDirectory + "/" + queue) - validItems = [file for file in possibleItems if isValidItemName(file)] - queueInfo.append( {"name": queue, "number_of_items": len(validItems)} ) - - # Sorts list of queue info alphabetically - sortedQueueInfo = sorted(queueInfo, key = lambda queueInfoList: queueInfoList['name']) - - return sortedQueueInfo - -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. - """ - queues = [] - - for queue in getValidQueues(): - queues.append(Queue(queue, headersOnly=headersOnly)) - - return queues \ No newline at end of file diff --git a/api/ECNQueue_old.py b/api/ECNQueue_old.py deleted file mode 100755 index 8bef24b..0000000 --- a/api/ECNQueue_old.py +++ /dev/null @@ -1,473 +0,0 @@ -#!/usr/local/bin/python - -#------------------------------------------------------------# -# Summary: Generates pages viewed by end users -#------------------------------------------------------------# - - -#------------------------------------------------------------# -# Import Libraries -#------------------------------------------------------------# -import email -import base64 -import os -import sys -import time -from string import digits -from email.Utils import parseaddr - - -#------------------------------------------------------------# -# Configure Script Environment -#------------------------------------------------------------# -c_queue_command_dir = '/usr/local/etc/ecn/queue/' - -# Use Live Queue: -# c_queue_dir = '/home/pier/e/queue/Mail/' - -# Use Test Queue: -c_queue_dir = '/Users/justincampbell/GitHub/ecn-queue/webqueue/q-snapshot' - -c_queues_to_skip = ['archives', 'drafts', 'inbox'] - -""" -c_header_map = { - 'priority':'qpriority', - 'status':'qstatus', - 'building':'qbuilding', - 'status-updated-by':'qstatus-updated-by', - 'time':'qtime', - 'assigned-to':'qassigned-to' -} -""" - - -class QueueItem: - - def __init__(self, queue_name, number, archive=''): - " Initialize a new Queue Item " - self.queue_name = queue_name # name of the queue containing the item - self.number = int(number) # queue item number - self.attributes = {'number': int(number), 'queue_name': queue_name} # dictionary of headers, attributes, etc. - self.headers = '' # text version of e-mail headers - self.content = '' # text version of e-mail content - self.body = {} # text version of the body of the e-mail - self.attachments = {} # dictionary of attachments keyed on filename - self.message = None # python e-mail representation of the message - self.archive = archive - - def load(self, headers_only=0): - " Load the content of this queue item " - - # Get an open file ready to read the queue item - # file = qCmd('qshow', self.queue_name, self.number, file=1) - if self.archive: - queue_dir = '%sarchives/%s/%s/' % (c_queue_dir, - self.queue_name, self.archive) - else: - queue_dir = '%s%s/' % (c_queue_dir, self.queue_name) - - if os.access(queue_dir + str(self.number), os.R_OK): - file = open(queue_dir + str(self.number)) - else: - return False - - self.attributes['last_updated'] = os.path.getmtime( - queue_dir + str(self.number)) - - in_headers = True - self.headers = '' - self.content = '' - - sys.stdout.flush() - line = 1 - num_lines = 0 - while line: - if in_headers: - line = file.readline() - num_lines += 1 - - # Skip lines beginning with [ or ( in the headers - if len(line) and line[0] in '[(': - if line.find('[%s] ' % self.queue_name) == 0: - line = line.replace('[%s] ' % self.queue_name, '') - else: - continue - - # a newline designates the end of the headers - if len(line) and line[0] == '\n': - in_headers = False - if headers_only: break - - self.headers += line - - else: - self.content += file.read() - break - - file.close() - - self.content = self.stripAttachments(self.content) - - self.message = email.message_from_string(self.headers + self.content) - - # Populate the message attributes from the - # message headers - for key in self.message.keys(): - self[key.lower()] = self.message[key] - - self['from_username'] = parseaddr(self['from'])[1].split('@')[0] - self['subject_status'] = '
%(subject)s
%(qstatus)s
' % self - self['for_username'] = self['qassigned-to'].lower() - - if not headers_only: - # Populate the message attachments from the - # message objects - self.body = {} - - if self.message.is_multipart(): - for part in self.message.walk(): - if part.get_filename(): - self.attachments[part.get_filename()] = {'content-type':part.get_content_type(), 'file':part.get_payload(decode=True)} - - elif part.get_content_type()[:5] == 'text/': - content_type = part.get_content_type() - - if not self.body.has_key(content_type): - self.body[content_type] = '' - - self.body[content_type] += part.get_payload(decode=True) - - else: - # If the message is not multipart then - # this is the body of the document - self.body[self.message.get_content_type()] = self.message.get_payload(decode=True) - - return True - - - def locked(self): - if self.archive: - queue_dir = '%sarchives/%s/%s/' % (c_queue_dir, self.queue_name, self.archive) - else: - queue_dir = '%s%s/' % (c_queue_dir, self.queue_name) - - if os.access(queue_dir + str(self.number) + '.lck', os.R_OK): - file = open(queue_dir + str(self.number) + '.lck') - parts = file.read().replace('\n', '').split(' ') - lock_info = { - 'file':parts[0], - 'program':parts[1], - 'user':parts[-1] - } - return lock_info - return False - - - def stripAttachments(self, message): - if not message: return '' - - lines = message.split('\n') - - attachments = [] - attachment_boundary = '' - attachment_data = '' - message_data = '' - - for line_index, line in enumerate(lines): - if attachment_boundary: - if line.find(attachment_boundary) == 0: - attachments.append(attachment_data) - attachment_boundary = '' - attachment_data = '' - else: - attachment_data += line + '\n' - - elif line.lower().find('content-type:') == 0: - header, value = line.split(':', 1) - value = value.strip().lower() - if value.find('text') == 0 or \ - value.find('multipart') == 0 or \ - value.find('message') == 0: - message_data += line + '\n' - else: - for x in range(line_index-1, 0, -1): - if lines[x].find('--') == 0: - attachment_boundary = lines[x] - for y in range(x+1, line_index): - attachment_data += lines[y] + '\n' - break - attachment_data += line + '\n' - - else: - message_data += line + '\n' - - for attachment in attachments: - message = email.message_from_string(attachment) - self.message = message - self.attachments[str(message.get_filename())] = {'content-type':message.get_content_type(), 'file':message.get_payload(decode=True)} - - return message_data - - def loadHeaders(self): - """ Helper function which calls load with headers_only=1 """ - self.load(headers_only=1) - - def getHeaders(self): - " Return the HTML Headers for this queue item " - if not self.headers: self.loadHeaders() - return self.headers - - def getBody(self, content_types=['text/plain','text/html']): - """ - Return the body of the e-mail using the content_types - to specify a prefered content type. If no content type - matching the prefered type is found it returns the first - body element - """ - - if not self.content: self.load() - - body_text = '' - for content_type in content_types: - if content_type in self.body: - body_text += '\n' + self.body[content_type] - if self.message.epilogue: - body_text += '\n' + self.message.epilogue - if body_text: - return body_text - """ - for content_type in content_types: - if content_type in self.body: - return self.body[content_type] - """ - - if len(self.body.keys()): - return self.body[self.body.keys()[0]] - else: - return '' - - def getAttachments(self): - " Return a list of filenames for all attachments " - if not self.content: self.load() - return self.attachments.keys() - - def getAttachment(self, filename): - " Return the content of a specific attachment " - if not self.content: self.load() - return self.attachments[filename]['file'] - - def getAttachmentContentType(self, filename): - " Return the content-type of a specific attachment " - if not self.content: self.load() - return self.attachments[filename]['content-type'] - - def getNumber(self): - " Returns the number of the queue item " - return self.number - - def numAttachments(self): - " Returns the number of attachments " - if not self.content: self.load() - return len(self.getAttachments()) - - def lastUpdated(self): - return self.attributes['last_updated'] - - def __contains__(self, item): - return item in self.attributes - - def __getitem__(self, key): - if not self.headers: - try: - self.loadHeaders() - except: - raise Exception('Error In Headers', 'Queue Item #%s' % self.number) - - key = key.lower() - # key = c_header_map.get(key, key) - return self.attributes.get(key,'') - - def __setitem__(self, key, value): - self.attributes[key.lower()] = value - - def __str__(self): - return "%14s:%-4s %-10s %-40s" % (self.queue_name, self.number, self['from'], self['subject']) - -class Queue: - - - def __init__(self, queue_name, archive=''): - self.loaded = False - self.queue_name = queue_name - self.archive = archive - self.num_items = None - self.items = [] - self.filtered_items = [] - self.filters = {} - self.sort_on = '' - self.sort_direction = 'ascending' - - def loadItems(self): - self.loaded = True - self.items = [] - - if self.archive: - # Where self.archive = 'YM0502' - queue_dir = c_queue_dir + 'archives/' + self.queue_name + '/' + self.archive + '/' - else: - # lines = qCmd('qscan', self.queue_name).split('\n')[1:] - queue_dir = c_queue_dir + self.queue_name + '/' - - if not os.access(queue_dir, os.F_OK): - return - - for file in os.listdir(queue_dir): - valid = True - for letter in file: - if letter not in digits: - valid = False - break - - if valid and os.access(queue_dir + file, os.R_OK): - item_num = file - item = QueueItem(self.queue_name, file, self.archive) - self.items.append(item) - - self.num_items = len(self.items) - - def sort(self, sort_on, sort_direction='ascending'): - if not self.loaded: self.loadItems() - self.sort_on = sort_on - self.sort_direction = sort_direction - if self.sort_on == 'qpriority': - self.items.sort(lambda a,b:cmp(a[sort_on].upper(), b[sort_on].upper())) - elif self.sort_on == 'date': - self.items.sort(lambda a,b:cmp(time.mktime(email.utils.parsedate(a[sort_on])), time.mktime(email.utils.parsedate(b[sort_on])))) - else: - self.items.sort(lambda a,b:cmp(a[sort_on], b[sort_on])) - - if sort_direction == 'descending': - self.items.reverse() - - self.filtered_items = [] - - def setFilter(self, name, value): - self.filters[name.lower()] = value.lower() - self.filtered_items = [] - - def addItems(self, items): - self.items.extend(items) - self.num_items = len(self.items) - self.loaded = True - - def setItems(self, items): - self.items = items[:] - self.num_items = len(self.items) - self.loaded = True - - def getItems(self, exact_match=False): - if not self.loaded: self.loadItems() - - if not self.filters: - return self.items - - elif self.filtered_items: - return self.filtered_items - - for item in self.items: - matches = False - for filter in self.filters: - for word in self.filters[filter].split(' or '): - word = word.strip() - - if not word: - continue - - if word[0] == '!': - if item[filter].lower().find(word[1:]) < 0: - matches = True - - elif exact_match and item[filter].lower() == word: - matches = True - break - elif not exact_match and item[filter].lower().find(word) >= 0: - matches = True - break - - if matches: - self.filtered_items.append(item) - - return self.filtered_items - - def getName(self): - return self.queue_name - - def setNumItems(self, num): - self.num_items = num - - def getNumItems(self): - if self.num_items is None: - self.num_items = 0 - - if self.archive: - queue_dir = c_queue_dir + 'archives/' + self.queue_name + '/' + self.archive - else: - queue_dir = c_queue_dir + self.queue_name - - files = os.listdir(queue_dir) - for file in files: - valid = 1 - for c in str(file): - if c not in digits: - valid = 0 - break - if valid: - self.num_items += 1 - - return self.num_items - - def lastUpdated(self): - update_times = [] - for item in self.getItems(): - update_times.append(item['last_updated']) - update_times.sort() - update_times.reverse() - if len(update_times): - return update_times[0] - return -1 - - def __len__(self): - return self.getNumItems() - - def __str__(self): - return "%-20s %s" % (self.getName(), self.getNumItems()) - - def __add__(self, other): - new_queue = Queue(self.getName() + '+' + other.getName()) - new_queue.addItems(self.getItems()) - new_queue.addItems(other.getItems()) - return new_queue - - def __cmp__(self, other): - return cmp(self.getName(), other.getName()) - - def __getitem__(self, index): - return self.getItems()[index] - -def getQueues(): - queues = [] - - for file in os.listdir(c_queue_dir): - if os.access(c_queue_dir + file, os.R_OK) and os.path.isdir(c_queue_dir + file) and file not in c_queues_to_skip: - queue = Queue(file) - queues.append(queue) - - return queues - -if __name__ == '__main__': - - # Create a combined Queue - item = QueueItem('webmaster', 22) - body = item.getBody() - diff --git a/api/api.py b/api/api.py deleted file mode 100644 index 3a45319..0000000 --- a/api/api.py +++ /dev/null @@ -1,255 +0,0 @@ -from flask import Flask, request, after_this_request -from flask_restful import Api, Resource -from flask_jwt_extended import ( - JWTManager, create_access_token, create_refresh_token, - jwt_required, get_jwt_identity, jwt_refresh_token_required, - set_refresh_cookies, unset_refresh_cookies -) -import os, dotenv -from easyad import EasyAD -from ldap.filter import escape_filter_chars -# pylint says this is an error but it works so ¯\_(ツ)_/¯ -from ldap import INVALID_CREDENTIALS as LDAP_INVALID_CREDENTIALS -import ECNQueue - -# Load envrionment variables for ./.env -dotenv.load_dotenv() - -# Create Flask App -app = Flask(__name__) - -# Create API Interface -api = Api(app) - - -################################################################################ -# Configure Flask-JWT-Extended -################################################################################ - -# Set JWT secret key and create JWT manager -app.config["JWT_SECRET_KEY"] = os.environ.get("JWT_SECRET_KEY") -# Set identity claim field key to sub for JWT RFC complience -# Flask-JWT-Extended uses 'identity' by default for compatibility reasons -app.config["JWT_IDENTITY_CLAIM"] = "sub" -# Set the key for error messages generated by Flask-JWT-Extended -app.config["JWT_ERROR_MESSAGE_KEY"] = "message" - -# Look for JWTs in headers (for access) then cookies (for refresh) -app.config["JWT_TOKEN_LOCATION"] = ["headers", "cookies"] -# Restrict cookies to HTTPS in prod, allow HTTP in dev -app.config["JWT_COOKIE_SECURE"] = False if os.environ.get("ENVIRONMENT") == "dev" else True -# Restrict cookies using SameSite=strict flag -app.config["JWT_COOKIE_SAMESITE"] = "strict" -# Restrict refresh tokens to /token/refresh endpoint -app.config["JWT_REFRESH_COOKIE_PATH"] = '/api/tokens/refresh' -# Set the cookie key for CRSF validation string -# This is the default value. Adding it for easy reference -app.config["JWT_REFRESH_CSRF_HEADER_NAME"] = "X-CSRF-TOKEN" - -tokenManager = JWTManager(app) - - - -def user_is_valid(username: str, password: str) -> bool: - """Checks if user is valid and in webqueue2 login group. - - Args: - username (str): Career account username. - password (str): Career account passphrase. - - Returns: - bool: True if user is valid, otherwise False. - """ - - # Check for empty arguments - if (username == "" or password == ""): - return False - - # Initialize EasyAD - config = { - "AD_SERVER": "boilerad.purdue.edu", - "AD_DOMAIN": "boilerad.purdue.edu" - } - ad = EasyAD(config) - - # Prepare search critiera for Active Directory - credentials = { - "username": escape_filter_chars(username), - "password": password - } - attributes = [ 'cn', "memberOf" ] - filter_string = f'(&(objectClass=user)(|(sAMAccountName={username})))' - - # Do user search - try: - user = ad.search(credentials=credentials, attributes=attributes, filter_string=filter_string)[0] - except LDAP_INVALID_CREDENTIALS: - return False - - # Isolate group names - # Example: - # 'CN=00000227-ECNStuds,OU=BoilerADGroups,DC=BoilerAD,DC=Purdue,DC=edu' becomes - # `00000227-ECNStuds` - user_groups = [ group.split(',')[0].split('=')[1] for group in user["memberOf"] ] - - # Check group membership - webqueue_login_group = "00000227-ECN-webqueue" - if webqueue_login_group not in user_groups: - return False - - return True - - - -class Login(Resource): - def post(self) -> tuple: - """Validates username/password and returns both access and refresh tokens. - - Return Codes: - 200 (OK): On success. - 401 (Unauthroized): When username or password are incorrect. - 422 (Unprocessable Entitiy): When the username or password can't be parsed. - - Example: - curl -X POST - -H "Content-Type: application/json" - -d '{"username": "bob", "password": "super_secret"}' - - { "access_token": fjr09hfp09h932jp9ruj3.3r8ihf8h0w8hr08ifhj804h8i.8h48ith08ity409hip0t4 } - - Returns: - tuple: Response containing tokens and HTTP response code. - """ - if not request.is_json: - return ({ "message": "JSON missing from request body"}, 422) - - data = request.json - - fields_to_check = ["username", "password"] - for field in fields_to_check: - if field not in data.keys(): - return ({ "message": f"{field} missing from request body"}, 422) - - if not user_is_valid(data["username"], data["password"]): - return ({ "message": "Username or password is invalid"}, 401) - - access_token = create_access_token(data["username"]) - refresh_token = create_refresh_token(data["username"]) - - # This decorator is needed because Flask-RESTful's 'resourceful routing` - # doesn't allow for direct modification to the Flask response object. - # See: https://flask-restful.readthedocs.io/en/latest/quickstart.html#resourceful-routing - @after_this_request - def _does_this_work(response): - set_refresh_cookies(response, refresh_token) - return response - - return ({ "access_token": access_token }, 200) - -class RefreshAccessToken(Resource): - @jwt_refresh_token_required - def post(self): - username = get_jwt_identity() - access_token = create_access_token(username) - return ({"access_token": access_token}, 200) - -class Item(Resource): - @jwt_required - def get(self, queue: str, number: int) -> tuple: - """Returns the JSON representation of the item requested. - - Return Codes: - 200 (OK): On success. - - Example: - { - "lastUpdated": "07-23-20 10:11 PM", - "headers": [...], - "content": [...], - "isLocked": "ce 100 is locked by knewell using qvi", - "userEmail": "campb303@purdue.edu", - "userName": "Justin Campbell", - "userAlias": "campb303", - "assignedTo": "campb303", - "subject": "Beepboop", - "status": "Dont Delete", - "priority": "", - "deparment": "", - "building": "", - "dateReceived": "Tue, 23 Jun 2020 13:25:51 -0400" - } - - Args: - queue (str): The queue of the item requested. - item (int): The number of the item requested. - - Returns: - tuple: Item as JSON and HTTP response code. - """ - - 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. - - Args: - queues (str): Plus (+) deliminited list of queues. - - 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, headersOnly=headersOnly).toJson()) - return (queue_list, 200) - -class QueueList(Resource): - @jwt_required - def get(self) -> tuple: - """Returns a list of dictionaries with the number of items in each queue. - - Return Codes: - 200 (OK): On success. - - Example: - [ - { - name: "me", - number_of_items: 42 - }, - { - name: "bidc", - number_of_items: 3 - } - ] - - Returns: - tuple: Queues and item counts as JSON and HTTP response code. - """ - return (ECNQueue.getQueueCounts(), 200) - - - -api.add_resource(Login, "/api/login") -api.add_resource(RefreshAccessToken, "/api/tokens/refresh") -api.add_resource(Item, "/api/data//") -api.add_resource(Queue, "/api/data/") -api.add_resource(QueueList, "/api/data/get_queues") - -if __name__ == "__main__": - app.run() \ No newline at end of file From 888e1bb28cff469eb5bc84e11ce0f9b730ba4076 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 12 Jul 2021 15:22:25 -0400 Subject: [PATCH 2/4] Update requirements to point to new API package on GitHub --- api/requirements.txt | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/api/requirements.txt b/api/requirements.txt index 48ec2fb..7f52866 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -1,25 +1 @@ -# The Python virtual environment should be managed via the venv-manager utility, not directly by pip. -# See: https://pip.pypa.io/en/stable/reference/pip_install/#example-requirements-file - -# General Utilities -gunicorn -pipdeptree -pylint - -# API -python-dotenv -Flask-RESTful -python-dateutil -Flask-JWT-Extended -# Flask-JWT-Extended doesn't support PyJWT 2.x as of 3.25.0 -# Prevent upgrade to PyJWT 2.x until Flask-JWT-Extended is updated to support it. -# Check: https://github.com/vimalloc/flask-jwt-extended/tags -PyJWT == 1.* -# Custom python-ldap without SASL -git+https://github.itap.purdue.edu/ECN/python-ldap.git@python-ldap-3.3.1#egg=python-ldap==3.3.1 -easyad - -# API Documentation -mkdocs -mkdocs-material -mkautodoc \ No newline at end of file +webqueue2api @ git+https://github.itap.purdue.edu/ECN/webqueue2-api@1.0.1#egg=webqueue2api \ No newline at end of file From 38736e6bb8c56cc8227ed2bc3b874f1c6d2ab360 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 12 Jul 2021 15:22:40 -0400 Subject: [PATCH 3/4] Update npm run start:api command to point to new WSGI path --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ac29a77..5d6c6f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "webqueue2", "homepage": "", - "proxy": "http://localhost:5000/", + "proxy": "http://localhost:8000/", "version": "0.9.1", "private": true, "dependencies": { @@ -23,7 +23,7 @@ }, "scripts": { "start:frontend": "react-scripts start", - "start:api": "cd api/ && venv/bin/gunicorn -b 127.0.0.1:5000 --log-level debug api:app", + "start:api": "cd api/ && venv/bin/gunicorn -b 127.0.0.1:8000 --log-level debug webqueue2api.api:app", "start:docs": "start-storybook -p 6006 -s public -c storybook-config --no-manager-cache", "build:frontend": "react-scripts build", "build:docs": "build-storybook -s public -c storybook-config -o build-docs", From bbc4e66fd6ff589fcf3f288d1fd2756b053c9e25 Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 12 Jul 2021 15:22:52 -0400 Subject: [PATCH 4/4] Update object references to snake case --- src/components/ItemTable/ItemTable.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ItemTable/ItemTable.js b/src/components/ItemTable/ItemTable.js index 757bd6f..e124b19 100644 --- a/src/components/ItemTable/ItemTable.js +++ b/src/components/ItemTable/ItemTable.js @@ -60,15 +60,15 @@ export default function ItemTable({ data, rowCanBeSelected, loading }) { () => [ { Header: 'Queue', accessor: 'queue', }, { Header: 'Item #', accessor: 'number' }, - { Header: 'From', accessor: 'userAlias' }, - { Header: 'Assigned To', accessor: 'assignedTo' }, + { Header: 'From', accessor: 'user_alias' }, + { Header: 'Assigned To', accessor: 'assigned_to' }, { Header: 'Subject', accessor: 'subject' }, { Header: 'Status', accessor: 'status', }, { Header: 'Priority', accessor: 'priority' }, - { Header: 'Last Updated', accessor: 'lastUpdated', sortInverted: true, Cell: ({ value }) => }, + { Header: 'Last Updated', accessor: 'last_updated', sortInverted: true }, { Header: 'Department', accessor: 'department' }, { Header: 'Building', accessor: 'building' }, - { Header: 'Date Received', accessor: 'dateReceived', sortInverted: true, Cell: ({ value }) => }, + { Header: 'Date Received', accessor: 'date_received', sortInverted: true , ], []); const tableInstance = useTable( { @@ -187,13 +187,13 @@ export default function ItemTable({ data, rowCanBeSelected, loading }) { {row.cells.map(cell => ( cell.render(_ => { switch (cell.column.id) { - case "dateReceived": + case "date_received": return ( ); - case "lastUpdated": + case "last_updated": return (