Merge pull request #698 from andkit/exp-gui
Gui frontend for openslides
This commit is contained in:
commit
50e1cac469
0
extras/openslides_gui/__init__.py
Normal file
0
extras/openslides_gui/__init__.py
Normal file
4
extras/openslides_gui/__main__.py
Normal file
4
extras/openslides_gui/__main__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .gui import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
BIN
extras/openslides_gui/data/openslides-logo_wide.png
Normal file
BIN
extras/openslides_gui/data/openslides-logo_wide.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.2 KiB |
BIN
extras/openslides_gui/data/openslides.ico
Normal file
BIN
extras/openslides_gui/data/openslides.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
695
extras/openslides_gui/gui.py
Normal file
695
extras/openslides_gui/gui.py
Normal file
@ -0,0 +1,695 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import Queue
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import gettext
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import locale
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import wx
|
||||||
|
|
||||||
|
import openslides
|
||||||
|
import openslides.main
|
||||||
|
|
||||||
|
# NOTE: djangos translation module can't be used here since it requires
|
||||||
|
# a defined settings module
|
||||||
|
_translations = gettext.NullTranslations()
|
||||||
|
_ = lambda text: _translations.ugettext(text)
|
||||||
|
ungettext = lambda msg1, msg2, n: _translations.ungettext(msg1, msg2, n)
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_path(*args):
|
||||||
|
return os.path.join(os.path.dirname(__file__), "data", *args)
|
||||||
|
|
||||||
|
|
||||||
|
class RunCmdEvent(wx.PyCommandEvent):
|
||||||
|
def __init__(self, evt_type, evt_id):
|
||||||
|
super(RunCmdEvent, self).__init__(evt_type, evt_id)
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
self.exitcode = None
|
||||||
|
|
||||||
|
EVT_RUN_CMD_ID = wx.NewEventType()
|
||||||
|
EVT_RUN_CMD = wx.PyEventBinder(EVT_RUN_CMD_ID, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class RunCommandControl(wx.Panel):
|
||||||
|
UPDATE_INTERVAL = 500
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super(RunCommandControl, self).__init__(parent)
|
||||||
|
|
||||||
|
self.child_process = None
|
||||||
|
self.output_queue = Queue.Queue()
|
||||||
|
self.output_read_thread = None
|
||||||
|
self.canceled = False
|
||||||
|
self.output_mutex = threading.RLock()
|
||||||
|
|
||||||
|
vbox = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
self.te_output = wx.TextCtrl(
|
||||||
|
self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
|
||||||
|
vbox.Add(self.te_output, 1, wx.EXPAND)
|
||||||
|
|
||||||
|
self.update_timer = wx.Timer(self)
|
||||||
|
self.Bind(wx.EVT_TIMER, self.on_update_timer, self.update_timer)
|
||||||
|
|
||||||
|
self.SetSizerAndFit(vbox)
|
||||||
|
|
||||||
|
def _read_output(self):
|
||||||
|
while True:
|
||||||
|
# NOTE: don't use iterator interface since it uses an
|
||||||
|
# internal buffer and we don't see output in a timely fashion
|
||||||
|
line = self.child_process.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
self.output_queue.put(line)
|
||||||
|
|
||||||
|
def is_alive(self):
|
||||||
|
if self.child_process is None:
|
||||||
|
return False
|
||||||
|
return self.child_process.poll() is None
|
||||||
|
|
||||||
|
def run_command(self, *args):
|
||||||
|
if self.is_alive():
|
||||||
|
raise ValueError("already running a command")
|
||||||
|
|
||||||
|
cmd = [sys.executable, "-u", "-m", "openslides.main"]
|
||||||
|
cmd.extend(args)
|
||||||
|
creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
||||||
|
self.child_process = subprocess.Popen(
|
||||||
|
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT, creationflags=creationflags)
|
||||||
|
self.child_process.stdin.close()
|
||||||
|
self.output_read_thread = threading.Thread(target=self._read_output)
|
||||||
|
self.output_read_thread.start()
|
||||||
|
|
||||||
|
self.update_timer.Start(self.UPDATE_INTERVAL)
|
||||||
|
|
||||||
|
evt = RunCmdEvent(EVT_RUN_CMD_ID, self.GetId())
|
||||||
|
evt.running = True
|
||||||
|
self.GetEventHandler().ProcessEvent(evt)
|
||||||
|
|
||||||
|
def cancel_command(self):
|
||||||
|
if not self.is_alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: try sigint first, then get more aggressive if user insists
|
||||||
|
self.child_process.kill()
|
||||||
|
self.canceled = True
|
||||||
|
|
||||||
|
def on_update_timer(self, evt):
|
||||||
|
is_alive = self.is_alive()
|
||||||
|
if not is_alive:
|
||||||
|
# join thread to make sure everything was read
|
||||||
|
self.output_read_thread.join()
|
||||||
|
self.output_read_thread = None
|
||||||
|
|
||||||
|
for line_no in itertools.count():
|
||||||
|
try:
|
||||||
|
data = self.output_queue.get(block=False)
|
||||||
|
except Queue.Empty:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# XXX: check whether django uses utf-8 or locale for
|
||||||
|
# it's cli output
|
||||||
|
text = data.decode("utf-8", errors="replace")
|
||||||
|
with self.output_mutex:
|
||||||
|
self.te_output.AppendText(text)
|
||||||
|
|
||||||
|
# avoid waiting too long here if child is still alive
|
||||||
|
if is_alive and line_no > 10:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_alive:
|
||||||
|
exitcode = self.child_process.returncode
|
||||||
|
self.update_timer.Stop()
|
||||||
|
self.child_process = None
|
||||||
|
|
||||||
|
evt = RunCmdEvent(EVT_RUN_CMD_ID, self.GetId())
|
||||||
|
evt.running = False
|
||||||
|
evt.exitcode = exitcode
|
||||||
|
self.GetEventHandler().ProcessEvent(evt)
|
||||||
|
|
||||||
|
def append_message(self, text, newline="\n"):
|
||||||
|
with self.output_mutex:
|
||||||
|
self.te_output.AppendText(text + newline)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDialog(wx.Dialog):
|
||||||
|
def __init__(self, parent):
|
||||||
|
super(SettingsDialog, self).__init__(parent, wx.ID_ANY, _("Settings"))
|
||||||
|
|
||||||
|
grid = wx.GridBagSizer(5, 5)
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
lb_host = wx.StaticText(self, label=_("&Host:"))
|
||||||
|
grid.Add(lb_host, pos=(row, 0))
|
||||||
|
self.tc_host = wx.TextCtrl(self)
|
||||||
|
grid.Add(self.tc_host, pos=(row, 1), flag=wx.EXPAND)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
lb_port = wx.StaticText(self, label=_("&Port:"))
|
||||||
|
grid.Add(lb_port, pos=(row, 0))
|
||||||
|
self.tc_port = wx.TextCtrl(self)
|
||||||
|
grid.Add(self.tc_port, pos=(row, 1), flag=wx.EXPAND)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
sizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
|
||||||
|
if not sizer is None:
|
||||||
|
grid.Add((0, 0), pos=(row, 0), span=(1, 2))
|
||||||
|
row += 1
|
||||||
|
grid.Add(sizer, pos=(row, 0), span=(1, 2))
|
||||||
|
|
||||||
|
box = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
box.Add(
|
||||||
|
grid, flag=wx.EXPAND | wx.ALL | wx.ALIGN_CENTER_VERTICAL,
|
||||||
|
border=5, proportion=1)
|
||||||
|
|
||||||
|
self.SetSizerAndFit(box)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
return self.tc_host.GetValue()
|
||||||
|
|
||||||
|
@host.setter
|
||||||
|
def host(self, host):
|
||||||
|
self.tc_host.SetValue(host)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self):
|
||||||
|
return self.tc_port.GetValue()
|
||||||
|
|
||||||
|
@port.setter
|
||||||
|
def port(self, port):
|
||||||
|
self.tc_port.SetValue(port)
|
||||||
|
|
||||||
|
|
||||||
|
class BackupSettingsDialog(wx.Dialog):
|
||||||
|
# NOTE: keep order in sync with _update_interval_choices()
|
||||||
|
_INTERVAL_UNITS = ["second", "minute", "hour"]
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super(BackupSettingsDialog, self).__init__(
|
||||||
|
parent, wx.ID_ANY, _("Database backup"))
|
||||||
|
|
||||||
|
self._interval_units = {}
|
||||||
|
|
||||||
|
grid = wx.GridBagSizer(5, 5)
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
self.cb_backup = wx.CheckBox(
|
||||||
|
self, label=_("&Regularly backup database"))
|
||||||
|
self.cb_backup.SetValue(True)
|
||||||
|
self.cb_backup.Bind(wx.EVT_CHECKBOX, self.on_backup_checked)
|
||||||
|
grid.Add(self.cb_backup, pos=(row, 0), span=(1, 3))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
lb_dest = wx.StaticText(self, label=_("&Destination:"))
|
||||||
|
grid.Add(lb_dest, pos=(row, 0))
|
||||||
|
style = wx.FLP_SAVE | wx.FLP_USE_TEXTCTRL
|
||||||
|
self.fp_dest = wx.FilePickerCtrl(self, style=style)
|
||||||
|
grid.Add(self.fp_dest, pos=(row, 1), span=(1, 2), flag=wx.EXPAND)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
lb_interval = wx.StaticText(self, label=_("&Every"))
|
||||||
|
grid.Add(lb_interval, pos=(row, 0))
|
||||||
|
self.sb_interval = wx.SpinCtrl(self, min=1, initial=1)
|
||||||
|
self.sb_interval.Bind(wx.EVT_SPINCTRL, self.on_interval_changed)
|
||||||
|
grid.Add(self.sb_interval, pos=(row, 1))
|
||||||
|
self.ch_interval_unit = wx.Choice(self)
|
||||||
|
grid.Add(self.ch_interval_unit, pos=(row, 2))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
grid.AddGrowableCol(1)
|
||||||
|
|
||||||
|
sizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
|
||||||
|
if not sizer is None:
|
||||||
|
grid.Add((0, 0), pos=(row, 0), span=(1, 3))
|
||||||
|
row += 1
|
||||||
|
grid.Add(sizer, pos=(row, 0), span=(1, 3))
|
||||||
|
|
||||||
|
box = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
box.Add(
|
||||||
|
grid, flag=wx.EXPAND | wx.ALL | wx.ALIGN_CENTER_VERTICAL,
|
||||||
|
border=5, proportion=1)
|
||||||
|
|
||||||
|
self.SetSizerAndFit(box)
|
||||||
|
self._update_interval_choices()
|
||||||
|
self._update_backup_enabled()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backupdb_enabled(self):
|
||||||
|
return self.cb_backup.GetValue()
|
||||||
|
|
||||||
|
@backupdb_enabled.setter
|
||||||
|
def backupdb_enabled(self, enabled):
|
||||||
|
self.cb_backup.SetValue(enabled)
|
||||||
|
self._update_backup_enabled()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backupdb_destination(self):
|
||||||
|
return self.fp_dest.GetPath()
|
||||||
|
|
||||||
|
@backupdb_destination.setter
|
||||||
|
def backupdb_destination(self, path):
|
||||||
|
self.fp_dest.SetPath(path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interval(self):
|
||||||
|
return self.sb_interval.GetValue()
|
||||||
|
|
||||||
|
@interval.setter
|
||||||
|
def interval(self, value):
|
||||||
|
self.sb_interval.SetValue(value)
|
||||||
|
self._update_interval_choices()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interval_unit(self):
|
||||||
|
return self._INTERVAL_UNITS[self.ch_interval_unit.GetSelection()]
|
||||||
|
|
||||||
|
@interval_unit.setter
|
||||||
|
def interval_unit(self, unit):
|
||||||
|
try:
|
||||||
|
idx = self._INTERVAL_UNITS.index(unit)
|
||||||
|
except IndexError:
|
||||||
|
raise ValueError("Unknown unit {0}".format(unit))
|
||||||
|
|
||||||
|
self.ch_interval_unit.SetSelection(idx)
|
||||||
|
|
||||||
|
def _update_interval_choices(self):
|
||||||
|
count = self.sb_interval.GetValue()
|
||||||
|
choices = [
|
||||||
|
ungettext("second", "seconds", count),
|
||||||
|
ungettext("minute", "minutes", count),
|
||||||
|
ungettext("hour", "hours", count),
|
||||||
|
]
|
||||||
|
|
||||||
|
current = self.ch_interval_unit.GetSelection()
|
||||||
|
if current == wx.NOT_FOUND:
|
||||||
|
current = 2 # default to hour
|
||||||
|
|
||||||
|
self.ch_interval_unit.Clear()
|
||||||
|
self.ch_interval_unit.AppendItems(choices)
|
||||||
|
self.ch_interval_unit.SetSelection(current)
|
||||||
|
|
||||||
|
def _update_backup_enabled(self):
|
||||||
|
checked = self.cb_backup.IsChecked()
|
||||||
|
self.fp_dest.Enable(checked)
|
||||||
|
self.sb_interval.Enable(checked)
|
||||||
|
self.ch_interval_unit.Enable(checked)
|
||||||
|
|
||||||
|
def on_backup_checked(self, evt):
|
||||||
|
self._update_backup_enabled()
|
||||||
|
|
||||||
|
def on_interval_changed(self, evt):
|
||||||
|
self._update_interval_choices()
|
||||||
|
|
||||||
|
# TODO: validate settings on close (e.g. non-empty path if backup is
|
||||||
|
# enabled)
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(wx.Frame):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(MainWindow, self).__init__(parent, title="OpenSlides")
|
||||||
|
icons = wx.IconBundleFromFile(
|
||||||
|
get_data_path("openslides.ico"),
|
||||||
|
wx.BITMAP_TYPE_ICO)
|
||||||
|
self.SetIcons(icons)
|
||||||
|
|
||||||
|
self.server_running = False
|
||||||
|
if openslides.main.is_portable():
|
||||||
|
self.gui_settings_path = openslides.main.get_portable_path(
|
||||||
|
"openslides", "gui_settings.json")
|
||||||
|
else:
|
||||||
|
self.gui_settings_path = openslides.main.get_user_config_path(
|
||||||
|
"openslides", "gui_settings.json")
|
||||||
|
|
||||||
|
self.backupdb_enabled = False
|
||||||
|
self.backupdb_destination = ""
|
||||||
|
self.backupdb_interval = 15
|
||||||
|
self.backupdb_interval_unit = "minute"
|
||||||
|
self.last_backup = None
|
||||||
|
|
||||||
|
self.backup_timer = wx.Timer(self)
|
||||||
|
self.Bind(wx.EVT_TIMER, self.on_backup_timer, self.backup_timer)
|
||||||
|
|
||||||
|
spacing = 5
|
||||||
|
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
grid = wx.GridBagSizer(spacing, spacing)
|
||||||
|
|
||||||
|
# logo & about button
|
||||||
|
logo_box = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
grid.Add(logo_box, pos=(0, 0), flag=wx.EXPAND)
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
fp = get_data_path("openslides-logo_wide.png")
|
||||||
|
with open(fp, "rb") as f:
|
||||||
|
logo_wide_bmp = wx.ImageFromStream(f).ConvertToBitmap()
|
||||||
|
|
||||||
|
logo_wide = wx.StaticBitmap(panel, wx.ID_ANY, logo_wide_bmp)
|
||||||
|
logo_box.AddSpacer(2 * spacing)
|
||||||
|
logo_box.Add(logo_wide)
|
||||||
|
logo_box.AddStretchSpacer()
|
||||||
|
|
||||||
|
version_str = _("Version {0}").format(openslides.get_version())
|
||||||
|
lb_version = wx.StaticText(panel, label=version_str)
|
||||||
|
font = lb_version.GetFont()
|
||||||
|
font.SetPointSize(8)
|
||||||
|
lb_version.SetFont(font)
|
||||||
|
logo_box.Add(lb_version, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
|
||||||
|
self.bt_about = wx.Button(panel, label=_("&About..."))
|
||||||
|
self.bt_about.Bind(wx.EVT_BUTTON, self.on_about_clicked)
|
||||||
|
grid.Add(self.bt_about, pos=(row, 1), flag=wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
grid.Add((0, spacing), pos=(row, 0), span=(1, 2))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# server settings
|
||||||
|
server_settings = wx.StaticBox(panel, wx.ID_ANY, _("Server Settings"))
|
||||||
|
server_box = wx.StaticBoxSizer(server_settings, wx.VERTICAL)
|
||||||
|
grid.Add(server_box, pos=(row, 0), flag=wx.EXPAND)
|
||||||
|
|
||||||
|
self._host = None
|
||||||
|
self._port = None
|
||||||
|
hbox = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
server_box.Add(hbox, flag=wx.EXPAND)
|
||||||
|
self.lb_host = wx.StaticText(panel)
|
||||||
|
hbox.Add(self.lb_host, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
hbox.AddStretchSpacer()
|
||||||
|
self.lb_port = wx.StaticText(panel)
|
||||||
|
hbox.Add(self.lb_port, flag=wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
hbox.AddStretchSpacer()
|
||||||
|
self.bt_settings = wx.Button(panel, label=_("S&ettings..."))
|
||||||
|
self.bt_settings.Bind(wx.EVT_BUTTON, self.on_settings_clicked)
|
||||||
|
hbox.Add(self.bt_settings)
|
||||||
|
|
||||||
|
server_box.AddSpacer(spacing)
|
||||||
|
self.cb_start_browser = wx.CheckBox(
|
||||||
|
panel, label=_("Automatically open &browser"))
|
||||||
|
self.cb_start_browser.SetValue(True)
|
||||||
|
server_box.Add(self.cb_start_browser)
|
||||||
|
server_box.AddStretchSpacer()
|
||||||
|
|
||||||
|
server_box.AddSpacer(spacing)
|
||||||
|
self.bt_server = wx.Button(panel, label=_("&Start server"))
|
||||||
|
self.bt_server.Bind(wx.EVT_BUTTON, self.on_start_server_clicked)
|
||||||
|
server_box.Add(self.bt_server, flag=wx.EXPAND)
|
||||||
|
|
||||||
|
host, port = openslides.main.detect_listen_opts()
|
||||||
|
self.host = host
|
||||||
|
self.port = unicode(port)
|
||||||
|
|
||||||
|
# "action" buttons
|
||||||
|
action_vbox = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
action_vbox.AddSpacer(3 * spacing)
|
||||||
|
grid.Add(action_vbox, pos=(row, 1))
|
||||||
|
self.bt_backup = wx.Button(panel, label=_("&Backup database..."))
|
||||||
|
self.bt_backup.Bind(wx.EVT_BUTTON, self.on_backup_clicked)
|
||||||
|
action_vbox.Add(self.bt_backup)
|
||||||
|
action_vbox.AddSpacer(spacing)
|
||||||
|
self.bt_sync_db = wx.Button(panel, label=_("S&ync database"))
|
||||||
|
self.bt_sync_db.Bind(wx.EVT_BUTTON, self.on_syncdb_clicked)
|
||||||
|
action_vbox.Add(self.bt_sync_db)
|
||||||
|
action_vbox.AddSpacer(spacing)
|
||||||
|
self.bt_reset_admin = wx.Button(panel, label=_("&Reset admin"))
|
||||||
|
self.bt_reset_admin.Bind(wx.EVT_BUTTON, self.on_reset_admin_clicked)
|
||||||
|
action_vbox.Add(self.bt_reset_admin)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
# command output
|
||||||
|
self.cmd_run_ctrl = RunCommandControl(panel)
|
||||||
|
self.cmd_run_ctrl.Bind(EVT_RUN_CMD, self.on_run_cmd_changed)
|
||||||
|
grid.Add(
|
||||||
|
self.cmd_run_ctrl,
|
||||||
|
pos=(row, 0), span=(1, 2),
|
||||||
|
flag=wx.EXPAND)
|
||||||
|
|
||||||
|
grid.AddGrowableCol(0)
|
||||||
|
grid.AddGrowableRow(3)
|
||||||
|
|
||||||
|
box = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
box.Add(
|
||||||
|
grid, flag=wx.EXPAND | wx.ALL | wx.ALIGN_CENTER_VERTICAL,
|
||||||
|
border=spacing, proportion=1)
|
||||||
|
panel.SetSizerAndFit(box)
|
||||||
|
self.Fit()
|
||||||
|
self.SetMinSize(self.ClientToWindowSize(box.GetMinSize()))
|
||||||
|
self.SetInitialSize(wx.Size(500, 400))
|
||||||
|
|
||||||
|
self.Bind(wx.EVT_CLOSE, self.on_close)
|
||||||
|
|
||||||
|
self.load_gui_settings()
|
||||||
|
self.apply_backup_settings()
|
||||||
|
self.Show()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_interval_seconds(self):
|
||||||
|
if self.backupdb_interval_unit == "second":
|
||||||
|
factor = 1
|
||||||
|
elif self.backupdb_interval_unit == "minute":
|
||||||
|
factor = 60
|
||||||
|
elif self.backupdb_interval_unit == "hour":
|
||||||
|
factor = 3600
|
||||||
|
|
||||||
|
return self.backupdb_interval * factor
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
return self._host
|
||||||
|
|
||||||
|
@host.setter
|
||||||
|
def host(self, host):
|
||||||
|
self._host = host
|
||||||
|
self.lb_host.SetLabel(_("Host: {0}").format(host))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self):
|
||||||
|
return self._port
|
||||||
|
|
||||||
|
@port.setter
|
||||||
|
def port(self, port):
|
||||||
|
self._port = port
|
||||||
|
self.lb_port.SetLabel(_("Port: {0}").format(port))
|
||||||
|
|
||||||
|
def load_gui_settings(self):
|
||||||
|
try:
|
||||||
|
f = open(self.gui_settings_path, "rb")
|
||||||
|
except IOError as e:
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
|
||||||
|
with f:
|
||||||
|
settings = json.load(f)
|
||||||
|
|
||||||
|
def setattr_unless_none(attr, value):
|
||||||
|
if not value is None:
|
||||||
|
setattr(self, attr, value)
|
||||||
|
|
||||||
|
backup_settings = settings.get("database_backup", {})
|
||||||
|
setattr_unless_none("backupdb_enabled", backup_settings.get("enabled"))
|
||||||
|
setattr_unless_none(
|
||||||
|
"backupdb_destination", backup_settings.get("destination"))
|
||||||
|
setattr_unless_none(
|
||||||
|
"backupdb_interval", backup_settings.get("interval"))
|
||||||
|
setattr_unless_none(
|
||||||
|
"backupdb_interval_unit", backup_settings.get("interval_unit"))
|
||||||
|
last_backup = backup_settings.get("last_backup")
|
||||||
|
if not last_backup is None:
|
||||||
|
self.last_backup = datetime.datetime.strptime(
|
||||||
|
last_backup, "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
def save_gui_settings(self):
|
||||||
|
if self.last_backup is None:
|
||||||
|
last_backup = None
|
||||||
|
else:
|
||||||
|
last_backup = self.last_backup.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
settings = {
|
||||||
|
"database_backup": {
|
||||||
|
"enabled": self.backupdb_enabled,
|
||||||
|
"destination": self.backupdb_destination,
|
||||||
|
"internal": self.backupdb_interval,
|
||||||
|
"interval_unit": self.backupdb_interval_unit,
|
||||||
|
"last_backup": last_backup
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dp = os.path.dirname(self.gui_settings_path)
|
||||||
|
if not os.path.exists(dp):
|
||||||
|
os.makedirs(dp)
|
||||||
|
with open(self.gui_settings_path, "wb") as f:
|
||||||
|
json.dump(settings, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
def apply_backup_settings(self):
|
||||||
|
if self.backupdb_enabled and self.server_running:
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
delta = datetime.timedelta(seconds=self.backup_interval_seconds)
|
||||||
|
ref = self.last_backup
|
||||||
|
if ref is None:
|
||||||
|
ref = now
|
||||||
|
ref += delta
|
||||||
|
|
||||||
|
d = ref - now
|
||||||
|
seconds = d.days * 86400 + d.seconds
|
||||||
|
if seconds < 1:
|
||||||
|
seconds = 30 # avoid backup immediatly after start
|
||||||
|
self.backup_timer.Start(seconds * 1000, True)
|
||||||
|
else:
|
||||||
|
self.backup_timer.Stop()
|
||||||
|
|
||||||
|
def do_backup(self):
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-u", "-m", "openslides.main",
|
||||||
|
"--no-run",
|
||||||
|
"--backupdb={0}".format(self.backupdb_destination),
|
||||||
|
]
|
||||||
|
p = subprocess.Popen(
|
||||||
|
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
p.stdin.close()
|
||||||
|
output = p.stdout.read().strip()
|
||||||
|
exitcode = p.wait()
|
||||||
|
if output:
|
||||||
|
self.cmd_run_ctrl.append_message(output)
|
||||||
|
|
||||||
|
time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if exitcode == 0:
|
||||||
|
self.cmd_run_ctrl.append_message(
|
||||||
|
_("{0}: Database backup successful.").format(time))
|
||||||
|
else:
|
||||||
|
self.cmd_run_ctrl.append_message(
|
||||||
|
_("{0}: Database backup failed!").format(time))
|
||||||
|
|
||||||
|
self.last_backup = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
def on_syncdb_clicked(self, evt):
|
||||||
|
self.cmd_run_ctrl.append_message(_("Syncing database..."))
|
||||||
|
self.cmd_run_ctrl.run_command("--no-run", "--syncdb")
|
||||||
|
|
||||||
|
def on_reset_admin_clicked(self, evt):
|
||||||
|
self.cmd_run_ctrl.append_message(_("Resetting admin user..."))
|
||||||
|
self.cmd_run_ctrl.run_command("--no-run", "--reset-admin")
|
||||||
|
|
||||||
|
def on_about_clicked(self, evt):
|
||||||
|
info = wx.AboutDialogInfo()
|
||||||
|
info.SetName("OpenSlides")
|
||||||
|
info.SetVersion(openslides.get_version())
|
||||||
|
info.SetDescription(_(
|
||||||
|
"OpenSlides is a free web based presentation and "
|
||||||
|
"assembly system.\n"
|
||||||
|
"OpenSlides is free software; licensed under the GNU GPL v2+."
|
||||||
|
).replace(u" ", u"\u00a0"))
|
||||||
|
info.SetCopyright(_(u"\u00a9 2011-2013 by OpenSlides team"))
|
||||||
|
info.SetWebSite(("http://www.openslides.org/", "www.openslides.org"))
|
||||||
|
|
||||||
|
# XXX: at least on wxgtk this has no effect
|
||||||
|
info.SetIcon(self.GetIcon())
|
||||||
|
wx.AboutBox(info)
|
||||||
|
|
||||||
|
def on_start_server_clicked(self, evt):
|
||||||
|
if self.server_running:
|
||||||
|
self.cmd_run_ctrl.cancel_command()
|
||||||
|
return
|
||||||
|
|
||||||
|
args = ["--address", self._host, "--port", self._port]
|
||||||
|
if not self.cb_start_browser.GetValue():
|
||||||
|
args.append("--no-browser")
|
||||||
|
|
||||||
|
self.server_running = True
|
||||||
|
self.cmd_run_ctrl.run_command(*args)
|
||||||
|
|
||||||
|
# initiate backup_timer if backup is enabled
|
||||||
|
self.apply_backup_settings()
|
||||||
|
|
||||||
|
self.bt_server.SetLabel(_("&Stop server"))
|
||||||
|
|
||||||
|
def on_settings_clicked(self, evt):
|
||||||
|
dlg = SettingsDialog(self)
|
||||||
|
dlg.host = self._host
|
||||||
|
dlg.port = self._port
|
||||||
|
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
self.host = dlg.host
|
||||||
|
self.port = dlg.port
|
||||||
|
|
||||||
|
def on_backup_clicked(self, evt):
|
||||||
|
dlg = BackupSettingsDialog(self)
|
||||||
|
dlg.backupdb_enabled = self.backupdb_enabled
|
||||||
|
dlg.backupdb_destination = self.backupdb_destination
|
||||||
|
dlg.interval = self.backupdb_interval
|
||||||
|
dlg.interval_unit = self.backupdb_interval_unit
|
||||||
|
if dlg.ShowModal() == wx.ID_OK:
|
||||||
|
self.backupdb_enabled = dlg.backupdb_enabled
|
||||||
|
self.backupdb_destination = dlg.backupdb_destination
|
||||||
|
self.backupdb_interval = dlg.interval
|
||||||
|
self.backupdb_interval_unit = dlg.interval_unit
|
||||||
|
self.apply_backup_settings()
|
||||||
|
|
||||||
|
def on_run_cmd_changed(self, evt):
|
||||||
|
show_completion_msg = not evt.running
|
||||||
|
if self.server_running and not evt.running:
|
||||||
|
self.bt_server.SetLabel(_("&Start server"))
|
||||||
|
self.server_running = False
|
||||||
|
|
||||||
|
self.backup_timer.Stop()
|
||||||
|
if self.backupdb_enabled:
|
||||||
|
self.do_backup()
|
||||||
|
|
||||||
|
# no operation completed msg when stopping server
|
||||||
|
show_completion_msg = False
|
||||||
|
|
||||||
|
self.bt_settings.Enable(not evt.running)
|
||||||
|
self.bt_backup.Enable(not evt.running)
|
||||||
|
self.bt_sync_db.Enable(not evt.running)
|
||||||
|
self.bt_reset_admin.Enable(not evt.running)
|
||||||
|
self.bt_server.Enable(self.server_running or not evt.running)
|
||||||
|
|
||||||
|
if show_completion_msg:
|
||||||
|
if evt.exitcode == 0:
|
||||||
|
text = _("Operation successfully completed.")
|
||||||
|
else:
|
||||||
|
text = _("Operation failed (exit code = {0})").format(
|
||||||
|
evt.exitcode)
|
||||||
|
self.cmd_run_ctrl.append_message(text)
|
||||||
|
|
||||||
|
def on_backup_timer(self, evt):
|
||||||
|
if not self.backupdb_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.do_backup()
|
||||||
|
self.backup_timer.Start(1000 * self.backup_interval_seconds, True)
|
||||||
|
|
||||||
|
def on_close(self, ev):
|
||||||
|
self.cmd_run_ctrl.cancel_command()
|
||||||
|
self.save_gui_settings()
|
||||||
|
self.Destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
|
lang = locale.getdefaultlocale()[0]
|
||||||
|
if lang:
|
||||||
|
global _translations
|
||||||
|
localedir = os.path.dirname(openslides.__file__)
|
||||||
|
localedir = os.path.join(localedir, "locale")
|
||||||
|
_translations = gettext.translation(
|
||||||
|
"django", localedir, [lang], fallback=True)
|
||||||
|
|
||||||
|
app = wx.App(False)
|
||||||
|
window = MainWindow()
|
||||||
|
app.MainLoop()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -12,18 +12,9 @@
|
|||||||
|
|
||||||
#include <Python.h>
|
#include <Python.h>
|
||||||
|
|
||||||
static const char *site_code =
|
|
||||||
"import sys;"
|
|
||||||
"import os;"
|
|
||||||
"import site;"
|
|
||||||
"path = os.path.dirname(sys.executable);"
|
|
||||||
"site_dir = os.path.join(path, \"site-packages\");"
|
|
||||||
"site.addsitedir(site_dir);"
|
|
||||||
"sys.path.append(path)";
|
|
||||||
|
|
||||||
static const char *run_openslides_code =
|
static const char *run_openslides_code =
|
||||||
"import openslides.main;"
|
"import openslides_gui.gui;"
|
||||||
"openslides.main.win32_portable_main()";
|
"openslides_gui.gui.main()";
|
||||||
|
|
||||||
/* determine the path to the executable
|
/* determine the path to the executable
|
||||||
* NOTE: Py_GetFullProgramPath() can't be used because
|
* NOTE: Py_GetFullProgramPath() can't be used because
|
||||||
@ -61,7 +52,7 @@ _get_module_name()
|
|||||||
else if (res == size)
|
else if (res == size)
|
||||||
{
|
{
|
||||||
/* NOTE: Don't check GetLastError() == ERROR_INSUFFICIENT_BUFFER
|
/* NOTE: Don't check GetLastError() == ERROR_INSUFFICIENT_BUFFER
|
||||||
* here, it isn't set consisntently across all platforms
|
* here, it isn't set consistently across all platforms
|
||||||
*/
|
*/
|
||||||
|
|
||||||
size += 4096;
|
size += 4096;
|
||||||
@ -83,12 +74,6 @@ _get_module_name()
|
|||||||
static int
|
static int
|
||||||
_run()
|
_run()
|
||||||
{
|
{
|
||||||
if (PyRun_SimpleString(site_code) != 0)
|
|
||||||
{
|
|
||||||
fprintf(stderr, "ERROR: failed to initialize site path\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PyRun_SimpleString(run_openslides_code) != 0)
|
if (PyRun_SimpleString(run_openslides_code) != 0)
|
||||||
{
|
{
|
||||||
fprintf(stderr, "ERROR: failed to execute openslides\n");
|
fprintf(stderr, "ERROR: failed to execute openslides\n");
|
||||||
@ -99,13 +84,14 @@ _run()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
int WINAPI
|
||||||
main(int argc, char *argv[])
|
WinMain(HINSTANCE inst, HINSTANCE prev_inst, LPSTR cmdline, int show)
|
||||||
{
|
{
|
||||||
int returncode;
|
int returncode;
|
||||||
|
int run_py_main = __argc > 1;
|
||||||
char *py_home, *sep = NULL;
|
char *py_home, *sep = NULL;
|
||||||
|
|
||||||
Py_SetProgramName(argv[0]);
|
Py_SetProgramName(__argv[0]);
|
||||||
|
|
||||||
py_home = _get_module_name();
|
py_home = _get_module_name();
|
||||||
|
|
||||||
@ -116,14 +102,24 @@ main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
*sep = '\0';
|
*sep = '\0';
|
||||||
Py_SetPythonHome(py_home);
|
Py_SetPythonHome(py_home);
|
||||||
|
Py_IgnoreEnvironmentFlag = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (run_py_main)
|
||||||
|
{
|
||||||
|
/* we where given extra arguments, behave like python.exe */
|
||||||
|
returncode = Py_Main(__argc, __argv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* no arguments given => start openslides gui */
|
||||||
Py_Initialize();
|
Py_Initialize();
|
||||||
PySys_SetArgvEx(argc, argv, 0);
|
PySys_SetArgvEx(__argc, __argv, 0);
|
||||||
|
|
||||||
returncode = _run();
|
returncode = _run();
|
||||||
|
|
||||||
Py_Finalize();
|
Py_Finalize();
|
||||||
|
}
|
||||||
|
|
||||||
free(py_home);
|
free(py_home);
|
||||||
|
|
||||||
return returncode;
|
return returncode;
|
||||||
|
BIN
extras/win32-portable/openslides.exe
Normal file → Executable file
BIN
extras/win32-portable/openslides.exe
Normal file → Executable file
Binary file not shown.
@ -20,8 +20,12 @@ import distutils.sysconfig
|
|||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
import wx
|
||||||
|
|
||||||
sys.path.insert(0, os.getcwd())
|
sys.path.insert(0, os.getcwd())
|
||||||
|
sys.path.insert(1, os.path.join(os.getcwd(), "extras"))
|
||||||
import openslides
|
import openslides
|
||||||
|
import openslides_gui
|
||||||
|
|
||||||
COMMON_EXCLUDE = [
|
COMMON_EXCLUDE = [
|
||||||
r".pyc$",
|
r".pyc$",
|
||||||
@ -99,7 +103,43 @@ SITE_PACKAGES = {
|
|||||||
},
|
},
|
||||||
"html5lib": {
|
"html5lib": {
|
||||||
"copy": ["html5lib"],
|
"copy": ["html5lib"],
|
||||||
}
|
},
|
||||||
|
"wx": {
|
||||||
|
# NOTE: wxpython is a special case, see copy_wx
|
||||||
|
"copy": [],
|
||||||
|
"exclude": [
|
||||||
|
r"^wx/tools/",
|
||||||
|
r"^wx/py/",
|
||||||
|
r"^wx/build/",
|
||||||
|
r"^wx/lib/",
|
||||||
|
r"wx/_activex.pyd",
|
||||||
|
r"wx/_animate.pyd",
|
||||||
|
r"wx/_aui.pyd",
|
||||||
|
r"wx/_calendar.pyd",
|
||||||
|
r"wx/_combo.pyd",
|
||||||
|
r"wx/_gizmos.pyd",
|
||||||
|
r"wx/_glcanvas.pyd",
|
||||||
|
r"wx/_grid.pyd",
|
||||||
|
r"wx/_html.pyd",
|
||||||
|
r"wx/_media.pyd",
|
||||||
|
r"wx/_richtext.pyd",
|
||||||
|
r"wx/_stc.pyd",
|
||||||
|
r"wx/_webkit.pyd",
|
||||||
|
r"wx/_wizard.pyd",
|
||||||
|
r"wx/_xrc.pyd",
|
||||||
|
r"wx/gdiplus.dll",
|
||||||
|
r"wx/wxbase28uh_xml_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_aui_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_gizmos_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_gizmos_xrc_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_gl_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_media_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_qa_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_richtext_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_stc_vc.dll",
|
||||||
|
r"wx/wxmsw28uh_xrc_vc.dll",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
PY_DLLS = [
|
PY_DLLS = [
|
||||||
@ -128,10 +168,51 @@ bundled packages, please refer to the corresponding file in the
|
|||||||
licenses/ directory.
|
licenses/ directory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
OPENSLIDES_RC_TMPL = """
|
||||||
|
#include <winresrc.h>
|
||||||
|
|
||||||
|
#define ID_ICO_OPENSLIDES 1
|
||||||
|
|
||||||
|
ID_ICO_OPENSLIDES ICON "openslides.ico"
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION {version[0]},{version[1]},{version[2]},{version[4]}
|
||||||
|
PRODUCTVERSION {version[0]},{version[1]},{version[2]},{version[4]}
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
FILEFLAGS {file_flags}
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904E4"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "OpenSlides team\\0"
|
||||||
|
VALUE "FileDescription", "OpenSlides\\0"
|
||||||
|
VALUE "FileVersion", "{version_str}\\0"
|
||||||
|
VALUE "InternalName", "OpenSlides\\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright \\251 2011-2013\\0"
|
||||||
|
VALUE "OriginalFilename", "openslides.exe\\0"
|
||||||
|
VALUE "ProductName", "OpenSlides\\0"
|
||||||
|
VALUE "ProductVersion", "{version_str}\\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 0x4E4
|
||||||
|
END
|
||||||
|
END
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def compile_re_list(patterns):
|
def compile_re_list(patterns):
|
||||||
expr = "|".join("(?:{0})".format(x) for x in patterns)
|
expr = "|".join("(?:{0})".format(x) for x in patterns)
|
||||||
return re.compile(expr)
|
return re.compile(expr)
|
||||||
|
|
||||||
|
|
||||||
def relpath(base, path, addslash=False):
|
def relpath(base, path, addslash=False):
|
||||||
b = os.path.normpath(os.path.abspath(base))
|
b = os.path.normpath(os.path.abspath(base))
|
||||||
p = os.path.normpath(os.path.abspath(path))
|
p = os.path.normpath(os.path.abspath(path))
|
||||||
@ -150,6 +231,7 @@ def relpath(base, path, addslash = False):
|
|||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
def filter_excluded_dirs(exclude_pattern, basedir, dirpath, dnames):
|
def filter_excluded_dirs(exclude_pattern, basedir, dirpath, dnames):
|
||||||
i, l = 0, len(dnames)
|
i, l = 0, len(dnames)
|
||||||
while i < l:
|
while i < l:
|
||||||
@ -160,6 +242,7 @@ def filter_excluded_dirs(exclude_pattern, basedir, dirpath, dnames):
|
|||||||
else:
|
else:
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
def copy_dir_exclude(exclude, basedir, srcdir, destdir):
|
def copy_dir_exclude(exclude, basedir, srcdir, destdir):
|
||||||
for dp, dnames, fnames in os.walk(srcdir):
|
for dp, dnames, fnames in os.walk(srcdir):
|
||||||
filter_excluded_dirs(exclude, basedir, dp, dnames)
|
filter_excluded_dirs(exclude, basedir, dp, dnames)
|
||||||
@ -177,22 +260,29 @@ def copy_dir_exclude(exclude, basedir, srcdir, destdir):
|
|||||||
|
|
||||||
shutil.copyfile(fp, os.path.join(destdir, rp))
|
shutil.copyfile(fp, os.path.join(destdir, rp))
|
||||||
|
|
||||||
|
|
||||||
def collect_lib(libdir, odir):
|
def collect_lib(libdir, odir):
|
||||||
exclude = compile_re_list(COMMON_EXCLUDE + LIBEXCLUDE)
|
exclude = compile_re_list(COMMON_EXCLUDE + LIBEXCLUDE)
|
||||||
copy_dir_exclude(exclude, libdir, libdir, os.path.join(odir, "Lib"))
|
copy_dir_exclude(exclude, libdir, libdir, os.path.join(odir, "Lib"))
|
||||||
|
|
||||||
|
|
||||||
def get_pkg_exclude(name, extra=()):
|
def get_pkg_exclude(name, extra=()):
|
||||||
exclude = COMMON_EXCLUDE[:]
|
exclude = COMMON_EXCLUDE[:]
|
||||||
exclude.extend(SITE_PACKAGES.get(name, {}).get("exclude", []))
|
exclude.extend(SITE_PACKAGES.get(name, {}).get("exclude", []))
|
||||||
exclude.extend(extra)
|
exclude.extend(extra)
|
||||||
return compile_re_list(exclude)
|
return compile_re_list(exclude)
|
||||||
|
|
||||||
|
|
||||||
def copy_package(name, info, odir):
|
def copy_package(name, info, odir):
|
||||||
|
copy_things = info.get("copy", [])
|
||||||
|
if not copy_things:
|
||||||
|
return
|
||||||
|
|
||||||
dist = pkg_resources.get_distribution(name)
|
dist = pkg_resources.get_distribution(name)
|
||||||
exclude = get_pkg_exclude(name)
|
exclude = get_pkg_exclude(name)
|
||||||
|
|
||||||
site_dir = dist.location
|
site_dir = dist.location
|
||||||
for thing in info.get("copy", []):
|
for thing in copy_things:
|
||||||
fp = os.path.join(site_dir, thing)
|
fp = os.path.join(site_dir, thing)
|
||||||
if not os.path.isdir(fp):
|
if not os.path.isdir(fp):
|
||||||
rp = relpath(site_dir, fp)
|
rp = relpath(site_dir, fp)
|
||||||
@ -201,6 +291,14 @@ def copy_package(name, info, odir):
|
|||||||
else:
|
else:
|
||||||
copy_dir_exclude(exclude, site_dir, fp, odir)
|
copy_dir_exclude(exclude, site_dir, fp, odir)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_wx(odir):
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(wx.__file__))
|
||||||
|
wx_dir = os.path.join(base_dir, "wx")
|
||||||
|
exclude = get_pkg_exclude("wx")
|
||||||
|
copy_dir_exclude(exclude, base_dir, wx_dir, odir)
|
||||||
|
|
||||||
|
|
||||||
def collect_site_packages(sitedir, odir):
|
def collect_site_packages(sitedir, odir):
|
||||||
if not os.path.exists(odir):
|
if not os.path.exists(odir):
|
||||||
os.makedirs(odir)
|
os.makedirs(odir)
|
||||||
@ -208,6 +306,10 @@ def collect_site_packages(sitedir, odir):
|
|||||||
for name, info in SITE_PACKAGES.iteritems():
|
for name, info in SITE_PACKAGES.iteritems():
|
||||||
copy_package(name, info, odir)
|
copy_package(name, info, odir)
|
||||||
|
|
||||||
|
assert "wx" in SITE_PACKAGES
|
||||||
|
copy_wx(odir)
|
||||||
|
|
||||||
|
|
||||||
def compile_openslides_launcher():
|
def compile_openslides_launcher():
|
||||||
try:
|
try:
|
||||||
cc = distutils.ccompiler.new_compiler()
|
cc = distutils.ccompiler.new_compiler()
|
||||||
@ -219,10 +321,34 @@ def compile_openslides_launcher():
|
|||||||
cc.add_include_dir(distutils.sysconfig.get_python_inc())
|
cc.add_include_dir(distutils.sysconfig.get_python_inc())
|
||||||
cc.add_library_dir(os.path.join(sys.exec_prefix, "Libs"))
|
cc.add_library_dir(os.path.join(sys.exec_prefix, "Libs"))
|
||||||
|
|
||||||
objs = cc.compile(["extras/win32-portable/openslides.c"])
|
gui_data_dir = os.path.dirname(openslides_gui.__file__)
|
||||||
cc.link_executable(objs, "extras/win32-portable/openslides")
|
gui_data_dir = os.path.join(gui_data_dir, "data")
|
||||||
|
shutil.copyfile(
|
||||||
|
os.path.join(gui_data_dir, "openslides.ico"),
|
||||||
|
"extras/win32-portable/openslides.ico")
|
||||||
|
rcfile = "extras/win32-portable/openslides.rc"
|
||||||
|
with open(rcfile, "w") as f:
|
||||||
|
if openslides.VERSION[3] == "final":
|
||||||
|
file_flags = "0"
|
||||||
|
else:
|
||||||
|
file_flags = "VS_FF_PRERELEASE"
|
||||||
|
|
||||||
|
f.write(OPENSLIDES_RC_TMPL.format(
|
||||||
|
version=openslides.VERSION,
|
||||||
|
version_str=openslides.get_version(),
|
||||||
|
file_flags=file_flags))
|
||||||
|
|
||||||
|
objs = cc.compile([
|
||||||
|
"extras/win32-portable/openslides.c",
|
||||||
|
rcfile,
|
||||||
|
])
|
||||||
|
cc.link_executable(
|
||||||
|
objs, "extras/win32-portable/openslides",
|
||||||
|
extra_preargs=["/subsystem:windows"],
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def copy_dlls(odir):
|
def copy_dlls(odir):
|
||||||
dll_src = os.path.join(sys.exec_prefix, "DLLs")
|
dll_src = os.path.join(sys.exec_prefix, "DLLs")
|
||||||
dll_dest = os.path.join(odir, "DLLs")
|
dll_dest = os.path.join(odir, "DLLs")
|
||||||
@ -239,6 +365,7 @@ def copy_dlls(odir):
|
|||||||
dest = os.path.join(odir, pydllname)
|
dest = os.path.join(odir, pydllname)
|
||||||
shutil.copyfile(src, dest)
|
shutil.copyfile(src, dest)
|
||||||
|
|
||||||
|
|
||||||
def copy_msvcr(odir):
|
def copy_msvcr(odir):
|
||||||
candidates = glob.glob("{0}/x86_*{1}_{2}*".format(
|
candidates = glob.glob("{0}/x86_*{1}_{2}*".format(
|
||||||
os.path.join(os.environ["WINDIR"], "winsxs"),
|
os.path.join(os.environ["WINDIR"], "winsxs"),
|
||||||
@ -253,11 +380,11 @@ def copy_msvcr(odir):
|
|||||||
msvcr_dll_dir = dp
|
msvcr_dll_dir = dp
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
sys.stderr.write("Warning could not determine msvcr runtime location\n")
|
sys.stderr.write(
|
||||||
sys.stderr.write("Private asssembly for VC runtime must be added manually\n")
|
"Warning could not determine msvcr runtime location\n"
|
||||||
|
"Private asssembly for VC runtime must be added manually\n")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
msvcr_dest_dir = os.path.join(odir, MSVCR_NAME)
|
msvcr_dest_dir = os.path.join(odir, MSVCR_NAME)
|
||||||
if not os.path.exists(msvcr_dest_dir):
|
if not os.path.exists(msvcr_dest_dir):
|
||||||
os.makedirs(msvcr_dest_dir)
|
os.makedirs(msvcr_dest_dir)
|
||||||
@ -267,7 +394,8 @@ def copy_msvcr(odir):
|
|||||||
dest = os.path.join(msvcr_dest_dir, fn)
|
dest = os.path.join(msvcr_dest_dir, fn)
|
||||||
shutil.copyfile(src, dest)
|
shutil.copyfile(src, dest)
|
||||||
|
|
||||||
src = os.path.join(os.environ["WINDIR"], "winsxs", "Manifests",
|
src = os.path.join(
|
||||||
|
os.environ["WINDIR"], "winsxs", "Manifests",
|
||||||
"{0}.manifest".format(msvcr_local_name))
|
"{0}.manifest".format(msvcr_local_name))
|
||||||
dest = os.path.join(msvcr_dest_dir, "{0}.manifest".format(MSVCR_NAME))
|
dest = os.path.join(msvcr_dest_dir, "{0}.manifest".format(MSVCR_NAME))
|
||||||
shutil.copyfile(src, dest)
|
shutil.copyfile(src, dest)
|
||||||
@ -279,9 +407,13 @@ def write_readme(orig_readme, outfile):
|
|||||||
|
|
||||||
text.extend(["\n", "\n", "Included Packages\n", 17 * "=" + "\n"])
|
text.extend(["\n", "\n", "Included Packages\n", 17 * "=" + "\n"])
|
||||||
for pkg in sorted(SITE_PACKAGES):
|
for pkg in sorted(SITE_PACKAGES):
|
||||||
|
try:
|
||||||
dist = pkg_resources.get_distribution(pkg)
|
dist = pkg_resources.get_distribution(pkg)
|
||||||
text.append("{0}-{1}\n".format(dist.project_name, dist.version))
|
text.append("{0}-{1}\n".format(dist.project_name, dist.version))
|
||||||
|
except pkg_resources.DistributionNotFound:
|
||||||
|
# FIXME: wxpython comes from an installer and has no distribution
|
||||||
|
# see what we can do about that
|
||||||
|
text.append("{0}-???\n".format(pkg))
|
||||||
|
|
||||||
with open(outfile, "w") as f:
|
with open(outfile, "w") as f:
|
||||||
f.writelines(text)
|
f.writelines(text)
|
||||||
@ -301,7 +433,7 @@ def main():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
os.makedirs(odir)
|
os.makedirs(odir)
|
||||||
out_site_packages = os.path.join(odir, "site-packages")
|
out_site_packages = os.path.join(odir, "Lib", "site-packages")
|
||||||
|
|
||||||
collect_lib(libdir, odir)
|
collect_lib(libdir, odir)
|
||||||
collect_site_packages(sitedir, out_site_packages)
|
collect_site_packages(sitedir, out_site_packages)
|
||||||
@ -312,20 +444,26 @@ def main():
|
|||||||
if not compile_openslides_launcher():
|
if not compile_openslides_launcher():
|
||||||
sys.stdout.write("Using prebuild openslides.exe\n")
|
sys.stdout.write("Using prebuild openslides.exe\n")
|
||||||
|
|
||||||
shutil.copyfile("extras/win32-portable/openslides.exe",
|
shutil.copyfile(
|
||||||
|
"extras/win32-portable/openslides.exe",
|
||||||
os.path.join(odir, "openslides.exe"))
|
os.path.join(odir, "openslides.exe"))
|
||||||
|
|
||||||
|
shutil.copytree(
|
||||||
|
"extras/openslides_gui",
|
||||||
|
os.path.join(out_site_packages, "openslides_gui"))
|
||||||
|
|
||||||
copy_dlls(odir)
|
copy_dlls(odir)
|
||||||
copy_msvcr(odir)
|
copy_msvcr(odir)
|
||||||
|
|
||||||
shutil.copytree("extras/win32-portable/licenses",
|
shutil.copytree(
|
||||||
|
"extras/win32-portable/licenses",
|
||||||
os.path.join(odir, "licenses"))
|
os.path.join(odir, "licenses"))
|
||||||
|
|
||||||
zip_fp = os.path.join("dist", "openslides-{0}-portable.zip".format(
|
zip_fp = os.path.join(
|
||||||
|
"dist", "openslides-{0}-portable.zip".format(
|
||||||
openslides.get_version()))
|
openslides.get_version()))
|
||||||
|
|
||||||
write_readme("README.txt",
|
write_readme("README.txt", os.path.join(odir, "README.txt"))
|
||||||
os.path.join(odir, "README.txt"))
|
|
||||||
|
|
||||||
with zipfile.ZipFile(zip_fp, "w", zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(zip_fp, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
for dp, dnames, fnames in os.walk(odir):
|
for dp, dnames, fnames in os.walk(odir):
|
||||||
|
@ -89,6 +89,9 @@ def process_options(argv=None, manage_runserver=False):
|
|||||||
parser.add_option(
|
parser.add_option(
|
||||||
"--syncdb", action="store_true",
|
"--syncdb", action="store_true",
|
||||||
help="Update/create database before starting the server.")
|
help="Update/create database before starting the server.")
|
||||||
|
parser.add_option(
|
||||||
|
"--backupdb", action="store", metavar="BACKUP_PATH",
|
||||||
|
help="Make a backup copy of the database to BACKUP_PATH")
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
"--reset-admin", action="store_true",
|
"--reset-admin", action="store_true",
|
||||||
help="Make sure the user 'admin' exists and uses 'admin' as password.")
|
help="Make sure the user 'admin' exists and uses 'admin' as password.")
|
||||||
@ -101,6 +104,9 @@ def process_options(argv=None, manage_runserver=False):
|
|||||||
"--no-browser",
|
"--no-browser",
|
||||||
action="store_false", dest="start_browser", default=True,
|
action="store_false", dest="start_browser", default=True,
|
||||||
help="Do not automatically start web browser.")
|
help="Do not automatically start web browser.")
|
||||||
|
parser.add_option(
|
||||||
|
"--no-run", action="store_true",
|
||||||
|
help="Do not start the development server.")
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
"--version", action="store_true",
|
"--version", action="store_true",
|
||||||
help="Show version and exit.")
|
help="Show version and exit.")
|
||||||
@ -187,6 +193,12 @@ def _main(opts, database_path=None):
|
|||||||
elif opts.reset_admin:
|
elif opts.reset_admin:
|
||||||
create_or_reset_admin_user()
|
create_or_reset_admin_user()
|
||||||
|
|
||||||
|
if opts.backupdb:
|
||||||
|
backup_database(opts.backupdb)
|
||||||
|
|
||||||
|
if opts.no_run:
|
||||||
|
return
|
||||||
|
|
||||||
# Start OpenSlides
|
# Start OpenSlides
|
||||||
reload = True
|
reload = True
|
||||||
if opts.no_reload:
|
if opts.no_reload:
|
||||||
@ -238,7 +250,7 @@ def setup_django_environment(settings_path):
|
|||||||
os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name
|
os.environ[ENVIRONMENT_VARIABLE] = '%s' % settings_module_name
|
||||||
|
|
||||||
|
|
||||||
def detect_listen_opts(address, port):
|
def detect_listen_opts(address=None, port=None):
|
||||||
if address is None:
|
if address is None:
|
||||||
try:
|
try:
|
||||||
address = socket.gethostbyname(socket.gethostname())
|
address = socket.gethostbyname(socket.gethostname())
|
||||||
@ -314,6 +326,11 @@ def create_or_reset_admin_user():
|
|||||||
admin.save()
|
admin.save()
|
||||||
|
|
||||||
|
|
||||||
|
def backup_database(dest_path):
|
||||||
|
argv = ["", "backupdb", "--destination={0}".format(dest_path)]
|
||||||
|
execute_from_command_line(argv)
|
||||||
|
|
||||||
|
|
||||||
def start_browser(url):
|
def start_browser(url):
|
||||||
browser = webbrowser.get()
|
browser = webbrowser.get()
|
||||||
|
|
||||||
@ -353,13 +370,17 @@ def get_user_data_path(*args):
|
|||||||
return os.path.join(fs2unicode(data_home), *args)
|
return os.path.join(fs2unicode(data_home), *args)
|
||||||
|
|
||||||
|
|
||||||
|
def is_portable():
|
||||||
|
exename = os.path.basename(sys.executable).lower()
|
||||||
|
return exename == "openslides.exe"
|
||||||
|
|
||||||
|
|
||||||
def get_portable_path(*args):
|
def get_portable_path(*args):
|
||||||
# NOTE: sys.executable will be the path to openslides.exe
|
# NOTE: sys.executable will be the path to openslides.exe
|
||||||
# since it is essentially a small wrapper that embeds the
|
# since it is essentially a small wrapper that embeds the
|
||||||
# python interpreter
|
# python interpreter
|
||||||
|
|
||||||
exename = os.path.basename(sys.executable).lower()
|
if not is_portable():
|
||||||
if exename != "openslides.exe":
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Cannot determine portable path when "
|
"Cannot determine portable path when "
|
||||||
"not running as portable")
|
"not running as portable")
|
||||||
@ -396,4 +417,7 @@ def win32_get_app_data_path(*args):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
if is_portable():
|
||||||
|
win32_portable_main()
|
||||||
|
else:
|
||||||
main()
|
main()
|
||||||
|
53
openslides/utils/management/commands/backupdb.py
Normal file
53
openslides/utils/management/commands/backupdb.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import shutil
|
||||||
|
from optparse import make_option
|
||||||
|
|
||||||
|
import django.conf
|
||||||
|
import django.db
|
||||||
|
import django.db.transaction
|
||||||
|
from django.core.management.base import NoArgsCommand, CommandError
|
||||||
|
|
||||||
|
|
||||||
|
class Command(NoArgsCommand):
|
||||||
|
help = "Backup the openslides database"
|
||||||
|
option_list = NoArgsCommand.option_list + (
|
||||||
|
make_option(
|
||||||
|
"--destination", action="store",
|
||||||
|
help="path to the backup database (will be overwritten)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_noargs(self, *args, **kw):
|
||||||
|
db_settings = django.conf.settings.DATABASES
|
||||||
|
default = db_settings.get(django.db.DEFAULT_DB_ALIAS)
|
||||||
|
if not default:
|
||||||
|
raise CommandError("Default databases is not configured")
|
||||||
|
|
||||||
|
if default.get("ENGINE") != "django.db.backends.sqlite3":
|
||||||
|
raise CommandError(
|
||||||
|
"Only sqlite3 databases can currently be backuped")
|
||||||
|
|
||||||
|
src_path = default.get("NAME")
|
||||||
|
if not src_path:
|
||||||
|
raise CommandError("No path specified for default database")
|
||||||
|
|
||||||
|
dest_path = kw.get("destination")
|
||||||
|
if not dest_path:
|
||||||
|
raise CommandError("--destination must be specified")
|
||||||
|
|
||||||
|
self.do_backup(src_path, dest_path)
|
||||||
|
|
||||||
|
@django.db.transaction.commit_manually
|
||||||
|
def do_backup(self, src_path, dest_path):
|
||||||
|
# perform a simple file-copy backup of the database
|
||||||
|
# first we need a shared lock on the database, issuing a select()
|
||||||
|
# will do this for us
|
||||||
|
cursor = django.db.connection.cursor()
|
||||||
|
cursor.execute("SELECT count(*) from sqlite_master")
|
||||||
|
|
||||||
|
# now copy the file
|
||||||
|
try:
|
||||||
|
shutil.copy(src_path, dest_path)
|
||||||
|
except IOError as e:
|
||||||
|
raise CommandError("{0}\nDatabase backup failed!".format(e))
|
||||||
|
|
||||||
|
# and release the lock again
|
||||||
|
django.db.transaction.commit()
|
Loading…
Reference in New Issue
Block a user