From f51a6abc9a94b9714753c9047d42db767a3106c8 Mon Sep 17 00:00:00 2001 From: Adam Sher Date: Thu, 16 Mar 2023 12:37:45 -0400 Subject: [PATCH 1/3] Initial work on EOY Mode --- src/devicekiosk.py | 30 +++++-- src/qml/EOYReturn.qml | 194 ++++++++++++++++++++++++++++++++++++++++++ src/qml/Main.qml | 24 ++++++ 3 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/qml/EOYReturn.qml diff --git a/src/devicekiosk.py b/src/devicekiosk.py index 2d7ebb6..e321599 100644 --- a/src/devicekiosk.py +++ b/src/devicekiosk.py @@ -20,7 +20,12 @@ from enum import Enum from email.message import EmailMessage -class Mode(Enum): +class KioskMode(Enum): + normal = 0 + singleUser = 1 + eoyReturnOnly = 2 + +class ServiceMode(Enum): dropoff = 1 pickup = 2 @@ -48,6 +53,7 @@ class UI(QObject): showErrorSignal = Signal(str) showErrorPageSignal = Signal(None) showFinishSignal = Signal(None) + showEOYReturnSignal = Signal(None) firstName = "" lastName = "" @@ -58,7 +64,7 @@ class UI(QObject): loanerSerialNumber = "" studentDeviceBarcode = "" loanerDeviceBarcrod = "" - serviceMode = Mode.dropoff + serviceMode = ServiceMode.dropoff schoolLogo = "" errorMessage = "" config = None @@ -86,6 +92,10 @@ def checkForErrors(self): # print("python showEmailScreen ran") # self.showEmailScreenSignal.emit() + @Slot(None, result=int) + def getKioskMode(self): + return self.config["kiosk_mode"] + @Slot() def startOver(self): self.firstName = "" @@ -97,7 +107,7 @@ def startOver(self): self.loanerSerialNumber = "" self.studentDeviceBarcode = "" self.loanerDeviceBarcrod = "" - self.serviceMode = Mode.dropoff + self.serviceMode = ServiceMode.dropoff self.errorMessage = "" self.startOverSignal.emit() @@ -117,7 +127,7 @@ def submitUser(self, userInfo): self.studentID = userInfo[2] self.emailAddress = self.firstName + self.lastName + self.studentID + "@tolland.k12.ct.us" print("Student email is: " + self.emailAddress) - if (self.serviceMode == Mode.dropoff): + if (self.serviceMode == ServiceMode.dropoff): self.showDescriptionSignal.emit() else: self.showReturnSignal.emit() @@ -125,14 +135,14 @@ def submitUser(self, userInfo): @Slot('QString') def submitSerial(self, serial): self.serialNumber = serial - if (self.serviceMode == Mode.dropoff): + if (self.serviceMode == ServiceMode.dropoff): self.showPrintSignal.emit() else: self.showSubmitPickupSignal.emit() @Slot('int') def start(self, mode): - self.serviceMode = Mode(mode) + self.serviceMode = ServiceMode(mode) print("Service mode: ") print(self.serviceMode) self.showUserSignal.emit() @@ -200,6 +210,12 @@ def submitTicket(self): def processPickup(self): self.sendEmail() self.enableNextSignal.emit() + + @Slot() + def submitEOYReturn(self): + print("end of year submitted") + self.showEOYReturnSignal.emit() + def postToZenDesk(self): self.errorMessage = "" @@ -231,7 +247,7 @@ def sendEmail(self): msg = EmailMessage() # me == the sender's email address # you == the recipient's email address - if (self.serviceMode == Mode.dropoff): + if (self.serviceMode == ServiceMode.dropoff): msg['Subject'] = f'Student Device Drop Off: ' + self.firstName + ' ' + self.lastName body = self.firstName + " " + self.lastName + " has dropped off a laptop for repair.\nStudent Number: " + self.studentID + "\nDropped off student device serial number: " + self.serialNumber + "\nLoaner Serial Number: " + self.loanerSerialNumber msg.set_content(body) diff --git a/src/qml/EOYReturn.qml b/src/qml/EOYReturn.qml new file mode 100644 index 0000000..a99bceb --- /dev/null +++ b/src/qml/EOYReturn.qml @@ -0,0 +1,194 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Item { + + function verifyForm() { + if (inputSerial.text.toString().length > 0) { + btnSubmit.enabled = true + } + else { + btnSubmit.enabled = false + } + } + + function setFocus() { + inputSerial.focus = true + inputSerial.forceActiveFocus() + } + + property var chargerReturned: true + + function chargerYesClicked() { + console.log("Yes clicked") + chargerReturned = true + btnChargerYesText.color = "white" + btnChargerYesRect.color = "green" + btnChargerNoText.color = "red" + btnChargerNoRect.color = "white" + } + + function chargerNoClicked() { + console.log("No clicked") + chargerReturned = false + btnChargerYesText.color = "green" + btnChargerYesRect.color = "white" + btnChargerNoText.color = "white" + btnChargerNoRect.color = "red" + } + + function listProperty(item) + { + for (var p in item) + console.log(p + ": " + item[p]); + } + + ColumnLayout { + anchors.fill: parent + // spacing: 2 + + Text { + id: txtHeader + Layout.fillWidth: true + Layout.fillHeight: true + + text: "End of Year Return.\nPlease use the barcode scanner to scan the barcode sticker on your device." + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pointSize: 30 + + } + + // AnimatedImage { + // id: image + // Layout.alignment: Qt.AlignHCenter + // source: "../images/dropoff.gif" + // fillMode: Image.Image.PreserveAspectCrop + // } + + TextField { + id: inputSerial + // placeholderText: qsTr("Email Address") + font.pointSize: 20 + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: TextInput.AlignHCenter + focus: true + onTextChanged: { + verifyForm() + } + + Component.onCompleted: { + this.focus = true + this.forceActiveFocus() + } + } + + Text { + id: txtEmail + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Is the device's charger being returned as well?" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pointSize: 30 + + } + + RowLayout { + Button { + id: btnChargerYes + + enabled: true + Layout.fillWidth: true + Layout.fillHeight: true + + contentItem: Text { + id: btnChargerYesText + text: "Yes" + font.pointSize: 50 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + anchors.fill: parent + } + + background: Rectangle { + id: btnChargerYesRect + color: "green" + border.width: 1 + radius: 2 + } + + MouseArea{ + anchors.fill: parent + onClicked: { + chargerYesClicked() + } + } + } + + Button { + id: btnChargerNo + + enabled: true + Layout.fillWidth: true + Layout.fillHeight: true + + contentItem: Text { + id: btnChargerNoText + text: "No" + color: "red" + font.pointSize: 50 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + anchors.fill: parent + } + + background: Rectangle { + id: btnChargerNoRect + border.width: 1 + radius: 2 + } + + MouseArea{ + anchors.fill: parent + onClicked: { + chargerNoClicked() + } + } + } + } + + + + Button { + id: btnSubmit + text: "Submit" + enabled: false + font.pointSize: 50 + Layout.fillWidth: true + Layout.fillHeight: true + + onClicked: { + ui.submitEOYReturn() + } + } + + } + // Setting focus any other way doesn't seem to work. + // Kind of kludge, but works + Timer { + interval: 100 + running: true + repeat: false + onTriggered: setFocus() + } +} diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 6dd1757..28e35cf 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -73,8 +73,29 @@ ApplicationWindow { function onShowFinishSignal(error) { contentFrame.push(Qt.createComponent("Finish.qml")) } + + function onShowEOYReturnSignal() { + // contentFrame.clear() + contentFrame.push(Qt.createComponent("EOYReturn.qml")) + } } + // Automatically go to a non-standard kiosk mode start page + function setInitialPage() { + switch(ui.getKioskMode()) { + case 0: + break; + case 1: + // TODO + break; + case 2: + // EOY Return Only Mode + contentFrame.push(Qt.createComponent("EOYReturn.qml")) + default: + break; + } + } + // property var showedColon: false // function updateClock() @@ -88,6 +109,9 @@ ApplicationWindow { height: appWindow.height id: contentFrame initialItem: Qt.createComponent("Start.qml") + Component.onCompleted: { + setInitialPage() + } } From b9b9519419deb69fc92cdec412976745a1f2863f Mon Sep 17 00:00:00 2001 From: Adam Sher Date: Thu, 16 Mar 2023 13:09:10 -0400 Subject: [PATCH 2/3] Return information written to file --- src/devicekiosk.py | 23 +++++++++++++++++++++-- src/qml/EOYReturn.qml | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/devicekiosk.py b/src/devicekiosk.py index e321599..83ee07d 100644 --- a/src/devicekiosk.py +++ b/src/devicekiosk.py @@ -211,9 +211,13 @@ def processPickup(self): self.sendEmail() self.enableNextSignal.emit() - @Slot() - def submitEOYReturn(self): + @Slot(list) + def submitEOYReturn(self, returnInfo): + returnSerial = returnInfo[0] + chargerReturned = returnInfo[1] + print("end of year submitted") + self.addToReturnFile(returnSerial, chargerReturned) self.showEOYReturnSignal.emit() @@ -268,6 +272,21 @@ def sendEmail(self): except smtplib.SMTPException as ex: self.errorMessage = ex s.close() + + def addToReturnFile(self, serial, returnedCharger): + print("Adding " + serial + " to return file, with charger: " + str(returnedCharger)) + filename = "eoy-returns-" + str(datetime.date.today()) + ".csv" + returnsFile = os.path.join(os.path.dirname(os.path.abspath(__file__)),filename) + if not os.path.exists(returnsFile): + with open(returnsFile, 'a') as f: + f.writelines("Serial, Charger\n") + f.writelines(serial + "," + str(returnedCharger) + "\n") + f.close() + else: + # Populate version.txt with the newly installed build + with open(returnsFile, 'a') as f: + f.writelines(serial + "," + str(returnedCharger) + "\n") + f.close() if __name__ == "__main__": diff --git a/src/qml/EOYReturn.qml b/src/qml/EOYReturn.qml index a99bceb..a0505cc 100644 --- a/src/qml/EOYReturn.qml +++ b/src/qml/EOYReturn.qml @@ -178,7 +178,8 @@ Item { Layout.fillHeight: true onClicked: { - ui.submitEOYReturn() + var returnInfo = [inputSerial.text, chargerReturned] + ui.submitEOYReturn(returnInfo) } } From fc9c36150958f87a971fe54c7b68c8dab3bace9a Mon Sep 17 00:00:00 2001 From: Adam Sher Date: Thu, 16 Mar 2023 14:34:23 -0400 Subject: [PATCH 3/3] Added EOY Menu and archiving of return files when emailing --- src/devicekiosk.py | 45 +++++++++++++++++- src/qml/EOYFinish.qml | 103 ++++++++++++++++++++++++++++++++++++++++++ src/qml/EOYReturn.qml | 23 ++++++---- src/qml/EOYStart.qml | 59 ++++++++++++++++++++++++ src/qml/Main.qml | 9 +++- 5 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 src/qml/EOYFinish.qml create mode 100644 src/qml/EOYStart.qml diff --git a/src/devicekiosk.py b/src/devicekiosk.py index 83ee07d..8cae00e 100644 --- a/src/devicekiosk.py +++ b/src/devicekiosk.py @@ -11,6 +11,7 @@ import zipfile import traceback import subprocess +import uuid from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine @@ -54,6 +55,7 @@ class UI(QObject): showErrorPageSignal = Signal(None) showFinishSignal = Signal(None) showEOYReturnSignal = Signal(None) + showEOYStartSignal = Signal(None) firstName = "" lastName = "" @@ -220,6 +222,11 @@ def submitEOYReturn(self, returnInfo): self.addToReturnFile(returnSerial, chargerReturned) self.showEOYReturnSignal.emit() + @Slot('QString') + def submitEOYFinish(self, returnEmail): + self.emailEOYReturnFiles(returnEmail) + self.archiveReturns() + self.showEOYStartSignal.emit() def postToZenDesk(self): self.errorMessage = "" @@ -273,6 +280,34 @@ def sendEmail(self): self.errorMessage = ex s.close() + def emailEOYReturnFiles(self, returnAddress): + msg = EmailMessage() + msg['Subject'] = 'EOY Return Files' + body = "EOY Return Files" + msg.set_content(body) + attachmentFolder = os.path.dirname(os.path.abspath(__file__)) + # files = + for file in os.listdir(attachmentFolder): + if file.endswith(".csv"): + with open(os.path.join(attachmentFolder, file), 'rb') as content_file: + print(file) + content = content_file.read() + msg.add_attachment(content, maintype='application', subtype='txt', filename=str(file)) + # email.add_attachment(content, maintype='application', subtype='pdf', filename='example.pdf') + msg['From'] = self.config["smtp_user"] + msg['To'] = "asher@tolland.k12.ct.us, " + returnAddress + # Send the message via Gmail SMTP server + s = smtplib.SMTP("smtp.gmail.com", 587) + s.ehlo() + s.starttls() + s.login(self.config["smtp_user"], self.config["smtp_password"]) + try: + s.send_message(msg) + except smtplib.SMTPException as ex: + self.errorMessage = ex + print("Email error " + ex) + s.close() + def addToReturnFile(self, serial, returnedCharger): print("Adding " + serial + " to return file, with charger: " + str(returnedCharger)) filename = "eoy-returns-" + str(datetime.date.today()) + ".csv" @@ -287,7 +322,15 @@ def addToReturnFile(self, serial, returnedCharger): with open(returnsFile, 'a') as f: f.writelines(serial + "," + str(returnedCharger) + "\n") f.close() - + + def archiveReturns(self): + workingDir = os.path.dirname(os.path.abspath(__file__)) + archiveDir = os.path.join(workingDir, "return-archive", str(uuid.uuid4())) + if not os.path.exists(archiveDir): + os.makedirs(archiveDir) + for file in os.listdir(workingDir): + if file.endswith(".csv"): + shutil.move(os.path.join(workingDir, file), archiveDir) if __name__ == "__main__": QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) #enable highdpi scaling diff --git a/src/qml/EOYFinish.qml b/src/qml/EOYFinish.qml new file mode 100644 index 0000000..6ad25a4 --- /dev/null +++ b/src/qml/EOYFinish.qml @@ -0,0 +1,103 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Item { + + function verifyForm() { + if (inputEmail.text.toString().length > 0) { + btnSubmit.enabled = true + } + else { + btnSubmit.enabled = false + } + } + + function setFocus() { + inputEmail.focus = true + inputEmail.forceActiveFocus() + } + + property var chargerReturned: true + + + + ColumnLayout { + anchors.fill: parent + // spacing: 2 + + Text { + id: txtHeader + Layout.fillWidth: true + Layout.fillHeight: true + + text: "Enter an email address to receive the return files.\nEmail will also be sent to Candace de Loureiro.\nFiles will be backed up this computer in the event something goes wrong." + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pointSize: 30 + + } + + // AnimatedImage { + // id: image + // Layout.alignment: Qt.AlignHCenter + // source: "../images/dropoff.gif" + // fillMode: Image.Image.PreserveAspectCrop + // } + + TextField { + id: inputEmail + placeholderText: qsTr("Email Address") + font.pointSize: 20 + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: TextInput.AlignHCenter + focus: true + onTextChanged: { + verifyForm() + } + + Component.onCompleted: { + this.focus = true + this.forceActiveFocus() + } + } + + Button { + id: btnSubmit + text: "Submit Returns" + enabled: false + font.pointSize: 50 + Layout.fillWidth: true + Layout.fillHeight: true + + onClicked: { + ui.submitEOYFinish(inputEmail.text) + } + } + + Button { + id: btnMenu + text: "Return to EOY Menu" + enabled: true + font.pointSize: 20 + Layout.fillWidth: true + Layout.fillHeight: true + + onClicked: { + contentFrame.push(Qt.createComponent("EOYStart.qml")) + } + } + + } + // Setting focus any other way doesn't seem to work. + // Kind of kludge, but works + Timer { + interval: 100 + running: true + repeat: false + onTriggered: setFocus() + } +} diff --git a/src/qml/EOYReturn.qml b/src/qml/EOYReturn.qml index a0505cc..8879ac9 100644 --- a/src/qml/EOYReturn.qml +++ b/src/qml/EOYReturn.qml @@ -38,12 +38,6 @@ Item { btnChargerNoText.color = "white" btnChargerNoRect.color = "red" } - - function listProperty(item) - { - for (var p in item) - console.log(p + ": " + item[p]); - } ColumnLayout { anchors.fill: parent @@ -165,9 +159,7 @@ Item { } } } - } - - + } Button { id: btnSubmit @@ -182,6 +174,19 @@ Item { ui.submitEOYReturn(returnInfo) } } + + Button { + id: btnMenu + text: "Return to EOY Menu" + enabled: true + font.pointSize: 20 + Layout.fillWidth: true + Layout.fillHeight: true + + onClicked: { + contentFrame.push(Qt.createComponent("EOYStart.qml")) + } + } } // Setting focus any other way doesn't seem to work. diff --git a/src/qml/EOYStart.qml b/src/qml/EOYStart.qml new file mode 100644 index 0000000..86e8fbf --- /dev/null +++ b/src/qml/EOYStart.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts + +Item { + ColumnLayout { + anchors.fill: parent + // spacing: 2 + + Text { + id: txtIntro + Layout.fillWidth: true + Layout.fillHeight: true + + text: "EOY Return" + // anchors.fill: self + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pointSize: 30 + // fontSizeMode: Text.Fit + // minimumPixelSize: 30 + // font.pixelSize: 1000 + + // anchors.centerIn: self + } + + // Image { + // id: image + // width: 100 + // height: 100 + // source: "../images/logo.png" + // Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + // fillMode: Image.PreserveAspectFit + // } + + Button { + id: btnDropOff + text: "Begin Returns" + font.pointSize: 50 + Layout.fillWidth: true + Layout.fillHeight: true + onClicked: { + contentFrame.push(Qt.createComponent("EOYReturn.qml")) + } + } + Button { + id: btnPickUp + text: "Submit Saved Returns" + font.pointSize: 50 + Layout.fillWidth: true + Layout.fillHeight: true + onClicked: { + contentFrame.push(Qt.createComponent("EOYFinish.qml")) + } + } + } +} diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 28e35cf..216bd1a 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -77,7 +77,12 @@ ApplicationWindow { function onShowEOYReturnSignal() { // contentFrame.clear() contentFrame.push(Qt.createComponent("EOYReturn.qml")) - } + } + + function onShowEOYStartSignal() { + // contentFrame.clear() + contentFrame.push(Qt.createComponent("EOYStart.qml")) + } } // Automatically go to a non-standard kiosk mode start page @@ -90,7 +95,7 @@ ApplicationWindow { break; case 2: // EOY Return Only Mode - contentFrame.push(Qt.createComponent("EOYReturn.qml")) + contentFrame.push(Qt.createComponent("EOYStart.qml")) default: break; }