From 9826e3ec06e117286d79cda142bfcb15235f5a1e Mon Sep 17 00:00:00 2001 From: Andy Kittner Date: Sat, 9 Feb 2013 20:45:58 +0100 Subject: [PATCH] Add a simple wxPython based gui frontend for openslides --- extras/openslides_gui/__init__.py | 0 extras/openslides_gui/__main__.py | 4 + .../data/openslides-logo_wide.png | Bin 0 -> 7340 bytes extras/openslides_gui/data/openslides.ico | Bin 0 -> 22382 bytes extras/openslides_gui/gui.py | 695 ++++++++++++++++++ openslides/main.py | 15 +- 6 files changed, 710 insertions(+), 4 deletions(-) create mode 100644 extras/openslides_gui/__init__.py create mode 100644 extras/openslides_gui/__main__.py create mode 100644 extras/openslides_gui/data/openslides-logo_wide.png create mode 100644 extras/openslides_gui/data/openslides.ico create mode 100644 extras/openslides_gui/gui.py diff --git a/extras/openslides_gui/__init__.py b/extras/openslides_gui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extras/openslides_gui/__main__.py b/extras/openslides_gui/__main__.py new file mode 100644 index 000000000..14932bed3 --- /dev/null +++ b/extras/openslides_gui/__main__.py @@ -0,0 +1,4 @@ +from .gui import main + +if __name__ == "__main__": + main() diff --git a/extras/openslides_gui/data/openslides-logo_wide.png b/extras/openslides_gui/data/openslides-logo_wide.png new file mode 100644 index 0000000000000000000000000000000000000000..d46f12df86aee31c6a4997862db0ebf8d31d96bb GIT binary patch literal 7340 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 B*)sqD literal 0 HcmV?d00001 diff --git a/extras/openslides_gui/data/openslides.ico b/extras/openslides_gui/data/openslides.ico new file mode 100644 index 0000000000000000000000000000000000000000..0f562a295cbe197710b0b75df32b76ba341f533d GIT binary patch literal 22382 zcmeI437nQw`^O)}+aIY=mc$exOAA7X7Wh!ULCIbd#`O{vA>-ki#6dHUWxUi>3(H> z4mjX|&PN}8^xPwlJaTrUMvXd@*SHNEHazu+BaT==zsnCg=%CxntKV|&XFKNjj}uQk z(b~6fZ%vyv{ZjqctXZ?(9((N3ZSTGJ?w^*HHe$d1_M6bSapM{AX9;cTKKtym6#5qP zx4C=mwb%IFcH3=u?b@}|x8HtyEp;{Zap<9kPH)$)o$L3QV~$z7!wx&t*Zr;RQ{#|B z4q3(eTl4144O#dZ9?XL$za4w*vDT_pE7RXtLu=Wxr5%3w;daTYGU$CzXL{rBHL6WV`2;e->c zZQHiircE0={`lkVxZ{qqWO)D&9S@o`X(ApPvLap>{jWoge%g8GorgiYlZ&l@2ixwu z@4mfR%PGkHhEq>H)lNS7WIO4klbozdX2k>Xqjl@njt9tZv2uXSn0O#r@H~L8jt7io z=!w;FLFHhj5E%#GtWHJbUp2~(<~7WBnzGgk_GWV z`f$`yM>$=PY=?Cr$^+?xcpzC255NlpH*D8kcb$bU^n?SICJ)GA17!M#jvYJNdFP#H z=bUqnb?DH+&N}NX$Ai;PKfPEUMD;;D5GIHR!h{wrTA1*lBp!gBP8P(6I(6!N1`oEW z3`t;)FL&zH$u79y0z3cw^BoV)J@;HY`|PtF4hl zJP_uAZ`s|tb+ayAx@_jbrI%i6;z3v+gp0xhkBeblh{{126C?-4*@k3s5!>`(dDAbS z6`9RdLkDuPWw~HjE_2Rh&bipLT;w+w9Lr_xa=~-gz;yks(Azq{t{&^bxZh}N!TYtW z?OODFEq33xJnsic@58=;w}ajm$|sN=00#{j*s$Y{JC<9Fpx6VM|E))l9(L7LSJ{B2tQ9L1rQ0(zP zG`S@PFdfC-iUu`{m_O$NZyE`6Sam5vzc_3VnE_hrljcriOBwUoARFZx4yhs!` z2ouBy{KLD;N`9!M6%1CI&fK@=Ag`3NQ9qIe*mIV=+;*#?h`VLd?) zmOy{avhV;oopk;6*V}d1U1#FKHP>A82RVrHz}p6oi?Wa2Hh5f=pHvbLyk2;pS+P@8 zFG{iv@|B2{9x5&U@UqLb*Iv8frkifE8*aG4@qphc^Po7};C<$@#tlW;1~73s^zT{< z9#mtUvTnKM7VFcek9i(Q7Q_Ru55fbFi(y@e%0ZL|K5p{06h<&D**cg>}Y_DyM`SNpv^U6%m8~CfO)QAF9Y;Fz*Yv>#{fBX z4Xg@~`2brGU>^f8Fu*Si@QYWFOKOk`5132eJMOr{($mxJw%cyAlH{N$+aO;lY8#UE znZt2IS!{#+2Iy~{3jM4}Up^)t+?oF|_n#WN5 zhY_jB|IndBSI(F*!=_K4Zquesvnf-iSY~FXO`bg2CQX`TpMLtOee%gCHgV!an=oO5 zee}^sHh%ng`{08QY}~kU_Wt|t+n6z9Z1m{S_TGE%*}Lz)Ywx`Cj=lZ%+xFI5Z`sI^ zBkheh-muqSf89om7-6ry_L{x=>Z|t3E3eq_;lu5vmtL|LUwqM?fBt!U?z!je*=L`% zXP$Y+h7B8LPd)XNF+UqTc(6VB(x_i6`u_#~!nR0|(lpk3RaRh92jWGi;nJ z518kFKL7l4`@h12`|i6>xw2!DrJwa$yn6L&Te)(jtyrDci> z(naa8d>wRcE!dDwe4ntPqv^h(qv>kAld$0zq|;Yl;)e|EWWokcWcUVFWcUVNWJDYD z%}}G?zWB@O;f4=ERWS7rd5`Au{k%?rF{@MW<*)ljlzw>o(_fdG)Mx(x^zYSNzK)lF z;|uN;M9Px8H0j(*vG#ox-&koT@~^vm`uZ>3xnF?W@tU%58rW0bdp{{NK4 znv;X8Nlj|!k{UJla;aUHWv+Tx_W$XppLV%S<)g^Il$!p!b?fegFZqjdqCOw0JgDSK zx~1Hw@}A0hQcGJ{*14C*`=VcVPkCL8GYJ_=g{UuvM#8xtwBlcD5;pxMax^ zmplCW>#uG8{Q0JQp>oIM?awX`8p_g#=vSUm`EBL2$=@eUe}e`Mno@&5NsWEGoyg0_ z$wRKAjO6AUd0f|$$Bq+c#IZSXVvX7U{Y`z;D+?{9c~|ty z?tT36$EMsYa+x&!+Bf2JwUTL{zZaeeQ+>`}_7y(ZgAYDv_uqfN$=|;B-h1utyYJpy zdr~b09#pzKh<@qjXPRgUTys%feArSFeC^2n$3 zS>nEPY6la1Eu<(JkX{K}gs;WPfa;8uO+V`~PI#=^k@o9pkFw-avdo@{Jg_jQxJInB zUiBc!fXDS@WI%RSzJ|Ap!Ue`&o=W|uv}+!I51E{+_10Mx(XXcZuI>rfR2L05$+q;} zH`R*2`R1F=^{`ZBK=o18l~qp$KYw9v)x-0v=QjzdZfQ1&IEPj0{(U6_)!Z zKS8$AH|>k_wZX7$PeulWH{@ChHr0XuMJ@Z_il#T(KRVYH%+6P>5}kK-X4Nr~%O&iw z)IM`zK#`4F(Kz`wiDV!vE6df^y)6jifNI#-jtyW=A%4P0#;DYF5Ay-L{DN>#I;=S} zw*q3|r1n=+D;yK950@kZ!a?>}7HZ!GJPxR3whPZ?OMI98_jPF5X6Cjg*?B2XllQb=pd=ZPABY_(1XI^> zy`m!NNwsg$ul+7sM`0Q9ThjEihp@l=hOi9C9(tcpwu5~I>$RT+A8QfMC*@bAN<&$m z7ya_r6su_+sRiVu(BHP~OZ`Cd6O{q+1^*x)oX^LmK9EA6mHMdYmtEKX7s)bqFBSS@ z?8B^q|1eo=y>#hPTfBI&_AM2%pSqBBU%@`h11puF6bBOyu@`bA^IXDyl6mY&y)(rw zV#?*PzjeCyjIoF2XKcy_t^??&QqJq8I^T+TUOd2mzm~QGZL3*xv)|2D8J~D|^=a;k zw-LT(vP$|bu4I@M=$dA>E_|)c>-t^ef3Mm4O-GsGD;@RIZ-0$#>{cD?HrK+9*ES_x z-87g;QCDLs?lWMkxyBtE2UxbhLcQ4lC{0Unt-kTT;{$*H!?g|1>+cy{YZPu=9{O?1 z#+B7@{ieCA#oBDV_S3}%*%Wu(^iLD`-H_InwvGG7m(qW>VV~(_@NP6+Y|9y>V;ePU z)PsHKw{Y#t*{nXu{w18rXsRKI>BfR7v-$cr{VwZA@gaNRzhi&=znQ~N^zC9+#cw(< zq_aZW3yhsDB>((9^HI||sqM-#HncT^?xF1GUyaZ2&im@zg3hjhn+fNjJ?T2LrL$V( z24`|^vomu@`3yJq^+;lcJe{ZD>ds#$nuE>{>8ym#%)lSVhwvP9evaJy!>RIt{LloQ z?NE&9=b$rgI?tjrzuJFH-q-OVItQKU)p;e>!=2Y+E#jPIPZ}HHZFhgpgt~+~XGDF{ z@gdP1biPh!h;(j5@vQiub5q(Mud_lrGq&fRdyYzm2guPb`0GodI}Y7(_GiZ#GY;Kx z_#aoU7uw^{9%tM*vRI(}iC+Vq*YRhXHMT#`$T}2KC%CMnIWXS6I*+D3saKI3#cy`! zEOh>kxXGQx#lLdrtaP?UGNJw8I$MT4cmEHA<`dNo&4oG4EKU~TU0ufgr_O{aKjG(~ zcwg(GJ?)YSoqvqZL3`7a(GC2rZbk8cyy7tJuh)4af4(G4cUTUQPq!~VI<92V%Rv|$ zB#+WdWN{he*GM!6=pC!G5XwnvZ>-J&==_6nL*aSpEMRGMW7C<1eG-k&Skts8%b!gM zpTW>tXb#%19OjMYp>ufRjm{DvyPL7X>#1bJpSKf#m_zGCTFLpD1dM6?q@qH${0tu(ZhWv6I_@ zCnLz2jbRV*81_+*0b|DCr=48Zxlvn~vMGl$DB71|cG|vRc?QqqQ^(0CcbQM^CZ9Fa zS@tFRyZq(oyUG3rFQ*9e@iqQ5J~G)|bxqatlr!~vr&I$L-fJJJ^6Btlda~n%p97a` zvG!}kfBIU$-~NzCz~=W*zFIj}*(v4kWShp09qa0gnvd!?*uqkcPo1uhbWgsM>Qva1 z0_xfMzQLOU<@r=QQI1!3Qu%$=_;eOd_7K~h4~FXh@k;rFP3tJy<#WK}4dmp;Q#T*& z8}@&$@=Q@4sMe~n@zZ`~Pvb&-vpOjczUc+9Z<+ezY{a-|ANY4&l8;UsQvJ>Rrz$564*ltDH-6oDsx^2XsNTewd92Gf#0L#h9V^lE_$gDB4+MvXCir_b ze6NYB{Yw6zIlxC8oZz`q-@@)5N-T37@x_E38gkOOh-Z?%ZS%czaK-)J)HJ5zptmeB0u1?5=O53%fKECZW|fVb=M$$tfN^T513>_8s=cpiH> za;9-^@Usv=c2Y#OszL&~(>}(Ek?wP5)6MmLDPY(2*6~33s zcWBajmHK_{0mF`U*feH!_}qf?2if4xKfv4>`u_)un-BIOkLSAT8P=+$>ddk~vRA69 zi$6uZPrd2@?Z3iC{=!=Q2rd5>o@)P+Y(BNB0`P5YQSa+Me#csTq>J>OJVP_pz_qWB zJ#TAR+Y)0Y 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() diff --git a/openslides/main.py b/openslides/main.py index 6dc1757f1..75306a284 100644 --- a/openslides/main.py +++ b/openslides/main.py @@ -250,7 +250,7 @@ def setup_django_environment(settings_path): 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: try: address = socket.gethostbyname(socket.gethostname()) @@ -370,13 +370,17 @@ def get_user_data_path(*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): # NOTE: sys.executable will be the path to openslides.exe # since it is essentially a small wrapper that embeds the # python interpreter - exename = os.path.basename(sys.executable).lower() - if exename != "openslides.exe": + if not is_portable(): raise Exception( "Cannot determine portable path when " "not running as portable") @@ -413,4 +417,7 @@ def win32_get_app_data_path(*args): if __name__ == "__main__": - main() + if is_portable(): + win32_portable_main() + else: + main()