Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUI: Removed ability for a middle state to be a start/end state #30

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
8 changes: 4 additions & 4 deletions constrain/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Background
This tool builds workflows following the ConStrain API schema. The GUI provides the following:
- a graphical representation of their workflow
- a graphical representation of their workflow
- a layer of abstraction over the process of creating a workflow
- validation and submission of workflows

Expand Down Expand Up @@ -30,7 +30,7 @@ The basic form is meant to guide the user in the creation of a state. It is trig
The advanced form offers no guidance in the creation of a state. It is triggered by navigating to the 'State' tab and pressing the 'Add Advanced' button, and also triggered by clicking a state in the Workflow Diagram while using Advanced Settings. It is a text box where the user inputs a state. The state should be in the following format:

```json
{
{
"name of state": {
...
}
Expand Down Expand Up @@ -89,7 +89,7 @@ The application contains 9 Python source files:
- `submit`

#### app
This file runs the GUI. It is responsible for piecing together the main components of the app, like the meta form, the import form, and the workflow diagram. It also handles validate, import, and export functionalities. It contains 2 classes:
This file runs the GUI. It is responsible for piecing together the main components of the app, like the meta form, the import form, and the workflow diagram. It also handles validate, import, and export functionalities. It contains 2 classes:
- `GUI(QMainWindow)`: pieces classes together
- `UserSetting(QDialog)`: dialog to configure basic or advanced setting

Expand All @@ -108,7 +108,7 @@ This file is based on classes for organizing the UI of workflow visualization. I
- `WorkflowDiagram(QWidget)`: node manager

#### popup_window
This file contains the `PopupWindow(QDialog)` class. This class handles the Basic Popup, which is a form that describes a state. It is responsible for handling the creation of a state and the import of a state.
This file contains the `BasicPopup(QDialog)` class. This class handles the Basic Popup, which is a form that describes a state. It is responsible for handling the creation of a state and the import of a state.

#### advanced_popup
This file contains the `AdvancedPopup(QDialog)` class. This class handles the Advanced Popup, which is a text box that describes a state in JSON format. It is responsible for handling the creation of a state and the import of a state. The following is an example entry to this text box:
Expand Down
46 changes: 28 additions & 18 deletions constrain/app/advanced_popup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
QMessageBox,
)
from PyQt6.QtGui import QFontMetricsF
from constrain.app import utils


class AdvancedPopup(QDialog):
Expand Down Expand Up @@ -89,28 +90,37 @@ def check_state(self):
state = self.get_state()

if not state:
self.error_popup("Invalid state format")
utils.send_error("Error in State", "Invalid state format")
self.error = True
else:
state_type = state.get("Type")

if state_type not in ["Choice", "MethodCall"]:
self.error = True
self.error_popup("Invalid type")
elif state_type == "Choice" and "Choices" not in state.keys():
self.error = True
self.error_popup("Choice type, but no Choices key")
utils.send_error("Error in State", "Invalid state type")
elif state_type == "Choice":
if "Choices" not in state:
self.error = True
utils.send_error(
"Error in State", "Choice type, but no Choices key"
)
elif any(key in state for key in ("Start", "End")):
self.error = True
utils.send_error(
"Error in State", "Start/End keys not allowed in a Choice type"
)
else:
self.close()
else:
self.close()

def error_popup(self, text):
"""Executes an error popup with a given message.

Args:
text (str): The message to be displayed
"""
error_msg = QMessageBox()
error_msg.setIcon(QMessageBox.Icon.Critical)
error_msg.setWindowTitle("Error in State")
error_msg.setText(text)
error_msg.exec()
if all(key in state for key in ("Start", "End")):
self.error = True
utils.send_error(
"Error in State",
"Cannot have both Start and End keys in a state",
)

if state.get("End") and state.get("Next"):
self.error = True
utils.send_error(
"Error in State", "State has 'End' and 'Next' keys"
)
76 changes: 4 additions & 72 deletions constrain/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def exportFile(self):

if fp:
try:
workflow = self.get_workflow()
workflow = self.states_form.get_workflow(reformat=False)
with open(fp, "w", encoding="utf-8") as f:
json.dump(self.create_json(workflow), f, indent=4)
except Exception:
Expand Down Expand Up @@ -228,7 +228,7 @@ def importFile(self):
)
self.import_form.read_import(workflow.get("imports"))
self.states_form.read_import(workflow.get("states"))
self.get_workflow()
self.states_form.get_workflow(reformat=True)
else:
# error if selected file cannot be converted to a dict
print("error")
Expand Down Expand Up @@ -281,7 +281,7 @@ def submit_form(self):
"""Workflow for submitting the state. Triggered on the click of the Submit button. Displays a popup which
shows the progress of running the state.
"""
states = self.get_workflow(reformat=False)
states = self.states_form.get_workflow(reformat=False)
json_data = self.create_json(states)

popup = SubmitPopup()
Expand All @@ -293,79 +293,11 @@ def submit_form(self):
self.worker.start()
popup.exec()

def get_workflow(self, reformat=True):
"""Organizes the states into a single list, colors states based on placing, returns organized workflow

Returns:
list: A compiled version of the state dicts in DFS order
"""

# find all CustomItems in the scene
items = [
item
for item in self.states_form.scene.items()
if isinstance(item, CustomItem)
]

# prepare for DFS
roots = []
for i in items:
parent = True
for j in items:
if i in j.children:
parent = False
break
if parent:
roots.append(i)
visited = set()

paths = []

# DFS helper method
def dfs_helper(item, path):
path.append(item)
visited.add(item)

if item not in items or not item.children:
# item is a leaf node
item.setBrush("red")
item.state["End"] = "True"
paths.append(path[:])
else:
# item is not a leaf node
item.setBrush()

for child in item.children:
if child not in visited:
dfs_helper(child, path)

path.pop()
visited.remove(item)

for root in roots:
if reformat:
root.state["Start"] = "True"
self.states_form.view.arrange_tree(root, 0, 0, 150)
if root not in visited:
dfs_helper(root, [])
root.setBrush("green")

workflow_path = []
visited = set()

# create final path through workflow
for path in paths:
for node in path:
if node not in visited:
workflow_path.append(node.state)
visited.add(node)
return workflow_path

def validate_form(self):
"""Workflow for validating the state. Triggered on the click of the Validate button. Enables the submit
button if the workflow is validated.
"""
workflow_path = self.get_workflow(reformat=False)
workflow_path = self.states_form.get_workflow(reformat=False)
json_data = self.create_json(workflow_path)

warnings.simplefilter(action="ignore", category=FutureWarning)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Contained is the class for PopupWindow, the popup displayed when 'Add Basic' is chosen in the states tab.
Contained is the class for BasicPopup, the popup displayed when 'Add Basic' is chosen in the states tab.
"""

import json
Expand Down Expand Up @@ -36,7 +36,7 @@
api_to_method = json.load(f)


class PopupWindow(QDialog):
class BasicPopup(QDialog):
def __init__(self, payloads=[], state_names=[], rect=None, load=False):
"""Form to be displayed for user to edit or add a basic state

Expand Down
19 changes: 15 additions & 4 deletions constrain/app/import_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from PyQt6.QtGui import QAction
from PyQt6.QtCore import Qt
from constrain.app import utils


class ImportForm(QWidget):
Expand Down Expand Up @@ -63,12 +64,22 @@ def read_import(self, imports):
Args:
imports (list): A list of python imports
"""
if isinstance(imports, list) and all(isinstance(item, str) for item in imports):
for i in imports:
self.imports.append(i)
self.import_list.addItem(i)
try:
self.verify_import(imports)
except AssertionError:
utils.send_error("Error in Import", "Invalid imports form")
return

for i in imports:
self.imports.append(i)
self.import_list.addItem(i)
self.update()

def verify_import(self, imports):
assert isinstance(imports, list) and all(
isinstance(item, str) for item in imports
)

def show_context_menu(self, position):
"""Allows user to delete an import on right click of an item on the list
Args:
Expand Down
53 changes: 22 additions & 31 deletions constrain/app/meta_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
QHBoxLayout,
)
from PyQt6.QtCore import QDate
from constrain.app import utils


class MetaForm(QWidget):
Expand Down Expand Up @@ -58,42 +59,32 @@ def get_meta(self):
}

def read_import(self, workflow_name=None, meta=None):
def isStr(input):
try:
self.verify_import(workflow_name=workflow_name, meta=meta)
except AssertionError:
utils.send_error("Error in Import", "Invalid meta form")
return

self.name_input.setText(workflow_name)
self.author_input.setText(meta.get("author"))
d = QDate.fromString(meta.get("date"), self.date_format)
self.date_input.setDate(d)
self.version_input.setText(meta.get("version"))
self.description_input.setText(meta.get("description"))

self.update()

def verify_import(self, workflow_name=None, meta=None):
def is_str(input):
return isinstance(input, str)

if workflow_name:
if isStr(workflow_name):
self.name_input.setText(workflow_name)
else:
print("error")
assert is_str(workflow_name)

if isinstance(meta, dict):
if "author" in meta.keys():
author = meta["author"]
if isStr(author):
self.author_input.setText(author)
else:
print("invalid author")
if "date" in meta.keys():
date = meta["date"]
if isStr(date):
d = QDate.fromString(date, self.date_format)
self.date_input.setDate(d)
else:
print("invalid date")
if "version" in meta.keys():
version = meta["version"]
if isStr(version):
self.version_input.setText(version)
else:
print("invalid version")
if "description" in meta.keys():
description = meta["description"]
if isStr(description):
self.description_input.setText(description)
else:
print("invalid description")
self.update()
for k in ["author", "date", "version", "description"]:
if v := meta.get(k):
assert is_str(v)

def get_workflow_name(self):
return self.name_input.text()
Expand Down
Loading
Loading