From e72943142e4a1317b463bce10f9a971b3e2d2535 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 25 Mar 2010 18:47:35 -0500 Subject: [PATCH] Swithing over to the new code --- .gitignore | 1 + Makefile | 120 +++- data/high/multilist.png | Bin 1648 -> 0 bytes data/icons/26_multilist.png | Bin 0 -> 424 bytes data/icons/40_multilist.png | Bin 0 -> 1648 bytes data/icons/64_multilist.png | Bin 0 -> 804 bytes data/low/multilist.png | Bin 424 -> 0 bytes data/multilist.desktop | 11 +- data/multilist.png | Bin 424 -> 0 bytes data/multilist.service | 3 - data/scale/multilist.png | Bin 804 -> 0 bytes debian/changelog | 12 - debian/compat | 1 - debian/control | 20 - debian/copyright | 12 - debian/dirs | 2 - debian/docs | 2 - debian/files | 1 - debian/postinst | 3 - debian/rules | 93 --- remake | 6 - setup.py | 24 - src/__init__.py | 21 + src/copydb.py | 35 +- src/libbottombar.py | 166 +++++ src/libliststorehandler.py | 218 ++++++ src/libselection.py | 191 ++++++ src/libspeichern.py | 191 ++++++ src/libsync.py | 436 ++++++++++++ src/libview.py | 563 +++++++++++++++ src/multilist | 64 -- src/multilist.py | 64 ++ src/multilist_gtk.py | 401 +++++++++++ src/multilistclasses/__init__.py | 21 - src/multilistclasses/libbottombar.py | 166 ----- src/multilistclasses/libliststorehandler.py | 218 ------ src/multilistclasses/libmultilist.py | 401 ----------- src/multilistclasses/libselection.py | 191 ------ src/multilistclasses/libspeichern.py | 191 ------ src/multilistclasses/libsqldialog.py | 125 ---- src/multilistclasses/libsync.py | 436 ------------ src/multilistclasses/libview.py | 563 --------------- src/sqldialog.py | 125 ++++ src/upload.sh | 7 - support/builddeb.py | 188 +++++ support/fake_py2deb.py | 56 ++ support/py2deb.py | 991 +++++++++++++++++++++++++++ support/pylint.rc | 305 +++++++++ support/test_syntax.py | 45 ++ support/todo.py | 104 +++ www/download.html | 37 + www/index.html | 33 + www/multilist.deb | Bin 0 -> 33182 bytes 53 files changed, 4272 insertions(+), 2592 deletions(-) create mode 100644 .gitignore delete mode 100644 build-stamp delete mode 100644 data/high/multilist.png create mode 100644 data/icons/26_multilist.png create mode 100644 data/icons/40_multilist.png create mode 100644 data/icons/64_multilist.png delete mode 100644 data/low/multilist.png delete mode 100644 data/multilist.png delete mode 100644 data/multilist.service delete mode 100644 data/scale/multilist.png delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100644 debian/dirs delete mode 100644 debian/docs delete mode 100644 debian/files delete mode 100644 debian/postinst delete mode 100755 debian/rules delete mode 100755 remake delete mode 100644 setup.py create mode 100755 src/__init__.py create mode 100644 src/libbottombar.py create mode 100644 src/libliststorehandler.py create mode 100644 src/libselection.py create mode 100644 src/libspeichern.py create mode 100755 src/libsync.py create mode 100644 src/libview.py delete mode 100755 src/multilist create mode 100755 src/multilist.py create mode 100755 src/multilist_gtk.py delete mode 100755 src/multilistclasses/__init__.py delete mode 100644 src/multilistclasses/libbottombar.py delete mode 100644 src/multilistclasses/libliststorehandler.py delete mode 100755 src/multilistclasses/libmultilist.py delete mode 100644 src/multilistclasses/libselection.py delete mode 100644 src/multilistclasses/libspeichern.py delete mode 100755 src/multilistclasses/libsqldialog.py delete mode 100755 src/multilistclasses/libsync.py delete mode 100644 src/multilistclasses/libview.py create mode 100755 src/sqldialog.py delete mode 100755 src/upload.sh create mode 100755 support/builddeb.py create mode 100644 support/fake_py2deb.py create mode 100644 support/py2deb.py create mode 100644 support/pylint.rc create mode 100755 support/test_syntax.py create mode 100755 support/todo.py create mode 100644 www/download.html create mode 100644 www/index.html create mode 100644 www/multilist.deb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/Makefile b/Makefile index ab363c7..e1897c6 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,45 @@ -all: build_mo - python2.5 setup.py build -clean: - rm -rf ./locale ./po/templates.pot - python2.5 setup.py clean --all -install: build_mo - python2.5 setup.py install --root $(DESTDIR) +PROJECT_NAME=multilist +PROJECT_VERSION=0.3.1 +SOURCE_PATH=src +SOURCE=$(shell find $(SOURCE_PATH) -iname "*.py") +LOCALE_PATH=locale +LOCALE_FILES=$(shell find $(LOCALE_PATH) -iname "*.mo") +PROGRAM=$(SOURCE_PATH)/$(PROJECT_NAME).py +OBJ=$(SOURCE:.py=.pyc) +BUILD_PATH=./builddeb/ -TEXT_DOMAIN=multilist -POTFILES=src/multilist $(wildcard src/multilistclasses/*.py) +TEXT_DOMAIN=$(PROJECT_NAME) +POTFILES=$(wildcard src/quicknoteclasses/*.py) +TAG_FILE=~/.ctags/$(PROJECT_NAME).tags +TODO_FILE=./TODO + +DEBUGGER=winpdb +UNIT_TEST=nosetests --with-doctest -w . +SYNTAX_TEST=support/test_syntax.py +STYLE_TEST=../../Python/tools/pep8.py --ignore=W191,E501 +LINT_RC=./support/pylint.rc +LINT=pylint --rcfile=$(LINT_RC) +PROFILE_GEN=python -m cProfile -o .profile +PROFILE_VIEW=python -m pstats .profile +TODO_FINDER=support/todo.py +CTAGS=ctags-exuberant + +.PHONY: all run profile debug test lint tags todo clean distclean install update_po build_mo + +all: test + +run: $(OBJ) + $(PROGRAM) + +profile: $(OBJ) + $(PROFILE_GEN) $(PROGRAM) + $(PROFILE_VIEW) + +debug: $(OBJ) + $(DEBUGGER) $(PROGRAM) + +test: $(OBJ) + $(UNIT_TEST) update_po: po/templates.pot @for lang in $(basename $(notdir $(wildcard po/*.po))); do \ @@ -23,4 +55,72 @@ build_mo: msgfmt --statistics -c -o locale/$$lang/LC_MESSAGES/$(TEXT_DOMAIN).mo po/$$lang.po; \ done -.PHONES: update_po build_mo +package: $(OBJ) build_mo + rm -Rf $(BUILD_PATH) + + mkdir -p $(BUILD_PATH)/generic + cp $(SOURCE_PATH)/constants.py $(BUILD_PATH)/generic + cp $(PROGRAM) $(BUILD_PATH)/generic + $(foreach file, $(DATA), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + $(foreach file, $(SOURCE), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + #$(foreach file, $(OBJ), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + $(foreach file, $(LOCALE_FILES), cp $(file) $(BUILD_PATH)/generic/$(subst /,-,$(file)) ; ) + cp data/$(PROJECT_NAME).desktop $(BUILD_PATH)/generic + cp data/icons/26_$(PROJECT_NAME).png $(BUILD_PATH)/generic/26x26-$(PROJECT_NAME).png + cp data/icons/40_$(PROJECT_NAME).png $(BUILD_PATH)/generic/40x40-$(PROJECT_NAME).png + cp data/icons/64_$(PROJECT_NAME).png $(BUILD_PATH)/generic/scale-$(PROJECT_NAME).png + cp support/builddeb.py $(BUILD_PATH)/generic + cp support/py2deb.py $(BUILD_PATH)/generic + cp support/fake_py2deb.py $(BUILD_PATH)/generic + + mkdir -p $(BUILD_PATH)/diablo + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/diablo + cd $(BUILD_PATH)/diablo ; python builddeb.py diablo + mkdir -p $(BUILD_PATH)/fremantle + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/fremantle + cd $(BUILD_PATH)/fremantle ; python builddeb.py fremantle + mkdir -p $(BUILD_PATH)/debian + cp -R $(BUILD_PATH)/generic/* $(BUILD_PATH)/debian + cd $(BUILD_PATH)/debian ; python builddeb.py debian + +upload: + dput fremantle-extras-builder $(BUILD_PATH)/fremantle/$(PROJECT_NAME)*.changes + dput diablo-extras-builder $(BUILD_PATH)/diablo/$(PROJECT_NAME)*.changes + cp $(BUILD_PATH)/debian/*.deb www/$(PROJECT_NAME).deb + +lint: $(OBJ) + $(foreach file, $(SOURCE), $(LINT) $(file) ; ) + +tags: $(TAG_FILE) + +todo: $(TODO_FILE) + +clean: + rm -rf ./locale + rm -Rf $(OBJ) + rm -Rf $(BUILD_PATH) + rm -Rf $(TODO_FILE) + +distclean: + rm -Rf $(OBJ) + rm -Rf $(BUILD_PATH) + rm -Rf $(TAG_FILE) + find $(SOURCE_PATH) -name "*.*~" | xargs rm -f + find $(SOURCE_PATH) -name "*.swp" | xargs rm -f + find $(SOURCE_PATH) -name "*.bak" | xargs rm -f + find $(SOURCE_PATH) -name ".*.swp" | xargs rm -f + +$(TAG_FILE): $(OBJ) + mkdir -p $(dir $(TAG_FILE)) + $(CTAGS) -o $(TAG_FILE) $(SOURCE) + +$(TODO_FILE): $(SOURCE) + @- $(TODO_FINDER) $(SOURCE) > $(TODO_FILE) + +%.pyc: %.py + $(SYNTAX_TEST) $< + +#Makefile Debugging +#Target to print any variable, can be added to the dependencies of any other target +#Userfule flags for make, -d, -p, -n +print-%: ; @$(error $* is $($*) ($(value $*)) (from $(origin $*))) diff --git a/build-stamp b/build-stamp deleted file mode 100644 index e69de29..0000000 diff --git a/data/high/multilist.png b/data/high/multilist.png deleted file mode 100644 index a62085d82868c9097fdf8a32b034e24d090805c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1648 zcmV-$29NoPP)Px#24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iOG| z5;g)CL_?(j0013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z000HY zNkl5ggKi?q=^Qzxd7Xk z7-A>{mTjFw4J(cD zGfwjI?mhS1+wVK~d(J!eRfCWNfRTkIexOMc>Ui~fy;ZN*F9>A8V31%i*aoDI>%2~< zQ(h?IbUJ0g3y=`$3EJA)rhUScloYgDEdf9Hx?zn8)|$lFFQKWEb>nhhf(H_qEnt`> zp~5@}Ondo!K3SG!xuHguw<61xp{ZA@MSrqxvQ#?7SVK+h9$XX^vDb?%%VC$#=Zk3j zr4xSdfJV3IC+fhmV}AI!XfBfzb+AM$+C>idtzEP~birQR^mE&rAUhQrYvTxTeo&;NSGjc`Fm=>IR_ytSAZ+8QO2rD)Bsx>;lvy8PH|-JMg$lwokSw~ zP%z}VJXo%WDyIN3mHb0NC5ev*Q&t|-T@WcM;Ew|b_h-Uxt7se7!Qup1zX^W$2G(v2 zw|y7^V8J%wX@Hf6H~>u4skxJWOk7iCr9s*5m-mSK;so{UFeZStwbN(M%*;%5IvrfC zg<=QfIN?;W+VJZ#s5%E3*(8?KC;`*i4yA5r_!?4eP*xW;N+JSuI2;&_MzmUOn0{<* zjK01;tX8W47)>6JL474um#fEawLno841`GMZ3L+sw(gqU7JED%tX6B7PLd?dX0zH@ zGo@c&37_XHed{4{zug7}Z;6Q6k}U2IB&aBWYVZ8N>L%pd3-C{$vaOfSiuN)+l-G%E zyww8l9~PiY7Px!^UJ2!uKRTf0N_^fV6FKDZnBY{^IiX9nL1vBs(b+DxbsXHKYvL7@ z?-E<>Z4}vh-aVL+4ZEz$_LZI#?X+X?MT6p@Yi@;eRX@S{Bqa@ONfzy5M;tu}Q1hws zqetHr?LxZ%c6(9+EVL^%S_~CLRDf=`TUGiG4h}|NNeb0lWp%JURD<2=Py@KrG52D@ zYPBLslB!r39}7!ACX3>se3#(fRN0p)#v;jg7e$bnEU^RhdcCnfqT>u)xh#HQD!6=? zApiO#af*s0%k;`Ru>^^O(`Z;l8Ru2T!;Daihgv+`!>1hII%R11Qz8Fk&t+u8&OH+< zGdt0+3J0f!;>8`pX6oFTI9SekbS%X-ng;$Spwm!da8Zj#!~~TjVa88Cql}_Oi;>l; zbw1%D&9qX)2khf@3_OQKuVRpDi%g4X01uHyZKIn}8{|_&zW*$^AA$4)YL)_!$0?H8 z4uG8#v~d#?8#vBUQDw7&JWh#%lpfl+iNxRd=)^@SsboBTkl09rtPzrL>>!vjf@JE2 z;%6Zs3xf-zM*cM=nNVG()|d= z)KgrPQbVOEDi}ke7l~em8TtRt#bG5F>11O^W&oK18mQv{d*gR-_tTFX4*>?ky=Z$T zriE>!;h`EYSL1N1OJ0oKlrw?fMF~H0okp(D?nufCth_qE0K4gd8_R{aq$gOUO&C~? z#rOhPfQ^eltC@$dll=r4M9+%Y0d~_vI(8)>4iC}Y^(*PT3E4|Tn#o=gp+cR#G|`Hk zld9Z#hTP}?4{`*FUXn0lGo<0_ELA1lyY!%A=~DwUvYI5!+`~r`t*Y_k*k|UTjWk@8 zs`Q0Zo(`abj|nn}k>{g=GO>YmtP#geHggRRe)=QRpKbh1OJsn#&{a>hY32-GdMgSwOxF;Oz@GDi=MLLPiEIyf7dMBd?ixa zYqOb~Wd_@%{LL&@r+C*%vqroK7u&bCIXpC6`^oZOE}<(o2~}=*_1H6#v%oNJ>W!wP zD=NOMYp-3O(H_(8`KENv@^8C@9-ir*wTf-t%Y4UoYSt(DWUl&La(kjxCAD{-ol@6Q90OKFt8R#5Yno}cY~9A-r-9;8`{u9su+${-|MkMxuZo{ApPNwkmh(uiFEA(= NJYD@<);T3K0RSW&q;CKK literal 0 HcmV?d00001 diff --git a/data/icons/40_multilist.png b/data/icons/40_multilist.png new file mode 100644 index 0000000000000000000000000000000000000000..a62085d82868c9097fdf8a32b034e24d090805c2 GIT binary patch literal 1648 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 NJYD@<);T3K0RSW&q;CKK diff --git a/data/multilist.desktop b/data/multilist.desktop index 0613db5..900b0fb 100644 --- a/data/multilist.desktop +++ b/data/multilist.desktop @@ -1,9 +1,10 @@ [Desktop Entry] -Version=0.3.0 Encoding=UTF-8 Name=Multilist -Exec=/usr/bin/multilist -Icon=multilist +Comment=Multilist Type=Application -#X-Osso-Service=multilist -X-Osso-Type=application/x-executable +Exec=/usr/bin/run-standalone.sh /usr/bin/multilist.py +Icon=multilist +X-Window-Icon=multilist +X-Window-Icon-Dimmed=multilist +X-Osso-Type=application/x-executable diff --git a/data/multilist.png b/data/multilist.png deleted file mode 100644 index d978b6dc5baec1c15e6b9bf44fcf310d44f7a045..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI11|(N{`J4k%jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*x37`TN&n2}-D90{Nxdx@v7EBg&5R!%i$t%Y?9fkLt+t`Q~9`MJ5N zc_j?aMX8A;sVNHOnI#ztAsML(?w-B@?^9IsfC~S6x;Tb-96uX&P_Ws6XYx)ThU5=P z{}n3?+14zutJ2I9TF%FM>&{a>hY32-GdMgSwOxF;Oz@GDi=MLLPiEIyf7dMBd?ixa zYqOb~Wd_@%{LL&@r+C*%vqroK7u&bCIXpC6`^oZOE}<(o2~}=*_1H6#v%oNJ>W!wP zD=NOMYp-3O(H_(8`KENv@^8C@9-ir*wTf-t%Y4UoYSt(DWUl&La(kjxCAD{-ol@6Q90OKFt8R#5Yno}cY~9A-r-9;8`{u9su+${-|MkMxuZo{ApPNwkmh(uiFEA(= NJYD@<);T3K0RSW&q;CKK diff --git a/data/multilist.service b/data/multilist.service deleted file mode 100644 index 35e320e..0000000 --- a/data/multilist.service +++ /dev/null @@ -1,3 +0,0 @@ -[D-BUS Service] -Name=com.nokia.multilist -Exec=/usr/bin/multilist diff --git a/data/scale/multilist.png b/data/scale/multilist.png deleted file mode 100644 index 6caa276b5d9a82e98a4f96a5be797f901ba1a5f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 804 zcmV+<1Ka$GP)Px#24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iOG| z5;ZOkv3!L90013yMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HRA^-&M@dak?_?!z0007e zNklCn#fhKa zvz-(qB4~No@dY9_g#-~H0DytUhxH({Ylji>!`EgR@O(Z25!(gecDwl{utXU~g86vg zIVP?Xg>@72*A)dW+2V}*{SK$osV)OLtT*@L@c>p8AfnYvyVD=p-&!6364Ad379e9x zSPs~N^C@{jgaKE@t%Zs*SQ8-Af|IOPSWEP!szvjgq3QtC?hAq?5YrNeI@;lXDa1;{ zXRA5`LZaMZJ~nV{tukQm=L1!$fJ;$M)aULU?Zq(2N;~ZPE*JGrEXNDCa)64!`yz?@cP^aa6M1OVatj|tGL(Yw2@)U{d8q-zu9 zT$mKdCI$J>?@}T?UTUhq?_zLpLj6^owk`t+`fQ*q0zlBe>%Y&z@iP|)hI2j#YDgxG z0XWP@dHOjH02EJo=@EV;27R5RrUif?ntJ501f~W+t3Mc-f*>selU1hy)j*rn)c!?A z3JAlAyoye*3IGe#-Z}^-*7?1ZM8i&aH~`=Pi1>TuPK1&Tlw~`wbgY3e$5_iTw)6nB zMjI>E2!-ffIw$1X5n9OwYqB}A*=mcd)cmj4YaW0-?)riyQ0xJSvT-;ZbngX9kK^(9 i4!B#|h^QF`jN=FY)kK0Xr#-p=0000 Fri, 13 Mar 2009 13:42:23 +0300 - -multilist (0.3.0) unstable; urgency=low - - * Initial Release. - - -- unknown Mon, 6 Aug 2007 08:12:53 +0100 - diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b8626c4..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -4 diff --git a/debian/control b/debian/control deleted file mode 100644 index aa57b00..0000000 --- a/debian/control +++ /dev/null @@ -1,20 +0,0 @@ -Source: multilist -Section: user/other -Priority: optional -Maintainer: Christoph Würstle -Build-Depends: debhelper (>= 4.0.0), python2.5-dev -Standards-Version: 3.6.1 - -Package: multilist -Architecture: all -Depends: python2.5, python2.5-hildon, python2.5-gtk2, python2.5-osso, python2.5-dbus, python2.5-gstreamer -Description: Multilist -XB-Maemo-Icon-26: - iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAAAXNSR0IArs4c6QAAAAZiS0dEAAAA - AAAA+UO7fwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9gCBQkmAyqhfqEAAAAddEVYdENv - bW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAP9JREFUSMfNVsERgzAMk7lMAGPwYv8h - eDEGrKA+eiluEqcOBdrc5S5AkCxbmAhJ4obR4aYR4kJELiMh+QNFmv2sobPUWRtKszZGOWgGkm+z - BDrKDa4bBVjoVxa+rYNXVVcr5Kf6RFWpshJ5sOrjSVkJcDFeldiCYtQkTQWt1teYh1yHSex1nKf2 - uhLhzIuaqgae6W9B7iY7yQ4c1UzSRuQqehq9vi4QZq4De/V0U+zP+8Sag2Ao31euy7s31ucG9hkp - SQiGfZ9B0NZUFYCl4pRfuY42BSVWt5K669gDskFQqFslsFoGgpk2pq6xU+VRF1q+n6jwpTRZ/9dx - 6+pz5APOkH7tCcRtTQAAAABJRU5ErkJggg== diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 3c80733..0000000 --- a/debian/copyright +++ /dev/null @@ -1,12 +0,0 @@ -This package was debianized by Christoph Würstle on -Mon, 22 Jan 2007 22:44:33 +0200. - -It was downloaded from - -Copyright: - -Upstream Author(s): - -License: - -Special drop a mail diff --git a/debian/dirs b/debian/dirs deleted file mode 100644 index ca882bb..0000000 --- a/debian/dirs +++ /dev/null @@ -1,2 +0,0 @@ -usr/bin -usr/sbin diff --git a/debian/docs b/debian/docs deleted file mode 100644 index 724e084..0000000 --- a/debian/docs +++ /dev/null @@ -1,2 +0,0 @@ -README -TODO diff --git a/debian/files b/debian/files deleted file mode 100644 index 4e73ea0..0000000 --- a/debian/files +++ /dev/null @@ -1 +0,0 @@ -multilist_0.3.1_all.deb user/other optional diff --git a/debian/postinst b/debian/postinst deleted file mode 100644 index 1df47d1..0000000 --- a/debian/postinst +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -e -gtk-update-icon-cache -f /usr/share/icons/hicolor -exit 0 \ No newline at end of file diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 185e870..0000000 --- a/debian/rules +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - - - - -ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS))) - INSTALL_PROGRAM += -s -endif - -configure: configure-stamp -configure-stamp: - dh_testdir - # Add here commands to configure the package. - - touch configure-stamp - - -build: build-stamp - -build-stamp: configure-stamp - dh_testdir - - # Add here commands to compile the package. - $(MAKE) - - touch build-stamp - -clean: - dh_testdir - dh_testroot - rm -f build-stamp configure-stamp - - # Add here commands to clean up after the build process. - -$(MAKE) clean - - dh_clean - -install: build - dh_testdir - dh_testroot - dh_clean -k - dh_installdirs - - # Add here commands to install the package into debian/multilist - $(MAKE) install DESTDIR=$(CURDIR)/debian/multilist - - -# Build architecture-independent files here. -binary-indep: build install -# We have nothing to do by default. - -# Build architecture-dependent files here. -binary-arch: build install - dh_testdir - dh_testroot - dh_installchangelogs -# dh_installdocs - dh_installexamples -# dh_install -# dh_installmenu -# dh_installdebconf -# dh_installlogrotate -# dh_installemacsen -# dh_installpam -# dh_installmime -# dh_installinit -# dh_installcron -# dh_installinfo - dh_installman - dh_link - dh_strip - dh_compress - dh_fixperms -# dh_perl -# dh_python -# dh_makeshlibs - dh_installdeb - dh_shlibdeps - dh_gencontrol - dh_md5sums - dh_builddeb - -binary: binary-indep binary-arch -.PHONY: build clean binary-indep binary-arch binary install configure diff --git a/remake b/remake deleted file mode 100755 index 67a93f3..0000000 --- a/remake +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -make clean -make -dpkg-buildpackage -rfakeroot -dpkg -i ../multilist_0.3.1_all.deb diff --git a/setup.py b/setup.py deleted file mode 100644 index b40d031..0000000 --- a/setup.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python2.5 -# -*- coding: utf-8 -*- -# -# - -from distutils.core import setup - -setup(name='multilist', - version='0.3.1', - scripts=['src/multilist'], - packages=['multilistclasses'], - package_dir={'multilistclasses': 'src/multilistclasses'}, - data_files = [ - ('share/icons/hicolor/26x26/hildon', ['data/low/multilist.png']), - ('share/icons/hicolor/40x40/hildon', ['data/high/multilist.png']), - ('share/icons/hicolor/scalable/hildon', ['data/scale/multilist.png']), - #('share/pixmaps', ['data/multilist.png']), - ('share/applications/hildon', ['data/multilist.desktop']), - ('share/dbus-1/services', ['data/multilist.service']), - # I18N - ('share/locale/de/LC_MESSAGES', ['locale/de/LC_MESSAGES/multilist.mo']), - ('share/locale/ru/LC_MESSAGES', ['locale/ru/LC_MESSAGES/multilist.mo']), - ] - ) diff --git a/src/__init__.py b/src/__init__.py new file mode 100755 index 0000000..105eaaf --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" diff --git a/src/copydb.py b/src/copydb.py index e50f303..b489344 100755 --- a/src/copydb.py +++ b/src/copydb.py @@ -3,25 +3,30 @@ import time import sqlite3 -import shelve import uuid -db="/home/chris/Documents/Schule/Schule/schulplaner/schuljahr200708enni.s3db" -conn = sqlite3.connect(db) -cur = conn.cursor() -sql="UPDATE sync SET syncpartner=? WHERE syncpartner=?" -cur.execute(sql,(str(uuid.uuid4()),"self")) #Eigene Id ändern feststellen -conn.commit() +def copydb(db): + conn = sqlite3.connect(db) + cur = conn.cursor() -sql="DELETE FROM sync WHERE syncpartner=?" -cur.execute(sql,("self",)) -conn.commit() + sql = "UPDATE sync SET syncpartner=? WHERE syncpartner=?" + cur.execute(sql, (str(uuid.uuid4()), "self")) #Eigene Id ändern feststellen + conn.commit() -sql="INSERT INTO sync (syncpartner,uuid,pcdatum) VALUES (?,?,?)" -cur.execute(sql,("self",str(uuid.uuid4()),int(time.time()))) + sql = "DELETE FROM sync WHERE syncpartner=?" + cur.execute(sql, ("self", )) + conn.commit() -sql="UPDATE sync SET pcdatum=?" -cur.execute(sql,(int(time.time()),)) + sql = "INSERT INTO sync (syncpartner,uuid,pcdatum) VALUES (?,?,?)" + cur.execute(sql, ("self", str(uuid.uuid4()), int(time.time()))) -conn.commit() \ No newline at end of file + sql = "UPDATE sync SET pcdatum=?" + cur.execute(sql, (int(time.time()), )) + + conn.commit() + + +if __name__ == "__main__": + dbPath = "/home/chris/Documents/Schule/Schule/schulplaner/schuljahr200708enni.s3db" + copydb(dbPath) diff --git a/src/libbottombar.py b/src/libbottombar.py new file mode 100644 index 0000000..654c72d --- /dev/null +++ b/src/libbottombar.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + + +import gobject +import time +import logging + +import gtk + +class Bottombar(gtk.HBox): + + __gsignals__ = { + 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)), + #'changedCategory': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)) + } + + + def new_item(self,widget=None,data1=None,data2=None): + dialog = gtk.Dialog(_("New item name:"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + dialog.set_position(gtk.WIN_POS_CENTER) + entryKlasse=gtk.Entry() + entryKlasse.set_text("") + + dialog.vbox.pack_start(entryKlasse, True, True, 0) + + dialog.vbox.show_all() + #dialog.set_size_request(400,300) + + if dialog.run() == gtk.RESPONSE_ACCEPT: + #logging.info("new category name "+entryKlasse.get_text()) + #self.view.liststorehandler.rename_category(entryKlasse.get_text()) + self.view.liststorehandler.add_row(entryKlasse.get_text()) + dialog.destroy() + + + def del_item(self,widget=None,data1=None,data2=None): + path, col = self.view.treeview.get_cursor() + if path!=None: + mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_QUESTION,gtk.BUTTONS_YES_NO,_("Delete current item?")) + response=mbox.run() + mbox.hide() + mbox.destroy() + if response==gtk.RESPONSE_YES: + self.view.del_active_row() + else: + mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_ERROR,gtk.BUTTONS_OK,_("No item selected!")) + response=mbox.run() + mbox.hide() + mbox.destroy() + + + def checkout_items(self,widget=None,data1=None,data2=None): + #self.view.del_active_row() + mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_QUESTION,gtk.BUTTONS_YES_NO,(_("Really checkout all items?"))) + response=mbox.run() + mbox.hide() + mbox.destroy() + if response==gtk.RESPONSE_YES: + self.view.liststorehandler.checkout_rows() + #n=len(self.view.liststorehandler.get_liststore()) + #for i in range(n): + # self.view.liststorehandler.checkout_rows() + # #print i + + def search_list(self,widget=None,data1=None,data2=None): + self.view.liststorehandler.get_liststore(widget.get_text()) + + + def rename_category(self,widget=None,data1=None,data2=None): + dialog = gtk.Dialog(_("New category name:"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + dialog.set_position(gtk.WIN_POS_CENTER) + entryKlasse=gtk.Entry() + entryKlasse.set_text(self.view.liststorehandler.selection.get_category()) + + dialog.vbox.pack_start(entryKlasse, True, True, 0) + + dialog.vbox.show_all() + #dialog.set_size_request(400,300) + + if dialog.run() == gtk.RESPONSE_ACCEPT: + logging.info("new category name "+entryKlasse.get_text()) + self.view.liststorehandler.rename_category(entryKlasse.get_text()) + else: + #print "Cancel",res + pass + dialog.destroy() + + + def rename_list(self,widget=None,data1=None,data2=None): + dialog = gtk.Dialog(_("New list name:"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + dialog.set_position(gtk.WIN_POS_CENTER) + entryKlasse=gtk.Entry() + entryKlasse.set_text(self.view.liststorehandler.selection.get_list()) + + dialog.vbox.pack_start(entryKlasse, True, True, 0) + + dialog.vbox.show_all() + #dialog.set_size_request(400,300) + + if dialog.run() == gtk.RESPONSE_ACCEPT: + logging.info("new list name "+entryKlasse.get_text()) + self.view.liststorehandler.rename_list(entryKlasse.get_text()) + else: + #print "Cancel",res + pass + dialog.destroy() + + def __init__(self,db,view,isHildon): + gtk.HBox.__init__(self,homogeneous=False, spacing=3) + + self.db=db + self.isHildon=isHildon + self.view=view + + logging.info("libBottomBar, init") + + + button=gtk.Button(_("New item")) + button.connect("clicked",self.new_item) + self.pack_start(button, expand=False, fill=True, padding=0) + + label=gtk.Label(" ") + self.pack_start(label, expand=True, fill=True, padding=0) + + label=gtk.Label(_("Search:")) + self.pack_start(label, expand=False, fill=True, padding=0) + searchEntry=gtk.Entry() + searchEntry.connect("changed",self.search_list) + self.pack_start(searchEntry, expand=True, fill=True, padding=0) + + label=gtk.Label(" ") + self.pack_start(label, expand=True, fill=True, padding=0) + + button=gtk.Button(_("Checkout all items")) + button.connect("clicked",self.checkout_items) + self.pack_start(button, expand=False, fill=True, padding=0) + + button=gtk.Button(_("Del item")) + button.connect("clicked",self.del_item) + self.pack_start(button, expand=False, fill=True, padding=0) + + + diff --git a/src/libliststorehandler.py b/src/libliststorehandler.py new file mode 100644 index 0000000..933a90c --- /dev/null +++ b/src/libliststorehandler.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + +import gtk +import gobject +import libspeichern +import logging + + +class Liststorehandler(): + + def get_unitsstore(self): + if (self.unitsstore==None): + self.unitsstore=gtk.ListStore(str, str, str,str,str, str,str, str, str,str, str, str,str) + self.unitsstore.clear() + #row(3) quantities + #row 4 units + #row 6 priority + self.unitsstore.append(["-1","-1","","","","","","","","","","",""]) + self.unitsstore.append(["-1","-1","","1","g","","0","","","","","",""]) + self.unitsstore.append(["-1","-1","","2","kg","","1","","","","","",""]) + self.unitsstore.append(["-1","-1","","3","liter","","2","","","","","",""]) + self.unitsstore.append(["-1","-1","","4","packs","","3","","","","","",""]) + self.unitsstore.append(["-1","-1","","5","","","4","","","","","",""]) + self.unitsstore.append(["-1","-1","","6","","","5","","","","","",""]) + self.unitsstore.append(["-1","-1","","7","","","6","","","","","",""]) + self.unitsstore.append(["-1","-1","","8","","","7","","","","","",""]) + self.unitsstore.append(["-1","-1","","9","","","8","","","","","",""]) + self.unitsstore.append(["-1","-1","","","","","9","","","","","",""]) + + return self.unitsstore + + + + + def get_liststore(self,titlesearch=""): + if (self.liststore==None): + self.liststore=gtk.ListStore(str, str, str,str,str, str,str, str, str,str, str, str,str) + self.liststore.clear() + + titlesearch=titlesearch+"%" + + + if (self.selection.get_status()=="0"): #only 0 and 1 (active items) + sql="SELECT uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2 FROM items WHERE list=? AND category LIKE ? AND status>=? AND title like ? ORDER BY category, status, title" + rows=self.db.ladeSQL(sql,(self.selection.get_list(),self.selection.get_category(True),self.selection.get_status(),titlesearch)) + else: + sql="SELECT uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2 FROM items WHERE list=? AND category LIKE ? AND title LIKE ? ORDER BY category, title ASC" + rows=self.db.ladeSQL(sql,(self.selection.get_list(),self.selection.get_category(True),titlesearch)) + + #print rows + if ((rows!=None)and(len(rows)>0)): + for row in rows: + uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2 = row + if unit==None: + pass + #unit="" + self.liststore.append([uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2]) + #else: + #self.liststore.append(["-1","-1",""," ","","","","","","","","",""]) + #import uuid + #self.liststore.append(["-1","-1","","","","","","","","","","",""]) + + return self.liststore + + + def emptyValueExists(self): + for child in self.liststore: + if child[2]=="": + return True + return False + + + + def update_row(self,irow,icol,new_text): + #print "liststore 1" + #for x in self.liststore: + # print x[0],x[2] + + if (irow>-1)and(self.liststore[irow][0]!="-1")and(self.liststore[irow][0]!=None): + sql = "UPDATE items SET "+self.collist[icol]+"=? WHERE uid=?" + self.db.speichereSQL(sql,(new_text,self.liststore[irow][0]),rowid=self.liststore[irow][0]) + + logging.info("Updated row: "+self.collist[icol]+" new text "+new_text+" Titel: "+str(self.liststore[irow][2])+" with uid "+str(self.liststore[irow][0])) + + self.liststore[irow][icol]=new_text + else: + logging.warning("update_row: row does not exist") + return + #if (self.emptyValueExists()==True)and(icol<2): + # #print "letzter Eintrag ohne inhalt" + # return + + #print "liststore 2" + #for x in self.liststore: + # print x[0],x[2] + + + def checkout_rows(self): + sql = "UPDATE items SET status=? WHERE list=? AND category LIKE ? AND status=?" + self.db.speichereSQL(sql,("-1",self.selection.get_list(),self.selection.get_category(True),"1")) + for i in range(len(self.liststore)): + if self.liststore[i][1]=="1": + self.liststore[i][1]="-1" + + + + + def add_row(self,title=""): + #self.update_row(-1,1,"-1") + #for x in self.liststore: + # print x[0],x[2] + status=self.selection.get_status() + import uuid + uid=str(uuid.uuid4()) + sql = "INSERT INTO items (uid,list,category,status, title) VALUES (?,?,?,?,?)" + self.db.speichereSQL(sql,(uid,self.selection.get_list(),self.selection.get_category(),status,title),rowid=uid) + logging.info("Insertet row: status = "+status+" with uid "+str(uid)) + #self.liststore[irow][0]=str(uuid.uuid4()) + + self.liststore.append([uid,status,title," ","","","","","","","","",""]) + self.selection.comboLists_check_for_update() + # if (irow>-1): + # self.liststore[irow][icol]=new_text + # self.liststore[irow][0]=uid + # else: + # self.liststore.append([uid,"-1",""," ","","","","","","","","",""]) + #print "xy",self.liststore[len(self.liststore)-1][0] + #Check if a new list/category is opened + # self.selection.comboLists_check_for_update() + + def del_row(self,irow,row_iter): + uid=self.liststore[irow][0] + self.liststore.remove(row_iter) + sql = "DELETE FROM items WHERE uid=?" + self.db.speichereSQL(sql,(uid,)) + # + + + def get_colname(self,i): + if i. + + Copyright (C) 2008 Christoph Würstle +""" + + +import gobject +import time +import logging + +import gtk + +class Selection(gtk.HBox): + + __gsignals__ = { + 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)), + #'changedCategory': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)) + } + + def load(self): + model=self.comboList.get_model() + model.clear() + #self.comboList.remove(0) + + + sql="SELECT DISTINCT list FROM items ORDER BY list" + rows=self.db.ladeSQL(sql) + if ((rows!=None)and(len(rows)>0)): + for row in rows: + self.comboList.append_text(row[0]) + else: + self.comboList.append_text("default") + + s=self.db.ladeDirekt("comboListText") + if s!="": + self.comboList.get_child().set_text(s) + else: + self.comboList.set_active(0) + + def comboList_changed(self, widget=None, data=None): + #self.comboCategory.set_model(None) + #print "reload categories" + while len(self.comboCategory.get_model())>0: + self.comboCategory.remove_text(0) + + sql="SELECT DISTINCT category FROM items WHERE list=? ORDER BY category" + rows=self.db.ladeSQL(sql,(self.get_list(),)) + + self.comboCategory.append_text(_("all")) + if ((rows!=None)and(len(rows)>0)): + for row in rows: + if (row[0]!=_("all")): + self.comboCategory.append_text(row[0]) + + s=self.db.ladeDirekt("comboCategoryText"+self.comboList.get_child().get_text()) + if len(s)>0: + self.comboCategory.get_child().set_text(s) + else: + self.comboCategory.set_active(0) + + self.emit("changed","list","") + self.db.speichereDirekt("comboListText",self.comboList.get_child().get_text()) + + + + def comboCategory_changed(self, widget=None, data=None): + #logging.info("Klasse geaendert zu ") + #self.hauptRegister.set_current_page(0) + self.emit("changed","category","") + if self.comboCategory.get_active()>-1: + self.db.speichereDirekt("comboCategoryText"+self.comboList.get_child().get_text(),self.comboCategory.get_child().get_text()) + + def radioActive_changed(self, widget, data=None): + self.emit("changed","radio","") + + def comboLists_check_for_update(self): + if self.comboCategory.get_active()==-1: + model=self.comboCategory.get_model() + found=False + cat=self.get_category() + for x in model: + if x[0]==cat: + found=True + if found==False: + self.comboCategory.append_text(self.get_category()) + self.comboCategory.set_active(len(self.comboCategory.get_model())-1) + if self.comboList.get_active()==-1: + model=self.comboList.get_model() + found=False + list=self.get_list() + for x in model: + if x[0]==list: + found=True + if found==False: + self.comboList.append_text(self.get_list()) + self.comboList.set_active(len(self.comboList.get_model())-1) + + + def lade(self): + logging.warning("Laden der aktuellen position noch nicht implementiert") + + + def speichere(self): + logging.warning("Speichern der aktuellen position noch nicht implementiert") + + + def getIsHildon(self): + return self.isHildon + + def get_category(self,select=False): + s=self.comboCategory.get_child().get_text() + if s==_("all"): + if (select==False): + return "undefined" + else: + return "%" + else: + return s + def set_category(self,category): + self.comboCategory.get_child().set_text(category) + + def set_list(self,listname): + self.comboList.get_child().set_text(listname) + + def get_list(self): + return self.comboList.get_child().get_text() + + + + def get_status(self): + #return self.comboCategory.get_child().get_text() + if self.radio_all.get_active()==True: + return "-1" + else: + return "0" + + + def __init__(self,db,isHildon): + gtk.HBox.__init__(self,homogeneous=False, spacing=3) + + self.db=db + self.isHildon=isHildon + + logging.info("libSelection, init") + + + label=gtk.Label(_("List:")) + self.pack_start(label, expand=False, fill=True, padding=0) + + self.comboList = gtk.combo_box_entry_new_text() + self.comboList.set_size_request(180,-1) + self.pack_start(self.comboList, expand=False, fill=True, padding=0) + + label=gtk.Label(_(" Category:")) + self.pack_start(label, expand=False, fill=True, padding=0) + + self.comboCategory = gtk.combo_box_entry_new_text() + self.comboCategory.set_size_request(180,-1) + self.pack_start(self.comboCategory, expand=False, fill=True, padding=0) + + self.comboList.connect("changed", self.comboList_changed, None) + self.comboCategory.connect("changed", self.comboCategory_changed, None) + + label=gtk.Label(_(" View:")) + self.pack_start(label, expand=False, fill=True, padding=0) + + self.radio_all=gtk.RadioButton(group=None, label=_("All"), use_underline=True) + self.pack_start(self.radio_all, expand=False, fill=True, padding=0) + self.radio_active=gtk.RadioButton(group=self.radio_all, label=_("Active"), use_underline=True) + self.pack_start(self.radio_active, expand=False, fill=True, padding=0) + self.radio_all.connect("toggled",self.radioActive_changed, None) + + + diff --git a/src/libspeichern.py b/src/libspeichern.py new file mode 100644 index 0000000..e887340 --- /dev/null +++ b/src/libspeichern.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + +import time +import sqlite3 +import shelve +import sys +import string +import shutil +import os +import logging + +class Speichern(): + def speichereDirekt(self,schluessel,daten): + self.d[schluessel]=daten + logging.info("speichereDirekt "+str(schluessel)+" "+str(daten)+" lesen: "+str(self.d[schluessel])) + + + def ladeDirekt(self,schluessel,default=""): + #print "ladeDirekt",schluessel, "Schluessel vorhanden",self.d.has_key(schluessel) + if (self.d.has_key(schluessel)==True): + data=self.d[schluessel] + #print data + return data + else: + return default + + + def speichereSQL(self,sql,tupel=None,commit=True,host="self",log=True,pcdatum=None,rowid=""): + #print "speichereSQL:",sql,tupel + try: + programSQLError=True + if (tupel==None): + self.cur.execute(sql) + else: + self.cur.execute(sql,tupel) + programSQLError=False + + #print int(time.time()), sql, pickle.dumps(tupel), host + if (log==True): + strtupel=[] + if (tupel!=None): + for t in tupel: + strtupel.append(str(t)) + + + if pcdatum==None: pcdatum=int(time.time()) + self.cur.execute("INSERT INTO logtable ( pcdatum,sql,param,host,rowid ) VALUES (?,?,?,?,?)",(pcdatum, sql, string.join(strtupel," <> "), host,str(rowid) )) + if (commit==True): self.conn.commit() + + return True + except: + s=str(sys.exc_info()) + if (s.find(" already exists")==-1): + #if len(s)>0: + if (programSQLError==True): + logging.error("speichereSQL-Exception "+str(sys.exc_info())+" "+str(sql)+" "+str(tupel)) + else: + logging.error("speichereSQL-Exception in Logging!!!! :"+str(sys.exc_info())+" "+str(sql)+" "+str(tupel)) + return False + + def commitSQL(self): + self.conn.commit() + + + def ladeSQL(self,sql,tupel=None): + #print sql,tupel + try: + if (tupel==None): + self.cur.execute(sql) + else: + self.cur.execute(sql,tupel) + return self.cur.fetchall() + except: + logging.error("ladeSQL-Exception "+str(sys.exc_info())+" "+str(sql)+" "+str(tupel)) + return () + + def ladeHistory(self,sql_condition,param_condition): + sql="SELECT * FROM logtable WHERE sql LIKE '%"+str(sql_condition)+"%' AND param LIKE '%"+str(param_condition)+"%'" + rows=self.ladeSQL(sql) + #print rows + i=0 + erg=[] + while i> ")]) + #pcdatum #datum #sql # Param_org #param + + i+=1 + + return erg + + def delHistory(self,sql_condition,param_condition,exceptTheLastXSeconds=0): + pcdatum=int(time.time())-exceptTheLastXSeconds + sql="DELETE FROM logtable WHERE sql LIKE '%"+str(sql_condition)+"%' AND param LIKE '%"+str(param_condition)+"%' AND pcdatum. +""" + +import gobject +import time +import string +from SimpleXMLRPCServer import SimpleXMLRPCServer +import random +import socket +socket.setdefaulttimeout(60) # Timeout auf 60 sec. setzen +import xmlrpclib +import select +#import fcntl +import struct +import gtk +import uuid +import sys +import logging + +import libspeichern + + +class ProgressDialog(gtk.Dialog): + + def pulse(self): + #self.progressbar.pulse() + pass + + def __init__(self,title=_("Sync process"), parent=None): + gtk.Dialog.__init__(self,title,parent,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,()) + + logging.info("ProgressDialog, init") + + label=gtk.Label(_("Sync process running...please wait")) + self.vbox.pack_start(label, True, True, 0) + label=gtk.Label(_("(this can take some minutes)")) + self.vbox.pack_start(label, True, True, 0) + + #self.progressbar=gtk.ProgressBar() + #self.vbox.pack_start(self.progressbar, True, True, 0) + + #self.set_keep_above(True) + self.vbox.show_all() + self.show() + +class Sync(gtk.VBox): + + __gsignals__ = { + 'syncFinished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,)), + 'syncBeforeStart' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,)), + } + + def changeSyncStatus(self,active,title): + self.syncStatusLabel.set_text(title) + if active==True: + if self.progress==None: + self.progress=ProgressDialog(parent=self.parentwindow) + self.emit("syncBeforeStart","syncBeforeStart") + + + else: + if self.progress!=None: + self.progress.hide() + self.progress.destroy() + self.progress=None + self.emit("syncFinished","syncFinished") + + def pulse(self): + if self.progress!=None: + self.progress.pulse() + #if self.server!=None: + # self.server.pulse() + + + def getUeberblickBox(self): + frame=gtk.Frame(_("Query")) + return frame + + def handleRPC(self): + try: + if (self.rpcserver==None): return False + except: + return False + + while (len(self.poll.poll(0))>0): + self.rpcserver.handle_request() + return True + + def get_ip_address(self,ifname): + return socket.gethostbyname(socket.gethostname()) + #try: + # s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # ip=socket.inet_ntoa(fcntl.ioctl(s.fileno(),0x8915,struct.pack('256s', ifname[:15]))[20:24]) + # s.close() + #except: + # ip=socket.gethostbyname(socket.gethostname()) + # s.close() + + #return ip FixME + + def getLastSyncDate(self,sync_uuid): + sql="SELECT syncpartner,pcdatum FROM sync WHERE uuid=?" + rows=self.db.ladeSQL(sql,(sync_uuid,)) + if (rows!=None)and(len(rows)==1): + syncpartner,pcdatum = rows[0] + else: + pcdatum=-1 + logging.info("LastSyncDatum: "+str(pcdatum)+" Jetzt "+str(int(time.time()))) + return pcdatum + + + def check4commit(self,newSQL,lastdate): + logging.info("check4commit 1") + if self.concernedRows==None: + logging.info("check4commit Updatung concernedRows") + sql="SELECT pcdatum,rowid FROM logtable WHERE pcdatum>? ORDER BY pcdatum DESC" + self.concernedRows=self.db.ladeSQL(sql,(lastdate,)) + + + if (self.concernedRows!=None)and(len(self.concernedRows)>0): + #logging.info("check4commit 2") + id1, pcdatum,sql, param, host, rowid = newSQL + + if len(rowid)>0: + for x in self.concernedRows: + #logging.info("check4commit 3") + if (x[1]==rowid): + if (x[0]>pcdatum): + logging.info("newer sync entry, ignoring old one") + #logging.info("check4commit 9.1") + return False + else: + #logging.info("check4commit 9.2") + return True + + #logging.info("check4commit 9.3") + return True + + def writeSQLTupel(self,newSQLs,lastdate): + if (newSQLs==None): + return + + self.concernedRows=None + pausenzaehler=0 + logging.info("writeSQLTupel got "+str(len(newSQLs))+" sql tupels") + for newSQL in newSQLs: + #print "" + #print "SQL1: ",newSQL[1] + #print "SQL2: ",newSQL[2] + #print "SQL3: ",newSQL[3] + #print "Param:",string.split(newSQL[3]," <> ") + #print "" + if (newSQL[3]!=""): + param=string.split(newSQL[3]," <> ") + else: + param=None + + if (len(newSQL)>2): + commitSQL=True + + if (newSQL[5]!=None)and(len(newSQL[5])>0): + commitSQL=self.check4commit(newSQL,lastdate) + + if (commitSQL==True): + self.db.speichereSQL(newSQL[2],param,commit=False,pcdatum=newSQL[1],rowid=newSQL[5]) + else: + logging.error("writeSQLTupel: Error") + + pausenzaehler+=1 + if (pausenzaehler % 10)==0: + self.pulse() + while (gtk.events_pending()): + gtk.main_iteration(); + + logging.info("Alle SQLs an sqlite geschickt, commiting now") + self.db.commitSQL() + logging.info("Alle SQLs commited") + + + def doSync(self,sync_uuid,pcdatum,newSQLs,pcdatumjetzt): + #print uuid,pcdatum,newSQLs + #logging.info("doSync 0") + self.changeSyncStatus(True,_("sync process running")) + self.pulse() + #logging.info("doSync 1") + + while (gtk.events_pending()): + gtk.main_iteration(); + diff=time.time()-pcdatumjetzt + if diff<0: + diff=diff*(-1) + if diff>30: + return -1 + + logging.info("doSync read sqls") + sql="SELECT * FROM logtable WHERE pcdatum>?" + rows=self.db.ladeSQL(sql,(pcdatum,)) + logging.info("doSync read sqls") + self.writeSQLTupel(newSQLs,pcdatum) + logging.info("doSync wrote "+str(len(newSQLs))+" sqls") + logging.info("doSync sending "+str(len(rows))+" sqls") + i=0 + return rows + + def getRemoteSyncUUID(self): + return self.sync_uuid + + + def startServer(self, widget, data=None): + #Starte RPCServer + self.db.speichereDirekt("syncServerIP",self.comboIP.get_child().get_text()) + + if (widget.get_active()==True): + logging.info("Starting Server") + + try: + ip=self.comboIP.get_child().get_text() + self.rpcserver = SimpleXMLRPCServer((ip, self.port),allow_none=True) + self.rpcserver.register_function(pow) + self.rpcserver.register_function(self.getLastSyncDate) + self.rpcserver.register_function(self.doSync) + self.rpcserver.register_function(self.getRemoteSyncUUID) + self.rpcserver.register_function(self.doSaveFinalTime) + self.rpcserver.register_function(self.pulse) + self.poll=select.poll() + self.poll.register(self.rpcserver.fileno()) + gobject.timeout_add(1000, self.handleRPC) + self.syncServerStatusLabel.set_text(_("Syncserver running...")) + + #save + self.db.speichereDirekt("startSyncServer",True) + + except: + s=str(sys.exc_info()) + logging.error("libsync: could not start server. Error: "+s) + mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_ERROR,gtk.BUTTONS_OK,_("Sync server could not start. Please check IP and port.")) #gtk.DIALOG_MODAL + mbox.set_modal(False) + response=mbox.run() + mbox.hide() + mbox.destroy() + widget.set_active(False) + + else: + logging.info("Stopping Server") + try: + del self.rpcserver + except: + pass + self.syncServerStatusLabel.set_text(_("Syncserver not running...")) + #save + self.db.speichereDirekt("startSyncServer",False) + + def doSaveFinalTime(self,sync_uuid,pcdatum=None): + if (pcdatum==None): pcdatum=int(time.time()) + if (time.time()>pcdatum): + pcdatum=int(time.time()) #größere Zeit nehmen + + self.pulse() + + #fime save time+uuid + sql="DELETE FROM sync WHERE uuid=?" + self.db.speichereSQL(sql,(sync_uuid,),log=False) + sql="INSERT INTO sync (syncpartner,uuid,pcdatum) VALUES (?,?,?)" + self.db.speichereSQL(sql,("x",str(sync_uuid),pcdatum),log=False) + self.pulse() + self.changeSyncStatus(False,_("no sync process (at the moment)")) + return (self.sync_uuid,pcdatum) + + + def syncButton(self, widget, data=None): + logging.info("Syncing") + #sql="DELETE FROM logtable WHERE sql LIKE externeStundenplanung" + #self.db.speichereSQL(sql) + + self.changeSyncStatus(True,_("sync process running")) + while (gtk.events_pending()): + gtk.main_iteration(); + + self.db.speichereDirekt("syncRemoteIP",self.comboRemoteIP.get_child().get_text()) + try: + self.server = xmlrpclib.ServerProxy("http://"+self.comboRemoteIP.get_child().get_text()+":"+str(self.port),allow_none=True) + #lastDate=server.getLastSyncDate(str(self.sync_uuid)) + server_sync_uuid=self.server.getRemoteSyncUUID() + lastDate=self.getLastSyncDate(str(server_sync_uuid)) + + #print ("LastSyncDate: "+str(lastDate)+" Now: "+str(int(time.time()))) + + sql="SELECT * FROM logtable WHERE pcdatum>?" + rows=self.db.ladeSQL(sql,(lastDate,)) + + logging.info("loaded concerned rows") + + newSQLs=self.server.doSync(self.sync_uuid,lastDate,rows,time.time()) + + logging.info("did do sync, processing sqls now") + if newSQLs!=-1: + self.writeSQLTupel(newSQLs,lastDate) + + sync_uuid, finalpcdatum=self.server.doSaveFinalTime(self.sync_uuid) + self.doSaveFinalTime(sync_uuid, finalpcdatum) + + self.changeSyncStatus(False,_("no sync process (at the moment)")) + + mbox = gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,_("Synchronization successfully completed")) + response = mbox.run() + mbox.hide() + mbox.destroy() + else: + logging.warning("Zeitdiff zu groß/oder anderer db-Fehler") + self.changeSyncStatus(False,_("no sync process (at the moment)")) + mbox = gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,_("The clocks are not synchronized between stations")) + response = mbox.run() + mbox.hide() + mbox.destroy() + except: + logging.warning("Sync connect failed") + self.changeSyncStatus(False,_("no sync process (at the moment)")) + mbox = gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,_("Sync failed, reason: ")+unicode(sys.exc_info()[1][1])) + response = mbox.run() + mbox.hide() + mbox.destroy() + self.server=None + self.server=None + + + + + def __init__(self,db,parentwindow,port): + gtk.VBox.__init__(self,homogeneous=False, spacing=0) + + logging.info("Sync, init") + self.db=db + self.progress=None + self.server=None + self.port=int(port) + self.parentwindow=parentwindow + self.concernedRows=None + + #print "Sync, 2" + #sql = "DROP TABLE sync" + #self.db.speichereSQL(sql,log=False) + + sql = "CREATE TABLE sync (id INTEGER PRIMARY KEY, syncpartner TEXT, uuid TEXT, pcdatum INTEGER)" + self.db.speichereSQL(sql,log=False) + + #print "Sync, 3" + + sql="SELECT uuid,pcdatum FROM sync WHERE syncpartner=?" + rows=self.db.ladeSQL(sql,("self",)) #Eigene Id feststellen + + #print "Sync, 3a" + if (rows==None)or(len(rows)!=1): + sql="DELETE FROM sync WHERE syncpartner=?" + self.db.speichereSQL(sql,("self",),log=False) + + #uuid1=uuid() + #print "Sync, 3b" + + #print "Sync, 3bb" + self.sync_uuid=str(uuid.uuid4()) + sql="INSERT INTO sync (syncpartner,uuid,pcdatum) VALUES (?,?,?)" + self.db.speichereSQL(sql,("self",str(self.sync_uuid),int(time.time())),log=False) + #print "Sync, 3c" + else: + sync_uuid,pcdatum = rows[0] + self.sync_uuid=sync_uuid + #print "x1" + + + + #print "Sync, 4" + + + frame=gtk.Frame(_("Local SyncServer (port ")+str(self.port)+")") + + + + self.comboIP=gtk.combo_box_entry_new_text() + + + self.comboIP.append_text("") #self.get_ip_address("eth0")) + #self.comboIP.append_text(self.get_ip_address("eth1")) #fixme + #self.comboIP.append_text(self.get_ip_address("eth2")) + #self.comboIP.append_text(self.get_ip_address("eth3")) + #print "Sync, 4d" + #self.comboIP.append_text(self.get_ip_address("wlan0")) + #self.comboIP.append_text(self.get_ip_address("wlan1")) + + #print "Sync, 4e" + + frame.add(self.comboIP) + serverbutton=gtk.ToggleButton(_("Start SyncServer")) + serverbutton.connect("clicked",self.startServer,(None,)) + self.pack_start(frame, expand=False, fill=True, padding=1) + self.pack_start(serverbutton, expand=False, fill=True, padding=1) + self.syncServerStatusLabel=gtk.Label(_("Syncserver not running")) + self.pack_start(self.syncServerStatusLabel, expand=False, fill=True, padding=1) + + frame=gtk.Frame(_("RemoteSync-Server (Port ")+str(self.port)+")") + self.comboRemoteIP=gtk.combo_box_entry_new_text() + self.comboRemoteIP.append_text("192.168.0.?") + self.comboRemoteIP.append_text("192.168.1.?") + self.comboRemoteIP.append_text("192.168.176.?") + frame.add(self.comboRemoteIP) + syncbutton=gtk.Button(_("Connect to remote SyncServer")) + syncbutton.connect("clicked",self.syncButton,(None,)) + self.pack_start(frame, expand=False, fill=True, padding=1) + self.pack_start(syncbutton, expand=False, fill=True, padding=1) + self.syncStatusLabel=gtk.Label(_("no sync process (at the moment)")) + self.pack_start(self.syncStatusLabel, expand=False, fill=True, padding=1) + + + #self.comboRemoteIP.set_text_column("Test") + self.comboRemoteIP.get_child().set_text(self.db.ladeDirekt("syncRemoteIP")) + self.comboIP.get_child().set_text(self.db.ladeDirekt("syncServerIP")) + + #load + if (self.db.ladeDirekt("startSyncServer",False)==True): + serverbutton.set_active(True) + + #print "Sync, 9" diff --git a/src/libview.py b/src/libview.py new file mode 100644 index 0000000..481464c --- /dev/null +++ b/src/libview.py @@ -0,0 +1,563 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + +import gtk +import gobject +import logging +import pango +import libliststorehandler + + +class Columns_dialog(gtk.VBox): + + def is_col_selected(self, icol): + children=self.framebox.get_children() + if icol5: + default="0" + else: + default="1" + if self.db.ladeDirekt("showcol_"+str(self.liststorehandler.get_colname(i)),default)=="1": + + if (i==1): + self.cell[i] = CellRendererTriple() + self.tvcolumn[i] = gtk.TreeViewColumn(self.liststorehandler.get_colname(i),self.cell[i]) + self.cell[i].connect( 'status_changed', self.col_toggled) + self.tvcolumn[i].set_attributes( self.cell[i], status=i) + + elif (i==3)or(i==4)or(i==6): + self.cell[i] = gtk.CellRendererCombo() + self.tvcolumn[i] = gtk.TreeViewColumn(self.liststorehandler.get_colname(i),self.cell[i]) + self.cell[i].set_property("model",m) + self.cell[i].set_property('text-column', i) + self.cell[i].set_property('editable',True) + self.cell[i].connect("edited", self.col_edited,i) + self.tvcolumn[i].set_attributes( self.cell[i], text=i) + else: + self.cell[i] = gtk.CellRendererText() + self.tvcolumn[i] = gtk.TreeViewColumn(self.liststorehandler.get_colname(i),self.cell[i]) + self.cell[i].set_property('editable',True) + self.cell[i].set_property('editable-set',True) + self.cell[i].connect("edited", self.col_edited,i) + #self.cell[i].connect("editing-canceled", self.col_edited2,i) + self.tvcolumn[i].set_attributes(self.cell[i], text=i) + + self.cell[i].set_property('cell-background', 'lightgray') + self.tvcolumn[i].set_sort_column_id(i) + self.tvcolumn[i].set_resizable(True) + + + if (i>0): + self.treeview.append_column(self.tvcolumn[i]) + + + # Allow NOT drag and drop reordering of rows + self.treeview.set_reorderable(False) + + + if self.scrolled_window != None: + self.scrolled_window.destroy() + + self.scrolled_window = gtk.ScrolledWindow() + self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) + + self.scrolled_window.add(self.treeview) + self.pack_start(self.scrolled_window, expand=True, fill=True, padding=0) + self.loadList() + + self.show_all() + + def __init__(self,db,liststorehandler,parent_window): + + self.db=db + self.parent_window=parent_window + self.liststorehandler = liststorehandler + + + gtk.VBox.__init__(self,homogeneous=False, spacing=0) + + logging.info("libview, init") + + self.scrolled_window = None + self.reload_view() + + + + + + """ + bearbeitenFrame=gtk.Frame("Verteilung kopieren nach") + bearbeitenvBox=gtk.VBox(homogeneous=False, spacing=0) + + bearbeitenhBox=gtk.HBox(homogeneous=False, spacing=0) + self.comboKlassen = gtk.combo_box_new_text() + bearbeitenhBox.pack_start(self.comboKlassen, expand=False, fill=True, padding=0) + button=gtk.Button("Kopieren") + button.connect("clicked", self.kopiereStoffverteilung, None) + bearbeitenhBox.pack_start(button, expand=False, fill=True, padding=0) + + label=gtk.Label(" ") + bearbeitenhBox.pack_start(label, expand=False, fill=True, padding=0) + + button=gtk.Button("Export in CSV-Datei") + button.connect("clicked", self.exportStoffverteilung, None) + bearbeitenhBox.pack_start(button, expand=False, fill=True, padding=0) + + bearbeitenvBox.pack_start(bearbeitenhBox, expand=False, fill=True, padding=0) + + + bearbeitenFrame.add(bearbeitenvBox) + self.pack_start(bearbeitenFrame, expand=False, fill=True, padding=0) + """ + + #self.connect("unmap", self.speichere) + #self.connect("map", self.ladeWirklich) + + #self.show_all() + + + + #print "libstoffverteilung 9: ",time.clock() + diff --git a/src/multilist b/src/multilist deleted file mode 100755 index aa7cc92..0000000 --- a/src/multilist +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" - -import os -import sys - -## -## I18N -## -import locale -import gettext -gettext.install('multilist', unicode=1) - -if __name__ == "__main__": - - try: - import tempfile - import gtk - tmpdir=tempfile.gettempdir() - - os.mkdir(os.path.join(tmpdir, "multilist_lock")) - except OSError: - ## Failed: another instance is running - - mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_ERROR,gtk.BUTTONS_YES_NO,_("Multilist is already running. Start anyway? (Could result in db problems!)")) - response=mbox.run() - mbox.hide() - mbox.destroy() - if response==gtk.RESPONSE_NO: - sys.exit() - - try: - - from multilistclasses import libmultilist - #print dir(eggtimerclasses) - app = libmultilist.multilistclass() - app.main() - - finally: - ## Remove the PID file - # (...) - ## Delete directory - os.rmdir(os.path.join(tmpdir, "multilist_lock")) - - diff --git a/src/multilist.py b/src/multilist.py new file mode 100755 index 0000000..aa7cc92 --- /dev/null +++ b/src/multilist.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + +import os +import sys + +## +## I18N +## +import locale +import gettext +gettext.install('multilist', unicode=1) + +if __name__ == "__main__": + + try: + import tempfile + import gtk + tmpdir=tempfile.gettempdir() + + os.mkdir(os.path.join(tmpdir, "multilist_lock")) + except OSError: + ## Failed: another instance is running + + mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_ERROR,gtk.BUTTONS_YES_NO,_("Multilist is already running. Start anyway? (Could result in db problems!)")) + response=mbox.run() + mbox.hide() + mbox.destroy() + if response==gtk.RESPONSE_NO: + sys.exit() + + try: + + from multilistclasses import libmultilist + #print dir(eggtimerclasses) + app = libmultilist.multilistclass() + app.main() + + finally: + ## Remove the PID file + # (...) + ## Delete directory + os.rmdir(os.path.join(tmpdir, "multilist_lock")) + + diff --git a/src/multilist_gtk.py b/src/multilist_gtk.py new file mode 100755 index 0000000..69321c6 --- /dev/null +++ b/src/multilist_gtk.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + +#/scratchbox/login +#Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac +#af-sb-init.sh start +#run-standalone.sh ./eggtimer.py +# +#https://stage.maemo.org/svn/maemo/projects/haf/trunk/ +#http://www.maemo.org/platform/docs/pymaemo/pyosso_context.html +#http://maemo-hackers.org/apt/ + +import time +import os +import sys +import logging + +try: + import gtk + #import gtk.glade +except: + print "gtk import failed" + sys.exit(1) + +try: + import hildon + import osso + isHildon=True +except: + isHildon=False + class hildon(): + def __init__(self): + print "PseudoClass hildon" + class Program(): + def __init__(self): + print "PseudoClass hildon.Program" + +#import libextdatei +import libspeichern +import libsqldialog +import libselection +import libview +import libliststorehandler +import libsync +import libbottombar + +version = "0.3.0" +app_name = "multilist" + + + + +class multilistclass(hildon.Program): + + def on_key_press(self, widget, event, *args): + #Hildon Fullscreen Modus + if (isHildon==False): return + if event.keyval == gtk.keysyms.F6: + # The "Full screen" hardware key has been pressed + if self.window_in_fullscreen: + self.window.unfullscreen () + else: + self.window.fullscreen () + + def on_window_state_change(self, widget, event, *args): + if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: + self.window_in_fullscreen = True + else: + self.window_in_fullscreen = False + + + def speichereAlles(self,data=None,data2=None): + logging.info("Speichere alles") + + + def ladeAlles(self,data=None,data2=None): + logging.info("Lade alles") + + def beforeSync(self,data=None,data2=None): + logging.info("Lade alles") + + + def sync_finished(self,data=None,data2=None): + self.selection.comboList_changed() + self.selection.comboCategory_changed() + self.liststorehandler.update_list() + + + def prepare_sync_dialog(self): + self.sync_dialog = gtk.Dialog(_("Sync"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + self.sync_dialog.set_position(gtk.WIN_POS_CENTER) + sync=libsync.Sync(self.db,self.window,50503) + sync.connect("syncFinished",self.sync_finished) + self.sync_dialog.vbox.pack_start(sync, True, True, 0) + self.sync_dialog.set_size_request(500,350) + self.sync_dialog.vbox.show_all() + sync.connect("syncFinished",self.sync_finished) + + + def sync_notes(self,widget=None,data=None): + if self.sync_dialog==None: + self.prepare_sync_dialog() + self.sync_dialog.run() + self.sync_dialog.hide() + + + def show_columns_dialog(self,widget=None,data=None): + col_dialog = gtk.Dialog(_("Choose columns"),self.window,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + col_dialog.set_position(gtk.WIN_POS_CENTER) + cols=libview.Columns_dialog(self.db,self.liststorehandler) + + col_dialog.vbox.pack_start(cols, True, True, 0) + col_dialog.set_size_request(500,350) + col_dialog.vbox.show_all() + + resp=col_dialog.run() + col_dialog.hide() + if resp==gtk.RESPONSE_ACCEPT: + logging.info("changing columns") + cols.save_column_setting() + self.view.reload_view() + #children=self.vbox.get_children() + #while len(children)>1: + # self.vbox.remove(children[1]) + + #self.vbox.pack_end(self.bottombar, expand=True, fill=True, padding=0) + #self.vbox.pack_end(view, expand=True, fill=True, padding=0) + #self.vbox.pack_end(self.selection, expand=False, fill=True, padding=0) + + + col_dialog.destroy() + + + + def __init__(self): + home_dir = os.path.expanduser('~') + dblog=os.path.join(home_dir, "multilist.log") + logging.basicConfig(level=logging.INFO,format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S', filename=dblog,filemode='a') + #logging.getLogger('').addHandler(console) + + # define a Handler which writes INFO messages or higher to the sys.stderr + console = logging.StreamHandler() + console.setLevel(logging.INFO) + # set a format which is simpler for console use + formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') + # tell the handler to use this format + console.setFormatter(formatter) + # add the handler to the root logger + logging.getLogger('').addHandler(console) + + logging.info('Starting Multilist') + + if (isHildon==True): + logging.info('Hildon erkannt, rufe Hildon constructor auf') + hildon.Program.__init__(self) + + #Get the Main Window, and connect the "destroy" event + if (isHildon==False): + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_default_size(700,500) + else: + self.window = hildon.Window() + self.add_window(self.window) + + #print "1b: ",time.clock() + + if (self.window): + self.window.connect("delete_event", self.delete_event) + self.window.connect("destroy", self.destroy) + self.window.set_title("Multilist") + + + + if (isHildon==True): self.window.connect("key-press-event", self.on_key_press) + self.window.connect("window-state-event", self.on_window_state_change) + self.window_in_fullscreen = False #The window isn't in full screen mode initially. + + + self.db=libspeichern.Speichern() + + self.selection=libselection.Selection(self.db,isHildon) + self.liststorehandler=libliststorehandler.Liststorehandler(self.db,self.selection) + self.view=libview.View(self.db,self.liststorehandler,self.window) + self.bottombar=libbottombar.Bottombar(self.db,self.view,isHildon) + + #Haupt vbox für alle Elemente + self.vbox = gtk.VBox(homogeneous=False, spacing=0) + + + + #Menue + dateimenu = gtk.Menu() + + menu_items = gtk.MenuItem(_("Choose database file")) + dateimenu.append(menu_items) + menu_items.connect("activate", self.select_db_dialog, None) + + menu_items = gtk.MenuItem(_("SQL history")) + dateimenu.append(menu_items) + menu_items.connect("activate", self.view_sql_history, None) + + menu_items = gtk.MenuItem(_("SQL optimize")) + dateimenu.append(menu_items) + menu_items.connect("activate", self.optimizeSQL, None) + + menu_items = gtk.MenuItem(_("Sync items")) + self.prepare_sync_dialog() + dateimenu.append(menu_items) + menu_items.connect("activate", self.sync_notes, None) + + + menu_items = gtk.MenuItem(_("Quit")) + dateimenu.append(menu_items) + menu_items.connect("activate", self.destroy, None) + #menu_items.show() + + datei_menu = gtk.MenuItem(_("File")) + datei_menu.show() + datei_menu.set_submenu(dateimenu) + + + toolsmenu = gtk.Menu() + + + menu_items = gtk.MenuItem(_("Choose columns")) + toolsmenu.append(menu_items) + menu_items.connect("activate", self.show_columns_dialog, None) + + menu_items = gtk.MenuItem(_("Rename Category")) + toolsmenu.append(menu_items) + menu_items.connect("activate", self.bottombar.rename_category, None) + + menu_items = gtk.MenuItem(_("Rename List")) + toolsmenu.append(menu_items) + menu_items.connect("activate", self.bottombar.rename_list, None) + + tools_menu = gtk.MenuItem(_("Tools")) + tools_menu.show() + tools_menu.set_submenu(toolsmenu) + + + hilfemenu = gtk.Menu() + menu_items = gtk.MenuItem(_("About")) + hilfemenu.append(menu_items) + menu_items.connect("activate", self.show_about, None) + + hilfe_menu = gtk.MenuItem(_("Help")) + hilfe_menu.show() + hilfe_menu.set_submenu(hilfemenu) + + menu_bar = gtk.MenuBar() + menu_bar.show() + menu_bar.append (datei_menu) + menu_bar.append (tools_menu) + # unten -> damit als letztes menu_bar.append (hilfe_menu) + #Als letztes menü + menu_bar.append (hilfe_menu) + + if (isHildon==True): + menu = gtk.Menu() + for child in menu_bar.get_children(): + child.reparent(menu) + self.window.set_menu(menu) + menu_bar.destroy() + else: + self.vbox.pack_start(menu_bar, False, False, 2) + + + + + #add to vbox below (to get it on top) + + + + self.vbox.pack_end(self.bottombar, expand=False, fill=True, padding=0) + self.vbox.pack_end(self.view, expand=True, fill=True, padding=0) + self.vbox.pack_end(self.selection, expand=False, fill=True, padding=0) + + + if (isHildon==True): self.osso_c = osso.Context(app_name, version, False) + self.window.add(self.vbox) + self.window.show_all() + + #print "8a" + self.ladeAlles() + + + #print "9: ",time.clock() + + def main(self): + gtk.main() + if (isHildon==True): self.osso_c.close() + + def destroy(self, widget=None, data=None): + self.speichereAlles() + self.db.close() + gtk.main_quit() + + + def delete_event(self, widget, event, data=None): + #print "delete event occurred" + return False + + def dlg_delete(self,widget,event,data=None): + return False + + + def show_about(self, widget=None,data=None): + dialog = gtk.AboutDialog() + dialog.set_position(gtk.WIN_POS_CENTER) + dialog.set_name(app_name) + dialog.set_version(version) + dialog.set_copyright("") + dialog.set_website("http://axique.de/f=Multilist") + comments = "%s is a program to handle multiple lists." % app_name + dialog.set_comments(comments) + dialog.run() + dialog.destroy() + + def on_info1_activate(self,menuitem): + self.show_about(menuitem) + + + def view_sql_history(self,widget=None,data=None,data2=None): + sqldiag=libsqldialog.sqlDialog(self.db) + res=sqldiag.run() + sqldiag.hide() + if res==444: + logging.info("exporting sql") + + if (isHildon==False): + dlg = gtk.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) + dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + dlg.add_button( gtk.STOCK_OK, gtk.RESPONSE_OK) + else: + #dlg = hildon.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) + dlg=hildon.FileChooserDialog(self.window, gtk.FILE_CHOOSER_ACTION_SAVE) + + dlg.set_title(_("Select SQL export file")) + if dlg.run() == gtk.RESPONSE_OK: + fileName = dlg.get_filename() + dlg.destroy() + sqldiag.exportSQL(fileName) + else: + dlg.destroy() + + sqldiag.destroy() + + + def optimizeSQL(self,widget=None,data=None,data2=None): + #optimiere sql + self.db.speichereSQL("VACUUM",log=False) + + + + + def select_db_dialog(self,widget=None,data=None,data2=None): + if (isHildon==False): + dlg = gtk.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) + dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + dlg.add_button( gtk.STOCK_OK, gtk.RESPONSE_OK) + else: + #dlg = hildon.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) + dlg=hildon.FileChooserDialog(self.window, gtk.FILE_CHOOSER_ACTION_SAVE) + + + if self.db.ladeDirekt('datenbank'): + dlg.set_filename(self.db.ladeDirekt('datenbank')) + dlg.set_title(_("Choose your database file")) + if dlg.run() == gtk.RESPONSE_OK: + fileName = dlg.get_filename() + self.db.speichereDirekt('datenbank',fileName) + self.speichereAlles() + self.db.openDB() + self.ladeAlles() + dlg.destroy() + + + + diff --git a/src/multilistclasses/__init__.py b/src/multilistclasses/__init__.py deleted file mode 100755 index 105eaaf..0000000 --- a/src/multilistclasses/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" diff --git a/src/multilistclasses/libbottombar.py b/src/multilistclasses/libbottombar.py deleted file mode 100644 index 654c72d..0000000 --- a/src/multilistclasses/libbottombar.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" - - -import gobject -import time -import logging - -import gtk - -class Bottombar(gtk.HBox): - - __gsignals__ = { - 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)), - #'changedCategory': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)) - } - - - def new_item(self,widget=None,data1=None,data2=None): - dialog = gtk.Dialog(_("New item name:"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - - dialog.set_position(gtk.WIN_POS_CENTER) - entryKlasse=gtk.Entry() - entryKlasse.set_text("") - - dialog.vbox.pack_start(entryKlasse, True, True, 0) - - dialog.vbox.show_all() - #dialog.set_size_request(400,300) - - if dialog.run() == gtk.RESPONSE_ACCEPT: - #logging.info("new category name "+entryKlasse.get_text()) - #self.view.liststorehandler.rename_category(entryKlasse.get_text()) - self.view.liststorehandler.add_row(entryKlasse.get_text()) - dialog.destroy() - - - def del_item(self,widget=None,data1=None,data2=None): - path, col = self.view.treeview.get_cursor() - if path!=None: - mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_QUESTION,gtk.BUTTONS_YES_NO,_("Delete current item?")) - response=mbox.run() - mbox.hide() - mbox.destroy() - if response==gtk.RESPONSE_YES: - self.view.del_active_row() - else: - mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_ERROR,gtk.BUTTONS_OK,_("No item selected!")) - response=mbox.run() - mbox.hide() - mbox.destroy() - - - def checkout_items(self,widget=None,data1=None,data2=None): - #self.view.del_active_row() - mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_QUESTION,gtk.BUTTONS_YES_NO,(_("Really checkout all items?"))) - response=mbox.run() - mbox.hide() - mbox.destroy() - if response==gtk.RESPONSE_YES: - self.view.liststorehandler.checkout_rows() - #n=len(self.view.liststorehandler.get_liststore()) - #for i in range(n): - # self.view.liststorehandler.checkout_rows() - # #print i - - def search_list(self,widget=None,data1=None,data2=None): - self.view.liststorehandler.get_liststore(widget.get_text()) - - - def rename_category(self,widget=None,data1=None,data2=None): - dialog = gtk.Dialog(_("New category name:"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - - dialog.set_position(gtk.WIN_POS_CENTER) - entryKlasse=gtk.Entry() - entryKlasse.set_text(self.view.liststorehandler.selection.get_category()) - - dialog.vbox.pack_start(entryKlasse, True, True, 0) - - dialog.vbox.show_all() - #dialog.set_size_request(400,300) - - if dialog.run() == gtk.RESPONSE_ACCEPT: - logging.info("new category name "+entryKlasse.get_text()) - self.view.liststorehandler.rename_category(entryKlasse.get_text()) - else: - #print "Cancel",res - pass - dialog.destroy() - - - def rename_list(self,widget=None,data1=None,data2=None): - dialog = gtk.Dialog(_("New list name:"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - - dialog.set_position(gtk.WIN_POS_CENTER) - entryKlasse=gtk.Entry() - entryKlasse.set_text(self.view.liststorehandler.selection.get_list()) - - dialog.vbox.pack_start(entryKlasse, True, True, 0) - - dialog.vbox.show_all() - #dialog.set_size_request(400,300) - - if dialog.run() == gtk.RESPONSE_ACCEPT: - logging.info("new list name "+entryKlasse.get_text()) - self.view.liststorehandler.rename_list(entryKlasse.get_text()) - else: - #print "Cancel",res - pass - dialog.destroy() - - def __init__(self,db,view,isHildon): - gtk.HBox.__init__(self,homogeneous=False, spacing=3) - - self.db=db - self.isHildon=isHildon - self.view=view - - logging.info("libBottomBar, init") - - - button=gtk.Button(_("New item")) - button.connect("clicked",self.new_item) - self.pack_start(button, expand=False, fill=True, padding=0) - - label=gtk.Label(" ") - self.pack_start(label, expand=True, fill=True, padding=0) - - label=gtk.Label(_("Search:")) - self.pack_start(label, expand=False, fill=True, padding=0) - searchEntry=gtk.Entry() - searchEntry.connect("changed",self.search_list) - self.pack_start(searchEntry, expand=True, fill=True, padding=0) - - label=gtk.Label(" ") - self.pack_start(label, expand=True, fill=True, padding=0) - - button=gtk.Button(_("Checkout all items")) - button.connect("clicked",self.checkout_items) - self.pack_start(button, expand=False, fill=True, padding=0) - - button=gtk.Button(_("Del item")) - button.connect("clicked",self.del_item) - self.pack_start(button, expand=False, fill=True, padding=0) - - - diff --git a/src/multilistclasses/libliststorehandler.py b/src/multilistclasses/libliststorehandler.py deleted file mode 100644 index 933a90c..0000000 --- a/src/multilistclasses/libliststorehandler.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" - -import gtk -import gobject -import libspeichern -import logging - - -class Liststorehandler(): - - def get_unitsstore(self): - if (self.unitsstore==None): - self.unitsstore=gtk.ListStore(str, str, str,str,str, str,str, str, str,str, str, str,str) - self.unitsstore.clear() - #row(3) quantities - #row 4 units - #row 6 priority - self.unitsstore.append(["-1","-1","","","","","","","","","","",""]) - self.unitsstore.append(["-1","-1","","1","g","","0","","","","","",""]) - self.unitsstore.append(["-1","-1","","2","kg","","1","","","","","",""]) - self.unitsstore.append(["-1","-1","","3","liter","","2","","","","","",""]) - self.unitsstore.append(["-1","-1","","4","packs","","3","","","","","",""]) - self.unitsstore.append(["-1","-1","","5","","","4","","","","","",""]) - self.unitsstore.append(["-1","-1","","6","","","5","","","","","",""]) - self.unitsstore.append(["-1","-1","","7","","","6","","","","","",""]) - self.unitsstore.append(["-1","-1","","8","","","7","","","","","",""]) - self.unitsstore.append(["-1","-1","","9","","","8","","","","","",""]) - self.unitsstore.append(["-1","-1","","","","","9","","","","","",""]) - - return self.unitsstore - - - - - def get_liststore(self,titlesearch=""): - if (self.liststore==None): - self.liststore=gtk.ListStore(str, str, str,str,str, str,str, str, str,str, str, str,str) - self.liststore.clear() - - titlesearch=titlesearch+"%" - - - if (self.selection.get_status()=="0"): #only 0 and 1 (active items) - sql="SELECT uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2 FROM items WHERE list=? AND category LIKE ? AND status>=? AND title like ? ORDER BY category, status, title" - rows=self.db.ladeSQL(sql,(self.selection.get_list(),self.selection.get_category(True),self.selection.get_status(),titlesearch)) - else: - sql="SELECT uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2 FROM items WHERE list=? AND category LIKE ? AND title LIKE ? ORDER BY category, title ASC" - rows=self.db.ladeSQL(sql,(self.selection.get_list(),self.selection.get_category(True),titlesearch)) - - #print rows - if ((rows!=None)and(len(rows)>0)): - for row in rows: - uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2 = row - if unit==None: - pass - #unit="" - self.liststore.append([uid,status,title,quantitiy,unit,price,priority,date,private,stores,note,custom1,custom2]) - #else: - #self.liststore.append(["-1","-1",""," ","","","","","","","","",""]) - #import uuid - #self.liststore.append(["-1","-1","","","","","","","","","","",""]) - - return self.liststore - - - def emptyValueExists(self): - for child in self.liststore: - if child[2]=="": - return True - return False - - - - def update_row(self,irow,icol,new_text): - #print "liststore 1" - #for x in self.liststore: - # print x[0],x[2] - - if (irow>-1)and(self.liststore[irow][0]!="-1")and(self.liststore[irow][0]!=None): - sql = "UPDATE items SET "+self.collist[icol]+"=? WHERE uid=?" - self.db.speichereSQL(sql,(new_text,self.liststore[irow][0]),rowid=self.liststore[irow][0]) - - logging.info("Updated row: "+self.collist[icol]+" new text "+new_text+" Titel: "+str(self.liststore[irow][2])+" with uid "+str(self.liststore[irow][0])) - - self.liststore[irow][icol]=new_text - else: - logging.warning("update_row: row does not exist") - return - #if (self.emptyValueExists()==True)and(icol<2): - # #print "letzter Eintrag ohne inhalt" - # return - - #print "liststore 2" - #for x in self.liststore: - # print x[0],x[2] - - - def checkout_rows(self): - sql = "UPDATE items SET status=? WHERE list=? AND category LIKE ? AND status=?" - self.db.speichereSQL(sql,("-1",self.selection.get_list(),self.selection.get_category(True),"1")) - for i in range(len(self.liststore)): - if self.liststore[i][1]=="1": - self.liststore[i][1]="-1" - - - - - def add_row(self,title=""): - #self.update_row(-1,1,"-1") - #for x in self.liststore: - # print x[0],x[2] - status=self.selection.get_status() - import uuid - uid=str(uuid.uuid4()) - sql = "INSERT INTO items (uid,list,category,status, title) VALUES (?,?,?,?,?)" - self.db.speichereSQL(sql,(uid,self.selection.get_list(),self.selection.get_category(),status,title),rowid=uid) - logging.info("Insertet row: status = "+status+" with uid "+str(uid)) - #self.liststore[irow][0]=str(uuid.uuid4()) - - self.liststore.append([uid,status,title," ","","","","","","","","",""]) - self.selection.comboLists_check_for_update() - # if (irow>-1): - # self.liststore[irow][icol]=new_text - # self.liststore[irow][0]=uid - # else: - # self.liststore.append([uid,"-1",""," ","","","","","","","","",""]) - #print "xy",self.liststore[len(self.liststore)-1][0] - #Check if a new list/category is opened - # self.selection.comboLists_check_for_update() - - def del_row(self,irow,row_iter): - uid=self.liststore[irow][0] - self.liststore.remove(row_iter) - sql = "DELETE FROM items WHERE uid=?" - self.db.speichereSQL(sql,(uid,)) - # - - - def get_colname(self,i): - if i. - - Copyright (C) 2008 Christoph Würstle -""" - -#/scratchbox/login -#Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac -#af-sb-init.sh start -#run-standalone.sh ./eggtimer.py -# -#https://stage.maemo.org/svn/maemo/projects/haf/trunk/ -#http://www.maemo.org/platform/docs/pymaemo/pyosso_context.html -#http://maemo-hackers.org/apt/ - -import time -import os -import sys -import logging - -try: - import gtk - #import gtk.glade -except: - print "gtk import failed" - sys.exit(1) - -try: - import hildon - import osso - isHildon=True -except: - isHildon=False - class hildon(): - def __init__(self): - print "PseudoClass hildon" - class Program(): - def __init__(self): - print "PseudoClass hildon.Program" - -#import libextdatei -import libspeichern -import libsqldialog -import libselection -import libview -import libliststorehandler -import libsync -import libbottombar - -version = "0.3.0" -app_name = "multilist" - - - - -class multilistclass(hildon.Program): - - def on_key_press(self, widget, event, *args): - #Hildon Fullscreen Modus - if (isHildon==False): return - if event.keyval == gtk.keysyms.F6: - # The "Full screen" hardware key has been pressed - if self.window_in_fullscreen: - self.window.unfullscreen () - else: - self.window.fullscreen () - - def on_window_state_change(self, widget, event, *args): - if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN: - self.window_in_fullscreen = True - else: - self.window_in_fullscreen = False - - - def speichereAlles(self,data=None,data2=None): - logging.info("Speichere alles") - - - def ladeAlles(self,data=None,data2=None): - logging.info("Lade alles") - - def beforeSync(self,data=None,data2=None): - logging.info("Lade alles") - - - def sync_finished(self,data=None,data2=None): - self.selection.comboList_changed() - self.selection.comboCategory_changed() - self.liststorehandler.update_list() - - - def prepare_sync_dialog(self): - self.sync_dialog = gtk.Dialog(_("Sync"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - - self.sync_dialog.set_position(gtk.WIN_POS_CENTER) - sync=libsync.Sync(self.db,self.window,50503) - sync.connect("syncFinished",self.sync_finished) - self.sync_dialog.vbox.pack_start(sync, True, True, 0) - self.sync_dialog.set_size_request(500,350) - self.sync_dialog.vbox.show_all() - sync.connect("syncFinished",self.sync_finished) - - - def sync_notes(self,widget=None,data=None): - if self.sync_dialog==None: - self.prepare_sync_dialog() - self.sync_dialog.run() - self.sync_dialog.hide() - - - def show_columns_dialog(self,widget=None,data=None): - col_dialog = gtk.Dialog(_("Choose columns"),self.window,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - - col_dialog.set_position(gtk.WIN_POS_CENTER) - cols=libview.Columns_dialog(self.db,self.liststorehandler) - - col_dialog.vbox.pack_start(cols, True, True, 0) - col_dialog.set_size_request(500,350) - col_dialog.vbox.show_all() - - resp=col_dialog.run() - col_dialog.hide() - if resp==gtk.RESPONSE_ACCEPT: - logging.info("changing columns") - cols.save_column_setting() - self.view.reload_view() - #children=self.vbox.get_children() - #while len(children)>1: - # self.vbox.remove(children[1]) - - #self.vbox.pack_end(self.bottombar, expand=True, fill=True, padding=0) - #self.vbox.pack_end(view, expand=True, fill=True, padding=0) - #self.vbox.pack_end(self.selection, expand=False, fill=True, padding=0) - - - col_dialog.destroy() - - - - def __init__(self): - home_dir = os.path.expanduser('~') - dblog=os.path.join(home_dir, "multilist.log") - logging.basicConfig(level=logging.INFO,format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S', filename=dblog,filemode='a') - #logging.getLogger('').addHandler(console) - - # define a Handler which writes INFO messages or higher to the sys.stderr - console = logging.StreamHandler() - console.setLevel(logging.INFO) - # set a format which is simpler for console use - formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') - # tell the handler to use this format - console.setFormatter(formatter) - # add the handler to the root logger - logging.getLogger('').addHandler(console) - - logging.info('Starting Multilist') - - if (isHildon==True): - logging.info('Hildon erkannt, rufe Hildon constructor auf') - hildon.Program.__init__(self) - - #Get the Main Window, and connect the "destroy" event - if (isHildon==False): - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - self.window.set_default_size(700,500) - else: - self.window = hildon.Window() - self.add_window(self.window) - - #print "1b: ",time.clock() - - if (self.window): - self.window.connect("delete_event", self.delete_event) - self.window.connect("destroy", self.destroy) - self.window.set_title("Multilist") - - - - if (isHildon==True): self.window.connect("key-press-event", self.on_key_press) - self.window.connect("window-state-event", self.on_window_state_change) - self.window_in_fullscreen = False #The window isn't in full screen mode initially. - - - self.db=libspeichern.Speichern() - - self.selection=libselection.Selection(self.db,isHildon) - self.liststorehandler=libliststorehandler.Liststorehandler(self.db,self.selection) - self.view=libview.View(self.db,self.liststorehandler,self.window) - self.bottombar=libbottombar.Bottombar(self.db,self.view,isHildon) - - #Haupt vbox für alle Elemente - self.vbox = gtk.VBox(homogeneous=False, spacing=0) - - - - #Menue - dateimenu = gtk.Menu() - - menu_items = gtk.MenuItem(_("Choose database file")) - dateimenu.append(menu_items) - menu_items.connect("activate", self.select_db_dialog, None) - - menu_items = gtk.MenuItem(_("SQL history")) - dateimenu.append(menu_items) - menu_items.connect("activate", self.view_sql_history, None) - - menu_items = gtk.MenuItem(_("SQL optimize")) - dateimenu.append(menu_items) - menu_items.connect("activate", self.optimizeSQL, None) - - menu_items = gtk.MenuItem(_("Sync items")) - self.prepare_sync_dialog() - dateimenu.append(menu_items) - menu_items.connect("activate", self.sync_notes, None) - - - menu_items = gtk.MenuItem(_("Quit")) - dateimenu.append(menu_items) - menu_items.connect("activate", self.destroy, None) - #menu_items.show() - - datei_menu = gtk.MenuItem(_("File")) - datei_menu.show() - datei_menu.set_submenu(dateimenu) - - - toolsmenu = gtk.Menu() - - - menu_items = gtk.MenuItem(_("Choose columns")) - toolsmenu.append(menu_items) - menu_items.connect("activate", self.show_columns_dialog, None) - - menu_items = gtk.MenuItem(_("Rename Category")) - toolsmenu.append(menu_items) - menu_items.connect("activate", self.bottombar.rename_category, None) - - menu_items = gtk.MenuItem(_("Rename List")) - toolsmenu.append(menu_items) - menu_items.connect("activate", self.bottombar.rename_list, None) - - tools_menu = gtk.MenuItem(_("Tools")) - tools_menu.show() - tools_menu.set_submenu(toolsmenu) - - - hilfemenu = gtk.Menu() - menu_items = gtk.MenuItem(_("About")) - hilfemenu.append(menu_items) - menu_items.connect("activate", self.show_about, None) - - hilfe_menu = gtk.MenuItem(_("Help")) - hilfe_menu.show() - hilfe_menu.set_submenu(hilfemenu) - - menu_bar = gtk.MenuBar() - menu_bar.show() - menu_bar.append (datei_menu) - menu_bar.append (tools_menu) - # unten -> damit als letztes menu_bar.append (hilfe_menu) - #Als letztes menü - menu_bar.append (hilfe_menu) - - if (isHildon==True): - menu = gtk.Menu() - for child in menu_bar.get_children(): - child.reparent(menu) - self.window.set_menu(menu) - menu_bar.destroy() - else: - self.vbox.pack_start(menu_bar, False, False, 2) - - - - - #add to vbox below (to get it on top) - - - - self.vbox.pack_end(self.bottombar, expand=False, fill=True, padding=0) - self.vbox.pack_end(self.view, expand=True, fill=True, padding=0) - self.vbox.pack_end(self.selection, expand=False, fill=True, padding=0) - - - if (isHildon==True): self.osso_c = osso.Context(app_name, version, False) - self.window.add(self.vbox) - self.window.show_all() - - #print "8a" - self.ladeAlles() - - - #print "9: ",time.clock() - - def main(self): - gtk.main() - if (isHildon==True): self.osso_c.close() - - def destroy(self, widget=None, data=None): - self.speichereAlles() - self.db.close() - gtk.main_quit() - - - def delete_event(self, widget, event, data=None): - #print "delete event occurred" - return False - - def dlg_delete(self,widget,event,data=None): - return False - - - def show_about(self, widget=None,data=None): - dialog = gtk.AboutDialog() - dialog.set_position(gtk.WIN_POS_CENTER) - dialog.set_name(app_name) - dialog.set_version(version) - dialog.set_copyright("") - dialog.set_website("http://axique.de/f=Multilist") - comments = "%s is a program to handle multiple lists." % app_name - dialog.set_comments(comments) - dialog.run() - dialog.destroy() - - def on_info1_activate(self,menuitem): - self.show_about(menuitem) - - - def view_sql_history(self,widget=None,data=None,data2=None): - sqldiag=libsqldialog.sqlDialog(self.db) - res=sqldiag.run() - sqldiag.hide() - if res==444: - logging.info("exporting sql") - - if (isHildon==False): - dlg = gtk.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) - dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dlg.add_button( gtk.STOCK_OK, gtk.RESPONSE_OK) - else: - #dlg = hildon.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) - dlg=hildon.FileChooserDialog(self.window, gtk.FILE_CHOOSER_ACTION_SAVE) - - dlg.set_title(_("Select SQL export file")) - if dlg.run() == gtk.RESPONSE_OK: - fileName = dlg.get_filename() - dlg.destroy() - sqldiag.exportSQL(fileName) - else: - dlg.destroy() - - sqldiag.destroy() - - - def optimizeSQL(self,widget=None,data=None,data2=None): - #optimiere sql - self.db.speichereSQL("VACUUM",log=False) - - - - - def select_db_dialog(self,widget=None,data=None,data2=None): - if (isHildon==False): - dlg = gtk.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) - dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) - dlg.add_button( gtk.STOCK_OK, gtk.RESPONSE_OK) - else: - #dlg = hildon.FileChooserDialog(parent = self.window, action = gtk.FILE_CHOOSER_ACTION_SAVE) - dlg=hildon.FileChooserDialog(self.window, gtk.FILE_CHOOSER_ACTION_SAVE) - - - if self.db.ladeDirekt('datenbank'): - dlg.set_filename(self.db.ladeDirekt('datenbank')) - dlg.set_title(_("Choose your database file")) - if dlg.run() == gtk.RESPONSE_OK: - fileName = dlg.get_filename() - self.db.speichereDirekt('datenbank',fileName) - self.speichereAlles() - self.db.openDB() - self.ladeAlles() - dlg.destroy() - - - - diff --git a/src/multilistclasses/libselection.py b/src/multilistclasses/libselection.py deleted file mode 100644 index 9ab8651..0000000 --- a/src/multilistclasses/libselection.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" - - -import gobject -import time -import logging - -import gtk - -class Selection(gtk.HBox): - - __gsignals__ = { - 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)), - #'changedCategory': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,gobject.TYPE_STRING)) - } - - def load(self): - model=self.comboList.get_model() - model.clear() - #self.comboList.remove(0) - - - sql="SELECT DISTINCT list FROM items ORDER BY list" - rows=self.db.ladeSQL(sql) - if ((rows!=None)and(len(rows)>0)): - for row in rows: - self.comboList.append_text(row[0]) - else: - self.comboList.append_text("default") - - s=self.db.ladeDirekt("comboListText") - if s!="": - self.comboList.get_child().set_text(s) - else: - self.comboList.set_active(0) - - def comboList_changed(self, widget=None, data=None): - #self.comboCategory.set_model(None) - #print "reload categories" - while len(self.comboCategory.get_model())>0: - self.comboCategory.remove_text(0) - - sql="SELECT DISTINCT category FROM items WHERE list=? ORDER BY category" - rows=self.db.ladeSQL(sql,(self.get_list(),)) - - self.comboCategory.append_text(_("all")) - if ((rows!=None)and(len(rows)>0)): - for row in rows: - if (row[0]!=_("all")): - self.comboCategory.append_text(row[0]) - - s=self.db.ladeDirekt("comboCategoryText"+self.comboList.get_child().get_text()) - if len(s)>0: - self.comboCategory.get_child().set_text(s) - else: - self.comboCategory.set_active(0) - - self.emit("changed","list","") - self.db.speichereDirekt("comboListText",self.comboList.get_child().get_text()) - - - - def comboCategory_changed(self, widget=None, data=None): - #logging.info("Klasse geaendert zu ") - #self.hauptRegister.set_current_page(0) - self.emit("changed","category","") - if self.comboCategory.get_active()>-1: - self.db.speichereDirekt("comboCategoryText"+self.comboList.get_child().get_text(),self.comboCategory.get_child().get_text()) - - def radioActive_changed(self, widget, data=None): - self.emit("changed","radio","") - - def comboLists_check_for_update(self): - if self.comboCategory.get_active()==-1: - model=self.comboCategory.get_model() - found=False - cat=self.get_category() - for x in model: - if x[0]==cat: - found=True - if found==False: - self.comboCategory.append_text(self.get_category()) - self.comboCategory.set_active(len(self.comboCategory.get_model())-1) - if self.comboList.get_active()==-1: - model=self.comboList.get_model() - found=False - list=self.get_list() - for x in model: - if x[0]==list: - found=True - if found==False: - self.comboList.append_text(self.get_list()) - self.comboList.set_active(len(self.comboList.get_model())-1) - - - def lade(self): - logging.warning("Laden der aktuellen position noch nicht implementiert") - - - def speichere(self): - logging.warning("Speichern der aktuellen position noch nicht implementiert") - - - def getIsHildon(self): - return self.isHildon - - def get_category(self,select=False): - s=self.comboCategory.get_child().get_text() - if s==_("all"): - if (select==False): - return "undefined" - else: - return "%" - else: - return s - def set_category(self,category): - self.comboCategory.get_child().set_text(category) - - def set_list(self,listname): - self.comboList.get_child().set_text(listname) - - def get_list(self): - return self.comboList.get_child().get_text() - - - - def get_status(self): - #return self.comboCategory.get_child().get_text() - if self.radio_all.get_active()==True: - return "-1" - else: - return "0" - - - def __init__(self,db,isHildon): - gtk.HBox.__init__(self,homogeneous=False, spacing=3) - - self.db=db - self.isHildon=isHildon - - logging.info("libSelection, init") - - - label=gtk.Label(_("List:")) - self.pack_start(label, expand=False, fill=True, padding=0) - - self.comboList = gtk.combo_box_entry_new_text() - self.comboList.set_size_request(180,-1) - self.pack_start(self.comboList, expand=False, fill=True, padding=0) - - label=gtk.Label(_(" Category:")) - self.pack_start(label, expand=False, fill=True, padding=0) - - self.comboCategory = gtk.combo_box_entry_new_text() - self.comboCategory.set_size_request(180,-1) - self.pack_start(self.comboCategory, expand=False, fill=True, padding=0) - - self.comboList.connect("changed", self.comboList_changed, None) - self.comboCategory.connect("changed", self.comboCategory_changed, None) - - label=gtk.Label(_(" View:")) - self.pack_start(label, expand=False, fill=True, padding=0) - - self.radio_all=gtk.RadioButton(group=None, label=_("All"), use_underline=True) - self.pack_start(self.radio_all, expand=False, fill=True, padding=0) - self.radio_active=gtk.RadioButton(group=self.radio_all, label=_("Active"), use_underline=True) - self.pack_start(self.radio_active, expand=False, fill=True, padding=0) - self.radio_all.connect("toggled",self.radioActive_changed, None) - - - diff --git a/src/multilistclasses/libspeichern.py b/src/multilistclasses/libspeichern.py deleted file mode 100644 index e887340..0000000 --- a/src/multilistclasses/libspeichern.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" - -import time -import sqlite3 -import shelve -import sys -import string -import shutil -import os -import logging - -class Speichern(): - def speichereDirekt(self,schluessel,daten): - self.d[schluessel]=daten - logging.info("speichereDirekt "+str(schluessel)+" "+str(daten)+" lesen: "+str(self.d[schluessel])) - - - def ladeDirekt(self,schluessel,default=""): - #print "ladeDirekt",schluessel, "Schluessel vorhanden",self.d.has_key(schluessel) - if (self.d.has_key(schluessel)==True): - data=self.d[schluessel] - #print data - return data - else: - return default - - - def speichereSQL(self,sql,tupel=None,commit=True,host="self",log=True,pcdatum=None,rowid=""): - #print "speichereSQL:",sql,tupel - try: - programSQLError=True - if (tupel==None): - self.cur.execute(sql) - else: - self.cur.execute(sql,tupel) - programSQLError=False - - #print int(time.time()), sql, pickle.dumps(tupel), host - if (log==True): - strtupel=[] - if (tupel!=None): - for t in tupel: - strtupel.append(str(t)) - - - if pcdatum==None: pcdatum=int(time.time()) - self.cur.execute("INSERT INTO logtable ( pcdatum,sql,param,host,rowid ) VALUES (?,?,?,?,?)",(pcdatum, sql, string.join(strtupel," <> "), host,str(rowid) )) - if (commit==True): self.conn.commit() - - return True - except: - s=str(sys.exc_info()) - if (s.find(" already exists")==-1): - #if len(s)>0: - if (programSQLError==True): - logging.error("speichereSQL-Exception "+str(sys.exc_info())+" "+str(sql)+" "+str(tupel)) - else: - logging.error("speichereSQL-Exception in Logging!!!! :"+str(sys.exc_info())+" "+str(sql)+" "+str(tupel)) - return False - - def commitSQL(self): - self.conn.commit() - - - def ladeSQL(self,sql,tupel=None): - #print sql,tupel - try: - if (tupel==None): - self.cur.execute(sql) - else: - self.cur.execute(sql,tupel) - return self.cur.fetchall() - except: - logging.error("ladeSQL-Exception "+str(sys.exc_info())+" "+str(sql)+" "+str(tupel)) - return () - - def ladeHistory(self,sql_condition,param_condition): - sql="SELECT * FROM logtable WHERE sql LIKE '%"+str(sql_condition)+"%' AND param LIKE '%"+str(param_condition)+"%'" - rows=self.ladeSQL(sql) - #print rows - i=0 - erg=[] - while i> ")]) - #pcdatum #datum #sql # Param_org #param - - i+=1 - - return erg - - def delHistory(self,sql_condition,param_condition,exceptTheLastXSeconds=0): - pcdatum=int(time.time())-exceptTheLastXSeconds - sql="DELETE FROM logtable WHERE sql LIKE '%"+str(sql_condition)+"%' AND param LIKE '%"+str(param_condition)+"%' AND pcdatum. - - Copyright (C) 2008 Christoph Würstle -""" - -import gobject -import time -import string -import gtk -import sys -import logging - -import libspeichern - - -class sqlDialog(gtk.Dialog): - - def exportSQL(self,filename): - f = open(filename, 'w') - msgstring="" - sql="SELECT pcdatum,sql,param FROM logtable WHERE pcdatum>? ORDER BY pcdatum DESC" - rows=self.db.ladeSQL(sql,(time.time()-2*24*3600,)) - for row in rows: - pcdatum,sql,param = row - datum=str(time.strftime("%d.%m.%y %H:%M:%S ", (time.localtime(pcdatum)))) - f.write( datum +"\t" + sql + "\t\t" + param+ "\n") - - - f.close() - - - def __init__(self,db): - self.db=db - - logging.info("sqldialog, init") - - gtk.Dialog.__init__(self,_("SQL History (the past two days):"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - - - self.add_button(_("Export"), 444) - self.set_position(gtk.WIN_POS_CENTER) - - - self.liststore = gtk.ListStore(str, str, str) - - # create the TreeView using liststore - self.treeview = gtk.TreeView(self.liststore) - - # create a CellRenderers to render the data - self.cell1 = gtk.CellRendererText() - self.cell2 = gtk.CellRendererText() - self.cell3 = gtk.CellRendererText() - #self.cell1.set_property('markup', 1) - - # create the TreeViewColumns to display the data - self.tvcolumn1 = gtk.TreeViewColumn(_('Date')) - self.tvcolumn2 = gtk.TreeViewColumn(_('SQL')) - self.tvcolumn3 = gtk.TreeViewColumn(_('Parameter')) - - # add columns to treeview - self.treeview.append_column(self.tvcolumn1) - self.treeview.append_column(self.tvcolumn2) - self.treeview.append_column(self.tvcolumn3) - - - self.tvcolumn1.pack_start(self.cell1, True) - self.tvcolumn2.pack_start(self.cell2, True) - self.tvcolumn3.pack_start(self.cell3, True) - - self.tvcolumn1.set_attributes(self.cell1, text=0) #Spalten setzten hier!!!! - self.tvcolumn2.set_attributes(self.cell2, text=1) - self.tvcolumn3.set_attributes(self.cell3, text=2) - - # make treeview searchable - #self.treeview.set_search_column(0) - #self.tvcolumn.set_sort_column_id(0) - - # Allow NOT drag and drop reordering of rows - self.treeview.set_reorderable(False) - - scrolled_window = gtk.ScrolledWindow() - scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) - scrolled_window.add(self.treeview) - #self.pack_start(scrolled_window, expand=True, fill=True, padding=0) - - - self.vbox.pack_start(scrolled_window, True, True, 0) - - self.vbox.show_all() - - msgstring="" - sql="SELECT pcdatum,sql,param FROM logtable WHERE pcdatum>? ORDER BY pcdatum DESC" - rows=db.ladeSQL(sql,(time.time()-3*24*3600,)) - i=0 - for row in rows: - pcdatum,sql,param = row - datum=str(time.strftime("%d.%m.%y %H:%M:%S ", (time.localtime(pcdatum)))) - if len(param)>100: - param=param[:20]+_(" (Reduced parameter) ")+param[20:] - self.liststore.append([datum, sql,param]) - i+=1 - if (i>50): - break - - self.set_size_request(500,400) - - - diff --git a/src/multilistclasses/libsync.py b/src/multilistclasses/libsync.py deleted file mode 100755 index a793a16..0000000 --- a/src/multilistclasses/libsync.py +++ /dev/null @@ -1,436 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import gobject -import time -import string -from SimpleXMLRPCServer import SimpleXMLRPCServer -import random -import socket -socket.setdefaulttimeout(60) # Timeout auf 60 sec. setzen -import xmlrpclib -import select -#import fcntl -import struct -import gtk -import uuid -import sys -import logging - -import libspeichern - - -class ProgressDialog(gtk.Dialog): - - def pulse(self): - #self.progressbar.pulse() - pass - - def __init__(self,title=_("Sync process"), parent=None): - gtk.Dialog.__init__(self,title,parent,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,()) - - logging.info("ProgressDialog, init") - - label=gtk.Label(_("Sync process running...please wait")) - self.vbox.pack_start(label, True, True, 0) - label=gtk.Label(_("(this can take some minutes)")) - self.vbox.pack_start(label, True, True, 0) - - #self.progressbar=gtk.ProgressBar() - #self.vbox.pack_start(self.progressbar, True, True, 0) - - #self.set_keep_above(True) - self.vbox.show_all() - self.show() - -class Sync(gtk.VBox): - - __gsignals__ = { - 'syncFinished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,)), - 'syncBeforeStart' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,(gobject.TYPE_STRING,)), - } - - def changeSyncStatus(self,active,title): - self.syncStatusLabel.set_text(title) - if active==True: - if self.progress==None: - self.progress=ProgressDialog(parent=self.parentwindow) - self.emit("syncBeforeStart","syncBeforeStart") - - - else: - if self.progress!=None: - self.progress.hide() - self.progress.destroy() - self.progress=None - self.emit("syncFinished","syncFinished") - - def pulse(self): - if self.progress!=None: - self.progress.pulse() - #if self.server!=None: - # self.server.pulse() - - - def getUeberblickBox(self): - frame=gtk.Frame(_("Query")) - return frame - - def handleRPC(self): - try: - if (self.rpcserver==None): return False - except: - return False - - while (len(self.poll.poll(0))>0): - self.rpcserver.handle_request() - return True - - def get_ip_address(self,ifname): - return socket.gethostbyname(socket.gethostname()) - #try: - # s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - # ip=socket.inet_ntoa(fcntl.ioctl(s.fileno(),0x8915,struct.pack('256s', ifname[:15]))[20:24]) - # s.close() - #except: - # ip=socket.gethostbyname(socket.gethostname()) - # s.close() - - #return ip FixME - - def getLastSyncDate(self,sync_uuid): - sql="SELECT syncpartner,pcdatum FROM sync WHERE uuid=?" - rows=self.db.ladeSQL(sql,(sync_uuid,)) - if (rows!=None)and(len(rows)==1): - syncpartner,pcdatum = rows[0] - else: - pcdatum=-1 - logging.info("LastSyncDatum: "+str(pcdatum)+" Jetzt "+str(int(time.time()))) - return pcdatum - - - def check4commit(self,newSQL,lastdate): - logging.info("check4commit 1") - if self.concernedRows==None: - logging.info("check4commit Updatung concernedRows") - sql="SELECT pcdatum,rowid FROM logtable WHERE pcdatum>? ORDER BY pcdatum DESC" - self.concernedRows=self.db.ladeSQL(sql,(lastdate,)) - - - if (self.concernedRows!=None)and(len(self.concernedRows)>0): - #logging.info("check4commit 2") - id1, pcdatum,sql, param, host, rowid = newSQL - - if len(rowid)>0: - for x in self.concernedRows: - #logging.info("check4commit 3") - if (x[1]==rowid): - if (x[0]>pcdatum): - logging.info("newer sync entry, ignoring old one") - #logging.info("check4commit 9.1") - return False - else: - #logging.info("check4commit 9.2") - return True - - #logging.info("check4commit 9.3") - return True - - def writeSQLTupel(self,newSQLs,lastdate): - if (newSQLs==None): - return - - self.concernedRows=None - pausenzaehler=0 - logging.info("writeSQLTupel got "+str(len(newSQLs))+" sql tupels") - for newSQL in newSQLs: - #print "" - #print "SQL1: ",newSQL[1] - #print "SQL2: ",newSQL[2] - #print "SQL3: ",newSQL[3] - #print "Param:",string.split(newSQL[3]," <> ") - #print "" - if (newSQL[3]!=""): - param=string.split(newSQL[3]," <> ") - else: - param=None - - if (len(newSQL)>2): - commitSQL=True - - if (newSQL[5]!=None)and(len(newSQL[5])>0): - commitSQL=self.check4commit(newSQL,lastdate) - - if (commitSQL==True): - self.db.speichereSQL(newSQL[2],param,commit=False,pcdatum=newSQL[1],rowid=newSQL[5]) - else: - logging.error("writeSQLTupel: Error") - - pausenzaehler+=1 - if (pausenzaehler % 10)==0: - self.pulse() - while (gtk.events_pending()): - gtk.main_iteration(); - - logging.info("Alle SQLs an sqlite geschickt, commiting now") - self.db.commitSQL() - logging.info("Alle SQLs commited") - - - def doSync(self,sync_uuid,pcdatum,newSQLs,pcdatumjetzt): - #print uuid,pcdatum,newSQLs - #logging.info("doSync 0") - self.changeSyncStatus(True,_("sync process running")) - self.pulse() - #logging.info("doSync 1") - - while (gtk.events_pending()): - gtk.main_iteration(); - diff=time.time()-pcdatumjetzt - if diff<0: - diff=diff*(-1) - if diff>30: - return -1 - - logging.info("doSync read sqls") - sql="SELECT * FROM logtable WHERE pcdatum>?" - rows=self.db.ladeSQL(sql,(pcdatum,)) - logging.info("doSync read sqls") - self.writeSQLTupel(newSQLs,pcdatum) - logging.info("doSync wrote "+str(len(newSQLs))+" sqls") - logging.info("doSync sending "+str(len(rows))+" sqls") - i=0 - return rows - - def getRemoteSyncUUID(self): - return self.sync_uuid - - - def startServer(self, widget, data=None): - #Starte RPCServer - self.db.speichereDirekt("syncServerIP",self.comboIP.get_child().get_text()) - - if (widget.get_active()==True): - logging.info("Starting Server") - - try: - ip=self.comboIP.get_child().get_text() - self.rpcserver = SimpleXMLRPCServer((ip, self.port),allow_none=True) - self.rpcserver.register_function(pow) - self.rpcserver.register_function(self.getLastSyncDate) - self.rpcserver.register_function(self.doSync) - self.rpcserver.register_function(self.getRemoteSyncUUID) - self.rpcserver.register_function(self.doSaveFinalTime) - self.rpcserver.register_function(self.pulse) - self.poll=select.poll() - self.poll.register(self.rpcserver.fileno()) - gobject.timeout_add(1000, self.handleRPC) - self.syncServerStatusLabel.set_text(_("Syncserver running...")) - - #save - self.db.speichereDirekt("startSyncServer",True) - - except: - s=str(sys.exc_info()) - logging.error("libsync: could not start server. Error: "+s) - mbox=gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_ERROR,gtk.BUTTONS_OK,_("Sync server could not start. Please check IP and port.")) #gtk.DIALOG_MODAL - mbox.set_modal(False) - response=mbox.run() - mbox.hide() - mbox.destroy() - widget.set_active(False) - - else: - logging.info("Stopping Server") - try: - del self.rpcserver - except: - pass - self.syncServerStatusLabel.set_text(_("Syncserver not running...")) - #save - self.db.speichereDirekt("startSyncServer",False) - - def doSaveFinalTime(self,sync_uuid,pcdatum=None): - if (pcdatum==None): pcdatum=int(time.time()) - if (time.time()>pcdatum): - pcdatum=int(time.time()) #größere Zeit nehmen - - self.pulse() - - #fime save time+uuid - sql="DELETE FROM sync WHERE uuid=?" - self.db.speichereSQL(sql,(sync_uuid,),log=False) - sql="INSERT INTO sync (syncpartner,uuid,pcdatum) VALUES (?,?,?)" - self.db.speichereSQL(sql,("x",str(sync_uuid),pcdatum),log=False) - self.pulse() - self.changeSyncStatus(False,_("no sync process (at the moment)")) - return (self.sync_uuid,pcdatum) - - - def syncButton(self, widget, data=None): - logging.info("Syncing") - #sql="DELETE FROM logtable WHERE sql LIKE externeStundenplanung" - #self.db.speichereSQL(sql) - - self.changeSyncStatus(True,_("sync process running")) - while (gtk.events_pending()): - gtk.main_iteration(); - - self.db.speichereDirekt("syncRemoteIP",self.comboRemoteIP.get_child().get_text()) - try: - self.server = xmlrpclib.ServerProxy("http://"+self.comboRemoteIP.get_child().get_text()+":"+str(self.port),allow_none=True) - #lastDate=server.getLastSyncDate(str(self.sync_uuid)) - server_sync_uuid=self.server.getRemoteSyncUUID() - lastDate=self.getLastSyncDate(str(server_sync_uuid)) - - #print ("LastSyncDate: "+str(lastDate)+" Now: "+str(int(time.time()))) - - sql="SELECT * FROM logtable WHERE pcdatum>?" - rows=self.db.ladeSQL(sql,(lastDate,)) - - logging.info("loaded concerned rows") - - newSQLs=self.server.doSync(self.sync_uuid,lastDate,rows,time.time()) - - logging.info("did do sync, processing sqls now") - if newSQLs!=-1: - self.writeSQLTupel(newSQLs,lastDate) - - sync_uuid, finalpcdatum=self.server.doSaveFinalTime(self.sync_uuid) - self.doSaveFinalTime(sync_uuid, finalpcdatum) - - self.changeSyncStatus(False,_("no sync process (at the moment)")) - - mbox = gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,_("Synchronization successfully completed")) - response = mbox.run() - mbox.hide() - mbox.destroy() - else: - logging.warning("Zeitdiff zu groß/oder anderer db-Fehler") - self.changeSyncStatus(False,_("no sync process (at the moment)")) - mbox = gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,_("The clocks are not synchronized between stations")) - response = mbox.run() - mbox.hide() - mbox.destroy() - except: - logging.warning("Sync connect failed") - self.changeSyncStatus(False,_("no sync process (at the moment)")) - mbox = gtk.MessageDialog(None,gtk.DIALOG_MODAL,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,_("Sync failed, reason: ")+unicode(sys.exc_info()[1][1])) - response = mbox.run() - mbox.hide() - mbox.destroy() - self.server=None - self.server=None - - - - - def __init__(self,db,parentwindow,port): - gtk.VBox.__init__(self,homogeneous=False, spacing=0) - - logging.info("Sync, init") - self.db=db - self.progress=None - self.server=None - self.port=int(port) - self.parentwindow=parentwindow - self.concernedRows=None - - #print "Sync, 2" - #sql = "DROP TABLE sync" - #self.db.speichereSQL(sql,log=False) - - sql = "CREATE TABLE sync (id INTEGER PRIMARY KEY, syncpartner TEXT, uuid TEXT, pcdatum INTEGER)" - self.db.speichereSQL(sql,log=False) - - #print "Sync, 3" - - sql="SELECT uuid,pcdatum FROM sync WHERE syncpartner=?" - rows=self.db.ladeSQL(sql,("self",)) #Eigene Id feststellen - - #print "Sync, 3a" - if (rows==None)or(len(rows)!=1): - sql="DELETE FROM sync WHERE syncpartner=?" - self.db.speichereSQL(sql,("self",),log=False) - - #uuid1=uuid() - #print "Sync, 3b" - - #print "Sync, 3bb" - self.sync_uuid=str(uuid.uuid4()) - sql="INSERT INTO sync (syncpartner,uuid,pcdatum) VALUES (?,?,?)" - self.db.speichereSQL(sql,("self",str(self.sync_uuid),int(time.time())),log=False) - #print "Sync, 3c" - else: - sync_uuid,pcdatum = rows[0] - self.sync_uuid=sync_uuid - #print "x1" - - - - #print "Sync, 4" - - - frame=gtk.Frame(_("Local SyncServer (port ")+str(self.port)+")") - - - - self.comboIP=gtk.combo_box_entry_new_text() - - - self.comboIP.append_text("") #self.get_ip_address("eth0")) - #self.comboIP.append_text(self.get_ip_address("eth1")) #fixme - #self.comboIP.append_text(self.get_ip_address("eth2")) - #self.comboIP.append_text(self.get_ip_address("eth3")) - #print "Sync, 4d" - #self.comboIP.append_text(self.get_ip_address("wlan0")) - #self.comboIP.append_text(self.get_ip_address("wlan1")) - - #print "Sync, 4e" - - frame.add(self.comboIP) - serverbutton=gtk.ToggleButton(_("Start SyncServer")) - serverbutton.connect("clicked",self.startServer,(None,)) - self.pack_start(frame, expand=False, fill=True, padding=1) - self.pack_start(serverbutton, expand=False, fill=True, padding=1) - self.syncServerStatusLabel=gtk.Label(_("Syncserver not running")) - self.pack_start(self.syncServerStatusLabel, expand=False, fill=True, padding=1) - - frame=gtk.Frame(_("RemoteSync-Server (Port ")+str(self.port)+")") - self.comboRemoteIP=gtk.combo_box_entry_new_text() - self.comboRemoteIP.append_text("192.168.0.?") - self.comboRemoteIP.append_text("192.168.1.?") - self.comboRemoteIP.append_text("192.168.176.?") - frame.add(self.comboRemoteIP) - syncbutton=gtk.Button(_("Connect to remote SyncServer")) - syncbutton.connect("clicked",self.syncButton,(None,)) - self.pack_start(frame, expand=False, fill=True, padding=1) - self.pack_start(syncbutton, expand=False, fill=True, padding=1) - self.syncStatusLabel=gtk.Label(_("no sync process (at the moment)")) - self.pack_start(self.syncStatusLabel, expand=False, fill=True, padding=1) - - - #self.comboRemoteIP.set_text_column("Test") - self.comboRemoteIP.get_child().set_text(self.db.ladeDirekt("syncRemoteIP")) - self.comboIP.get_child().set_text(self.db.ladeDirekt("syncServerIP")) - - #load - if (self.db.ladeDirekt("startSyncServer",False)==True): - serverbutton.set_active(True) - - #print "Sync, 9" diff --git a/src/multilistclasses/libview.py b/src/multilistclasses/libview.py deleted file mode 100644 index 481464c..0000000 --- a/src/multilistclasses/libview.py +++ /dev/null @@ -1,563 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" - This file is part of Multilist. - - Multilist is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Multilist is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Multilist. If not, see . - - Copyright (C) 2008 Christoph Würstle -""" - -import gtk -import gobject -import logging -import pango -import libliststorehandler - - -class Columns_dialog(gtk.VBox): - - def is_col_selected(self, icol): - children=self.framebox.get_children() - if icol5: - default="0" - else: - default="1" - if self.db.ladeDirekt("showcol_"+str(self.liststorehandler.get_colname(i)),default)=="1": - - if (i==1): - self.cell[i] = CellRendererTriple() - self.tvcolumn[i] = gtk.TreeViewColumn(self.liststorehandler.get_colname(i),self.cell[i]) - self.cell[i].connect( 'status_changed', self.col_toggled) - self.tvcolumn[i].set_attributes( self.cell[i], status=i) - - elif (i==3)or(i==4)or(i==6): - self.cell[i] = gtk.CellRendererCombo() - self.tvcolumn[i] = gtk.TreeViewColumn(self.liststorehandler.get_colname(i),self.cell[i]) - self.cell[i].set_property("model",m) - self.cell[i].set_property('text-column', i) - self.cell[i].set_property('editable',True) - self.cell[i].connect("edited", self.col_edited,i) - self.tvcolumn[i].set_attributes( self.cell[i], text=i) - else: - self.cell[i] = gtk.CellRendererText() - self.tvcolumn[i] = gtk.TreeViewColumn(self.liststorehandler.get_colname(i),self.cell[i]) - self.cell[i].set_property('editable',True) - self.cell[i].set_property('editable-set',True) - self.cell[i].connect("edited", self.col_edited,i) - #self.cell[i].connect("editing-canceled", self.col_edited2,i) - self.tvcolumn[i].set_attributes(self.cell[i], text=i) - - self.cell[i].set_property('cell-background', 'lightgray') - self.tvcolumn[i].set_sort_column_id(i) - self.tvcolumn[i].set_resizable(True) - - - if (i>0): - self.treeview.append_column(self.tvcolumn[i]) - - - # Allow NOT drag and drop reordering of rows - self.treeview.set_reorderable(False) - - - if self.scrolled_window != None: - self.scrolled_window.destroy() - - self.scrolled_window = gtk.ScrolledWindow() - self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) - - self.scrolled_window.add(self.treeview) - self.pack_start(self.scrolled_window, expand=True, fill=True, padding=0) - self.loadList() - - self.show_all() - - def __init__(self,db,liststorehandler,parent_window): - - self.db=db - self.parent_window=parent_window - self.liststorehandler = liststorehandler - - - gtk.VBox.__init__(self,homogeneous=False, spacing=0) - - logging.info("libview, init") - - self.scrolled_window = None - self.reload_view() - - - - - - """ - bearbeitenFrame=gtk.Frame("Verteilung kopieren nach") - bearbeitenvBox=gtk.VBox(homogeneous=False, spacing=0) - - bearbeitenhBox=gtk.HBox(homogeneous=False, spacing=0) - self.comboKlassen = gtk.combo_box_new_text() - bearbeitenhBox.pack_start(self.comboKlassen, expand=False, fill=True, padding=0) - button=gtk.Button("Kopieren") - button.connect("clicked", self.kopiereStoffverteilung, None) - bearbeitenhBox.pack_start(button, expand=False, fill=True, padding=0) - - label=gtk.Label(" ") - bearbeitenhBox.pack_start(label, expand=False, fill=True, padding=0) - - button=gtk.Button("Export in CSV-Datei") - button.connect("clicked", self.exportStoffverteilung, None) - bearbeitenhBox.pack_start(button, expand=False, fill=True, padding=0) - - bearbeitenvBox.pack_start(bearbeitenhBox, expand=False, fill=True, padding=0) - - - bearbeitenFrame.add(bearbeitenvBox) - self.pack_start(bearbeitenFrame, expand=False, fill=True, padding=0) - """ - - #self.connect("unmap", self.speichere) - #self.connect("map", self.ladeWirklich) - - #self.show_all() - - - - #print "libstoffverteilung 9: ",time.clock() - diff --git a/src/sqldialog.py b/src/sqldialog.py new file mode 100755 index 0000000..2f384a8 --- /dev/null +++ b/src/sqldialog.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + This file is part of Multilist. + + Multilist is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Multilist is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Multilist. If not, see . + + Copyright (C) 2008 Christoph Würstle +""" + +import gobject +import time +import string +import gtk +import sys +import logging + +import libspeichern + + +class sqlDialog(gtk.Dialog): + + def exportSQL(self,filename): + f = open(filename, 'w') + msgstring="" + sql="SELECT pcdatum,sql,param FROM logtable WHERE pcdatum>? ORDER BY pcdatum DESC" + rows=self.db.ladeSQL(sql,(time.time()-2*24*3600,)) + for row in rows: + pcdatum,sql,param = row + datum=str(time.strftime("%d.%m.%y %H:%M:%S ", (time.localtime(pcdatum)))) + f.write( datum +"\t" + sql + "\t\t" + param+ "\n") + + + f.close() + + + def __init__(self,db): + self.db=db + + logging.info("sqldialog, init") + + gtk.Dialog.__init__(self,_("SQL History (the past two days):"),None,gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + + self.add_button(_("Export"), 444) + self.set_position(gtk.WIN_POS_CENTER) + + + self.liststore = gtk.ListStore(str, str, str) + + # create the TreeView using liststore + self.treeview = gtk.TreeView(self.liststore) + + # create a CellRenderers to render the data + self.cell1 = gtk.CellRendererText() + self.cell2 = gtk.CellRendererText() + self.cell3 = gtk.CellRendererText() + #self.cell1.set_property('markup', 1) + + # create the TreeViewColumns to display the data + self.tvcolumn1 = gtk.TreeViewColumn(_('Date')) + self.tvcolumn2 = gtk.TreeViewColumn(_('SQL')) + self.tvcolumn3 = gtk.TreeViewColumn(_('Parameter')) + + # add columns to treeview + self.treeview.append_column(self.tvcolumn1) + self.treeview.append_column(self.tvcolumn2) + self.treeview.append_column(self.tvcolumn3) + + + self.tvcolumn1.pack_start(self.cell1, True) + self.tvcolumn2.pack_start(self.cell2, True) + self.tvcolumn3.pack_start(self.cell3, True) + + self.tvcolumn1.set_attributes(self.cell1, text=0) #Spalten setzten hier!!!! + self.tvcolumn2.set_attributes(self.cell2, text=1) + self.tvcolumn3.set_attributes(self.cell3, text=2) + + # make treeview searchable + #self.treeview.set_search_column(0) + #self.tvcolumn.set_sort_column_id(0) + + # Allow NOT drag and drop reordering of rows + self.treeview.set_reorderable(False) + + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) + scrolled_window.add(self.treeview) + #self.pack_start(scrolled_window, expand=True, fill=True, padding=0) + + + self.vbox.pack_start(scrolled_window, True, True, 0) + + self.vbox.show_all() + + msgstring="" + sql="SELECT pcdatum,sql,param FROM logtable WHERE pcdatum>? ORDER BY pcdatum DESC" + rows=db.ladeSQL(sql,(time.time()-3*24*3600,)) + i=0 + for row in rows: + pcdatum,sql,param = row + datum=str(time.strftime("%d.%m.%y %H:%M:%S ", (time.localtime(pcdatum)))) + if len(param)>100: + param=param[:20]+_(" (Reduced parameter) ")+param[20:] + self.liststore.append([datum, sql,param]) + i+=1 + if (i>50): + break + + self.set_size_request(500,400) + + + diff --git a/src/upload.sh b/src/upload.sh deleted file mode 100755 index 3a3fd46..0000000 --- a/src/upload.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -scp ../../multilist_0.3.0_all.deb user@192.168.0.34:/media/mmc2/ - - - - diff --git a/support/builddeb.py b/support/builddeb.py new file mode 100755 index 0000000..72f777c --- /dev/null +++ b/support/builddeb.py @@ -0,0 +1,188 @@ +#!/usr/bin/python2.5 + +import os +import sys + +try: + import py2deb +except ImportError: + import fake_py2deb as py2deb + +import constants + + +__appname__ = constants.__app_name__ +__description__ = """Simple list management application +. +Homepage: http://multilist.garage.maemo.org/ +""" +__author__ = "Christoph Wurstle" +__email__ = "n800@axique.net" +__version__ = constants.__version__ +__build__ = constants.__build__ +__changelog__ = """ +0.3.6 +* Adding filtering for new and complete in addition to all and active + +0.3.5 +* Bugfix: Fixing the application launcher + +0.3.4 +* Making rotation configurable in the Settings window +* Persisting full-screen / rotation settings + +0.3.3 +* Rotation support +* Turned the settings dialog into a window + +0.3.2 +* Massive code cleanup +* Re-arrangement of GTK Menus +* Added Maemo 5 Menu +* Moved Active/All filter to menu +* Moved Checkout All to menu +* Improved Search bar +* Removed unnesary UI elements +* Switched to real inconsistent check boxes for tasks +* Switching the settings dialog to be more Maemo 5 like + +0.3.1 +* I18N, extract de.po, add ru.po. + +0.3.0 +* Initial Release. +""" + + +__postinstall__ = """#!/bin/sh -e + +gtk-update-icon-cache -f /usr/share/icons/hicolor +rm -f ~/.multilist/multilist.log +""" + + +def find_files(path, root): + print path, root + for unusedRoot, dirs, files in os.walk(path): + for file in files: + if file.startswith(root+"-"): + print "\t", root, file + fileParts = file.split("-") + unused, relPathParts, newName = fileParts[0], fileParts[1:-1], fileParts[-1] + assert unused == root + relPath = os.sep.join(relPathParts) + yield relPath, file, newName + + +def unflatten_files(files): + d = {} + for relPath, oldName, newName in files: + if relPath not in d: + d[relPath] = [] + d[relPath].append((oldName, newName)) + return d + + +def build_package(distribution): + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + + py2deb.Py2deb.SECTIONS = py2deb.SECTIONS_BY_POLICY[distribution] + p = py2deb.Py2deb(__appname__) + p.prettyName = constants.__pretty_app_name__ + p.description = __description__ + p.bugTracker = "https://bugs.maemo.org/enter_bug.cgi?product=quicknote" + p.upgradeDescription = __changelog__.split("\n\n", 1)[0] + p.author = __author__ + p.mail = __email__ + p.license = "gpl" + p.depends = ", ".join([ + "python2.6 | python2.5", + "python-gtk2 | python2.5-gtk2", + "python-xml | python2.5-xml", + "python-dbus | python2.5-dbus", + ]) + maemoSpecificDepends = ", python-osso | python2.5-osso, python-hildon | python2.5-hildon" + p.depends += { + "debian": ", python-glade2", + "diablo": maemoSpecificDepends, + "fremantle": maemoSpecificDepends + ", python-glade2", + }[distribution] + p.section = { + "debian": "editors", + "diablo": "user/office", + "fremantle": "user/office", + }[distribution] + p.arch = "all" + p.urgency = "low" + p.distribution = "diablo fremantle debian" + p.repository = "extras" + p.changelog = __changelog__ + p.postinstall = __postinstall__ + p.icon = { + "debian": "26x26-multilist.png", + "diablo": "26x26-multilist.png", + "fremantle": "40x40-multilist.png", # Fremantle natively uses 48x48 + }[distribution] + p["/usr/bin"] = [ "multilist.py" ] + for relPath, files in unflatten_files(find_files(".", "locale")).iteritems(): + fullPath = "/usr/share/locale" + if relPath: + fullPath += os.sep+relPath + p[fullPath] = list( + "|".join((oldName, newName)) + for (oldName, newName) in files + ) + for relPath, files in unflatten_files(find_files(".", "src")).iteritems(): + fullPath = "/usr/lib/multilist" + if relPath: + fullPath += os.sep+relPath + p[fullPath] = list( + "|".join((oldName, newName)) + for (oldName, newName) in files + ) + p["/usr/share/applications/hildon"] = ["multilist.desktop"] + p["/usr/share/icons/hicolor/26x26/hildon"] = ["26x26-multilist.png|multilist.png"] + p["/usr/share/icons/hicolor/40x40/hildon"] = ["40x40-multilist.png|multilist.png"] + p["/usr/share/icons/hicolor/scalable/hildon"] = ["scale-multilist.png|multilist.png"] + + if distribution == "debian": + print p + print p.generate( + version="%s-%s" % (__version__, __build__), + changelog=__changelog__, + build=True, + tar=False, + changes=False, + dsc=False, + ) + print "Building for %s finished" % distribution + else: + print p + print p.generate( + version="%s-%s" % (__version__, __build__), + changelog=__changelog__, + build=False, + tar=True, + changes=True, + dsc=True, + ) + print "Building for %s finished" % distribution + + +if __name__ == "__main__": + if len(sys.argv) > 1: + try: + import optparse + except ImportError: + optparse = None + + if optparse is not None: + parser = optparse.OptionParser() + (commandOptions, commandArgs) = parser.parse_args() + else: + commandArgs = None + commandArgs = ["diablo"] + build_package(commandArgs[0]) diff --git a/support/fake_py2deb.py b/support/fake_py2deb.py new file mode 100644 index 0000000..5d6149d --- /dev/null +++ b/support/fake_py2deb.py @@ -0,0 +1,56 @@ +import pprint + + +class Py2deb(object): + + def __init__(self, appName): + self._appName = appName + self.description = "" + self.author = "" + self.mail = "" + self.license = "" + self.depends = "" + self.section = "" + self.arch = "" + self.ugency = "" + self.distribution = "" + self.repository = "" + self.changelog = "" + self.postinstall = "" + self.icon = "" + self._install = {} + + def generate(self, appVersion, appBuild, changelog, tar, dsc, changes, build, src): + return """ +Package: %s +version: %s-%s +Changes: +%s + +Build Options: + Tar: %s + Dsc: %s + Changes: %s + Build: %s + Src: %s + """ % ( + self._appName, appVersion, appBuild, changelog, tar, dsc, changes, build, src + ) + + def __str__(self): + parts = [] + parts.append("%s Package Settings:" % (self._appName, )) + for settingName in dir(self): + if settingName.startswith("_"): + continue + parts.append("\t%s: %s" % (settingName, getattr(self, settingName))) + + parts.append(pprint.pformat(self._install)) + + return "\n".join(parts) + + def __getitem__(self, key): + return self._install[key] + + def __setitem__(self, key, item): + self._install[key] = item diff --git a/support/py2deb.py b/support/py2deb.py new file mode 100644 index 0000000..4a47513 --- /dev/null +++ b/support/py2deb.py @@ -0,0 +1,991 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +## +## Copyright (C) 2009 manatlan manatlan[at]gmail(dot)com +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +""" +Known limitations : +- don't sign package (-us -uc) +- no distinctions between author and maintainer(packager) + +depends on : +- dpkg-dev (dpkg-buildpackage) +- alien +- python +- fakeroot + +changelog + - ??? ?/??/20?? (By epage) + - PEP8 + - added recommends + - fixed bug where it couldn't handle the contents of the pre/post scripts being specified + - Added customization based on the targeted policy for sections (Maemo support) + - Added maemo specific tarball, dsc, changes file generation support (including icon support) + - Added armel architecture + - Reduced the size of params being passed around by reducing the calls to locals() + - Added respository, distribution, priority + - Made setting control file a bit more flexible + - 0.5 05/09/2009 + - pre/post install/remove scripts enabled + - deb package install py2deb in dist-packages for py2.6 + - 0.4 14/10/2008 + - use os.environ USERNAME or USER (debian way) + - install on py 2.(4,5,6) (*FIX* do better here) + +""" + +import os +import hashlib +import sys +import shutil +import time +import string +import StringIO +import stat +import commands +import base64 +import tarfile +from glob import glob +from datetime import datetime +import socket # gethostname() +from subprocess import Popen, PIPE + +#~ __version__ = "0.4" +__version__ = "0.5" +__author__ = "manatlan" +__mail__ = "manatlan@gmail.com" + + +PERMS_URW_GRW_OR = stat.S_IRUSR | stat.S_IWUSR | \ + stat.S_IRGRP | stat.S_IWGRP | \ + stat.S_IROTH + +UID_ROOT = 0 +GID_ROOT = 0 + + +def run(cmds): + p = Popen(cmds, shell=False, stdout=PIPE, stderr=PIPE) + time.sleep(0.01) # to avoid "IOError: [Errno 4] Interrupted system call" + out = string.join(p.stdout.readlines()).strip() + outerr = string.join(p.stderr.readlines()).strip() + return out + + +def deb2rpm(file): + txt=run(['alien', '-r', file]) + return txt.split(" generated")[0] + + +def py2src(TEMP, name): + l=glob("%(TEMP)s/%(name)s*.tar.gz" % locals()) + if len(l) != 1: + raise Py2debException("don't find source package tar.gz") + + tar = os.path.basename(l[0]) + shutil.move(l[0], tar) + + return tar + + +def md5sum(filename): + f = open(filename, "r") + try: + return hashlib.md5(f.read()).hexdigest() + finally: + f.close() + + +class Py2changes(object): + + def __init__(self, ChangedBy, description, changes, files, category, repository, **kwargs): + self.options = kwargs # TODO: Is order important? + self.description = description + self.changes=changes + self.files=files + self.category=category + self.repository=repository + self.ChangedBy=ChangedBy + + def getContent(self): + content = ["%s: %s" % (k, v) + for k,v in self.options.iteritems()] + + if self.description: + description=self.description.replace("\n","\n ") + content.append('Description: ') + content.append(' %s' % description) + if self.changes: + changes=self.changes.replace("\n","\n ") + content.append('Changes: ') + content.append(' %s' % changes) + if self.ChangedBy: + content.append("Changed-By: %s" % self.ChangedBy) + + content.append('Files:') + + for onefile in self.files: + md5 = md5sum(onefile) + size = os.stat(onefile).st_size.__str__() + content.append(' ' + md5 + ' ' + size + ' ' + self.category +' '+self.repository+' '+os.path.basename(onefile)) + + return "\n".join(content) + "\n\n" + + +def py2changes(params): + changescontent = Py2changes( + "%(author)s <%(mail)s>" % params, + "%(description)s" % params, + "%(changelog)s" % params, + ( + "%(TEMP)s/%(name)s_%(version)s.tar.gz" % params, + "%(TEMP)s/%(name)s_%(version)s.dsc" % params, + ), + "%(section)s" % params, + "%(repository)s" % params, + Format='1.7', + Date=time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()), + Source="%(name)s" % params, + Architecture="%(arch)s" % params, + Version="%(version)s" % params, + Distribution="%(distribution)s" % params, + Urgency="%(urgency)s" % params, + Maintainer="%(author)s <%(mail)s>" % params + ) + f = open("%(TEMP)s/%(name)s_%(version)s.changes" % params,"wb") + f.write(changescontent.getContent()) + f.close() + + fileHandle = open('/tmp/py2deb2.tmp', 'w') + fileHandle.write('#!/bin/sh\n') + fileHandle.write("cd " +os.getcwd()+ "\n") + # TODO Renable signing + # fileHandle.write("gpg --local-user %(mail)s --clearsign %(TEMP)s/%(name)s_%(version)s.changes\n" % params) + fileHandle.write("mv %(TEMP)s/%(name)s_%(version)s.changes.asc %(TEMP)s/%(name)s_%(version)s.changes\n" % params) + fileHandle.write('\nexit') + fileHandle.close() + commands.getoutput("chmod 777 /tmp/py2deb2.tmp") + commands.getoutput("/tmp/py2deb2.tmp") + + ret = [] + + l=glob("%(TEMP)s/%(name)s*.tar.gz" % params) + if len(l)!=1: + raise Py2debException("don't find source package tar.gz") + tar = os.path.basename(l[0]) + shutil.move(l[0],tar) + ret.append(tar) + + l=glob("%(TEMP)s/%(name)s*.dsc" % params) + if len(l)!=1: + raise Py2debException("don't find source package dsc") + tar = os.path.basename(l[0]) + shutil.move(l[0],tar) + ret.append(tar) + + l = glob("%(TEMP)s/%(name)s*.changes" % params) + if len(l)!=1: + raise Py2debException("don't find source package changes") + tar = os.path.basename(l[0]) + shutil.move(l[0],tar) + ret.append(tar) + + return ret + + +class Py2dsc(object): + + def __init__(self, StandardsVersion, BuildDepends, files, **kwargs): + self.options = kwargs # TODO: Is order important? + self.StandardsVersion = StandardsVersion + self.BuildDepends=BuildDepends + self.files=files + + @property + def content(self): + content = ["%s: %s" % (k, v) + for k,v in self.options.iteritems()] + + if self.BuildDepends: + content.append("Build-Depends: %s" % self.BuildDepends) + if self.StandardsVersion: + content.append("Standards-Version: %s" % self.StandardsVersion) + + content.append('Files:') + + for onefile in self.files: + print onefile + md5 = md5sum(onefile) + size = os.stat(onefile).st_size.__str__() + content.append(' '+md5 + ' ' + size +' '+os.path.basename(onefile)) + + return "\n".join(content)+"\n\n" + + +def py2dsc(TEMP, name, version, depends, author, mail, arch): + dsccontent = Py2dsc( + "%(version)s" % locals(), + "%(depends)s" % locals(), + ("%(TEMP)s/%(name)s_%(version)s.tar.gz" % locals(),), + Format='1.0', + Source="%(name)s" % locals(), + Version="%(version)s" % locals(), + Maintainer="%(author)s <%(mail)s>" % locals(), + Architecture="%(arch)s" % locals(), + ) + + filename = "%(TEMP)s/%(name)s_%(version)s.dsc" % locals() + + f = open(filename, "wb") + try: + f.write(dsccontent.content) + finally: + f.close() + + fileHandle = open('/tmp/py2deb.tmp', 'w') + try: + fileHandle.write('#!/bin/sh\n') + fileHandle.write("cd " + os.getcwd() + "\n") + # TODO Renable signing + # fileHandle.write("gpg --local-user %(mail)s --clearsign %(TEMP)s/%(name)s_%(version)s.dsc\n" % locals()) + fileHandle.write("mv %(TEMP)s/%(name)s_%(version)s.dsc.asc %(filename)s\n" % locals()) + fileHandle.write('\nexit') + fileHandle.close() + finally: + f.close() + + commands.getoutput("chmod 777 /tmp/py2deb.tmp") + commands.getoutput("/tmp/py2deb.tmp") + + return filename + + +class Py2tar(object): + + def __init__(self, dataDirectoryPath): + self._dataDirectoryPath = dataDirectoryPath + + def packed(self): + return self._getSourcesFiles() + + def _getSourcesFiles(self): + directoryPath = self._dataDirectoryPath + + outputFileObj = StringIO.StringIO() # TODO: Do more transparently? + + tarOutput = tarfile.TarFile.open('sources', + mode = "w:gz", + fileobj = outputFileObj) + + # Note: We can't use this because we need to fiddle permissions: + # tarOutput.add(directoryPath, arcname = "") + + for root, dirs, files in os.walk(directoryPath): + archiveRoot = root[len(directoryPath):] + + tarinfo = tarOutput.gettarinfo(root, archiveRoot) + # TODO: Make configurable? + tarinfo.uid = UID_ROOT + tarinfo.gid = GID_ROOT + tarinfo.uname = "" + tarinfo.gname = "" + tarOutput.addfile(tarinfo) + + for f in files: + tarinfo = tarOutput.gettarinfo(os.path.join(root, f), + os.path.join(archiveRoot, f)) + tarinfo.uid = UID_ROOT + tarinfo.gid = GID_ROOT + tarinfo.uname = "" + tarinfo.gname = "" + tarOutput.addfile(tarinfo, file(os.path.join(root, f))) + + tarOutput.close() + + data_tar_gz = outputFileObj.getvalue() + + return data_tar_gz + + +def py2tar(DEST, TEMP, name, version): + tarcontent = Py2tar("%(DEST)s" % locals()) + filename = "%(TEMP)s/%(name)s_%(version)s.tar.gz" % locals() + f = open(filename, "wb") + try: + f.write(tarcontent.packed()) + finally: + f.close() + return filename + + +class Py2debException(Exception): + pass + + +SECTIONS_BY_POLICY = { + # http://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections + "debian": "admin, base, comm, contrib, devel, doc, editors, electronics, embedded, games, gnome, graphics, hamradio, interpreters, kde, libs, libdevel, mail, math, misc, net, news, non-free, oldlibs, otherosfs, perl, python, science, shells, sound, tex, text, utils, web, x11", + # http://maemo.org/forrest-images/pdf/maemo-policy.pdf + "chinook": "accessories, communication, games, multimedia, office, other, programming, support, themes, tools", + # http://wiki.maemo.org/Task:Package_categories + "diablo": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities", + # http://wiki.maemo.org/Task:Fremantle_application_categories + "mer": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities", + # http://wiki.maemo.org/Task:Fremantle_application_categories + "fremantle": "user/desktop, user/development, user/education, user/games, user/graphics, user/multimedia, user/navigation, user/network, user/office, user/science, user/system, user/utilities", +} + + +LICENSE_AGREEMENT = { + "gpl": """ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU General +Public License can be found in `/usr/share/common-licenses/GPL'. +""", + "lgpl":""" + This package is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this package; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +On Debian systems, the complete text of the GNU Lesser General +Public License can be found in `/usr/share/common-licenses/LGPL'. +""", + "bsd": """ + Redistribution and use in source and binary forms, with or without + modification, are permitted under the terms of the BSD License. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +On Debian systems, the complete text of the BSD License can be +found in `/usr/share/common-licenses/BSD'. +""", + "artistic": """ + This program is free software; you can redistribute it and/or modify it + under the terms of the "Artistic License" which comes with Debian. + + THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES + OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +On Debian systems, the complete text of the Artistic License +can be found in `/usr/share/common-licenses/Artistic'. +""" +} + + +class Py2deb(object): + """ + heavily based on technic described here : + http://wiki.showmedo.com/index.php?title=LinuxJensMakingDeb + """ + ## STATICS + clear = False # clear build folder after py2debianization + + SECTIONS = SECTIONS_BY_POLICY["debian"] + + #http://www.debian.org/doc/debian-policy/footnotes.html#f69 + ARCHS = "all i386 ia64 alpha amd64 armeb arm hppa m32r m68k mips mipsel powerpc ppc64 s390 s390x sh3 sh3eb sh4 sh4eb sparc darwin-i386 darwin-ia64 darwin-alpha darwin-amd64 darwin-armeb darwin-arm darwin-hppa darwin-m32r darwin-m68k darwin-mips darwin-mipsel darwin-powerpc darwin-ppc64 darwin-s390 darwin-s390x darwin-sh3 darwin-sh3eb darwin-sh4 darwin-sh4eb darwin-sparc freebsd-i386 freebsd-ia64 freebsd-alpha freebsd-amd64 freebsd-armeb freebsd-arm freebsd-hppa freebsd-m32r freebsd-m68k freebsd-mips freebsd-mipsel freebsd-powerpc freebsd-ppc64 freebsd-s390 freebsd-s390x freebsd-sh3 freebsd-sh3eb freebsd-sh4 freebsd-sh4eb freebsd-sparc kfreebsd-i386 kfreebsd-ia64 kfreebsd-alpha kfreebsd-amd64 kfreebsd-armeb kfreebsd-arm kfreebsd-hppa kfreebsd-m32r kfreebsd-m68k kfreebsd-mips kfreebsd-mipsel kfreebsd-powerpc kfreebsd-ppc64 kfreebsd-s390 kfreebsd-s390x kfreebsd-sh3 kfreebsd-sh3eb kfreebsd-sh4 kfreebsd-sh4eb kfreebsd-sparc knetbsd-i386 knetbsd-ia64 knetbsd-alpha knetbsd-amd64 knetbsd-armeb knetbsd-arm knetbsd-hppa knetbsd-m32r knetbsd-m68k knetbsd-mips knetbsd-mipsel knetbsd-powerpc knetbsd-ppc64 knetbsd-s390 knetbsd-s390x knetbsd-sh3 knetbsd-sh3eb knetbsd-sh4 knetbsd-sh4eb knetbsd-sparc netbsd-i386 netbsd-ia64 netbsd-alpha netbsd-amd64 netbsd-armeb netbsd-arm netbsd-hppa netbsd-m32r netbsd-m68k netbsd-mips netbsd-mipsel netbsd-powerpc netbsd-ppc64 netbsd-s390 netbsd-s390x netbsd-sh3 netbsd-sh3eb netbsd-sh4 netbsd-sh4eb netbsd-sparc openbsd-i386 openbsd-ia64 openbsd-alpha openbsd-amd64 openbsd-armeb openbsd-arm openbsd-hppa openbsd-m32r openbsd-m68k openbsd-mips openbsd-mipsel openbsd-powerpc openbsd-ppc64 openbsd-s390 openbsd-s390x openbsd-sh3 openbsd-sh3eb openbsd-sh4 openbsd-sh4eb openbsd-sparc hurd-i386 hurd-ia64 hurd-alpha hurd-amd64 hurd-armeb hurd-arm hurd-hppa hurd-m32r hurd-m68k hurd-mips hurd-mipsel hurd-powerpc hurd-ppc64 hurd-s390 hurd-s390x hurd-sh3 hurd-sh3eb hurd-sh4 hurd-sh4eb hurd-sparc armel".split(" ") + + # license terms taken from dh_make + LICENSES = list(LICENSE_AGREEMENT.iterkeys()) + + def __setitem__(self, path, files): + + if not type(files)==list: + raise Py2debException("value of key path '%s' is not a list"%path) + if not files: + raise Py2debException("value of key path '%s' should'nt be empty"%path) + if not path.startswith("/"): + raise Py2debException("key path '%s' malformed (don't start with '/')"%path) + if path.endswith("/"): + raise Py2debException("key path '%s' malformed (shouldn't ends with '/')"%path) + + nfiles=[] + for file in files: + + if ".." in file: + raise Py2debException("file '%s' contains '..', please avoid that!"%file) + + + if "|" in file: + if file.count("|")!=1: + raise Py2debException("file '%s' is incorrect (more than one pipe)"%file) + + file, nfile = file.split("|") + else: + nfile=file # same localisation + + if os.path.isdir(file): + raise Py2debException("file '%s' is a folder, and py2deb refuse folders !"%file) + + if not os.path.isfile(file): + raise Py2debException("file '%s' doesn't exist"%file) + + if file.startswith("/"): # if an absolute file is defined + if file==nfile: # and not renamed (pipe trick) + nfile=os.path.basename(file) # it's simply copied to 'path' + + nfiles.append((file, nfile)) + + nfiles.sort(lambda a, b: cmp(a[1], b[1])) #sort according new name (nfile) + + self.__files[path]=nfiles + + def __delitem__(self, k): + del self.__files[k] + + def __init__(self, + name, + description="no description", + license="gpl", + depends="", + section="utils", + arch="all", + + url="", + author = None, + mail = None, + + preinstall = None, + postinstall = None, + preremove = None, + postremove = None + ): + + if author is None: + author = ("USERNAME" in os.environ) and os.environ["USERNAME"] or None + if author is None: + author = ("USER" in os.environ) and os.environ["USER"] or "unknown" + + if mail is None: + mail = author+"@"+socket.gethostname() + + self.name = name + self.prettyName = "" + self.description = description + self.upgradeDescription = "" + self.bugTracker = "" + self.license = license + self.depends = depends + self.recommends = "" + self.section = section + self.arch = arch + self.url = url + self.author = author + self.mail = mail + self.icon = "" + self.distribution = "" + self.respository = "" + self.urgency = "low" + + self.preinstall = preinstall + self.postinstall = postinstall + self.preremove = preremove + self.postremove = postremove + + self.__files={} + + def __repr__(self): + name = self.name + license = self.license + description = self.description + depends = self.depends + recommends = self.recommends + section = self.section + arch = self.arch + url = self.url + author = self.author + mail = self.mail + + preinstall = self.preinstall + postinstall = self.postinstall + preremove = self.preremove + postremove = self.postremove + + paths=self.__files.keys() + paths.sort() + files=[] + for path in paths: + for file, nfile in self.__files[path]: + #~ rfile=os.path.normpath(os.path.join(path, nfile)) + rfile=os.path.join(path, nfile) + if nfile==file: + files.append(rfile) + else: + files.append(rfile + " (%s)"%file) + + files.sort() + files = "\n".join(files) + + + lscripts = [ preinstall and "preinst", + postinstall and "postinst", + preremove and "prerm", + postremove and "postrm", + ] + scripts = lscripts and ", ".join([i for i in lscripts if i]) or "None" + return """ +---------------------------------------------------------------------- +NAME : %(name)s +---------------------------------------------------------------------- +LICENSE : %(license)s +URL : %(url)s +AUTHOR : %(author)s +MAIL : %(mail)s +---------------------------------------------------------------------- +DEPENDS : %(depends)s +RECOMMENDS : %(recommends)s +ARCH : %(arch)s +SECTION : %(section)s +---------------------------------------------------------------------- +DESCRIPTION : +%(description)s +---------------------------------------------------------------------- +SCRIPTS : %(scripts)s +---------------------------------------------------------------------- +FILES : +%(files)s +""" % locals() + + def generate(self, version, changelog="", rpm=False, src=False, build=True, tar=False, changes=False, dsc=False): + """ generate a deb of version 'version', with or without 'changelog', with or without a rpm + (in the current folder) + return a list of generated files + """ + if not sum([len(i) for i in self.__files.values()])>0: + raise Py2debException("no files are defined") + + if not changelog: + changelog="* no changelog" + + name = self.name + description = self.description + license = self.license + depends = self.depends + recommends = self.recommends + section = self.section + arch = self.arch + url = self.url + distribution = self.distribution + repository = self.repository + urgency = self.urgency + author = self.author + mail = self.mail + files = self.__files + preinstall = self.preinstall + postinstall = self.postinstall + preremove = self.preremove + postremove = self.postremove + + if section not in Py2deb.SECTIONS: + raise Py2debException("section '%s' is unknown (%s)" % (section, str(Py2deb.SECTIONS))) + + if arch not in Py2deb.ARCHS: + raise Py2debException("arch '%s' is unknown (%s)"% (arch, str(Py2deb.ARCHS))) + + if license not in Py2deb.LICENSES: + raise Py2debException("License '%s' is unknown (%s)" % (license, str(Py2deb.LICENSES))) + + # create dates (buildDate, buildDateYear) + d=datetime.now() + buildDate=d.strftime("%a, %d %b %Y %H:%M:%S +0000") + buildDateYear=str(d.year) + + #clean description (add a space before each next lines) + description=description.replace("\r", "").strip() + description = "\n ".join(description.split("\n")) + + #clean changelog (add 2 spaces before each next lines) + changelog=changelog.replace("\r", "").strip() + changelog = "\n ".join(changelog.split("\n")) + + TEMP = ".py2deb_build_folder" + DEST = os.path.join(TEMP, name) + DEBIAN = os.path.join(DEST, "debian") + + packageContents = locals() + + # let's start the process + try: + shutil.rmtree(TEMP) + except: + pass + + os.makedirs(DEBIAN) + try: + rules=[] + dirs=[] + for path in files: + for ofile, nfile in files[path]: + if os.path.isfile(ofile): + # it's a file + + if ofile.startswith("/"): # if absolute path + # we need to change dest + dest=os.path.join(DEST, nfile) + else: + dest=os.path.join(DEST, ofile) + + # copy file to be packaged + destDir = os.path.dirname(dest) + if not os.path.isdir(destDir): + os.makedirs(destDir) + + shutil.copy2(ofile, dest) + + ndir = os.path.join(path, os.path.dirname(nfile)) + nname = os.path.basename(nfile) + + # make a line RULES to be sure the destination folder is created + # and one for copying the file + fpath = "/".join(["$(CURDIR)", "debian", name+ndir]) + rules.append('mkdir -p "%s"' % fpath) + rules.append('cp -a "%s" "%s"' % (ofile, os.path.join(fpath, nname))) + + # append a dir + dirs.append(ndir) + + else: + raise Py2debException("unknown file '' "%ofile) # shouldn't be raised (because controlled before) + + # make rules right + rules= "\n\t".join(rules) + "\n" + packageContents["rules"] = rules + + # make dirs right + dirs= [i[1:] for i in set(dirs)] + dirs.sort() + + #========================================================================== + # CREATE debian/dirs + #========================================================================== + open(os.path.join(DEBIAN, "dirs"), "w").write("\n".join(dirs)) + + #========================================================================== + # CREATE debian/changelog + #========================================================================== + clog="""%(name)s (%(version)s) stable; urgency=low + + %(changelog)s + + -- %(author)s <%(mail)s> %(buildDate)s +""" % packageContents + + open(os.path.join(DEBIAN, "changelog"), "w").write(clog) + + #========================================================================== + #Create pre/post install/remove + #========================================================================== + def mkscript(name, dest): + if name and name.strip()!="": + if os.path.isfile(name): # it's a file + content = file(name).read() + else: # it's a script + content = name + open(os.path.join(DEBIAN, dest), "w").write(content) + + mkscript(preinstall, "preinst") + mkscript(postinstall, "postinst") + mkscript(preremove, "prerm") + mkscript(postremove, "postrm") + + + #========================================================================== + # CREATE debian/compat + #========================================================================== + open(os.path.join(DEBIAN, "compat"), "w").write("5\n") + + #========================================================================== + # CREATE debian/control + #========================================================================== + generalParagraphFields = [ + "Source: %(name)s", + "Maintainer: %(author)s <%(mail)s>", + "Section: %(section)s", + "Priority: extra", + "Build-Depends: debhelper (>= 5)", + "Standards-Version: 3.7.2", + ] + + specificParagraphFields = [ + "Package: %(name)s", + "Architecture: %(arch)s", + "Depends: %(depends)s", + "Recommends: %(recommends)s", + "Description: %(description)s", + ] + + if self.prettyName: + prettyName = "XSBC-Maemo-Display-Name: %s" % self.prettyName.strip() + specificParagraphFields.append("\n ".join(prettyName.split("\n"))) + + if self.bugTracker: + bugTracker = "XSBC-Bugtracker: %s" % self.bugTracker.strip() + specificParagraphFields.append("\n ".join(bugTracker.split("\n"))) + + if self.upgradeDescription: + upgradeDescription = "XSBC-Maemo-Upgrade-Description: %s" % self.upgradeDescription.strip() + specificParagraphFields.append("\n ".join(upgradeDescription.split("\n"))) + + if self.icon: + f = open(self.icon, "rb") + try: + rawIcon = f.read() + finally: + f.close() + uueIcon = base64.b64encode(rawIcon) + uueIconLines = [] + for i, c in enumerate(uueIcon): + if i % 60 == 0: + uueIconLines.append("") + uueIconLines[-1] += c + uueIconLines[0:0] = ("XSBC-Maemo-Icon-26:", ) + specificParagraphFields.append("\n ".join(uueIconLines)) + + generalParagraph = "\n".join(generalParagraphFields) + specificParagraph = "\n".join(specificParagraphFields) + controlContent = "\n\n".join((generalParagraph, specificParagraph)) % packageContents + open(os.path.join(DEBIAN, "control"), "w").write(controlContent) + + #========================================================================== + # CREATE debian/copyright + #========================================================================== + packageContents["txtLicense"] = LICENSE_AGREEMENT[license] + packageContents["pv"] =__version__ + txt="""This package was py2debianized(%(pv)s) by %(author)s <%(mail)s> on +%(buildDate)s. + +It was downloaded from %(url)s + +Upstream Author: %(author)s <%(mail)s> + +Copyright: %(buildDateYear)s by %(author)s + +License: + +%(txtLicense)s + +The Debian packaging is (C) %(buildDateYear)s, %(author)s <%(mail)s> and +is licensed under the GPL, see above. + + +# Please also look if there are files or directories which have a +# different copyright/license attached and list them here. +""" % packageContents + open(os.path.join(DEBIAN, "copyright"), "w").write(txt) + + #========================================================================== + # CREATE debian/rules + #========================================================================== + txt="""#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + + + + +CFLAGS = -Wall -g + +ifneq (,$(findstring noopt,$(DEB_BUILD_OPTIONS))) + CFLAGS += -O0 +else + CFLAGS += -O2 +endif + +configure: configure-stamp +configure-stamp: + dh_testdir + # Add here commands to configure the package. + + touch configure-stamp + + +build: build-stamp + +build-stamp: configure-stamp + dh_testdir + touch build-stamp + +clean: + dh_testdir + dh_testroot + rm -f build-stamp configure-stamp + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + + # ====================================================== + #$(MAKE) DESTDIR="$(CURDIR)/debian/%(name)s" install + mkdir -p "$(CURDIR)/debian/%(name)s" + + %(rules)s + # ====================================================== + +# Build architecture-independent files here. +binary-indep: build install +# We have nothing to do by default. + +# Build architecture-dependent files here. +binary-arch: build install + dh_testdir + dh_testroot + dh_installchangelogs debian/changelog + dh_installdocs + dh_installexamples +# dh_install +# dh_installmenu +# dh_installdebconf +# dh_installlogrotate +# dh_installemacsen +# dh_installpam +# dh_installmime +# dh_python +# dh_installinit +# dh_installcron +# dh_installinfo + dh_installman + dh_link + dh_strip + dh_compress + dh_fixperms +# dh_perl +# dh_makeshlibs + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb + +binary: binary-indep binary-arch +.PHONY: build clean binary-indep binary-arch binary install configure +""" % packageContents + open(os.path.join(DEBIAN, "rules"), "w").write(txt) + os.chmod(os.path.join(DEBIAN, "rules"), 0755) + + ########################################################################### + ########################################################################### + ########################################################################### + + generatedFiles = [] + + if build: + #http://www.debian.org/doc/manuals/maint-guide/ch-build.fr.html + ret = os.system('cd "%(DEST)s"; dpkg-buildpackage -tc -rfakeroot -us -uc' % packageContents) + if ret != 0: + raise Py2debException("buildpackage failed (see output)") + + l=glob("%(TEMP)s/%(name)s*.deb" % packageContents) + if len(l) != 1: + raise Py2debException("didn't find builded deb") + + tdeb = l[0] + deb = os.path.basename(tdeb) + shutil.move(tdeb, deb) + + generatedFiles = [deb, ] + + if rpm: + rpmFilename = deb2rpm(deb) + generatedFiles.append(rpmFilename) + + if src: + tarFilename = py2src(TEMP, name) + generatedFiles.append(tarFilename) + + if tar: + tarFilename = py2tar(DEST, TEMP, name, version) + generatedFiles.append(tarFilename) + + if dsc: + dscFilename = py2dsc(TEMP, name, version, depends, author, mail, arch) + generatedFiles.append(dscFilename) + + if changes: + changesFilenames = py2changes(packageContents) + generatedFiles.extend(changesFilenames) + + return generatedFiles + + #~ except Exception,m: + #~ raise Py2debException("build error :"+str(m)) + + finally: + if Py2deb.clear: + shutil.rmtree(TEMP) + + +if __name__ == "__main__": + try: + os.chdir(os.path.dirname(sys.argv[0])) + except: + pass + + p=Py2deb("python-py2deb") + p.description="Generate simple deb(/rpm/tgz) from python (2.4, 2.5 and 2.6)" + p.url = "http://www.manatlan.com/page/py2deb" + p.author=__author__ + p.mail=__mail__ + p.depends = "dpkg-dev, fakeroot, alien, python" + p.section="python" + p["/usr/lib/python2.6/dist-packages"] = ["py2deb.py", ] + p["/usr/lib/python2.5/site-packages"] = ["py2deb.py", ] + p["/usr/lib/python2.4/site-packages"] = ["py2deb.py", ] + #~ p.postinstall = "s.py" + #~ p.preinstall = "s.py" + #~ p.postremove = "s.py" + #~ p.preremove = "s.py" + print p + print p.generate(__version__, changelog = __doc__, src=True) diff --git a/support/pylint.rc b/support/pylint.rc new file mode 100644 index 0000000..2a371a1 --- /dev/null +++ b/support/pylint.rc @@ -0,0 +1,305 @@ +# lint Python modules using external checkers. +# +# This is the main checker controling the other ones and the reports +# generation. It is itself both a raw checker and an astng checker in order +# to: +# * handle message activation / deactivation at the module level +# * handle some basic but necessary stats'data (number of classes, methods...) +# +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# Set the cache size for astng objects. +cache-size=500 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable only checker(s) with the given id(s). This option conflicts with the +# disable-checker option +#enable-checker= + +# Enable all checker(s) except those with the given id(s). This option +# conflicts with the enable-checker option +#disable-checker= + +# Enable all messages in the listed categories. +#enable-msg-cat= + +# Disable all messages in the listed categories. +#disable-msg-cat= + +# Enable the message(s) with the given id(s). +#enable-msg= + +# Disable the message(s) with the given id(s). +disable-msg=W0403,W0612,W0613,C0103,C0111,C0301,R0903,W0142,W0603,R0904,R0921,R0201 + +[REPORTS] + +# set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=colorized + +# Include message's id in output +include-ids=yes + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells wether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note).You have access to the variables errors warning, statement which +# respectivly contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (R0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (R0004). +comment=no + +# Enable the report(s) with the given id(s). +#enable-report= + +# Disable the report(s) with the given id(s). +#disable-report= + + +# checks for +# * unused variables / imports +# * undefined variables +# * redefinition of variable from builtins or from an outer scope +# * use of variable before assigment +# +[VARIABLES] + +# Tells wether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching names used for dummy variables (i.e. not used). +dummy-variables-rgx=_|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +# checks for : +# * doc strings +# * modules / classes / functions / methods / arguments / variables name +# * number of arguments, local variables, branchs, returns and statements in +# functions, methods +# * required module attributes +# * dangerous default values as arguments +# * redefinition of function / method / class +# * uses of the global statement +# +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=__.*__ + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-zA-Z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + + +# try to find bugs in the code using type inference +# +[TYPECHECK] + +# Tells wether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# When zope mode is activated, consider the acquired-members option to ignore +# access to some undefined attributes. +zope=no + +# List of members which are usually get through zope's acquisition mecanism and +# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). +acquired-members=REQUEST,acl_users,aq_parent + + +# checks for sign of poor/misdesign: +# * number of methods, attributes, local variables... +# * size, complexity of functions, methods +# +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=15 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=1 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +# checks for : +# * methods without self as first argument +# * overridden methods signature +# * access only to existant members via self +# * attributes not defined in the __init__ method +# * supported interfaces implementation +# * unreachable code +# +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + + +# checks for +# * external modules dependencies +# * relative / wildcard imports +# * cyclic imports +# * uses of deprecated modules +# +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report R0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report R0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report R0402 must +# not be disabled) +int-import-graph= + + +# checks for similarities and duplicated code. This computation may be +# memory / CPU intensive, so you should disable it if you experiments some +# problems. +# +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + + +# checks for: +# * warning notes in the code like FIXME, XXX +# * PEP 263: source code with non ascii character but no encoding declaration +# +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +# checks for : +# * unauthorized constructions +# * strict indentation +# * line length +# * use of <> instead of != +# +[FORMAT] + +# Maximum number of characters on a single line. +# @note Limiting this to the most extreme cases +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string='\t' diff --git a/support/test_syntax.py b/support/test_syntax.py new file mode 100755 index 0000000..65a373c --- /dev/null +++ b/support/test_syntax.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import commands + + +verbose = False + + +def syntax_test(file): + commandTemplate = """ + python -t -t -W all -c "import py_compile; py_compile.compile ('%(filename)s', doraise=False)" """ + compileCommand = commandTemplate % {"filename": file} + (status, text) = commands.getstatusoutput (compileCommand) + text = text.rstrip() + passed = len(text) == 0 + + if passed: + output = ("Syntax is correct for "+file) if verbose else "" + else: + output = ("Syntax is invalid for %s\n" % file) if verbose else "" + output += text + return (passed, output) + + +if __name__ == "__main__": + import sys + import os + import optparse + + opar = optparse.OptionParser() + opar.add_option("-v", "--verbose", dest="verbose", help="Toggle verbosity", action="store_true", default=False) + options, args = opar.parse_args(sys.argv[1:]) + verbose = options.verbose + + completeOutput = [] + allPassed = True + for filename in args: + passed, output = syntax_test(filename) + if not passed: + allPassed = False + if output.strip(): + completeOutput.append(output) + print "\n".join(completeOutput) + + sys.exit(0 if allPassed else 1); diff --git a/support/todo.py b/support/todo.py new file mode 100755 index 0000000..90cbd04 --- /dev/null +++ b/support/todo.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +from __future__ import with_statement +import itertools + + +verbose = False + + +def tag_parser(file, tag): + """ + >>> nothing = [] + >>> for todo in tag_parser(nothing, "@todo"): + ... print todo + ... + >>> one = ["@todo Help!"] + >>> for todo in tag_parser(one, "@todo"): + ... print todo + ... + 1: @todo Help! + >>> mixed = ["one", "@todo two", "three"] + >>> for todo in tag_parser(mixed, "@todo"): + ... print todo + ... + 2: @todo two + >>> embedded = ["one @todo two", "three"] + >>> for todo in tag_parser(embedded, "@todo"): + ... print todo + ... + 1: @todo two + >>> continuation = ["one", "@todo two", " three"] + >>> for todo in tag_parser(continuation, "@todo"): + ... print todo + ... + 2: @todo two three + >>> series = ["one", "@todo two", "@todo three"] + >>> for todo in tag_parser(series, "@todo"): + ... print todo + ... + 2: @todo two + 3: @todo three + """ + currentTodo = [] + prefix = None + for lineNumber, line in enumerate(file): + column = line.find(tag) + if column != -1: + if currentTodo: + yield "\n".join (currentTodo) + prefix = line[0:column] + currentTodo = ["%d: %s" % (lineNumber+1, line[column:].strip())] + elif prefix is not None and len(prefix)+1 < len(line) and line.startswith(prefix) and line[len(prefix)].isspace(): + currentTodo.append (line[len(prefix):].rstrip()) + elif currentTodo: + yield "\n".join (currentTodo) + currentTodo = [] + prefix = None + if currentTodo: + yield "\n".join (currentTodo) + + +def tag_finder(filename, tag): + todoList = [] + + with open(filename) as file: + body = "\n".join (tag_parser(file, tag)) + passed = not body + if passed: + output = "No %s's for %s" % (tag, filename) if verbose else "" + else: + header = "%s's for %s:\n" % (tag, filename) if verbose else "" + output = header + body + output += "\n" if verbose else "" + + return (passed, output) + + +if __name__ == "__main__": + import sys + import os + import optparse + + opar = optparse.OptionParser() + opar.add_option("-v", "--verbose", dest="verbose", help="Toggle verbosity", action="store_true", default=False) + options, args = opar.parse_args(sys.argv[1:]) + verbose = options.verbose + + bugsAsError = True + todosAsError = False + + completeOutput = [] + allPassed = True + for filename in args: + bugPassed, bugOutput = tag_finder(filename, "@bug") + todoPassed, todoOutput = tag_finder(filename, "@todo") + output = "\n".join ([bugOutput, todoOutput]) + if (not bugPassed and bugsAsError) or (not todoPassed and todosAsError): + allPassed = False + output = output.strip() + if output: + completeOutput.append(filename+":\n"+output+"\n\n") + print "\n".join(completeOutput) + + sys.exit(0 if allPassed else 1); diff --git a/www/download.html b/www/download.html new file mode 100644 index 0000000..c2499f2 --- /dev/null +++ b/www/download.html @@ -0,0 +1,37 @@ + + + Multilist + + +

+ Multilist +

+

Simple checklist program

+

[About] [Download] +

Download

+ +

Packages

+

Maemo 5 and Later: Go to the application installer, enable Extras, and download Dialcentral (Note: technically its still in extras-testing because people haven't been reviewing it)

+ +

Maemo 4.1 and Earlier: Add the Extras Repository and check the App Manager for DialCentral

+ +

Linux: .deb files

+ +

Development

+

Source

+

For the most up to date version check out svn. +

+

Requires

+
    +
  • PyGTK / Glade
  • +
  • Python bindings for hildon, and osso
  • +
+ +

Bugs

+ +

Discuss your issue on Maemo.Org Talk

+ +

File a bug report against Extras->ejpi

+

View existing bug reports

+ + diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..2c1bdd1 --- /dev/null +++ b/www/index.html @@ -0,0 +1,33 @@ + + + Multilist + + +

+ Multilist +

+

Simple checklist program

+

[About] [Download] + +

Documentation

+ +

About

+

+ +

Multilist has been tested on Ubuntu 9.05, Maemo 5, and Maemo 4.1

+ +

See also Multilist's old webpage

+ +

Quicknote is Free Software and available under the GPLv2.1. + +

Features

+

Multilist 0.3.1 (See t.m.o Thread)

+ +
    +
  • +
+ +

Sample Screenshots

+ + + diff --git a/www/multilist.deb b/www/multilist.deb new file mode 100644 index 0000000000000000000000000000000000000000..a9a6d0d646ddce5046729755a7ad99ac93d0965d GIT binary patch literal 33182 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