class StatisticsPanel(QWidget): _MAX_STAT_ROW = 3 def __init__(self, parent: QWidget = None): super().__init__(parent) self.spinner = QtWaitingSpinner(parent=self, centerOnParent=True, disableParentWhenSpinning=True) self.spinner.setInnerRadius(15) self.layout = QGridLayout() self.layout.setHorizontalSpacing(2) self.layout.setVerticalSpacing(4) self.setLayout(self.layout) def setStatistics(self, stat: Dict[str, object]) -> None: item: QLayoutItem = self.layout.takeAt(0) while item: item.widget().deleteLater() self.layout.removeItem(item) item = self.layout.takeAt(0) r: int = 0 c: int = 0 for k, v in stat.items(): self.layout.addWidget(QLabel('{}:'.format(k), self), r, c, 1, 1, alignment=Qt.AlignLeft) self.layout.addWidget(QLabel('{}'.format(str(v)), self), r, c + 1, 1, 1, alignment=Qt.AlignLeft) r += 1 if r % StatisticsPanel._MAX_STAT_ROW == 0: self.layout.setColumnMinimumWidth(c + 2, 5) # separator c += 3 r = 0
class PageAllegroMonitored(QWidget): def __init__(self, parent=None, shared_dict=None): QWidget.__init__(self) parent.addWidget(self) self.shared_dict = shared_dict self.parent = parent self.setLayoutDirection(Qt.LeftToRight) self.gridLayout = QGridLayout(self) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.scrollArea = QScrollArea(self) self.scrollArea.setStyleSheet("""QScrollArea{border: none;}""") self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.scrollArea.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored) self.scrollArea.setWidgetResizable(True) self.gridLayout.addWidget(self.scrollArea, 0, 0, 1, 1) self.scrollAreaWidgetContents = QWidget() self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 654, 479)) self.scrollArea.setWidget(self.scrollAreaWidgetContents) self.gridLayout_scroll_area = QGridLayout( self.scrollAreaWidgetContents) self.gridLayout_scroll_area.setSpacing(0) self.gridLayout_scroll_area.setContentsMargins(40, 0, 40, -1) self.label_title = QLabel("List of your monitored objects", self.scrollAreaWidgetContents) self.label_title.setStyleSheet(styles.label_title) self.label_title.setAlignment(Qt.AlignCenter) self.gridLayout_scroll_area.addWidget(self.label_title) self.spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) self.load_list() def load_list(self): elements = data.read_monitored_elements() for element in elements: e = ElementAllegroMonitored(element['name'], element['link'], element['is_done'], element['price'], element['xpath'], element['time'], element['is_monitoring'], self.scrollAreaWidgetContents, self.shared_dict) self.gridLayout_scroll_area.addWidget(e) self.gridLayout_scroll_area.addItem(self.spacer) def add_to_list(self, name, link, is_done, price, xpath, time, is_monitoring): self.gridLayout_scroll_area.removeItem(self.spacer) e = ElementAllegroMonitored(name, link, is_done, price, xpath, time, is_monitoring) self.gridLayout_scroll_area.addWidget(e) self.gridLayout_scroll_area.addItem(self.spacer)
class MainWindow(QMainWindow): def __init__(self): super().__init__() w = QWidget() hb = QHBoxLayout() hb.setSizeConstraint(QLayout.SetFixedSize) self._timer = QTimer() self._timer.timeout.connect(self.update_timer) self._timer.start(1000) # 1 second timer # tag::status[] self.mines = QLabel() self.mines.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.clock = QLabel() self.clock.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) f = self.mines.font() f.setPointSize(24) f.setWeight(75) self.mines.setFont(f) self.clock.setFont(f) self.clock.setText("000") self.button = QPushButton() self.button.setFixedSize(QSize(32, 32)) self.button.setIconSize(QSize(32, 32)) self.button.setIcon(QIcon("./icons/smiley.png")) self.button.setFlat(True) self.button.pressed.connect(self.button_pressed) self.statusBar() l = QLabel() l.setPixmap(QPixmap.fromImage(IMG_BOMB)) l.setAlignment(Qt.AlignRight | Qt.AlignVCenter) hb.addWidget(l) hb.addWidget(self.mines) hb.addWidget(self.button) hb.addWidget(self.clock) l = QLabel() l.setPixmap(QPixmap.fromImage(IMG_CLOCK)) l.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) hb.addWidget(l) vb = QVBoxLayout() vb.setSizeConstraint(QLayout.SetFixedSize) vb.addLayout(hb) # end::status[] # tag::grid[] self.grid = QGridLayout() self.grid.setSpacing(5) self.grid.setSizeConstraint(QLayout.SetFixedSize) # end::grid[] vb.addLayout(self.grid) w.setLayout(vb) self.setCentralWidget(w) self.menuBar().setNativeMenuBar(False) # tag::menuGame[] game_menu = self.menuBar().addMenu("&Game") new_game_action = QAction("New game", self) new_game_action.setStatusTip( "Start a new game (your current game will be lost)" ) new_game_action.triggered.connect(self.reset_map) game_menu.addAction(new_game_action) levels = game_menu.addMenu("Levels") for n, level in enumerate(LEVELS): level_action = QAction(level[0], self) level_action.setStatusTip("{1}x{1} grid, with {2} mines".format(*level)) level_action.triggered.connect(lambda _, n=n: self.set_level(n)) levels.addAction(level_action) # end::menuGame[] # Start on easy self.set_level(0) self.show() # tag::levelsInit[] def set_level(self, level): self.level_name, self.b_size, self.n_mines = LEVELS[level] self.setWindowTitle("Moonsweeper - %s" % (self.level_name)) self.mines.setText("%03d" % self.n_mines) self.clear_map() self.init_map() self.reset_map() # end::levelsInit[] # tag::clearMap[] def clear_map(self): # Remove all positions from the map, up to maximum size. for x in range(0, LEVELS[-1][1]): # <1> for y in range(0, LEVELS[-1][1]): c = self.grid.itemAtPosition(y, x) if c: # <2> self.grid.removeItem(c) c.widget().setParent(None) # end::clearMap[] # tag::initMap[] def init_map(self): # Add positions to the map for x in range(0, self.b_size): for y in range(0, self.b_size): w = Pos(x, y) self.grid.addWidget(w, y, x) # Connect signal to handle expansion. w.clicked.connect(self.trigger_start) w.revealed.connect(self.on_reveal) w.expandable.connect(self.expand_reveal) # Place resize on the event queue, giving control back to Qt before. QTimer.singleShot(0, lambda: self.resize(1, 1)) # <1> # end::initMap[] # tag::resetMap[] def reset_map(self): self._reset_position_data() self._reset_add_mines() self._reset_calculate_adjacency() self._reset_add_starting_marker() self.update_timer() # end::resetMap[] # tag::resetMap1[] def _reset_position_data(self): # Clear all mine positions for x in range(0, self.b_size): for y in range(0, self.b_size): w = self.grid.itemAtPosition(y, x).widget() w.reset() # end::resetMap1[] # tag::resetMap2[] def _reset_add_mines(self): # Add mine positions positions = [] while len(positions) < self.n_mines: x, y = ( random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1), ) if (x, y) not in positions: w = self.grid.itemAtPosition(y, x).widget() w.is_mine = True positions.append((x, y)) # Calculate end-game condition self.end_game_n = (self.b_size * self.b_size) - (self.n_mines + 1) return positions # end::resetMap2[] # tag::resetMap3[] def _reset_calculate_adjacency(self): def get_adjacency_n(x, y): positions = self.get_surrounding(x, y) return sum(1 for w in positions if w.is_mine) # Add adjacencies to the positions for x in range(0, self.b_size): for y in range(0, self.b_size): w = self.grid.itemAtPosition(y, x).widget() w.adjacent_n = get_adjacency_n(x, y) # end::resetMap3[] # tag::resetMap4[] def _reset_add_starting_marker(self): # Place starting marker. # Set initial status (needed for .click to function) self.update_status(STATUS_READY) while True: x, y = ( random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1), ) w = self.grid.itemAtPosition(y, x).widget() # We don't want to start on a mine. if not w.is_mine: w.is_start = True w.is_revealed = True w.update() # Reveal all positions around this, if they are not mines either. for w in self.get_surrounding(x, y): if not w.is_mine: w.click() break # Reset status to ready following initial clicks. self.update_status(STATUS_READY) # end::resetMap4[] # tag::surrounding[] def get_surrounding(self, x, y): positions = [] for xi in range(max(0, x - 1), min(x + 2, self.b_size)): for yi in range(max(0, y - 1), min(y + 2, self.b_size)): if not (xi == x and yi == y): positions.append(self.grid.itemAtPosition(yi, xi).widget()) return positions # end::surrounding[] # tag::statusButton[] def button_pressed(self): if self.status == STATUS_PLAYING: self.game_over() elif self.status == STATUS_FAILED or self.status == STATUS_SUCCESS: self.reset_map() # end::statusButton[] def reveal_map(self): for x in range(0, self.b_size): for y in range(0, self.b_size): w = self.grid.itemAtPosition(y, x).widget() w.reveal(False) # tag::expandReveal[] def expand_reveal(self, x, y): """ Iterate outwards from the initial point, adding new locations to the queue. This allows us to expand all in a single go, rather than relying on multiple callbacks. """ to_expand = [(x, y)] to_reveal = [] any_added = True while any_added: any_added = False to_expand, l = [], to_expand for x, y in l: positions = self.get_surrounding(x, y) for w in positions: if not w.is_mine and w not in to_reveal: to_reveal.append(w) if w.adjacent_n == 0: to_expand.append((w.x, w.y)) any_added = True # Iterate an reveal all the positions we have found. for w in to_reveal: w.reveal() # end::expandReveal[] def trigger_start(self, *args): if self.status == STATUS_READY: # First click. self.update_status(STATUS_PLAYING) # Start timer. self._timer_start_nsecs = int(time.time()) # tag::updateStatus[] def update_status(self, status): self.status = status self.button.setIcon(QIcon(STATUS_ICONS[self.status])) if status == STATUS_READY: self.statusBar().showMessage("Ready") # end::updateStatus[] def update_timer(self): if self.status == STATUS_PLAYING: n_secs = int(time.time()) - self._timer_start_nsecs self.clock.setText("%03d" % n_secs) elif self.status == STATUS_READY: self.clock.setText("%03d" % 0) # tag::endGame[] def on_reveal(self, w): if w.is_mine: self.game_over() else: self.end_game_n -= 1 # decrement remaining empty spaces if self.end_game_n == 0: self.game_won() def game_over(self): self.reveal_map() self.update_status(STATUS_FAILED) def game_won(self): self.reveal_map() self.update_status(STATUS_SUCCESS)
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, ui_demo=False): super(MainWindow, self).__init__() self.setupUi(self) self.grid_layout = QGridLayout() self.btnDetectClient.clicked.connect(self.detectClients) self.lineEditIpRange.returnPressed.connect(self.detectClients) self.btnSelectAllClients.clicked.connect(self.selectAllCLients) shortcut = QShortcut(QKeySequence(self.tr("Ctrl+A")), self) shortcut.activated.connect(self.selectAllCLients) self.btnUnselectClients.clicked.connect(self.unselectAllCLients) self.btnSelectExam.clicked.connect(self.selectExamByWizard) self.btnSelectExam.setEnabled(True) self.btnPrepareExam.clicked.connect(self.prepareExam) self.btnPrepareExam.setEnabled(False) self.btnGetExams.clicked.connect(self.retrieveExamFilesByWizard) self.btnGetExams.setEnabled(True) self.btnSaveExamLog.clicked.connect(self.saveExamLog) # self.btnSaveExamLog.setEnabled(False) self.actionBearbeiten.triggered.connect(self.openConfigDialog) self.actionAlle_Benutzer_benachrichtigen.triggered.connect( self.sendMessage) self.actionAlle_Clients_zur_cksetzen.triggered.connect( self.resetClients) self.actionAlle_Clients_rebooten.triggered.connect( self.rebootAllClients) self.actionAlle_Clients_herunterfahren.triggered.connect( self.shutdownAllClients) self.actionOnlineHelp.triggered.connect(self.openHelpUrl) self.actionOfflineHelp.triggered.connect(self.openHelpUrlOffline) self.actionSortClientByCandidateName.triggered.connect( self.sortButtonsByCandidateName) self.actionSortClientByComputerName.triggered.connect( self.sortButtonsByComputerName) self.actionVersionInfo.triggered.connect(self.showVersionInfo) self.actionDisplayIPs.triggered.connect(self.toggleClientIpDisplay) self.btnApplyCandidateNames.clicked.connect(self.applyCandidateNames) self.btnNameClients.clicked.connect(self.activateNameTab) self.btnBlockUsb.clicked.connect(self.blockUsb) self.btnBlockWebAccess.clicked.connect(self.blockWebAccess) self.appTitle = 'ECMan - Exam Client Manager' self.setWindowTitle(self.appTitle) self.logger = Logger(self.textEditLog) self.lb_directory = None self.advancedUi = False self.configure() self.show() if ui_demo is False: self.detectClients() else: self.clientFrame.setLayout(self.grid_layout) for i in range(6): self.addTestClient(i + 100) self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) def sortButtonsByCandidateName(self): ''' sort LbClient-widgets by candidate name :return: nothing ''' clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) ] clients = sorted(clients, key=lambda client: client.computer.candidateName) self.arrangeClientButtons(clients) def sortButtonsByComputerName(self): ''' supposed to sort LbClient-widgets by candidate name :return: nothing ''' clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) ] clients = sorted(clients, key=lambda client: client.computer.getHostName()) self.arrangeClientButtons(clients) def toggleClientIpDisplay(self): clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) ] for client in clients: client.toggleShowIp() def arrangeClientButtons(self, clients): try: for i in reversed(range(self.grid_layout.count())): self.grid_layout.removeItem(self.grid_layout.itemAt(i)) except: pass self.clientFrame.setLayout(self.grid_layout) for button in clients: self.grid_layout.addWidget(button, self.grid_layout.count() / 4, self.grid_layout.count() % 4) self.clientFrame.setLayout(self.grid_layout) def activateNameTab(self): if self.textEditCandidates.toPlainText() == "": candidates = "\n".join( str(x + 1) for x in range(self.grid_layout.count())) self.textEditCandidates.setText(candidates) self.tabs.setCurrentWidget(self.tab_candidates) def openHelpUrl(self): webbrowser.open( self.config.get( "General", "wikiurl", fallback="https://github.com/greenorca/ECMan/wiki")) def openHelpUrlOffline(self): webbrowser.open("file://" + os.getcwd().replace("\\", "/") + "/help/Home.html") def checkOldLogFiles(self): """ dummy, """ pass def blockUsb(self): block = self.btnBlockUsb.text() == "USB blockieren" clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) if self.grid_layout.itemAt(i).widget().isSelected ] if block: for client in clients: client.blockUsbAccessThread() self.btnBlockUsb.setText("USB freigeben") else: for client in clients: client.allowUsbAccessThread() self.btnBlockUsb.setText("USB blockieren") def blockWebAccess(self): block = self.btnBlockWebAccess.text() == "Web blockieren" clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) if self.grid_layout.itemAt(i).widget().isSelected ] if block: for client in clients: client.blockInternetAccessThread() self.btnBlockWebAccess.setText("Web freigeben") else: for client in clients: client.allowInternetAccessThread() self.btnBlockWebAccess.setText("Web blockieren") def resetClients(self): """ resets remote files and configuration for all connected clients """ items = ["Ja, Namen zurücksetzen", "Nein, Namen beibehalten"] item, ok = QInputDialog().getItem( self, "LB-Status zurücksetzen", "USB-Sticks und Internet werden freigeben.\nDaten im Benutzerverzeichnis werden NICHT gelöscht.\nKandidaten-Namen zurücksetzen? ", items, 0, False) if ok is False: return resetCandidateName = True if item.startswith("Ja") else False clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) ] progressDialog = EcManProgressDialog(self, "Reset Clients") progressDialog.setMaxValue(self.grid_layout.count()) progressDialog.resetValue() progressDialog.open() self.worker = ResetClientsWorker(clients, resetCandidateName) self.worker.updateProgressSignal.connect(progressDialog.incrementValue) self.worker.start() def closeEvent(self, event): """ overrides closeEvent of base class, clean up """ print("cleaning up...") # try:* especially remove shares created on Windows hosts # for share in self.sharenames: # Thread(target=self.runLocalPowerShellAsRoot("Remove-SmbShare -Name {} -Force".format(share))).start() # except Exception: # pass # print("done cleaning up...") super(MainWindow, self).closeEvent(event) def selectAllCLients(self): """ marks / selects all connected client pcs """ for i in range(self.grid_layout.count()): self.grid_layout.itemAt(i).widget().select() def unselectAllCLients(self): """ unmarks / unselects all connected client pcs """ for i in range(self.grid_layout.count()): self.grid_layout.itemAt(i).widget().unselect() def shutdownAllClients(self): for i in range(self.grid_layout.count()): self.grid_layout.itemAt(i).widget().shutdownClient() def rebootAllClients(self): for i in range(self.grid_layout.count()): self.grid_layout.itemAt(i).widget().computer.reboot() self.grid_layout.itemAt(i).widget().deleteLater() # TODO TEST sven #for i in range(self.grid_layout.count()): # self.grid_layout.removeItem(self.grid_layout.itemAt(i)) def sendMessage(self): message, ok = QInputDialog.getText(self, "Eingabe", "Nachricht an Kandidaten eingeben") if ok: for i in range(self.grid_layout.count()): QThreadPool.globalInstance().start( SendMessageTask( self.grid_layout.itemAt(i).widget(), message)) def applyCandidateNames(self): """ reads candidate names from respective textEditField (line by line) and applies these names to (random) client pcs """ names = self.textEditCandidates.toPlainText().rstrip().splitlines() # cleanup and remove duplicate names names = [x.strip() for x in names] names = list(set(names)) clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) ] # select only the computers without candidate name if self.checkBox_OverwriteExisitingNames.checkState( ) != Qt.CheckState.Checked: clients = [ x for x in clients if x.computer.getCandidateName() == "" or x.computer.getCandidateName() is None ] if len(names) > len(clients): self.showMessageBox( "Fehler", "Nicht genug Prüfungs-PCs für alle {} Kandidaten".format( str(len(names))), messageType=QMessageBox.Warning) return progressDialog = EcManProgressDialog( self, "Fortschritt Kandidatennamen setzen") progressDialog.setMaxValue(len(names)) progressDialog.resetValue() self.worker = SetCandidateNamesWorker(clients, names) self.worker.updateProgressSignal.connect(progressDialog.incrementValue) self.worker.start() progressDialog.open() self.tabs.setCurrentWidget(self.tab_pcs) def configure(self): """ sets inial values for app """ self.port = 5986 self.server = None self.configFile = Path(str(Path.home()) + "/.ecman.conf") if not (self.configFile.exists()): result = self.openConfigDialog() if self.configFile.exists() or result == 1: self.readConfigFile() self.result_directory = "" self.sharenames = [ ] # dump all eventually created local Windows-Shares in this array self.debug = True # fetch own ip adddress s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(('9.9.9.9', 1)) # connect() for UDP doesn't send packets self.ip_address = s.getsockname()[0] parts = self.ip_address.split(".")[0:-1] self.ipRange = ".".join(parts) + ".*" self.lineEditIpRange.setText(self.ipRange) self.appTitle = self.windowTitle() + " on " + self.ip_address self.setWindowTitle(self.appTitle) except Exception as ex: self.ipRange, ok = QInputDialog.getText( self, "Keine Verbindung zum Internet", "Möglicherweise gelingt der Sichtflug. Bitte geben Sie die lokale IP-Adresse ein:" ) self.log("no connection to internet:" + str(ex)) def readConfigFile(self): """ reads config file into class variables """ self.config = ConfigParser() self.config.read_file(open(str(self.configFile))) self.port = self.config.get("General", "winrm_port", fallback=5986) self.client_lb_user = self.config.get("Client", "lb_user", fallback="student") self.user = self.config.get("Client", "user", fallback="") self.passwd = self.config.get("Client", "pwd", fallback="") self.maxFiles = int( self.config.get("Client", "max_files", fallback="1000")) self.maxFileSize = int( self.config.get("Client", "max_filesize", fallback="1000")) * 1024 * 1024 # thats MB now... self.advancedUi = self.config.get('General', 'advanced_ui', fallback="False") == "True" pass def openConfigDialog(self): """ opens configuration dialog """ config_dialog = EcManConfigDialog(self) result = config_dialog.exec_() if result == 1: config_dialog.saveConfig() self.readConfigFile() return result def log(self, message): """ basic logging functionality TODO: improve... """ self.logger.log(message) def updateProgressBar(self, value): self.progressBar.setValue(value) if (value == self.progressBar.maximum()): self.enableButtons(True) self.progressBar.setEnabled(False) def enableButtons(self, enable): if type(enable) != bool: raise Exception("Invalid parameter, must be boolean") self.btnNameClients.setEnabled(enable) self.btnSelectAllClients.setEnabled(enable) self.btnUnselectClients.setEnabled(enable) self.btnPrepareExam.setEnabled(enable) self.btnGetExams.setEnabled(enable) self.btnSaveExamLog.setEnabled(enable) self.btnDetectClient.setEnabled(enable) self.btnBlockWebAccess.setEnabled(enable) self.btnBlockUsb.setEnabled(enable) def retrieveExamFilesByWizard(self): """ retrieve exam files for all clients """ # find all suitable clients (required array for later threading) clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) if self.grid_layout.itemAt(i).widget().isSelected and self.grid_layout.itemAt(i).widget().computer.state in [ Computer.State.STATE_DEPLOYED, Computer.State.STATE_FINISHED, Computer.State.STATE_RETRIVAL_FAIL ] ] if len(clients) == 0: self.showMessageBox("Achtung", "Keine Clients ausgewählt bzw. deployed") return unknownClients = [ c for c in clients if c.computer.getCandidateName() == "" or c.computer.getCandidateName() == None ] for unknown in unknownClients: unknown.computer.state = Computer.State.STATE_RETRIVAL_FAIL unknown._colorizeWidgetByClientState() if unknownClients != []: choice = QMessageBox.critical( self, "Achtung", "{} clients ohne gültigen Kandidatennamen.<br>Rückholung für alle anderen fortsetzen?" .format(str(len(unknownClients))), QMessageBox.Yes, QMessageBox.No) if choice == QMessageBox.No: return clients = [c for c in clients if c not in unknownClients] retVal = QMessageBox.StandardButton.Yes if self.result_directory != "": items = [ "Ergebnispfad neu auswählen", "Weiter mit bisherigem Verzeichnis" ] item, ok = QInputDialog().getItem( self, "Achtung", "LB-Ergebnisverzeichnis ist bereits ausgewählt.\nErneutes Abholen kann existierende Ergebnisse überschreiben.\nWas möchten Sie tun?", items, 0, False) if ok is False: return if self.result_directory == "" or item == "Ergebnispfad neu auswählen": if self.server is None or self.server.connect() is not True: self.server = self.getServerCredentialsByWizard() if self.server is None: return wizard = EcShareWizard( parent=self, server=self.server, wizardType=EcShareWizard.TYPE_RESULT_DESTINATION, advanced_Ui=self.advancedUi) wizard.setModal(True) result = wizard.exec_() print("I'm done, wizard result=" + str(result)) if result == 1: print("selected values: %s - %s - %s" % (wizard.field("username"), wizard.field("servername"), wizard.selectedPath)) self.result_directory = wizard.selectedPath else: print("Abbruch, kein Zielverzeichnis ausgewählt") return self.result_directory = self.result_directory.replace("/", "#") self.log("save result files into: " + self.result_directory.replace("#", "\\")) progressDialog = EcManProgressDialog( self, "Fortschritt Ergebnisse kopieren") progressDialog.setMaxValue(len(clients)) progressDialog.resetValue() progressDialog.open() self.log("starting to retrieve files") self.worker = RetrieveResultsWorker(clients, self.server.user, self.server.password, self.server.domain, self.result_directory, self.maxFiles, self.maxFileSize) self.worker.updateProgressSignal.connect(progressDialog.incrementValue) self.worker.start() self.btnSaveExamLog.setEnabled(True) def prepareExam(self): self.copyFilesToClient() pass def copyFilesToClient(self): """ copies selected exam folder to all connected clients that are selected and not in STATE_DEPLOYED or STATE_FINISHED """ if self.lb_directory == None or self.lb_directory == "": self.showMessageBox("Fehler", "Kein Prüfungsordner ausgewählt") return clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) if self.grid_layout.itemAt(i).widget().isSelected and self.grid_layout.itemAt(i).widget().computer.state not in [Computer.State.STATE_DEPLOYED, Computer.State.STATE_FINISHED] ] if len([x for x in clients if x.computer.candidateName == None]) > 0: self.showMessageBox("Warnung", "Bitte Kandidatenname für alle PCs vergeben") return if len(clients) == 0: self.showMessageBox( "Warnung", "Keine Client-PCs ausgewählt oder Prüfungen bereits aktiv") return progressDialog = EcManProgressDialog( self, "Fortschritt LB-Client-Deployment") progressDialog.setMaxValue(len(clients)) progressDialog.resetValue() progressDialog.open() self.worker = CopyExamsWorker( clients, self.server.user, self.server.password, self.server.domain, src=self.lb_directory, reset=(self.checkBoxWipeHomedir.checkState() == Qt.CheckState.Checked)) self.worker.updateProgressSignal.connect(progressDialog.incrementValue) self.worker.start() def detectClients(self): """ starts portscan to search for winrm enabled clients """ ip_range = self.lineEditIpRange.text() if not (ip_range.endswith('*')): self.showMessageBox( 'Eingabefehler', 'Gültiger IP-V4 Bereich endet mit * (z.B. 192.168.0.*)') return try: self.worker.exit() except Exception as ex: print("crashed on stopping existing scanner thread: " + str(ex)) self.ipRange = ip_range self.progressBar.setEnabled(True) self.progressBar.setValue(0) self.enableButtons(enable=False) self.clientCount = 0 # clear previous client buttons try: for i in reversed(range(self.grid_layout.count())): self.grid_layout.itemAt(i).widget().close() self.grid_layout.itemAt(i).widget().deleteLater() except: pass self.clientFrame.setLayout(self.grid_layout) self.progressBar.setMaximum(253) self.worker = ScannerWorker(self.ipRange, self.ip_address) self.worker.updateProgressSignal.connect(self.updateProgressBar) self.worker.addClientSignal.connect(self.addClient) self.worker.start() def addClient(self, ip): """ populate GUI with newly received client ips param ip: only last byte required param scan: wether or not scan the Client (set to False only for GUI testing) """ self.log("new client signal received: " + str(ip)) self.clientCount += 1 self.statusBar.showMessage( str(self.clientCount) + " clients detected", 0) clientIp = self.ipRange.replace("*", str(ip)) button = LbClient(clientIp, remoteAdminUser=self.user, passwd=self.passwd, candidateLogin=self.client_lb_user, parentApp=self) button.setMinimumHeight(50) # button.installEventFilter(self) self.grid_layout.addWidget(button, self.grid_layout.count() / 4, self.grid_layout.count() % 4) self.clientFrame.setLayout(self.grid_layout) # QtGui.qApp.processEvents() def addTestClient(self, ip): """ populate GUI with dummy buttons param ip: only last byte required """ self.log("new client signal received: " + str(ip)) clientIp = self.ipRange.replace("*", str(ip)) button = LbClient(clientIp, remoteAdminUser=self.user, passwd=self.passwd, candidateLogin=self.client_lb_user, parentApp=self, test=True) button.setMinimumHeight(50) # button.installEventFilter(self) self.grid_layout.addWidget(button, self.grid_layout.count() / 4, self.grid_layout.count() % 4) self.clientFrame.setLayout(self.grid_layout) # QtGui.qApp.processEvents() def getExamPath(self): return self.lb_directory def getServerCredentialsByWizard(self): """ open server config and login dialog, returns server object or None """ wizard = EcLoginWizard(parent=self, username=self.config.get("General", "username", fallback=""), domain=self.config.get("General", "domain", fallback=""), servername=self.config.get("General", "lb_server", fallback="")) wizard.setModal(True) result = wizard.exec_() print("I'm done, wizard result=" + str(result)) if result == 1: self.config["General"]["username"] = wizard.field("username") self.config["General"]["domain"] = wizard.field("domainname") self.config["General"]["servername"] = wizard.server.serverName self.saveConfig() return wizard.server return None def selectExamByWizard(self): """ provides ability to select serverName share plus logon credentials and lb directory using a wizard """ if self.server == None or self.server.connect() is False: self.server = self.getServerCredentialsByWizard() wizard = EcShareWizard(parent=self, server=self.server, wizardType=EcShareWizard.TYPE_LB_SELECTION, advanced_Ui=self.advancedUi) wizard.setModal(True) result = wizard.exec_() print("I'm done, wizard result=" + str(result)) if result == 1: self.lb_directory = wizard.selectedPath self.setWindowTitle(self.appTitle + " - LB-Verzeichnis::" + self.lb_directory.split("/")[-1]) self.log("setup LB directory: " + self.lb_directory.split("/")[-1]) self.btnPrepareExam.setEnabled(True) self.lblExamName.setText(self.lb_directory) else: self.log("no valid share selected") def saveExamLog(self): """ on demand, store all client logs as PDF """ if self.result_directory == None or len(self.result_directory) == 0: self.showMessageBox( "Fehler", "Ergebnispfad für Prüfungsdaten nicht gesetzt.<br>Bitte zuerst Prüfungsdaten abholen.", QMessageBox.Error) return clients = [ self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count()) ] for client in clients: lb_dataDirectory = client.computer.lb_dataDirectory.split("#")[-1] pdfFileName = "protocol_" + date.today( ).__str__() + "_" + client.computer.getCandidateName().replace( " ", "_") + ".pdf" LogfileHandler(client.computer.logfile_name, client.computer.getCandidateName()). \ createPdf(pdfFileName) if not (self.server.connect()): self.showMessageBox( "Fehler", "Verbindung zum Server kann nicht aufgebaut werden.", QMessageBox.Error) return with open(pdfFileName, "rb") as file: sharename = self.result_directory.replace("##", "").split("#")[1] destination = "/".join( self.result_directory.replace( "##", "").split("#")[2:]) + "/" + lb_dataDirectory + "/" self.server.conn.storeFile(sharename, destination + pdfFileName, file) def __runLocalPowerShellAsRoot(self, command): """ maybe useful at some stage again # see https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-shellexecutew#parameters :param command: :return: """ retval = ctypes.windll.shell32.ShellExecuteW( None, "runas", # runas admin, "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\powershell.exe", # file to run command, # actual powershell command None, 0) # last param disables popup powershell window... if retval != 42: self.log("ReturnCode after creating smbShare: " + str(retval)) subprocess.run([ "C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\powershell.exe", "Get-SmbShare" ]) raise Exception("ReturnCode after running powershell: " + str(retval)) def __openFile(self): fname = QFileDialog.getOpenFileName(self, 'Open file', '/home') if fname[0]: f = open(fname[0], 'r') with f: data = f.read() doc = QTextDocument(data, None) self.textEditLog.setDocument(doc) def saveConfig(self): """ write to file what's currently set in config """ if not (self.configFile.exists()): self.configFile.touch() self.config.write(open(self.configFile, 'w')) def eventFilter(self, currentObject, event): """ unused, define mouseover events (with tooltips) for LbClient widgets """ if event.type() == QEvent.Enter: if isinstance(currentObject, LbClient): print("Mouseover event catched") currentObject.setOwnToolTip() return True else: self.log(str(type(currentObject)) + " not recognized") # elif event.type() == QEvent.Leave: # pass return False def showMessageBox(self, title, message, messageType=QMessageBox.Information): """ convinence wrapper """ msg = QMessageBox(messageType, title, message, parent=self) if messageType != QMessageBox.Information: msg.setStandardButtons(QMessageBox.Abort) return msg.exec_() def showVersionInfo(self): info = "<b>Offizielles Release:</b><br>" + version try: # latest release - make sure to save this file before building :-) modDate = time.localtime(os.path.getmtime(__file__)) info = info + "<br><br><b>Diese Version:</b><br>" + time.strftime( "%Y-%m-%d", modDate) except: pass self.showMessageBox("ECMan - Version", info)
class SequenceRecordsWindow(QWidget): def __init__(self, parent): super(SequenceRecordsWindow, self).__init__(parent) self.grid_layout = QGridLayout() self.grid_layout.setContentsMargins(0, 0, 0, 0) self.grid_layout.setSpacing(0) self.setLayout(self.grid_layout) self.seq_font = QFont() self.seq_font.setFamily("Noto Sans Mono") self.seq_font.setPointSize(12) self.seq_font.setFixedPitch(True) self.seq_font.setStyleHint(QFont.Monospace) self.seq_h_scroll_bar = QScrollBar(self, self.parent()) self.seq_h_scroll_bar.setOrientation(Qt.Horizontal) self.seq_h_scroll_bar.setMinimum(0) self.seq_h_scroll_bar.setMaximum(self.longest_seq_len - self.char_nb) self.seq_h_scroll_bar.valueChanged.connect(self.move_seqs) self.grid_layout.addWidget(self.seq_h_scroll_bar, self.grid_layout.rowCount(), 5) self.lower_spacer_item = QSpacerItem(1, 1, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) self.grid_layout.addItem(self.lower_spacer_item) self.seq_record_items = [] def sizeHint(self): # Workaroud QTBUG-70305 return self.parent().parent().size() def populate(self, seq_records): self.grid_layout.removeWidget(self.seq_h_scroll_bar) self.grid_layout.removeItem(self.lower_spacer_item) for seq_record in seq_records: new_row = self.grid_layout.rowCount() self.seq_record_items.append( SequenceRecordItem(self, seq_record, self.seq_font)) for widget_index in range(0, len(self.seq_record_items[-1].widgets)): col = widget_index self.seq_record_items[-1].seqLabel.installEventFilter(self) self.grid_layout.addWidget( self.seq_record_items[-1].widgets[widget_index], new_row, col) if len(seq_record) > self.longest_seq_len: self.longest_seq_len = len(seq_record) self.update_char_nb() self.grid_layout.addWidget(self.seq_h_scroll_bar, self.grid_layout.rowCount(), 5) self.grid_layout.addItem(self.lower_spacer_item) self.display_all_seq() def clear(self): # TODO pass def eventFilter(self, watched, event): if event.type() == QEvent.Resize: self.update_char_nb() self.update_scrollbar() self.display_all_seq() return super(SequenceRecordsWindow, self).eventFilter(watched, event) def display_all_seq(self): for seq_record_item in self.seq_record_items: seq_record_item.seqLabel.display_seq( seq_record_item.seq_record.seq, self.display_begin, self.char_nb) def update_char_nb(self): font_metrics = QFontMetrics(self.seq_font) px_wide_char = font_metrics.width("A") label_width = self.seq_record_items[0].seqLabel.width( ) # width of first seq label = all seq labels approx_char_nb = label_width // px_wide_char test_str = "A" * approx_char_nb while font_metrics.width( test_str) < label_width: # fontMetrics not precise at all... test_str += "A" while font_metrics.width( test_str) >= label_width: # fontMetrics not precise at all... test_str = test_str[:-1] self.char_nb = len(test_str) def update_scrollbar(self): self.seq_h_scroll_bar.setMaximum(self.longest_seq_len - self.char_nb + 12) def move_seqs(self, value): print(value) self.display_begin = value self.display_all_seq() char_nb = 0 longest_seq_len = 0 display_begin = 0
class JointStatePublisherGui(QWidget): sliderUpdateTrigger = Signal() def __init__(self, title, jsp, num_rows=0): super(JointStatePublisherGui, self).__init__() self.jsp = jsp self.joint_map = {} self.vlayout = QVBoxLayout(self) self.scrollable = QWidget() self.gridlayout = QGridLayout() self.scroll = QScrollArea() self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll.setWidgetResizable(True) font = QFont("Helvetica", 9, QFont.Bold) ### Generate sliders ### sliders = [] for name in self.jsp.joint_list: if name not in self.jsp.free_joints: continue joint = self.jsp.free_joints[name] if joint['min'] == joint['max']: continue joint_layout = QVBoxLayout() row_layout = QHBoxLayout() label = QLabel(name) label.setFont(font) row_layout.addWidget(label) display = QLineEdit("0.00") display.setAlignment(Qt.AlignRight) display.setFont(font) display.setReadOnly(True) row_layout.addWidget(display) joint_layout.addLayout(row_layout) slider = QSlider(Qt.Horizontal) slider.setFont(font) slider.setRange(0, RANGE) slider.setValue(RANGE / 2) joint_layout.addWidget(slider) self.joint_map[name] = { 'slidervalue': 0, 'display': display, 'slider': slider, 'joint': joint } # Connect to the signal provided by QSignal slider.valueChanged.connect(self.onValueChanged) sliders.append(joint_layout) # Determine number of rows to be used in grid self.num_rows = num_rows # if desired num of rows wasn't set, default behaviour is a vertical layout if self.num_rows == 0: self.num_rows = len(sliders) # equals VBoxLayout # Generate positions in grid and place sliders there self.positions = self.generate_grid_positions(len(sliders), self.num_rows) for item, pos in zip(sliders, self.positions): self.gridlayout.addLayout(item, *pos) # Set zero positions read from parameters self.center() # Synchronize slider and displayed value self.sliderUpdate(None) # Set up a signal for updating the sliders based on external joint info self.sliderUpdateTrigger.connect(self.updateSliders) self.scrollable.setLayout(self.gridlayout) self.scroll.setWidget(self.scrollable) self.vlayout.addWidget(self.scroll) # Buttons for randomizing and centering sliders and # Spinbox for on-the-fly selecting number of rows self.randbutton = QPushButton('Randomize', self) self.randbutton.clicked.connect(self.randomize_event) self.vlayout.addWidget(self.randbutton) self.ctrbutton = QPushButton('Center', self) self.ctrbutton.clicked.connect(self.center_event) self.vlayout.addWidget(self.ctrbutton) self.maxrowsupdown = QSpinBox() self.maxrowsupdown.setMinimum(1) self.maxrowsupdown.setMaximum(len(sliders)) self.maxrowsupdown.setValue(self.num_rows) self.maxrowsupdown.lineEdit().setReadOnly( True) # don't edit it by hand to avoid weird resizing of window self.maxrowsupdown.valueChanged.connect(self.reorggrid_event) self.vlayout.addWidget(self.maxrowsupdown) self.setLayout(self.vlayout) @pyqtSlot(int) def onValueChanged(self, event): # A slider value was changed, but we need to change the joint_info metadata. for name, joint_info in self.joint_map.items(): joint_info['slidervalue'] = joint_info['slider'].value() joint = joint_info['joint'] joint['position'] = self.sliderToValue(joint_info['slidervalue'], joint) joint_info['display'].setText("%.2f" % joint['position']) @pyqtSlot() def updateSliders(self): self.update_sliders() def update_sliders(self): for name, joint_info in self.joint_map.items(): joint = joint_info['joint'] joint_info['slidervalue'] = self.valueToSlider( joint['position'], joint) joint_info['slider'].setValue(joint_info['slidervalue']) def center_event(self, event): self.center() def center(self): rospy.loginfo("Centering") for name, joint_info in self.joint_map.items(): joint = joint_info['joint'] joint_info['slider'].setValue( self.valueToSlider(joint['zero'], joint)) def reorggrid_event(self, event): self.reorganize_grid(event) def reorganize_grid(self, number_of_rows): self.num_rows = number_of_rows # Remove items from layout (won't destroy them!) items = [] for pos in self.positions: item = self.gridlayout.itemAtPosition(*pos) items.append(item) self.gridlayout.removeItem(item) # Generate new positions for sliders and place them in their new spots self.positions = self.generate_grid_positions(len(items), self.num_rows) for item, pos in zip(items, self.positions): self.gridlayout.addLayout(item, *pos) def generate_grid_positions(self, num_items, num_rows): if num_rows == 0: return [] positions = [ (y, x) for x in range(int((math.ceil(float(num_items) / num_rows)))) for y in range(num_rows) ] positions = positions[:num_items] return positions def randomize_event(self, event): self.randomize() def randomize(self): rospy.loginfo("Randomizing") for name, joint_info in self.joint_map.items(): joint = joint_info['joint'] joint_info['slider'].setValue( self.valueToSlider(random.uniform(joint['min'], joint['max']), joint)) def sliderUpdate(self, event): for name, joint_info in self.joint_map.items(): joint_info['slidervalue'] = joint_info['slider'].value() self.update_sliders() def valueToSlider(self, value, joint): return (value - joint['min']) * float(RANGE) / (joint['max'] - joint['min']) def sliderToValue(self, slider, joint): pctvalue = slider / float(RANGE) return joint['min'] + (joint['max'] - joint['min']) * pctvalue
class CodePreview_Widget(QWidget): def __init__(self): super(CodePreview_Widget, self).__init__() self.text_edit = CodePreview_TextEdit() self.node_instance = None self.buttons_obj_dict = {} self.active_class_index = -1 self.edited_codes = {} settings_layout = QHBoxLayout() info_and_SH_layout = QVBoxLayout() # info label info_label = QLabel('Click on edit for more info!') info_label.setFont(QFont('Poppins', 8)) info_and_SH_layout.addWidget(info_label) # syntax highlighting self.syntax_highlighting_check_box = QCheckBox('syntax highlighting (alpha)') self.syntax_highlighting_check_box.toggled.connect(self.syntax_highlighting_toggled) self.syntax_highlighting_check_box.setChecked(True) info_and_SH_layout.addWidget(self.syntax_highlighting_check_box) settings_layout.addLayout(info_and_SH_layout) # class radio buttons widget self.class_selection_layout = QGridLayout() settings_layout.addLayout(self.class_selection_layout) settings_layout.setAlignment(self.class_selection_layout, Qt.AlignRight) # edit source code buttons edit_buttons_layout = QVBoxLayout() self.edit_code_button = QPushButton('edit') self.edit_code_button.setMaximumWidth(100) self.edit_code_button.clicked.connect(self.edit_code_button_clicked) self.override_code_button = QPushButton('override') self.override_code_button.setMaximumWidth(100) self.override_code_button.setEnabled(False) self.override_code_button.clicked.connect(self.override_code_button_clicked) self.reset_code_button = QPushButton('reset') self.reset_code_button.setMaximumWidth(206) self.reset_code_button.setEnabled(False) self.reset_code_button.clicked.connect(self.reset_code_button_clicked) edit_buttons_top_layout = QHBoxLayout() edit_buttons_top_layout.addWidget(self.edit_code_button) edit_buttons_top_layout.addWidget(self.override_code_button) edit_buttons_layout.addLayout(edit_buttons_top_layout) edit_buttons_layout.addWidget(self.reset_code_button) settings_layout.addLayout(edit_buttons_layout) main_layout = QVBoxLayout() main_layout.addLayout(settings_layout) main_layout.addWidget(self.text_edit) self.setLayout(main_layout) self.set_new_NI(None) def set_new_NI(self, ni): self.disable_editing() self.rebuild_class_selection(ni) self.update_edit_status() self.node_instance = ni if ni is None: # no NI selected self.text_edit.set_code('') self.edit_code_button.setEnabled(False) self.override_code_button.setEnabled(False) self.reset_code_button.setEnabled(False) return self.edit_code_button.setEnabled(True) self.update_code() def update_code(self): self.disable_editing() if self.active_class_index == -1 or self.node_instance is None: return if self.get_current_code_obj() not in self.edited_codes: self.text_edit.set_code(inspect.getsource(self.get_current_code_class())) self.reset_code_button.setEnabled(False) else: self.text_edit.set_code(self.edited_codes[self.get_current_code_obj()]) self.reset_code_button.setEnabled(True) def get_current_code_class(self): return self.get_current_code_obj().__class__ def get_current_code_obj(self): return list(self.buttons_obj_dict.values())[self.active_class_index] def rebuild_class_selection(self, obj): # clear layout for i in range(self.class_selection_layout.count()): item = self.class_selection_layout.itemAt(0) widget = item.widget() widget.hide() self.class_selection_layout.removeItem(item) self.buttons_obj_dict = {} self.active_class_index = -1 if find_type_in_object(obj, NodeInstance): # NI class node_inst_class_RB = QRadioButton('NodeInstance') node_inst_class_RB.toggled.connect(self.class_RB_toggled) self.buttons_obj_dict[node_inst_class_RB] = obj self.class_selection_layout.addWidget(node_inst_class_RB, 0, 0) # main_widget class if obj.main_widget is not None: main_widget_class_RB = QRadioButton('MainWidget') main_widget_class_RB.toggled.connect(self.class_RB_toggled) self.buttons_obj_dict[main_widget_class_RB] = obj.main_widget self.class_selection_layout.addWidget(main_widget_class_RB, 1, 0) # data input widgets row_count = 0 for inp in obj.inputs: if inp.widget is not None: inp_widget_class_RB = QRadioButton('Input '+str(obj.inputs.index(inp))) inp_widget_class_RB.toggled.connect(self.class_RB_toggled) self.buttons_obj_dict[inp_widget_class_RB] = inp.widget self.class_selection_layout.addWidget(inp_widget_class_RB, row_count, 1) row_count += 1 node_inst_class_RB.setChecked(True) def update_edit_status(self): for o in list(self.buttons_obj_dict.keys()): if self.edited_codes.keys().__contains__(self.buttons_obj_dict[o]): o.setStyleSheet('color: #3B9CD9;') f = o.font() f.setBold(True) o.setFont(f) else: o.setStyleSheet('color: white;') f = o.font() f.setBold(False) o.setFont(f) def class_RB_toggled(self, checked): if checked: self.active_class_index = list(self.buttons_obj_dict.keys()).index(self.sender()) self.update_code() def syntax_highlighting_toggled(self): if self.syntax_highlighting_check_box.isChecked(): self.text_edit.enable_highlighting() else: self.text_edit.disable_highlighting() def edit_code_button_clicked(self): info_dialog = EditSourceCode_Dialog(self) accepted = info_dialog.exec_() if accepted: self.enable_editing() def enable_editing(self): self.text_edit.enable_editing() self.override_code_button.setEnabled(True) def disable_editing(self): self.text_edit.disable_editing() self.override_code_button.setEnabled(False) def override_code_button_clicked(self): new_code = self.text_edit.get_code() override_code(self.get_current_code_obj(), new_code) self.disable_editing() self.edited_codes[self.get_current_code_obj()] = new_code self.reset_code_button.setEnabled(True) self.update_edit_status() def reset_code_button_clicked(self): code = inspect.getsource(self.get_current_code_class()) override_code(self.get_current_code_obj(), code) del self.edited_codes[self.get_current_code_obj()] self.update_code() self.update_edit_status()