Merge pull request #698 from andkit/exp-gui

Gui frontend for openslides
This commit is contained in:
Emanuel Schütze 2013-06-03 15:52:21 -07:00
commit 50e1cac469
10 changed files with 958 additions and 48 deletions

View File

View File

@ -0,0 +1,4 @@
from .gui import main
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View 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()

View File

@ -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;
} }
Py_Initialize(); if (run_py_main)
PySys_SetArgvEx(argc, argv, 0); {
/* we where given extra arguments, behave like python.exe */
returncode = Py_Main(__argc, __argv);
}
else
{
/* no arguments given => start openslides gui */
Py_Initialize();
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

Binary file not shown.

View File

@ -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,11 +168,52 @@ 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))
if p == b: if p == b:
@ -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):
dist = pkg_resources.get_distribution(pkg) try:
text.append("{0}-{1}\n".format(dist.project_name, dist.version)) dist = pkg_resources.get_distribution(pkg)
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):

View File

@ -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__":
main() if is_portable():
win32_portable_main()
else:
main()

View 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()