# SPDX-License-Identifier: WTFPL """ Spezial: Notebook mit Vessel, User, Lists, Settings """ import os import pyudev from gi.repository import GLib, Gtk, Gdk, Rsvg from datetime import datetime import subprocess import locale from .view import View from yms import gtkutils, combobox, treeview from yms.lookup import Dropdown from yms.model import user class BaseView(View): def __init__(self, context): super().__init__(context, _("Base Data"), "base.ui") self.context = context self.user = user.User(context.db) # current user record self.newuser = False self.backupdevice = None # USB-Stick detected only on demand # Settings self.sys_currency = context.db.load_setting(4)[0] or '€' self.entry['sys_currency'].set_text(self.sys_currency) self.sys_max_width = context.db.load_setting(20)[1] or 1024 self.spin['sys_max_width'].set_value(self.sys_max_width) self.sys_max_height = context.db.load_setting(21)[1] or 768 self.spin['sys_max_height'].set_value(self.sys_max_height) # combos roles = {'captain': 'Captain', 'helmsman': 'Helmsman', 'sailor': 'Sailor'} combobox.load_from_dict(self.combo['user_role'], roles) langs = {'de': 'Deutsch', 'en': 'English'} combobox.load_from_dict(self.combo['user_lang'], langs) today = datetime.now() datedict = { key: f"{prefix} - {today.strftime(fmt)}" for key, (fmt, prefix) in context.datefmt_map.items() } self.datefmt = context.db.load_setting(2)[0] or 'Y-m-d' combobox.load_from_dict(self.combo['datefmt'], datedict, self.datefmt) # shipyard self.db.cur.execute("SELECT compid, compname FROM company WHERE comptype=3 ORDER BY compname") shipyards = dict(self.db.cur.fetchall()) combobox.load_from_dict(self.combo['shipyard'], {-1: _("─── none ───")} | shipyards) # get current vessel data self.db.cur.execute("SELECT valint FROM settings WHERE userid=0 AND sno=1") row = self.db.cur.fetchone() if not row: self.context.vessel.vid = 0 self.context.vessel.vesselname = "Ghost" self.db.cur.execute("INSERT INTO settings (userid, sno, valint) VALUES (0, 1, 0)") self.db.conn.commit() else: # check if vessel from settings exist self.db.cur.execute("SELECT count(*) FROM vessel WHERE vid=%s", (row[0],)) res = self.db.cur.fetchone()[0] if res > 0: self.context.vessel.vid = row[0] else: # TODO set vessel to first in list self.context.vessel.vid = 0 self.context.vessel.vesselname = "Ghost" # default storage for current vessel sql = "SELECT sid, sname FROM storage WHERE stype=0 AND vid=%s ORDER BY sname" self.db.cur.execute(sql, (self.context.vessel.vid,)) storages = dict(self.db.cur.fetchall()) combobox.load_from_dict(self.combo['storage'], {-1: _("─── none ───")} | storages) # get list of vessels vessellist = {} self.db.dcur.execute("SELECT vid, vesselname, model FROM vessel ORDER BY vid") for row in self.db.dcur.fetchall(): vessellist[row['vid']] = ( row['vesselname'] if row['model'] is None else row['vesselname'] + " - " + row['model'] ) combobox.load_from_dict(self.combo['vessel'], vessellist, self.context.vessel.vid) # vessel load triggered from combobox init # prepare lists self.lst_user = Gtk.ListStore(int, str) self.db.cur.execute("SELECT userid, login FROM user WHERE userid>0 ORDER BY login") for row in self.db.cur.fetchall(): self.lst_user.append(row) treeview.add_column(self.tree['user'], 'ID', 0) treeview.add_column(self.tree['user'], 'Login', 1) self.tree['user'].set_model(self.lst_user) # list of dropdown lists self.lst_lists = Gtk.ListStore(int, str, int) # self.db.cur.execute("SELECT ddval, ddtext FROM dropdown WHERE ddid=0 AND ddval>0 ORDER BY ddval") sql = ( "SELECT dd.ddval, dd.ddtext, COUNT(n.ddval) AS records " "FROM dropdown AS dd LEFT JOIN dropdown AS n ON dd.ddval=n.ddid " "WHERE dd.ddid=0 " "GROUP BY dd.ddval, dd.ddtext " "ORDER BY dd.ddval" ) self.db.cur.execute(sql) for row in self.db.cur.fetchall(): self.lst_lists.append(row) treeview.add_column(self.tree['ddlists'], _('ID'), 0) treeview.add_column(self.tree['ddlists'], _('List'), 1) treeview.add_column(self.tree['ddlists'], _('Records'), 2) self.tree['ddlists'].set_model(self.lst_lists) # Settings date format selection list # self.context.datefmt_map # load shape of vessel if available if self.context.vessel.vid > 0: self.db.cur.execute("SELECT shapefile FROM vessel WHERE vid=%s", (self.context.vessel.vid, )) filename = self.db.cur.fetchone()[0] or '' shapefilepath = os.path.join(context.cfg['shapedir'], filename) if os.path.isfile(shapefilepath): handle = Rsvg.Handle() self._svg = handle.new_from_file(shapefilepath) #self._svg = handle.new_from_file(os.path.join(sys.path[0], cfg['shapedir'], "elizabethan29.svg")) #viewport = Rsvg.Rectangle() #viewport.x = 0 #viewport.y = 0 #viewport.width = 320 #viewport.height = 240 #self._svg.render_document(ctx, viewport) # WIP generic remarks handling self.text['vessel_remarks'].set_size_request(200, 70) def load_dropdowns(self): # non static dropdown (re-)load feature # default storage for current vessel sql = "SELECT sid, sname FROM storage WHERE stype=0 AND vid=%s ORDER BY sname" self.db.cur.execute(sql, (self.context.vessel.vid,)) storages = dict(self.db.cur.fetchall()) combobox.refresh_dict(self.combo['storage'], {-1: _("─── none ───")} | storages) def find_usb_sticks(self): result = [] for name in os.listdir("/sys/block"): device = "/sys/block/" + name removable = device + "/removable" if not os.path.exists(removable): continue with open(removable) as f: if f.read().strip() != "1": continue device_path = os.path.realpath(device + "/device") if "/usb" in device_path.lower(): result.append(name) return result def is_mounted(self, device): with open("/proc/mounts") as f: for line in f: mounted_device = line.split()[0] if mounted_device == device: return True return False def on_but_usbdevice_clicked(self, widget): self.backupdevice = None devlist = self.find_usb_sticks() if len(devlist) == 0: self.label['usb_device'].set_text('n/a') return if len(devlist) != 1: print(devlist) print("Multiple devices not supported yet") self.label['usb_device'].set_text('multiple') return usbdev = devlist[0] self.label['usb_device'].set_text(f"/dev/{usbdev}") """ serial number via python3-pyudev: import pyudev Settings 7 - usb backup device serial 8 - usb backup partition uuid 9 - last backup timestamp """ # first get stick hardware data udevctx = pyudev.Context() device = pyudev.Devices.from_sys_path(udevctx, os.path.join("/sys/class/block", usbdev)) serial = device.get("ID_SERIAL_SHORT") vendor = device.get("ID_VENDOR") model = device.get("ID_MODEL") self.label['usb_serial'].set_text(serial or 'n/a') self.label['usb_vendor'].set_text(vendor or 'n/a') self.label['usb_model'].set_text(model or 'n/a') # TODO second get partition, filesystem type and label # there must be only one ext4 partition on disk partitions = [ child for child in device.children if child.device_type == "partition" ] partition = partitions[0] print(f"Partition={partition}") if len(partitions) != 1: print("error: more than one partition!") return partuuid = partition.properties.get("ID_PART_ENTRY_UUID") fstype = partition.get("ID_FS_TYPE") plabel = partition.get("ID_FS_LABEL") self.label['usb_partition'].set_text(plabel) self.label['usb_uuid'].set_text(partuuid) self.label['usb_fstype'].set_text(fstype) if fstype != "ext4": print(f"error: invalid fileystem type {fstype}, ext4 expected") return self.backupdevice = f"/dev/{usbdev}1" # partition! # check if device is mounted if not self.is_mounted(self.backupdevice): print("device is not mounted. trying to mount now") pass # For backup there must be an directory names ymsbackup which contains # backup description and backup sets # Desription and identification file backup.json? # if plabel != 'BACKUP' or not os.path.isdir('ymsbackup') mountpoint = os.path.join("/media", os.getlogin(), "BACKUP") if not os.path.isdir(mountpoint): print(f"Device is not mounted at {mountpoint}") def on_but_usbinit_clicked(self, widget): if self.backupdevice is None: return # UUID of devive -> settings # check if backups already exist answer = gtkutils.questionbox( _("Initialize USB-Stick\n" "All data on that device will be lost!\n" "Are you really sure?" ), title="YMS") if answer == False: return if self.label['usb_fstype'].get_text() != 'ext4': print("formatting {self.backupdevice} with ext4") #try: # subprocess.run(["sudo", "mkfs.ext4", "-L", "BACKUP", self.backupdevice], check=True) #except subprocess.CalledProcessError as e: # print("formatting failed: ", e) myself = os.getlogin() mountpoint = os.path.join("/media", myself, "backup") basedir = os.path.join(mountpoint, 'ymsbackup') print(basedir) # mkdir basedir # chown (os.getlogin(), 'yms') # chmod 770 else: # pass def on_cmb_vessel_changed(self, widget): self.context.vessel.load(combobox.get_list_value(widget)) sql = ("UPDATE settings SET valint=%s WHERE userid=0 AND sno=1") self.db.cur.execute(sql, (self.context.vessel.vid,)) self.db.conn.commit() # Refresh dependent data self.load_dropdowns() # Show data self.display_vessel(self.context.vessel) def on_but_vessel_add_clicked(self, widget): answer = gtkutils.questionbox( _("Add new vessel.\n" "Are you sure?" ), title="YMS") if answer == False: return self.context.vessel.create() model = self.combo['vessel'].get_model() model.append((self.context.vessel.vid, self.context.vessel.vesselname)) combobox.activate_row(self.combo['vessel'], self.context.vessel.vid) self.edit_vessel(self.context.vessel) self.stack['vessel'].set_visible_child(self.box['vessel_edit']) def on_but_vessel_edit_clicked(self, widget): self.edit_vessel(self.context.vessel) self.stack['vessel'].set_visible_child(self.box['vessel_edit']) def on_but_vessel_save_clicked(self, widget): print("Save vessel") vessel = self.context.vessel vessel.vesselname = self.entry['vesselname'].get_text() vessel.model = self.entry['model'].get_text() vessel.year_built = self.spin['year_built'].get_value_as_int() vessel.loa = self.spin['loa'].get_value() vessel.lwl = self.spin['lwl'].get_value() vessel.beam = self.spin['beam'].get_value() vessel.draught = self.spin['draught'].get_value() vessel.draught_min = self.spin['draught_min'].get_value() vessel.displacement = self.spin['displacement'].get_value_as_int() vessel.ballast = self.spin['ballast'].get_value_as_int() vessel.remarks = gtkutils.get_text_contents(self.text['vessel_remarks']) vessel.save() self.stack['vessel'].set_visible_child(self.box['vessel']) def on_but_vessel_cancel_clicked(self, widget): self.stack['vessel'].set_visible_child(self.box['vessel']) def on_but_vessel_delete_clicked(self, widget): pass def on_but_settings_save_clicked(self, widget): print("Save settings") #self.entry['sys_currency'] #self.spin['sys_max_width'] #self.spin['sys_max_height'] def display_vessel(self, vessel): self.label['vesselname'].set_text(vessel.vesselname) self.label['model'].set_text(vessel.model or '') self.label['shipyard'].set_text(str(vessel.shipyard)) self.label['year_built'].set_text(self.format_int(vessel.year_built, False, default='n/a')) self.label['loa'].set_text(self.format_float(vessel.loa, 2, suffix="m")) self.label['lwl'].set_text(self.format_float(vessel.lwl, 2, suffix="m")) self.label['beam'].set_text(self.format_float(vessel.beam, 2, suffix="m")) self.label['draught'].set_text(self.format_float(vessel.draught, 2, suffix="m")) self.label['draught_min'].set_text(self.format_float(vessel.draught_min, 2, suffix="m")) self.label['displacement'].set_text(self.format_int(vessel.displacement, 2, suffix="kg")) self.label['ballast'].set_text(self.format_int(vessel.ballast, 2, suffix="kg")) self.label['belt_bow'].set_text(self.format_float(vessel.beltpos_bow, 2, suffix="m")) self.label['belt_aft'].set_text(self.format_float(vessel.beltpos_aft, 2, suffix="m")) self.label['hull_shape'].set_text(vessel.shapefile or '') self.label['storage_def'].set_text(str(vessel.sid_default)) self.label['remarks'].set_text(vessel.remarks or '') def edit_vessel(self, vessel): self.entry['vesselname'].set_text(vessel.vesselname) self.entry['model'].set_text(vessel.model or '') self.spin['year_built'].set_value(vessel.year_built or 0) self.spin['loa'].set_value(vessel.loa or 0) self.spin['lwl'].set_value(vessel.lwl or 0) self.spin['beam'].set_value(vessel.beam or 0) self.spin['draught'].set_value(vessel.draught or 0) self.spin['draught_min'].set_value(vessel.draught_min or 0) self.spin['displacement'].set_value(vessel.displacement or 0) self.spin['ballast'].set_value(vessel.ballast or 0) self.text['vessel_remarks'].get_buffer().set_text(vessel.remarks or '') combobox.activate_row(self.combo['shipyard'], vessel.shipyard or -1) combobox.activate_row(self.combo['storage'], vessel.sid_default or -1) def display_user(self, user): self.entry['user_login'].set_text(user.login) self.entry['user_displayname'].set_text(user.displayname) combobox.activate_row(self.combo['user_role'], user.role) combobox.activate_row(self.combo['user_lang'], user.language) self.check['user_locked'].set_active('locked' in user.flags) self.check['user_deleted'].set_active('deleted' in user.flags) self.entry['user_pass'].set_text('') def on_tvw_user_row_activated(self, widget, path, column): # load selected user self.context.user.user_id = treeview.get_value(widget, 0) self.context.user.load() self.display_user(self.context.user) def on_but_user_new_clicked(self, widget): # get first free user id and create a new record with name "user" self.context.user.create() self.lst_user.append((self.context.user.user_id, self.context.user.login)) def on_but_user_del_clicked(self, widget): print("Delete user") answer = gtkutils.questionbox( _("Delete user!\n" "Are you sure?" ), title="YMS") if answer == False: return self.context.user.delete() selection = self.tree['user'].get_selection() model, tree_iter = selection.get_selected() model.remove(tree_iter) # TODO Select some other user from new list selection self.context.user.user_id = treeview.get_value(self.tree['user']) self.context.user.load() self.display_user(self.context.user) def on_but_user_newpass_clicked(self, widget): self.entry['user_pass'].set_text(self.context.user.genpass()) def on_but_user_save_clicked(self, widget): newlogin = self.entry['user_login'].get_text() # check if changed login exists and throw error in that case if newlogin != self.context.user.login: sql = "SELECT COUNT(login) FROM user WHERE login=%s" self.db.cur.execute(sql, (newlogin, )) if self.db.cur.fetchone()[0] > 0: # TODO set error indicator print("Error: Login already exists!") return sql = "UPDATE user SET login=%s WHERE userid=%s" self.db.cur.execute(sql, (newlogin, self.context.user.user_id)) treeview.set_value(self.tree['user'], 1, newlogin) self.context.user.displayname = self.entry['user_displayname'].get_text() self.context.user.role = combobox.get_list_value(self.combo['user_role']) self.context.user.language = combobox.get_list_value(self.combo['user_lang']) self.context.user.flags = [] if self.check['user_locked'].get_active(): self.context.user.flags.append('locked') if self.check['user_deleted'].get_active(): self.context.user.flags.append('deleted') self.context.user.save() # password field is different. if filled set new password newpass = self.entry['user_pass'].get_text() if len(newpass) >= 8: try: self.context.user.setpass(newpass) finally: newpass = None self.entry['user_pass'].set_text('') else: # Password to short pass