439 lines
18 KiB
Python
439 lines
18 KiB
Python
# 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<nnn>"
|
|
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
|