Example #1
0
class ConnectSettingsDialog(QDialog):
    def __init__(self, endpoints, security_mode, security_policy,
                 certificate_path, private_key_path):
        super().__init__()
        self.ui = Ui_ConnectSettingsDialog()
        self.ui.setupUi(self)

        self.endpoints_model = QStandardItemModel()
        self.ui.endpointsView.setModel(self.endpoints_model)
        self.endpoints_model.setHorizontalHeaderLabels([
            "Endpoint URL", "Security Mode", "Security Policy",
            "Transport Profile URI"
        ])
        self.ui.endpointsView.setColumnWidth(0, 300)
        self.ui.endpointsView.selectionModel().selectionChanged.connect(
            self.select_security)

        # Dict of endpoints with security modes as keys and security policies as values
        self.endpoints_dict = {"None": [], "Sign": [], "SignAndEncrypt": []}
        self.certificate_path = certificate_path
        self.private_key_path = private_key_path

        self._init_fields(endpoints, security_mode, security_policy)

        self.ui.modeComboBox.currentTextChanged.connect(self._change_policies)
        self.ui.policyComboBox.currentTextChanged.connect(
            self._select_endpoint)
        self.ui.certificateButton.clicked.connect(self.select_certificate)
        self.ui.privateKeyButton.clicked.connect(self.select_private_key)
        self.ui.generateButton.clicked.connect(self.generate_certificate)
        self.ui.connectButton.clicked.connect(self.accept)
        self.ui.cancelButton.clicked.connect(self.reject)

    @trycatchslot
    def _init_fields(self, endpoints, security_mode, security_policy):
        for edp in endpoints:
            mode = edp.SecurityMode.name
            if mode == "None_":
                mode = "None"
            policy = edp.SecurityPolicyUri.split("#")[1]
            transport_profile = edp.TransportProfileUri
            row = [
                QStandardItem(edp.EndpointUrl),
                QStandardItem(mode),
                QStandardItem(policy),
                QStandardItem(transport_profile)
            ]
            if transport_profile == "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary":
                # Endpoint supported
                self.endpoints_dict[mode].append(policy)
            else:
                # Endpoint not supported
                for col in row:
                    col.setData(QBrush(QColor(255, 183, 183)),
                                Qt.BackgroundRole)
            self.endpoints_model.appendRow(row)

        self.ui.modeComboBox.clear()
        self.ui.policyComboBox.clear()

        self.ui.modeComboBox.addItems(self.endpoints_dict.keys())
        self.ui.modeComboBox.setCurrentText(security_mode)
        current_mode = self.ui.modeComboBox.currentText()
        self.ui.policyComboBox.addItems(self.endpoints_dict[current_mode])
        self.ui.policyComboBox.setCurrentText(security_policy)

        self._select_endpoint(security_policy)

        self.ui.certificateLabel.setText(Path(self.certificate_path).name)
        self.ui.privateKeyLabel.setText(Path(self.private_key_path).name)

        self._toggle_security_fields(current_mode)

    def _change_policies(self, mode):
        self.ui.policyComboBox.clear()
        self.ui.policyComboBox.addItems(self.endpoints_dict[mode])
        self._toggle_security_fields(mode)

    def _toggle_security_fields(self, mode):
        if mode == "None":
            self.ui.certificateButton.setEnabled(False)
            self.ui.certificateLabel.hide()
            self.ui.privateKeyButton.setEnabled(False)
            self.ui.privateKeyLabel.hide()
            self.ui.generateButton.setEnabled(False)
            self.ui.connectButton.setEnabled(True)
        else:
            self.ui.certificateButton.setEnabled(True)
            self.ui.certificateLabel.show()
            self.ui.privateKeyButton.setEnabled(True)
            self.ui.privateKeyLabel.show()
            self.ui.generateButton.setEnabled(True)
            if self.certificate_path and self.private_key_path:
                self.ui.connectButton.setEnabled(True)
            else:
                self.ui.connectButton.setEnabled(False)

    def _select_endpoint(self, policy):
        if policy:
            mode = self.ui.modeComboBox.currentText()
            # Get indices of supported endpoints
            idxlist = self.endpoints_model.match(
                self.endpoints_model.index(0, 3), Qt.DisplayRole,
                "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary",
                -1, Qt.MatchExactly)
            for idx in idxlist:
                if self.endpoints_model.data(idx.siblingAtColumn(
                        1)) == mode and self.endpoints_model.data(
                            idx.siblingAtColumn(2)) == policy and idx.row(
                            ) != self.ui.endpointsView.currentIndex().row():
                    self.ui.endpointsView.setCurrentIndex(idx)

    def select_security(self, selection):
        if isinstance(selection, QItemSelection):
            if not selection.indexes():  # no selection
                return
        idx = self.ui.endpointsView.currentIndex()
        transport_profile_uri = self.endpoints_model.data(
            idx.siblingAtColumn(3))
        if transport_profile_uri != "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary":
            blocker = QSignalBlocker(self.ui.endpointsView.selectionModel())
            self.ui.endpointsView.selectionModel().clearSelection()
        else:
            security_mode = self.endpoints_model.data(idx.siblingAtColumn(1))
            security_policy = self.endpoints_model.data(idx.siblingAtColumn(2))
            self.ui.modeComboBox.setCurrentText(security_mode)
            self.ui.policyComboBox.setCurrentText(security_policy)

    def select_certificate(self):
        path = QFileDialog.getOpenFileName(self, "Select certificate",
                                           self.certificate_path,
                                           "Certificate (*.der)")[0]
        if path:
            self.certificate_path = path
            self.ui.certificateLabel.setText(Path(path).name)
            if self.private_key_path:
                self.ui.connectButton.setEnabled(True)

    def select_private_key(self):
        path = QFileDialog.getOpenFileName(self, "Select private key",
                                           self.private_key_path,
                                           "Private key (*.pem)")[0]
        if path:
            self.private_key_path = path
            self.ui.privateKeyLabel.setText(Path(path).name)
            if self.certificate_path:
                self.ui.connectButton.setEnabled(True)

    def generate_certificate(self):
        private_key_path = QFileDialog.getSaveFileName(
            self, "Save private key file", "my_private_key.pem",
            "Private key (*.pem)")[0]
        if private_key_path:
            private_key_ext = Path(private_key_path).suffix
            if private_key_ext != ".pem":
                private_key_path += ".pem"
            path = Path(private_key_path).parent
            certificate_path = QFileDialog.getSaveFileName(
                self, "Save certificate file",
                str(path.joinpath("my_cert.der")), "Certificate (*.der)")[0]
            if certificate_path:
                certificate_ext = Path(certificate_path).suffix
                if certificate_ext != ".der":
                    certificate_path += ".der"
                return_code = subprocess.call(
                    f"""openssl req -x509 -newkey rsa:2048 \
                    -keyout {private_key_path} -nodes \
                    -outform der -out {certificate_path} \
                    -subj '/C=IT/ST=Catania/O=UniCT' \
                    -addext 'subjectAltName = URI:urn:example.org:OpcUa:python-client'""",
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL)
                if return_code == 0:
                    QMessageBox.information(
                        self, "Success", "Certificate generated successfully")
                    self.certificate_path = certificate_path
                    self.ui.certificateLabel.setText(
                        Path(certificate_path).name)
                    self.private_key_path = private_key_path
                    self.ui.privateKeyLabel.setText(
                        Path(private_key_path).name)
                    self.ui.connectButton.setEnabled(True)
                else:
                    QMessageBox.warning(self, "Error",
                                        "Unable to generate certificate")

    def get_selected_options(self):
        return self.ui.modeComboBox.currentText(
        ), self.ui.policyComboBox.currentText(
        ), self.certificate_path, self.private_key_path
Example #2
0
class MainWindow(QMainWindow):
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    MAIN_UI_FILE = os.path.join(BASE_DIR, "main.ui")
    NEW_DISH_POPUP_UI_FILE = os.path.join(BASE_DIR, "new_dish_popup.ui")
    NEW_DISH_MULTI_POPUP_UI_FILE = os.path.join(BASE_DIR,
                                                "new_dish_multi_popup.ui")
    NEW_DISH_DATA_POPUP_UI_FILE = os.path.join(BASE_DIR,
                                               "new_dish_data_popup.ui")
    MODIFY_DISH_POPUP_UI_FILE = os.path.join(BASE_DIR, "modify_dish_popup.ui")
    DB_FILE = os.path.join(BASE_DIR, "restaurant.db")

    def __init__(self):
        super(MainWindow, self).__init__()
        # Initialize variable
        self.db_connection = None
        self.new_dish_popup = QWidget()
        self.new_dish_multi_popup = QWidget()
        self.new_dish_data_popup = QWidget()
        self.modify_dish_popup = QWidget()
        self.dish_table_model = QStandardItemModel(0, 6)
        self.dish_table_proxy = TableFilter()
        self.dish_data_table_model = QStandardItemModel(0, 6)
        self.dish_data_table_proxy = TableFilter()
        self.graph_chart = None
        self.graph_series = {}

        # Load UI designs
        uic.loadUi(self.MAIN_UI_FILE, self)
        uic.loadUi(self.NEW_DISH_POPUP_UI_FILE, self.new_dish_popup)
        uic.loadUi(self.NEW_DISH_MULTI_POPUP_UI_FILE,
                   self.new_dish_multi_popup)
        uic.loadUi(self.NEW_DISH_DATA_POPUP_UI_FILE, self.new_dish_data_popup)
        uic.loadUi(self.MODIFY_DISH_POPUP_UI_FILE, self.modify_dish_popup)
        self.init_dish_table()
        self.init_dish_data_table()
        self.init_graph()

        # Connect to database
        self.init_db_connection()

        # MainWindow Bind action triggers
        self.action_new_dish.triggered.connect(self.show_new_dish_popup)
        self.action_new_dish_multi.triggered.connect(
            self.show_new_dish_multi_popup)
        self.action_new_data_multi.triggered.connect(
            lambda: self.modify_new_dish_data_popup_table(show=True))
        self.tabWidget.currentChanged.connect(self.update_graph)

        # Dish Table filter bind
        self.dish_lineEdit.textChanged.connect(
            lambda text, col_idx=1: self.dish_table_proxy.set_col_regex_filter(
                col_idx, text))
        self.lower_price_doubleSpinBox.valueChanged.connect(
            lambda value, col_idx=2: self.dish_table_proxy.
            set_col_number_filter(col_idx, value, -1))
        self.higher_price_doubleSpinBox.valueChanged.connect(
            lambda value, col_idx=2: self.dish_table_proxy.
            set_col_number_filter(col_idx, -1, value))
        self.lower_week_sell_spinBox.valueChanged.connect(
            lambda value, col_idx=3: self.dish_table_proxy.
            set_col_number_filter(col_idx, value, -1))
        self.higher_week_sell_spinBox.valueChanged.connect(
            lambda value, col_idx=3: self.dish_table_proxy.
            set_col_number_filter(col_idx, -1, value))

        # Dish Data Table filter bind
        self.lower_data_dateEdit.dateChanged.connect(
            lambda date, col_idx=1: self.dish_data_table_proxy.
            set_col_date_filter(col_idx, date, -1))
        self.higher_data_dateEdit.dateChanged.connect(
            lambda date, col_idx=1: self.dish_data_table_proxy.
            set_col_date_filter(col_idx, -1, date))
        self.data_lineEdit.textChanged.connect(
            lambda text, col_idx=2: self.dish_data_table_proxy.
            set_col_regex_filter(col_idx, text))
        self.lower_data_doubleSpinBox.valueChanged.connect(
            lambda value, col_idx=3: self.dish_data_table_proxy.
            set_col_number_filter(col_idx, value, -1))
        self.higher_data_doubleSpinBox.valueChanged.connect(
            lambda value, col_idx=3: self.dish_data_table_proxy.
            set_col_number_filter(col_idx, -1, value))
        self.lower_data_spinBox.valueChanged.connect(
            lambda value, col_idx=4: self.dish_data_table_proxy.
            set_col_number_filter(col_idx, value, -1))
        self.higher_data_spinBox.valueChanged.connect(
            lambda value, col_idx=4: self.dish_data_table_proxy.
            set_col_number_filter(col_idx, -1, value))
        self.data_all_check_checkBox.stateChanged.connect(
            lambda state, col_idx=5: self.data_table_check_state(
                state, col_idx))
        self.dish_data_table_model.itemChanged.connect(self.update_series)

        # Popup bind action triggers
        self.new_dish_popup.create_new_dish_btn.clicked.connect(
            self.create_new_dish)
        self.new_dish_multi_popup.pushButton_ok.clicked.connect(
            self.create_new_dish_multi)
        self.new_dish_data_popup.dateEdit.dateChanged.connect(
            self.modify_new_dish_data_popup_table)
        self.new_dish_data_popup.pushButton_ok.clicked.connect(
            self.create_new_dish_data)

        # Get current dishes
        self.load_dish_table()
        self.load_dish_data_table()
        self.new_dish_data_popup.dateEdit.setDate(QtCore.QDate.currentDate())

    def init_dish_table(self):
        self.dish_tableView.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        # Set Header data and stretch
        for col, col_name in enumerate(
            ["ID", "菜品", "价格", "近7天总售出", "操作", "备注"]):
            self.dish_table_model.setHeaderData(col, Qt.Horizontal, col_name,
                                                Qt.DisplayRole)
        self.dish_table_proxy.setSourceModel(self.dish_table_model)
        self.dish_tableView.setModel(self.dish_table_proxy)
        self.dish_tableView.setColumnHidden(0, True)
        for (col, method) in [(1, "Regex"), (2, "Number"), (3, "Number"),
                              (5, "Regex")]:
            self.dish_table_proxy.filter_method[col] = method

    def init_dish_data_table(self):
        self.data_tableView.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        for col, col_name in enumerate(
            ["Dish_ID", "日期", "菜品", "价格", "售出", "选择"]):
            self.dish_data_table_model.setHeaderData(col, Qt.Horizontal,
                                                     col_name, Qt.DisplayRole)
        self.dish_data_table_proxy.setSourceModel(self.dish_data_table_model)
        self.data_tableView.setModel(self.dish_data_table_proxy)
        self.data_tableView.setColumnHidden(0, True)
        for (col, method) in [(1, "Date"), (2, "Regex"), (3, "Number"),
                              (4, "Number")]:
            self.dish_data_table_proxy.filter_method[col] = method

    def init_graph(self):
        self.graph_chart = QChart(title="售出图")
        self.graph_chart.legend().setVisible(True)
        self.graph_chart.setAcceptHoverEvents(True)

        graph_view = QChartView(self.graph_chart)
        graph_view.setRenderHint(QPainter.Antialiasing)
        self.gridLayout_5.addWidget(graph_view)

    def init_db_connection(self):
        self.db_connection = sqlite3.connect(self.DB_FILE)
        cursor = self.db_connection.cursor()
        # check create table if not exist
        sql_create_dish_table = """ CREATE TABLE IF NOT EXISTS dish (
                                        id integer PRIMARY KEY,
                                        name text NOT NULL,
                                        price numeric Not NULL,
                                        remarks text,
                                        UNIQUE (name, price)
                                    ); """
        sql_create_dish_data_table = """ CREATE TABLE IF NOT EXISTS dish_data (
                                            dish_id integer NOT NULL REFERENCES dish(id) ON DELETE CASCADE,
                                            date date,
                                            sell_num integer DEFAULT 0,
                                            PRIMARY KEY (dish_id, date),
                                            CONSTRAINT dish_fk 
                                                FOREIGN KEY (dish_id) 
                                                REFERENCES dish (id) ON DELETE CASCADE 
                                        ); """
        sql_trigger = """
            CREATE TRIGGER IF NOT EXISTS place_holder_data
            AFTER INSERT ON dish
            BEGIN
                INSERT INTO dish_data (dish_id, date, sell_num) VALUES(new.id, null, 0);
            END;
        """
        cursor.execute(sql_create_dish_table)
        cursor.execute(sql_create_dish_data_table)
        cursor.execute("PRAGMA FOREIGN_KEYS = on")
        cursor.execute(sql_trigger)

        cursor.close()

    def load_dish_table(self):
        today = datetime.today()
        sql_select_query = """
            SELECT dish.id, dish.name, dish.price, COALESCE(SUM(dish_data.sell_num), 0), dish.remarks
            FROM dish LEFT JOIN dish_data 
            ON dish.id = dish_data.dish_id
            WHERE dish_data.date IS NULL OR dish_data.date BETWEEN date('{}') and date('{}')
            GROUP BY dish.id
            ORDER BY dish.name, dish.price;""".format(
            (today - timedelta(days=7)).strftime("%Y-%m-%d"),
            today.strftime("%Y-%m-%d"))
        cursor = self.db_connection.cursor()
        cursor.execute(sql_select_query)
        records = cursor.fetchall()
        for row_idx, record in enumerate(records):
            self.dish_table_model.appendRow(create_dish_table_row(*record))
        cursor.close()
        self.dish_tableView.setItemDelegateForColumn(
            4,
            DishTableDelegateCell(self.show_modify_dish_popup,
                                  self.delete_dish, self.dish_tableView))

    def load_dish_data_table(self):
        sql_select_query = """
            SELECT dish_data.dish_id, dish_data.date, dish.name, dish.price, dish_data.sell_num
            FROM dish_data LEFT JOIN dish  
            ON dish_data.dish_id = dish.id
            WHERE dish_data.date IS NOT NULL
            ORDER BY dish_data.date DESC, dish.name, dish.price, dish_data.sell_num;"""
        cursor = self.db_connection.cursor()
        cursor.execute(sql_select_query)
        records = cursor.fetchall()
        for row_idx, record in enumerate(records):
            self.dish_data_table_model.appendRow(
                create_dish_data_table_row(*record))
        cursor.close()
        self.lower_data_dateEdit.setDate(QDate.currentDate().addDays(-7))
        self.higher_data_dateEdit.setDate(QDate.currentDate())
        self.data_tableView.setItemDelegateForColumn(
            5, DishDataTableDelegateCell(self.data_tableView))

    def data_table_check_state(self, state, col):
        for row in range(self.dish_data_table_proxy.rowCount()):
            index = self.dish_data_table_proxy.mapToSource(
                self.dish_data_table_proxy.index(row, col))
            if index.isValid():
                self.dish_data_table_model.setData(index, str(state),
                                                   Qt.DisplayRole)

    def show_new_dish_popup(self):
        # Move popup to center
        point = self.rect().center()
        global_point = self.mapToGlobal(point)
        self.new_dish_popup.move(
            global_point - QtCore.QPoint(self.new_dish_popup.width() // 2,
                                         self.new_dish_popup.height() // 2))
        self.new_dish_popup.show()

    def show_new_dish_multi_popup(self):
        file_name = QFileDialog().getOpenFileName(None, "选择文件", "",
                                                  self.tr("CSV文件 (*.csv)"))[0]
        self.new_dish_multi_popup.tableWidget.setRowCount(0)
        if file_name:
            with open(file_name, "r") as file:
                csv_reader = csv.reader(file, delimiter=",")
                for idx, row_data in enumerate(csv_reader):
                    if len(row_data) == 2:
                        name, price = row_data
                        remark = ""
                    elif len(row_data) == 3:
                        name, price, remark = row_data
                    else:
                        QMessageBox.warning(
                            self, "格式错误",
                            self.tr('格式为"菜品 价格"或者"菜品 价格 备注"\n第{}行输入有误'.format(
                                idx)))
                        return
                    self.new_dish_multi_popup.tableWidget.insertRow(
                        self.new_dish_multi_popup.tableWidget.rowCount())
                    self.new_dish_multi_popup.tableWidget.setItem(
                        idx, 0, QTableWidgetItem(name))
                    price_type = str_type(price)
                    if price_type == str or (isinstance(
                            price_type, (float, int)) and float(price) < 0):
                        QMessageBox.warning(
                            self, "格式错误",
                            self.tr('第{}行价格输入有误'.format(idx + 1)))
                        return
                    self.new_dish_multi_popup.tableWidget.setItem(
                        idx, 1,
                        QTableWidgetItem("{:.2f}".format(float(price))))
                    self.new_dish_multi_popup.tableWidget.setItem(
                        idx, 2, QTableWidgetItem(remark))
            self.new_dish_multi_popup.show()

    def modify_new_dish_data_popup_table(self, *args, show=False):
        sql_select_query = """
                    SELECT id, name, price, dish_data.sell_num
                    FROM dish LEFT JOIN dish_data
                    ON dish.id=dish_data.dish_id
                    WHERE dish_data.date IS NULL OR dish_data.date = date('{}')
                    GROUP BY id, name, price
                    ORDER BY dish.name, dish.price;""".format(
            self.new_dish_data_popup.dateEdit.date().toString("yyyy-MM-dd"))

        cursor = self.db_connection.cursor()
        cursor.execute(sql_select_query)
        records = cursor.fetchall()
        self.new_dish_data_popup.tableWidget.setRowCount(len(records))
        self.new_dish_data_popup.tableWidget.setColumnHidden(0, True)
        for row_idx, record in enumerate(records):
            dish_id, name, price, sell_num = record
            self.new_dish_data_popup.tableWidget.setItem(
                row_idx, 0, QTableWidgetItem(str(dish_id)))
            self.new_dish_data_popup.tableWidget.setItem(
                row_idx, 1, QTableWidgetItem(name))
            self.new_dish_data_popup.tableWidget.setItem(
                row_idx, 2, QTableWidgetItem("{:.2f}".format(price)))
            spin_box = QSpinBox()
            spin_box.setMaximum(9999)
            spin_box.setValue(sell_num)
            self.new_dish_data_popup.tableWidget.setCellWidget(
                row_idx, 3, spin_box)
        cursor.close()
        if show:
            self.new_dish_data_popup.show()

    def create_new_dish(self):
        cursor = self.db_connection.cursor()
        sql_insert = """ INSERT INTO dish(name, price, remarks)
                         VALUES(?,?,?)"""
        dish_name = self.new_dish_popup.dish_name.text()
        dish_price = self.new_dish_popup.dish_price.value()
        dish_remark = self.new_dish_popup.dish_remark.toPlainText()
        try:
            cursor.execute(sql_insert, (dish_name, dish_price, dish_remark))
            new_dish_id = cursor.lastrowid
            cursor.close()
            self.db_connection.commit()
            # Update dish table and dish comboBox in UI
            self.dish_table_model.appendRow(
                create_dish_table_row(new_dish_id, dish_name, dish_price, 0,
                                      dish_remark))
            self.new_dish_popup.hide()
        except sqlite3.Error:
            cursor.close()
            QMessageBox.warning(self, "菜品价格重复", self.tr('菜品价格组合重复,请检查'))

    def create_new_dish_multi(self):
        cursor = self.db_connection.cursor()
        sql_insert = """ 
            INSERT INTO dish(name, price, remarks)     
            VALUES (?, ?, ?)"""
        for row in range(self.new_dish_multi_popup.tableWidget.rowCount()):
            dish_name = self.new_dish_multi_popup.tableWidget.item(row,
                                                                   0).text()
            dish_price = float(
                self.new_dish_multi_popup.tableWidget.item(row, 1).text())
            dish_remark = self.new_dish_multi_popup.tableWidget.item(row,
                                                                     2).text()
            try:
                cursor.execute(sql_insert,
                               (dish_name, dish_price, dish_remark))
                new_dish_id = cursor.lastrowid
                self.dish_table_model.appendRow(
                    create_dish_table_row(new_dish_id, dish_name, dish_price,
                                          0, dish_remark))
            except sqlite3.Error:
                cursor.close()
                QMessageBox.warning(
                    self, "菜品价格重复",
                    self.tr('前{}行已插入。\n第{}行菜品价格组合重复,请检查'.format(row, row + 1)))
                return
        cursor.close()
        self.db_connection.commit()
        self.new_dish_multi_popup.hide()

    def create_new_dish_data(self):
        current_date = self.new_dish_data_popup.dateEdit.date().toString(
            "yyyy-MM-dd")
        table_filter = TableFilter()
        table_filter.setSourceModel(self.dish_data_table_model)
        table_filter.set_col_regex_filter(1, current_date)
        for row in range(table_filter.rowCount()):
            index = table_filter.mapToSource(table_filter.index(0, 1))
            if index.isValid():
                self.dish_data_table_model.removeRow(index.row())
        del table_filter
        cursor = self.db_connection.cursor()
        sql_insert = """ 
            INSERT OR REPLACE INTO dish_data(dish_id, date, sell_num)     
            VALUES (?, ?, ?)"""
        for row in range(self.new_dish_data_popup.tableWidget.rowCount()):
            dish_id = int(
                self.new_dish_data_popup.tableWidget.item(row, 0).text())
            name = self.new_dish_data_popup.tableWidget.item(row, 1).text()
            price = float(
                self.new_dish_data_popup.tableWidget.item(row, 2).text())
            sell_num = self.new_dish_data_popup.tableWidget.cellWidget(
                row, 3).value()
            cursor.execute(sql_insert, (dish_id, current_date, sell_num))
            self.dish_data_table_model.appendRow(
                create_dish_data_table_row(dish_id, current_date, name, price,
                                           sell_num))
        cursor.close()
        self.db_connection.commit()
        self.new_dish_data_popup.hide()

    def delete_dish(self, dish_id):
        cursor = self.db_connection.cursor()
        sql_delete = """ DELETE FROM dish WHERE id=?"""
        cursor.execute(sql_delete, tuple([dish_id]))
        cursor.close()
        self.db_connection.commit()

        # Update dish table and dish comboBox in UI
        for row in self.dish_data_table_model.findItems(str(dish_id)):
            index = row.index()
            if index.isValid():
                self.dish_data_table_model.removeRow(index.row())

        for row in self.dish_table_model.findItems(str(dish_id)):
            index = row.index()
            if index.isValid():
                self.dish_table_model.removeRow(index.row())

    def show_modify_dish_popup(self, dish_id):
        point = self.rect().center()
        global_point = self.mapToGlobal(point)
        self.modify_dish_popup.move(
            global_point - QtCore.QPoint(self.modify_dish_popup.width() // 2,
                                         self.modify_dish_popup.height() // 2))
        # Find the row and get necessary info
        index = self.dish_table_model.match(self.dish_table_model.index(0, 0),
                                            Qt.DisplayRole, str(dish_id))
        if index:
            row_idx = index[0]
            dish_name = self.dish_table_model.data(row_idx.siblingAtColumn(1))
            dish_price = self.dish_table_model.data(row_idx.siblingAtColumn(2))
            dish_remark = self.dish_table_model.data(
                row_idx.siblingAtColumn(5))
            self.modify_dish_popup.dish_name.setText(dish_name)
            self.modify_dish_popup.dish_price.setValue(float(dish_price))
            self.modify_dish_popup.dish_remark.setText(dish_remark)

            try:
                self.modify_dish_popup.modify_dish_btn.clicked.disconnect()
            except TypeError:
                pass
            self.modify_dish_popup.modify_dish_btn.clicked.connect(
                lambda: self.modify_dish(row_idx, dish_id))
            self.modify_dish_popup.show()

    def modify_dish(self, row, dish_id):
        cursor = self.db_connection.cursor()
        sql_update = """ UPDATE dish
                         SET name = ?, price = ?, remarks = ?
                         WHERE id=?"""
        dish_name = self.modify_dish_popup.dish_name.text()
        dish_price = self.modify_dish_popup.dish_price.value()
        dish_remark = self.modify_dish_popup.dish_remark.toPlainText()
        cursor.execute(sql_update,
                       (dish_name, dish_price, dish_remark, dish_id))
        cursor.close()
        self.db_connection.commit()
        self.modify_dish_popup.hide()

        # Update dish table and dish comboBox in UI
        old_name = self.dish_table_model.data(row.siblingAtColumn(1))
        old_price = self.dish_table_model.data(row.siblingAtColumn(2))
        sell_num = self.dish_table_model.data(row.siblingAtColumn(3))
        row_idx = row.row()
        self.dish_table_model.removeRow(row_idx)
        self.dish_table_model.insertRow(
            row_idx,
            create_dish_table_row(dish_id, dish_name, dish_price, sell_num,
                                  dish_remark))

        for row in self.dish_data_table_model.findItems(str(dish_id)):
            index = row.index()
            if index.isValid():
                self.dish_data_table_model.setData(index.siblingAtColumn(2),
                                                   dish_name)
                self.dish_data_table_model.setData(index.siblingAtColumn(3),
                                                   "{:.2f}".format(dish_price))
        old_key = old_name + '(' + old_price + ')'
        if old_key in self.graph_line_series:
            self.graph_line_series[dish_name + '(' + str(dish_price) +
                                   ')'] = self.graph_line_series[old_key]
            del self.graph_line_series[old_key]

    def update_series(self, item: QStandardItem):
        if item.column() == 5:  # check for checkbox column
            item_idx = item.index()
            date = self.dish_data_table_model.data(item_idx.siblingAtColumn(1))
            dish_name = self.dish_data_table_model.data(
                item_idx.siblingAtColumn(2))
            dish_price = self.dish_data_table_model.data(
                item_idx.siblingAtColumn(3))
            sell_num = self.dish_data_table_model.data(
                item_idx.siblingAtColumn(4))
            set_name = dish_name + "(" + dish_price + ")"
            key = str(
                QDateTime(QDate.fromString(date,
                                           "yyyy-MM-dd")).toSecsSinceEpoch())
            if key not in self.graph_series:
                self.graph_series[key] = {}

            if int(item.text()) == 0:
                if set_name in self.graph_series[key]:
                    del self.graph_series[key][set_name]
                if not self.graph_series[key]:
                    del self.graph_series[key]
            else:
                self.graph_series[key][set_name] = int(sell_num)

    def update_graph(self, index):
        if index == 2:
            self.graph_chart.removeAllSeries()

            axis_x = QBarCategoryAxis()
            axis_x.setTitleText("日期")
            if self.graph_chart.axisX():
                self.graph_chart.removeAxis(self.graph_chart.axisX())
            self.graph_chart.addAxis(axis_x, Qt.AlignBottom)

            axis_y = QValueAxis()
            axis_y.setLabelFormat("%i")
            axis_y.setTitleText("售出量")
            if self.graph_chart.axisY():
                self.graph_chart.removeAxis(self.graph_chart.axisY())
            self.graph_chart.addAxis(axis_y, Qt.AlignLeft)

            max_num = 0
            total_date = 0
            set_dict = {}
            for key, data in sorted(self.graph_series.items(),
                                    key=lambda i: int(i[0])):
                axis_x.append(
                    QDateTime.fromSecsSinceEpoch(
                        int(key)).toString("yyyy年MM月dd日"))
                for set_name, value in data.items():
                    if set_name not in set_dict:
                        set_dict[set_name] = QBarSet(set_name)
                        for _ in range(total_date):
                            set_dict[set_name].append(0)
                    set_dict[set_name].append(value)
                    max_num = max(max_num, value)
                total_date += 1
                for _, bar_set in set_dict.items():
                    if bar_set.count() < total_date:
                        bar_set.append(0)
            bar_series = QBarSeries()
            for _, bar_set in set_dict.items():
                bar_series.append(bar_set)
            bar_series.hovered.connect(self.graph_tooltip)
            axis_y.setMax(max_num + 1)
            axis_y.setMin(0)
            self.graph_chart.addSeries(bar_series)
            bar_series.attachAxis(axis_x)
            bar_series.attachAxis(axis_y)

    def graph_tooltip(self, status, index, bar_set: QBarSet):
        if status:
            QToolTip.showText(
                QCursor.pos(),
                "{}\n日期: {}\n售出: {}".format(bar_set.label(),
                                            self.graph_chart.axisX().at(index),
                                            int(bar_set.at(index))))
Example #3
0
class CommentsTable(QTableView):
    """
    The comment table below the video.
    """

    state_changed = pyqtSignal(bool)

    def __init__(self, main_handler: MainHandler):
        super().__init__()
        self.__widget_mpv = main_handler.widget_mpv
        self.__mpv_player = self.__widget_mpv.player

        palette = self.palette()
        palette.setColor(QPalette.Inactive, QPalette.Highlight,
                         palette.color(QPalette.Active, QPalette.Highlight))
        palette.setColor(QPalette.Inactive, QPalette.HighlightedText,
                         palette.color(QPalette.Active, QPalette.HighlightedText))
        self.setPalette(palette)

        # Model
        self.__model = QStandardItemModel(self)
        self.setModel(self.__model)
        self.selectionModel().selectionChanged.connect(lambda sel, __: self.__on_row_selection_changed())

        # Headers
        self.horizontalHeader().setStretchLastSection(True)
        self.horizontalHeader().hide()
        self.verticalHeader().hide()

        # Delegates
        delegate_time = CommentTimeDelegate(self)
        delegate_coty = CommentTypeDelegate(self)
        delegate_note = CommentNoteDelegate(self)

        delegate_time.editing_done.connect(self.__on_after_user_changed_time)
        delegate_coty.editing_done.connect(self.__on_after_user_changed_comment_type)
        delegate_note.editing_done.connect(self.__on_after_user_changed_comment_note)

        self.setItemDelegateForColumn(0, delegate_time)
        self.setItemDelegateForColumn(1, delegate_coty)
        self.setItemDelegateForColumn(2, delegate_note)

        # Misc
        self.setEditTriggers(QAbstractItemView.DoubleClicked)
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.__selection_flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows

        self.setAlternatingRowColors(True)
        self.setSortingEnabled(True)
        self.setWordWrap(False)
        self.setShowGrid(False)

    def delete_current_selected_comment(self) -> None:
        """
        Will delete the current selected comment row.
        If selection is empty, no action will be invoked.
        """

        def delete(selected: List[QModelIndex]):
            self.__model.removeRows(selected[0].row(), 1)
            self.state_changed.emit(False)
            EventDistributor.send_event(EventCommentAmountChanged(self.__model.rowCount()))

        self.__do_with_selected_comment_row(delete)

    def edit_current_selected_comment(self) -> None:
        """
        Will start edit mode on selected comment row.
        If selection is empty, no action will be invoked.
        """

        def edit(selected: List[QModelIndex]):
            row = selected[0].row()
            idx = self.__model.item(row, 2).index()
            self.edit(idx)
            self.state_changed.emit(False)

        self.__do_with_selected_comment_row(edit)

    def copy_current_selected_comment(self) -> None:
        """
        Will copy the complete row to clipboard.
        If selection is empty, no action will be invoked.
        """

        def copy(selected: List[QModelIndex]):
            row = selected[0].row()
            time = self.__model.item(row, 0).text()
            coty = self.__model.item(row, 1).text()
            note = self.__model.item(row, 2).text()
            QApplication.clipboard().setText("[{}] [{}] {}".format(time, coty, note))

        self.__do_with_selected_comment_row(copy)

    def add_comments(self, comments: Tuple[Comment], changes_qc=False, edit=False, resize_ct_column=True):
        if not comments:
            return

        model = self.__model
        last_entry = None

        # Always resize on the first comment imported
        resize_ct_column = resize_ct_column or not model.hasChildren()

        for comment in comments:
            time = QStandardItem(comment.comment_time)
            time.setTextAlignment(Qt.AlignCenter)
            ct = QStandardItem(_translate("CommentTypes", comment.comment_type))
            note = QStandardItem(comment.comment_note)
            last_entry = [time, ct, note]
            model.appendRow(last_entry)

        if resize_ct_column:
            self.resize_column_type_column()

        self.sort()

        EventDistributor.send_event(EventCommentAmountChanged(model.rowCount()))
        self.__on_row_selection_changed()

        if changes_qc:
            self.state_changed.emit(False)

        if edit:
            new_index = model.indexFromItem(last_entry[2])
            self.scrollTo(new_index)
            self.setCurrentIndex(new_index)
            self.edit(new_index)
        else:
            self.ensure_selection()

    def add_comment(self, comment_type: str) -> None:
        comment = Comment(
            comment_time=self.__widget_mpv.player.position_current(),
            comment_type=comment_type,
            comment_note=""
        )
        self.add_comments((comment,), changes_qc=True, edit=True, resize_ct_column=False)

    def get_all_comments(self) -> Tuple[Comment]:
        """
        Returns all comments.

        :return: all comments.
        """

        ret_list = []
        model = self.__model

        for r in range(0, model.rowCount()):
            time = model.item(r, 0).text()
            coty = model.item(r, 1).text()
            note = model.item(r, 2).text()
            ret_list.append(Comment(comment_time=time, comment_type=coty, comment_note=note))
        return tuple(ret_list)

    def reset_comments_table(self) -> None:
        """
        Will clear all comments.
        """

        self.__model.clear()
        self.state_changed.emit(True)
        EventDistributor.send_event(EventCommentAmountChanged(self.__model.rowCount()))
        EventDistributor.send_event(EventCommentCurrentSelectionChanged(-1))

    def sort(self) -> None:
        """
        Will sort the comments table by time column.
        """

        # Sorting is only triggered if the sorting policy changes
        self.setSortingEnabled(False)
        self.setSortingEnabled(True)
        self.sortByColumn(0, Qt.AscendingOrder)

    def __do_with_selected_comment_row(self, consume_selected_function) -> None:
        """
        This function takes a **function** as argument.

        *It will call the function with the current selection as argument if selection is not empty.*

        :param consume_selected_function: The function to apply if selection is not empty
        """

        is_empty: bool = self.__model.rowCount() == 0

        if not is_empty:
            selected = self.selectionModel().selectedRows()

            if selected:
                consume_selected_function(selected)

    def __on_after_user_changed_time(self) -> None:
        """
        Action to invoke after time was changed manually by the user.
        """

        self.sort()
        self.state_changed.emit(False)

    def __on_after_user_changed_comment_type(self) -> None:
        """
        Action to invoke after comment type was changed manually by the user.
        """

        self.state_changed.emit(False)

    # noinspection PyMethodMayBeStatic
    def __on_after_user_changed_comment_note(self) -> None:
        """
        Action to invoke after comment note was changed manually by the user.
        """

        self.state_changed.emit(False)

    # noinspection PyMethodMayBeStatic
    def __on_row_selection_changed(self) -> None:

        def after_model_updated():
            current_index = self.selectionModel().currentIndex()
            if current_index.isValid():
                new_row = current_index.row()
            else:
                new_row = -1
            EventDistributor.send_event(EventCommentCurrentSelectionChanged(new_row), EventReceiver.WIDGET_STATUS_BAR)

        QTimer.singleShot(0, after_model_updated)

    def keyPressEvent(self, e: QKeyEvent):
        mod = e.modifiers()
        key = e.key()

        # Only key up and key down are handled here because they require to call super
        if (key == Qt.Key_Up or key == Qt.Key_Down) and mod == Qt.NoModifier:
            super().keyPressEvent(e)
        else:
            self.__widget_mpv.keyPressEvent(e)

    def mousePressEvent(self, e: QMouseEvent):

        if e.button() == Qt.LeftButton:
            mdi: QModelIndex = self.indexAt(e.pos())
            if mdi.column() == 0 and self.__mpv_player.has_video():
                position = self.__model.item(mdi.row(), 0).text()
                self.__widget_mpv.player.position_jump(position=position)
                e.accept()
            elif mdi.column() == 1 and mdi == self.selectionModel().currentIndex():
                self.edit(mdi)
                e.accept()
        super().mousePressEvent(e)

    def wheelEvent(self, e: QWheelEvent):
        delta = e.angleDelta()

        x_d = delta.x()
        y_d = delta.y()

        if x_d == 0 and y_d != 0:
            position = self.verticalScrollBar().value()
            if y_d > 0:
                self.verticalScrollBar().setValue(position - 1)
            else:
                self.verticalScrollBar().setValue(position + 1)
        else:
            super().wheelEvent(e)

    def ensure_selection(self) -> None:
        """
        If no row is highlighted the first row will be highlighted.
        """

        self.setFocus()
        if self.__model.rowCount() != 0:
            if not self.selectionModel().currentIndex().isValid():
                self.__highlight_row(self.model().index(0, 2))

    def perform_search(self, query: str, top_down: bool, new_query: bool, last_index: QModelIndex) -> SearchResult:
        """
        Will perform the search for the given query and return a SearchResult.

        :param last_index: The index of the latest search result or any invalid index.
        :param query: search string ignore case (Qt.MatchContains)
        :param top_down: If True the next, if False the previous occurrence will be returned
        :param new_query: If True the search will be handled as a new one.
        :return:
        """

        current_index = self.selectionModel().currentIndex()

        if new_query:
            start_row = 0
        elif last_index and last_index.isValid():
            start_row = last_index.row()
        elif current_index and current_index.isValid():
            start_row = current_index.row()
        else:
            start_row = 0

        if query == "":
            return self.__generate_search_result(query)

        start = self.__model.index(start_row, 2)
        match: List[QModelIndex] = self.__model.match(start, Qt.DisplayRole, query, -1, Qt.MatchContains | Qt.MatchWrap)

        if not match:
            return self.__generate_search_result(query)

        return self.__provide_search_result(query, match, top_down, new_query)

    def __provide_search_result(self, query: str, match: List[QModelIndex], top_down: bool,
                                new_query: bool) -> SearchResult:

        if top_down and len(match) > 1:
            if new_query or self.selectionModel().currentIndex() not in match:
                model_index = match[0]
            else:
                model_index = match[1]
        else:
            model_index = match[-1]
        current_hit = sorted(match, key=lambda k: k.row()).index(model_index)
        return self.__generate_search_result(query, model_index, current_hit + 1, len(match))

    def __generate_search_result(self, query, model_index=None, current_hit=0, total_hits=0) -> SearchResult:
        result = SearchResult(query, model_index, current_hit, total_hits)
        result.highlight.connect(lambda index: self.__highlight_row(index))
        return result

    def __highlight_row(self, model_index: QModelIndex):
        if model_index:
            self.selectionModel().setCurrentIndex(model_index, self.__selection_flags)
            self.selectionModel().select(model_index, self.__selection_flags)
            self.scrollTo(model_index, QAbstractItemView.PositionAtCenter)

    def resize_column_type_column(self):
        self.resizeColumnToContents(1)
Example #4
0
class PinStatusWidget(GalacteekTab):
    COL_TS = 0
    COL_QUEUE = 1
    COL_PATH = 2
    COL_STATUS = 3
    COL_PROGRESS = 4
    COL_CTRL = 5

    def __init__(self, gWindow, **kw):
        super(PinStatusWidget, self).__init__(gWindow, **kw)

        self.tree = QTreeView()
        self.tree.setObjectName('pinStatusWidget')
        self.boxLayout = QVBoxLayout()
        self.boxLayout.addWidget(self.tree)

        self.ctrlLayout = QHBoxLayout()
        self.btnPin = QPushButton(iPin())
        self.pathLabel = QLabel(iCidOrPath())
        self.pathEdit = QLineEdit()
        self.ctrlLayout.addWidget(self.pathLabel)
        self.ctrlLayout.addWidget(self.pathEdit)
        self.ctrlLayout.addWidget(self.btnPin)
        self.vLayout.addLayout(self.ctrlLayout)
        self.vLayout.addLayout(self.boxLayout)

        self.app.ipfsCtx.pinItemStatusChanged.connect(self.onPinStatusChanged)
        self.app.ipfsCtx.pinFinished.connect(self.onPinFinished)
        self.app.ipfsCtx.pinItemRemoved.connect(self.onItemRemoved)
        self.pathEdit.returnPressed.connect(self.onPathEntered)
        self.btnPin.clicked.connect(self.onPathEntered)

        self.model = QStandardItemModel()
        self.model.setHorizontalHeaderLabels(
            ['TS', iQueue(),
             iPath(),
             iStatus(),
             iNodesProcessed(), ''])

        self.tree.setSortingEnabled(True)
        self.tree.setModel(self.model)
        self.tree.sortByColumn(self.COL_TS, Qt.DescendingOrder)

        for col in [self.COL_QUEUE, self.COL_PATH, self.COL_PROGRESS]:
            self.tree.header().setSectionResizeMode(
                col, QHeaderView.ResizeToContents)

        self.tree.hideColumn(self.COL_TS)

    def resort(self):
        self.model.sort(self.COL_TS, Qt.DescendingOrder)

    def onPathEntered(self):
        text = self.pathEdit.text()
        self.pathEdit.clear()

        path = IPFSPath(text)
        if path.valid:
            ensure(self.app.ipfsCtx.pinner.queue(path.objPath, True, None))
        else:
            messageBox(iInvalidInput())

    def removeItem(self, path):
        modelSearch(self.model,
                    search=path,
                    columns=[self.COL_PATH],
                    delete=True)

    def onItemRemoved(self, qname, path):
        self.removeItem(path)

    def getIndexFromPath(self, path):
        idxList = self.model.match(self.model.index(0, self.COL_PATH),
                                   PinObjectPathRole, path, 1,
                                   Qt.MatchFixedString | Qt.MatchWrap)
        if len(idxList) > 0:
            return idxList.pop()

    def findPinItems(self, path):
        idx = self.getIndexFromPath(path)

        if not idx:
            return None

        itemP = self.model.itemFromIndex(idx)

        if not itemP:
            return None

        idxQueue = self.model.index(itemP.row(), self.COL_QUEUE,
                                    itemP.index().parent())
        idxProgress = self.model.index(itemP.row(), self.COL_PROGRESS,
                                       itemP.index().parent())
        idxStatus = self.model.index(itemP.row(), self.COL_STATUS,
                                     itemP.index().parent())
        idxC = self.model.index(itemP.row(), self.COL_CTRL,
                                itemP.index().parent())
        cancelButton = self.tree.indexWidget(idxC)

        return {
            'itemPath': itemP,
            'itemQname': self.model.itemFromIndex(idxQueue),
            'itemProgress': self.model.itemFromIndex(idxProgress),
            'itemStatus': self.model.itemFromIndex(idxStatus),
            'cancelButton': cancelButton
        }

    def updatePinStatus(self, path, status, progress):
        idx = self.getIndexFromPath(path)
        if not idx:
            return

        try:
            itemPath = self.model.itemFromIndex(idx)
            if itemPath and time.time() - itemPath.lastProgressUpdate < 5:
                return

            itemProgress = self.model.itemFromIndex(
                self.model.index(idx.row(), self.COL_PROGRESS, idx.parent()))

            itemStatus = self.model.itemFromIndex(
                self.model.index(idx.row(), self.COL_STATUS, idx.parent()))

            itemStatus.setText(status)
            itemProgress.setText(progress)
            itemPath.lastProgressUpdate = time.time()
        except:
            pass

    def onPinFinished(self, path):
        items = self.findPinItems(path)

        if items:
            items['itemStatus'].setText(iPinned())
            items['itemProgress'].setText('OK')

            color1 = QBrush(QColor('#4a9ea1'))
            color2 = QBrush(QColor('#66a56e'))

            for item in [
                    items['itemQname'], items['itemPath'], items['itemStatus'],
                    items['itemProgress']
            ]:
                item.setBackground(color1)

            for item in [items['itemStatus'], items['itemProgress']]:
                item.setBackground(color2)

            if items['cancelButton']:
                items['cancelButton'].setEnabled(False)

        self.resort()
        self.purgeFinishedItems()

    def purgeFinishedItems(self):
        maxFinished = 16
        ret = modelSearch(self.model,
                          search=iPinned(),
                          columns=[self.COL_STATUS])

        if len(ret) > maxFinished:
            rows = []
            for idx in ret:
                item = self.model.itemFromIndex(idx)
                if not item:
                    continue
                rows.append(item.row())

            try:
                for row in list(sorted(rows))[int(maxFinished / 2):]:
                    self.model.removeRow(row)
            except:
                pass

    async def onCancel(self, qname, path, *a):
        self.removeItem(path)
        await self.app.ipfsCtx.pinner.cancel(qname, path)

    def onPinStatusChanged(self, qname, path, statusInfo):
        nodesProcessed = statusInfo['status'].get('Progress', iUnknown())

        idx = self.getIndexFromPath(path)

        if not idx:
            # Register it
            btnCancel = QToolButton()
            btnCancel.setIcon(getIcon('cancel.png'))
            btnCancel.setText(iCancel())
            btnCancel.clicked.connect(partialEnsure(self.onCancel, qname,
                                                    path))
            btnCancel.setFixedWidth(140)

            displayPath = path
            if len(displayPath) > 64:
                displayPath = displayPath[0:64] + ' ..'

            itemTs = UneditableItem(str(statusInfo['ts_queued']))
            itemQ = UneditableItem(qname)
            itemP = UneditableItem(displayPath)
            itemP.setData(path, PinObjectPathRole)
            itemP.setToolTip(path)
            itemP.lastProgressUpdate = time.time()

            itemStatus = UneditableItem(iPinning())
            itemProgress = UneditableItem(str(nodesProcessed))

            itemC = UneditableItem('')

            self.model.invisibleRootItem().appendRow(
                [itemTs, itemQ, itemP, itemStatus, itemProgress, itemC])
            idx = self.model.indexFromItem(itemC)
            self.tree.setIndexWidget(idx, btnCancel)
            self.resort()
        else:
            self.updatePinStatus(path, iPinning(), str(nodesProcessed))